Deferred-tool call emitted in the same turn as its `ToolSearch` load permanently bricks the session via thinking-block 400 (extended thinking)
Environment
- Claude Code: 2.1.150
- Model:
claude-opus-4-8(1M-context variant[1m], extended thinking enabled, max effort) - Platform: macOS (darwin)
- Context: long-running autonomous CLI session
Summary
When the assistant emits, in a single generation, (a) an extended-thinking block, (b) a ToolSearch to load a deferred tool's schema, and (c) a call to that same deferred tool — and the deferred-tool call then fails input validation (because its schema wasn't loaded yet) — the session enters an unrecoverable state. Every subsequent request returns:
API Error: 400 messages.13.content.4: `thinking` or `redacted_thinking` blocks in the
latest assistant message cannot be modified. These blocks must remain as they were in the
original response.
/compact fails with the same 400. The only recovery is killing and relaunching the session (all context lost).
Severity: High (for autonomous / long-running agents)
A single, easy-to-make tool-call mistake permanently bricks the session with no in-session recovery. In a fleet of autonomous agents, a worker that hits this loses all working context and needs an external restart.
Steps to reproduce
- Run Claude Code 2.1.150 on
claude-opus-4-8with extended thinking on. - Have a deferred tool available (e.g.,
CronCreate, surfaced by name in a<system-reminder>deferred-tools block; schema not yet loaded). - In one turn the model emits: a thinking block,
ToolSearch(select:CronCreate,…), and aCronCreate(…)call — i.e., it loads and uses the tool in the same generation. CronCreatefails withInputValidationError(schema wasn't loaded → typed params get stringified / wrong keys guessed).- The next request returns
400 … thinking blocks … cannot be modified. - Every subsequent message — and
/compact— returns the same 400. Session bricked.
Expected
A recoverable tool-input validation error should leave the session recoverable: the model reads the error, loads the tool properly (separate turn), and retries. At worst, /compact should recover.
Actual
The session is permanently bricked. Appending the tool error and re-sending makes the assistant message's thinking block "modified" per the API, violating the extended-thinking requirement that thinking blocks be byte-identical → 400 on every later request, including /compact.
Evidence (real transcript)
Session d06064a7 (JSONL transcript). Sequence:
- One assistant message (id
msg_01NHz9…) contains a thinking block +ToolSearch(select:CronCreate,CronList,CronDelete)+CronCreate(input={schedule,…,model,notify,allowedTools,prompt})— multiple tool_uses in one generation. TheCronCreateinput used guessed params because the real schema wasn't loaded yet. - Tool result:
> InputValidationError: CronCreate failed … The required parameter 'cron' is missing … unexpected parameter 'schedule' … This tool's schema was not sent to the API — it was not in the discovered-tool set derived from message history … Load the tool first: call ToolSearch with query "select:CronCreate", then retry.
- Immediately after (synthetic assistant message):
> API Error: 400 messages.13.content.4: 'thinking' or 'redacted_thinking' blocks in the latest assistant message cannot be modified.
- The same 400 recurs at every subsequent timestamp (observed +1h and +1.5h later) and on
/compact:
> Error during compaction: API Error: 400 messages.13.content.4: 'thinking' … cannot be modified.
- Recovery required killing the process and relaunching a fresh session.
Root-cause hypothesis
The deferred-tool-loading mechanism processes ToolSearch inline and continues the same assistant generation with the deferred-tool call. When that call errors and the harness builds the next request (appending the tool_result), the assistant message's content blocks get reconstructed in a way that alters the extended-thinking block (content.4 of messages.13). Extended thinking requires thinking blocks returned byte-identical; the modification → 400. The corrupted message is now fixed in history, so every later request — and /compact's summarization request (which re-sends history) — re-hits it.
Suggested fixes (any one mitigates)
- Preserve thinking blocks byte-identical when reconstructing a multi-tool-use assistant message after a tool error (don't reorder/strip surrounding content).
- Don't emit a deferred-tool call in the same generation as its
ToolSearch— defer the call until the schema is loaded (next turn) so it can't be emitted unschema'd. - Provide an in-session recovery path — when a request 400s on thinking-block immutability, let the harness repair/drop the offending message client-side so a retry or
/compactcan recover, instead of permanently bricking. - At minimum, make
/compactrecover — it currently re-sends the corrupted history and re-hits the 400.
Current workaround
- Behavioral: never call a deferred tool in the same turn as its
ToolSearch— load in one turn, call in the next with the real schema. - Recovery: if a session 400s repeatedly on thinking blocks, kill and relaunch (context lost).