Skip to content

Commit 2089b0e

Browse files
committed
fix: handle edge case when all visible messages except first are truncated
- Add nullish coalescing to handle undefined firstKeptVisibleIndex - Add test case for truncating all messages except the first - Fixes issue where marker insertion would duplicate the array
1 parent fec5b40 commit 2089b0e

File tree

2 files changed

+35
-1
lines changed

2 files changed

+35
-1
lines changed

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,39 @@ describe("Non-Destructive Sliding Window Truncation", () => {
389389
expect(result.messagesRemoved).toBe(0)
390390
})
391391

392+
it("should handle truncating all visible messages except first", () => {
393+
// This tests the edge case where visibleIndices[messagesToRemove + 1] would be undefined
394+
// 3 messages total: first is preserved, 2 others can be truncated
395+
const threeMessages: ApiMessage[] = [
396+
{ role: "user", content: "Initial", ts: 1000 },
397+
{ role: "assistant", content: "Response 1", ts: 1100 },
398+
{ role: "user", content: "Message 2", ts: 1200 },
399+
]
400+
401+
// With fracToRemove = 1.0:
402+
// visibleCount = 3
403+
// rawMessagesToRemove = floor((3-1) * 1.0) = 2
404+
// messagesToRemove = 2 (already even)
405+
// This truncates ALL messages except the first
406+
const result = truncateConversation(threeMessages, 1.0, "test-task-id")
407+
408+
expect(result.messagesRemoved).toBe(2)
409+
// Should have 3 original messages + 1 marker = 4
410+
expect(result.messages.length).toBe(4)
411+
412+
// First message should be untouched
413+
expect(result.messages[0].truncationParent).toBeUndefined()
414+
expect(result.messages[0].content).toBe("Initial")
415+
416+
// Messages at indices 1 and 2 should be tagged
417+
expect(result.messages[1].truncationParent).toBe(result.truncationId)
418+
expect(result.messages[2].truncationParent).toBe(result.truncationId)
419+
420+
// Marker should be at the end (index 3)
421+
expect(result.messages[3].isTruncationMarker).toBe(true)
422+
expect(result.messages[3].role).toBe("user")
423+
})
424+
392425
it("should handle empty condenseParent and truncationParent gracefully", () => {
393426
const messagesWithoutTags: ApiMessage[] = [
394427
{ role: "user", content: "Message 1", ts: 1000 },

src/core/context-management/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ export function truncateConversation(messages: ApiMessage[], fracToRemove: numbe
104104

105105
// Find the actual boundary - the index right after the last truncated message
106106
const lastTruncatedVisibleIndex = visibleIndices[messagesToRemove] // Last visible message being truncated
107-
const firstKeptVisibleIndex = visibleIndices[messagesToRemove + 1] // First visible message being kept
107+
// If all visible messages except the first are truncated, insert marker at the end
108+
const firstKeptVisibleIndex = visibleIndices[messagesToRemove + 1] ?? taggedMessages.length
108109

109110
// Insert truncation marker at the actual boundary (between last truncated and first kept)
110111
const firstKeptTs = messages[firstKeptVisibleIndex]?.ts ?? Date.now()

0 commit comments

Comments
 (0)