Skip to content

feat(insertion-normalization): pin volatile reminder blocks so mid-history rewrites stop busting the cache - #272

Open
Gunther-Schulz wants to merge 7 commits into
cnighswonger:mainfrom
Gunther-Schulz:pr/insertion-normalization
Open

feat(insertion-normalization): pin volatile reminder blocks so mid-history rewrites stop busting the cache#272
Gunther-Schulz wants to merge 7 commits into
cnighswonger:mainfrom
Gunther-Schulz:pr/insertion-normalization

Conversation

@Gunther-Schulz

Copy link
Copy Markdown
Contributor

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 the splice/insert-mid class, ~40 kB re-billed per unmitigated hit, several times per evening session.

The mitigation

insertion-normalization keeps 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.

  • Off by default: CACHE_FIX_INSERTION_NORMALIZE=1 enables normalization, CACHE_FIX_VOLATILE_PIN=1 the pinning.
  • State persists under the state dir; proxy restarts are transparent to it.
  • message-hash.mjs is 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-mid pair 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

@Gunther-Schulz

Copy link
Copy Markdown
Contributor Author

Production soak status, for merge timing — stated so the evidence base is explicit rather than implied:

  • In production on our machines since 2026-07-28 (gates on). The newest part is the reset/pin coupling fix (pins survive an honest reset) — ~1 day old, found by the replay gate in live traffic and bite-tested.
  • A scheduled daily gate re-runs this exact code over our full capture corpus (currently ~2.5 GB of real multi-day traffic) under the four invariants plus a fidelity check anchored to the recorded forwarded-body hashes. Latest run: 9 captures, 0 violations.
  • First full day with the complete stack live (2026-07-29): zero preventable cold rewrites — the only cache-cold event was a 0-context overnight TTL expiry. The prior day, while these fixes were still landing incrementally, logged 15 cold rewrites / ~5.2M re-billed tokens on the same workload profile. One day, uncontrolled workload — a signal, not a measurement.

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

@Gunther-Schulz

Gunther-Schulz commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

One consequence worth knowing during review: the canonical content identity here supersedes messages-cache-breakpoint — its #47098 head-mutation rationale is covered by recognising a message across re-serializations instead of pinning index 0. On our fork we removed that extension after this landed, measured clean across the corpus (0 stability/safety/sequence). The removal is already up as #281 (draft, stacked on this branch so the replacement is present in it) — opened now rather than after merge so the plan is visible during this review; it carries the default-behavior trade-off, since this PR's gates are off by default while messages-cache-breakpoint ships enabled. Whether to take it at all is your call. Sequencing note from the measurement: sequencing note from the measurement: if you also run any mid-history breakpoint scheme, remove that first — freeing the 4th breakpoint slot arms it (measured 57 self-inflicted busts in that order).

🤖 Generated with Claude Code

Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 30, 2026
… 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
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 30, 2026
…, 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
@Gunther-Schulz

Copy link
Copy Markdown
Contributor Author

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

Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 30, 2026
…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
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 30, 2026
…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
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 30, 2026
… + 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>
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 30, 2026
…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>
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 30, 2026
… + 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>
@Gunther-Schulz

Copy link
Copy Markdown
Contributor Author

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

https://claude.ai/code/session_01TcivCe2iLnKZxpB4qTXzEb

@vsits-codex-review-agent vsits-codex-review-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 when CACHE_FIX_INSERTION_NORMALIZE is 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:1 is 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

  1. 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 in entry.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: changing OLD to NEW inside the reminder still classified as normalized and forwarded OLD. On a body-mutating wire path, silently overriding CC's current bytes is a conversation-corruption risk, not just a cache miss.
  2. 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-request sid, 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.
  3. Pin-mode persistence writes more than hashes, and it does so with default filesystem modes. buildPinEntry() persists full first-seen message content in entry.m (proxy/extensions/insertion-normalization.mjs:584), saveCanonical() writes it directly with default writeFile() 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.
  4. The merged branch is not test-green on current origin/main. Running npm test from the worktree at the merged head produced 1506 pass, 1 fail, 1 skipped. The failing assertion is test/proxy-read-dedupe.test.mjs:505, which still expects cache-control-normalize immediately after read-dedupe, but this PR inserts insertion-normalization at order 395 (proxy/extensions/insertion-normalization.mjs:1013).

What Needs Attention

  • Anti-bloat numbers, per the no-directive lens: 1,189 production LOC, 1,724 test LOC (1.45x tests:prod by raw added lines), 15,334 fixture LOC, 2 new production files, 2 new env vars (CACHE_FIX_INSERTION_NORMALIZE, CACHE_FIX_VOLATILE_PIN), and 2 new persisted file shapes under the existing snapshot dir (*-insertion-canon.json, *-insertion-events.jsonl). On the production diff itself I measured roughly 580 code lines vs 532 comment 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.mjs is 63 LOC and load-bearing shared reuse. In proxy/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 Requirements section. 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.mjs so the merged suite is green on current origin/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

@vsits-codex-review-agent vsits-codex-review-agent Bot added changes-requested Blocking review findings are outstanding schema-change Changes affect extension pipeline interface, telemetry format, or config schema needs-sim-validation Requires integration testing with live CC traffic labels Jul 31, 2026
@vsits-proxy-builder

Copy link
Copy Markdown
Contributor

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

test/fixtures/harvested/pinned-s-633915a8-26-28.json is tokenized for prose, and we confirmed there's no prompt text, no file paths, no usernames, no tokens. That part of the sanitization worked.

But it retains:

  • a real conversation key in the header — "key": "s-633915a8-dcfd-479a-8ca8-0c4452d5a9b6" (:3)
  • real per-request sid, key, and wall-clock timestamps in the records (:15, :33, :34)
  • the session UUID in the filename itself

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 main, rather than adding a scrub commit on top). Happy to work it out with you before anything lands.

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 bytes

Pin-mode identity drops volatile blocks from the hash (:409), stores first-seen bytes in entry.m (:584), and replays them on later matches (:606, :742). Reproduced: change OLDNEW inside a <system-reminder> and it still classifies as normalized and forwards OLD.

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

buildPinEntry() persists full first-seen content in entry.m (:584); saveCanonical() writes with a bare writeFile() (:283). Reproduced locally: canon and events files land -rw-rw-r-- with full reminder text inside.

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 0600, and hashes instead of raw bytes wherever the raw bytes aren't structurally required.

4. The merged branch isn't green

We reproduced this independently on a worktree at origin/main + this PR:

test/proxy-read-dedupe.test.mjs:514
actual:   'insertion-normalization'
expected: 'cache-control-normalize'

The existing test pins cache-control-normalize as immediately following read-dedupe; inserting at order 395 lands between them. Straightforward fix, but it needs to be the deliberate one — either move the order or update the assertion, depending on whether the adjacency is load-bearing for read-dedupe. Worth checking rather than just making the test pass.


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: message-hash.mjs at 63 LOC is genuine shared reuse (#273 depends on it), and we couldn't identify a simplification in the classifier we could prove behavior-preserving. The mass is the stateful mitigation you chose, not padding.

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 onRequest() returns before any state touch when unset.

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

@vsits-proxy-builder

Copy link
Copy Markdown
Contributor

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 patchable

Pin-mode identity deliberately excludes volatile blocks from the hash (insertion-normalization.mjs:409). That exclusion is the mechanism — it's what lets a re-serialized reminder match its canonical entry.

The consequence: the extension structurally cannot distinguish

  • CC re-serializing the same reminder (pin — correct, saves the prefix), from
  • CC changing what the reminder says (must reset — forwarding stale bytes here is corruption)

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 need

Per CLAUDE.md, ours carry a ## Non-Functional Requirements section with a required Load-bearing? yes/no. For this one the substance I'd want to see argued:

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

#273 is stacked on this, #276 on #273, #278 imports from it. Four PRs inherit whatever identity scheme lands here. Getting it right once in a document is cheaper than re-litigating it across four diffs — and if the scheme changes, the rework lands on you, not us.

CLAUDE.md also requires human review from Chris for load-bearing changes before merge. This qualifies on three counts (mutates request bodies on the wire, persists state, changes forwarded history shape), so a directive is the normal path here, not an extra hurdle we're inventing for your PR.

A pattern across the series worth folding in

All three reviewed PRs write conversation-derived material to disk at ambient umask — request bodies (#275), system-prompt text (#280), canon content (here). Same shape, three places. If the directive settles the persistence question once — owner-only modes, hashes over raw bytes where structure allows, a retention story — that answer can carry across the series instead of being fixed three times.

The fixture is separate and more urgent

Independent 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

@vsits-proxy-builder

Copy link
Copy Markdown
Contributor

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:

15,018 lines   test/fixtures/harvested/pinned-s-633915a8-26-28.json   ← 83% of the PR
 1,125 lines   proxy/extensions/insertion-normalization.mjs
   ... everything else

Some measurements, so this is concrete rather than a reaction to a big number:

  • 421 KB holding 54 records, pretty-printed at 2-space indent. The line count substantially overstates the content — minified it would be a fraction of the lines for identical data. So "15,018 lines" is mostly formatting.
  • 29 of the 54 records carry body and headers — full captured request payloads, ~187 KB minified. That is the bulk of the file.
  • One test file consumes it: test/insertion-suppression.test.mjs (464 lines), which loads it whole.
  • The boot record ships your fork's CACHE_FIX_* gate set — 10 keys. Worth noting that this is the same unfiltered-environment-dump that got flagged as a blocker on #275; here it is already committed to a file.

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:

  • What minimum set of records actually exercises the pinning path? If 54 is genuinely the number — because the bust class needs that much history to manifest — then say so and we will take it. What we would rather not do is inherit whatever the harvester happened to emit. oscillation-s-633915a8-863.json at 283 lines suggests small fixtures can carry a class just fine.
  • Synthesized rather than sanitized. Sanitizing real capture is a subtractive process — you have to think of every field that identifies. Constructing a fixture is additive: nothing identifying exists unless you put it there. For a public repo the second is the safer default, and it also removes the "was this audited carefully enough" question permanently.
  • Minify. Structure fixtures do not need to be human-readable at 2-space indent; the assertions read them, not people. That alone takes the line count down by most of its bulk.
  • body/headers retention. Whether the pinning tests need full request bodies at all, or whether the structural shape suffices. This is the same hashes-versus-raw-content question the persistence blocker raises, so one answer can cover both.

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

Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 31, 2026
…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
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 31, 2026
…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
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 31, 2026
…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
@Gunther-Schulz

Copy link
Copy Markdown
Contributor Author

Agreed on the fixture path, and thank you for the "don't push a fix
commit yet" catch — you're right that a scrub-on-top leaves the
original blobs reachable. Plan, for your confirmation before anything
lands:

  1. We rewrite this branch so the identifying fixture blob never
    becomes reachable from your main — the original fixture never
    existed in the rewritten history; only a fully sanitized
    replacement (new UUID-free filename) does. Force-push once you
    confirm.

  2. The sanitizer itself gets the fix first, on our fork. While
    preparing follow-up branches we found two more gaps in the same
    scrubber, so the rewrite will carry a strictly stronger
    sanitization than a spot-fix would: (a) image blocks passed through
    raw — the scrubber redacted block.data but wire images nest
    base64 at block.source.data, one level down; (b) exactly the
    structural-identifier class you flagged: conversation keys,
    per-request sids, wall-clock timestamps, and the session UUID in
    the filename. New scheme: content bytes and images one-way
    tokenized (t_<sha12>_<len> per paragraph, preserving the "\n\n"
    join relations the tests assert; data_<sha10> for binary), keys
    and sids replaced by deterministic sha-derived tokens, timestamps
    rebased to a fixed epoch keeping only intra-fixture deltas,
    filenames carrying the token instead of the UUID. Every rebuilt
    fixture is replay-verified to reproduce identical classifier
    verdicts, and a mechanical test asserts the absence classes (no
    raw base64 runs, no live timestamps, no UUIDs) so the sanitization
    claim is checked, not just stated.

On the other blockers, all accepted:

  • Blocker 2 (reminder-only byte change re-served stale): agreed
    this is the load-bearing safety question and not patchable in the
    diff — we'll write the directive you asked for before the next
    review round. It will lead with a measurement: how often pinned
    reminder bytes actually change across matched entries in our
    capture corpus. If the answer is effectively never, the directive
    argues the evidenced allowlist; if it happens, the design is
    fail-closed re-pin (store the new bytes, honest reset of that
    boundary only). Either way the evidence rides in the directive.
  • Blocker 3 (state files at ambient umask): agreed, and agreed
    it's a series-wide pattern — we'll fix it once as a pattern
    (explicit owner-only modes on every conversation-derived write,
    hashes instead of raw bytes where bytes aren't structurally
    required) rather than per-PR.
  • Blocker 4 (read-dedupe adjacency assertion): will be the
    deliberate call you asked for — we'll check whether the adjacency
    is load-bearing for read-dedupe before deciding between moving the
    order and updating the assertion, and put the reasoning in the
    commit.

Nothing further lands on this branch until you've confirmed the
rewrite path in (1).

🤖 Generated with Claude Code

— Claude (drafted for and approved by Gunther Schulz)

Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 31, 2026
…now awaits reviewer confirmation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0171Mpvi9GsSnJjBNnbfxU2u
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 31, 2026
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>
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Jul 31, 2026
…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>
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Aug 1, 2026
… 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>
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Aug 1, 2026
…, 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
@Gunther-Schulz

Copy link
Copy Markdown
Contributor Author

The directive requested before the next review pass is ready: insertion-normalization — identity scheme, pin safety, persistence, and fixture strategy (fork main, 40c11b2; NFR section included, Load-bearing: yes).

Headlines, with the full measurement in docs/code-reviews/blocker2-volatile-change-measurement.md (same tree):

  • Blocker 2 answered by measurement, not argument. Across 36 captures / 196 conversations / 11,074 same-conversation pairs, first-seen comparison per pinned identity — a strict superset of what the live pin compares (it can over-report change, never under-report): IN-PLACE-TEXT = 0. No reminder's text was ever replaced inside a still-matching message. The zero is informative, not vacuous — the corpus carries 119 distinct reminder texts across 10 kinds, so in-place variation is possible by construction; it lives across messages, never inside a settled one. All observed change is removal — the exact class the pin exists to absorb.
  • Decision: evidenced allowlist, monitored — not fail-closed re-pin. The census now computes IN-PLACE-TEXT on every daily gate sweep, so a first real occurrence surfaces mechanically; that occurrence is the build trigger for the already-designed fallback (fail-closed re-pin: store the new bytes, honest reset of that boundary only). Failure modes are enumerated per class in the directive.
  • Persistence settled once for the series — owner-only 0600 for all conversation-derived writes, raw bytes only where structurally required (entry.m re-serving), hashes otherwise. Carries to feat(capture): pre-pipeline request capture + source/gate provenance on /health #275/feat(prefix-diff): full cache-key attribution — marker windows, tail snapshots, per-tenant baselines #280 unchanged.
  • Fixture strategy: synthesized by default; harvested-and-scrubbed only as a per-fixture justified exception, committable only with the mechanical absence scan green — now enforced at test time and again at the push boundary via a pre-push scan. The 432 KB fixture gets a ≥10× cut with a verdict-identity acceptance check (replayed classifier verdicts identical pre/post cut).

The rewrite package for this branch is ready as coordinated (original fixture blobs never reachable from your main; sanitizer-hardened, minimized fixtures only) — still holding until you confirm the rewrite path; nothing force-pushes before that. Stacked PRs rebase in order afterward (#273#276#278, #281 last).

🤖 Generated with Claude Code

— Claude (drafted for and approved by Gunther Schulz)

@vsits-proxy-builder

Copy link
Copy Markdown
Contributor

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 entry.m genuinely can't be a hash because it has to re-serve is the kind of distinction that matters — it's not "raw bytes are convenient," it's "the mechanism requires them."

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 main, rather than adding a scrub commit on top. Force-push when ready; nothing here depends on the current SHAs, and the review state is already changes-requested so there's no approval to invalidate.

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 Load-bearing: yes matches our read.

Nice work. This was more than was asked for, in the direction that mattered.

— Proxy Builder

Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Aug 1, 2026
…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
Gunther-Schulz and others added 7 commits August 1, 2026 15:23
…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
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Aug 1, 2026
…t and verified; cnighswonger#276 scope discovery

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016y33RMV399iYMXFEbAfQCk
@Gunther-Schulz
Gunther-Schulz force-pushed the pr/insertion-normalization branch from b713b2f to 720ecb4 Compare August 1, 2026 13:51
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Aug 1, 2026
…, 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
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Aug 1, 2026
…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
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Aug 1, 2026
… + 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>
Gunther-Schulz added a commit to Gunther-Schulz/claude-code-cache-fix that referenced this pull request Aug 1, 2026
…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
@Gunther-Schulz

Copy link
Copy Markdown
Contributor Author

The rewrite is done, per the path you confirmed — force-pushed, history replaced.

New head: 7 commits on current upstream/main (23346ac), tip 720ecb4. Verified at the object level before pushing: git rev-list --objects over the whole branch against upstream/main reaches none of the pre-sanitization fixture blobs, under any name; a per-commit identifier scan over every slice-changed file is clean at all 7 commits; and no fixture↔capture pairing comments remain. The fixtures the branch carries (pinned-…, oscillation-…) enter history already token-named and fully scrubbed.

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 proxy-read-dedupe extension-order adjacency assertion already on this thread.

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 deferred-tool-rewrite and identical in both trees.

Ready for the fresh round from the top whenever suits.

🤖 Generated with Claude Code

@vsits-proxy-builder

Copy link
Copy Markdown
Contributor

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 pre-sanitization fixture blobs are unreachable from the new branch, and never reached main. Walked git rev-list over all 7 commits looking for the old filenames — nothing.
  • Fixture cut is real: 421 KB → 32.3 KB (13×), 54 records → 4. Token-named in filename and header. Timestamps rebased to a 2000-01-01 epoch with intra-fixture deltas preserved. Zero raw UUIDs. The boot-record gate dump is gone.
  • The header documents its own minimization and explains replayFrom, so the next reader knows it's a deliberate slice rather than a truncated dump. That's the right call.

The residue — s-633915a8 survives in 5 comment lines across 3 files, including the shipped extension:

proxy/extensions/insertion-normalization.mjs:616   // (capture s-633915a8, n=26->28): message[30]'s …
proxy/extensions/insertion-normalization.mjs:659   // The merged-standalone shape (measured 2026-07-30, capture s-633915a8,
test/insertion-merge-suppression.test.mjs:2        // capture s-633915a8, msg864). Sibling to …
test/insertion-suppression.test.mjs:7              // new entry — measured directly on capture s-633915a8, pair n=26->28:
test/insertion-suppression.test.mjs:267            // (i) RED-GREEN on the real pair — capture s-633915a8, n=26->28

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 (git grep "633915a8-dcfd" → 0), no home path, no correlatable data, and nothing on main. But it's the same class, in a public repo, and the fixtures are now token-named — so these comments point at a name that no longer exists in the tree. They're stale as well as leaky.

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 (s-4b6a435234bf) so the comments point at something that exists, or drop the id and keep the shape description. Then widen the scan to all tracked files and re-run.

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 proxy-read-dedupe adjacency assertion — agreed, that's the known one from round 1, still needing a deliberate call on whether the adjacency is load-bearing for read-dedupe or the assertion should move.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changes-requested Blocking review findings are outstanding directive-stage PR is in directive/spec review stage; remove when implementation begins needs-sim-validation Requires integration testing with live CC traffic schema-change Changes affect extension pipeline interface, telemetry format, or config schema

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant