Skip to content

Commit c943206

Browse files
book the PR-prep dispatch: two STOP findings confirmed, slices prepared 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
1 parent f438923 commit c943206

5 files changed

Lines changed: 647 additions & 1 deletion

File tree

BACKLOG.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1120,7 +1120,52 @@ bullet, evidence pointer included.
11201120
in the PARKED reserved-entry-residuals entry; verdict-ab self-test
11211121
its own READY item.
11221122

1123-
- **READY — verdict-ab self-test (report gap c5).**
1123+
- **READY — harvest scrub misses image bytes: scrubBlock stops one
1124+
level above `source.data` (STOP finding, pr-prep report,
1125+
dispatcher-CONFIRMED by execution: harvest.mjs:169 tokenizes
1126+
`block.data`; wire images nest base64 at `block.source.data`;
1127+
reset-move fixture carries 5 raw PNGs, magic `iVBORw0KGgo`).**
1128+
Content verified benign (operator's own terminal prose, viewed),
1129+
but the sanitizer claim on the fixture is false as written and the
1130+
bug is generic. Build: (1) scrubBlock recurses into `source`
1131+
(`source.data``data_<sha10>` token, keep type/media_type
1132+
shape); (2) red-first bite that goes red on the committed
1133+
reset-move fixture's raw bytes BEFORE the fix; (3) re-scrub the
1134+
committed fixture in place and verify verdict-neutrality by
1135+
replay (images are not join constituents — same actions, same
1136+
suppression indices, same stability result); (4) corpus sweep:
1137+
every harvested fixture grepped for raw base64 under source.data,
1138+
results in the commit. Load-bearing (sanitizer, public repo).
1139+
1140+
- **READY — flap fixture rebuilt SANITIZED via the scrub
1141+
homomorphism (pr-prep STOP finding 2).**
1142+
flap-s-0d6f38ba-86.json keeps 6 operator hook-prose strings raw
1143+
(quoting dispatch-discipline §1 / CLAUDE-maintenance by name) —
1144+
the raw retention pre-dated bffcb05's join-preserving scrub, and
1145+
the reset-move fixture's own header names this precedent as the
1146+
reason it deviated. Rebuild the fixture through scrubMessage with
1147+
the merged message re-joined from sanitized constituents (the
1148+
reset-move fixture's documented method); verifier: the three flap
1149+
pairs reproduce identical verdicts/suppression indices and
1150+
test/insertion-join-move.test.mjs + replay-gate-selfcheck stay
1151+
green. Fork main first; the upstream slices then carry the clean
1152+
fixture.
1153+
1154+
- **HOLD — prepared PR-slice branches, push blocked on two
1155+
conditions (operator-visible).** State: pr/verification-tools
1156+
advanced 53761a3 → a0a051f (15 commits, tools/ byte-equal to fork
1157+
main) in worktree cache-fix-pr4; NEW pr/insertion-join-moves at
1158+
fbec02f (b713b2f + merge of a0a051f + 7 commits, extension
1159+
byte-equal to fork main) in worktree cache-fix-pr12; both merge
1160+
clean onto upstream/main 0817302; suites green except the
1161+
pre-existing proxy-read-dedupe failure (#272 open blocker 4,
1162+
proven pre-existing at 53761a3). Drafts + exact push/gh commands:
1163+
docs/audits/pr-prep-2026-07-31/. Conditions before any push:
1164+
(1) the two fixture items above land on fork main; (2) #272's
1165+
reviewer asked for an agreed sanitization path before more
1166+
fixture movement — respond on-thread first. Also fold in at
1167+
push time: the stacked PR body should name #273 as the third
1168+
stacked parent (the merge carries deferred-tool-rewrite.mjs).
11241169
`tools/verdict-ab.mjs` has no committed test: its COULD-NOT-VERIFY
11251170
exit-2 path was demonstrated red by execution and its fixture
11261171
reader was corrected after a real 2-of-6-corpora miss (report
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Dispatcher actions — prepared 2026-07-31
2+
3+
Everything below is prepared and unpushed. Two worktrees hold the work:
4+
5+
| worktree | branch | head | state |
6+
|---|---|---|---|
7+
| `/home/g/dev/vendor/cache-fix-pr4` | `pr/verification-tools` | `a0a051f` | 15 commits appended to `53761a3` |
8+
| `/home/g/dev/vendor/cache-fix-pr12` | `pr/insertion-join-moves` | `fbec02f` | new branch: `b713b2f` + merge of `a0a051f` + 7 commits |
9+
10+
Both merge cleanly onto `upstream/main` (`0817302`) — `git merge-tree
11+
--write-tree` exits 0 for each, so no rebase is needed.
12+
13+
---
14+
15+
## STOP — decide this before step 1
16+
17+
The two harvested fixtures these branches carry contain material that the OPEN
18+
sanitization blocker on #272 is about. **Both are already on `origin/main`** (so
19+
they are already public in the fork), but pushing them into an upstream-facing PR
20+
extends the exposure into `cnighswonger`'s repo while that blocker is unresolved,
21+
and #272's reviewer explicitly asked us not to move on fixture sanitization
22+
before agreeing a path.
23+
24+
1. **`test/fixtures/harvested/reset-move-s-dc3f8071-196-197.json` embeds a real
25+
screenshot, five times.** Five identical `image/png` blocks, 13,060 base64
26+
chars each (~9.8 kB decoded, 951×55), PNG `tEXt` chunks reading
27+
`Creation Time: Mi 29 Jul 2026 16:46:01 CEST` and `Software: gnome-screenshot`.
28+
Decoded and viewed: it shows a terminal line of the operator's own prose
29+
("So: when I write PR #184 — do you see just PR #184, or ..."). Benign content,
30+
but it is unsanitized real capture, plus a locale + desktop-environment
31+
fingerprint and a wall-clock stamp — and the fixture's own `_sanitization`
32+
header claims it "keeps no raw text at all". That claim is false as written.
33+
34+
**Root cause, and it is a tool bug, not a fixture slip:** `tools/harvest.mjs:169`
35+
redacts `block.data`, but an Anthropic image block nests the base64 at
36+
`block.source.data` — one level below where the scrubber looks. Every image in
37+
every harvested fixture passes through raw. The fix belongs in `scrubBlock`
38+
(recurse into `source`), and it needs a bite that goes red on this fixture
39+
first.
40+
41+
2. **`test/fixtures/harvested/flap-s-0d6f38ba-86.json` retains raw operator hook
42+
prose.** Its `_sanitization` header says the participating reminder texts are
43+
kept RAW on purpose. Six of the eight raw strings are the operator's own hook
44+
text, quoting private files by name — `dispatch-discipline.md §1`,
45+
`CLAUDE-maintenance.md Provenance` — and paraphrasing private corpus rules.
46+
Only "The task tools haven't been used recently…" is genuine harness text.
47+
The *later* `reset-move` fixture's header states it deviated from this
48+
precedent for exactly this reason: "two of the three constituents here are the
49+
operator's own hook texts rather than harness-generated reminders, so raw
50+
retention would put operator prose in a public repo."
51+
52+
So the fork's own standard, set one day later, is not met by the fixture the
53+
verification slice would now carry upstream.
54+
55+
Both fixtures are load-bearing for tests on these branches: `flap-…` is read by
56+
`test/insertion-join-move.test.mjs` and `test/replay-gate-selfcheck.test.mjs`,
57+
`reset-move-…` by `test/insertion-join-move.test.mjs`. Dropping them means those
58+
checks lose their real-capture leg. **This is an operator decision — go / re-scrub
59+
/ hold — and nothing below should run until it is made.**
60+
61+
Everything else scanned clean on both deltas: no IPv4 or IPv6 literals, no `ssh
62+
user@host` lines, no non-loopback `host:port`, no absolute user paths, no
63+
`sk-ant-` / `Bearer` / `ghp_` shapes, no fork-only files
64+
(`BACKLOG.md`, `FORK-NOTES.md`, `CLAUDE.local.md`, `docs/directives/**`,
65+
`docs/audits/**`, `docs/code-reviews/**`, `docs/release-tests/**`,
66+
`test/fixtures/harvested/LEDGER-*.json`).
67+
68+
---
69+
70+
## 1 — push the verification-tools update
71+
72+
```bash
73+
cd /home/g/dev/vendor/cache-fix-pr4
74+
git push origin pr/verification-tools
75+
```
76+
77+
Fast-forward: the 15 new commits sit on top of the pushed `53761a3`, no force
78+
needed.
79+
80+
## 2 — comment on #276
81+
82+
```bash
83+
cd /home/g/dev/vendor/claude-code-cache-fix
84+
gh pr comment 276 --repo cnighswonger/claude-code-cache-fix \
85+
--body-file /tmp/claude-1000/-home-g-dev-vendor-claude-code-cache-fix/adf6cadb-d98f-49c5-ae2c-578f4596ca9a/scratchpad/pr-prep-comment-276.md
86+
```
87+
88+
## 3 — push the new branch
89+
90+
```bash
91+
cd /home/g/dev/vendor/cache-fix-pr12
92+
git push -u origin pr/insertion-join-moves
93+
```
94+
95+
## 4 — open the draft PR
96+
97+
Title and body live in `pr-prep-body-join-moves.md`; the file's first section is
98+
the title, the rest (from `# BODY`) is the body. Strip the two headers before
99+
posting, e.g.:
100+
101+
```bash
102+
SP=/tmp/claude-1000/-home-g-dev-vendor-claude-code-cache-fix/adf6cadb-d98f-49c5-ae2c-578f4596ca9a/scratchpad
103+
sed -n '/^# BODY$/,$p' $SP/pr-prep-body-join-moves.md | tail -n +3 > $SP/pr-prep-body-join-moves.rendered.md
104+
105+
cd /home/g/dev/vendor/claude-code-cache-fix
106+
gh pr create --repo cnighswonger/claude-code-cache-fix --draft \
107+
--base main \
108+
--head Gunther-Schulz:pr/insertion-join-moves \
109+
--title "feat(insertion-normalization): un-merge CC's join-moves, and stop a re-served entry from re-binding" \
110+
--body-file $SP/pr-prep-body-join-moves.rendered.md
111+
```
112+
113+
## 5 — cross-link on #272
114+
115+
Not drafted as a separate file; one line is enough, and it should only go up
116+
after step 4 so it can carry the real number:
117+
118+
> The join-move / reserved-entry work that this branch's pinning made possible
119+
> is now up as #NNN (draft, stacked on this branch and on #276). It closes the
120+
> identity mis-binding where one extra copy of a recurring reminder re-binds an
121+
> entry CC has stopped sending — corpus A/B: stability violations 10 → 2, and
122+
> both survivors belong to `deferred-tool-rewrite` and are identical in both
123+
> trees.
124+
>
125+
> 🤖 Generated with [Claude Code](https://claude.com/claude-code)
126+
127+
## Cleanup
128+
129+
Both worktrees have an untracked `node_modules` symlink pointing at the fork
130+
main checkout's, added so the suites could run (`hpagent` etc. are not installed
131+
per-worktree). Remove if you want the trees pristine:
132+
133+
```bash
134+
rm /home/g/dev/vendor/cache-fix-pr4/node_modules /home/g/dev/vendor/cache-fix-pr12/node_modules
135+
```
136+
137+
Note `.gitignore` has `node_modules/` with a trailing slash, so a *symlink* of
138+
that name shows as untracked rather than ignored. It was never staged.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# TITLE
2+
3+
feat(insertion-normalization): un-merge CC's join-moves, and stop a re-served entry from re-binding
4+
5+
# BODY
6+
7+
> **DRAFT — stacks on #272 and #276, review alongside them.** This branch is
8+
> cut from #272's head (`b713b2f`) and merges #276's updated head, because its
9+
> tests assert against `tools/replay.mjs`'s gate functions. Base is
10+
> `upstream/main`, so the diff shown by GitHub includes both parents' content;
11+
> the material new to *this* PR is the six commits after the merge. Same
12+
> precedent as #281.
13+
14+
## What this is
15+
16+
CC sometimes **merges a `<system-reminder>` message and its immediate
17+
standalone neighbour into a single message** mid-history, then sometimes
18+
un-merges them again a few requests later. Every one of those flips rewrites
19+
history the model has already seen, which busts the prefix cache and re-bills
20+
the whole conversation.
21+
22+
#272 gave the proxy the ability to recognise a message across
23+
re-serializations and to pin the first-seen bytes. This PR closes the two
24+
cases that pinning alone could not:
25+
26+
**1. The join-move un-merge.** When we recognise that a message CC used to send
27+
standalone has been absorbed into its neighbour, we serve the first-seen
28+
(un-merged) form upstream instead of the newly merged bytes. The join grammar
29+
is a single `"\n\n"` separator — the same literal the duplicate-suppression
30+
path already keys on — and the probe is byte-exact: the merged wire message's
31+
text must equal `pinnedReminderText(predecessor) + "\n\n" + absorbed.text`, both
32+
messages must be `role: "system"`, and the absorbed entry's neighbourhood
33+
bounds must resolve on the current wire. Anything else fails closed: no
34+
substitution, raw forward, today's behaviour.
35+
36+
**2. The identity fix — a re-served entry leaves the wire-identity space.**
37+
This is the part that took a rebuild rather than a patch. Pinned entries are
38+
keyed by `(content-hash, role, occurrence-ordinal-within-the-request)`. A
39+
recognised move keeps the absorbed entry alive in *our* canonical while CC has
40+
stopped sending it — so its ordinal is a claim about an array it is not in.
41+
The moment a later request carries **one more copy of the same recurring text**
42+
(measured: a fresh tail reminder taking `o=7`), the stale entry binds to that
43+
unrelated copy at an inverted position, which both removes it from the dropped
44+
set — so no move recognition can fire — and trips the subsequence check. The
45+
merged message then goes out raw and *our* bytes flip at an index where CC's
46+
were identical.
47+
48+
The fix marks such an entry `rs: true` and takes it out of `(h, r, o)` matching
49+
entirely. Its identity becomes its stored first-seen bytes plus the canonical
50+
slot where we last forwarded them. Each request it gets exactly one of three
51+
dispositions, checked in order: **re-fire** (the merged form is present again →
52+
re-serve), **reclaim** (CC flipped back to the original form → clear the mark
53+
and rebind as an ordinary matched entry), **lapse** (neither form present → the
54+
entry is dropped, never re-served into a region CC no longer carries). Entries
55+
that were never re-served keep absolute `(h, r, o)` matching byte-for-byte.
56+
57+
## Measured — A/B over the live corpus
58+
59+
Two detached worktrees differing only by this diff, replayed over 8.5 GB of
60+
real capture (36 captures, ~10 000 requests) under the serving gate set:
61+
62+
| capture | before | after | requests (identical both runs) |
63+
|---|---|---|---|
64+
| s-dc3f8071 | 2 | **0** | 769 |
65+
| s-58c979ce | 2 | **0** | 2073 |
66+
| s-633915a8 | 2 | **0** | 2630 |
67+
| s-9f9d8a9d | 1 | **0** | 209 |
68+
| s-0d6f38ba | 3 | **2** | 1058 |
69+
| **corpus total** | **10** | **2** | |
70+
71+
Cross-request byte-stability violations go **10 → 2**, and the two survivors
72+
are attributed by the gate's own attribution line to a *different* extension
73+
(`deferred-tool-rewrite`) and are byte-for-byte the same two pairs in both
74+
trees. **Zero insertion-normalization stability violations remain in the whole
75+
corpus.** Safety, conservation, sequence and canonical order read 0 on every
76+
capture in both trees.
77+
78+
Worth stating plainly: the same ordinal collision was firing on **four**
79+
captures, not the one it was found on. That only became visible because the
80+
measurement was corpus-wide rather than fixture-wide.
81+
82+
Two honesty notes about the sweep as an A/B. The capture *count* differs (33 vs
83+
36) — three tiny captures (2, 13 and 1 requests) were present only for the
84+
second run, all clean, none in the failing set. And several captures are live
85+
and still growing, so two sequential 8 GB sweeps are confounded in principle —
86+
but the per-capture request counts are identical for every capture in the table,
87+
so for the captures the comparison is about, both trees replayed the same input.
88+
89+
**Old-canon compatibility** was measured, not argued: `tools/verdict-ab.mjs
90+
--seed-from-a` replays decisions over canon files written by the pre-change
91+
code and is **identical across 44 verdict lines / 6 corpora**. `rs` is a new
92+
optional field; canon files from the old code contain none, and under the new
93+
code they take identical decisions. A restart shipping this is
94+
cache-transparent for every existing conversation.
95+
96+
## Non-Functional Requirements
97+
98+
- **Size/complexity budget.** ~590 added lines in
99+
`proxy/extensions/insertion-normalization.mjs` (no new production file, no
100+
new abstraction, no new env var — it extends `classifyPinned`,
101+
`resetKeepingPins` and `findJoinMoves` in place). The originating directive
102+
budgeted 120–200 LOC; the overrun is the reconciliation with #272's
103+
reset-path duplicate suppression, which had to unify two declaration paths
104+
rather than add a second one. Tests: ~1,170 lines across three files, plus a
105+
20.6k-line harvested fixture — see the fixture caveat below.
106+
- **Threat model.** Conversation fidelity is the protected property; the
107+
conservation and stability gates are the enforcement. The new risk this
108+
design introduces is re-serving stored bytes into a context CC has pruned or
109+
compacted away. The lapse disposition is the mitigation and it fails **closed**
110+
— no re-serve — whenever its preconditions are not byte-established on the
111+
current wire. No new persisted state shape beyond `rs`, one optional boolean
112+
on an existing entry.
113+
- **Maintainability.** The join grammar stays single-copy (`JOIN_SEPARATOR`);
114+
the merged-form probe is the same literal the duplicate suppression already
115+
uses, seen from the other side.
116+
- **Performance/reliability.** The disposition pass is O(reserved entries ×
117+
neighbourhood) per request; reserved entries measured at 1–2 per conversation
118+
in every observed instance.
119+
- **Load-bearing? YES.** It changes canonical state entries and the bytes
120+
forwarded on the wire.
121+
122+
## Open, and inherited from #272
123+
124+
- **#272's four blockers are not addressed here.** In particular the
125+
reminder-only-edit case (blocker 1) and the canon-file permissions (blocker 3)
126+
are properties of the base this branch sits on. Whatever lands there lands
127+
here.
128+
- **`test/proxy-read-dedupe.test.mjs:505` is red on this branch**, exactly as it
129+
is on #272 — the extension-order adjacency assertion. Reproduced at #272's
130+
head before any commit here: 41/42 on that file alone. Left for the
131+
deliberate fix #272's review asked for (move the order, or update the
132+
assertion) rather than patched in a stacked PR.
133+
- **Fixture sanitization is an open question, not a settled one.** This branch
134+
carries `test/fixtures/harvested/reset-move-s-dc3f8071-196-197.json` (the
135+
reset leg) and, via the #276 merge, `flap-s-0d6f38ba-86.json` (the
136+
oscillation leg). #272's review flagged the harvested-fixture class as a
137+
blocker and asked us to agree a path before pushing changes. Neither fixture
138+
should be treated as reviewed on that axis by this PR.
139+
140+
🤖 Generated with [Claude Code](https://claude.com/claude-code)

0 commit comments

Comments
 (0)