Skip to content

triage: parallel tool-call turns send tool_result blocks one message too late, so the next request is rejected with 400 #3361

Closed

Description

@c01dsummer

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 existing tool_use/tool_result pairing 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

  1. c01dsummer commented on Oct 16, 2026

    @c01dsummer
    Author

    Baseline for the proposed fix, measured on the branch under review
    (fix/roadmap-835-838, the tree behind #3360):

    $ cd rust && cargo test --workspace -- --test-threads=1
    42 targets: 1806 passed; 0 failed; 1 ignored
    
    • --test-threads=1 is needed because of a pre-existing fixture flake, not because of this change:
      runtime's mcp_stdio::tests and mcp_tool_bridge::tests spend their #[cfg(test)] 200ms/300ms
      handshake budgets on python3 cold start when a dozen fixture children start at once. That is
      filed 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 --workspace is green in every other respect.
    • The single ignored case is api/tests/client_integration.rs::live_stream_smoke_test, marked
      requires ANTHROPIC_API_KEY and network access.
    • The three tests added by ROADMAP 怎么全都是中文issue xs #838 all pass in this run, and the two regression tests for this
      issue and for https://github.com/e10nMa2k/cc-mini #836 pass here while failing against the pre-fix code:
    test parallel_tool_calls_are_answered_in_one_following_message ... ok
    test long_hex_keys_use_safe_path_components ... ok
    test anthropic_missing_credentials_hint_names_a_custom_base_url ... ok
    
    • scripts/fmt.sh --check passes; cargo clippy --workspace --all-targets -- -D warnings reports no
      findings in the crates this change touches (api, sdk, rusty-claude-cli, and the comment in
      runtime). Pre-existing findings in runtime/trident.rs, runtime/file_ops.rs,
      runtime/line_range_selector.rs, runtime/compact.rs and claw-rag-service are 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

  2. XinwangTao commented on Oct 17, 2026

    @XinwangTao

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions