﻿# Thinking blocks are repaired after a 400 JP could have predicted

- **Status**: Todo
- **Kind**: Feature
- **Authors**: jp
- **Date**: 2026-09-22
- **Label**: domain=llm
- **Label**: llm-provider=anthropic
- **Label**: package=jp_llm
- **Label**: type=enhancement

Anthropic validates replayed `thinking` blocks against the request that would
have produced them.
A request whose `system`, `tools`, or earlier messages changed since the blocks
were generated is rejected with `thinking blocks ... cannot be modified`, and a
block whose signature does not validate for the model being called is rejected
with `Invalid 'signature' in 'thinking' block`.
For accounts created on or after 2026-08-31 the first of these is the default
rather than an opt-in.

JP handles both, in `anthropic.rs`: `classify_thinking_rejection` names the
rejection, `build_thinking_patches` rewrites the whole assistant turn holding
the offending block as `<think>` text, and the stream yields
`FinishReason::Retry` so `turn_loop` reissues the request.
It is tested and it converges.

It is also entirely reactive.
Every repair costs a round trip that ends in a 400, and it pays for that round
trip by discarding that turn's native reasoning — the API keeps the text, but
the model no longer sees the blocks as its own.
On a turn-scoped rejection that is one round and one turn's reasoning.
On a signature rejection spanning a long conversation it is one round per turn
that carries thinking.

## What JP already knows

The conversation's config is an ordered series of deltas in the stream
(`jp_conversation::stream::config_delta`), so the model, the tools, and the
system prompt in force at every point are recoverable by folding forward.
When JP is about to issue a request it can compare the config that produced the
stored thinking blocks against the config it is about to send, and strip the
provider metadata itself when the two disagree in a way the API rejects.
That turns a failed request plus a repair into no failed request at all, and
lets JP say what it dropped and why instead of logging a warning about someone
else's 400.

## Why this surfaces now

Two of JP's own features are the trigger.
A `--cfg` delta applied mid-conversation changes `system` or `tools` between one
turn and the next, which is the exact edit the append-only requirement forbids.
Changing `assistant.model.id` mid-conversation moves the conversation to a model
that may not read the stored blocks at all.

Claude Opus 5.5 makes both more likely: thinking cannot be turned off, so every
assistant turn carries blocks, and its blocks are read only by Opus 5.5, Fable
5.1, and Mythos 5.1.

## To settle first

Whether a cross-model replay is a 400 or a silent drop.
The migration guide's wording — "runs those turns without them" — reads like
the blocks are ignored rather than rejected, which would make the model-switch
half of this a silent context loss rather than a failed request.
If so it still deserves a notice, but it is a different fix from the
tools-and-system half, and possibly a different ticket.

## Severity

Low and self-correcting today.
The repair works, and the user sees a slower turn rather than a broken one.
It gets worse the longer a conversation runs and the more often its config
changes, and it is silent about the reasoning it discards to get unstuck.
