Deferred-tool call emitted in the same turn as its `ToolSearch` load permanently bricks the session via thinking-block 400 (extended thinking)

Open 💬 0 comments Opened Jun 21, 2026 by jeffroizen-web

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

  1. Run Claude Code 2.1.150 on claude-opus-4-8 with extended thinking on.
  2. Have a deferred tool available (e.g., CronCreate, surfaced by name in a <system-reminder> deferred-tools block; schema not yet loaded).
  3. In one turn the model emits: a thinking block, ToolSearch(select:CronCreate,…), and a CronCreate(…) call — i.e., it loads and uses the tool in the same generation.
  4. CronCreate fails with InputValidationError (schema wasn't loaded → typed params get stringified / wrong keys guessed).
  5. The next request returns 400 … thinking blocks … cannot be modified.
  6. 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. The CronCreate input 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)

  1. Preserve thinking blocks byte-identical when reconstructing a multi-tool-use assistant message after a tool error (don't reorder/strip surrounding content).
  2. 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.
  3. 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 /compact can recover, instead of permanently bricking.
  4. At minimum, make /compact recover — 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).

View original on GitHub ↗