fix(anthropic): demote dead thinking signature when orphan-strip mutates the latest turn#35859
Merged
Conversation
…tes the latest turn
Extended-thinking Claude models (4.6+, e.g. Opus 4.8) emit a signed `thinking`
block on assistant turns that also carry parallel `tool_use` blocks. Anthropic
signs that block against the full, original turn content.
When a parallel tool batch is interrupted before every `tool_result` returns,
`_strip_orphaned_tool_blocks` removes the unanswered `tool_use` on replay — which
mutates the turn. The latest-assistant branch of `_manage_thinking_signatures`
then replays the now-stale signed thinking block verbatim, and Anthropic rejects
the request with a non-retryable HTTP 400:
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.
Because the poisoned turn is rebuilt from the persisted store every turn, the
gateway crash-loops with no self-recovery (a soft session reset does not clear
it). The drifting content index in the error is the changing count of stripped
`tool_use` blocks across rebuilds.
Fix: when orphan-stripping removes a `tool_use` from a turn that also holds a
thinking/redacted_thinking block, flag the turn. `_manage_thinking_signatures`
then demotes every thinking block on that latest turn to a plain text block
(preserving the reasoning text) instead of replaying a signature that can no
longer validate. An intact turn is unaffected — its signed thinking is still
replayed verbatim. The internal flag is stripped before the payload is sent.
Adds two regression tests:
- demotion when an orphaned parallel tool_use is stripped
- control: signed thinking preserved verbatim when nothing is stripped
Contributor
🔎 Lint report:
|
7 tasks
Closed
9 tasks
This was referenced May 31, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extended-thinking Claude models (Opus 4.8 etc.) no longer crash-loop the gateway with a non-retryable HTTP 400 when a parallel tool batch is interrupted before every tool result returns.
Root cause:
_strip_orphaned_tool_blocks()legitimately removes atool_usewhose matchingtool_resultnever arrived, but that mutates the latest assistant turn._manage_thinking_signatures()then replays the signedthinkingblock verbatim — its Anthropic signature was computed over the original (un-stripped) turn, so Anthropic rejects it:thinking blocks in the latest assistant message cannot be modified. The 400 is non-retryable and the transcript is rebuilt from the store every turn → infinite loop.Salvage of #35846 by @fesalfayed onto current
main(cherry-picked, authorship preserved). Fixes #35847.Changes
agent/anthropic_adapter.py: flag a turn_thinking_signature_invalidatedwhen orphan-strip mutates a thinking-bearing turn; propagate the flag through_merge_consecutive_roles; in_manage_thinking_signaturesdemote the dead signed thinking block to a plain text block (reasoning preserved) instead of replaying it. Intact turns keep their signed thinking verbatim. Internal flag stripped before payload send.tests/agent/test_anthropic_adapter.py: regression + control test.scripts/release.py: AUTHOR_MAP entry for the contributor email (our follow-up commit).Validation
tool_usestripped, latest turnTargeted suite: 23 passed (
-k "thinking or signature or orphan or merge or preserved or redacted"). E2E on realconvert_messages_to_anthropic(no mocks) confirms both cases.Infographic