[BUG] totalTokensReminder causes repeatable prompt-cache floor in tool loops; off restores incremental hits
Preflight Checklist
- [x] I searched existing issues. #88211 covers the meaning of the padded countdown, but not prompt-cache invalidation.
- [x] This is a single bug report.
- [x] I reproduced on the latest Claude Code version available to me.
What's Wrong?
With the default totalTokensReminder behavior (padded-countdown), a long tool-use loop repeatedly fell back to the same prompt-cache floor even though the conversation was growing append-only in the local transcript.
In one continuous Claude Desktop session, ten consecutive requests reported cache_read=8192 while uncached input grew from 60,327 to 77,252 tokens:
| Request | input | cache_read |
|---:|---:|---:|
| 1 | 60,327 | 8,192 |
| 2 | 63,476 | 8,192 |
| 3 | 66,656 | 8,192 |
| 4 | 68,497 | 8,192 |
| 5 | 70,562 | 8,192 |
| 6 | 74,477 | 8,192 |
| 7 | 75,292 | 8,192 |
| 8 | 76,161 | 8,192 |
| 9 | 76,735 | 8,192 |
| 10 | 77,252 | 8,192 |
The local transcript contained a changing total_tokens_reminder after tool results during this window, for example:
<total_tokens>14931235 tokens left</total_tokens>
<total_tokens>14926828 tokens left</total_tokens>
<total_tokens>14923572 tokens left</total_tokens>
I then changed exactly one user setting:
{
"totalTokensReminder": "off"
}
The setting hot-reloaded into the same real Claude Code session. I did not change the Claude Code version, Desktop version, gateway, selected model, tools, or conversation. After the change:
total_tokens_reminderattachments in the observed window: 10 before, 0 after.- The first request was expectedly cold because removing the reminder changed the system prompt:
input=89,880,cache_read=1,024. - The next request immediately recovered to
input=3,438,cache_read=90,112. - Cache reads then grew incrementally to 120,832 while uncached input stayed mostly in the hundreds or low thousands.
Selected post-change requests:
| Request | input | cache_read |
|---:|---:|---:|
| cold after setting change | 89,880 | 1,024 |
| next | 3,438 | 90,112 |
| next | 5,358 | 93,184 |
| next | 613 | 98,304 |
| next | 1,445 | 100,352 |
| next | 1,434 | 103,424 |
| next | 870 | 107,520 |
| next | 329 | 112,640 |
| next | 3,379 | 115,712 |
| next | 2,477 | 118,784 |
| final observed | 1,547 | 120,832 |
hook_success events continued to occur after the setting was disabled, while the cache prefix kept growing. No task_reminder occurred in the post-change window.
This same-session A/B result suggests that the dynamic totalTokensReminder path may be changing the effective cacheable prefix during tool loops. One possible client-side mechanism is the message normalization / reminder-to-tool_result folding family described in #49585 and the historical message drift in #48734.
I do not have the final raw request bodies because my tracing layer intentionally logs hashes and usage but omits message content, so I cannot yet name the exact differing byte or content block.
Important Provider Scope
This was observed through an Anthropic Messages-compatible LLM gateway. The selected Claude Code model alias was claude-fable-5, but the gateway routed to a non-Anthropic upstream model. The gateway, upstream model, and routing stayed unchanged throughout the same-session A/B test.
I have not reproduced this against api.anthropic.com, and I understand that non-Anthropic upstream model behavior may be outside Anthropic's support scope.
I am reporting this because the only changed variable was the Claude Code client setting, and the result matches previously reported client-side reminder re-serialization/cache-floor signatures. It would be useful for the maintainers to verify the outgoing request-body invariant using a first-party endpoint.
What Should Happen?
Changing reminder values should not mutate or re-serialize an already cached historical message prefix. A reminder can be appended at the current tail, or historical serialized messages should remain byte-stable between adjacent tool-loop requests.
With the default setting, consecutive requests should continue reading the previous request's stable message prefix instead of repeatedly falling back to the system/tools cache floor.
Steps to Reproduce
- Use default settings with
totalTokensReminderunset (padded-countdown). - Start a long-context session and run a task that produces many sequential tool calls.
- Confirm that the transcript receives changing
attachment.type = "total_tokens_reminder"entries after tool results. - Record per-request
input_tokensandcache_read_input_tokens. - If cache reads repeatedly collapse to a fixed system/tools floor, set
"totalTokensReminder": "off". - Continue the same session with the same model and tools.
- Confirm that new
total_tokens_reminderattachments stop and compare whether cache reads resume incremental growth.
For a definitive first-party reproduction, capture two adjacent outgoing /v1/messages bodies and compare the ordered messages content blocks and cache_control placement before and after each tool result.
Environment
- Claude Code:
2.1.246 - Claude Desktop:
1.37937.3 - Entry point: Claude Desktop third-party inference / gateway configuration
- Operating system: macOS
- Shell: zsh
- Context mode: 1M variant enabled
- Prompt cache controls observed: 3 on the main streaming requests
- Last locally observed version without
total_tokens_reminder:2.1.229 - First locally observed version with it:
2.1.234(this may also involve rollout/feature-gate timing, so I am not asserting a hard introduction boundary)
Related Issues
- #88211 — exact
totalTokensReminder/ 15M padded-countdown behavior, but focused on misleading context headroom. - #49585 — dynamic system reminders folded into historical
tool_result.content, breaking the prompt cache. - #48734 — byte drift in historical system-reminder content invalidating the prompt prefix.
- #63930 — repeated collapse to a system/tools-only cache floor after tool-heavy turns.
I can provide a redacted per-request timeline and message-structure metadata if useful, but I cannot publish the private conversation or tool-result contents.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗