From 08fff5ae7a5e368485e439539580e3c8e172fc79 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Fri, 23 Jan 2026 13:06:28 -0700 Subject: [PATCH 1/3] feat: remove context grew check and add environment details to condensed summaries - Remove the 'context grew during condensing' error check entirely - Fresh start benefit of condensation matters more than raw token count - Add environmentDetails parameter to summarizeConversation() - Include environment details as separate text block in summary content - Update all callers in Task.ts (condenseContext, handleContextWindowExceededError, attemptApiRequest) - Remove condense_context_grew translation key from 18 locale files - Update tests to expect new parameter --- src/core/condense/__tests__/condense.spec.ts | 14 +- src/core/condense/__tests__/index.spec.ts | 147 ++---------------- src/core/condense/index.ts | 17 +- .../__tests__/context-management.spec.ts | 8 +- src/core/context-management/index.ts | 5 +- src/core/task/Task.ts | 13 +- src/i18n/locales/ca/common.json | 1 - src/i18n/locales/de/common.json | 1 - src/i18n/locales/en/common.json | 1 - src/i18n/locales/es/common.json | 1 - src/i18n/locales/fr/common.json | 1 - src/i18n/locales/hi/common.json | 1 - src/i18n/locales/id/common.json | 1 - src/i18n/locales/it/common.json | 1 - src/i18n/locales/ja/common.json | 1 - src/i18n/locales/ko/common.json | 1 - src/i18n/locales/nl/common.json | 1 - src/i18n/locales/pl/common.json | 1 - src/i18n/locales/pt-BR/common.json | 1 - src/i18n/locales/ru/common.json | 1 - src/i18n/locales/tr/common.json | 1 - src/i18n/locales/vi/common.json | 1 - src/i18n/locales/zh-CN/common.json | 1 - src/i18n/locales/zh-TW/common.json | 1 - 24 files changed, 54 insertions(+), 168 deletions(-) diff --git a/src/core/condense/__tests__/condense.spec.ts b/src/core/condense/__tests__/condense.spec.ts index 52175448174..dcb05cd74df 100644 --- a/src/core/condense/__tests__/condense.spec.ts +++ b/src/core/condense/__tests__/condense.spec.ts @@ -136,7 +136,7 @@ Line 2 { role: "user", content: "Ninth message" }, ] - const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, false) // Verify we have a summary message with role "user" (fresh start model) const summaryMessage = result.messages.find((msg) => msg.isSummary) @@ -164,7 +164,7 @@ Line 2 { role: "user", content: "Fifth message" }, ] - const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, false) // All original messages should be tagged with condenseParent const taggedMessages = result.messages.filter((msg) => !msg.isSummary) @@ -193,7 +193,7 @@ Line 2 { role: "user", content: "Ninth message" }, ] - const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, false) const summaryMessage = result.messages.find((msg) => msg.isSummary) expect(summaryMessage).toBeTruthy() @@ -227,7 +227,7 @@ Line 2 { role: "user", content: "Perfect!" }, ] - const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, false) // Effective history should contain only the summary (fresh start) const effectiveHistory = getEffectiveApiHistory(result.messages) @@ -239,7 +239,7 @@ Line 2 it("should return error when not enough messages to summarize", async () => { const messages: ApiMessage[] = [{ role: "user", content: "Only one message" }] - const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, false) // Should return an error since we have only 1 message expect(result.error).toBeDefined() @@ -253,7 +253,7 @@ Line 2 { role: "assistant", content: "Previous summary", isSummary: true }, ] - const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, false) // Should return an error due to recent summary with no substantial messages after expect(result.error).toBeDefined() @@ -286,7 +286,7 @@ Line 2 { role: "user", content: "Seventh" }, ] - const result = await summarizeConversation(messages, emptyHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation(messages, emptyHandler, "System prompt", taskId, false) expect(result.error).toBeDefined() expect(result.messages).toEqual(messages) diff --git a/src/core/condense/__tests__/index.spec.ts b/src/core/condense/__tests__/index.spec.ts index 70c3e089ee2..3a13c1d63fd 100644 --- a/src/core/condense/__tests__/index.spec.ts +++ b/src/core/condense/__tests__/index.spec.ts @@ -30,7 +30,6 @@ vi.mock("@roo-code/telemetry", () => ({ })) const taskId = "test-task-id" -const DEFAULT_PREV_CONTEXT_TOKENS = 1000 describe("extractCommandBlocks", () => { it("should extract command blocks from string content", () => { @@ -713,13 +712,7 @@ describe("summarizeConversation", () => { it("should not summarize when there are not enough messages", async () => { const messages: ApiMessage[] = [{ role: "user", content: "Hello", ts: 1 }] - const result = await summarizeConversation( - messages, - mockApiHandler, - defaultSystemPrompt, - taskId, - DEFAULT_PREV_CONTEXT_TOKENS, - ) + const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId) expect(result.messages).toEqual(messages) expect(result.cost).toBe(0) expect(result.summary).toBe("") @@ -739,13 +732,7 @@ describe("summarizeConversation", () => { { role: "user", content: "Tell me more", ts: 7 }, ] - const result = await summarizeConversation( - messages, - mockApiHandler, - defaultSystemPrompt, - taskId, - DEFAULT_PREV_CONTEXT_TOKENS, - ) + const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId) // Check that the API was called correctly expect(mockApiHandler.createMessage).toHaveBeenCalled() @@ -798,13 +785,7 @@ describe("summarizeConversation", () => { { role: "user", content: "What's new?", ts: 5 }, ] - const result = await summarizeConversation( - messages, - mockApiHandler, - defaultSystemPrompt, - taskId, - DEFAULT_PREV_CONTEXT_TOKENS, - ) + const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId) const summaryMessage = result.messages.find((m) => m.isSummary) expect(summaryMessage).toBeDefined() @@ -827,13 +808,7 @@ describe("summarizeConversation", () => { { role: "user", content: "What's new?", ts: 5 }, ] - const result = await summarizeConversation( - messages, - mockApiHandler, - defaultSystemPrompt, - taskId, - DEFAULT_PREV_CONTEXT_TOKENS, - ) + const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId) const summaryMessage = result.messages.find((m) => m.isSummary) expect(summaryMessage).toBeDefined() @@ -870,13 +845,7 @@ describe("summarizeConversation", () => { return messages.map(({ role, content }: { role: string; content: any }) => ({ role, content })) }) - const result = await summarizeConversation( - messages, - mockApiHandler, - defaultSystemPrompt, - taskId, - DEFAULT_PREV_CONTEXT_TOKENS, - ) + const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId) // Should return original messages when summary is empty expect(result.messages).toEqual(messages) @@ -897,7 +866,7 @@ describe("summarizeConversation", () => { { role: "user", content: "Tell me more", ts: 7 }, ] - await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS) + await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId) // Verify that createMessage was called with the SUMMARY_PROMPT (which contains CRITICAL instructions), messages array, and optional metadata expect(mockApiHandler.createMessage).toHaveBeenCalledWith( @@ -929,7 +898,7 @@ describe("summarizeConversation", () => { { role: "user", content: "Newest", ts: 7 }, ] - await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS) + await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId) const mockCallArgs = (maybeRemoveImageBlocks as Mock).mock.calls[0][0] as any[] @@ -967,13 +936,7 @@ describe("summarizeConversation", () => { // Override the mock for this test mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithUsage) as any - const result = await summarizeConversation( - messages, - mockApiHandler, - systemPrompt, - taskId, - DEFAULT_PREV_CONTEXT_TOKENS, - ) + const result = await summarizeConversation(messages, mockApiHandler, systemPrompt, taskId) // Verify that countTokens was called with system prompt expect(mockApiHandler.countTokens).toHaveBeenCalled() @@ -985,47 +948,7 @@ describe("summarizeConversation", () => { expect(result.error).toBeUndefined() }) - it("should return error when new context tokens >= previous context tokens", 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 produces a summary - const streamWithLargeTokens = (async function* () { - yield { type: "text" as const, text: "This is a very long summary that uses many tokens" } - yield { type: "usage" as const, totalCost: 0.08, outputTokens: 500 } - })() - - // Override the mock for this test - mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithLargeTokens) as any - - // Mock countTokens to return a value >= prevContextTokens - mockApiHandler.countTokens = vi.fn().mockImplementation(() => Promise.resolve(600)) as any - - const prevContextTokens = 600 - const result = await summarizeConversation( - messages, - mockApiHandler, - defaultSystemPrompt, - taskId, - prevContextTokens, - ) - - // Should return original messages when context would not shrink - expect(result.messages).toEqual(messages) - expect(result.cost).toBe(0.08) - expect(result.summary).toBe("") - expect(result.error).toBeTruthy() // Error should be set - expect(result.newContextTokens).toBeUndefined() - }) - - it("should successfully summarize when new context tokens < previous context tokens", async () => { + it("should successfully summarize conversation", async () => { const messages: ApiMessage[] = [ { role: "user", content: "Hello", ts: 1 }, { role: "assistant", content: "Hi there", ts: 2 }, @@ -1045,17 +968,10 @@ describe("summarizeConversation", () => { // Override the mock for this test mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithSmallTokens) as any - // Mock countTokens to return a small value so total is < prevContextTokens + // Mock countTokens to return a small value mockApiHandler.countTokens = vi.fn().mockImplementation(() => Promise.resolve(30)) as any - const prevContextTokens = 200 - const result = await summarizeConversation( - messages, - mockApiHandler, - defaultSystemPrompt, - taskId, - prevContextTokens, - ) + const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId) // Result contains all messages plus summary expect(result.messages.length).toBe(messages.length + 1) @@ -1069,7 +985,6 @@ describe("summarizeConversation", () => { expect(result.summary).toBe("Concise summary") expect(result.error).toBeUndefined() expect(result.newContextTokens).toBe(80) // outputTokens(50) + countTokens(30) - expect(result.newContextTokens).toBeLessThan(prevContextTokens) }) it("should return error when API handler is invalid", async () => { @@ -1095,13 +1010,7 @@ describe("summarizeConversation", () => { const mockError = vi.fn() console.error = mockError - const result = await summarizeConversation( - messages, - invalidHandler, - defaultSystemPrompt, - taskId, - DEFAULT_PREV_CONTEXT_TOKENS, - ) + const result = await summarizeConversation(messages, invalidHandler, defaultSystemPrompt, taskId) // Should return original messages when handler is invalid expect(result.messages).toEqual(messages) @@ -1126,13 +1035,7 @@ describe("summarizeConversation", () => { { role: "user", content: "Thanks", ts: 5 }, ] - const result = await summarizeConversation( - messages, - mockApiHandler, - defaultSystemPrompt, - taskId, - DEFAULT_PREV_CONTEXT_TOKENS, - ) + const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId) const summaryMessage = result.messages.find((m) => m.isSummary) expect(summaryMessage).toBeDefined() @@ -1153,13 +1056,7 @@ describe("summarizeConversation", () => { { role: "user", content: "Thanks", ts: 5 }, ] - const result = await summarizeConversation( - messages, - mockApiHandler, - defaultSystemPrompt, - taskId, - DEFAULT_PREV_CONTEXT_TOKENS, - ) + const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId) // Summary should be the last message const lastMessage = result.messages[result.messages.length - 1] @@ -1228,7 +1125,6 @@ describe("summarizeConversation with custom settings", () => { mockMainApiHandler, defaultSystemPrompt, localTaskId, - DEFAULT_PREV_CONTEXT_TOKENS, false, customPrompt, ) @@ -1248,15 +1144,7 @@ describe("summarizeConversation with custom settings", () => { */ it("should use default systemPrompt when custom prompt is empty or not provided", async () => { // Test with empty string - await summarizeConversation( - sampleMessages, - mockMainApiHandler, - defaultSystemPrompt, - localTaskId, - DEFAULT_PREV_CONTEXT_TOKENS, - false, - " ", // Empty custom prompt - ) + await summarizeConversation(sampleMessages, mockMainApiHandler, defaultSystemPrompt, localTaskId, false, " ") // Verify the default SUMMARY_PROMPT was used (contains CRITICAL instructions) let createMessageCalls = (mockMainApiHandler.createMessage as Mock).mock.calls @@ -1273,9 +1161,8 @@ describe("summarizeConversation with custom settings", () => { mockMainApiHandler, defaultSystemPrompt, localTaskId, - DEFAULT_PREV_CONTEXT_TOKENS, false, - undefined, // No custom prompt + undefined, ) // Verify the default SUMMARY_PROMPT was used again (contains CRITICAL instructions) @@ -1296,7 +1183,6 @@ describe("summarizeConversation with custom settings", () => { mockMainApiHandler, defaultSystemPrompt, localTaskId, - DEFAULT_PREV_CONTEXT_TOKENS, false, "Custom prompt", ) @@ -1318,7 +1204,6 @@ describe("summarizeConversation with custom settings", () => { mockMainApiHandler, defaultSystemPrompt, localTaskId, - DEFAULT_PREV_CONTEXT_TOKENS, true, // isAutomaticTrigger "Custom prompt", ) diff --git a/src/core/condense/index.ts b/src/core/condense/index.ts index 9d76eae6557..51e766773be 100644 --- a/src/core/condense/index.ts +++ b/src/core/condense/index.ts @@ -131,15 +131,16 @@ export type SummarizeResponse = { * - Post-condense, the model sees only the summary (true fresh start) * - All messages are still stored but tagged with condenseParent * - blocks from the original task are preserved across condensings + * - is included to provide current workspace context * * @param {ApiMessage[]} messages - The conversation messages * @param {ApiHandler} apiHandler - The API handler to use for summarization and token counting * @param {string} systemPrompt - The system prompt for API requests (fallback if customCondensingPrompt not provided) * @param {string} taskId - The task ID for the conversation, used for telemetry - * @param {number} prevContextTokens - The number of tokens currently in the context, used to ensure we don't grow the context * @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically * @param {string} customCondensingPrompt - Optional custom prompt to use for condensing * @param {ApiHandlerCreateMessageMetadata} metadata - Optional metadata to pass to createMessage (tools, taskId, etc.) + * @param {string} environmentDetails - Optional environment details string to include in the summary * @returns {SummarizeResponse} - The result of the summarization operation (see above) */ export async function summarizeConversation( @@ -147,10 +148,10 @@ export async function summarizeConversation( apiHandler: ApiHandler, systemPrompt: string, taskId: string, - prevContextTokens: number, isAutomaticTrigger?: boolean, customCondensingPrompt?: string, metadata?: ApiHandlerCreateMessageMetadata, + environmentDetails?: string, ): Promise { TelemetryService.instance.captureContextCondensed( taskId, @@ -293,6 +294,14 @@ ${commandBlocks} }) } + // Add environment details as a separate text block if provided + if (environmentDetails?.trim()) { + summaryContent.push({ + type: "text", + text: environmentDetails, + }) + } + // Generate a unique condenseId for this summary const condenseId = crypto.randomUUID() @@ -342,10 +351,6 @@ ${commandBlocks} ) const newContextTokens = outputTokens + (await apiHandler.countTokens(contextBlocks)) - if (newContextTokens >= prevContextTokens) { - const error = t("common:errors.condense_context_grew") - return { ...response, cost, error } - } return { messages: newMessages, summary, cost, newContextTokens, condenseId } } diff --git a/src/core/context-management/__tests__/context-management.spec.ts b/src/core/context-management/__tests__/context-management.spec.ts index 85697d416aa..7c5db2d5104 100644 --- a/src/core/context-management/__tests__/context-management.spec.ts +++ b/src/core/context-management/__tests__/context-management.spec.ts @@ -617,10 +617,10 @@ describe("Context Management", () => { mockApiHandler, "System prompt", taskId, - 70001, - true, + true, // isAutomaticTrigger undefined, // customCondensingPrompt undefined, // metadata + undefined, // environmentDetails ) // Verify the result contains the summary information @@ -792,10 +792,10 @@ describe("Context Management", () => { mockApiHandler, "System prompt", taskId, - 60000, - true, + true, // isAutomaticTrigger undefined, // customCondensingPrompt undefined, // metadata + undefined, // environmentDetails ) // Verify the result contains the summary information diff --git a/src/core/context-management/index.ts b/src/core/context-management/index.ts index b781a7aa89d..250dbeccbe7 100644 --- a/src/core/context-management/index.ts +++ b/src/core/context-management/index.ts @@ -220,6 +220,8 @@ export type ContextManagementOptions = { currentProfileId: string /** Optional metadata to pass through to the condensing API call (tools, taskId, etc.) */ metadata?: ApiHandlerCreateMessageMetadata + /** Optional environment details string to include in the condensed summary */ + environmentDetails?: string } export type ContextManagementResult = SummarizeResponse & { @@ -249,6 +251,7 @@ export async function manageContext({ profileThresholds, currentProfileId, metadata, + environmentDetails, }: ContextManagementOptions): Promise { let error: string | undefined let errorDetails: string | undefined @@ -299,10 +302,10 @@ export async function manageContext({ apiHandler, systemPrompt, taskId, - prevContextTokens, true, // automatic trigger customCondensingPrompt, metadata, + environmentDetails, ) if (result.error) { error = result.error diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 2513040e6d4..28a43ede320 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1626,6 +1626,9 @@ export class Task extends EventEmitter implements TaskLike { } : {}), } + // Generate environment details to include in the condensed summary + const environmentDetails = await getEnvironmentDetails(this, true) + const { messages, summary, @@ -1639,10 +1642,10 @@ export class Task extends EventEmitter implements TaskLike { this.api, // Main API handler (fallback) systemPrompt, // Default summarization prompt (fallback) this.taskId, - prevContextTokens, false, // manual trigger customCondensingPrompt, // User's custom prompt metadata, // Pass metadata with tools + environmentDetails, // Include environment details in summary ) if (error) { await this.say( @@ -3772,6 +3775,9 @@ export class Task extends EventEmitter implements TaskLike { } try { + // Generate environment details to include in the condensed summary + const environmentDetails = await getEnvironmentDetails(this, true) + // Force aggressive truncation by keeping only 75% of the conversation history const truncateResult = await manageContext({ messages: this.apiConversationHistory, @@ -3786,6 +3792,7 @@ export class Task extends EventEmitter implements TaskLike { profileThresholds, currentProfileId, metadata, + environmentDetails, }) if (truncateResult.messages !== this.apiConversationHistory) { @@ -3985,6 +3992,9 @@ export class Task extends EventEmitter implements TaskLike { : {}), } + // Generate environment details to include in the condensed summary + const contextMgmtEnvironmentDetails = await getEnvironmentDetails(this, true) + try { const truncateResult = await manageContext({ messages: this.apiConversationHistory, @@ -4000,6 +4010,7 @@ export class Task extends EventEmitter implements TaskLike { profileThresholds, currentProfileId, metadata: contextMgmtMetadata, + environmentDetails: contextMgmtEnvironmentDetails, }) if (truncateResult.messages !== this.apiConversationHistory) { await this.overwriteApiConversationHistory(truncateResult.messages) diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index f240dc0a196..9f8f961e73e 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -66,7 +66,6 @@ "condense_not_enough_messages": "No hi ha prou missatges per condensar el context", "condensed_recently": "El context s'ha condensat recentment; s'omet aquest intent", "condense_handler_invalid": "El gestor de l'API per condensar el context no és vàlid", - "condense_context_grew": "La mida del context ha augmentat durant la condensació; s'omet aquest intent", "condense_api_failed": "La crida a l'API de condensació ha fallat: {{message}}", "url_timeout": "El lloc web ha trigat massa a carregar (timeout). Això pot ser degut a una connexió lenta, un lloc web pesat o temporalment no disponible. Pots tornar-ho a provar més tard o comprovar si la URL és correcta.", "url_not_found": "No s'ha pogut trobar l'adreça del lloc web. Comprova si la URL és correcta i torna-ho a provar.", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 681a57d6b86..086372dda85 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "Nicht genügend Nachrichten zum Verdichten des Kontexts", "condensed_recently": "Kontext wurde kürzlich verdichtet; dieser Versuch wird übersprungen", "condense_handler_invalid": "API-Handler zum Verdichten des Kontexts ist ungültig", - "condense_context_grew": "Kontextgröße ist während der Verdichtung gewachsen; dieser Versuch wird übersprungen", "condense_api_failed": "Verdichtungs-API-Aufruf fehlgeschlagen: {{message}}", "url_timeout": "Die Website hat zu lange zum Laden gebraucht (Timeout). Das könnte an einer langsamen Verbindung, einer schweren Website oder vorübergehender Nichtverfügbarkeit liegen. Du kannst es später nochmal versuchen oder prüfen, ob die URL korrekt ist.", "url_not_found": "Die Website-Adresse konnte nicht gefunden werden. Bitte prüfe, ob die URL korrekt ist und versuche es erneut.", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index c70556b2d2b..d24060dc61e 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "Not enough messages to condense context", "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_api_failed": "Condensing API call failed: {{message}}", "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.", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 053cbdc3cd1..bc22040c6a8 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "No hay suficientes mensajes para condensar el contexto", "condensed_recently": "El contexto se condensó recientemente; se omite este intento", "condense_handler_invalid": "El manejador de API para condensar el contexto no es válido", - "condense_context_grew": "El tamaño del contexto aumentó durante la condensación; se omite este intento", "condense_api_failed": "La llamada API de condensación falló: {{message}}", "url_timeout": "El sitio web tardó demasiado en cargar (timeout). Esto podría deberse a una conexión lenta, un sitio web pesado o que esté temporalmente no disponible. Puedes intentarlo más tarde o verificar si la URL es correcta.", "url_not_found": "No se pudo encontrar la dirección del sitio web. Por favor verifica si la URL es correcta e inténtalo de nuevo.", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index d3c9a287219..f7a76a53c12 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "Pas assez de messages pour condenser le contexte", "condensed_recently": "Le contexte a été condensé récemment ; cette tentative est ignorée", "condense_handler_invalid": "Le gestionnaire d'API pour condenser le contexte est invalide", - "condense_context_grew": "La taille du contexte a augmenté pendant la condensation ; cette tentative est ignorée", "condense_api_failed": "L'appel API de condensation a échoué : {{message}}", "url_timeout": "Le site web a pris trop de temps à charger (timeout). Cela pourrait être dû à une connexion lente, un site web lourd ou temporairement indisponible. Tu peux réessayer plus tard ou vérifier si l'URL est correcte.", "url_not_found": "L'adresse du site web n'a pas pu être trouvée. Vérifie si l'URL est correcte et réessaie.", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 23e4379c24b..e51d177d946 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "संदर्भ को संक्षिप्त करने के लिए पर्याप्त संदेश नहीं हैं", "condensed_recently": "संदर्भ हाल ही में संक्षिप्त किया गया था; इस प्रयास को छोड़ा जा रहा है", "condense_handler_invalid": "संदर्भ को संक्षिप्त करने के लिए API हैंडलर अमान्य है", - "condense_context_grew": "संक्षिप्तीकरण के दौरान संदर्भ का आकार बढ़ गया; इस प्रयास को छोड़ा जा रहा है", "condense_api_failed": "संक्षिप्तीकरण API कॉल विफल: {{message}}", "url_timeout": "वेबसाइट लोड होने में बहुत समय लगा (टाइमआउट)। यह धीमे कनेक्शन, भारी वेबसाइट या अस्थायी रूप से अनुपलब्ध होने के कारण हो सकता है। आप बाद में फिर से कोशिश कर सकते हैं या जांच सकते हैं कि URL सही है या नहीं।", "url_not_found": "वेबसाइट का पता नहीं मिल सका। कृपया जांचें कि URL सही है और फिर से कोशिश करें।", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 3ddfefafb0f..cfb165979d3 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "Tidak cukup pesan untuk mengompres konteks", "condensed_recently": "Konteks baru saja dikompres; melewati percobaan ini", "condense_handler_invalid": "Handler API untuk mengompres konteks tidak valid", - "condense_context_grew": "Ukuran konteks bertambah saat mengompres; melewati percobaan ini", "condense_api_failed": "Panggilan API pengompresan gagal: {{message}}", "url_timeout": "Situs web membutuhkan waktu terlalu lama untuk dimuat (timeout). Ini bisa disebabkan oleh koneksi lambat, situs web berat, atau sementara tidak tersedia. Kamu bisa mencoba lagi nanti atau memeriksa apakah URL sudah benar.", "url_not_found": "Alamat situs web tidak dapat ditemukan. Silakan periksa apakah URL sudah benar dan coba lagi.", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 06d43563279..e5fa6d68db3 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "Non ci sono abbastanza messaggi per condensare il contesto", "condensed_recently": "Il contesto è stato condensato di recente; questo tentativo viene saltato", "condense_handler_invalid": "Il gestore API per condensare il contesto non è valido", - "condense_context_grew": "La dimensione del contesto è aumentata durante la condensazione; questo tentativo viene saltato", "condense_api_failed": "Chiamata API di condensazione fallita: {{message}}", "url_timeout": "Il sito web ha impiegato troppo tempo a caricarsi (timeout). Questo potrebbe essere dovuto a una connessione lenta, un sito web pesante o temporaneamente non disponibile. Puoi riprovare più tardi o verificare se l'URL è corretto.", "url_not_found": "L'indirizzo del sito web non è stato trovato. Verifica se l'URL è corretto e riprova.", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 28ef44fe67b..7ebe0de597d 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "コンテキストを圧縮するのに十分なメッセージがありません", "condensed_recently": "コンテキストは最近圧縮されました;この試行をスキップします", "condense_handler_invalid": "コンテキストを圧縮するためのAPIハンドラーが無効です", - "condense_context_grew": "圧縮中にコンテキストサイズが増加しました;この試行をスキップします", "condense_api_failed": "圧縮API呼び出しが失敗しました:{{message}}", "url_timeout": "ウェブサイトの読み込みがタイムアウトしました。接続が遅い、ウェブサイトが重い、または一時的に利用できない可能性があります。後でもう一度試すか、URLが正しいか確認してください。", "url_not_found": "ウェブサイトのアドレスが見つかりませんでした。URLが正しいか確認してもう一度試してください。", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index e2e837400e6..0c1ed5ba518 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "컨텍스트를 압축할 메시지가 충분하지 않습니다", "condensed_recently": "컨텍스트가 최근 압축되었습니다; 이 시도를 건너뜁니다", "condense_handler_invalid": "컨텍스트 압축을 위한 API 핸들러가 유효하지 않습니다", - "condense_context_grew": "압축 중 컨텍스트 크기가 증가했습니다; 이 시도를 건너뜁니다", "condense_api_failed": "압축 API 호출 실패: {{message}}", "url_timeout": "웹사이트 로딩이 너무 오래 걸렸습니다(타임아웃). 느린 연결, 무거운 웹사이트 또는 일시적으로 사용할 수 없는 상태일 수 있습니다. 나중에 다시 시도하거나 URL이 올바른지 확인해 주세요.", "url_not_found": "웹사이트 주소를 찾을 수 없습니다. URL이 올바른지 확인하고 다시 시도해 주세요.", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 14211ffc004..0bbf5695364 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "Niet genoeg berichten om context te comprimeren", "condensed_recently": "Context is recent gecomprimeerd; deze poging wordt overgeslagen", "condense_handler_invalid": "API-handler voor het comprimeren van context is ongeldig", - "condense_context_grew": "Contextgrootte nam toe tijdens comprimeren; deze poging wordt overgeslagen", "condense_api_failed": "Comprimeer API-oproep mislukt: {{message}}", "url_timeout": "De website deed er te lang over om te laden (timeout). Dit kan komen door een trage verbinding, een zware website of tijdelijke onbeschikbaarheid. Je kunt het later opnieuw proberen of controleren of de URL correct is.", "url_not_found": "Het websiteadres kon niet worden gevonden. Controleer of de URL correct is en probeer opnieuw.", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 9c7074d5719..23bc09e4d78 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "Za mało wiadomości do skondensowania kontekstu", "condensed_recently": "Kontekst został niedawno skondensowany; pomijanie tej próby", "condense_handler_invalid": "Nieprawidłowy handler API do kondensowania kontekstu", - "condense_context_grew": "Rozmiar kontekstu wzrósł podczas kondensacji; pomijanie tej próby", "condense_api_failed": "Wywołanie API kondensacji nie powiodło się: {{message}}", "url_timeout": "Strona internetowa ładowała się zbyt długo (timeout). Może to być spowodowane wolnym połączeniem, ciężką stroną lub tymczasową niedostępnością. Możesz spróbować ponownie później lub sprawdzić, czy URL jest poprawny.", "url_not_found": "Nie można znaleźć adresu strony internetowej. Sprawdź, czy URL jest poprawny i spróbuj ponownie.", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index e8a23d1013a..d1ff5797646 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -66,7 +66,6 @@ "condense_not_enough_messages": "Não há mensagens suficientes para condensar o contexto", "condensed_recently": "O contexto foi condensado recentemente; pulando esta tentativa", "condense_handler_invalid": "O manipulador de API para condensar o contexto é inválido", - "condense_context_grew": "O tamanho do contexto aumentou durante a condensação; pulando esta tentativa", "condense_api_failed": "Chamada de API de condensação falhou: {{message}}", "url_timeout": "O site demorou muito para carregar (timeout). Isso pode ser devido a uma conexão lenta, site pesado ou temporariamente indisponível. Você pode tentar novamente mais tarde ou verificar se a URL está correta.", "url_not_found": "O endereço do site não pôde ser encontrado. Verifique se a URL está correta e tente novamente.", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 4467ceaeed2..7ac53199ba8 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "Недостаточно сообщений для сжатия контекста", "condensed_recently": "Контекст был недавно сжат; пропускаем эту попытку", "condense_handler_invalid": "Обработчик API для сжатия контекста недействителен", - "condense_context_grew": "Размер контекста увеличился во время сжатия; пропускаем эту попытку", "condense_api_failed": "Ошибка вызова API сжатия: {{message}}", "url_timeout": "Веб-сайт слишком долго загружался (таймаут). Это может быть из-за медленного соединения, тяжелого веб-сайта или временной недоступности. Ты можешь попробовать позже или проверить правильность URL.", "url_not_found": "Адрес веб-сайта не найден. Проверь правильность URL и попробуй снова.", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 5112581a1f0..fca268c0ff6 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "Bağlamı sıkıştırmak için yeterli mesaj yok", "condensed_recently": "Bağlam yakın zamanda sıkıştırıldı; bu deneme atlanıyor", "condense_handler_invalid": "Bağlamı sıkıştırmak için API işleyicisi geçersiz", - "condense_context_grew": "Sıkıştırma sırasında bağlam boyutu arttı; bu deneme atlanıyor", "condense_api_failed": "Sıkıştırma API çağrısı başarısız oldu: {{message}}", "url_timeout": "Web sitesi yüklenmesi çok uzun sürdü (zaman aşımı). Bu yavaş bağlantı, ağır web sitesi veya geçici olarak kullanılamama nedeniyle olabilir. Daha sonra tekrar deneyebilir veya URL'nin doğru olup olmadığını kontrol edebilirsin.", "url_not_found": "Web sitesi adresi bulunamadı. URL'nin doğru olup olmadığını kontrol et ve tekrar dene.", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 1540019ae1c..bd9bb72b474 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "Không đủ tin nhắn để nén ngữ cảnh", "condensed_recently": "Ngữ cảnh đã được nén gần đây; bỏ qua lần thử này", "condense_handler_invalid": "Trình xử lý API để nén ngữ cảnh không hợp lệ", - "condense_context_grew": "Kích thước ngữ cảnh tăng lên trong quá trình nén; bỏ qua lần thử này", "condense_api_failed": "Cuộc gọi API nén thất bại: {{message}}", "url_timeout": "Trang web mất quá nhiều thời gian để tải (timeout). Điều này có thể do kết nối chậm, trang web nặng hoặc tạm thời không khả dụng. Bạn có thể thử lại sau hoặc kiểm tra xem URL có đúng không.", "url_not_found": "Không thể tìm thấy địa chỉ trang web. Vui lòng kiểm tra URL có đúng không và thử lại.", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 71e1c2fd6c0..494c246d658 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -67,7 +67,6 @@ "condense_not_enough_messages": "没有足够的对话来压缩上下文", "condensed_recently": "上下文最近已压缩;跳过此次尝试", "condense_handler_invalid": "压缩上下文的API处理程序无效", - "condense_context_grew": "压缩过程中上下文大小增加;跳过此次尝试", "condense_api_failed": "压缩 API 调用失败:{{message}}", "url_timeout": "网站加载超时。这可能是由于网络连接缓慢、网站负载过重或暂时不可用。你可以稍后重试或检查 URL 是否正确。", "url_not_found": "找不到网站地址。请检查 URL 是否正确并重试。", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index f7df6e8fb18..572cdb46519 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -62,7 +62,6 @@ "condense_not_enough_messages": "沒有足夠的訊息來壓縮上下文", "condensed_recently": "上下文最近已壓縮;跳過此次嘗試", "condense_handler_invalid": "壓縮上下文的 API 處理程式無效", - "condense_context_grew": "壓縮過程中上下文大小增加;跳過此次嘗試", "condense_api_failed": "壓縮 API 呼叫失敗:{{message}}", "url_timeout": "網站載入超時。這可能是由於網路連線緩慢、網站負載過重或暫時無法使用。你可以稍後重試或檢查 URL 是否正確。", "url_not_found": "找不到網站位址。請檢查 URL 是否正確並重試。", From af19492b73148374487a75d75cb0751faee8f6b1 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Fri, 23 Jan 2026 13:36:38 -0700 Subject: [PATCH 2/3] fix: address PR review feedback - lazy getEnvironmentDetails and accurate newContextTokens - Only generate environment details when context management will run (avoids recursive workspace listing overhead on every API request) - Count actual summaryMessage content instead of using outputTokens as proxy (accounts for wrapper text: ## Conversation Summary, , ) - Update test expectations for new token calculation --- src/core/condense/__tests__/index.spec.ts | 12 +++++++----- src/core/condense/index.ts | 9 +++++---- src/core/task/Task.ts | 8 ++++++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/core/condense/__tests__/index.spec.ts b/src/core/condense/__tests__/index.spec.ts index 3a13c1d63fd..8a0f3bea63a 100644 --- a/src/core/condense/__tests__/index.spec.ts +++ b/src/core/condense/__tests__/index.spec.ts @@ -768,7 +768,8 @@ describe("summarizeConversation", () => { // Check the cost and token counts expect(result.cost).toBe(0.05) expect(result.summary).toBe("This is a summary") - expect(result.newContextTokens).toBe(250) // outputTokens(150) + countTokens(100) + // newContextTokens = countTokens(systemPrompt + summaryMessage) - counts actual content, not outputTokens + expect(result.newContextTokens).toBe(100) // countTokens mock returns 100 expect(result.error).toBeUndefined() }) @@ -938,11 +939,11 @@ describe("summarizeConversation", () => { const result = await summarizeConversation(messages, mockApiHandler, systemPrompt, taskId) - // Verify that countTokens was called with system prompt + // Verify that countTokens was called with system prompt + summary message expect(mockApiHandler.countTokens).toHaveBeenCalled() - // newContextTokens includes the summary output tokens plus countTokens(systemPrompt) - expect(result.newContextTokens).toBe(300) // outputTokens(200) + countTokens(100) + // newContextTokens = countTokens(systemPrompt + summaryMessage) - counts actual content + expect(result.newContextTokens).toBe(100) // countTokens mock returns 100 expect(result.cost).toBe(0.06) expect(result.summary).toBe("This is a summary with system prompt") expect(result.error).toBeUndefined() @@ -984,7 +985,8 @@ describe("summarizeConversation", () => { expect(result.cost).toBe(0.03) expect(result.summary).toBe("Concise summary") expect(result.error).toBeUndefined() - expect(result.newContextTokens).toBe(80) // outputTokens(50) + countTokens(30) + // newContextTokens = countTokens(systemPrompt + summaryMessage) - counts actual content + expect(result.newContextTokens).toBe(30) // countTokens mock returns 30 }) it("should return error when API handler is invalid", async () => { diff --git a/src/core/condense/index.ts b/src/core/condense/index.ts index 51e766773be..13592acc771 100644 --- a/src/core/condense/index.ts +++ b/src/core/condense/index.ts @@ -342,15 +342,16 @@ ${commandBlocks} // Count the tokens in the context for the next API request // After condense, the context will only contain the system prompt and the summary + // Note: This doesn't include tool definitions - it's an informational estimate for the UI const systemPromptMessage: ApiMessage = { role: "user", content: systemPrompt } - const contextMessages = outputTokens ? [systemPromptMessage] : [systemPromptMessage, summaryMessage] - - const contextBlocks = contextMessages.flatMap((message) => + // Count actual summaryMessage content directly instead of using outputTokens as a proxy + // This ensures we account for wrapper text (## Conversation Summary, , ) + const contextBlocks = [systemPromptMessage, summaryMessage].flatMap((message) => typeof message.content === "string" ? [{ text: message.content, type: "text" as const }] : message.content, ) - const newContextTokens = outputTokens + (await apiHandler.countTokens(contextBlocks)) + const newContextTokens = await apiHandler.countTokens(contextBlocks) return { messages: newMessages, summary, cost, newContextTokens, condenseId } } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 28a43ede320..7c9bfa342e4 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -3992,8 +3992,12 @@ export class Task extends EventEmitter implements TaskLike { : {}), } - // Generate environment details to include in the condensed summary - const contextMgmtEnvironmentDetails = await getEnvironmentDetails(this, true) + // Only generate environment details when context management will actually run. + // getEnvironmentDetails(this, true) triggers a recursive workspace listing which + // adds overhead - avoid this for the common case where context is below threshold. + const contextMgmtEnvironmentDetails = contextManagementWillRun + ? await getEnvironmentDetails(this, true) + : undefined try { const truncateResult = await manageContext({ From 7dec02af0c317c76a7ae33a34aaca119b3404fbe Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Fri, 23 Jan 2026 13:40:39 -0700 Subject: [PATCH 3/3] feat(condense): include tool tokens in newContextTokens calculation Add tool definition token counting to provide a more accurate estimate of the context size after condensation. Previously only system prompt and summary message tokens were counted. Now newContextTokens = messageTokens (system + summary) + toolTokens --- src/core/condense/index.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/core/condense/index.ts b/src/core/condense/index.ts index 13592acc771..e8aec6cd0e2 100644 --- a/src/core/condense/index.ts +++ b/src/core/condense/index.ts @@ -341,8 +341,7 @@ ${commandBlocks} newMessages.push(summaryMessage) // Count the tokens in the context for the next API request - // After condense, the context will only contain the system prompt and the summary - // Note: This doesn't include tool definitions - it's an informational estimate for the UI + // After condense, the context will contain: system prompt + summary + tool definitions const systemPromptMessage: ApiMessage = { role: "user", content: systemPrompt } // Count actual summaryMessage content directly instead of using outputTokens as a proxy @@ -351,7 +350,16 @@ ${commandBlocks} typeof message.content === "string" ? [{ text: message.content, type: "text" as const }] : message.content, ) - const newContextTokens = await apiHandler.countTokens(contextBlocks) + const messageTokens = await apiHandler.countTokens(contextBlocks) + + // Count tool definition tokens if tools are provided + let toolTokens = 0 + if (metadata?.tools && metadata.tools.length > 0) { + const toolsText = JSON.stringify(metadata.tools) + toolTokens = await apiHandler.countTokens([{ text: toolsText, type: "text" }]) + } + + const newContextTokens = messageTokens + toolTokens return { messages: newMessages, summary, cost, newContextTokens, condenseId } }