fix: handle context window exceeded errors during context condensing

When the selected condensing model has a smaller context window than the
current conversation, the API throws an error that was previously unhandled.
This caused the UI to hang with "Condensing Chat" forever.

Changes:
- Add try-catch around the streaming API call in summarizeConversation()
- Detect context window exceeded errors and return user-friendly error message
- Add new translation key for context window exceeded error
- Add tests for error handling behavior

Fixes #10781
This commit is contained in:
Roo Code 2026-01-16 17:46:26 +00:00
parent 9bf7173725
commit 3c3ef359bd
3 changed files with 152 additions and 9 deletions

View file

@ -1484,6 +1484,122 @@ describe("summarizeConversation", () => {
expect(result.error).toBeUndefined()
})
it("should return context window exceeded error when API throws context length error", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
{ role: "assistant", content: "Hi there", ts: 2 },
{ role: "user", content: "How are you?", ts: 3 },
{ role: "assistant", content: "I'm good", ts: 4 },
{ role: "user", content: "What's new?", ts: 5 },
{ role: "assistant", content: "Not much", ts: 6 },
{ role: "user", content: "Tell me more", ts: 7 },
]
// Create a stream that throws a context length exceeded error
// eslint-disable-next-line require-yield
const errorStream = (async function* () {
throw new Error(
"400 This endpoint's maximum context length is 131072 tokens. However, you requested about 145406 tokens.",
)
})()
mockApiHandler.createMessage = vi.fn().mockReturnValue(errorStream) as any
const result = await summarizeConversation(
messages,
mockApiHandler,
defaultSystemPrompt,
taskId,
DEFAULT_PREV_CONTEXT_TOKENS,
)
// Should return original messages with error
expect(result.messages).toEqual(messages)
expect(result.summary).toBe("")
expect(result.error).toBeTruthy()
// The error message comes from the translation key
expect(result.error).toContain("condense_context_window_exceeded")
expect(result.newContextTokens).toBeUndefined()
})
it("should return context window exceeded error for various context length error messages", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
{ role: "assistant", content: "Hi there", ts: 2 },
{ role: "user", content: "How are you?", ts: 3 },
{ role: "assistant", content: "I'm good", ts: 4 },
{ role: "user", content: "What's new?", ts: 5 },
{ role: "assistant", content: "Not much", ts: 6 },
{ role: "user", content: "Tell me more", ts: 7 },
]
// Test various error message patterns
const errorPatterns = [
"context_length_exceeded",
"maximum context length exceeded",
"prompt is too long",
"too many tokens in your request",
]
for (const errorPattern of errorPatterns) {
vi.clearAllMocks()
// eslint-disable-next-line require-yield
const errorStream = (async function* () {
throw new Error(errorPattern)
})()
mockApiHandler.createMessage = vi.fn().mockReturnValue(errorStream) as any
const result = await summarizeConversation(
messages,
mockApiHandler,
defaultSystemPrompt,
taskId,
DEFAULT_PREV_CONTEXT_TOKENS,
)
expect(result.error).toBeTruthy()
// The error message comes from the translation key
expect(result.error).toContain("condense_context_window_exceeded")
}
})
it("should return generic error for non-context-window API errors", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
{ role: "assistant", content: "Hi there", ts: 2 },
{ role: "user", content: "How are you?", ts: 3 },
{ role: "assistant", content: "I'm good", ts: 4 },
{ role: "user", content: "What's new?", ts: 5 },
{ role: "assistant", content: "Not much", ts: 6 },
{ role: "user", content: "Tell me more", ts: 7 },
]
// Create a stream that throws a generic API error
// eslint-disable-next-line require-yield
const errorStream = (async function* () {
throw new Error("Internal server error")
})()
mockApiHandler.createMessage = vi.fn().mockReturnValue(errorStream) as any
const result = await summarizeConversation(
messages,
mockApiHandler,
defaultSystemPrompt,
taskId,
DEFAULT_PREV_CONTEXT_TOKENS,
)
// Should return original messages with error
expect(result.messages).toEqual(messages)
expect(result.summary).toBe("")
expect(result.error).toBeTruthy()
expect(result.error).toContain("Internal server error")
expect(result.newContextTokens).toBeUndefined()
})
})
describe("summarizeConversation with custom settings", () => {

View file

@ -319,19 +319,45 @@ export async function summarizeConversation(
}
}
const stream = handlerToUse.createMessage(promptToUse, requestMessages)
let summary = ""
let cost = 0
let outputTokens = 0
for await (const chunk of stream) {
if (chunk.type === "text") {
summary += chunk.text
} else if (chunk.type === "usage") {
// Record final usage chunk only
cost = chunk.totalCost ?? 0
outputTokens = chunk.outputTokens ?? 0
try {
const stream = handlerToUse.createMessage(promptToUse, requestMessages)
for await (const chunk of stream) {
if (chunk.type === "text") {
summary += chunk.text
} else if (chunk.type === "usage") {
// Record final usage chunk only
cost = chunk.totalCost ?? 0
outputTokens = chunk.outputTokens ?? 0
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
// Check for context window exceeded error
if (
errorMessage.includes("context_length_exceeded") ||
errorMessage.includes("context length") ||
errorMessage.includes("maximum context") ||
errorMessage.includes("too long") ||
errorMessage.includes("too many tokens")
) {
return {
...response,
cost,
error: t("common:errors.condense_context_window_exceeded"),
}
}
// Return generic error for other failures
return {
...response,
cost,
error: `${t("common:errors.condense_failed")}: ${errorMessage}`,
}
}

View file

@ -63,6 +63,7 @@
"condensed_recently": "Context was condensed recently; skipping this attempt",
"condense_handler_invalid": "API handler for condensing context is invalid",
"condense_context_grew": "Context size increased during condensing; skipping this attempt",
"condense_context_window_exceeded": "The selected condensing model's context window is too small for the current conversation. Please select a model with a larger context window or manually reduce the conversation size first.",
"url_timeout": "The website took too long to load (timeout). This could be due to a slow connection, heavy website, or the site being temporarily unavailable. You can try again later or check if the URL is correct.",
"url_not_found": "The website address could not be found. Please check if the URL is correct and try again.",
"no_internet": "No internet connection. Please check your network connection and try again.",