From 848fd82d9fa68cddff98cbeeec0e6b9bd44ecb9f Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 9 Sep 2025 07:30:21 +0000 Subject: [PATCH 1/2] fix: prevent context truncation when condensing is disabled (threshold=100%) - Modified truncateConversationIfNeeded to skip summarization when threshold is 100% - Prevent sliding window truncation when condensing is explicitly disabled - Added test to verify no truncation occurs when threshold is 100% - Updated existing tests to use appropriate thresholds Fixes #7811 --- .../__tests__/sliding-window.spec.ts | 46 ++++++++++++++++++- src/core/sliding-window/index.ts | 10 +++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/core/sliding-window/__tests__/sliding-window.spec.ts b/src/core/sliding-window/__tests__/sliding-window.spec.ts index 0f2c70c81b..f8a4312d28 100644 --- a/src/core/sliding-window/__tests__/sliding-window.spec.ts +++ b/src/core/sliding-window/__tests__/sliding-window.spec.ts @@ -577,7 +577,7 @@ describe("Sliding Window", () => { maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, autoCondenseContext: true, - autoCondenseContextPercent: 100, + autoCondenseContextPercent: 50, // Use a threshold less than 100% systemPrompt: "System prompt", taskId, profileThresholds: {}, @@ -644,7 +644,7 @@ describe("Sliding Window", () => { maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, autoCondenseContext: true, - autoCondenseContextPercent: 100, + autoCondenseContextPercent: 50, // Use a threshold less than 100% systemPrompt: "System prompt", taskId, profileThresholds: {}, @@ -821,6 +821,48 @@ describe("Sliding Window", () => { // Clean up summarizeSpy.mockRestore() }) + + it("should not truncate when autoCondenseContext is true and threshold is 100% even if tokens exceed allowedTokens", async () => { + const modelInfo = createModelInfo(100000, 30000) + const totalTokens = 75000 // This exceeds allowedTokens (60000) but should not truncate when disabled + + const messagesWithSmallContent = [ + ...messages.slice(0, -1), + { ...messages[messages.length - 1], content: "" }, + ] + + // Spy on summarizeConversation to ensure it's not called + const summarizeSpy = vi.spyOn(condenseModule, "summarizeConversation") + + const result = await truncateConversationIfNeeded({ + messages: messagesWithSmallContent, + totalTokens, + contextWindow: modelInfo.contextWindow, + maxTokens: modelInfo.maxTokens, + apiHandler: mockApiHandler, + autoCondenseContext: true, // Enabled but with 100% threshold + autoCondenseContextPercent: 100, // 100% threshold means never condense + systemPrompt: "System prompt", + taskId, + profileThresholds: {}, + currentProfileId: "default", + }) + + // Verify summarizeConversation was NOT called when threshold is 100% + expect(summarizeSpy).not.toHaveBeenCalled() + + // Should NOT truncate even though tokens exceed allowedTokens when threshold is 100% + expect(result).toEqual({ + messages: messagesWithSmallContent, + summary: "", + cost: 0, + prevContextTokens: totalTokens, + error: undefined, + }) + + // Clean up + summarizeSpy.mockRestore() + }) }) /** diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts index 1e518c9a56..d83730b8e6 100644 --- a/src/core/sliding-window/index.ts +++ b/src/core/sliding-window/index.ts @@ -142,7 +142,7 @@ export async function truncateConversationIfNeeded({ } // If no specific threshold is found for the profile, fall back to global setting - if (autoCondenseContext) { + if (autoCondenseContext && effectiveThreshold < 100) { const contextPercent = (100 * prevContextTokens) / contextWindow if (contextPercent >= effectiveThreshold || prevContextTokens > allowedTokens) { // Attempt to intelligently condense the context @@ -166,7 +166,15 @@ export async function truncateConversationIfNeeded({ } // Fall back to sliding window truncation if needed + // Exception: When context condensing is explicitly disabled (threshold = 100%), don't truncate if (prevContextTokens > allowedTokens) { + // Check if condensing is explicitly disabled (threshold is 100% and autoCondenseContext is true) + // This means the user has set the threshold to 100% to disable condensing + if (autoCondenseContext && effectiveThreshold >= 100) { + // Context condensing is explicitly disabled by user, don't truncate + return { messages, summary: "", cost, prevContextTokens, error } + } + // Apply sliding window truncation in all other cases const truncatedMessages = truncateConversation(messages, 0.5, taskId) return { messages: truncatedMessages, prevContextTokens, summary: "", cost, error } } From ec0a03d6312c302dd2aba814c587c2a01609971f Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 10 Sep 2025 06:00:26 +0000 Subject: [PATCH 2/2] fix: use autoCondenseContext checkbox instead of 100% threshold check The issue was that the code was checking for a 100% threshold value to determine if condensing was disabled, when it should have been using the existing "Automatically trigger intelligent context condensing" checkbox (autoCondenseContext boolean). Changes: - When autoCondenseContext is false, no truncation or condensing occurs - Removed the special case for 100% threshold - Updated all related tests to reflect the correct behavior --- .../__tests__/sliding-window.spec.ts | 109 ++++++++---------- src/core/sliding-window/index.ts | 11 +- 2 files changed, 51 insertions(+), 69 deletions(-) diff --git a/src/core/sliding-window/__tests__/sliding-window.spec.ts b/src/core/sliding-window/__tests__/sliding-window.spec.ts index f8a4312d28..9da589bbe3 100644 --- a/src/core/sliding-window/__tests__/sliding-window.spec.ts +++ b/src/core/sliding-window/__tests__/sliding-window.spec.ts @@ -284,7 +284,7 @@ describe("Sliding Window", () => { }) }) - it("should truncate if tokens are above max tokens threshold", async () => { + it("should not truncate if tokens are above max tokens threshold when autoCondenseContext is false", async () => { const modelInfo = createModelInfo(100000, 30000) const totalTokens = 70001 // Above threshold @@ -294,21 +294,13 @@ describe("Sliding Window", () => { { ...messages[messages.length - 1], content: "" }, ] - // When truncating, always uses 0.5 fraction - // With 4 messages after the first, 0.5 fraction means remove 2 messages - const expectedMessages = [ - messagesWithSmallContent[0], - messagesWithSmallContent[3], - messagesWithSmallContent[4], - ] - const result = await truncateConversationIfNeeded({ messages: messagesWithSmallContent, totalTokens, contextWindow: modelInfo.contextWindow, maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - should not truncate autoCondenseContextPercent: 100, systemPrompt: "System prompt", taskId, @@ -316,8 +308,9 @@ describe("Sliding Window", () => { currentProfileId: "default", }) + // When autoCondenseContext is false, should NOT truncate expect(result).toEqual({ - messages: expectedMessages, + messages: messagesWithSmallContent, // Original messages preserved summary: "", cost: 0, prevContextTokens: totalTokens, @@ -343,7 +336,7 @@ describe("Sliding Window", () => { contextWindow: modelInfo1.contextWindow, maxTokens: modelInfo1.maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - no truncation autoCondenseContextPercent: 100, systemPrompt: "System prompt", taskId, @@ -357,7 +350,7 @@ describe("Sliding Window", () => { contextWindow: modelInfo2.contextWindow, maxTokens: modelInfo2.maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - no truncation autoCondenseContextPercent: 100, systemPrompt: "System prompt", taskId, @@ -370,7 +363,7 @@ describe("Sliding Window", () => { expect(result1.cost).toEqual(result2.cost) expect(result1.prevContextTokens).toEqual(result2.prevContextTokens) - // Test above threshold + // Test above threshold - with autoCondenseContext false, should not truncate const aboveThreshold = 70001 const result3 = await truncateConversationIfNeeded({ messages: messagesWithSmallContent, @@ -378,7 +371,7 @@ describe("Sliding Window", () => { contextWindow: modelInfo1.contextWindow, maxTokens: modelInfo1.maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - no truncation autoCondenseContextPercent: 100, systemPrompt: "System prompt", taskId, @@ -392,7 +385,7 @@ describe("Sliding Window", () => { contextWindow: modelInfo2.contextWindow, maxTokens: modelInfo2.maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - no truncation autoCondenseContextPercent: 100, systemPrompt: "System prompt", taskId, @@ -400,6 +393,9 @@ describe("Sliding Window", () => { currentProfileId: "default", }) + // Both should preserve original messages when autoCondenseContext is false + expect(result3.messages).toEqual(messagesWithSmallContent) + expect(result4.messages).toEqual(messagesWithSmallContent) expect(result3.messages).toEqual(result4.messages) expect(result3.summary).toEqual(result4.summary) expect(result3.cost).toEqual(result4.cost) @@ -463,14 +459,15 @@ describe("Sliding Window", () => { contextWindow: modelInfo.contextWindow, maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - no truncation autoCondenseContextPercent: 100, systemPrompt: "System prompt", taskId, profileThresholds: {}, currentProfileId: "default", }) - expect(resultWithLarge.messages).not.toEqual(messagesWithLargeContent) // Should truncate + // When autoCondenseContext is false, should NOT truncate + expect(resultWithLarge.messages).toEqual(messagesWithLargeContent) // Should NOT truncate expect(resultWithLarge.summary).toBe("") expect(resultWithLarge.cost).toBe(0) expect(resultWithLarge.prevContextTokens).toBe(baseTokensForLarge + largeContentTokens) @@ -491,20 +488,21 @@ describe("Sliding Window", () => { contextWindow: modelInfo.contextWindow, maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - no truncation autoCondenseContextPercent: 100, systemPrompt: "System prompt", taskId, profileThresholds: {}, currentProfileId: "default", }) - expect(resultWithVeryLarge.messages).not.toEqual(messagesWithVeryLargeContent) // Should truncate + // When autoCondenseContext is false, should NOT truncate + expect(resultWithVeryLarge.messages).toEqual(messagesWithVeryLargeContent) // Should NOT truncate expect(resultWithVeryLarge.summary).toBe("") expect(resultWithVeryLarge.cost).toBe(0) expect(resultWithVeryLarge.prevContextTokens).toBe(baseTokensForVeryLarge + veryLargeContentTokens) }) - it("should truncate if tokens are within TOKEN_BUFFER_PERCENTAGE of the threshold", async () => { + it("should not truncate if tokens are within TOKEN_BUFFER_PERCENTAGE when autoCondenseContext is false", async () => { const modelInfo = createModelInfo(100000, 30000) const dynamicBuffer = modelInfo.contextWindow * TOKEN_BUFFER_PERCENTAGE // 10% of 100000 = 10000 const totalTokens = 70000 - dynamicBuffer + 1 // Just within the dynamic buffer of threshold (70000) @@ -515,29 +513,22 @@ describe("Sliding Window", () => { { ...messages[messages.length - 1], content: "" }, ] - // When truncating, always uses 0.5 fraction - // With 4 messages after the first, 0.5 fraction means remove 2 messages - const expectedResult = [ - messagesWithSmallContent[0], - messagesWithSmallContent[3], - messagesWithSmallContent[4], - ] - const result = await truncateConversationIfNeeded({ messages: messagesWithSmallContent, totalTokens, contextWindow: modelInfo.contextWindow, maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - no truncation autoCondenseContextPercent: 100, systemPrompt: "System prompt", taskId, profileThresholds: {}, currentProfileId: "default", }) + // When autoCondenseContext is false, should NOT truncate expect(result).toEqual({ - messages: expectedResult, + messages: messagesWithSmallContent, // Original messages preserved summary: "", cost: 0, prevContextTokens: totalTokens, @@ -676,21 +667,13 @@ describe("Sliding Window", () => { { ...messages[messages.length - 1], content: "" }, ] - // When truncating, always uses 0.5 fraction - // With 4 messages after the first, 0.5 fraction means remove 2 messages - const expectedMessages = [ - messagesWithSmallContent[0], - messagesWithSmallContent[3], - messagesWithSmallContent[4], - ] - const result = await truncateConversationIfNeeded({ messages: messagesWithSmallContent, totalTokens, contextWindow: modelInfo.contextWindow, maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - should not truncate autoCondenseContextPercent: 50, // This shouldn't matter since autoCondenseContext is false systemPrompt: "System prompt", taskId, @@ -701,9 +684,9 @@ describe("Sliding Window", () => { // Verify summarizeConversation was not called expect(summarizeSpy).not.toHaveBeenCalled() - // Verify it used truncation + // When autoCondenseContext is false, should NOT truncate even if above threshold expect(result).toEqual({ - messages: expectedMessages, + messages: messagesWithSmallContent, // Original messages preserved summary: "", cost: 0, prevContextTokens: totalTokens, @@ -822,7 +805,7 @@ describe("Sliding Window", () => { summarizeSpy.mockRestore() }) - it("should not truncate when autoCondenseContext is true and threshold is 100% even if tokens exceed allowedTokens", async () => { + it("should not truncate when autoCondenseContext is false even if tokens exceed allowedTokens", async () => { const modelInfo = createModelInfo(100000, 30000) const totalTokens = 75000 // This exceeds allowedTokens (60000) but should not truncate when disabled @@ -840,18 +823,18 @@ describe("Sliding Window", () => { contextWindow: modelInfo.contextWindow, maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: true, // Enabled but with 100% threshold - autoCondenseContextPercent: 100, // 100% threshold means never condense + autoCondenseContext: false, // Disabled - should not truncate + autoCondenseContextPercent: 50, // This shouldn't matter since autoCondenseContext is false systemPrompt: "System prompt", taskId, profileThresholds: {}, currentProfileId: "default", }) - // Verify summarizeConversation was NOT called when threshold is 100% + // Verify summarizeConversation was NOT called expect(summarizeSpy).not.toHaveBeenCalled() - // Should NOT truncate even though tokens exceed allowedTokens when threshold is 100% + // Should NOT truncate even though tokens exceed allowedTokens when autoCondenseContext is false expect(result).toEqual({ messages: messagesWithSmallContent, summary: "", @@ -1123,22 +1106,22 @@ describe("Sliding Window", () => { prevContextTokens: 39999, }) - // Above max tokens - truncate + // Above max tokens - but with autoCondenseContext false, should not truncate const result2 = await truncateConversationIfNeeded({ messages: messagesWithSmallContent, totalTokens: 50001, // Above threshold contextWindow: modelInfo.contextWindow, maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - no truncation autoCondenseContextPercent: 100, systemPrompt: "System prompt", taskId, profileThresholds: {}, currentProfileId: "default", }) - expect(result2.messages).not.toEqual(messagesWithSmallContent) - expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction + // When autoCondenseContext is false, should NOT truncate + expect(result2.messages).toEqual(messagesWithSmallContent) expect(result2.summary).toBe("") expect(result2.cost).toBe(0) expect(result2.prevContextTokens).toBe(50001) @@ -1176,22 +1159,22 @@ describe("Sliding Window", () => { prevContextTokens: 81807, }) - // Above max tokens - truncate + // Above max tokens - but with autoCondenseContext false, should not truncate const result2 = await truncateConversationIfNeeded({ messages: messagesWithSmallContent, totalTokens: 81809, // Above threshold (81808) contextWindow: modelInfo.contextWindow, maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - no truncation autoCondenseContextPercent: 100, systemPrompt: "System prompt", taskId, profileThresholds: {}, currentProfileId: "default", }) - expect(result2.messages).not.toEqual(messagesWithSmallContent) - expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction + // When autoCondenseContext is false, should NOT truncate + expect(result2.messages).toEqual(messagesWithSmallContent) expect(result2.summary).toBe("") expect(result2.cost).toBe(0) expect(result2.prevContextTokens).toBe(81809) @@ -1223,22 +1206,22 @@ describe("Sliding Window", () => { }) expect(result1.messages).toEqual(messagesWithSmallContent) - // Above max tokens - truncate + // Above max tokens - but with autoCondenseContext false, should not truncate const result2 = await truncateConversationIfNeeded({ messages: messagesWithSmallContent, totalTokens: 40001, // Above threshold contextWindow: modelInfo.contextWindow, maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - no truncation autoCondenseContextPercent: 100, systemPrompt: "System prompt", taskId, profileThresholds: {}, currentProfileId: "default", }) - expect(result2).not.toEqual(messagesWithSmallContent) - expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction + // When autoCondenseContext is false, should NOT truncate + expect(result2.messages).toEqual(messagesWithSmallContent) }) it("should handle large context windows appropriately", async () => { @@ -1268,22 +1251,22 @@ describe("Sliding Window", () => { }) expect(result1.messages).toEqual(messagesWithSmallContent) - // Above max tokens - truncate + // Above max tokens - but with autoCondenseContext false, should not truncate const result2 = await truncateConversationIfNeeded({ messages: messagesWithSmallContent, totalTokens: 170001, // Above threshold contextWindow: modelInfo.contextWindow, maxTokens: modelInfo.maxTokens, apiHandler: mockApiHandler, - autoCondenseContext: false, + autoCondenseContext: false, // Disabled - no truncation autoCondenseContextPercent: 100, systemPrompt: "System prompt", taskId, profileThresholds: {}, currentProfileId: "default", }) - expect(result2).not.toEqual(messagesWithSmallContent) - expect(result2.messages.length).toBe(3) // Truncated with 0.5 fraction + // When autoCondenseContext is false, should NOT truncate + expect(result2.messages).toEqual(messagesWithSmallContent) }) }) }) diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts index d83730b8e6..0eaa041bd3 100644 --- a/src/core/sliding-window/index.ts +++ b/src/core/sliding-window/index.ts @@ -142,6 +142,7 @@ export async function truncateConversationIfNeeded({ } // If no specific threshold is found for the profile, fall back to global setting + // Only apply condensing if autoCondenseContext is enabled if (autoCondenseContext && effectiveThreshold < 100) { const contextPercent = (100 * prevContextTokens) / contextWindow if (contextPercent >= effectiveThreshold || prevContextTokens > allowedTokens) { @@ -166,15 +167,13 @@ export async function truncateConversationIfNeeded({ } // Fall back to sliding window truncation if needed - // Exception: When context condensing is explicitly disabled (threshold = 100%), don't truncate + // When autoCondenseContext is false, don't truncate - user has explicitly disabled context management if (prevContextTokens > allowedTokens) { - // Check if condensing is explicitly disabled (threshold is 100% and autoCondenseContext is true) - // This means the user has set the threshold to 100% to disable condensing - if (autoCondenseContext && effectiveThreshold >= 100) { - // Context condensing is explicitly disabled by user, don't truncate + if (!autoCondenseContext) { + // Context condensing is disabled by the checkbox, don't truncate return { messages, summary: "", cost, prevContextTokens, error } } - // Apply sliding window truncation in all other cases + // Apply sliding window truncation only when condensing is enabled but failed or threshold not reached const truncatedMessages = truncateConversation(messages, 0.5, taskId) return { messages: truncatedMessages, prevContextTokens, summary: "", cost, error } }