From ffd7f20b2623098b44ea4c1cd4ca4abd3c9835cc Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Thu, 12 Mar 2026 19:44:00 -0700 Subject: [PATCH] fix: cover compaction safeguard trim restore flow --- CHANGELOG.md | 1 + .../compaction-safeguard.test.ts | 130 ++++++++++++++++++ .../pi-extensions/compaction-safeguard.ts | 2 + 3 files changed, 133 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea5667e5282..4c9c1efc235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -275,6 +275,7 @@ Docs: https://docs.openclaw.ai - Agents/Anthropic replay: drop replayed assistant thinking blocks for native Anthropic and Bedrock Claude providers so persisted follow-up turns no longer fail on stored thinking blocks. (#44843) Thanks @jmcte. - Docs/Brave pricing: escape literal dollar signs in Brave Search cost text so the docs render the free credit and per-request pricing correctly. (#44989) Thanks @keelanfh. - Feishu/file uploads: preserve literal UTF-8 filenames in `im.file.create` so Chinese and other non-ASCII filenames no longer appear percent-encoded in chat. (#34262) Thanks @fabiaodemianyang and @KangShuaiFu. +- Agents/compaction safeguard: trim large kept `toolResult` payloads consistently for budgeting, pruning, and identifier seeding, then restore preserved payloads after prune so oversized safeguard summaries stay stable. (#44133) thanks @SayrWolfridge. ## 2026.3.11 diff --git a/src/agents/pi-extensions/compaction-safeguard.test.ts b/src/agents/pi-extensions/compaction-safeguard.test.ts index 882099f3569..5560d4c0a1d 100644 --- a/src/agents/pi-extensions/compaction-safeguard.test.ts +++ b/src/agents/pi-extensions/compaction-safeguard.test.ts @@ -28,6 +28,8 @@ const mockSummarizeInStages = vi.mocked(compactionModule.summarizeInStages); const { collectToolFailures, formatToolFailuresSection, + trimToolResultsForSummarization, + restoreOriginalToolResultsForKeptMessages, splitPreservedRecentTurns, formatPreservedTurnsSection, buildCompactionStructureInstructions, @@ -45,6 +47,26 @@ const { SAFETY_MARGIN, } = __testing; +function readTextBlocks(message: AgentMessage): string { + const content = (message as { content?: unknown }).content; + if (typeof content === "string") { + return content; + } + if (!Array.isArray(content)) { + return ""; + } + return content + .map((block) => { + if (!block || typeof block !== "object") { + return ""; + } + const text = (block as { text?: unknown }).text; + return typeof text === "string" ? text : ""; + }) + .filter(Boolean) + .join("\n"); +} + function stubSessionManager(): ExtensionContext["sessionManager"] { const stub: ExtensionContext["sessionManager"] = { getCwd: () => "/stub", @@ -234,6 +256,114 @@ describe("compaction-safeguard tool failures", () => { }); }); +describe("compaction-safeguard toolResult trimming", () => { + it("truncates oversized tool results and compacts older entries to stay within budget", () => { + const messages: AgentMessage[] = Array.from({ length: 7 }, (_unused, index) => ({ + role: "toolResult", + toolCallId: `call-${index}`, + toolName: "read", + content: [ + { + type: "text", + text: `head-${index}\n${"x".repeat(25_000)}\ntail-${index}`, + }, + ], + timestamp: index + 1, + })) as AgentMessage[]; + + const trimmed = trimToolResultsForSummarization(messages); + + expect(trimmed.stats.truncatedCount).toBe(7); + expect(trimmed.stats.compactedCount).toBe(1); + expect(readTextBlocks(trimmed.messages[0])).toBe(""); + expect(trimmed.stats.afterChars).toBeLessThan(trimmed.stats.beforeChars); + expect(readTextBlocks(trimmed.messages[6])).toContain("head-6"); + expect(readTextBlocks(trimmed.messages[6])).toContain("[truncated for compaction stability]"); + expect(readTextBlocks(trimmed.messages[6])).toContain("tail-6"); + }); + + it("restores kept tool results after prune for both toolCallId and toolUseId", () => { + const originalMessages: AgentMessage[] = [ + { role: "user", content: "keep these tool results", timestamp: 1 }, + { + role: "toolResult", + toolCallId: "call-1", + toolName: "read", + content: [{ type: "text", text: "original call payload" }], + timestamp: 2, + } as AgentMessage, + { + role: "toolResult", + toolUseId: "use-1", + toolName: "exec", + content: [{ type: "text", text: "original use payload" }], + timestamp: 3, + } as AgentMessage, + ]; + const prunedMessages: AgentMessage[] = [ + originalMessages[0], + { + role: "toolResult", + toolCallId: "call-1", + toolName: "read", + content: [{ type: "text", text: "trimmed call payload" }], + timestamp: 2, + } as AgentMessage, + { + role: "toolResult", + toolUseId: "use-1", + toolName: "exec", + content: [{ type: "text", text: "trimmed use payload" }], + timestamp: 3, + } as AgentMessage, + ]; + + const restored = restoreOriginalToolResultsForKeptMessages({ + prunedMessages, + originalMessages, + }); + + expect(readTextBlocks(restored[1])).toBe("original call payload"); + expect(readTextBlocks(restored[2])).toBe("original use payload"); + }); + + it("extracts identifiers from the trimmed kept payloads after prune restore", () => { + const hiddenIdentifier = "DEADBEEF12345678"; + const restored = restoreOriginalToolResultsForKeptMessages({ + prunedMessages: [ + { role: "user", content: "recent ask", timestamp: 1 }, + { + role: "toolResult", + toolCallId: "call-1", + toolName: "read", + content: [{ type: "text", text: "placeholder" }], + timestamp: 2, + } as AgentMessage, + ], + originalMessages: [ + { role: "user", content: "recent ask", timestamp: 1 }, + { + role: "toolResult", + toolCallId: "call-1", + toolName: "read", + content: [ + { + type: "text", + text: `visible head ${"a".repeat(16_000)}${hiddenIdentifier}${"b".repeat(16_000)} visible tail`, + }, + ], + timestamp: 2, + } as AgentMessage, + ], + }); + + const trimmed = trimToolResultsForSummarization(restored).messages; + const identifierSeedText = trimmed.map((message) => readTextBlocks(message)).join("\n"); + + expect(extractOpaqueIdentifiers(identifierSeedText)).not.toContain(hiddenIdentifier); + }); +}); + describe("computeAdaptiveChunkRatio", () => { const CONTEXT_WINDOW = 200_000; diff --git a/src/agents/pi-extensions/compaction-safeguard.ts b/src/agents/pi-extensions/compaction-safeguard.ts index 6fa27f5eb1d..a8c73f2efcd 100644 --- a/src/agents/pi-extensions/compaction-safeguard.ts +++ b/src/agents/pi-extensions/compaction-safeguard.ts @@ -1218,6 +1218,8 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { export const __testing = { collectToolFailures, formatToolFailuresSection, + trimToolResultsForSummarization, + restoreOriginalToolResultsForKeptMessages, splitPreservedRecentTurns, formatPreservedTurnsSection, buildCompactionStructureInstructions,