Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 8aa1346

Browse files
Refactor: Unified context-management architecture with improved UX (#9795)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent 946fd03 commit 8aa1346

34 files changed

Lines changed: 893 additions & 138 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, it, expect } from "vitest"
2+
import { CONTEXT_MANAGEMENT_EVENTS, isContextManagementEvent } from "../context-management.js"
3+
4+
describe("context-management", () => {
5+
describe("CONTEXT_MANAGEMENT_EVENTS", () => {
6+
it("should contain all expected event types", () => {
7+
expect(CONTEXT_MANAGEMENT_EVENTS).toContain("condense_context")
8+
expect(CONTEXT_MANAGEMENT_EVENTS).toContain("condense_context_error")
9+
expect(CONTEXT_MANAGEMENT_EVENTS).toContain("sliding_window_truncation")
10+
expect(CONTEXT_MANAGEMENT_EVENTS).toHaveLength(3)
11+
})
12+
})
13+
14+
describe("isContextManagementEvent", () => {
15+
it("should return true for valid context management events", () => {
16+
expect(isContextManagementEvent("condense_context")).toBe(true)
17+
expect(isContextManagementEvent("condense_context_error")).toBe(true)
18+
expect(isContextManagementEvent("sliding_window_truncation")).toBe(true)
19+
})
20+
21+
it("should return false for non-context-management events", () => {
22+
expect(isContextManagementEvent("text")).toBe(false)
23+
expect(isContextManagementEvent("error")).toBe(false)
24+
expect(isContextManagementEvent(null)).toBe(false)
25+
expect(isContextManagementEvent(undefined)).toBe(false)
26+
})
27+
})
28+
})
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Context Management Types
3+
*
4+
* This module provides type definitions for context management events.
5+
* These events are used to handle different strategies for managing conversation context
6+
* when approaching token limits.
7+
*
8+
* Event Types:
9+
* - `condense_context`: Context was condensed using AI summarization
10+
* - `condense_context_error`: An error occurred during context condensation
11+
* - `sliding_window_truncation`: Context was truncated using sliding window strategy
12+
*/
13+
14+
/**
15+
* Array of all context management event types.
16+
* Used for runtime type checking.
17+
*/
18+
export const CONTEXT_MANAGEMENT_EVENTS = [
19+
"condense_context",
20+
"condense_context_error",
21+
"sliding_window_truncation",
22+
] as const
23+
24+
/**
25+
* Union type representing all possible context management event types.
26+
*/
27+
export type ContextManagementEvent = (typeof CONTEXT_MANAGEMENT_EVENTS)[number]
28+
29+
/**
30+
* Type guard function to check if a value is a valid context management event.
31+
*/
32+
export function isContextManagementEvent(value: unknown): value is ContextManagementEvent {
33+
return typeof value === "string" && (CONTEXT_MANAGEMENT_EVENTS as readonly string[]).includes(value)
34+
}

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from "./api.js"
22
export * from "./cloud.js"
33
export * from "./codebase-index.js"
4+
export * from "./context-management.js"
45
export * from "./cookie-consent.js"
56
export * from "./events.js"
67
export * from "./experiment.js"

packages/types/src/message.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,17 @@ export type ToolProgressStatus = z.infer<typeof toolProgressStatusSchema>
197197

198198
/**
199199
* ContextCondense
200+
*
201+
* Data associated with a successful context condensation event.
202+
* This is attached to messages with `say: "condense_context"` when
203+
* the condensation operation completes successfully.
204+
*
205+
* @property cost - The API cost incurred for the condensation operation
206+
* @property prevContextTokens - Token count before condensation
207+
* @property newContextTokens - Token count after condensation
208+
* @property summary - The condensed summary that replaced the original context
209+
* @property condenseId - Optional unique identifier for this condensation operation
200210
*/
201-
202211
export const contextCondenseSchema = z.object({
203212
cost: z.number(),
204213
prevContextTokens: z.number(),
@@ -212,21 +221,39 @@ export type ContextCondense = z.infer<typeof contextCondenseSchema>
212221
/**
213222
* ContextTruncation
214223
*
215-
* Used to track sliding window truncation events for the UI.
224+
* Data associated with a sliding window truncation event.
225+
* This is attached to messages with `say: "sliding_window_truncation"` when
226+
* messages are removed from the conversation history to stay within token limits.
227+
*
228+
* Unlike condensation, truncation simply removes older messages without
229+
* summarizing them. This is a faster but less context-preserving approach.
230+
*
231+
* @property truncationId - Unique identifier for this truncation operation
232+
* @property messagesRemoved - Number of conversation messages that were removed
233+
* @property prevContextTokens - Token count before truncation occurred
234+
* @property newContextTokens - Token count after truncation occurred
216235
*/
217-
218236
export const contextTruncationSchema = z.object({
219237
truncationId: z.string(),
220238
messagesRemoved: z.number(),
221239
prevContextTokens: z.number(),
240+
newContextTokens: z.number(),
222241
})
223242

224243
export type ContextTruncation = z.infer<typeof contextTruncationSchema>
225244

226245
/**
227246
* ClineMessage
247+
*
248+
* The main message type used for communication between the extension and webview.
249+
* Messages can either be "ask" (requiring user response) or "say" (informational).
250+
*
251+
* Context Management Fields:
252+
* - `contextCondense`: Present when `say: "condense_context"` and condensation succeeded
253+
* - `contextTruncation`: Present when `say: "sliding_window_truncation"` and truncation occurred
254+
*
255+
* Note: These fields are mutually exclusive - a message will have at most one of them.
228256
*/
229-
230257
export const clineMessageSchema = z.object({
231258
ts: z.number(),
232259
type: z.union([z.literal("ask"), z.literal("say")]),
@@ -239,7 +266,15 @@ export const clineMessageSchema = z.object({
239266
conversationHistoryIndex: z.number().optional(),
240267
checkpoint: z.record(z.string(), z.unknown()).optional(),
241268
progressStatus: toolProgressStatusSchema.optional(),
269+
/**
270+
* Data for successful context condensation.
271+
* Present when `say: "condense_context"` and `partial: false`.
272+
*/
242273
contextCondense: contextCondenseSchema.optional(),
274+
/**
275+
* Data for sliding window truncation.
276+
* Present when `say: "sliding_window_truncation"`.
277+
*/
243278
contextTruncation: contextTruncationSchema.optional(),
244279
isProtected: z.boolean().optional(),
245280
apiProtocol: z.union([z.literal("openai"), z.literal("anthropic")]).optional(),

src/core/context-management/__tests__/context-management.spec.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ import { BaseProvider } from "../../../api/providers/base-provider"
99
import { ApiMessage } from "../../task-persistence/apiMessages"
1010
import * as condenseModule from "../../condense"
1111

12-
import { TOKEN_BUFFER_PERCENTAGE, estimateTokenCount, truncateConversation, manageContext } from "../index"
12+
import {
13+
TOKEN_BUFFER_PERCENTAGE,
14+
estimateTokenCount,
15+
truncateConversation,
16+
manageContext,
17+
willManageContext,
18+
} from "../index"
1319

1420
// Create a mock ApiHandler for testing
1521
class MockApiHandler extends BaseProvider {
@@ -1280,4 +1286,125 @@ describe("Context Management", () => {
12801286
expect(result2.truncationId).toBeDefined()
12811287
})
12821288
})
1289+
1290+
/**
1291+
* Tests for the willManageContext helper function
1292+
*/
1293+
describe("willManageContext", () => {
1294+
it("should return true when context percent exceeds threshold", () => {
1295+
const result = willManageContext({
1296+
totalTokens: 60000,
1297+
contextWindow: 100000, // 60% of context window
1298+
maxTokens: 30000,
1299+
autoCondenseContext: true,
1300+
autoCondenseContextPercent: 50, // 50% threshold
1301+
profileThresholds: {},
1302+
currentProfileId: "default",
1303+
lastMessageTokens: 0,
1304+
})
1305+
expect(result).toBe(true)
1306+
})
1307+
1308+
it("should return false when context percent is below threshold", () => {
1309+
const result = willManageContext({
1310+
totalTokens: 40000,
1311+
contextWindow: 100000, // 40% of context window
1312+
maxTokens: 30000,
1313+
autoCondenseContext: true,
1314+
autoCondenseContextPercent: 50, // 50% threshold
1315+
profileThresholds: {},
1316+
currentProfileId: "default",
1317+
lastMessageTokens: 0,
1318+
})
1319+
expect(result).toBe(false)
1320+
})
1321+
1322+
it("should return true when tokens exceed allowedTokens even if autoCondenseContext is false", () => {
1323+
// allowedTokens = contextWindow * (1 - 0.1) - reservedTokens = 100000 * 0.9 - 30000 = 60000
1324+
const result = willManageContext({
1325+
totalTokens: 60001, // Exceeds allowedTokens
1326+
contextWindow: 100000,
1327+
maxTokens: 30000,
1328+
autoCondenseContext: false, // Even with auto-condense disabled
1329+
autoCondenseContextPercent: 50,
1330+
profileThresholds: {},
1331+
currentProfileId: "default",
1332+
lastMessageTokens: 0,
1333+
})
1334+
expect(result).toBe(true)
1335+
})
1336+
1337+
it("should return false when autoCondenseContext is false and tokens are below allowedTokens", () => {
1338+
// allowedTokens = contextWindow * (1 - 0.1) - reservedTokens = 100000 * 0.9 - 30000 = 60000
1339+
const result = willManageContext({
1340+
totalTokens: 59999, // Below allowedTokens
1341+
contextWindow: 100000,
1342+
maxTokens: 30000,
1343+
autoCondenseContext: false,
1344+
autoCondenseContextPercent: 50, // This shouldn't matter since autoCondenseContext is false
1345+
profileThresholds: {},
1346+
currentProfileId: "default",
1347+
lastMessageTokens: 0,
1348+
})
1349+
expect(result).toBe(false)
1350+
})
1351+
1352+
it("should use profile-specific threshold when available", () => {
1353+
const result = willManageContext({
1354+
totalTokens: 55000,
1355+
contextWindow: 100000, // 55% of context window
1356+
maxTokens: 30000,
1357+
autoCondenseContext: true,
1358+
autoCondenseContextPercent: 80, // Global threshold 80%
1359+
profileThresholds: { "test-profile": 50 }, // Profile threshold 50%
1360+
currentProfileId: "test-profile",
1361+
lastMessageTokens: 0,
1362+
})
1363+
// Should trigger because 55% > 50% (profile threshold)
1364+
expect(result).toBe(true)
1365+
})
1366+
1367+
it("should fall back to global threshold when profile threshold is -1", () => {
1368+
const result = willManageContext({
1369+
totalTokens: 55000,
1370+
contextWindow: 100000, // 55% of context window
1371+
maxTokens: 30000,
1372+
autoCondenseContext: true,
1373+
autoCondenseContextPercent: 80, // Global threshold 80%
1374+
profileThresholds: { "test-profile": -1 }, // Profile uses global
1375+
currentProfileId: "test-profile",
1376+
lastMessageTokens: 0,
1377+
})
1378+
// Should NOT trigger because 55% < 80% (global threshold)
1379+
expect(result).toBe(false)
1380+
})
1381+
1382+
it("should include lastMessageTokens in the calculation", () => {
1383+
// Without lastMessageTokens: 49000 tokens = 49%
1384+
// With lastMessageTokens: 49000 + 2000 = 51000 tokens = 51%
1385+
const resultWithoutLastMessage = willManageContext({
1386+
totalTokens: 49000,
1387+
contextWindow: 100000,
1388+
maxTokens: 30000,
1389+
autoCondenseContext: true,
1390+
autoCondenseContextPercent: 50, // 50% threshold
1391+
profileThresholds: {},
1392+
currentProfileId: "default",
1393+
lastMessageTokens: 0,
1394+
})
1395+
expect(resultWithoutLastMessage).toBe(false)
1396+
1397+
const resultWithLastMessage = willManageContext({
1398+
totalTokens: 49000,
1399+
contextWindow: 100000,
1400+
maxTokens: 30000,
1401+
autoCondenseContext: true,
1402+
autoCondenseContextPercent: 50, // 50% threshold
1403+
profileThresholds: {},
1404+
currentProfileId: "default",
1405+
lastMessageTokens: 2000, // Pushes total to 51%
1406+
})
1407+
expect(resultWithLastMessage).toBe(true)
1408+
})
1409+
})
12831410
})

0 commit comments

Comments
 (0)