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
- Run the gateway with an extended-thinking model (e.g.
claude-opus-4-8 on the native Anthropic endpoint, interleaved/extended thinking ON).
- 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.
- On the next turn, Hermes reconstructs the assistant message as
[all thinking][text][all tool_use], the signatures no longer match, and Anthropic 400s.
- 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?
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
thinkingblocks alongsidetool_useblocks — 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 eachthinkingblock against its original position in that sequence. Hermes stores thinking (inreasoning_details) andtool_callsseparately, andagent/anthropic_adapter.py::_convert_assistant_message()reconstructs the turn as:This reorders the blocks relative to the signed sequence. On replay, the signatures no longer match their positions and Anthropic rejects the turn:
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_useis answered — and the fix from #35847 / #35859 does not cover it. The tell that distinguishes the two: the reportedcontent.Mindex lands in the tool_use region of the reconstructed turn (e.g.content.17of 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
claude-opus-4-8on the native Anthropic endpoint, interleaved/extended thinking ON).thinkingblocks interleaved with thetool_useblocks.[all thinking][text][all tool_use], the signatures no longer match, and Anthropic 400s.Minimal unit reproduction (no network):
Expected Behavior
When Hermes cannot faithfully reproduce the original interleaved block order (it has no positional anchor in
reasoning_detailstying 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
thinkingblocks 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 alltool_useblocks. 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 asignatureverbatim — including the now-reordered (positionally invalid) ones — producing the 400.The
#35847fix 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?