From f826a665a217ebf4e2ff11dc6e338cb8f12f1808 Mon Sep 17 00:00:00 2001 From: Yuval Dinodia <102706514+yetval@users.noreply.github.com> Date: Tue, 23 Jun 2026 03:34:33 -0400 Subject: [PATCH] fix(compaction): trim prefix when transcript ends in an oversized tool result (#95860) findCutPoint defaulted cutIndex to the earliest valid cut (cutPoints[0], keep everything) and only moved it forward to a cut point at or after the backward token cursor. When the final entry is a toolResult whose estimate alone meets keepRecentTokens, the cursor stops at that trailing toolResult index, no valid cut point sits at or after it (toolResult entries are not valid cut points), and the default stuck at keep-everything. Compaction then summarized zero messages, so preflight and overflow compaction silently no-op and the session loops on a context it cannot shrink. Default cutIndex to the most recent valid cut before the forward search. When a cut point exists at or after the cursor the search still finds it and behavior is unchanged; only the trailing-tool-result case now keeps the recent tail and summarizes the prefix. --- .../compaction-trailing-toolresult.test.ts | 80 +++++++++++++++++++ .../src/harness/compaction/compaction.ts | 1 + 2 files changed, 81 insertions(+) create mode 100644 packages/agent-core/src/harness/compaction/compaction-trailing-toolresult.test.ts diff --git a/packages/agent-core/src/harness/compaction/compaction-trailing-toolresult.test.ts b/packages/agent-core/src/harness/compaction/compaction-trailing-toolresult.test.ts new file mode 100644 index 00000000000..4e861f291f1 --- /dev/null +++ b/packages/agent-core/src/harness/compaction/compaction-trailing-toolresult.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vitest"; +import type { AgentMessage } from "../../types.js"; +import type { SessionTreeEntry } from "../types.js"; +import { estimateTokens, findCutPoint } from "./compaction.js"; + +const KEEP_RECENT_TOKENS = 20000; +const LARGE_TOOL_OUTPUT = "x".repeat(120000); + +function userText(text: string, timestamp: number): AgentMessage { + return { role: "user", content: [{ type: "text", text }], timestamp }; +} + +function assistantText(text: string, timestamp: number): AgentMessage { + return { + role: "assistant", + content: [{ type: "text", text }], + api: "anthropic-messages", + provider: "anthropic", + model: "claude-fable-5", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp, + }; +} + +function toolResultText(text: string, timestamp: number): AgentMessage { + return { + role: "toolResult", + toolCallId: "call-1", + toolName: "bash", + content: [{ type: "text", text }], + isError: false, + timestamp, + }; +} + +function messageEntry(message: AgentMessage, index: number): SessionTreeEntry { + return { + type: "message", + id: `entry-${index}`, + parentId: index === 0 ? null : `entry-${index - 1}`, + timestamp: new Date(message.timestamp).toISOString(), + message, + }; +} + +function buildTranscript(): SessionTreeEntry[] { + const messages: AgentMessage[] = [ + userText("start of the conversation", 1), + assistantText("first reply", 2), + userText("please run the command", 3), + assistantText("running it now", 4), + toolResultText(LARGE_TOOL_OUTPUT, 5), + ]; + return messages.map((message, index) => messageEntry(message, index)); +} + +describe("findCutPoint with a trailing oversized tool result", () => { + it("counts the final tool result as larger than the keep budget", () => { + const trailing = toolResultText(LARGE_TOOL_OUTPUT, 5); + + expect(estimateTokens(trailing)).toBeGreaterThanOrEqual(KEEP_RECENT_TOKENS); + }); + + it("trims the prefix instead of keeping the whole transcript", () => { + const entries = buildTranscript(); + + const result = findCutPoint(entries, 0, entries.length, KEEP_RECENT_TOKENS); + + expect(result.firstKeptEntryIndex).toBeGreaterThan(0); + expect(result.firstKeptEntryIndex).toBe(3); + }); +}); diff --git a/packages/agent-core/src/harness/compaction/compaction.ts b/packages/agent-core/src/harness/compaction/compaction.ts index 26e86a3f842..479f90a32cf 100644 --- a/packages/agent-core/src/harness/compaction/compaction.ts +++ b/packages/agent-core/src/harness/compaction/compaction.ts @@ -407,6 +407,7 @@ export function findCutPoint( const messageTokens = estimateTokens(entry.message); accumulatedTokens += messageTokens; if (accumulatedTokens >= keepRecentTokens) { + cutIndex = cutPoints[cutPoints.length - 1]; for (const cutPoint of cutPoints) { if (cutPoint >= i) { cutIndex = cutPoint;