Resumed extended-thinking sessions permanently 400 with "thinking blocks ... cannot be modified" (esp. after model change / with /goal)
Resolved 💬 2 comments Opened May 28, 2026 by adamfarag Closed Jul 2, 2026
Summary
A long-running session that uses extended thinking + tool use can get into a state where every turn fails with:
API Error: 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.
Once it starts, the session is permanently unusable — every subsequent user message 400s at the same messages.N.content.M index. The session can receive messages but can never respond.
Environment
- Claude Code 2.1.154–2.1.156 (reproduced on both macOS arm64 and Linux)
- Models: Opus 4.8 (
claude-opus-4-8) and 4.7; high/xhigh effort (extended thinking enabled) - Sessions launched with
--channels,--permission-mode bypassPermissions, resumed with--resume <id>
Reproduction (reliable)
- Run a long session with extended thinking + tool use (e.g. a coding session at high effort, many tool calls).
- Resume it with
--resume <id>— especially after changing the model (e.g. relaunch the same conversation with--model opus[1m]when it previously ran on 4.7). - On the next turn, the request 400s with the error above, and keeps 400ing on every subsequent message.
- An active
/goalloop makes it constant — the loop regenerates thinking every iteration and errors every few seconds.
Impact
- The session is effectively bricked (can't respond at all).
- During a fleet upgrade (resuming many sessions from 4.7 → 4.8), multiple sessions hit this independently.
Debugging findings
- Stripping all
thinking/redacted_thinkingcontent blocks from the assistant messages in the session.jsonl, then resuming, temporarily fixes it (clean resume, no error). But new thinking blocks regenerate on subsequent turns and it re-breaks — the strip is not durable. - The error index (e.g.
messages.47.content.26) stays constant even after the.jsonlis verified (JSON-parsed) to contain 0 thinking blocks. So the rejected blocks appear to come from somewhere other than the current on-disk conversation — prompt-cache replay, in-memory state, or a/goalloop's separate context — OR the persisted blocks have emptied text with a retained signature that fails validation on reload. - Occurs both across a model-version change and within continued use of a single model.
/rewind(restore conversation only) to before the offending turn also helps, but if a/goalwas active it re-activates on resume and re-breaks.
Suspected cause
Thinking-block signatures (model-specific, computed over the original thinking text) are invalidated on persist/reload. The API requires the latest assistant turn's thinking blocks to be passed back byte-identical; the persisted/replayed version doesn't match (emptied text and/or model-version mismatch), so the API rejects it.
Workarounds (none fully durable)
- Strip thinking blocks from the
.jsonl+ resume (re-breaks as new blocks accumulate) - Lower effort / disable extended thinking (no blocks generated → nothing to corrupt) — most reliable
/goal clearto stop a looping goal that accelerates the failure
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗