mirror of https://github.com/openclaw/openclaw.git
fix: cover compaction safeguard trim restore flow
This commit is contained in:
parent
c87b42fe76
commit
ffd7f20b26
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1218,6 +1218,8 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
|||
export const __testing = {
|
||||
collectToolFailures,
|
||||
formatToolFailuresSection,
|
||||
trimToolResultsForSummarization,
|
||||
restoreOriginalToolResultsForKeptMessages,
|
||||
splitPreservedRecentTurns,
|
||||
formatPreservedTurnsSection,
|
||||
buildCompactionStructureInstructions,
|
||||
|
|
|
|||
Loading…
Reference in New Issue