standalone-reminder-strip: strip CC bookkeeping that arrives as a whole role:system message - #341
standalone-reminder-strip: strip CC bookkeeping that arrives as a whole role:system message#341deafsquad wants to merge 2 commits into
Conversation
CC injects a standalone {"role":"system"} message carrying
<total_tokens>N tokens left</total_tokens> on most turns, then prunes older
ones out of mid-history as the conversation grows. Each prune rewrites the
prefix from that index. The count changes every turn, so no two are
byte-identical and nothing can dedupe them.
Measured on one private deployment (CC 2.1.223, 2026-08-15), one session:
483-529 markers in a single body, ~30% of the message list; a prune at warm[9]
cost a 224,979-token cold write, and one at warm[1362,1441,1508,1537] cost
771,212.
content-strip cannot reach it: BOOKKEEPING_PATTERNS matches
<system-reminder>-wrapped blocks INSIDE a message, and this arrives as a whole
unwrapped role:system message.
Stripping beats repairing — a marker that never leaves the proxy cannot be
pruned, so CC's pruning of them becomes invisible to the cache.
Guards the subagent usage block (<usage><total_tokens>N</total_tokens>...) on
three independent grounds, and skips any marker carrying cache_control so a
removal can never collapse the breakpoint set.
Renames total-tokens-strip and widens it from one marker to the location. CC emits bookkeeping as a WHOLE role:system message as well as a <system-reminder>-wrapped block. content-strip already owns the vocabulary and strips the wrapped shape; this covers the same content one location over, by importing BOOKKEEPING_PATTERNS rather than restating them — so a pattern added to content-strip is covered in both places without a second edit. The second measured member is content-strip's own task reminder. It is in BOOKKEEPING_PATTERNS already but only reachable wrapped, and CC emits it standalone AND inconsistently between consecutive requests — present at 02:05:22, absent at 02:06:12 on one session. Three cold writes replayed against their archived predecessors all ended the same way: the warm body carried a standalone role:system the current body did not. Two broke at exactly that message, prefix 794->826 and 862->898. Importing rather than copying also picks up the TodoWrite reminder, the token/USD counters and the auto-compact countdown in this location for free. content-strip gains one export line. All guards unchanged: the subagent usage block, cache_control-carrying messages, mixed content, both serializations. 17/17 here, content-strip 22/22.
|
Widened this before anyone spent review time on the narrow version — sorry for What changed. The defect is not really "the budget marker": it is that CC emits bookkeeping as The second measured member is your own task reminder. Importing rather than copying also picks up the TodoWrite reminder, the Unchanged: every guard from the first revision. The subagent 17/17 here, If you would rather this lived inside — Claude Opus 5, working with @deafsquad |
The defect
CC injects a standalone
{"role":"system"}message carryingon most turns, then prunes older ones out of mid-history as the
conversation grows. Each prune rewrites the prefix from that index. The count
changes every turn, so no two markers are byte-identical and nothing can dedupe
them.
Measured on one private deployment (CC 2.1.223, 2026-08-15), a single session:
warm[9]warm[1362,1441,1508,1537]It is CC's, not a proxy artifact:
"tokens left"appears in the CC 2.1.223binary and not in an April build.
Why
content-stripdoes not already cover itBOOKKEEPING_PATTERNSis gated onREMINDER_WRAP_REGEX— it matches<system-reminder>-wrapped blocks inside a message. This arrives as a wholeunwrapped
role:systemmessage, so that gate cannot see it. I checkedbefore writing a new extension; happy to fold it into
content-stripinsteadif you would rather, though the message-level vs block-level distinction is
why I did not.
Approach
Strip rather than repair. A marker that never leaves the proxy cannot be
pruned, so once every outbound body omits them CC's pruning of them is
invisible to the cache. The test models that directly: warm and cold bodies
differing only by a pruned marker share a prefix of 1; after the strip, 3.
Safety
<usage><total_tokens>N</total_tokens><tool_uses>…</usage>inside subagenttool_results is parsed back by CC with/<total_tokens>(\d+)<\/total_tokens>/and operators rely on it forper-session subagent cost. Safe on three independent grounds: the pattern is
anchored at
^<total_tokens>so it cannot match a string starting with<usage>; it requires" tokens left", which the digits-only form lacks; andthe gate only inspects
role:systemmessages while that copy lives in arole:usertool_result. Four tests guard it, including an end-to-end one.cache_controlis kept. Removing a message shiftsevery later index, and a removed message carrying a breakpoint takes the
breakpoint with it. Two live incidents on the deployment this came from (664k
and 251k cold) were exactly that, on a different class of standalone
role:systemmessage. This class has never been observed carrying one, sothe guard should never fire — it can only under-strip.
string or as a single
[{type:"text"}]block and flips between them ashistory ages (122 role:system messages flipped inside one 28-second window in
the capture). Matching one form reproduces the drift one level down.
it.
Default ON
Unlike #340, this one defaults on (
CACHE_FIX_STRIP_TOTAL_TOKENS=0todisable). It removes bytes the model never reads, and shipping a measured
six-figure cache cost as opt-in seemed the wrong default. Say the word and I
will flip it.
Verification
node --test test/proxy-total-tokens-strip.test.mjs→ 14/14. Also ransession-key-invariants(4/4),proxy-pipeline(15/15) andproxy-content-strip(22/22) locally; your CI is the authoritative run.tools/absence-scan.mjsclean over both new files.Load-bearing?
Yes — it removes messages from the outbound body.
— Claude Opus 5, working with @deafsquad