hermes - 💡(How to fix) Fix [Bug]: extended-thinking + interleaved multi-thinking tool turn → non-retryable HTTP 400 crash-loop (reordered thinking signature) [3 pull requests]

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…

Error Message

  1. The gateway loops on the same error indefinitely.

Root Cause

agent/anthropic_adapter.py:

  • _convert_assistant_message() builds the assistant turn by extracting all preserved thinking blocks first (_extract_preserved_thinking_blocks), then text, then appending all tool_use blocks. For a single leading thinking block this is fine, but for interleaved extended thinking (2+ thinking blocks woven among tool_use) it reorders the blocks relative to the signed sequence.
  • _manage_thinking_signatures() latest-assistant branch then replays any block with a signature verbatim — including the now-reordered (positionally invalid) ones — producing the 400.

The #35847 fix only handles signatures invalidated by orphan-stripping (_thinking_signature_invalidated); it does not flag the reorder case, which fires with no orphans at all.

Fix Action

Fixed

Code Example

[ all thinking blocks ][ text ][ all tool_use blocks ]

---

messages.N.content.M: `thinking` or `redacted_thinking` blocks in the latest
assistant message cannot be modified. These blocks must remain as they were
in the original response.

---

from agent.anthropic_adapter import convert_messages_to_anthropic
messages = [
    {
        "role": "assistant",
        "content": "Working on it.",
        "reasoning_details": [
            {"type": "thinking", "thinking": "First step.",  "signature": "sig1"},
            {"type": "thinking", "thinking": "Second step.", "signature": "sig2"},
        ],
        "tool_calls": [
            {"id": "tc_a", "function": {"name": "a", "arguments": "{}"}},
            {"id": "tc_b", "function": {"name": "b", "arguments": "{}"}},
        ],
    },
    {"role": "tool", "tool_call_id": "tc_a", "content": "result A"},
    {"role": "tool", "tool_call_id": "tc_b", "content": "result B"},
]
_, result = convert_messages_to_anthropic(messages)
# Before the fix: the latest assistant turn replays two signed `thinking` blocks
# whose signatures were computed over the original interleaved order → 400 on replay.
RAW_BUFFERClick to expand / collapse

Bug Description

Extended-thinking Claude models (4.6+, e.g. Opus 4.8) crash-loop the gateway with a non-retryable HTTP 400 on any assistant turn that emits 2 or more thinking blocks alongside tool_use blocks — i.e. interleaved extended thinking during a multi-step / parallel tool batch.

With interleaved thinking enabled, Anthropic emits the assistant turn as an interleaved sequence (thinking, tool_use, thinking, tool_use, …) and signs each thinking block against its original position in that sequence. Hermes stores thinking (in reasoning_details) and tool_calls separately, and agent/anthropic_adapter.py::_convert_assistant_message() reconstructs the turn as:

[ all thinking blocks ][ text ][ all tool_use blocks ]

This reorders the blocks relative to the signed sequence. On replay, the signatures no longer match their positions and Anthropic rejects the turn:

messages.N.content.M: `thinking` or `redacted_thinking` blocks in the latest
assistant message cannot be modified. These blocks must remain as they were
in the original response.

The 400 is classified non-retryable, so the gateway reloads the same poisoned transcript from the persisted store every turn → infinite crash-loop with no self-recovery (a soft session reset does not clear it, because history is rebuilt from the store).

This is distinct from #35847 (orphan-stripped tool_use invalidating a signature). Here there are no orphans — every tool_use is answered — and the fix from #35847 / #35859 does not cover it. The tell that distinguishes the two: the reported content.M index lands in the tool_use region of the reconstructed turn (e.g. content.17 of a turn with 9 thinking + 19 tool_use blocks), not at the front where the thinking blocks sit in Hermes' layout — because Anthropic is validating against the original interleaved layout where thinking is woven among the tool_use blocks.

Steps to Reproduce

  1. Run the gateway with an extended-thinking model (e.g. claude-opus-4-8 on the native Anthropic endpoint, interleaved/extended thinking ON).
  2. Send a message whose first turn does multi-step reasoning with a large parallel tool batch — the model emits several signed thinking blocks interleaved with the tool_use blocks.
  3. On the next turn, Hermes reconstructs the assistant message as [all thinking][text][all tool_use], the signatures no longer match, and Anthropic 400s.
  4. The gateway loops on the same error indefinitely.

Minimal unit reproduction (no network):

from agent.anthropic_adapter import convert_messages_to_anthropic
messages = [
    {
        "role": "assistant",
        "content": "Working on it.",
        "reasoning_details": [
            {"type": "thinking", "thinking": "First step.",  "signature": "sig1"},
            {"type": "thinking", "thinking": "Second step.", "signature": "sig2"},
        ],
        "tool_calls": [
            {"id": "tc_a", "function": {"name": "a", "arguments": "{}"}},
            {"id": "tc_b", "function": {"name": "b", "arguments": "{}"}},
        ],
    },
    {"role": "tool", "tool_call_id": "tc_a", "content": "result A"},
    {"role": "tool", "tool_call_id": "tc_b", "content": "result B"},
]
_, result = convert_messages_to_anthropic(messages)
# Before the fix: the latest assistant turn replays two signed `thinking` blocks
# whose signatures were computed over the original interleaved order → 400 on replay.

Expected Behavior

When Hermes cannot faithfully reproduce the original interleaved block order (it has no positional anchor in reasoning_details tying each thinking block to a specific tool_use), it should demote the thinking blocks on the latest turn to plain text — preserving the reasoning so the model can re-plan — instead of replaying dead signatures. The gateway must not enter a non-retryable crash-loop.

Actual Behavior

The reordered signed thinking blocks are replayed verbatim, Anthropic returns a non-retryable HTTP 400 ("blocks in the latest assistant message cannot be modified"), and the gateway crash-loops because the poisoned transcript is rebuilt from the store on every turn. No self-recovery; a soft session reset does not clear it.

Affected Component

Agent Core (conversation loop, context compression, memory)

Messaging Platform (if gateway-related)

N/A (CLI / gateway, native Anthropic endpoint)

Operating System

macOS 26.4 (Apple Silicon)

Python Version

3.11.15

Hermes Version

v0.15.1 (2026.5.29)

Root Cause Analysis

agent/anthropic_adapter.py:

  • _convert_assistant_message() builds the assistant turn by extracting all preserved thinking blocks first (_extract_preserved_thinking_blocks), then text, then appending all tool_use blocks. For a single leading thinking block this is fine, but for interleaved extended thinking (2+ thinking blocks woven among tool_use) it reorders the blocks relative to the signed sequence.
  • _manage_thinking_signatures() latest-assistant branch then replays any block with a signature verbatim — including the now-reordered (positionally invalid) ones — producing the 400.

The #35847 fix only handles signatures invalidated by orphan-stripping (_thinking_signature_invalidated); it does not flag the reorder case, which fires with no orphans at all.

Proposed Fix

In _manage_thinking_signatures(), treat the signature as dead on the latest turn when the turn carries 2+ thinking blocks alongside any tool_use (the only case Hermes' reconstruction reorders), and demote those thinking blocks to text — exactly as the orphan-strip path already does. A single leading thinking block is left signed (its position is unchanged → no over-fire).

PR ready with two regression tests (one for the demotion, one for the single-thinking no-over-fire guard).

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

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

hermes - 💡(How to fix) Fix [Bug]: extended-thinking + interleaved multi-thinking tool turn → non-retryable HTTP 400 crash-loop (reordered thinking signature) [3 pull requests]