Skip to content

Commit 2ca7e3b

Browse files
committed
feat: implement non-destructive sliding window truncation
- Add truncationId, truncationParent, and isTruncationMarker fields to ApiMessage type - Refactor truncateConversation() to tag messages instead of deleting them - Update getEffectiveApiHistory() to filter messages by truncationParent - Enhance cleanupAfterTruncation() to handle orphaned truncation tags - Add sliding_window_truncation ClineMessage type and ContextTruncation schema - Create UI events when truncation occurs - Sync truncation marker removal during rewind operations - Add comprehensive test suite with 45 passing tests This makes sliding window truncation rewindable by preserving messages with tags, mirroring the non-destructive approach used for condensing.
1 parent 70825ff commit 2ca7e3b

File tree

9 files changed

+1037
-101
lines changed

9 files changed

+1037
-101
lines changed

packages/types/src/message.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export const clineSays = [
175175
"diff_error",
176176
"condense_context",
177177
"condense_context_error",
178+
"sliding_window_truncation",
178179
"codebase_search_result",
179180
"user_edit_todos",
180181
] as const
@@ -208,6 +209,20 @@ export const contextCondenseSchema = z.object({
208209

209210
export type ContextCondense = z.infer<typeof contextCondenseSchema>
210211

212+
/**
213+
* ContextTruncation
214+
*
215+
* Used to track sliding window truncation events for the UI.
216+
*/
217+
218+
export const contextTruncationSchema = z.object({
219+
truncationId: z.string(),
220+
messagesRemoved: z.number(),
221+
prevContextTokens: z.number(),
222+
})
223+
224+
export type ContextTruncation = z.infer<typeof contextTruncationSchema>
225+
211226
/**
212227
* ClineMessage
213228
*/
@@ -225,6 +240,7 @@ export const clineMessageSchema = z.object({
225240
checkpoint: z.record(z.string(), z.unknown()).optional(),
226241
progressStatus: toolProgressStatusSchema.optional(),
227242
contextCondense: contextCondenseSchema.optional(),
243+
contextTruncation: contextTruncationSchema.optional(),
228244
isProtected: z.boolean().optional(),
229245
apiProtocol: z.union([z.literal("openai"), z.literal("anthropic")]).optional(),
230246
isAnswered: z.boolean().optional(),

src/core/condense/index.ts

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -370,59 +370,103 @@ export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[
370370
* Filters the API conversation history to get the "effective" messages to send to the API.
371371
* Messages with a condenseParent that points to an existing summary are filtered out,
372372
* as they have been replaced by that summary.
373+
* Messages with a truncationParent that points to an existing truncation marker are also filtered out,
374+
* as they have been hidden by sliding window truncation.
373375
*
374-
* This allows non-destructive condensing where messages are tagged but not deleted,
375-
* enabling accurate rewind operations while still sending condensed history to the API.
376+
* This allows non-destructive condensing and truncation where messages are tagged but not deleted,
377+
* enabling accurate rewind operations while still sending condensed/truncated history to the API.
376378
*
377379
* @param messages - The full API conversation history including tagged messages
378380
* @returns The filtered history that should be sent to the API
379381
*/
380382
export function getEffectiveApiHistory(messages: ApiMessage[]): ApiMessage[] {
381383
// Collect all condenseIds of summaries that exist in the current history
382384
const existingSummaryIds = new Set<string>()
385+
// Collect all truncationIds of truncation markers that exist in the current history
386+
const existingTruncationIds = new Set<string>()
387+
383388
for (const msg of messages) {
384389
if (msg.isSummary && msg.condenseId) {
385390
existingSummaryIds.add(msg.condenseId)
386391
}
392+
if (msg.isTruncationMarker && msg.truncationId) {
393+
existingTruncationIds.add(msg.truncationId)
394+
}
387395
}
388396

389397
// Filter out messages whose condenseParent points to an existing summary
390-
// Messages with orphaned condenseParent (summary was deleted) are included
398+
// or whose truncationParent points to an existing truncation marker.
399+
// Messages with orphaned parents (summary/marker was deleted) are included
391400
return messages.filter((msg) => {
392-
if (!msg.condenseParent) {
393-
return true // No parent, always include
401+
// Filter out condensed messages if their summary exists
402+
if (msg.condenseParent && existingSummaryIds.has(msg.condenseParent)) {
403+
return false
404+
}
405+
// Filter out truncated messages if their truncation marker exists
406+
if (msg.truncationParent && existingTruncationIds.has(msg.truncationParent)) {
407+
return false
394408
}
395-
// Include if the parent summary no longer exists (was deleted by rewind)
396-
return !existingSummaryIds.has(msg.condenseParent)
409+
return true
397410
})
398411
}
399412

400413
/**
401-
* Cleans up orphaned condenseParent references after a truncation operation (rewind/delete).
402-
* When a summary message is deleted, messages that were tagged with its condenseId
403-
* should have their condenseParent cleared so they become active again.
414+
* Cleans up orphaned condenseParent and truncationParent references after a truncation operation (rewind/delete).
415+
* When a summary message or truncation marker is deleted, messages that were tagged with its ID
416+
* should have their parent reference cleared so they become active again.
404417
*
405418
* This function should be called after any operation that truncates the API history
406-
* to ensure messages are properly restored when their summary is deleted.
419+
* to ensure messages are properly restored when their summary or truncation marker is deleted.
407420
*
408421
* @param messages - The API conversation history after truncation
409-
* @returns The cleaned history with orphaned condenseParent fields cleared
422+
* @returns The cleaned history with orphaned condenseParent and truncationParent fields cleared
410423
*/
411424
export function cleanupAfterTruncation(messages: ApiMessage[]): ApiMessage[] {
412425
// Collect all condenseIds of summaries that still exist
413426
const existingSummaryIds = new Set<string>()
427+
// Collect all truncationIds of truncation markers that still exist
428+
const existingTruncationIds = new Set<string>()
429+
414430
for (const msg of messages) {
415431
if (msg.isSummary && msg.condenseId) {
416432
existingSummaryIds.add(msg.condenseId)
417433
}
434+
if (msg.isTruncationMarker && msg.truncationId) {
435+
existingTruncationIds.add(msg.truncationId)
436+
}
418437
}
419438

420-
// Clear condenseParent for messages whose summary was deleted
439+
// Clear orphaned parent references for messages whose summary or truncation marker was deleted
421440
return messages.map((msg) => {
441+
let needsUpdate = false
442+
const updates: Partial<ApiMessage> = {}
443+
444+
// Check for orphaned condenseParent
422445
if (msg.condenseParent && !existingSummaryIds.has(msg.condenseParent)) {
423-
// Summary was deleted, restore this message by clearing the parent reference
424-
const { condenseParent, ...rest } = msg
425-
return rest as ApiMessage
446+
needsUpdate = true
447+
}
448+
449+
// Check for orphaned truncationParent
450+
if (msg.truncationParent && !existingTruncationIds.has(msg.truncationParent)) {
451+
needsUpdate = true
452+
}
453+
454+
if (needsUpdate) {
455+
// Create a new object without orphaned parent references
456+
const { condenseParent, truncationParent, ...rest } = msg
457+
const result: ApiMessage = rest as ApiMessage
458+
459+
// Keep condenseParent if its summary still exists
460+
if (condenseParent && existingSummaryIds.has(condenseParent)) {
461+
result.condenseParent = condenseParent
462+
}
463+
464+
// Keep truncationParent if its truncation marker still exists
465+
if (truncationParent && existingTruncationIds.has(truncationParent)) {
466+
result.truncationParent = truncationParent
467+
}
468+
469+
return result
426470
}
427471
return msg
428472
})

0 commit comments

Comments
 (0)