Skip to content

Commit fd09d2e

Browse files
committed
fix(compaction): anchor forced manual boundary off trailing tool results
1 parent 3aecc4e commit fd09d2e

2 files changed

Lines changed: 86 additions & 2 deletions

File tree

packages/agent-core/src/harness/compaction/compaction.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,84 @@ describe("prepareCompaction", () => {
125125
});
126126
});
127127

128+
it("anchors a forced boundary on the assistant tool call, not a trailing tool result", () => {
129+
const entries: SessionTreeEntry[] = [
130+
{
131+
type: "message",
132+
id: "user-1",
133+
parentId: null,
134+
timestamp: "2026-06-17T08:45:00.000Z",
135+
message: { role: "user", content: "Read the notes file.", timestamp: 1 },
136+
},
137+
{
138+
type: "message",
139+
id: "assistant-1",
140+
parentId: "user-1",
141+
timestamp: "2026-06-17T08:45:10.000Z",
142+
message: {
143+
role: "assistant",
144+
content: [
145+
{ type: "toolCall", id: "call-1", name: "read_file", arguments: { path: "notes.md" } },
146+
],
147+
api: "openai-responses",
148+
provider: "openai",
149+
model: "gpt-test",
150+
usage: {
151+
input: 625,
152+
output: 6,
153+
cacheRead: 172_928,
154+
cacheWrite: 0,
155+
totalTokens: 173_559,
156+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
157+
},
158+
stopReason: "toolUse",
159+
timestamp: 2,
160+
},
161+
},
162+
{
163+
type: "message",
164+
id: "tool-1",
165+
parentId: "assistant-1",
166+
timestamp: "2026-06-17T08:45:11.000Z",
167+
message: {
168+
role: "toolResult",
169+
toolCallId: "call-1",
170+
toolName: "read_file",
171+
content: [{ type: "text", text: "notes body" }],
172+
isError: false,
173+
timestamp: 3,
174+
},
175+
},
176+
];
177+
178+
const preparation = prepareCompaction(entries, DEFAULT_COMPACTION_SETTINGS, { force: true });
179+
180+
// Anchor must be the assistant that owns the tool call, never the trailing
181+
// tool result, or the rebuilt context would replay an orphaned tool result.
182+
expect(preparation).toEqual({
183+
ok: true,
184+
value: expect.objectContaining({ firstKeptEntryId: "assistant-1" }),
185+
});
186+
187+
const compactedContext = buildSessionContext([
188+
...entries,
189+
{
190+
type: "compaction",
191+
id: "compaction-1",
192+
parentId: "tool-1",
193+
timestamp: "2026-06-17T08:45:20.000Z",
194+
summary: "Checkpoint of the file read.",
195+
firstKeptEntryId: "assistant-1",
196+
tokensBefore: 173_559,
197+
},
198+
]);
199+
expect(compactedContext.messages.map((message) => message.role)).toEqual([
200+
"compactionSummary",
201+
"assistant",
202+
"toolResult",
203+
]);
204+
});
205+
128206
it("shows why the old empty-summary compaction replayed the whole transcript", () => {
129207
const entries: SessionTreeEntry[] = [
130208
{

packages/agent-core/src/harness/compaction/compaction.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -701,14 +701,20 @@ export function prepareCompaction(
701701
forcedMessagesToSummarize.push(msg);
702702
}
703703
}
704-
if (forcedMessagesToSummarize.length > 0) {
704+
// Anchor the kept tail on the last valid cut point, not the raw final entry.
705+
// findValidCutPoints excludes tool results, so a forced boundary that is not
706+
// collapsed to summary-only later never keeps an orphaned tool result.
707+
const forcedCutPoints = findValidCutPoints(pathEntries, boundaryStart, boundaryEnd);
708+
const forcedKeepIndex =
709+
forcedCutPoints.length > 0 ? forcedCutPoints[forcedCutPoints.length - 1] : -1;
710+
if (forcedMessagesToSummarize.length > 0 && forcedKeepIndex >= 0) {
705711
const forcedFileOps = extractFileOperations(
706712
forcedMessagesToSummarize,
707713
pathEntries,
708714
prevCompactionIndex,
709715
);
710716
return ok({
711-
firstKeptEntryId: pathEntries[boundaryEnd - 1].id,
717+
firstKeptEntryId: pathEntries[forcedKeepIndex].id,
712718
messagesToSummarize: forcedMessagesToSummarize,
713719
turnPrefixMessages: [],
714720
isSplitTurn: false,

0 commit comments

Comments
 (0)