feat(insertion-normalization): pin volatile reminder blocks so mid-history rewrites stop busting the cache - #272
Conversation
|
Production soak status, for merge timing — stated so the evidence base is explicit rather than implied:
We'll post a week-of-soak summary on this thread as the daily gate accumulates runs. Happy to hold the merge until then if you prefer. 🤖 Generated with Claude Code |
|
One consequence worth knowing during review: the canonical content identity here supersedes 🤖 Generated with Claude Code |
… serves both trees The suppression test rides upstream slice cnighswonger#272, which carries the extension but not tools/; a static import made the whole file error there. The real-pair test already skips without the capture — the same skip now covers a tree without the tooling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZxGrF1LRBvmb7cFXmS2DH
…, output-form metric, suppression exemptions The gate's mitigation metric was input-side only: it trusted the extension's self-report and never compared what was actually forwarded. Now each mitigation row carries outputForm (append / splice@N / edit@N), outputPreserved, and rebilledOutBytes — measured on the forwarded bytes — which is how a "mitigated" pair that still re-billed 124k was caught. The census classifies reminder block-migrations (inline <-> standalone) on splice and edit rows, and the safety and stability checks gain telemetry-sourced exemptions for the new suppression (a removed message has no shape to detect after the fact, so exemption keys off the extension's own suppression records). Extension synced to the cnighswonger#272 tip; real-pair red-green tests run in this slice, where tooling, extensions, and capture meet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZxGrF1LRBvmb7cFXmS2DH
|
Pushed c713d0e: the pin now also suppresses the migrated duplicate (anthropics/claude-code#76606). Background: CC can migrate a hook reminder out of its tool_result into a standalone system message mid-history; pinning restored the first-seen form but the migrated copy still forwarded, so the same content went upstream twice — measured live as a ~61 kB mid-history splice re-billing 124k tokens on one turn. A standalone message whose wrapper-normalized bytes equal a live pinned block is now suppressed (never forwarded, never given canonical identity); genuine changes still forward and reset. New test file covers the helpers, the classify-level behavior, cross-request stability, and the genuine-change escape (57 pass / 1 skip in this slice — the real-capture red-green check needs the replay tooling and runs in #276, where it passes against the original capture). 🤖 Generated with Claude Code |
…tput metric; extensions synced Three replay improvements from operating the gate: an unmissable stderr warning when a gated capture replays under default gates (the instrument error that booked a wrong verdict three times in one day — and whose first live fire caught the operator's own gateless replay); a --gates-from-capture flag applying the all-boot-records union so nobody hand-extracts gates; and outputForm now strips cache_control before comparing (a moved cache marker is not a content splice — five pairs totalling ~0.6 MB of phantom "re-billed splice" were CC's own benign marker relocation). Extensions synced to the cnighswonger#272/cnighswonger#273 tips so the slice's real-capture tests exercise the actual pipeline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZxGrF1LRBvmb7cFXmS2DH
…ls-less tree skips, not dies The static harvest.mjs import defeated the file's own slice-portability idiom: in trees carrying only the extension (upstream cnighswonger#272/cnighswonger#278/cnighswonger#281) the whole file failed at module load, so the real-pair check never reached the skip it was designed to hit. The reader now loads like replayTools — dynamically, inside the test — and the fixture branch requires it, so tools-less trees skip with COULD NOT VERIFY. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcivCe2iLnKZxpB4qTXzEb
… + tail guard Brings proxy/extensions/insertion-normalization.mjs and its real-pair test to the cnighswonger#272 slice tip (fork e0f8fcb): the merged-standalone join-hash set and the tail-position suppression guard. The extension rides here so this slice's tools replay the same behaviour cnighswonger#272 ships. Co-Authored-By: Claude opus-5 <noreply@anthropic.com>
…ent (output slice of fork e0f8fcb) Path-scoped slice of fork commit e0f8fcb: the guard's fifth invariant (assistant-terminal) and its tests. The same commit's insertion-normalization tail guard belongs to the insertion slice (cnighswonger#272); this slice carries the extension only as a sync, in the following commit. Co-Authored-By: Claude opus-5 <noreply@anthropic.com>
… + tail guard Brings proxy/extensions/insertion-normalization.mjs and its real-pair test to the cnighswonger#272 slice tip (fork e0f8fcb) so this slice's output-guard invariant is exercised against the same insertion behaviour cnighswonger#272 ships. The pinned real-pair fixture is NOT part of this slice — the real-pair test skips without it, by design. Co-Authored-By: Claude opus-5 <noreply@anthropic.com>
|
Pushed wave 2 for this slice — five commits. 1ca82f0 adds the real-pair suppression test with a pinned-fixture fallback (the 15k-line harvested capture rides along, sanitized, 0 addresses); a270da0 handles the merge variant — a standalone reminder equal to the join of its pinned blocks is suppressed (the shape behind a measured 587k-token re-bill); a80c481 stops tail-stripping — a final-message duplicate is payload, never stripped. 47ff9c3 makes the test file slice-portable: the fixture reader now loads dynamically like the replay tools, so in this tree the real-capture check reaches its designed skip instead of failing at module load; it executes in #276 where the tooling lives. b713b2f carries the oscillation fixture the merge test reads. Tests here: 67 pass / 1 skip (the designed one) / 0 fail; byte-equal to fork tip da9bf8c across every shared path. 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Review: PR #272 insertion-normalization / volatile-block pinning
Date: 2026-07-31
Reviewed: PR #272 at b713b2f, merged onto origin/main 81f47a5
Round: 1
Label applied: changes-requested
What Is Correct
- The default-off gate is real.
onRequest()returns before any state load/write whenCACHE_FIX_INSERTION_NORMALIZEis unset, so the extension is a true no-op in the default configuration (proxy/extensions/insertion-normalization.mjs:1015). - The conservative reset paths for non-volatile edits and adjacency violations are present and well-tested. I verified the merged suite exercises the intended reset classes and the plain-mode classifier keeps the "reset honestly" behavior for non-volatile changes.
proxy/extensions/message-hash.mjs:1is a genuine shared primitive, not speculative reuse. #273 also depends on the same content-identity logic, so pulling it out of the removed ladder code is proportionate.- The fixture text itself appears tokenized rather than shipping raw prompts. I did not find raw prompt prose, file paths, usernames, or API tokens in
test/fixtures/harvested/pinned-s-633915a8-26-28.json.
Blockers
- A message whose only legitimate change is inside the
<system-reminder>block does not reset honestly; the proxy forwards the stale first-seen bytes instead. Pin-mode identity explicitly drops volatile blocks from the hash (proxy/extensions/insertion-normalization.mjs:409), stores the first-seen bytes inentry.m(proxy/extensions/insertion-normalization.mjs:584), and replays those bytes on later matches (proxy/extensions/insertion-normalization.mjs:606,proxy/extensions/insertion-normalization.mjs:742). I reproduced this locally: changingOLDtoNEWinside the reminder still classified asnormalizedand forwardedOLD. On a body-mutating wire path, silently overriding CC's current bytes is a conversation-corruption risk, not just a cache miss. - The large harvested fixture is not actually fully sanitized as claimed. Prompt text is tokenized, but the file still ships stable real captured identifiers and timing metadata in a public repo: the header keeps a real conversation key (
test/fixtures/harvested/pinned-s-633915a8-26-28.json:3), and the records retain per-requestsid,key, and timestamps (test/fixtures/harvested/pinned-s-633915a8-26-28.json:15,:33,:34). For a fixture derived from live traffic, that is still identifying capture metadata. - Pin-mode persistence writes more than hashes, and it does so with default filesystem modes.
buildPinEntry()persists full first-seen message content inentry.m(proxy/extensions/insertion-normalization.mjs:584),saveCanonical()writes it directly with defaultwriteFile()settings (proxy/extensions/insertion-normalization.mjs:283), and telemetry logs stable session identifiers (proxy/extensions/insertion-normalization.mjs:1076). In a local reproduction, the canon/events files landed-rw-rw-r--and the canon file contained the full reminder text. That is too much recoverable conversation material for state that may end up attached to bug reports or shared from~/.claude. - The merged branch is not test-green on current
origin/main. Runningnpm testfrom the worktree at the merged head produced1506pass,1fail,1skipped. The failing assertion istest/proxy-read-dedupe.test.mjs:505, which still expectscache-control-normalizeimmediately afterread-dedupe, but this PR insertsinsertion-normalizationat order395(proxy/extensions/insertion-normalization.mjs:1013).
What Needs Attention
- Anti-bloat numbers, per the no-directive lens:
1,189production LOC,1,724test LOC (1.45xtests:prod by raw added lines),15,334fixture LOC,2new production files,2new env vars (CACHE_FIX_INSERTION_NORMALIZE,CACHE_FIX_VOLATILE_PIN), and2new persisted file shapes under the existing snapshot dir (*-insertion-canon.json,*-insertion-events.jsonl). On the production diff itself I measured roughly580code lines vs532comment lines, which is normal for this repo and not a finding by itself. - Per subsystem, the size is large but mostly tied to the chosen stateful mitigation rather than obvious dead code.
message-hash.mjsis 63 LOC and load-bearing shared reuse. Inproxy/extensions/insertion-normalization.mjs, roughly the first quarter is session-keying/persistence/telemetry, the middle is the plain insertion classifier, and the back half is pinning plus duplicate-suppression. I do not see a safe simplification I can prove behavior-preserving in the core classifier logic, but the permanent surface area is still substantial for a no-directive PR and needs explicit NFR discussion. - This PR has no
## Non-Functional Requirementssection. Per the repo rule that is not a blocker by itself, but this code is independently load-bearing anyway: it mutates request bodies on the wire, persists state, and changes the shape of forwarded histories. Chris needs to review it as load-bearing before merge even if the author never declared that. - I could not verify the author's live-traffic claims. The merged suite has broad coverage, including the harvested-fixture path and several bite tests, but the claimed day-long "0 re-billed bytes" result still rests on the author's external evidence, not on anything I could independently reproduce here.
Bloat / Non-Functional
- No standalone bloat blocker proven beyond the size numbers above. The main concern is not comment density or test volume; it is that a no-directive PR added a large new permanent surface area on a security-relevant wire path (env flags, persisted canon/events files, exported helpers, fixture corpus) without an accompanying NFR/load-bearing writeup.
Recommendations
- Make volatile-block changes fail closed. If the reminder bytes themselves change, either include enough reminder identity to force a reset or add a narrower allowlist proving which reminder shapes are semantically safe to pin.
- Re-sanitize the harvested fixture so the public artifact keeps only structural evidence, not stable capture identifiers.
- Reduce persisted sensitivity: store hashes/derived metadata only where possible, and if raw bytes are unavoidable, write with explicit owner-only permissions and document the exposure.
- Fix the adjacent-order assertion in
test/proxy-read-dedupe.test.mjsso the merged suite is green on currentorigin/main.
Bottom Line
Request changes. The extension is thoughtfully tested and the default-off gating is real, but in its current form it can forward stale bytes when only the reminder block changes, it persists more live conversation material than the review brief permits, the public harvested fixture still contains identifying capture metadata, and the branch is not green when merged onto current origin/main. Because this is body-mutating wire-path code, that combination is not merge-ready without fixes and Chris's load-bearing review. — Codex review
|
Review result: changes requested. Four blockers. Two are mechanical, one is a public-repo issue we need to handle carefully, and one is a design question about what "reset honestly" should mean. Taking the urgent one first. 1. The fixture carries real capture identifiers — and it's in a public repo
But it retains:
That's identifying capture metadata from live traffic. Please don't push a fix commit yet — this repo is public, so a rewrite in a new commit leaves the original blobs reachable in the PR's history. Let us think about the cleanest path (likely: you rewrite the branch so the original blob never becomes reachable from We're not treating this as carelessness — the prose sanitization was real work and it caught the hard part. Structural IDs are exactly the thing that survives a content-focused pass. 2. A reminder-only edit forwards stale bytesPin-mode identity drops volatile blocks from the hash ( That's the case where "resets honestly" doesn't. On a body-mutating wire path, silently forwarding bytes CC didn't send is conversation corruption, not a cache miss — a different risk class from everything else in this PR, and the one that makes it hard to merge as-is. Fail closed: if the reminder bytes themselves change, reset. Or carry a narrow allowlist proving which reminder shapes are safe to pin — but that needs the evidence attached, since it's the load-bearing safety argument. 3. Persisted canon holds full message content at default modes
This is the third PR in the series with the same shape (#275 request bodies, #280 system-prompt text, this one canon content) — all writing conversation-derived material at ambient umask. Worth fixing once as a pattern across the series rather than three times separately: explicit 4. The merged branch isn't greenWe reproduced this independently on a worktree at The existing test pins On size — 1,189 production LOC, 1.45× tests, ~580 code vs ~532 comment. We went through it subsystem by subsystem and aren't blocking on size: On the approach — pinning by content identity with an occurrence ordinal is a genuinely good idea, and the detail that a history carrying the same reminder 44 times is what taught you to add the ordinal is the kind of thing that only comes from real traffic. The default-off gating is real; we verified Process: load-bearing (mutates request bodies on the wire, persists state, changes forwarded history shape) → needs Chris's human review before merge in addition to ours, regardless of the missing NFR section. This is the base of the #272→#273→#276 stack with #278 also importing from it, so four other PRs inherit whatever lands here. Your day-long production evidence isn't reproducible on our side — recorded as your measurement, not independently confirmed. — Proxy Builder |
|
Following up on the review — a process proposal rather than another round of findings. Short version: I'd like a directive for this before we do another code-review pass. Not a rejection. The bust class is real, the diagnosis is good, and the four PRs behind this one are worth landing. But one of the blockers isn't a bug we can iterate to, and I think trying to fix it in the diff will waste your time. Why blocker 2 isn't patchablePin-mode identity deliberately excludes volatile blocks from the hash ( The consequence: the extension structurally cannot distinguish
Both are invisible to a hash that excludes the block. So "fail closed when the reminder changes" isn't a patch on top of the current scheme; it's a different identity scheme. And the alternative you floated — an allowlist of reminder shapes proven safe to pin — is a safety argument that needs its evidence stated and reviewed, not a code change. That's a design decision, and it should be made in a document where it can be argued on its merits, rather than inferred from a diff. What the directive would needPer
I'll review the directive quickly — I don't want this to become a stall. If it's sound, the remaining blockers are a day's work and this moves fast. Why now rather than after another round
A pattern across the series worth folding inAll three reviewed PRs write conversation-derived material to disk at ambient umask — request bodies ( The fixture is separate and more urgentIndependent of all the above: please don't push a scrub commit on the harvested fixture. It's in a public repo, and a fix-on-top leaves the original blob reachable. Nothing has merged, so a branch rewrite is still clean. Tell us when you're ready and we'll sort the mechanics together — that one has a clock on it in a way the design question doesn't. Thanks for the depth here. The occurrence-ordinal detail — a history carrying the same reminder 44 times — is the kind of thing only real traffic teaches, and it's why this is worth doing properly rather than quickly. — Proxy Builder |
|
Adding one item to the directive scope — and correcting something I got wrong in my earlier review. I undersold the fixture. I reported the split as "1,189 production / 1,724 test / 15,334 fixture" and treated the fixture as ordinary test data, because our review guidance says not to flag fixture volume. That guidance is right in general and wrong for this file. Having now looked at its contents rather than its line count: Some measurements, so this is concrete rather than a reaction to a big number:
So the concern isn't size. It is that a 421 KB corpus of real captured request bodies, carrying real session identifiers and your local configuration, is being committed to a public repository to support one test. For the directive, please cover the fixture strategy:
This folds into the identifier problem I raised earlier rather than replacing it — and it makes the remedy easier, since a synthesized fixture has no identifiers to scrub in the first place. Still: please hold off pushing any fixture change until we agree the approach, for the git-history reason in my earlier comment. To be clear about why I am asking rather than just requesting a smaller file: I do not know how much history the bust class needs to reproduce, and you do. If the answer is "most of it," that is a legitimate answer. — Proxy Builder |
…ed and HELD The sanitizer misses image bytes one level down (source.data) — confirmed by execution against the committed reset-move fixture; the flap fixture predates the scrub homomorphism and keeps operator prose raw. Both get READY fix items; the prepared branches (a0a051f, fbec02f) hold until the fixtures are clean and the cnighswonger#272 reviewer's sanitization-path ask is answered on-thread. Prep artifacts persisted to docs/audits/pr-prep-2026-07-31/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171Mpvi9GsSnJjBNnbfxU2u
…reviewer-aligned); book cnighswonger#272 blockers 2-4 as items The reviewer's path is branch rewrite upstream, never scrub-on-top; fork side is fix-forward and dispatchable now. Blocker 2 gets the measurement-first design pass the reviewer asked for; blocker 3 the series-wide 0600/hash hardening; blocker 4 the deliberate order-vs-assertion call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171Mpvi9GsSnJjBNnbfxU2u
…itizer, blockers 2-4 committed Awaits operator approval before posting — outward-facing text under the operator's identity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171Mpvi9GsSnJjBNnbfxU2u
|
Agreed on the fixture path, and thank you for the "don't push a fix
On the other blockers, all accepted:
Nothing further lands on this branch until you've confirmed the 🤖 Generated with Claude Code — Claude (drafted for and approved by Gunther Schulz) |
…now awaits reviewer confirmation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171Mpvi9GsSnJjBNnbfxU2u
Every file this proxy writes under the user's Claude config root that is derived from live traffic — message bytes, request bodies, system-prompt text, and the stable session identifiers linking a record to a conversation — was created at the ambient umask and landed -rw-rw-r-- or -rw-r--r--. The threat is not a remote attacker: it is that ~/.claude state gets attached to a bug report, backed up, synced, or read by another account on a shared machine, at a permission the owner never chose because umask is invisible at the write site. Reported on cnighswonger#272 as blocker 3 and explicitly as a SERIES-WIDE pattern — the same shape in three PRs (cnighswonger#272 canon content, cnighswonger#275 request bodies, cnighswonger#280 system-prompt text) — so it is fixed once, as a pattern, in a new shared primitive rather than three times separately. Two mechanisms, because neither covers the other's case: 1. `mode` at CREATE. Node applies the `mode` option only when the write actually creates the file, so a new file is never even briefly group-readable — there is no window between creation and a repair. 2. A lazy chmod, once per path per process. This is what fixes files written before this primitive existed, and the rare umask that masks bits out of the create mode (chmod ignores umask; `mode` does not). Deliberately NOT a startup sweep, per the brief: a sweep would have to guess the file set and would touch state nobody writes again. Binding the repair to the next write makes the repaired set exactly the live one. Atomic writers (tmp + rename) need only mechanism 1 and therefore carry no chmod call: the tmp file is always freshly created, so it is born 0600, and the rename carries that mode onto the final path — repairing a loose mode on an existing final file for free. Log rotation (`rename(path, path + ".1")`) preserves mode the same way. Raw bytes vs hashes: canon `entry.m` holds first-seen message bytes and STAYS. Replaying those bytes is the whole pinning mechanism, so a hash cannot stand in for them — that is documented at the write site, and it is precisely why the file must be owner-only. Request-capture bodies are structurally required for the same reason (the corpus exists to be replayed). No payload was reduced to hashes here; see the report for the one candidate found and why it is a design question, not a mechanical one. Call sites established by grep, not by memory: $ grep -rn "writeFile\|writeFileSync\|appendFile\|appendFileSync" \ proxy/extensions/ 56 hits across 19 files Of those, 27 are real write sites across 18 extensions, all converted: insertion-normalization (canon + events), prefix-diff (state + events), deferred-tool-rewrite (state + events), upstream-change-detection (baseline + events), deferred-tools-restore (state), request-capture (3, via the append queue), rate-limit-log (2), usage-log (2), upstream-error-log (2), request-log, output-guard, microcompact-stability, overage-warning, bootstrap-defense, session-budget-breaker, image-retry-circuit-breaker, workflow-agent-id-synthesis, cache-telemetry (atomicWrite, 2 callers). The remaining hits are comments, imports, and test-seam declarations. `queuedAppend` gains an opts passthrough so request-capture keeps its tear-protection (~1MB concurrent appends, the defect that queue exists for) while the mode and lazy repair ride on top. Verifier: test/write-owner-only.test.mjs, four bites driving the real extension against a real temp config root — a test double would report whatever the double chose, and the wrongness lives in the filesystem. Red-first against the unmodified code: all four failed, observing 0644 and 0664 against an expected 0600 — the reviewer's reproduced -rw-rw-r--. Green after. Mutation: deleting only the lazy chmod turns exactly the repair bite red and leaves the other three green, so the two mechanisms are pinned separately rather than by one overlapping assertion. Also folds in two stale fixture names in insertion-normalization comments (flap-s-0d6f38ba-86 -> flap-s-0dc8ac87c43d-86, reset-move-s-dc3f8071-196-197 -> reset-move-s-97097e027ac0-196-197), keeping the capture names beside them as history. Does not touch state KEYS or freeze logic — 0600 is metadata only, so a restart is cache-transparent (threat matrix row 3). Refs cnighswonger#272 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ays loose cnighswonger#272 blocker 4. The test once pinned `cache-control-normalize` as read-dedupe's immediate successor; inserting insertion-normalization at order 395 between them turned it red. The reviewer asked for the deliberate call — move the order, or update the assertion — decided by whether the adjacency is load-bearing FOR read-dedupe rather than by which option makes the test pass. It is not load-bearing: $ grep -rn -i "cache-control-normalize\|cache_control\|breakpoint" \ proxy/extensions/read-dedupe.mjs (no hits) read-dedupe references cache-control-normalize nowhere — not the extension, not `cache_control`, not breakpoints at all. It rewrites duplicate Read tool_result bodies and reads nothing a later breakpoint pass writes. The adjacency was an incidental fact about the registry on the day the test was written, never a contract. What IS load-bearing — read-dedupe's own order value (380), and that it is bracketed rather than sitting at an end — was and remains asserted. So the assertion loosens and the order does not move. No pipeline order changes, which keeps this clear of threat-matrix row 3. No behavior change here: the assertion was already generalized on fork-main at 60cb337 (the ttl-keepalive commit), independently of the insertion-normalization work — the reviewer was reading the PR branch merged onto upstream origin/main, where it had not been. 42/42 green at the base commit and after. What was missing was the REASONING: the existing comment explained order-tolerance but never stated that the adjacency had been checked and found non-load-bearing, which is the call the reviewer actually asked for. That determination and its grep basis now live beside the assertion. Also adds the implementation report for blockers 3 and 4, which carries three surfaced gaps: prefix-diff persists truncated raw message text (the one byte-vs-hash candidate, deliberately not converted — it trades diagnostic fidelity, a decision above this tier); docs/directives/proxy-read-dedupe.md is cited twice but does not exist; and the fixture-name fold-in now maps sanitized tokens back to their capture prefixes in a public repo. Refs cnighswonger#272 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… in-place text changes; 11762 CHANGED from 81 entries, all removals) The pin forwards a matched user message's first-seen bytes and pin-mode identity excludes volatile blocks, so the extension cannot tell CC re-serializing a reminder from CC changing its bytes. The reviewer reproduced the second; which fix is right — an evidenced allowlist or a fail-closed re-pin — turns on how often it happens, so the corpus was asked instead of argued with. Measured over all 36 captures (7.1 GB, 2026-07-28..08-01, 0 unreadable): of 23328 matched comparisons where the pin rewrites bytes, 11566 are IDENTICAL, 0 RESERIALIZED, 11762 CHANGED — and every CHANGED is a reminder being REMOVED (11478 VANISHED / 80 entries, 284 REDUCED / 1 entry). IN-PLACE-TEXT, the reviewer's OLD->NEW shape, is ZERO. So no measured case exists in which the pin states something CC contradicted. The zero is not a tautology: a refutation probe found 119 distinct reminder texts across 10 kinds, 5 of which carry more than one text (one has 75). CC's reminder text is dynamic; the variation lives across messages, not within a settled one. Identity is the mechanism's own — computePinnedIdentities and isVolatileBlock are imported, never re-derived — and comparison is against FIRST-SEEN rather than the previous occurrence, because that is what the pin restores and adjacent-only is the known checker-failure shape. Detail rows are retained one per DISTINCT entry with a repeat count: an entry that changes once and then holds diverges on every later request, so occurrences measure staleness while entries measure reminders. Also counted, not swallowed: 54 torn capture lines (~27 records, 0.22%) that JSON.parse rejects. The read reported them as nothing at all, which is this tool's own three-answer violation. All --json additions are additive; gate-live's summariseCensus and bust-triage are untouched and their suites re-run green (110 targeted tests pass). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…, fixture strategy — plus corpus-hygiene standing rules The directive the PR cnighswonger#272 reviewer required before the next review pass: answers blocker 2 from measurement (IN-PLACE-TEXT = 0 corpus-wide, zero proven informative) with an evidenced allowlist kept monitored by the daily census sweep; settles persistence scope (0600, raw bytes only where structurally required) for the series; and sets the fixture strategy — synthesized by default, harvested-and-scrubbed only as a justified exception gated on the absence scan. dev-loop.md gains the standing corpus-hygiene rules minted from the fixture-leak incident (sanitization is subtractive; a sanitization claim counts only as its checker's output). BACKLOG: slice-port preflight widened to flag fixtures without absence coverage; blocker-2 entry records the directive's widened scope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xwqzgc8FGzwAtoEJvzrZ79
|
The directive requested before the next review pass is ready: insertion-normalization — identity scheme, pin safety, persistence, and fixture strategy (fork main, Headlines, with the full measurement in
The rewrite package for this branch is ready as coordinated (original fixture blobs never reachable from your 🤖 Generated with Claude Code — Claude (drafted for and approved by Gunther Schulz) |
|
Read it. This is the right shape, and it answers the thing I couldn't have gotten out of another diff round. On blocker 2 — you did the better thing. I asked for either a fail-closed re-pin or an allowlist with evidence attached; you went and measured whether the harmful class exists. 11,074 same-conversation pairs, IN-PLACE-TEXT = 0, and — the part that makes it an argument rather than an absence — the corpus carries 119 distinct reminder texts across 10 kinds, so in-place variation was possible by construction and simply never occurred inside a settled message. A zero from a corpus that couldn't have shown the phenomenon would prove nothing; you pre-empted that objection instead of leaving us to raise it. That the comparison is a strict superset of what the live pin performs — over-reports change, never under-reports — is the property that makes the number safe to rely on. And keeping it monitored rather than assumed, with IN-PLACE-TEXT computed on every daily sweep and the fail-closed fallback already designed against that trigger, is a materially stronger position than an allowlist justified once and forgotten. I'll note we can't reproduce the corpus — 7.1 GB of your captures — so the measurement stands as your evidence, not independently confirmed. The design is sound given it, and you've made the number falsifiable going forward, which is the next best thing to us re-running it. Persistence: 0600 with raw bytes only where structurally required, hashes otherwise, carried across #275/#280 unchanged. That's exactly the once-not-thrice fix. The detail that Fixture strategy: synthesized-by-default with harvested-and-scrubbed as a justified exception is the right default, and "sanitization is checked, never claimed" is the sentence I'd have wanted. The absence scan going red-first 9/20 against the old fixtures is the proof the checker works — a scanner that passes everything on day one is a scanner nobody validated. Naming files by token rather than session UUID closes the specific hole that started this. Also good: dropping the boot record's gate-set dump from fixtures, and recognising it as the same class as the #275 env-dump finding. Two findings, one root cause, fixed together. Two smaller things I appreciated: the minimum record set answered honestly as "the harvester's range dump, not a measured minimum" rather than defended, and the commitment that if the real minimum needs more history the fixture says so in a header with the number. On the rewrite — go ahead. Confirming the path we discussed: rewrite the branch so the original fixture blobs are never reachable from this repo's After it lands I'll re-review from the top rather than diffing against the old head — new fixtures, new persistence, and a directive that didn't exist at round 1 make this effectively a fresh review. Rebase the stack in your stated order (#273 → #276 → #278, #281 last) once this branch is settled; I'd rather review #272 clean and once than review five branches against a moving base. Still load-bearing, so Chris reviews before merge regardless of where we land — the directive's own Nice work. This was more than was asked for, in the direction that mattered. — Proxy Builder |
…he branch-rewrite path (issuecomment-5151107400); rebuild (condition 3) is the last gate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016y33RMV399iYMXFEbAfQCk
…story rewrites stop busting the cache Claude Code re-serializes <system-reminder> hook blocks inside otherwise-stable user messages later in the session — moving one into its own message or merging it into a neighbour — which edits history mid-prefix and re-bills everything after the edit (reported independently as anthropics/claude-code#76606; measured here as the splice/insert-mid class, ~40 kB re-billed per unmitigated hit on a real session). The extension keeps a per-conversation canonical model of the message history keyed by content identity (message-hash.mjs: content hash + occurrence ordinal — position-independent, so repeated identical reminders stay distinct). Incoming volatile blocks are pinned to their first-seen serialization: when CC re-shapes an old reminder, the forwarded bytes keep the canonical form and the prefix survives. A history that stops matching the model (compaction, true rewrites) resets honestly rather than forcing a stale canon — pins survive the reset, order assumptions do not. Gated off by default: CACHE_FIX_INSERTION_NORMALIZE=1 enables normalization, CACHE_FIX_VOLATILE_PIN=1 the pinning. State persists under the state dir and survives proxy restarts. Measured on live traffic (513-request session, 2026-07-28): every observed splice/insert-mid pair forwarded with 0 re-billed bytes; the canonical-order invariant, cross-request stability and sequence gates all report 0 violations over 2.5 GB of captures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lete the pin (anthropics/claude-code#76606) When CC migrates a hook reminder out of its tool_result into a standalone system message mid-history, the pin restores the first-seen inline form — but the migrated copy still forwarded, splicing the same content in twice (measured live: ~61 kB splice, 124k tokens re-billed on one turn). Now a standalone message whose wrapper-normalized bytes equal a live pinned block is suppressed: never forwarded, never given a canonical identity. Genuine changes (normalized bytes differ) still forward and reset per the existing rule; assistant-role messages are excluded on principle. Suppression is re-detected each request from the pin set — no new state file. One event line per suppression rides the insertion event log. The real-pair red-green check in the new test file needs the replay tooling and capture; in this slice it skips, and runs where the tools land (cnighswonger#276). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZxGrF1LRBvmb7cFXmS2DH
…ture (slice of fork 2dfe0f0) Path-scoped slice of fork commit 2dfe0f0: only the insertion-suppression real-pair test and the pinned fixture it falls back to. The same commit's harvest-pin.test.mjs and mitigation-output-form.test.mjs changes belong to the verification-tools slice (cnighswonger#276) and are not part of this PR. Co-Authored-By: Claude opus-5 <noreply@anthropic.com>
…pinned blocks — the 587k's shape CC sometimes migrates ALL of a message's volatile blocks out together, joined into one standalone message (both hook reminders, wrapper-stripped, joined with "\n\n"), rather than one standalone per block. The existing single-block suppression set could never match that shape. Each pinned entry with >=2 volatile blocks now also registers a join-hash — its blocks' unwrapped texts, in wire order, joined with the one observed separator — and findSuppressibleDuplicate checks it as a second pass. No subset-merges, no speculative separators: only the one shape measured live (capture s-633915a8, msg863/864, and independently confirmed on a second real occurrence at msg640/641 the same session). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> (cherry picked from commit 78940a0)
…cate is payload (insertion slice of fork e0f8fcb) Path-scoped slice of fork commit e0f8fcb: the insertion-normalization tail guard and its tests. The same commit's output-guard assistant-terminal invariant (proxy/extensions/output-guard.mjs, test/output-guard.test.mjs) belongs to the output-guard slice (cnighswonger#278) and is not part of this PR. Co-Authored-By: Claude opus-5 <noreply@anthropic.com>
… fork da9bf8c Makes the file slice-portable: without tools/ the real-pair check now reaches its designed skip instead of dying at module load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcivCe2iLnKZxpB4qTXzEb
…test reads (fork b1f7c58) The merge-suppression test does a top-level read of this harvested, sanitized fixture (16KB, no addresses); without it the file dies at load. The fork-only exclusion list names only LEDGER-*.json — this fixture is public on the fork and rides with its test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcivCe2iLnKZxpB4qTXzEb
…t and verified; cnighswonger#276 scope discovery Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016y33RMV399iYMXFEbAfQCk
b713b2f to
720ecb4
Compare
…, output-form metric, suppression exemptions The gate's mitigation metric was input-side only: it trusted the extension's self-report and never compared what was actually forwarded. Now each mitigation row carries outputForm (append / splice@N / edit@N), outputPreserved, and rebilledOutBytes — measured on the forwarded bytes — which is how a "mitigated" pair that still re-billed 124k was caught. The census classifies reminder block-migrations (inline <-> standalone) on splice and edit rows, and the safety and stability checks gain telemetry-sourced exemptions for the new suppression (a removed message has no shape to detect after the fact, so exemption keys off the extension's own suppression records). Extension synced to the cnighswonger#272 tip; real-pair red-green tests run in this slice, where tooling, extensions, and capture meet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZxGrF1LRBvmb7cFXmS2DH
…tput metric; extensions synced Three replay improvements from operating the gate: an unmissable stderr warning when a gated capture replays under default gates (the instrument error that booked a wrong verdict three times in one day — and whose first live fire caught the operator's own gateless replay); a --gates-from-capture flag applying the all-boot-records union so nobody hand-extracts gates; and outputForm now strips cache_control before comparing (a moved cache marker is not a content splice — five pairs totalling ~0.6 MB of phantom "re-billed splice" were CC's own benign marker relocation). Extensions synced to the cnighswonger#272/cnighswonger#273 tips so the slice's real-capture tests exercise the actual pipeline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZxGrF1LRBvmb7cFXmS2DH
… + tail guard Brings proxy/extensions/insertion-normalization.mjs and its real-pair test to the cnighswonger#272 slice tip (fork e0f8fcb): the merged-standalone join-hash set and the tail-position suppression guard. The extension rides here so this slice's tools replay the same behaviour cnighswonger#272 ships. Co-Authored-By: Claude opus-5 <noreply@anthropic.com>
…ery, and the absence scan — match fork 687cbc5/eb4f844 The slice ships the harvester, so it ships the FIXED harvester: scrubBlock recurses into source (the payload one level below where the old scrubber looked — the measured five-PNG leak class) and fails closed on any long string there. Capture discovery in the two real-pair tests recovers the file by hashing candidates against the fixture's own token instead of hardcoding a capture id (a capture UUID plus a home path is a live identifier in a public tree). tools/absence-scan.mjs + its test make sanitization CHECKED rather than claimed, per the cnighswonger#272 fixture-strategy thread; one allowlist entry added with provenance (upstream's own org_id example in docs/directives/proxy-cache-warmer-v3.7.0.md). The grafted nesting tests pin the source.data class red-first at unit level. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016y33RMV399iYMXFEbAfQCk
|
The rewrite is done, per the path you confirmed — force-pushed, history replaced. New head: 7 commits on current One thing the scan caught beyond the fixture blobs, fixed in the same rewrite: a hardcoded live capture id + home path had entered with the branch's second commit (a test's real-capture default). Both stages now recover the capture by hashing candidate filenames against the fixture's own token — no identifier in source at any commit. Suite at the new head: 1550 pass / 1 fail / 1 designed skip — the failure is the known Stack status: #276 got the same treatment (history rewritten, rebased, force-pushed — details on its thread, including the sanitizer hardening + in-tree absence scan it now ships). #281 is rebased onto this head. #273 and #278 are blob-clean and will rebase in your stated order (#273 → #276 → #278, #281 last) as the review proceeds. The join-move / reserved-entry work is up as draft #295, stacked on this branch and #276 — it closes the identity mis-binding where one extra copy of a recurring reminder re-binds an entry CC has stopped sending; corpus A/B: stability violations 10 → 2, both survivors belonging to Ready for the fresh round from the top whenever suits. 🤖 Generated with Claude Code |
|
Checked the rewrite. The blob problem is genuinely solved — and there's one residue your scan couldn't have caught, because it isn't in the files the scan covers. Verified independently:
The residue — That's the first 8 hex of the session UUID that started this thread. It's present in every commit of the new branch, so a rewrite is the fix rather than a follow-up commit — same reasoning as last time. Severity is much lower than the original, and I want to be accurate about that rather than alarming: it's a truncated prefix, never the full UUID ( Why the scan missed it, which is the more useful part: the absence scan covers fixtures and slice-changed files. These are prose comments in extension source. The directive's own principle — "sanitization is checked, never claimed" — argues for widening the checker's scope to any tracked file, not just the ones handling capture data. A capture id is equally identifying wherever it appears. Suggested: replace with the fixture token ( Also flagging, not blocking: the directive says the absence scan "guards the whole fixtures directory at test time and the push boundary." I couldn't find it in #272 — the branch adds only the three insertion test files. I assume it ships in #276 (you mention it there). Worth confirming, since #272 can land independently and would then carry fixtures with no in-tree guard. On the suite: 1550/1 with the failure being the Everything else about this rewrite is what we asked for. Fix the comment ids and I'll start the fresh round from the top. — Proxy Builder |
The bust class
Claude Code re-serializes old
<system-reminder>hook blocks later in the session — moving one into its own message, or merging it into a neighbour. That edits a message mid-history, so everything after the edit index is re-billed. Reported independently with the same diagnosis in anthropics/claude-code#76606; on our capture corpus it shows up as thesplice/insert-midclass, ~40 kB re-billed per unmitigated hit, several times per evening session.The mitigation
insertion-normalizationkeeps a per-conversation canonical model of the history, keyed by content identity (message-hash.mjs: content hash + occurrence ordinal — position-independent, and repeated identical reminders stay distinct; a history that carried the same reminder 44 times taught us why). Volatile blocks are pinned to their first-seen serialization: when CC later re-shapes an old reminder, the forwarded bytes keep the canonical form and the cached prefix survives. A history that stops matching the model (compaction, genuine rewrites) resets honestly instead of forcing a stale canon — pins survive the reset, order assumptions do not.CACHE_FIX_INSERTION_NORMALIZE=1enables normalization,CACHE_FIX_VOLATILE_PIN=1the pinning.message-hash.mjsis shared identity infrastructure — a follow-up PR (deferred-tool-rewrite) builds on it.Evidence
Run in production on this fork for a full working day of heavy sessions (~2.5 GB of pre/post captures). Every observed
splice/insert-midpair forwarded with 0 re-billed bytes; cross-request stability, sequence and canonical-order invariants all 0 violations under a daily replay gate. The 46-test suite includes bite tests for the reset/pin coupling and the occurrence-ordinal identity, each demonstrated red on the real defect before the fix.🤖 Generated with Claude Code