mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
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:
parent
3a47c55a2e
commit
6deba15f9f
3 changed files with 243 additions and 77 deletions
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue