fix: prevent grey screen when VS Code LM API hits context window limits

- Added error recovery mechanism in vscode-lm.ts to detect and handle context window errors
- Implemented client reset on context window errors to recover from stuck states
- Enhanced handleContextWindowExceededError in Task.ts with try-catch and fallback truncation
- Added non-blocking error handling to prevent UI freeze during context operations
- Improved error handling in ClineProvider for context condensing operations
- Made handleContextWindowExceededError public to allow recovery from ClineProvider

Fixes #8594
This commit is contained in:
Roo Code 2025-10-10 11:20:36 +00:00
parent 3a47c55a2e
commit 6deba15f9f
3 changed files with 243 additions and 77 deletions

View file

@ -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<void> {
if (this.isRecovering) {
console.warn("Roo Code <Language Model API>: Already recovering from context window error")
return
}
this.isRecovering = true
try {
console.warn("Roo Code <Language Model API>: 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 <Language Model API>: Recovery from context window error successful")
} catch (recoveryError) {
console.error("Roo Code <Language Model API>: 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 <Language Model API>: 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}`)
}

View file

@ -2456,7 +2456,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
)
}
private async handleContextWindowExceededError(): Promise<void> {
public async handleContextWindowExceededError(): Promise<void> {
const state = await this.providerRef.deref()?.getState()
const { profileThresholds = {} } = state ?? {}
@ -2481,38 +2481,62 @@ export class Task extends EventEmitter<TaskEvents> 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<TaskEvents> 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

View file

@ -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)
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
// 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"
// 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
// Capture the current instance to detect if rehydrate already occurred elsewhere
const originalInstanceId = task.instanceId
// Begin abort (non-blocking)
task.abortTask()
// Begin abort (non-blocking)
task.abortTask()
// Immediately mark the original instance as abandoned to prevent any residual activity
task.abandoned = true
// 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")
})
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")
})
// 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
}
// Final race check before rehydrate to avoid duplicate rehydration
{
const currentAfterCheck = this.getCurrentTask()
if (currentAfterCheck && currentAfterCheck.instanceId !== originalInstanceId) {
// Defensive safeguard: if current instance already changed, skip rehydrate
const current = this.getCurrentTask()
if (current && current.instanceId !== originalInstanceId) {
this.log(
`[cancelTask] Skipping rehydrate after final check: current instance ${currentAfterCheck.instanceId} != original ${originalInstanceId}`,
`[cancelTask] Skipping rehydrate: current instance ${current.instanceId} != original ${originalInstanceId}`,
)
return
}
}
// Clears task again, so we need to abortTask manually above.
await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask })
// 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 })
} catch (error) {
// If cancellation fails, ensure UI doesn't freeze
this.log(`[cancelTask] Error during task cancellation: ${error}`)
// Try to clear the task to prevent UI from being stuck
await this.clearTask()
// Notify the user
vscode.window.showErrorMessage(
"Failed to cancel task properly. The task has been cleared. You can start a new task.",
)
}
}
// Clear the current task without treating it as a subtask.