diff --git a/src/agents/pi-extensions/compaction-safeguard.test.ts b/src/agents/pi-extensions/compaction-safeguard.test.ts index b6256554c52..ef294a4f4c9 100644 --- a/src/agents/pi-extensions/compaction-safeguard.test.ts +++ b/src/agents/pi-extensions/compaction-safeguard.test.ts @@ -417,6 +417,48 @@ describe("compaction-safeguard recent-turn preservation", () => { ); }); + it("drops orphaned tool results from preserved assistant turns", () => { + const messages: AgentMessage[] = [ + { role: "user", content: "older ask", timestamp: 1 }, + { + role: "assistant", + content: [{ type: "toolCall", id: "call_old", name: "read", arguments: {} }], + timestamp: 2, + } as unknown as AgentMessage, + { + role: "toolResult", + toolCallId: "call_old", + toolName: "read", + content: [{ type: "text", text: "old result" }], + timestamp: 3, + } as unknown as AgentMessage, + { role: "user", content: "recent ask", timestamp: 4 }, + { + role: "assistant", + content: [{ type: "toolCall", id: "call_recent", name: "read", arguments: {} }], + timestamp: 5, + } as unknown as AgentMessage, + { + role: "toolResult", + toolCallId: "call_recent", + toolName: "read", + content: [{ type: "text", text: "recent result" }], + timestamp: 6, + } as unknown as AgentMessage, + ]; + + const split = splitPreservedRecentTurns({ + messages, + recentTurnsPreserve: 1, + }); + + const summarizableToolResultIds = split.summarizableMessages + .filter((msg) => msg.role === "toolResult") + .map((msg) => (msg as { toolCallId?: unknown }).toolCallId); + expect(summarizableToolResultIds).toContain("call_old"); + expect(summarizableToolResultIds).not.toContain("call_recent"); + }); + it("clamps preserve count into a safe range", () => { expect(resolveRecentTurnsPreserve(undefined)).toBe(3); expect(resolveRecentTurnsPreserve(-1)).toBe(0); diff --git a/src/agents/pi-extensions/compaction-safeguard.ts b/src/agents/pi-extensions/compaction-safeguard.ts index 0c492b26a15..a801fb800f6 100644 --- a/src/agents/pi-extensions/compaction-safeguard.ts +++ b/src/agents/pi-extensions/compaction-safeguard.ts @@ -18,6 +18,7 @@ import { summarizeInStages, } from "../compaction.js"; import { collectTextContentBlocks } from "../content-blocks.js"; +import { repairToolUseResultPairing } from "../session-transcript-repair.js"; import { getCompactionSafeguardRuntime } from "./compaction-safeguard-runtime.js"; const log = createSubsystemLogger("compaction-safeguard"); @@ -222,13 +223,16 @@ function splitPreservedRecentTurns(params: { } const preservedIndexSet = new Set(candidateIndexes); const summarizableMessages = params.messages.filter((_, idx) => !preservedIndexSet.has(idx)); + // Preserving recent assistant turns can orphan downstream toolResult messages. + // Repair pairings here so compaction summarization doesn't trip strict providers. + const repairedSummarizableMessages = repairToolUseResultPairing(summarizableMessages).messages; const preservedMessages = params.messages .filter((_, idx) => preservedIndexSet.has(idx)) .filter((msg) => { const role = (msg as { role?: unknown }).role; return role === "user" || role === "assistant"; }); - return { summarizableMessages, preservedMessages }; + return { summarizableMessages: repairedSummarizableMessages, preservedMessages }; } function formatPreservedTurnsSection(messages: AgentMessage[]): string {