openclaw - 💡(How to fix) Fix Synthetic 'missing tool result' entries injected for parallel tool calls on Anthropic Claude, despite real results being produced

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Inside an active session, a meaningful percentage of tool calls (most reliably exec / read in parallel batches) return the synthetic string [openclaw] missing tool result in session history; inserted synthetic error result for transcript repair. instead of the real result. The tools actually ran (verified: side effects landed). Single-call turns succeed; parallel batches of 2-4 calls fail reliably.

Error Message

Inside an active session, a meaningful percentage of tool calls (most reliably exec / read in parallel batches) return the synthetic string [openclaw] missing tool result in session history; inserted synthetic error result for transcript repair. instead of the real result. The tools actually ran (verified: side effects landed). Single-call turns succeed; parallel batches of 2-4 calls fail reliably. 3. Inspect the session JSONL: at least one synthetic-error tool_result will appear, with no matching real result anywhere in the file. Defer the flush triggered by shouldFlushBeforeNewToolCalls until the runner has positively confirmed all pending tool results were attempted (success, error, or timeout) — not when a new assistant turn merely arrives. A minimal band-aid is to make the guard tick-aware (don't flush within the same tick as trackToolCalls), but the structural fix is in the runner: enforce strict result-then-next-turn ordering at the sessionManager.appendMessage boundary. Separately filed: approval-gate denials routed via followup-channel produce phantom synthetic placeholders (a different code path that produces the same surface error).

Root Cause

installSessionToolResultGuard tracks pending tool calls in an in-memory Map<tool_use_id, toolName>. shouldFlushBeforeNewToolCalls returns pending.size > 0 && toolCallCount > 0, which means any incoming assistant message with new tool calls flushes all still-pending IDs as synthetic errors — including IDs whose real appendMessage(toolResult) is still in flight.

This creates a race between the runner's per-tool result-append path and the next assistant turn's append. The persisted JSONL ends up with tool_use(id=X)tool_result(id=X, isError=true, "missing tool result…") while the real result is dropped as an orphan by repairToolUseResultPairing on the next request.

Code Example

--- a/src/agents/session-tool-result-state.ts
+++ b/src/agents/session-tool-result-state.ts
@@
 export function createPendingToolCallState() {
   const pending = new Map<string, string | undefined>();
+  let lastTrackTimestamp = 0;
   return {
     trackToolCalls: (calls: Array<{ id: string; name?: string }>) => {
       for (const call of calls) pending.set(call.id, call.name);
+      lastTrackTimestamp = Date.now();
     },
-    shouldFlushBeforeNewToolCalls: (toolCallCount: number) =>
-      pending.size > 0 && toolCallCount > 0,
+    shouldFlushBeforeNewToolCalls: (toolCallCount: number) => {
+      if (pending.size === 0 || toolCallCount === 0) return false;
+      // If a brand-new assistant tool-use turn arrives within the
+      // same-tick window as pending tool calls were tracked, we are
+      // almost certainly racing the result-append for parallel calls.
+      return Date.now() - lastTrackTimestamp > 0;
+    },
   };
 }
RAW_BUFFERClick to expand / collapse

Summary

Inside an active session, a meaningful percentage of tool calls (most reliably exec / read in parallel batches) return the synthetic string [openclaw] missing tool result in session history; inserted synthetic error result for transcript repair. instead of the real result. The tools actually ran (verified: side effects landed). Single-call turns succeed; parallel batches of 2-4 calls fail reliably.

Version

openclaw 2026.5.27 (reproducible on HEAD per source review) Model: anthropic/claude-opus-4-7 (anthropic-messages API) Channel: webchat (main agent) — but the bug is in session-tool-result-guard.ts so any channel is affected.

Root cause

installSessionToolResultGuard tracks pending tool calls in an in-memory Map<tool_use_id, toolName>. shouldFlushBeforeNewToolCalls returns pending.size > 0 && toolCallCount > 0, which means any incoming assistant message with new tool calls flushes all still-pending IDs as synthetic errors — including IDs whose real appendMessage(toolResult) is still in flight.

This creates a race between the runner's per-tool result-append path and the next assistant turn's append. The persisted JSONL ends up with tool_use(id=X)tool_result(id=X, isError=true, "missing tool result…") while the real result is dropped as an orphan by repairToolUseResultPairing on the next request.

Repro

  1. Open a session against an Anthropic Claude model.
  2. Ask the model to perform 3+ exec or read calls in one turn (must end up as a single assistant message with parallel tool_use blocks).
  3. Inspect the session JSONL: at least one synthetic-error tool_result will appear, with no matching real result anywhere in the file.

Why it durably corrupts the transcript

Once a synthetic tool_result is persisted, the tool_use it claims to resolve is "satisfied" from the guard's perspective. On the next turn, repairToolUseResultPairing drops any late-arriving real result for that ID as an "orphan" (droppedOrphanCount += 1). The synthetic stays.

Proposed fix

Defer the flush triggered by shouldFlushBeforeNewToolCalls until the runner has positively confirmed all pending tool results were attempted (success, error, or timeout) — not when a new assistant turn merely arrives. A minimal band-aid is to make the guard tick-aware (don't flush within the same tick as trackToolCalls), but the structural fix is in the runner: enforce strict result-then-next-turn ordering at the sessionManager.appendMessage boundary.

Minimum-blast-radius diff:

--- a/src/agents/session-tool-result-state.ts
+++ b/src/agents/session-tool-result-state.ts
@@
 export function createPendingToolCallState() {
   const pending = new Map<string, string | undefined>();
+  let lastTrackTimestamp = 0;
   return {
     trackToolCalls: (calls: Array<{ id: string; name?: string }>) => {
       for (const call of calls) pending.set(call.id, call.name);
+      lastTrackTimestamp = Date.now();
     },
-    shouldFlushBeforeNewToolCalls: (toolCallCount: number) =>
-      pending.size > 0 && toolCallCount > 0,
+    shouldFlushBeforeNewToolCalls: (toolCallCount: number) => {
+      if (pending.size === 0 || toolCallCount === 0) return false;
+      // If a brand-new assistant tool-use turn arrives within the
+      // same-tick window as pending tool calls were tracked, we are
+      // almost certainly racing the result-append for parallel calls.
+      return Date.now() - lastTrackTimestamp > 0;
+    },
   };
 }

This is a defensive band-aid, not the real fix. The real fix is in the runner: ensure appendMessage(toolResult) for every spawned tool completes (or errors) before any subsequent appendMessage(assistant) is attempted.

Related observation

The synthetic placeholder mentions "transcript repair," which misleads operators into investigating transport-layer bugs. Consider distinguishing between "truly missing" and "explicitly denied" or "explicit timeout" cases in the placeholder text, and emitting a structured log line so operators can correlate.

Related issue

Separately filed: approval-gate denials routed via followup-channel produce phantom synthetic placeholders (a different code path that produces the same surface error).

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix Synthetic 'missing tool result' entries injected for parallel tool calls on Anthropic Claude, despite real results being produced