You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
stabilizePromptCache re-attaches Claude Code's <total_tokens> footer as a user message; routed models treat every tool result as a new user turn and re-plan #6646
With claudeCode.stabilizePromptCache: true, the Claude Messages → Responses translation peels Claude Code's <total_tokens>N tokens left</total_tokens> footer off the system prompt (good for the cache) and re-attaches it as a trailing role: "user" input message on every request (src/claude/inbound.ts, the stabilized.dynamicNotice branch). The routed model reads that as a new user turn. After every tool result it starts a fresh turn: re-announces the task ("Starting on …"), re-runs orientation commands (git status, re-reads the issue), and never gets to the edit it was about to make.
Observed with Claude Code subagents routed to meta-muse/muse-spark-1.3-contributor through a combo, at low, medium and high effort: 19–302 plan restarts per agent, for hours, with the model answering normally on every turn. Short read-only tasks still finish (a fresh start just picks the next file), which hid the cause for a while.
Expected: the peeled footer stays out of the conversation, so the routed model sees only the user turns Claude Code actually sent.
Cause.stabilizeClaudeInstructionsForPromptCache returns the peeled footer as dynamicNotice; translateAnthropicRequest then does
so the conversation ends with a user message the user never wrote. PR #4052 introduced this; its review thread raised "models that take the token XML as user speech" as a concern and it was not measured.
Fix that works here. Drop the peeled notice instead of re-attaching it. Nothing on the translated route acts on Claude Code's token bookkeeping (compaction is client-side), and the cache stays stable because the footer still leaves instructions. Patched locally this way: the reproduction below answers 1, prompt-cache hits are unchanged, and the same long agent runs that looped for hours now progress.
If the notice must be kept, a role: "developer" item would at least not read as the user speaking, but I have not tested whether every Responses-compatible provider accepts that role.
Reproduction
One request, no agent needed. POST /v1/messages to a translated model with:
system: [{"type":"text","text":"You are a test harness. Answer only what is asked."}, {"type":"text","text":"<total_tokens>14900000 tokens left</total_tokens>"}]
one user message: How many user messages are in this conversation, counting this one? Answer with the number, then quote each user message verbatim on its own line.
stabilizePromptCache
model's answer
true
2, and it quotes <total_tokens>14900000 tokens left</total_tokens> as the second user message
false
1
Version
2.76.0 (the same code is present in 2.78.0-preview.20261005)
Operating system
macOS 26.6.2 (arm64)
Provider and model
meta-muse / muse-spark-1.3-contributor (via combo); not provider-specific, the extra user message is added before routing
Confirmed the translation shape on current dev f924820652: with the explicit stabilizePromptCache opt-in, src/claude/inbound.ts appends the peeled dynamicNotice as a trailing user message. Existing tests intentionally assert that behavior, so correcting it also requires changing the contract and regression coverage, not just deleting a line.
Your report establishes an extra synthetic user item; it does not by itself measure the cause of every long-running model loop. A scoped fix should separate token bookkeeping from other supported notices (for example TaskCreate guidance), preserve fenced/unmatched text and stable cache-key behavior, and leave non-opted-in/native passthrough requests unchanged. A developer-role alternative needs schema/provider compatibility evidence rather than assuming every target accepts it.
As a temporary workaround, setting the existing opt-in to false avoids this synthetic user-item relocation, at the cost of retaining the original changing system suffix; no cache-hit or agent-completion guarantee is implied. Keeping this open for the contract correction; no installed settings or live model session was changed.
Lane triage from the compat hardening sweep: confirmed on dev 38455fb that claudeCode.stabilizePromptCache: true appends the peeled footer as a trailing role: "user" input item (src/claude/inbound.ts, stabilized.dynamicNotice).
This needs a maintainer decision before a fix, so it was not changed in this sweep. The relocation to a user turn is the documented opt-in contract (docs-site Claude Code guide: "Enable it only when this role change is appropriate"), and the existing tests assert it. The options are:
Drop the <total_tokens> footer on the translated route and keep relocating only the TaskCreate nudge (smallest change; the token count has no consumer on this route).
Drop every peeled notice, as the report proposes.
Relocate as role: "developer", which needs per-provider acceptance evidence first.
Setting stabilizePromptCache back to false avoids the synthetic user turn in the meantime.
Fixed on dev by #6714 (52ebe52): with claudeCode.stabilizePromptCache the <total_tokens> footer is now dropped instead of being re-attached as a user turn; TaskCreate reminders are unchanged. It ships with the next release cut from dev.
Activity
Ingwannu commentedon Oct 6, 2026
Confirmed the translation shape on current dev
f924820652: with the explicitstabilizePromptCacheopt-in,src/claude/inbound.tsappends the peeleddynamicNoticeas a trailing user message. Existing tests intentionally assert that behavior, so correcting it also requires changing the contract and regression coverage, not just deleting a line.Your report establishes an extra synthetic user item; it does not by itself measure the cause of every long-running model loop. A scoped fix should separate token bookkeeping from other supported notices (for example TaskCreate guidance), preserve fenced/unmatched text and stable cache-key behavior, and leave non-opted-in/native passthrough requests unchanged. A developer-role alternative needs schema/provider compatibility evidence rather than assuming every target accepts it.
As a temporary workaround, setting the existing opt-in to false avoids this synthetic user-item relocation, at the cost of retaining the original changing system suffix; no cache-hit or agent-completion guarantee is implied. Keeping this open for the contract correction; no installed settings or live model session was changed.
lidge-jun commentedon Oct 6, 2026
Lane triage from the compat hardening sweep: confirmed on dev 38455fb that
claudeCode.stabilizePromptCache: trueappends the peeled footer as a trailingrole: "user"input item (src/claude/inbound.ts,stabilized.dynamicNotice).This needs a maintainer decision before a fix, so it was not changed in this sweep. The relocation to a user turn is the documented opt-in contract (docs-site Claude Code guide: "Enable it only when this role change is appropriate"), and the existing tests assert it. The options are:
<total_tokens>footer on the translated route and keep relocating only the TaskCreate nudge (smallest change; the token count has no consumer on this route).role: "developer", which needs per-provider acceptance evidence first.Setting
stabilizePromptCacheback tofalseavoids the synthetic user turn in the meantime.lidge-jun commentedon Oct 7, 2026
Fixed on dev by #6714 (52ebe52): with claudeCode.stabilizePromptCache the <total_tokens> footer is now dropped instead of being re-attached as a user turn; TaskCreate reminders are unchanged. It ships with the next release cut from dev.