[BUG] max_tokens recovery mutates prompt history, invalidates cache, and drops the interrupted thinking output
Preflight Checklist
- [x] I searched existing issues and found no report covering these specific request-payload and cache transitions.
- [x] This is a single bug report: all three symptoms occur in Claude Code's
max_tokensrecovery path. - [x] Reproduced on Claude Code 2.1.220, the latest stable version at the time of reproduction.
What's Wrong?
Affected version: 2.1.220 (Claude Code), the latest stable version at the time of reproduction.
In two independent sessions, a response reached stop_reason=max_tokens at the 64K output limit. Claude Code then injected this hidden recovery message:
Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.
The recovery path produced three related problems.
1. Inconsistent continuation request assembly
When max_tokens occurred on the initial request, the continuation rewrote the original two-message prefix:
-[user: U]
-[system: S]
+[user: U + <system-reminder>S</system-reminder> + recovery]
The merged form remained throughout that top-level query. When the query ended and a notification triggered the next top-level user turn, the prefix returned to its original structure:
-[user: U + <system-reminder>S</system-reminder> + recovery]
+[user: U]
+[system: S]
...
+[assistant: previous task's final response]
+[user: notification]
At this transition, the prompt-cache hit rate dropped from 99.52% to 15.94%.
In the other session, where max_tokens occurred after conversation history already existed, Claude Code preserved all prior messages and only appended recovery:
...
[user: tool_result]
+[user: recovery]
That continuation correctly preserved the existing prefix and achieved a 99.85% cache hit rate.
2. Recovery is later deleted from existing history
Recovery remained in the prompt across many tool-use requests in the same top-level query. After end_turn, the next user query removed it from the earlier history while normally appending the completed response and new user message at the tail:
[user: U]
[system: S]
...
-[user: recovery]
[assistant: responses generated after recovery]
...
+[assistant: previous task's final response]
+[user: new query]
Apart from the normal tail additions, recovery was the only historical message removed. The cache transition was:
| State | Cache read | Cache creation | Cache hit |
|---|---:|---:|---:|
| Recovery still present | 244,344 | 532 | 99.78% |
| New user query removes recovery | 29,693 | 216,923 | 12.04% |
| Next request reuses the rebuilt prefix | 246,616 | 1,804 | 99.27% |
Deleting a message that already participated in many generations retroactively changes the prompt prefix and forces a large one-time cache rebuild.
3. The interrupted 64K thinking output is not continued
Both max_tokens responses consisted almost entirely of thinking, but the continuation input grew by only a few dozen tokens:
| Occurrence | Output / Thinking | max_tokens request input | Continuation input | Increase |
|---|---:|---:|---:|---:|
| Mid-conversation | 64,000 / 64,000 | 43,423 | 43,486 | 63 |
| Initial request | 64,000 / 63,999 | 33,402 | 33,479 | 77 |
Input is calculated as cache_read_input_tokens + cache_creation_input_tokens + input_tokens.
The continuation contained no assistant message corresponding to the truncated response, no thinking block or signature from that response, and the signature never appeared in later requests. The model is therefore asked to “Pick up mid-thought” without receiving the interrupted thought in its subsequent prompt history.
What Should Happen?
- Continuation should append recovery without rewriting the existing message prefix.
- Once recovery has participated in subsequent requests, it should not be removed from existing history when a new user query arrives. If it must be scoped to one top-level query, that scope should not require mutating the prior prefix.
- The continuation should include the interrupted assistant output or equivalent resumable state. If that state cannot be retained, the recovery instruction should not tell the model to continue
mid-thought.
Error Messages/Logs
There is no user-visible error. The issue is observable in outbound Messages API payloads and response usage fields.
stop_reason=max_tokens
output_tokens=64000
thinking_tokens=64000 (63999 in the other occurrence)
cache hit after deleting recovery:
99.78% -> 12.04%
Steps to Reproduce
- Run Claude Code with extended thinking and capture its outbound Messages API request bodies and response usage.
- Use a task that causes an Opus response to reach the 64K output limit and return
stop_reason=max_tokens. - Inspect the immediate continuation request:
- If the limit is reached on the initial
[user][system]request, compare the first messages before and after recovery. - If the limit is reached after conversation history exists, verify that recovery is appended as a hidden user message.
- Allow the recovered task to continue through several tool-use turns and return
end_turn. - Send another top-level user query and compare the message arrays. Recovery is removed from the earlier history.
- Compare
cache_read_input_tokensandcache_creation_input_tokensacross the boundary. - Search the continuation and later requests for the
max_tokensresponse's assistant content and thinking signature; neither is present.
Claude Model
Opus (claude-opus-4-8)
Is this a regression?
I don't know.
Last Working Version
Unknown.
Claude Code Version
2.1.220 (Claude Code) — the latest stable version at the time of reproduction.
Platform
Other — Anthropic-compatible endpoint. The reported behavior is present in Claude Code's outbound request payload before provider processing.
Operating System
Other Linux
Terminal/Shell
Non-interactive/CI environment
Additional Information
- Related: #64589 documents the hidden output-token recovery loop, but not the concrete message rewrites/removal and cache transitions reported here.
- Related: #74427 reports correctness failures at the output-token-limit resume boundary, but focuses on fabricated tool results rather than prompt-history and cache behavior.
- Sanitized request-payload diffs and usage data can be provided if needed.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗