Skip to content

Comments

fix(agents): skip extracting tool calls from errored assistant turns#1859

Closed
zerone0x wants to merge 1 commit intoopenclaw:mainfrom
zerone0x:fix/transcript-repair-skip-errored-turns
Closed

fix(agents): skip extracting tool calls from errored assistant turns#1859
zerone0x wants to merge 1 commit intoopenclaw:mainfrom
zerone0x:fix/transcript-repair-skip-errored-turns

Conversation

@zerone0x
Copy link
Contributor

@zerone0x zerone0x commented Jan 25, 2026

Summary

Fixes #1826

When an assistant turn has stopReason: "error" (terminated/interrupted), the tool calls within it were never completed. The transcript repair logic was incorrectly inserting synthetic tool_result messages for these incomplete tool calls, causing Anthropic's API to reject all subsequent requests with "unexpected tool_use_id" errors, permanently breaking the session.

Changes

  • Modified extractToolCallsFromAssistant() in session-transcript-repair.ts to return early (empty array) when the assistant message has stopReason: "error"
  • Added test case covering the errored/terminated assistant turn scenario

Test Plan

  • Run pnpm test src/agents/session-transcript-repair.test.ts - all 5 tests pass
  • Full test suite passes (4590 passed, 1 unrelated timeout)
  • Lint passes
  • Build passes

🤖 Generated with Claude Code (issue-hunter-pro)

Greptile Overview

Greptile Summary

This PR updates the session transcript repair logic to avoid extracting tool calls from assistant turns that ended with stopReason: "error", preventing insertion of synthetic toolResult messages for tool calls that were never actually executed (which can cause downstream API rejections due to unexpected tool IDs). It also adds a regression test covering an errored/terminated assistant turn containing an incomplete tool call (with partialJson) and verifies no synthetic results are inserted.

These changes fit into src/agents/session-transcript-repair.ts’s role as a sanitizer that reorders/deduplicates tool results and inserts synthetic missing results to satisfy strict provider transcript rules; the new guard narrows that behavior so it doesn’t manufacture results for interrupted turns where tool execution never occurred.

Confidence Score: 4/5

  • This PR is likely safe to merge and should prevent a known session-breaking failure mode.
  • The change is small and localized (an early return when stopReason === "error"), and the added regression test exercises the reported failure scenario. The main remaining risk is behavioral: skipping tool-call extraction on errored turns may leave other providers’ transcripts inconsistent if they still require results, but for the described Anthropic-compatible behavior this is the correct tradeoff.
  • src/agents/session-transcript-repair.ts (the stopReason handling and assumptions about message shapes)

When an assistant turn has stopReason: "error", the tool calls within
it were never completed (often contain partialJson). Inserting synthetic
tool_results for these caused Anthropic's API to reject requests with
"unexpected tool_use_id" errors, permanently breaking the session.

Now we skip extracting tool calls from assistant messages with
stopReason: "error", preventing the broken synthetic results.

Fixes openclaw#1826

Co-Authored-By: Claude <[email protected]>
@csaftoiu
Copy link

  ⎿  Added 3 lines
      1  function extractToolCallsFromAssistant(msg) {
      2 +    const stopReason = msg.stopReason;
      3 +    if (stopReason === "error")
      4 +        return [];
      5      const content = msg.content;
      6      if (!Array.isArray(content))
      7          return [];

● Done! The fix is applied. Now restart the gateway to test it:

This fix worked, plz merge :)

@sfo2001
Copy link
Contributor

sfo2001 commented Jan 31, 2026

LGTM! Good fix with clear comments explaining the edge case. The early return for errored turns prevents the transcript repair from inserting synthetic tool_results that would break the session. In current code no stopReason check, so PR is still valid.

@adam91holt
Copy link
Contributor

🔍 Overlapping PRs Detected

This PR appears to overlap with 22 other open PRs addressing tool call/tool_result pairing and sanitization:

Similarity PR Author Title
94.1% #4598 @aisling404 fix(agents): skip tool extraction for aborted/errored assistant messag
93.1% #4516 @chesterbella fix: drop errored assistant tool calls and their orphan tool_results
92.8% #4476 @kira-ariaki fix: skip tool calls from aborted assistant messages in transcript rep
92.1% #4844 @lailoo fix(agents): skip error/aborted assistant messages in transcript repai
88.9% #3125 @snejati86 fix: prevent orphan tool_result errors from streaming failures
88.7% #2253 @Zedit42 fix: sanitize incomplete tool calls with partialJson
88.2% #3194 @koriyoshi2041 fix: skip incomplete tool calls in transcript repair [AI-assisted]
88.1% #3880 @SalimBinYousuf1 fix: drop assistant messages with stopReason 'error' to avoid orphanin
86.9% #3362 @samhotchkiss fix: auto-repair and retry on orphan tool_result errors
85.2% #4700 @marcelomar21 fix: deduplicate tool_use IDs and enable sanitization for Anthropic
84.4% #4009 @drag88 fix(agent): sanitize messages after orphan user repair
83.7% #5557 @NSEvent fix(session): strip malformed tool_use blocks to prevent session corru
83.2% #3622 @mickobizzle fix(agents): drop orphan tool results
83.1% #2557 @steve-rodri fix(agents): preserve tool call/result pairing in history limiting
81.4% #4852 @lailoo fix(agents): sanitize tool pairing after compaction and history trunca
81.3% #3707 @bheemreddy181 fix: repair unpaired tool calls when loading sessions
81.1% #3565 @kiranjd fix(sessions): truncate at incomplete tool calls instead of synthetic
81.0% #5032 @shayan919293 fix: re-run sanitization after limitHistoryTurns to fix orphaned tool
80.8% #2213 @manzienkog fix: normalize toolCall arguments to prevent Anthropic API rejection
78.9% #5482 @bsmithelion-arcadia fix(session): normalize tool call blocks for cross-provider compatibil
77.8% #3647 @nhangen fix: sanitize tool arguments in session history
77.1% #4719 @bsmithelion-arcadia fix(session): normalize tool call blocks for cross-provider compatibil

Similarity scores computed using Voyage AI embeddings (cosine similarity) on standardized PR summaries.

@bheemreddy181
Copy link

Happy to close mine :)

@aisling404
Copy link
Contributor

Hey! Just flagging that #4598 covers both stopReason: "error" (like this PR) and "aborted" (like #4476) in a single fix with tests for each case.

Might help consolidate the 22+ overlapping PRs if maintainers want a unified approach!

(My CI is currently blocked by the TypeScript errors on main - not related to my changes.)

@sfo2001
Copy link
Contributor

sfo2001 commented Feb 2, 2026

Following up on @adam91holt's excellent overlap detection, I askeded Claude Code to investigated the current state of main and the 22+ overlapping PRs.

Current State in main

Checked src/agents/session-transcript-repair.ts (lines 8-34) - the extractToolCallsFromAssistant() function has no stopReason check. The following gaps exist:

Issue Status in main Key PRs
Skip extraction for stopReason: "error" Missing #1859, #4598, #4516, #4476, #4844, #3880
Skip extraction for stopReason: "aborted" Missing #4598, #4476, #4844
Strip malformed tool_use blocks (partialJson, missing id) Missing #5557, #2253, #3194
Re-run sanitization after limitHistoryTurns Missing #5032, #4852
Re-run sanitization after compaction Partial #5032, #4852

Root Cause Chain

errored/aborted assistant turn
→ tool_use blocks remain in transcript
→ extractToolCallsFromAssistant() extracts them (no stopReason check)
→ makeMissingToolResult() creates synthetic results
→ API rejects: "unexpected tool_use_id" → session permanently broken

Consolidation Recommendation

Rather than merging 22 PRs piecemeal, three PRs together would provide comprehensive coverage:

  1. fix(agents): skip tool extraction for aborted/errored assistant messages #4598 (@aisling404) - Covers both error and aborted stopReasons with tests for each
  2. fix(session): strip malformed tool_use blocks to prevent session corruption #5557 (@NSEvent) - Strips malformed tool_use blocks before pairing repair runs
  3. fix: re-run sanitization after limitHistoryTurns to fix orphaned tool results #5032 (@shayan919293) - Re-runs sanitization after limitHistoryTurns and compaction

This would resolve the related issues: #1826, #4597, #4650, #5497, #5481, #5430, #5518, and likely others.

Related but Separate

Issue #6118 is a separate loading bug - these PRs fix the sanitization logic itself.


Analysis performed with Claude Code

@adam91holt
Copy link
Contributor

🔍 Overlapping PRs Detected

This PR appears to overlap with 27 other open PRs addressing agents, memory:

Similarity PR Author Title
95.2% #3054 @dominicnunez fix: skip synthetic toolResult for aborted/errored assistant messages
94.3% #6831 @zerone0x fix: skip synthetic tool_result for aborted assistant messages
93.1% #4476 @kira-ariaki fix: skip tool calls from aborted assistant messages in transcript repair
92.9% #4516 @chesterbella fix: drop errored assistant tool calls and their orphan tool_results
91.2% #5822 @Alfa-ccvs-tech fix: skip incomplete tool calls with partialJson in session repair
91.0% #3125 @snejati86 fix: prevent orphan tool_result errors from streaming failures
90.9% #4844 @lailoo fix(agents): skip error/aborted assistant messages in transcript repair
90.6% #3362 @samhotchkiss fix: auto-repair and retry on orphan tool_result errors
90.5% #4598 @aisling404 fix(agents): skip tool extraction for aborted/errored assistant messages
90.5% #3194 @koriyoshi2041 fix: skip incomplete tool calls in transcript repair [AI-assisted]
90.1% #3060 @sid1943 [AI-Assisted] fix: skip tool_use blocks with stopReason error in transcript r...
90.1% #2253 @Zedit42 fix: sanitize incomplete tool calls with partialJson
90.0% #3223 @mbelinky [AI] fix: ignore tool calls from aborted assistant turns
89.3% #3880 @SalimBinYousuf1 fix: drop assistant messages with stopReason 'error' to avoid orphaning tool ...
87.8% #6687 @NSEvent fix(session-repair): strip malformed tool_use blocks to prevent permanent ses...
87.4% #2557 @steve-rodri fix(agents): preserve tool call/result pairing in history limiting
87.4% #6684 @NSEvent fix(session-repair): strip malformed tool_use blocks to prevent permanent ses...
87.2% #5738 @henryjrobinson Agents: fix orphaned toolResult after history truncation
86.4% #4387 @spiceoogway fix: repair tool_use/tool_result pairings after history truncation
86.2% #6681 @NSEvent fix(session-repair): strip malformed tool_use blocks to prevent permanent ses...
86.1% #6667 @NSEvent fix(session-repair): strip malformed tool_use blocks to prevent permanent ses...
86.1% #2806 @Arthur742Ramos [AI-Assisted] Fix: Repair tool_use/tool_result pairing for Claude on any prov...
86.0% #3622 @mickobizzle fix(agents): drop orphan tool results
85.7% #4700 @marcelomar21 fix: deduplicate tool_use IDs and enable sanitization for Anthropic
85.4% #5899 @Ayush10 fix(history): re-repair tool_use/tool_result pairing after truncation
85.3% #3565 @kiranjd fix(sessions): truncate at incomplete tool calls instead of synthetic results
85.2% #5557 @NSEvent fix(session): strip malformed tool_use blocks to prevent session corruption

🔍 Detected by OpenClaw PR Tracker — AI-powered duplicate detection for open source

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.

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +16 to +18
const stopReason = (msg as { stopReason?: unknown }).stopReason;
if (stopReason === "error") return [];

Copy link
Contributor

Choose a reason for hiding this comment

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

[P2] Avoid the (msg as { stopReason?: unknown }) cast and instead read msg.stopReason with a type guard or by extending the AgentMessage type locally. As written, TypeScript won’t protect you if the upstream type changes (and it’s easy to accidentally shadow a different stopReason property shape).

This is minor, but since the function already assumes msg is an assistant message, a safer pattern is to narrow with 'stopReason' in msg && msg.stopReason === "error" (or adjust the extracted type) rather than casting to unknown.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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

Comment:
[P2] Avoid the `(msg as { stopReason?: unknown })` cast and instead read `msg.stopReason` with a type guard or by extending the `AgentMessage` type locally. As written, TypeScript won’t protect you if the upstream type changes (and it’s easy to accidentally shadow a different `stopReason` property shape).

This is minor, but since the function already assumes `msg` is an assistant message, a safer pattern is to narrow with `'stopReason' in msg && msg.stopReason === "error"` (or adjust the extracted type) rather than casting to `unknown`.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

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

Comment on lines +117 to +120
// See: https://github.com/clawdbot/clawdbot/issues/1826
const input = [
{ role: "user", content: "schedule a reminder" },
{
Copy link
Contributor

Choose a reason for hiding this comment

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

[P3] The test comment links to clawdbot/clawdbot issue #1826, but this repo context is openclaw/openclaw. If the intent is to reference an internal issue in this repo, the link may be wrong and could confuse future readers.

If #1826 lives elsewhere intentionally, consider switching the comment to a plain Fixes #1826-style reference in the PR description only, or link the correct tracker for this repo.

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

Comment:
[P3] The test comment links to `clawdbot/clawdbot` issue #1826, but this repo context is `openclaw/openclaw`. If the intent is to reference an internal issue in this repo, the link may be wrong and could confuse future readers.

If #1826 lives elsewhere intentionally, consider switching the comment to a plain `Fixes #1826`-style reference in the PR description only, or link the correct tracker for this repo.

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

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 +16 to +17
const stopReason = (msg as { stopReason?: unknown }).stopReason;
if (stopReason === "error") return [];
Copy link
Contributor

Choose a reason for hiding this comment

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

[P2] This only skips tool-call extraction when stopReason === "error", but other termination stop reasons (e.g., cancelled/aborted/"interrupted" depending on provider) can also leave incomplete tool calls that were never executed. In those cases this code may still synthesize toolResults and recreate the same "unexpected tool_use_id" failure mode.

If this repo sees other stop reasons in practice, consider broadening the guard (or checking for an errorMessage/presence of partialJson in tool blocks) so all non-completed assistant turns are excluded from extraction.

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

Comment:
[P2] This only skips tool-call extraction when `stopReason === "error"`, but other termination stop reasons (e.g., cancelled/aborted/"interrupted" depending on provider) can also leave incomplete tool calls that were never executed. In those cases this code may still synthesize `toolResult`s and recreate the same "unexpected tool_use_id" failure mode.

If this repo sees other stop reasons in practice, consider broadening the guard (or checking for an `errorMessage`/presence of `partialJson` in tool blocks) so all non-completed assistant turns are excluded from extraction.

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

@aisling404
Copy link
Contributor

aisling404 commented Feb 3, 2026

Update on #4598:

Covers both "error" and "aborted" in one PR. Happy to squash commits if that helps!

Copy link
Contributor

@quotentiroler quotentiroler left a comment

Choose a reason for hiding this comment

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

Closing in favor of #4598 which provides more comprehensive coverage:

  • Handles both stopReason: "error" (this PR) AND stopReason: "aborted"
  • Includes 4 unit tests covering error, aborted, normal, and orphan-after-abort scenarios
  • CI passing

Thanks @csaftoiu for the original fix and confirming it worked! The consolidated approach in #4598 will help close ~20 overlapping PRs addressing the same root cause.

@zerone0x zerone0x deleted the fix/transcript-repair-skip-errored-turns branch February 6, 2026 06:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Synthetic tool_result for terminated tool_use causes all subsequent requests to fail

8 participants