-
-
Notifications
You must be signed in to change notification settings - Fork 40.5k
Description
Bug Description
When a conversation includes an assistant message with stopReason === "error" or "aborted" that contained toolCall blocks, subsequent API calls to Anthropic fail with:
HTTP 400 invalid_request_error: messages.110.content.1: unexpected tool_use_id found
in tool_result blocks: toolu_01Khaca7kS2D7fr6zYVubgtF. Each tool_result block must
have a corresponding tool_use block in the previous message.
The session becomes permanently broken — every subsequent message triggers this error until the session is manually reset.
Root Cause Analysis
Primary Bug: transformMessages() in pi-ai drops errored assistants without cleaning up their tool results
File: @mariozechner/pi-ai/dist/providers/transform-messages.js (lines 108–110)
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
continue; // ← assistant dropped, but its toolResult messages are NOT removed
}The second pass of transformMessages() handles orphaned toolCall blocks (assistant messages missing tool results) by inserting synthetic error results. However, it does not handle the reverse case: orphaned toolResult messages whose corresponding assistant was dropped.
What happens step-by-step:
- During a conversation, an assistant response errors or is aborted mid-stream (network issue, rate limit, user cancellation) after it had already emitted
toolCallblocks. - The tools may have already started executing, producing
toolResultmessages in the session transcript. - The assistant message is persisted with
stopReason: "error"(or"aborted"). - On the next turn,
transformMessages()processes the full history:- It encounters the errored assistant and drops it (
continueon line 110). - Critically,
pendingToolCallswas already cleared on line 100 (when handling the previous assistant's pending calls), and the errored assistant's tool calls are never tracked because the function returns early before reaching lines 113–117. - The orphaned
toolResultmessages pass through untouched (lines 120–122).
- It encounters the errored assistant and drops it (
- The Anthropic message converter (
anthropic.js:484–512) groups all consecutivetoolResultmessages into a singleusermessage with multipletool_resultcontent blocks. - The orphaned tool results get merged with the previous (valid) assistant's tool results.
- The Anthropic API sees
tool_resultblocks referencingtool_use_ids that don't exist in the immediately preceding assistant message → HTTP 400.
Concrete example of the message sequence:
messages[N]: assistant A (toolCall id: "X") → KEPT
messages[N+1]: toolResult (toolCallId: "X") → KEPT
messages[N+2]: assistant B (toolCall id: "Y", stopReason: "error") → DROPPED
messages[N+3]: toolResult (toolCallId: "Y") → KEPT (ORPHAN!)
messages[N+4]: assistant C (text response) → KEPT
messages[N+5]: user (new message) → KEPT
After transformMessages:
result[0]: assistant A (tool_use id: "X")
result[1]: toolResult (tool_use_id: "X")
result[2]: toolResult (tool_use_id: "Y") ← ORPHAN, no matching tool_use
result[3]: assistant C
result[4]: user
After Anthropic convertMessages:
API message 0: { role: "assistant", content: [{ type: "tool_use", id: "X", ... }] }
API message 1: { role: "user", content: [
{ type: "tool_result", tool_use_id: "X" }, ← valid
{ type: "tool_result", tool_use_id: "Y" } ← INVALID - no matching tool_use
]}
API message 2: { role: "assistant", content: [...] }
API message 3: { role: "user", content: [...] }
→ HTTP 400: "unexpected tool_use_id found in tool_result blocks: Y"
Secondary Issue: transcript-sanitize extension not registered for agent loop
File: agents/pi-embedded-runner/extensions.js (function buildEmbeddedExtensionPaths)
The transcript-sanitize extension exists and correctly calls repairToolUseResultPairing() on context events. This function (in session-transcript-repair.js) properly handles orphaned tool results by dropping them. However, this extension is never registered in buildEmbeddedExtensionPaths():
export function buildEmbeddedExtensionPaths(params) {
const paths = [];
if (resolveCompactionMode(params.cfg) === "safeguard") {
paths.push(resolvePiExtensionPath("compaction-safeguard"));
}
const pruning = buildContextPruningExtension(params);
if (pruning.additionalExtensionPaths) {
paths.push(...pruning.additionalExtensionPaths);
}
return paths;
// ← transcript-sanitize is NEVER added to paths
}repairToolUseResultPairing only runs once at session load time (via sanitizeSessionHistory in attempt.js:431), not during the agent loop. Even if it ran during the agent loop via the context event, it would execute before transformMessages (which runs inside pi-ai's provider layer), so it wouldn't catch orphans created by the transformMessages drop logic.
Suggested Fix
Fix 1 (in pi-ai): Clean up tool results when dropping errored/aborted assistants
In transform-messages.js, when dropping an errored/aborted assistant, track its tool call IDs and skip following tool results that reference them:
// Track IDs of tool calls from dropped (errored/aborted) assistants
const droppedToolCallIds = new Set();
// In the second pass, when processing assistant messages:
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
// Collect tool call IDs so orphaned results can be skipped
for (const block of assistantMsg.content) {
if (block.type === "toolCall" && block.id) {
droppedToolCallIds.add(block.id);
}
}
continue;
}
// In the toolResult branch:
else if (msg.role === "toolResult") {
if (droppedToolCallIds.has(msg.toolCallId)) {
// Skip orphaned tool result from a dropped assistant
continue;
}
existingToolResultIds.add(msg.toolCallId);
result.push(msg);
}Fix 2 (in openclaw): Register transcript-sanitize as a built-in extension
In extensions.js, always include the transcript-sanitize extension:
export function buildEmbeddedExtensionPaths(params) {
const paths = [];
// Always run transcript sanitization
paths.push(resolvePiExtensionPath("transcript-sanitize"));
if (resolveCompactionMode(params.cfg) === "safeguard") {
paths.push(resolvePiExtensionPath("compaction-safeguard"));
}
const pruning = buildContextPruningExtension(params);
if (pruning.additionalExtensionPaths) {
paths.push(...pruning.additionalExtensionPaths);
}
return paths;
}Note: Fix 2 alone would not fully resolve the issue because transformMessages runs after context event extensions, so orphans created by the drop logic in transformMessages would bypass the repair. Both fixes should be applied.
Environment
- OpenClaw version: 2026.2.3-1
- pi-ai version: 0.51.3
- pi-agent-core version: 0.51.3
- Provider: Anthropic (
claude-opus-4-5) - Platform: macOS (Darwin 25.2.0)
- Context pruning: enabled (
cache-ttl, 1h TTL) - Compaction:
safeguardmode
Steps to Reproduce
- Start a TUI session with an Anthropic model
- Have a conversation that involves tool usage
- Trigger an error/abort during an assistant response that has already emitted tool call blocks (e.g., network interruption, rate limit, or user cancellation mid-tool-execution)
- Send another message in the same session
- Observe the HTTP 400 error on every subsequent message
Workaround
Reset the session (e.g., /reset in the TUI) to clear the corrupted history containing the errored assistant message and its orphaned tool results.