fix: correct type errors and formatting in error/aborted tool_use stripping

- Remove invalid `added.push(strings)` that pushed strings into
  `Array<Extract<AgentMessage, { role: "toolResult" }>>` — stripped
  error/aborted tool calls should not appear in the `added` report
- Fix test assertions: `result.changed` → `result.moved` since
  `ToolUseRepairReport` exposes `moved` (which includes `changed`), not
  `changed` directly
- Fix formatting to pass `oxfmt --check`

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lanclaw 2026-03-17 22:11:32 -07:00 committed by mike2024-dev
parent 08af506916
commit 8c25ba3a0f
2 changed files with 38 additions and 25 deletions

View File

@ -167,7 +167,7 @@ describe("sanitizeToolUseResultPairing", () => {
expect(assistantContent.every((b: { type: string }) => b.type !== "toolCall")).toBe(true);
expect(result.messages[1]?.role).toBe("user");
expect(result.messages).toHaveLength(2);
expect(result.changed).toBe(true);
expect(result.moved).toBe(true);
});
it("strips tool_use blocks from assistant messages with stopReason 'aborted'", () => {
@ -190,7 +190,7 @@ describe("sanitizeToolUseResultPairing", () => {
const assistantContent = (result.messages[0] as { content: { type: string }[] }).content;
expect(assistantContent.every((b: { type: string }) => b.type !== "toolCall")).toBe(true);
expect(result.messages[1]?.role).toBe("user");
expect(result.changed).toBe(true);
expect(result.moved).toBe(true);
});
it("preserves text content alongside stripped tool_use in error messages", () => {
@ -210,7 +210,8 @@ describe("sanitizeToolUseResultPairing", () => {
const result = repairToolUseResultPairing(input);
const assistantContent = (result.messages[0] as { content: { type: string; text?: string }[] }).content;
const assistantContent = (result.messages[0] as { content: { type: string; text?: string }[] })
.content;
expect(assistantContent).toHaveLength(1);
expect(assistantContent[0]?.type).toBe("text");
expect(assistantContent[0]?.text).toBe("Let me try running that...");

View File

@ -348,7 +348,7 @@ export type ToolUseRepairReport = {
export function repairToolUseResultPairing(
messages: AgentMessage[],
options?: ToolUseResultPairingOptions,
_options?: ToolUseResultPairingOptions,
): ToolUseRepairReport {
// Anthropic (and Cloud Code Assist) reject transcripts where assistant tool calls are not
// immediately followed by matching tool results. Session files can end up with results
@ -400,6 +400,36 @@ export function repairToolUseResultPairing(
const assistant = msg as Extract<AgentMessage, { role: "assistant" }>;
// For aborted or errored assistant messages, strip any tool_use/toolCall blocks
// rather than skipping the message entirely. These messages are the most likely
// to contain orphaned tool_use blocks (tool call started but result never generated).
// Previously we skipped them to avoid creating synthetic tool_results for incomplete
// tool calls (see #4597), but skipping preserves the orphaned blocks, which then
// cause permanent session corruption — Anthropic rejects every subsequent request
// with "tool_use ids were found without tool_result blocks" (see #48354).
// The safe fix: strip the tool call blocks entirely instead of trying to pair them.
const stopReason = (assistant as { stopReason?: string }).stopReason;
if (stopReason === "error" || stopReason === "aborted") {
const errorToolCalls = extractToolCallsFromAssistant(assistant);
if (errorToolCalls.length > 0) {
const content = Array.isArray(assistant.content) ? assistant.content : [];
const stripped = content.filter(
(b: { type?: string }) => b && b.type !== "toolCall" && b.type !== "toolUse",
);
changed = true;
out.push({
...assistant,
content:
stripped.length > 0
? stripped
: [{ type: "text" as const, text: "[tool calls from errored turn stripped]" }],
} as typeof assistant);
} else {
out.push(msg);
}
continue;
}
const toolCalls = extractToolCallsFromAssistant(assistant);
if (toolCalls.length === 0) {
out.push(msg);
@ -457,27 +487,9 @@ export function repairToolUseResultPairing(
}
}
// Aborted/errored assistant turns should never synthesize missing tool results, but
// the replay sanitizer can still legitimately retain real tool results for surviving
// tool calls in the same turn after malformed siblings are dropped.
const stopReason = (assistant as { stopReason?: string }).stopReason;
if (stopReason === "error" || stopReason === "aborted") {
out.push(msg);
if (options?.preserveErroredAssistantResults) {
for (const toolCall of toolCalls) {
const result = spanResultsById.get(toolCall.id);
if (!result) {
continue;
}
pushToolResult(result);
}
}
for (const rem of remainder) {
out.push(rem);
}
i = j - 1;
continue;
}
// Note: aborted/errored assistant turns are already handled above (tool_use blocks
// are stripped and the message is emitted with `continue`). This point is only
// reached for normal assistant messages, so no stopReason guard is needed here.
out.push(msg);