diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts index d8a492f772c..5a3158448d2 100644 --- a/src/api/providers/vscode-lm.ts +++ b/src/api/providers/vscode-lm.ts @@ -44,6 +44,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan private client: vscode.LanguageModelChat | null private disposable: vscode.Disposable | null private currentRequestCancellation: vscode.CancellationTokenSource | null + private isRecovering: boolean = false constructor(options: ApiHandlerOptions) { super() @@ -330,6 +331,63 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan return content } + /** + * Check if an error is a context window error + */ + private isContextWindowError(error: unknown): boolean { + if (!error) return false + + const errorMessage = error instanceof Error ? error.message : String(error) + const lowerMessage = errorMessage.toLowerCase() + + // Check for common context window error patterns + return ( + (lowerMessage.includes("context") && + (lowerMessage.includes("window") || + lowerMessage.includes("length") || + lowerMessage.includes("limit"))) || + (lowerMessage.includes("token") && lowerMessage.includes("limit")) || + (lowerMessage.includes("maximum") && lowerMessage.includes("tokens")) || + lowerMessage.includes("too many tokens") || + lowerMessage.includes("exceeds") + ) + } + + /** + * Handle context window errors with recovery mechanism + */ + private async handleContextWindowError(error: unknown): Promise { + if (this.isRecovering) { + console.warn("Roo Code : Already recovering from context window error") + return + } + + this.isRecovering = true + + try { + console.warn("Roo Code : Context window error detected, attempting recovery") + + // Clean up current state + this.ensureCleanState() + + // Reset the client to force re-initialization + this.client = null + + // Wait a bit before retrying + await new Promise((resolve) => setTimeout(resolve, 1000)) + + // Re-initialize the client + await this.initializeClient() + + console.log("Roo Code : Recovery from context window error successful") + } catch (recoveryError) { + console.error("Roo Code : Failed to recover from context window error:", recoveryError) + throw recoveryError + } finally { + this.isRecovering = false + } + } + override async *createMessage( systemPrompt: string, messages: Anthropic.Messages.MessageParam[], @@ -452,6 +510,21 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan } catch (error: unknown) { this.ensureCleanState() + // Check if this is a context window error + if (this.isContextWindowError(error)) { + // Handle context window error with recovery + await this.handleContextWindowError(error) + + // Create a specific error for context window issues + const contextError = new Error( + "Context window exceeded. The conversation is too long for the current model. " + + "Please try condensing the context or starting a new conversation.", + ) + // Add a flag to indicate this is a context window error + ;(contextError as any).isContextWindowError = true + throw contextError + } + if (error instanceof vscode.CancellationError) { throw new Error("Roo Code : Request cancelled by user") } @@ -552,6 +625,12 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan } return result } catch (error) { + // Check if this is a context window error + if (this.isContextWindowError(error)) { + await this.handleContextWindowError(error) + throw new Error("Context window exceeded. Please reduce the prompt size and try again.") + } + if (error instanceof Error) { throw new Error(`VSCode LM completion error: ${error.message}`) } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 851df91e6c5..09204f4f1a0 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -2456,7 +2456,7 @@ export class Task extends EventEmitter implements TaskLike { ) } - private async handleContextWindowExceededError(): Promise { + public async handleContextWindowExceededError(): Promise { const state = await this.providerRef.deref()?.getState() const { profileThresholds = {} } = state ?? {} @@ -2481,38 +2481,62 @@ export class Task extends EventEmitter implements TaskLike { `Forcing truncation to ${FORCED_CONTEXT_REDUCTION_PERCENT}% of current context.`, ) - // Force aggressive truncation by keeping only 75% of the conversation history - const truncateResult = await truncateConversationIfNeeded({ - messages: this.apiConversationHistory, - totalTokens: contextTokens || 0, - maxTokens, - contextWindow, - apiHandler: this.api, - autoCondenseContext: true, - autoCondenseContextPercent: FORCED_CONTEXT_REDUCTION_PERCENT, - systemPrompt: await this.getSystemPrompt(), - taskId: this.taskId, - profileThresholds, - currentProfileId, - }) + try { + // Force aggressive truncation by keeping only 75% of the conversation history + const truncateResult = await truncateConversationIfNeeded({ + messages: this.apiConversationHistory, + totalTokens: contextTokens || 0, + maxTokens, + contextWindow, + apiHandler: this.api, + autoCondenseContext: true, + autoCondenseContextPercent: FORCED_CONTEXT_REDUCTION_PERCENT, + systemPrompt: await this.getSystemPrompt(), + taskId: this.taskId, + profileThresholds, + currentProfileId, + }) - if (truncateResult.messages !== this.apiConversationHistory) { - await this.overwriteApiConversationHistory(truncateResult.messages) - } + if (truncateResult.messages !== this.apiConversationHistory) { + await this.overwriteApiConversationHistory(truncateResult.messages) + } - if (truncateResult.summary) { - const { summary, cost, prevContextTokens, newContextTokens = 0 } = truncateResult - const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens } - await this.say( - "condense_context", - undefined /* text */, - undefined /* images */, - false /* partial */, - undefined /* checkpoint */, - undefined /* progressStatus */, - { isNonInteractive: true } /* options */, - contextCondense, - ) + if (truncateResult.summary) { + const { summary, cost, prevContextTokens, newContextTokens = 0 } = truncateResult + const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens } + await this.say( + "condense_context", + undefined /* text */, + undefined /* images */, + false /* partial */, + undefined /* checkpoint */, + undefined /* progressStatus */, + { isNonInteractive: true } /* options */, + contextCondense, + ) + } + } catch (error) { + // If truncation fails, log the error but don't throw to prevent UI from freezing + console.error(`[Task#${this.taskId}] Failed to handle context window error:`, error) + + // Try a more aggressive truncation as a last resort + if (this.apiConversationHistory.length > 2) { + const fallbackMessages = [ + this.apiConversationHistory[0], // Keep first message + ...this.apiConversationHistory.slice(-2), // Keep last 2 messages + ] + await this.overwriteApiConversationHistory(fallbackMessages) + + await this.say( + "error", + "Context window exceeded. Conversation history has been significantly reduced to continue.", + undefined, + false, + undefined, + undefined, + { isNonInteractive: true }, + ) + } } } @@ -2715,7 +2739,23 @@ export class Task extends EventEmitter implements TaskLike { `Retry attempt ${retryAttempt + 1}/${MAX_CONTEXT_WINDOW_RETRIES}. ` + `Attempting automatic truncation...`, ) + + // Notify UI that we're handling a context window error to prevent grey screen + await this.say( + "text", + "⚠️ Context window limit reached. Automatically reducing conversation size to continue...", + undefined, + false, + undefined, + undefined, + { isNonInteractive: true }, + ) + await this.handleContextWindowExceededError() + + // Give UI time to update before retrying + await delay(500) + // Retry the request after handling the context window error yield* this.attemptApiRequest(retryAttempt + 1) return diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 91b86879668..f8fac7fa16b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1536,8 +1536,42 @@ export class ClineProvider if (!task) { throw new Error(`Task with id ${taskId} not found in stack`) } - await task.condenseContext() - await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId }) + + try { + await task.condenseContext() + await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId }) + } catch (error) { + this.log(`[condenseTaskContext] Error condensing context: ${error}`) + + // If it's a context window error, try to recover + if (error instanceof Error && error.message.includes("context")) { + // Notify user about the issue + vscode.window.showWarningMessage("Context window limit reached. Attempting recovery...") + + // Try to recover by clearing some history + try { + // Force a more aggressive context reduction + await task.handleContextWindowExceededError() + + // Retry condensing with reduced context + await task.condenseContext() + await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId }) + + vscode.window.showInformationMessage("Context successfully reduced. You can continue working.") + } catch (recoveryError) { + this.log(`[condenseTaskContext] Recovery failed: ${recoveryError}`) + vscode.window.showErrorMessage( + "Failed to recover from context window error. Please start a new conversation.", + ) + throw recoveryError + } + } else { + vscode.window.showErrorMessage( + "Failed to condense context. Please try again or start a new conversation.", + ) + throw error + } + } } // this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder @@ -2583,62 +2617,75 @@ export class ClineProvider console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`) - const { historyItem, uiMessagesFilePath } = await this.getTaskWithId(task.taskId) - - // Preserve parent and root task information for history item. - const rootTask = task.rootTask - const parentTask = task.parentTask - - // Mark this as a user-initiated cancellation so provider-only rehydration can occur - task.abortReason = "user_cancelled" + try { + const { historyItem, uiMessagesFilePath } = await this.getTaskWithId(task.taskId) + + // Preserve parent and root task information for history item. + const rootTask = task.rootTask + const parentTask = task.parentTask + + // Mark this as a user-initiated cancellation so provider-only rehydration can occur + task.abortReason = "user_cancelled" + + // Capture the current instance to detect if rehydrate already occurred elsewhere + const originalInstanceId = task.instanceId + + // Begin abort (non-blocking) + task.abortTask() + + // Immediately mark the original instance as abandoned to prevent any residual activity + task.abandoned = true + + await pWaitFor( + () => + this.getCurrentTask()! === undefined || + this.getCurrentTask()!.isStreaming === false || + this.getCurrentTask()!.didFinishAbortingStream || + // If only the first chunk is processed, then there's no + // need to wait for graceful abort (closes edits, browser, + // etc). + this.getCurrentTask()!.isWaitingForFirstChunk, + { + timeout: 3_000, + }, + ).catch(() => { + console.error("Failed to abort task") + }) - // Capture the current instance to detect if rehydrate already occurred elsewhere - const originalInstanceId = task.instanceId + // Defensive safeguard: if current instance already changed, skip rehydrate + const current = this.getCurrentTask() + if (current && current.instanceId !== originalInstanceId) { + this.log( + `[cancelTask] Skipping rehydrate: current instance ${current.instanceId} != original ${originalInstanceId}`, + ) + return + } - // Begin abort (non-blocking) - task.abortTask() + // Final race check before rehydrate to avoid duplicate rehydration + { + const currentAfterCheck = this.getCurrentTask() + if (currentAfterCheck && currentAfterCheck.instanceId !== originalInstanceId) { + this.log( + `[cancelTask] Skipping rehydrate after final check: current instance ${currentAfterCheck.instanceId} != original ${originalInstanceId}`, + ) + return + } + } - // Immediately mark the original instance as abandoned to prevent any residual activity - task.abandoned = true + // Clears task again, so we need to abortTask manually above. + await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask }) + } catch (error) { + // If cancellation fails, ensure UI doesn't freeze + this.log(`[cancelTask] Error during task cancellation: ${error}`) - await pWaitFor( - () => - this.getCurrentTask()! === undefined || - this.getCurrentTask()!.isStreaming === false || - this.getCurrentTask()!.didFinishAbortingStream || - // If only the first chunk is processed, then there's no - // need to wait for graceful abort (closes edits, browser, - // etc). - this.getCurrentTask()!.isWaitingForFirstChunk, - { - timeout: 3_000, - }, - ).catch(() => { - console.error("Failed to abort task") - }) + // Try to clear the task to prevent UI from being stuck + await this.clearTask() - // Defensive safeguard: if current instance already changed, skip rehydrate - const current = this.getCurrentTask() - if (current && current.instanceId !== originalInstanceId) { - this.log( - `[cancelTask] Skipping rehydrate: current instance ${current.instanceId} != original ${originalInstanceId}`, + // Notify the user + vscode.window.showErrorMessage( + "Failed to cancel task properly. The task has been cleared. You can start a new task.", ) - return } - - // Final race check before rehydrate to avoid duplicate rehydration - { - const currentAfterCheck = this.getCurrentTask() - if (currentAfterCheck && currentAfterCheck.instanceId !== originalInstanceId) { - this.log( - `[cancelTask] Skipping rehydrate after final check: current instance ${currentAfterCheck.instanceId} != original ${originalInstanceId}`, - ) - return - } - } - - // Clears task again, so we need to abortTask manually above. - await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask }) } // Clear the current task without treating it as a subtask.