#22270: fix: add auto-recovery for thinking block immutability errors
agents
size: M
Cluster:
Tool Call ID Sanitization
Closes #22233
## Problem
The Anthropic API requires that `thinking` and `redacted_thinking` blocks in
the latest assistant message are sent back **byte-for-byte unchanged**,
including the `thinkingSignature` field. When session sanitizers strip or
modify those blocks, the API rejects the request with an
`invalid_request_error`:
> *"thinking or redacted_thinking blocks in the messages cannot be modified"*
Before this PR there was no handler for this error, so it surfaced as an
unhandled exception with no user-friendly guidance. Unattended channel
sessions (Telegram, Discord, etc.) would stay permanently broken until
someone manually ran `/new` or `/reset`.
---
## Root Cause
`resolveTranscriptPolicy` set `preserveSignatures: false` for standard
Anthropic models — only Antigravity Claude had it set to `true`. This caused
`sanitizeSessionHistory` to pass thinking blocks through
`stripThoughtSignatures`, which could strip or modify the `thinkingSignature`
field before the session history was submitted to the API, violating the
immutability requirement for the latest assistant turn.
---
## Changes
### `src/agents/pi-embedded-helpers/errors.ts`
Added `isThinkingImmutabilityError(raw: string): boolean` — a regex-based
classifier that detects the Anthropic thinking block immutability error
message.
### `src/agents/pi-embedded-helpers.ts`
Re-exported `isThinkingImmutabilityError` from the barrel file.
### `src/agents/pi-embedded-runner/types.ts`
Added `"thinking_immutability"` to the `EmbeddedPiRunMeta` error kind union
so the new kind is type-safe throughout the codebase.
### `src/agents/transcript-policy.ts` — root cause fix
Changed `preserveSignatures` from `isAntigravityClaudeModel` to `isAnthropic
|| isAntigravityClaudeModel`. This ensures `sanitizeSessionHistory` never
passes thinking blocks through `stripThoughtSignatures` for standard
Anthropic models, preventing the `thinkingSignature` from being stripped
before the session history is sent to the API.
### `src/agents/pi-embedded-runner/run.ts` — auto-recovery handler
When the immutability error is detected inside the `promptError` branch of
`runEmbeddedPiAgent`, the handler:
1. **Auto-resets** — clears the session file and retries with a fresh
session, keeping unattended channel sessions (Telegram, Discord, etc.) alive
without manual intervention. Mirrors the pattern used by the compaction
recovery handler.
2. **Single-attempt guard** — the reset is only attempted once per run via a
`thinkingImmutabilityResetAttempted` flag, preventing infinite retry loops if
the error persists after the reset.
3. **Graceful fallback** — if the session file path is empty or cannot be
cleared, returns a user-facing error payload with `meta.error.kind:
"thinking_immutability"` and guidance to `/new` or `/reset`.
---
## Tests Added
| File | Tests | What is covered |
|---|---|---|
| `errors.isThinkingImmutabilityError.test.ts` | 7 | Empty string, exact
Anthropic error message, embedded in longer payload, case-insensitivity,
false positives for role ordering / context overflow / generic errors |
| `run.thinking-immutability.test.ts` | 4 | Auto-reset clears session file
and retries, fallback when sessionFile is empty, single-attempt guard
prevents infinite retry, unrelated prompt errors are unaffected |
| `pi-embedded-runner.sanitize-session-history.test.ts` | 1 | Asserts
`preserveSignatures: true` is passed for `anthropic-messages` provider |
| `run.overflow-compaction.mocks.shared.ts` | — | Added
`isThinkingImmutabilityError` to the shared `vi.mock` so existing tests remain correct |
---
## Test Plan
- [ ] All new tests pass
- [ ] Existing overflow compaction and usage reporting tests unaffected
- [ ] `tsgo --noEmit` reports no type errors
- [ ] `oxfmt` formatting check passes
- [ ] `oxlint` reports no lint errors
- [ ] Session with a thinking block immutability error auto-resets and
retries instead of crashing
- [ ] Unattended channel sessions recover automatically without manual `/new`
or `/reset`
- [ ] If auto-reset fails, user receives a clear error message with recovery
instructions
Most Similar PRs
#15379: fix(antigravity): strip unsigned thinking blocks in agent loop via ...
by jg-noncelogic · 2026-02-13
77.7%
#18926: fix(agents): preserve thinking signatures for direct Anthropic API
by BinHPdev · 2026-02-17
77.3%
#20050: fix: Telegram polling regression and thinking blocks corruption (AI...
by Vaibhavee89 · 2026-02-18
75.9%
#20945: fix: strip thinking blocks with field-name signatures from OpenAI-c...
by austenstone · 2026-02-19
73.9%
#19407: fix(agents): strip thinking blocks on cross-provider model switch (...
by lailoo · 2026-02-17
73.5%
#16100: fix: convert unsigned thinking blocks to text to prevent signature ...
by claw-sylphx · 2026-02-14
73.3%
#6685: fix: suppress thinking leak for Synthetic reasoning models
by AkiLetschne · 2026-02-01
73.1%
#23462: fix: extract thinking blocks as fallback in extractTextFromChatContent
by nszhsl · 2026-02-22
72.9%
#10097: fix: add empty thinking blocks to tool call messages when thinking is…
by cyxer000 · 2026-02-06
72.8%
#18935: fix(agents): suppress reasoning blocks from channel delivery
by BinHPdev · 2026-02-17
72.4%