Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 12 additions & 30 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2169,7 +2169,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
break
} else {
const modelInfo = this.api.getModel().info
const state = await this.providerRef.deref()?.getState()
const toolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo)
nextUserContent = [{ type: "text", text: formatResponse.noToolsUsed(toolProtocol) }]
this.consecutiveMistakeCount++
Expand Down Expand Up @@ -2960,11 +2959,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Apply exponential backoff similar to first-chunk errors when auto-resubmit is enabled
const stateForBackoff = await this.providerRef.deref()?.getState()
if (stateForBackoff?.autoApprovalEnabled) {
await this.backoffAndAnnounce(
currentItem.retryAttempt ?? 0,
error,
streamingFailedMessage,
)
await this.backoffAndAnnounce(currentItem.retryAttempt ?? 0, error)

// Check if task was aborted during the backoff
if (this.abort) {
Expand Down Expand Up @@ -3066,10 +3061,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Need to save assistant responses to file before proceeding to
// tool use since user can exit at any moment and we wouldn't be
// able to save the assistant's response.
let didEndLoop = false

// Check if we have any content to process (text or tool uses)
const hasTextContent = assistantMessage.length > 0

const hasToolUses = this.assistantMessageContent.some(
(block) => block.type === "tool_use" || block.type === "mcp_tool_use",
)
Expand Down Expand Up @@ -3139,10 +3134,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}

await this.addToApiConversationHistory(
{
role: "assistant",
content: assistantContent,
},
{ role: "assistant", content: assistantContent },
reasoningMessage || undefined,
)

Expand Down Expand Up @@ -3191,7 +3183,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Add periodic yielding to prevent blocking
await new Promise((resolve) => setImmediate(resolve))
}
// Continue to next iteration instead of setting didEndLoop from recursive call

continue
} else {
// If there's no assistant_responses, that means we got no text
Expand All @@ -3218,13 +3210,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Reuse the state variable from above
if (state?.autoApprovalEnabled) {
// Auto-retry with backoff - don't persist failure message when retrying
const errorMsg =
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output."

await this.backoffAndAnnounce(
currentItem.retryAttempt ?? 0,
new Error("Empty assistant response"),
errorMsg,
new Error(
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output.",
),
)

// Check if task was aborted during the backoff
Expand Down Expand Up @@ -3811,18 +3801,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {

// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
if (autoApprovalEnabled) {
let errorMsg

if (error.error?.metadata?.raw) {
errorMsg = JSON.stringify(error.error.metadata.raw, null, 2)
} else if (error.message) {
errorMsg = error.message
} else {
errorMsg = "Unknown error"
}

// Apply shared exponential backoff and countdown UX
await this.backoffAndAnnounce(retryAttempt, error, errorMsg)
await this.backoffAndAnnounce(retryAttempt, error)

// CRITICAL: Check if task was aborted during the backoff countdown
// This prevents infinite loops when users cancel during auto-retry
Expand Down Expand Up @@ -3870,7 +3850,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}

// Shared exponential backoff for retries (first-chunk and mid-stream)
private async backoffAndAnnounce(retryAttempt: number, error: any, header?: string): Promise<void> {
private async backoffAndAnnounce(retryAttempt: number, error: any): Promise<void> {
try {
const state = await this.providerRef.deref()?.getState()
const baseDelay = state?.requestDelaySeconds || 5
Expand Down Expand Up @@ -3900,7 +3880,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}

const finalDelay = Math.max(exponentialDelay, rateLimitDelay)
if (finalDelay <= 0) return
if (finalDelay <= 0) {
return
}

// Build header text; fall back to error message if none provided
let headerText
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,7 @@ export const ChatRowContent = ({
? "https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
: undefined
}
errorDetails={apiReqStreamingFailedMessage}
/>
)}
</>
Expand Down
Loading