Skip to content
Closed
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
79 changes: 79 additions & 0 deletions src/api/providers/vscode-lm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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")
)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: The isContextWindowError() method uses simple string pattern matching which may not catch all VS Code LM API context window errors. Consider checking if the VS Code LM API provides specific error codes or types for context window errors that could be used for more reliable detection.

The current implementation relies on message text patterns which could produce false positives if error messages change or contain these keywords in different contexts.

}

/**
* 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[],
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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}`)
}
Expand Down
102 changes: 71 additions & 31 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? {}

Expand All @@ -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) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: The fallback truncation strategy keeps only the first message and last 2 messages. Consider if this is sufficient for maintaining conversation coherence. The first message might be the initial task description, but keeping only 2 recent messages might lose important context.

Suggestion: Document why this specific strategy (first + last 2) was chosen, or consider keeping a few more messages (e.g., first + last 5) to maintain better context.

// 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 },
)
}
}
}

Expand Down Expand Up @@ -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
Expand Down
Loading
Loading