Skip to content

Comments

fix: drop errored/aborted assistant tool pairs in transcript repair#9416

Open
xandorklein wants to merge 1 commit intoopenclaw:mainfrom
xandorklein:fix/drop-errored-assistant-tool-pairs
Open

fix: drop errored/aborted assistant tool pairs in transcript repair#9416
xandorklein wants to merge 1 commit intoopenclaw:mainfrom
xandorklein:fix/drop-errored-assistant-tool-pairs

Conversation

@xandorklein
Copy link

@xandorklein xandorklein commented Feb 5, 2026

Problem

When an assistant message with tool calls errors or aborts during execution, the message is persisted with stopReason: "error" (or "aborted") alongside its toolResult messages.

On subsequent turns, repairToolUseResultPairing() correctly pairs these tool results with the errored assistant. However, pi-ai's transformMessages() then strips the errored assistant (it skips messages with stopReason === "error" || "aborted") but keeps the orphaned tool results.

Those orphaned tool_result blocks get grouped with the preceding assistant's tool_use blocks in the API request, causing:

HTTP 400 invalid_request_error: unexpected tool_use_id found in tool_result blocks

This is a session-killing error — once triggered, every subsequent message fails with the same 400 until the errored messages are manually removed from the session file.

Fix

When repairToolUseResultPairing() encounters an assistant message with stopReason === "error" || "aborted" that has tool calls, it now drops both the assistant message and its associated tool results. Non-tool messages in between (remainder) are preserved.

This runs before transformMessages(), so the orphaned blocks never reach the API.

Test plan

  • 4 new test cases:
    • drops errored assistant with tool calls and its tool results
    • drops aborted assistant with tool calls and its tool results
    • keeps errored assistant without tool calls (no change to existing behavior)
    • preserves remainder messages when dropping errored assistant
  • All 10 existing + new tests pass

Note

This is also a bug in pi-ai's transformMessages() — when stripping errored/aborted assistant messages, it should also strip their associated tool results. This fix handles it at the openclaw layer as a defense-in-depth measure.

Greptile Overview

Greptile Summary

This PR updates transcript repair (repairToolUseResultPairing) to detect assistant messages that stopped with stopReason: "error" | "aborted" and contain tool calls, and then drop that assistant turn along with its associated tool results. This prevents downstream message transformation (in pi-ai) from stripping the assistant while leaving orphaned tool results, which can cause Anthropic-compatible APIs to reject requests due to unexpected tool result IDs.

Tests were added to cover dropping errored/aborted tool-call assistants, preserving errored assistants without tool calls, and ensuring non-tool “remainder” messages are preserved when the errored span is removed.

Confidence Score: 4/5

  • This PR is close to safe to merge, with one correctness concern around duplicate toolResult handling in the new drop branch.
  • Core change is narrowly scoped and well-covered by new tests, but the new branch drops tool results without recording their IDs in the global duplicate tracker, which can let later duplicates through in transcripts that contain repeated tool result IDs.
  • src/agents/session-transcript-repair.ts

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

pi-ai's transformMessages() strips errored/aborted assistant messages but
keeps their tool results, orphaning tool_result blocks and causing Anthropic
API rejections ('unexpected tool_use_id found in tool_result blocks').

Strip errored/aborted assistants along with their matched tool results in
repairToolUseResultPairing() before messages reach transformMessages().
@openclaw-barnacle openclaw-barnacle bot added the agents Agent runtime and tooling label Feb 5, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +272 to +280
// Drop errored/aborted assistants and their tool results entirely.
if (isErroredAssistant) {
changed = true;
droppedOrphanCount += spanResultsById.size;
for (const rem of remainder) {
out.push(rem);
}
i = j - 1;
continue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping tool results can undercount duplicates

When isErroredAssistant is true, this branch adds spanResultsById.size to droppedOrphanCount but does not update seenToolResultIds. That means any of these dropped tool results may be seen again later in the transcript and treated as “not duplicate”, allowing a later duplicate to survive when it should be dropped. If seenToolResultIds is intended to track all tool result IDs encountered (even dropped ones), you should add these IDs to seenToolResultIds here (or otherwise ensure later duplicates are still dropped).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/session-transcript-repair.ts
Line: 272:280

Comment:
**Dropping tool results can undercount duplicates**

When `isErroredAssistant` is true, this branch adds `spanResultsById.size` to `droppedOrphanCount` but does **not** update `seenToolResultIds`. That means any of these dropped tool results may be seen again later in the transcript and treated as “not duplicate”, allowing a later duplicate to survive when it should be dropped. If `seenToolResultIds` is intended to track *all* tool result IDs encountered (even dropped ones), you should add these IDs to `seenToolResultIds` here (or otherwise ensure later duplicates are still dropped).

How can I resolve this? If you propose a fix, please make it concise.

hongw pushed a commit to hongw/openclaw that referenced this pull request Feb 10, 2026
…repair

Port of PR openclaw#9416 - defense against pi-ai transformMessages() bug.

When an assistant message with tool calls errors or aborts during execution,
the message is persisted with stopReason: "error" (or "aborted") alongside
its toolResult messages.

On subsequent turns, repairToolUseResultPairing() correctly pairs these tool
results with the errored assistant. However, pi-ai's transformMessages() then
strips the errored assistant (it skips messages with stopReason === "error"
|| "aborted") but keeps the orphaned tool results.

Those orphaned tool_result blocks get grouped with the preceding assistant's
tool_use blocks in the API request, causing:

  HTTP 400 invalid_request_error: unexpected tool_use_id found in tool_result blocks

This is a session-killing error — once triggered, every subsequent message
fails with the same 400 until the errored messages are manually removed.

Fix: When repairToolUseResultPairing() encounters an assistant message with
stopReason === "error" || "aborted" that has tool calls, it now drops both
the assistant message and its associated tool results. Non-tool messages in
between (remainder) are preserved.

This runs before transformMessages(), so the orphaned blocks never reach the API.

Fixes openclaw#6158
See also: openclaw#10932, openclaw#12112
hongw pushed a commit to hongw/openclaw that referenced this pull request Feb 15, 2026
Cherry-picked from upstream OpenClaw:
- PR openclaw#9416: drop errored/aborted assistant tool pairs in transcript repair
- PR openclaw#12487: strip orphaned tool_result when tool_use is sanitized on retry
- PR openclaw#8243: repair orphaned tool_use blocks on session load

Fixes session-killing errors caused by orphaned tool_use/tool_result blocks.
When these orphaned blocks reach the API, they cause permanent HTTP 400 errors
that break every subsequent message in the session.
@openclaw-barnacle
Copy link

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Feb 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant