Skip to content

fix(agent-core): ignore truncated assistant tool calls#97092

Closed
galiniliev wants to merge 1 commit into
openclaw:mainfrom
galiniliev:bug-047-truncated-tool-call-commit
Closed

fix(agent-core): ignore truncated assistant tool calls#97092
galiniliev wants to merge 1 commit into
openclaw:mainfrom
galiniliev:bug-047-truncated-tool-call-commit

Conversation

@galiniliev

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes an issue where users running streamed agent tool calls could execute an unintended partial tool call when an assistant turn stopped at the output limit while producing a tool-call batch.

Closes #97091

Why This Change Was Made

The agent loop now treats only finalized toolUse assistant turns as executable. Non-toolUse final assistant messages have tool-call blocks removed before they are committed to context or emitted as message_end, so truncated length turns cannot dispatch side-effectful tools or leave dangling tool-call replay state.

User Impact

Users get safer streamed tool-call behavior: a max-token-truncated assistant turn can preserve visible assistant text, but it will not launch accidental child sessions or poison the next model request with unmatched tool calls.

Evidence

Before evidence:

2026-06-26T02:17:44.026Z extra sessions_spawn accepted
  childSessionKey: [redacted child session key]
  runId: [redacted run id]
  mode: run
  taskName: absent

2026-06-26T02:17:46.897Z accidental child first log record
2026-06-26T02:25:40.519Z accidental child last log record
duration: about 7m54s
assistant tool calls: 87
tool results: 87
tool names: exec=81, process=4, read=2

Focused regression proof after fix:

$ node scripts/run-vitest.mjs packages/agent-core/src/agent-loop.test.ts
[test] starting test/vitest/vitest.unit.config.ts

 RUN  v4.1.8 /home/galini/GitHub/worktrees/bug-047-truncated-tool-call-commit

 Test Files  1 passed (1)
      Tests  16 passed (16)
   Start at  19:02:13
   Duration  977ms (transform 445ms, setup 321ms, import 348ms, tests 117ms, environment 0ms)

[test] passed 1 Vitest shard in 8.91s

Patch hygiene:

$ git diff --check
# no output

Autoreview:

$ .agents/skills/autoreview/scripts/autoreview --mode local
autoreview clean: no accepted/actionable findings reported
overall: patch is correct (0.82)

Real behavior proof

Behavior addressed: A max-token-truncated assistant turn with a partial sessions_spawn tool call no longer executes the tool call and no longer leaves a dangling tool call in the committed assistant message, emitted message_end, or next-turn replay context.

Real environment tested: Local OpenClaw source worktree using the agent-core runtime seam and the real createAssistantMessageEventStream event protocol.

Exact steps or command run after this patch: node scripts/run-vitest.mjs packages/agent-core/src/agent-loop.test.ts; git diff --check; .agents/skills/autoreview/scripts/autoreview --mode local.

Evidence after fix:

$ node scripts/run-vitest.mjs packages/agent-core/src/agent-loop.test.ts
[test] starting test/vitest/vitest.unit.config.ts

 RUN  v4.1.8 /home/galini/GitHub/worktrees/bug-047-truncated-tool-call-commit

 Test Files  1 passed (1)
      Tests  16 passed (16)
   Start at  19:02:13
   Duration  977ms (transform 445ms, setup 321ms, import 348ms, tests 117ms, environment 0ms)

[test] passed 1 Vitest shard in 8.91s

Observed result after fix: The new regression verifies that a stopReason: "length" assistant message containing a sessions_spawn tool-call block does not emit tool_execution_start, does not call the tool implementation, removes the tool-call block from the returned assistant message, removes it from the message_end event, and removes it from the follow-up turn's replay context.

What was not tested: I did not rerun the original live Telegram/GitHub Copilot parent session because it depends on private channel credentials and reproducing a provider max-token truncation at the same point in a live tool-call batch.

@openclaw-barnacle openclaw-barnacle Bot added size: S maintainer Maintainer-authored PR labels Jun 26, 2026
@clawsweeper

clawsweeper Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 26, 2026, 3:48 PM ET / 19:48 UTC.

Summary
The PR changes agent-core so only toolUse assistant turns dispatch tool calls, strips tool-call blocks from non-executable final assistant messages, and adds a regression test for a length-truncated sessions_spawn turn.

PR surface: Source +8, Tests +114. Total +122 across 2 files.

Reproducibility: yes. Source inspection gives a high-confidence reproduction path: return a final assistant message with stopReason "length" and a toolCall block, and current main routes it into executeToolCalls.

Review metrics: 1 noteworthy metric.

  • Core tool dispatch contract: 1 condition tightened, 1 final-message scrub added. This is the compatibility-sensitive runtime boundary that determines whether provider-returned tool-call blocks execute.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #97091
Summary: This PR is the linked candidate fix for the open truncated sessions_spawn tool-call bug; related session-state reports are adjacent but not the same root cause.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦪 silver shellfish
Proof: 🦪 silver shellfish
Patch quality: 🦞 diamond lobster
Result: blocked until real behavior proof from a real setup is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Add redacted after-fix runtime logs, terminal output, copied live output, or a linked artifact from a patched setup showing a length-truncated tool-call turn no longer executes or persists the tool call.

Proof guidance:

  • [P1] Needs real behavior proof before merge: After-fix evidence is still a focused synthetic Vitest/event-stream run; the new sanitized comment is before-fix incident proof, not patched real-runtime proof. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] After-fix proof is still a focused synthetic event-stream regression test; the added sanitized log comment proves the original incident but not a patched real-runtime run suppressing it.
  • [P1] The diff intentionally stops executing tool-call blocks when the final assistant stopReason is not toolUse, so maintainers should accept that provider/plugin adapters must normalize complete executable calls to toolUse before merge.

Maintainer options:

  1. Accept the stopReason execution contract
    Maintainers can explicitly accept that only assistant turns ending with toolUse may execute tools, even if permissive legacy adapter behavior stops working.
  2. Prove supported adapters before merge
    Require targeted adapter-contract evidence showing supported providers emit toolUse for complete executable tool-call turns and length or stop for non-executable turns.
  3. Hold for real patched-runtime proof
    Wait for redacted after-fix runtime logs or live output from a patched setup showing the truncated tool-call turn is not executed or replayed.

Next step before merge

  • [P1] Manual review is needed because the PR has a protected maintainer label, a compatibility-sensitive execution-contract change, and a contributor real-behavior proof gate.

Security
Cleared: No security or supply-chain regression was found; the diff narrows side-effectful tool execution and does not add dependencies, scripts, workflow permissions, or secret handling.

Review details

Best possible solution:

Land the focused agent-core guard after maintainers accept the stopReason contract and the contributor adds redacted after-fix real-runtime proof.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection gives a high-confidence reproduction path: return a final assistant message with stopReason "length" and a toolCall block, and current main routes it into executeToolCalls.

Is this the best way to solve the issue?

Yes. Agent-core owns tool execution and replay context, so gating execution there is the right layer; provider-specific scrubbing would be narrower and would miss plugin/provider adapters that can produce the same invalid terminal shape.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 1089253ca97c.

Label changes

Label changes:

  • add rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦪 silver shellfish and patch quality is 🦞 diamond lobster.
  • remove rating: 🦐 gold shrimp: Current PR rating is rating: 🦪 silver shellfish, so this older rating label is no longer current.

Label justifications:

  • P1: The PR addresses an agent-core safety bug where a length-truncated assistant turn can launch unintended side-effectful subagent work.
  • merge-risk: 🚨 compatibility: The diff changes execution behavior for tool-call blocks returned with non-toolUse stop reasons, which can affect provider or plugin adapters that relied on permissive dispatch.
  • rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦪 silver shellfish and patch quality is 🦞 diamond lobster.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: After-fix evidence is still a focused synthetic Vitest/event-stream run; the new sanitized comment is before-fix incident proof, not patched real-runtime proof. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +8, Tests +114. Total +122 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 12 4 +8
Tests 1 116 2 +114
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 128 6 +122

What I checked:

  • Current main dispatches returned tool calls without a toolUse guard: Current main exits only for error or aborted, then executes executeToolCalls whenever the final assistant message contains toolCall blocks. (packages/agent-core/src/agent-loop.ts:339, 1089253ca97c)
  • PR head guards dispatch and scrubs final messages: The PR adds removeNonExecutableToolCalls and changes dispatch so only stopReason === "toolUse" messages execute tools. (packages/agent-core/src/agent-loop.ts:83, 4472364cc39b)
  • Regression test covers execution, message_end, and replay context: The added test creates a length-truncated assistant message containing sessions_spawn and verifies no tool execution event, no tool implementation call, no committed toolCall block, and no replayed toolCall block. (packages/agent-core/src/agent-loop.test.ts:168, 4472364cc39b)
  • LLM stream contract supports the stopReason boundary: Assistant streams terminate with done carrying stop, length, or toolUse and a final AssistantMessage result, which is the boundary the PR scrubs before commit/replay. (packages/llm-core/src/types.ts:361, 1089253ca97c)
  • Anthropic/Copilot path can produce length for max-token stops: The Anthropic adapter maps tool_use blocks to toolCall content and maps upstream max_tokens to OpenClaw stopReason length. (src/llm/providers/anthropic.ts:607, 1089253ca97c)
  • Supported sibling adapters already normalize executable tool calls: OpenAI completions strips toolCall blocks for non-toolUse stop reasons, while OpenAI Responses and Google promote tool-call finishes to toolUse. (src/llm/providers/openai-completions.ts:497, 1089253ca97c)

Likely related people:

  • steipete: GitHub path history shows the agent-core package extraction and later terminal-classifier work in the same agent-core loop area. (role: area introducer and recent agent-core contributor; confidence: high; commits: bb46b79d3c14, 0314819f918a; files: packages/agent-core/src/agent-loop.ts, packages/agent-core/src/agent-loop.test.ts)
  • vincentkoc: GitHub path history shows recent agent-loop stream snapshot work and reviewer/co-author metadata on an aborted-tool-run fix in the same files. (role: recent adjacent owner/reviewer; confidence: high; commits: 6fcc9457020e, 7fe287b0d30d; files: packages/agent-core/src/agent-loop.ts, packages/agent-core/src/agent-loop.test.ts, src/llm/providers/anthropic.ts)
  • fuller-stack-dev: GitHub path history shows recent tool search directory mode work touching the agent-loop and tests around deferred tool hydration and execution behavior. (role: recent feature contributor; confidence: medium; commits: 965fa05df38b; files: packages/agent-core/src/agent-loop.ts, packages/agent-core/src/agent-loop.test.ts)
  • szsip239: GitHub path history shows a recent agent-core abort/tool-run fix in the same loop files, co-authored with vincentkoc and reviewed by vincentkoc. (role: recent bug-fix contributor; confidence: medium; commits: 7fe287b0d30d; files: packages/agent-core/src/agent-loop.ts, packages/agent-core/src/agent-loop.test.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. labels Jun 26, 2026
@galiniliev

Copy link
Copy Markdown
Contributor Author

Sanitized direct log proof for the truncated sixth sessions_spawn incident.

Redactions applied: exact parent/child session IDs, session keys, run IDs, local paths/workspace dirs, agent id, task labels, and private PR details. I left timestamps, provider/model/API, terminal status, and aggregate tool counts intact because those are the behavior evidence.

Parent acceptance timeline preserved from the extracted incident notes:
2026-06-26T02:17:39.819Z assistant visible text:
  "Worktree is up at PR head ... Now let me dispatch all five specialists in parallel."

2026-06-26T02:17:43.997Z accepted intended child task #1
2026-06-26T02:17:44.008Z accepted intended child task #2
2026-06-26T02:17:44.018Z accepted intended child task #3
2026-06-26T02:17:44.026Z accepted extra child with no taskName
2026-06-26T02:18:53.205Z accepted intended child task #4
2026-06-26T02:18:53.246Z accepted intended child task #5

Parent later identified the extra child as the truncated sixth spawn from an incomplete prompt.

Direct accidental-child JSONL / trajectory proof, sanitized:

{
  "firstSessionRecord": {
    "type": "session",
    "version": 3,
    "id": "[redacted child session id]",
    "timestamp": "2026-06-26T02:17:46.897Z",
    "cwd": "[redacted local workspace]"
  },
  "trajectoryStarted": {
    "traceSchema": "openclaw-trajectory",
    "schemaVersion": 1,
    "source": "runtime",
    "type": "session.started",
    "ts": "2026-06-26T02:17:46.900Z",
    "seq": 1,
    "sourceSeq": 1,
    "traceId": "[redacted trace id]",
    "sessionId": "[redacted child session id]",
    "sessionKey": "[redacted child session key]",
    "runId": "[redacted run id]",
    "workspaceDir": "[redacted local workspace]",
    "provider": "github-copilot",
    "modelId": "claude-opus-4.7",
    "modelApi": "anthropic-messages",
    "data": {
      "trigger": "user",
      "sessionFile": "[redacted local session file]",
      "workspaceDir": "[redacted local workspace]",
      "agentId": "[redacted agent id]",
      "messageProvider": "telegram",
      "messageChannel": "telegram",
      "localModelLean": false,
      "toolCount": 10,
      "clientToolCount": 0
    }
  },
  "trajectoryEnded": {
    "traceSchema": "openclaw-trajectory",
    "schemaVersion": 1,
    "source": "runtime",
    "type": "session.ended",
    "ts": "2026-06-26T02:25:40.637Z",
    "seq": 7,
    "sourceSeq": 7,
    "traceId": "[redacted trace id]",
    "sessionId": "[redacted child session id]",
    "sessionKey": "[redacted child session key]",
    "runId": "[redacted run id]",
    "workspaceDir": "[redacted local workspace]",
    "provider": "github-copilot",
    "modelId": "claude-opus-4.7",
    "modelApi": "anthropic-messages",
    "data": {
      "status": "error",
      "aborted": false,
      "externalAbort": false,
      "timedOut": false,
      "idleTimedOut": false,
      "timedOutDuringCompaction": false,
      "timedOutDuringToolExecution": false,
      "terminalError": "non_deliverable_terminal_turn"
    }
  },
  "lastMessage": {
    "type": "message",
    "timestamp": "2026-06-26T02:25:40.519Z",
    "role": "assistant"
  },
  "counts": {
    "assistantMsgs": 88,
    "toolCalls": 87,
    "toolResults": 87,
    "toolNames": {
      "exec": 81,
      "read": 2,
      "process": 4
    }
  }
}

This is the direct behavior the patch blocks: a non-toolUse/truncated assistant turn should not dispatch an incomplete sessions_spawn, and any non-executable tool-call blocks should be removed before replay.

@galiniliev

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Jun 26, 2026
@galiniliev galiniliev assigned steipete and galiniliev and unassigned steipete Jun 26, 2026
@steipete

Copy link
Copy Markdown
Contributor

Landed via #97140 as 56259606d15f66c90415baddca9e08b156ef6b33.

Thank you @galiniliev for the reproduction and agent-loop fix. The maintainer replacement preserves your coauthor credit and adds the provider-normalization, missing-terminal, and error-event hardening found during review.

I could not safely update this fork branch: GitHub reported maintainerCanModify=true, but the server still denied this maintainer account push permission. Please enable or keep Allow edits by maintainers enabled on future PRs; here that setting was present but unfortunately insufficient. Closing this PR as superseded by the landed replacement.

@steipete steipete closed this Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer Maintainer-authored PR merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P1 High-priority user-facing bug, regression, or broken workflow. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. size: S status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: truncated assistant tool-call turn executes unintended sessions_spawn

2 participants