Repository navigation
triage: parallel tool-call turns send tool_result blocks one message too late, so the next request is rejected with 400 #3361
Description
Initial classification
actionable-bug
Evidence
Reported symptom. A claw REPL turn that issues two tool calls at once ends with the
provider rejecting the follow-up request:
✘ ❌ Request failed
[error-kind: api_http_error]
error: api returned 400 Bad Request (invalid_request_error): messages.3:`tool_use` ids were found
without `tool_result` blocks immediately after: call_01_4QJqcZ0075kla1ILwra75582. Each `tool_use`
block must have a corresponding `tool_result` block in the next message. (request_id: cfd...)
Reproduction. In an interactive claw session, send a prompt that makes the model request two
tools in one assistant turn (the session below asked to investigate a directory; the model emitted
ls-style bash calls twice). The first tool-result message pairs, the second does not, and the
request that follows is rejected with HTTP 400.
The durable session log written by that run shows the shape directly — this is
.claw/sessions/e49619b247aa3fce/session-1792170907799-0.jsonl, message records 1-5:
[3] assistant blocks=[thinking, thinking, tool_use, tool_use] <- two calls in ONE message
[4] tool blocks=[tool_result] <- call_00_8mxD6kU9VooBxwZzfYpO5328
[5] tool blocks=[tool_result] <- call_01_4QJqcZ0075kla1ILwra75582
Root cause. runtime records one MessageRole::Tool message per tool call, and
convert_messages() in rust/crates/rusty-claude-cli/src/main.rs turned each of them into its own
wire message. The Anthropic Messages API requires every tool_use of an assistant message to be
answered by tool_result blocks in the single message that follows it, so the wire history
became:
messages.3 assistant [thinking, thinking, tool_use, tool_use]
messages.4 user [tool_result] call_00_8mxD6kU9VooBxwZzfYpO5328 <- paired
messages.5 user [tool_result] call_01_4QJqcZ0075kla1ILwra75582 <- one message too late
Single-call turns are unaffected, which is why this only appears when the model requests several
tools at once. preflight_message_request only checks context length, and the OpenAI-compat path
has sanitize_tool_message_pairing while the Anthropic path has no equivalent.
Local verification of the failure mode. Replaying the session's message list through the
pre-fix convert_messages logic reproduces the provider's complaint verbatim, including the id it
names:
messages.3 assistant tool_use=['call_00_8mxD6kU9VooBxwZzfYpO5328', 'call_01_4QJqcZ0075kla1ILwra75582']
messages.4 tool_result=['call_00_8mxD6kU9VooBxwZzfYpO5328'] -> VIOLATION
==> messages.4: `tool_use` ids were found without `tool_result` blocks immediately after: call_01_4QJqcZ0075kla1ILwra75582
Proposed fix (offered, not applied here). Merge a run of adjacent tool-result messages into the
one user message that follows the assistant turn, extending only a message whose content is tool
results and nothing else. A regression test can drive the real binary against
mock-anthropic-service's multi_tool_turn_roundtrip scenario and assert the pairing contract on
the captured request body; run against the pre-fix code it fails with exactly the payload above.
Two further defects surfaced in the same session and are recorded as ROADMAP #836 (terminal-scope
record paths exceed the APFS 1024-byte total-path limit for a maximal key, invisible to the ubuntu
CI job because Linux PATH_MAX is 4096) and #838 (the missing-credential error never mentions a
configured non-Anthropic ANTHROPIC_BASE_URL).
Safe next action
Maintainer review of the proposed fix shape. A ready branch exists at
c01dsummer#1 (fork PR, closed as superseded because the review
target is this repository). If a same-repo PR is preferred so that CI runs without the
first-time-contributor workflow approval, I can open one; the diff is 574/-68 across 9 files and
carries its own unit and integration coverage plus the ROADMAP entries.
- I did not close, merge, or mutate remote state as part of this triage-only report.
- I checked for duplicates or related PRs/issues before recommending action. (searched this
repository for existingtool_use/tool_resultpairing reports: none found) - If this touches credentials, security, or private data, I avoided public reproduction details
and routed to the appropriate private/security path. (no credentials in this report)
Activity
c01dsummer commentedon Oct 16, 2026
Baseline for the proposed fix, measured on the branch under review
(
fix/roadmap-835-838, the tree behind #3360):--test-threads=1is needed because of a pre-existing fixture flake, not because of this change:runtime'smcp_stdio::testsandmcp_tool_bridge::testsspend their#[cfg(test)]200ms/300mshandshake budgets on
python3cold start when a dozen fixture children start at once. That isfiled separately as ROADMAP 为什么还在持续commit? #837 (KNOWN FLAKE, not fixed — the budgets are deliberately short so a
real handshake regression fails fast). The default parallel run reports 10-11 of those, plus
cargo test --workspaceis green in every other respect.ignoredcase isapi/tests/client_integration.rs::live_stream_smoke_test, markedrequires ANTHROPIC_API_KEY and network access.issue and for https://github.com/e10nMa2k/cc-mini #836 pass here while failing against the pre-fix code:
scripts/fmt.sh --checkpasses;cargo clippy --workspace --all-targets -- -D warningsreports nofindings in the crates this change touches (
api,sdk,rusty-claude-cli, and the comment inruntime). Pre-existing findings inruntime/trident.rs,runtime/file_ops.rs,runtime/line_range_selector.rs,runtime/compact.rsandclaw-rag-serviceare untouched.Not claiming completion here — this is the evidence for review, and the issue stays open for the
maintainer to decide. The full per-defect breakdown, including the pre-fix failure output, is in the
PR body: #3360
XinwangTao commentedon Oct 17, 2026
Confirmed — this is the classic tool_use/tool_result pairing race when parallel tool calls are emitted in a single assistant turn. The provider strictly requires every tool_use block to be answered with a matching tool_result in the very next message; when the second result is flushed too late, the follow-up request gets rejected with 400. Batching all results into one following message (exactly what the new regression test parallel_tool_calls_are_answered_in_one_following_message verifies) is the right fix. Thanks for the detailed session log — it makes the failure mode very clear.
fix: parallel tool-result pairing, terminal-scope path limit, credent…