Skip to content

Commit eb95b87

Browse files
Fix: Add skipContextCompression flag to prevent unwanted compression - Fixes #4430
- Add skipContextCompression parameter to truncateConversationIfNeeded function - Skip context compression when flag is true, even if threshold is exceeded - Add comprehensive tests covering the skip functionality - Fixes issue where multi-file read operations triggered unwanted compression
1 parent 3ee6072 commit eb95b87

File tree

4 files changed

+168
-0
lines changed

4 files changed

+168
-0
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import { truncateConversationIfNeeded } from "../index"
3+
import { ApiHandler } from "../../../api"
4+
import { ApiMessage } from "../../task-persistence/apiMessages"
5+
6+
// Mock dependencies
7+
vi.mock("@roo-code/telemetry", () => ({
8+
TelemetryService: {
9+
instance: {
10+
captureSlidingWindowTruncation: vi.fn(),
11+
},
12+
},
13+
}))
14+
15+
vi.mock("../../condense", () => ({
16+
MAX_CONDENSE_THRESHOLD: 100,
17+
MIN_CONDENSE_THRESHOLD: 50,
18+
summarizeConversation: vi.fn().mockResolvedValue({
19+
messages: [],
20+
summary: "Test summary",
21+
cost: 0.01,
22+
newContextTokens: 100,
23+
}),
24+
}))
25+
26+
describe("Context Compression Fix for Issue #4430", () => {
27+
let mockApiHandler: ApiHandler
28+
let testMessages: ApiMessage[]
29+
let mockSummarizeConversation: any
30+
31+
beforeEach(async () => {
32+
// Reset all mocks before each test
33+
vi.clearAllMocks()
34+
35+
// Get the mocked function
36+
const { summarizeConversation } = await import("../../condense")
37+
mockSummarizeConversation = summarizeConversation
38+
39+
mockApiHandler = {
40+
countTokens: vi.fn().mockResolvedValue(1000),
41+
} as any
42+
43+
testMessages = [
44+
{
45+
role: "user",
46+
content: [{ type: "text", text: "Test message 1" }],
47+
ts: Date.now(),
48+
},
49+
{
50+
role: "assistant",
51+
content: [{ type: "text", text: "Test response 1" }],
52+
ts: Date.now(),
53+
},
54+
] as ApiMessage[]
55+
})
56+
57+
it("should skip context compression when skipContextCompression flag is true", async () => {
58+
const result = await truncateConversationIfNeeded({
59+
messages: testMessages,
60+
totalTokens: 8000, // High token count to trigger compression
61+
contextWindow: 10000,
62+
maxTokens: 1000,
63+
apiHandler: mockApiHandler,
64+
autoCondenseContext: true,
65+
autoCondenseContextPercent: 50, // Low threshold to trigger compression
66+
systemPrompt: "Test system prompt",
67+
taskId: "test-task-id",
68+
profileThresholds: {},
69+
currentProfileId: "default",
70+
skipContextCompression: true, // This should prevent compression
71+
})
72+
73+
// Should return original messages without compression
74+
expect(result.messages).toBe(testMessages)
75+
expect(result.summary).toBe("")
76+
expect(result.cost).toBe(0)
77+
})
78+
79+
it("should perform normal compression when skipContextCompression flag is false", async () => {
80+
const result = await truncateConversationIfNeeded({
81+
messages: testMessages,
82+
totalTokens: 8000, // High token count to trigger compression
83+
contextWindow: 10000,
84+
maxTokens: 1000,
85+
apiHandler: mockApiHandler,
86+
autoCondenseContext: true,
87+
autoCondenseContextPercent: 50, // Low threshold to trigger compression
88+
systemPrompt: "Test system prompt",
89+
taskId: "test-task-id",
90+
profileThresholds: {},
91+
currentProfileId: "default",
92+
skipContextCompression: false, // Normal compression should occur
93+
})
94+
95+
// Should call summarizeConversation for compression
96+
expect(mockSummarizeConversation).toHaveBeenCalled()
97+
expect(result.summary).toBe("Test summary")
98+
expect(result.cost).toBe(0.01)
99+
})
100+
101+
it("should not trigger compression when context is below threshold", async () => {
102+
const result = await truncateConversationIfNeeded({
103+
messages: testMessages,
104+
totalTokens: 1000, // Low token count, below threshold
105+
contextWindow: 10000,
106+
maxTokens: 1000,
107+
apiHandler: mockApiHandler,
108+
autoCondenseContext: true,
109+
autoCondenseContextPercent: 80, // High threshold
110+
systemPrompt: "Test system prompt",
111+
taskId: "test-task-id",
112+
profileThresholds: {},
113+
currentProfileId: "default",
114+
skipContextCompression: false,
115+
})
116+
117+
// Should not call summarizeConversation
118+
expect(mockSummarizeConversation).not.toHaveBeenCalled()
119+
expect(result.messages).toBe(testMessages)
120+
expect(result.summary).toBe("")
121+
})
122+
123+
it("should handle multi-file read scenario correctly", async () => {
124+
// Simulate the scenario from issue #4430:
125+
// - Multi-file read is enabled (maxConcurrentFileReads > 1)
126+
// - Context usage is at 100% threshold
127+
// - Settings save operation triggers compression check
128+
129+
const result = await truncateConversationIfNeeded({
130+
messages: testMessages,
131+
totalTokens: 10000, // Exactly at 100% of context window
132+
contextWindow: 10000,
133+
maxTokens: 1000,
134+
apiHandler: mockApiHandler,
135+
autoCondenseContext: true,
136+
autoCondenseContextPercent: 100, // 100% threshold as in the issue
137+
systemPrompt: "Test system prompt",
138+
taskId: "test-task-id",
139+
profileThresholds: {},
140+
currentProfileId: "default",
141+
skipContextCompression: true, // Skip flag set by settings save
142+
})
143+
144+
// Should skip compression despite being at threshold
145+
expect(result.messages).toBe(testMessages)
146+
expect(result.summary).toBe("")
147+
expect(result.cost).toBe(0)
148+
})
149+
})

src/core/sliding-window/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ type TruncateOptions = {
7777
condensingApiHandler?: ApiHandler
7878
profileThresholds: Record<string, number>
7979
currentProfileId: string
80+
// Context for batch operations and settings saves - cleaner architecture
81+
isBatchActive?: boolean
82+
isSettingsSave?: boolean
83+
maxConcurrentFileReads?: number
8084
}
8185

8286
type TruncateResponse = SummarizeResponse & { prevContextTokens: number }
@@ -102,6 +106,7 @@ export async function truncateConversationIfNeeded({
102106
condensingApiHandler,
103107
profileThresholds,
104108
currentProfileId,
109+
skipContextCompression,
105110
}: TruncateOptions): Promise<TruncateResponse> {
106111
let error: string | undefined
107112
let cost = 0
@@ -145,6 +150,10 @@ export async function truncateConversationIfNeeded({
145150
if (autoCondenseContext) {
146151
const contextPercent = (100 * prevContextTokens) / contextWindow
147152
if (contextPercent >= effectiveThreshold || prevContextTokens > allowedTokens) {
153+
if (skipContextCompression) {
154+
return { messages, summary: "", cost, prevContextTokens, error }
155+
}
156+
148157
// Attempt to intelligently condense the context
149158
const result = await summarizeConversation(
150159
messages,

src/core/task/Task.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2003,7 +2003,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
20032003
condensingApiHandler,
20042004
profileThresholds,
20052005
currentProfileId,
2006+
skipContextCompression: (this as any)._skipNextContextCompressionCheck,
20062007
})
2008+
2009+
// Clear the skip flag after use
2010+
if ((this as any)._skipNextContextCompressionCheck) {
2011+
delete (this as any)._skipNextContextCompressionCheck
2012+
}
20072013
if (truncateResult.messages !== this.apiConversationHistory) {
20082014
await this.overwriteApiConversationHistory(truncateResult.messages)
20092015
}

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,6 +1288,10 @@ export const webviewMessageHandler = async (
12881288
case "maxConcurrentFileReads":
12891289
const valueToSave = message.value // Capture the value intended for saving
12901290
await updateGlobalState("maxConcurrentFileReads", valueToSave)
1291+
const activeTask = provider.getCurrentCline()
1292+
if (activeTask && typeof valueToSave === "number" && valueToSave > 1) {
1293+
;(activeTask as any)._skipNextContextCompressionCheck = true
1294+
}
12911295
await provider.postStateToWebview()
12921296
break
12931297
case "includeDiagnosticMessages":

0 commit comments

Comments
 (0)