Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
ebfc3ea
feat(insertion-normalization): pin volatile reminder blocks so mid-hi…
Gunther-Schulz Jul 29, 2026
7c1ebb6
feat(insertion-normalization): suppress the migrated duplicate — comp…
Gunther-Schulz Jul 30, 2026
1a1010a
test(insertion-suppression): real-pair check falls back to pinned fix…
Gunther-Schulz Jul 30, 2026
3217f5c
insertion-normalization: a merged standalone matches the join of its …
Gunther-Schulz Jul 30, 2026
54df5f5
insertion-normalization: never strip the tail — a final-message dupli…
Gunther-Schulz Jul 30, 2026
5f72fbb
sync(insertion-suppression): fixture reader loads dynamically — match…
Gunther-Schulz Jul 30, 2026
720ecb4
test(insertion-merge-suppression): carry the oscillation fixture the …
Gunther-Schulz Jul 30, 2026
d0bfa87
feat(insertion-normalization): pin volatile reminder blocks so mid-hi…
Gunther-Schulz Jul 29, 2026
9ac0bfb
feat(deferred-tool-rewrite): hold tools[] byte-stable; announce addit…
Gunther-Schulz Jul 29, 2026
57e591b
feat(tools): the verification stack — replay gate, census, harvest, s…
Gunther-Schulz Jul 29, 2026
0633e23
refresh: the tools as they actually are — checker fix, successions, s…
Gunther-Schulz Jul 29, 2026
c4569ec
refresh(tools): replay reads the output side — block-migration census…
Gunther-Schulz Jul 30, 2026
c56648d
refresh(tools): gate-warning + --gates-from-capture + marker-blind ou…
Gunther-Schulz Jul 30, 2026
7e38919
harvest: --pin freezes a sanitized range as a named fixture
Gunther-Schulz Jul 30, 2026
9702540
harvest: scrub wrapped reminders with their own token, not a fixed co…
Gunther-Schulz Jul 30, 2026
3d2095b
test: real-pair checks fall back to pinned fixtures — rotation stops …
Gunther-Schulz Jul 30, 2026
eba05c2
sync(insertion-normalization): match #272 tip — join-hash + tail guard
Gunther-Schulz Jul 30, 2026
6682282
gate-live: toolsDeltas summary rides the status row — row 6's consume…
Gunther-Schulz Jul 30, 2026
cd1037d
replay: adjacent-duplicate census counter — the #78420 falsifier re-a…
Gunther-Schulz Jul 30, 2026
31581de
shape-verdicts: every telemetry file gets a reader — alarm on content…
Gunther-Schulz Jul 30, 2026
4327ec7
shape-verdicts: gate state falls back to the sweep's recorded serving…
Gunther-Schulz Jul 30, 2026
bd68417
fresh-session-sort: relocations report themselves — the checker exemp…
Gunther-Schulz Jul 30, 2026
78c890c
replay: heldStable — the guarantee actually made, measured over the s…
Gunther-Schulz Jul 30, 2026
053314b
shape-verdicts: upstream-errors row lands before the gate's first fli…
Gunther-Schulz Jul 30, 2026
962917f
test: telemetry assertions derive from the exported table — the hardc…
Gunther-Schulz Jul 30, 2026
b242387
sync(insertion-suppression): fixture reader loads dynamically — match…
Gunther-Schulz Jul 30, 2026
84998a9
fixtures: the 2026-07-30 flap pinned before rotation — and its premis…
Gunther-Schulz Jul 30, 2026
9d6e37a
census: flap annotation — a blockMigration pair reversing within 5 re…
Gunther-Schulz Jul 30, 2026
b1cf7af
census: blockMigration counts only reminder-wrapped blocks — a messag…
Gunther-Schulz Jul 30, 2026
e6ec5b4
replay: fidelity gate — every CC byte accounted in the forwarded body…
Gunther-Schulz Jul 30, 2026
afe1a88
tools: byte-match census for the row-4 migration; export conversation…
Gunther-Schulz Jul 31, 2026
571cc14
tools: bust-triage — one command from an observed bust to a classifie…
Gunther-Schulz Jul 31, 2026
8e70af4
census: placement measured by host identity, not first-content-match
Gunther-Schulz Jul 31, 2026
8c05037
census: read captures by LINE and name what it could not read
Gunther-Schulz Jul 31, 2026
7ab7b5f
census: split EXTENDED into MERGED-STANDALONE and NEW-TEXT
Gunther-Schulz Jul 31, 2026
2a86b04
census: classify prune events, and ride the byte-gate on the daily sweep
Gunther-Schulz Jul 31, 2026
13e00d3
bust-triage: see the controlled-cost events the statusline shows
Gunther-Schulz Jul 31, 2026
42b3a91
harvest: scrub per paragraph so merge/extension relations survive san…
Gunther-Schulz Jul 31, 2026
14b6378
test: correct a census comment that bffcb05 refuted the same day
Gunther-Schulz Jul 31, 2026
5549d46
replay: the join-move re-serve is a declared action the gates must kn…
Gunther-Schulz Jul 31, 2026
3f42811
tools: verdict-ab — per-request classification verdicts for two trees…
Gunther-Schulz Jul 31, 2026
4bbc4cf
sync(sanitization): hardened scrubber, identifier-free capture discov…
Gunther-Schulz Aug 1, 2026
f9b74fb
merge pr/verification-tools: the replay gate stack this slice's tests…
Gunther-Schulz Aug 1, 2026
064e2cf
fix(insertion-normalization): run migrated-duplicate suppression on t…
Gunther-Schulz Jul 31, 2026
355691b
insertion-normalization: declare reset-path suppressions, and refuse …
Gunther-Schulz Jul 31, 2026
3f5a69d
insertion-normalization: integrate units 2 + 2b (join-move re-serve, …
Gunther-Schulz Jul 31, 2026
fb47ecf
insertion-normalization: a re-served entry leaves the wire-identity s…
Gunther-Schulz Jul 31, 2026
e47e878
insertion-normalization: the reset path's reclaim lookup is a map, no…
Gunther-Schulz Jul 31, 2026
d0bd5c5
test: TODO 15's control asserted the symptom — rewritten to assert th…
Gunther-Schulz Jul 31, 2026
8f41370
insertion-normalization: say why the reset-path guard re-checks what …
Gunther-Schulz Jul 31, 2026
edd3173
absence-scan: declared allowlist exemption for upstream's own transcr…
Gunther-Schulz Aug 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions docs/CONSUMER-SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Consumer setup — the protection set, nothing else

For running this fork purely as protection: no captures, no telemetry, no
development machinery. Install and launch mechanics are upstream's — follow
the main [README](../README.md) ("Quick Start", "Running as a service");
this page only tells you **which switches to turn on and why**.

## What it protects against

Claude Code re-sends the whole conversation every request; Anthropic bills
the unchanged part cheaply only while the bytes match exactly. Three CC
behaviors break that match and silently re-bill six-figure token counts:

1. **Old reminder blocks get re-shaped mid-history**
([anthropics/claude-code#76606](https://github.com/anthropics/claude-code/issues/76606),
[#78660](https://github.com/anthropics/claude-code/issues/78660)) — an
edit deep in the history re-bills everything after it.
2. **The tools list changes when a tool loads mid-session**
([#81967](https://github.com/anthropics/claude-code/issues/81967)) — the
tools list heads the cached prefix, so one late tool load re-bills the
entire context.
3. **Byte drift in already-sent messages** (stray whitespace, block
re-serialization —
[#48734](https://github.com/anthropics/claude-code/issues/48734)) — any
2-byte wobble invalidates the whole prefix.

The extensions below hold the forwarded bytes stable across all three, and
a last-line guard makes sure no mitigation can ever corrupt a conversation:
on any structural mismatch it forwards the original untouched.

## The switches

Bake these into the service at install time (see the README's
`install-service` section — flags set at install time land in the unit):

```sh
CACHE_FIX_FORWARD_PROXY=on \
CACHE_FIX_INSERTION_NORMALIZE=1 \
CACHE_FIX_VOLATILE_PIN=1 \
CACHE_FIX_TOOL_REWRITE=1 \
CACHE_FIX_OUTPUT_GUARD=1 \
cache-fix-proxy install-service
```

| switch | what it does |
|---|---|
| `FORWARD_PROXY=on` | transport mode — Claude Code connects through the proxy with no `ANTHROPIC_BASE_URL` change |
| `INSERTION_NORMALIZE=1` | recognizes messages by content, so relocated/re-shaped history is forwarded in its first-seen form |
| `VOLATILE_PIN=1` | pins reminder blocks to their first serialization — CC's re-stamps stop reaching the wire |
| `TOOL_REWRITE=1` | freezes the tools list; late-loaded tools are announced at the tail instead of re-writing the prefix (auto-limited to models measured to support it — everywhere else it degrades to stock behavior, never an error) |
| `OUTPUT_GUARD=1` | the safety net: validates structure after all mitigations and restores the original on any violation |

Everything upstream ships enabled by default stays enabled — those handle
further stabilization (fingerprint stripping, sort stabilization, etc.).

**Deliberately NOT enabled** (development/telemetry, not protection):
`REQUEST_CAPTURE`, `SESSION_MIRROR`, `PREFIXDIFF`, `UPSTREAM_DETECTION` —
these record traffic for the verification machinery. A consumer needs none
of them; leaving them off means nothing about your conversations is written
to disk beyond what Claude Code itself stores.

## How you'd notice it working

The mitigation is invisible by design — the observable is your usage:
long sessions stop hitting sudden six-figure `cache_creation` spikes on
turns where nothing big changed. If you suspect a problem, the guard's
restore events land in
`~/.claude/cache-fix-snapshots/guard-events.jsonl`; an empty or absent
file is the normal state.
476 changes: 476 additions & 0 deletions docs/dev-loop.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions proxy/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
"workflow-agent-id-synthesis": { "enabled": true, "order": 365 },
"image-retry-circuit-breaker": { "enabled": true, "order": 370 },
"read-dedupe": { "enabled": true, "order": 380 },
"insertion-normalization": { "enabled": true, "order": 395 },
"cache-control-normalize": { "enabled": true, "order": 400 },
"messages-cache-breakpoint": { "enabled": true, "order": 410 },
"deferred-tool-rewrite": { "enabled": true, "order": 425 },
"ttl-management": { "enabled": true, "order": 500 },
"cache-telemetry": { "enabled": true, "order": 600 },
"overage-warning": { "enabled": true, "order": 610 },
Expand Down
675 changes: 675 additions & 0 deletions proxy/extensions/deferred-tool-rewrite.mjs

Large diffs are not rendered by default.

32 changes: 29 additions & 3 deletions proxy/extensions/fresh-session-sort.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,29 @@ export default {
return;
}

// Scan backwards to find latest instance of each relocatable block type
// Scan backwards to find latest instance of each relocatable block type.
// `occurrences` counts EVERY instance seen in the same pass (not just the
// kept one) — the extension's own record of whether the type it is about
// to relocate has ever appeared anywhere else in this array (including
// already at messages[firstUserIdx], before mutation). One occurrence
// means this relocation is the type's first appearance in the whole
// array — the deliberate one-time bust this extension exists for
// (see the module doc). More than one means it recurred — reported as
// such rather than folded into the same "first appearance" telemetry,
// so a consumer (replay's stability exemption) can tell the two apart
// instead of re-deriving it from shape.
const found = new Map();
const occurrences = new Map();
for (let i = body.messages.length - 1; i >= firstUserIdx; i--) {
const msg = body.messages[i];
if (msg.role !== "user" || !Array.isArray(msg.content)) continue;
for (let j = msg.content.length - 1; j >= 0; j--) {
const block = msg.content[j];
const text = block.text || "";
const blockType = getBlockType(text);
if (!blockType || found.has(blockType)) continue;
if (!blockType) continue;
occurrences.set(blockType, (occurrences.get(blockType) ?? 0) + 1);
if (found.has(blockType)) continue;

const fixedText = fixBlockText(blockType, text);
const { cache_control, ...rest } = block;
Expand All @@ -180,11 +193,24 @@ export default {

// Prepend in deterministic order: deferred → mcp → skills → hooks
const ORDER = ["deferred", "mcp", "skills", "hooks"];
const toRelocate = ORDER.filter((t) => found.has(t)).map((t) => found.get(t));
const relocatedTypes = ORDER.filter((t) => found.has(t));
const toRelocate = relocatedTypes.map((t) => found.get(t));

body.messages[firstUserIdx] = {
...body.messages[firstUserIdx],
content: [...toRelocate, ...body.messages[firstUserIdx].content],
};

// Report what happened — nothing downstream re-derives this. A
// first-appearance relocation prepends content CC never had at
// messages[firstUserIdx] before, which is exactly the shape replay's
// cross-request stability check flags as a self-inflicted byte flip
// (module doc, top). Telemetry lets that check tell the deliberate
// one-time bust apart from a genuine repeat/thrash at the same index.
ctx.meta = ctx.meta || {};
ctx.meta.freshSessionSortStats = {
relocated: relocatedTypes.map((t) => ({ type: t, firstAppearance: occurrences.get(t) === 1 })),
targetIndex: firstUserIdx,
};
},
};
Loading
Loading