From 2d1f4af67ab45c09157e2fbf1aa675c7039fcbab Mon Sep 17 00:00:00 2001 From: Shakker Date: Sun, 12 Apr 2026 03:35:25 +0100 Subject: [PATCH] fix: preserve thinking turns during transcript repair --- src/agents/session-transcript-repair.test.ts | 34 ++++++++++++++++++++ src/agents/session-transcript-repair.ts | 16 +++++++++ 2 files changed, 50 insertions(+) diff --git a/src/agents/session-transcript-repair.test.ts b/src/agents/session-transcript-repair.test.ts index 3fa01fb1de0..3f460c40d80 100644 --- a/src/agents/session-transcript-repair.test.ts +++ b/src/agents/session-transcript-repair.test.ts @@ -380,6 +380,40 @@ describe("sanitizeToolCallInputs", () => { expect(types).toEqual(["text", "toolUse"]); }); + it("preserves assistant turns that include thinking blocks", () => { + const input = castAgentMessages([ + { + role: "assistant", + content: [ + { + type: "thinking", + thinking: "Let me check the gateway config.", + thinkingSignature: "sig_gateway", + }, + { + type: "toolCall", + id: "call_gateway", + name: "gateway", + arguments: { + action: "config.get", + path: "channels.telegram", + }, + }, + ], + }, + ]); + + const out = sanitizeToolCallInputs(input, { allowedToolNames: ["read"] }); + + expect(out).toBe(input); + const assistant = out[0] as Extract; + const types = Array.isArray(assistant.content) + ? assistant.content.map((block) => (block as { type?: unknown }).type) + : []; + expect(types).toEqual(["thinking", "toolCall"]); + expect((assistant.content?.[1] as { name?: unknown })?.name).toBe("gateway"); + }); + it.each([ { name: "trims leading whitespace from tool names", diff --git a/src/agents/session-transcript-repair.ts b/src/agents/session-transcript-repair.ts index dcb8661f742..044fa9731e1 100644 --- a/src/agents/session-transcript-repair.ts +++ b/src/agents/session-transcript-repair.ts @@ -17,6 +17,14 @@ type RawToolCallBlock = { arguments?: unknown; }; +function isThinkingLikeBlock(block: unknown): boolean { + if (!block || typeof block !== "object") { + return false; + } + const type = (block as { type?: unknown }).type; + return type === "thinking" || type === "redacted_thinking"; +} + function isRawToolCallBlock(block: unknown): block is RawToolCallBlock { if (!block || typeof block !== "object") { return false; @@ -239,6 +247,14 @@ export function repairToolCallInputs( continue; } + // Preserve provider-owned thinking turns verbatim. Anthropic replays can + // reject any historical assistant turn whose signed thinking block no + // longer matches the original response, including sibling tool calls. + if (msg.content.some((block) => isThinkingLikeBlock(block))) { + out.push(msg); + continue; + } + const nextContent: typeof msg.content = []; let droppedInMessage = 0; let messageChanged = false;