diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb72df0..cb7ab22e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,60 @@ - **`CACHE_FIX_DOWNLOAD_REWRITE=on` disables `claude update` entirely**, which the flag's name does not suggest. Rewriting a download URL requires MITM-ing `downloads.claude.ai`, whose release client pins public roots only, so the version check fails before anything downloads. It cannot be narrowed to the binary path (MITM is decided per host at `CONNECT`, and the version check shares the host) and no client-side override reaches that client. Documented with the measurement in the README. +## [4.4.0-beta.0] - 2026-08-07 + +Headline: **the attribution series.** This release is dominated by ten weeks of work from [@Gunther-Schulz](https://github.com/Gunther-Schulz) on one question — *when a cache bust happens, whose fault is it?* — and the answer turned out to require instrumenting the request path before we could fix anything on it. Two of the resulting features change what the proxy sends upstream. All are additive; nothing on the wire changes for an operator who sets no new env vars. + +**This is a beta, published on the npm `next` tag, and that is not a formality.** The dogfood host ran v4.3.0 for the entire development window of this release — none of the features below has executed against live traffic anywhere. The promote criteria for `v4.4.0-beta.0` → `v4.4.0` are written down in [`docs/releases/v4.4.0-beta-promote-criteria.md`](docs/releases/v4.4.0-beta-promote-criteria.md), including what would count as a failure, because a soak window with no stated pass condition promotes on the absence of complaints rather than on evidence. + +### Added + +- **Cache-key attribution: `capture` + `prefix-diff` ([#275](https://github.com/cnighswonger/claude-code-cache-fix/pull/275), [#280](https://github.com/cnighswonger/claude-code-cache-fix/pull/280); contributed by [@Gunther-Schulz](https://github.com/Gunther-Schulz)).** `capture` records what Claude Code *actually sent* at order 60, before any mutating extension runs, so a divergence present in the raw capture is CC's and one absent from it is ours. Without that line there is no way to distinguish a mitigated upstream bug from a self-inflicted one — which was learned the expensive way: **five of six suspected CC bugs turned out to be our own.** `prefix-diff` is the reader for it, reworked from a head-only preview into full attribution: marker-anchored and tail snapshot windows (long-session busts sat past the head window on every deep request and were simply invisible), coverage of every field the cache actually keys on (`model`, `max_tokens`, `thinking`, `output_config`, `speed`, `betas`, and the `anthropic-beta` header — each a measured blind spot that previously let a real bust report "no differences"), and per-tenant baselines keyed by agent-id header falling back to the full system-prompt hash, because truncating to 400 characters merged distinct agents into one baseline. Both are diagnostic surfaces; neither changes what is sent. + +- **`insertion-normalization` — volatile reminder blocks no longer bust the prefix ([#272](https://github.com/cnighswonger/claude-code-cache-fix/pull/272); contributed by [@Gunther-Schulz](https://github.com/Gunther-Schulz)).** Claude Code rewrites parts of conversation history mid-session; when a rewritten block sits ahead of the cache boundary the entire prefix re-bills. The extension pins the volatile blocks so the rewrite lands behind the boundary instead of ahead of it. **Carries `needs-sim-validation`** — the fork-context case cannot be reproduced locally, and the beta soak is its validation; see the waiver on #272. + +- **`deferred-tool-rewrite` — hold `tools[]` byte-stable ([#273](https://github.com/cnighswonger/claude-code-cache-fix/pull/273); contributed by [@Gunther-Schulz](https://github.com/Gunther-Schulz)).** The name reads backwards and is worth stating plainly: **this extension does not rewrite `tools[]`. It exists so that we never have to.** Every ToolSearch/deferred-tool load makes Claude Code re-send a different `tools[]` array; `tools[]` heads the cache prefix, so each load re-bills the entire context — the class reported in [anthropics/claude-code#81967](https://github.com/anthropics/claude-code/issues/81967), there triggered by LSP add/remove, but hit far more often on the deferred-tool path. Anthropic's API already ships the fix as the documented `mid-conversation-tool-changes-2026-07-01` beta, and Claude Code 2.1.220 carries that beta's documentation in its own binary without using it on the wire. The extension freezes `tools[]` at its first-seen form per session — keyed on session + system prompt + conversation, so subagents and sidecars never share a baseline — and announces mid-session additions through the beta channel instead of mutating the array. **Load-bearing:** it changes what we send upstream. + +- **`output-guard` — a last-line invariant on the outgoing request ([#278](https://github.com/cnighswonger/claude-code-cache-fix/pull/278); contributed by [@Gunther-Schulz](https://github.com/Gunther-Schulz)).** Behind `CACHE_FIX_OUTPUT_GUARD=1`, **default off.** A pipeline of body-mutating extensions needs one check that runs last and asks a different question: is the body we are about to send upstream still structurally valid, in every way Anthropic's API and the client's session depend on? The guard validates hard invariants on the outgoing request body — `tool_use`/`tool_result` adjacency, marker budget, role validity and system-message placement, content presence, assistant-terminal shape — and on violation **restores the pre-mutation body Claude Code originally sent**, forwarding that instead of the pipeline's output. A mutation bug becomes one logged `CRITICAL` line and a `guard-events.jsonl` row instead of a corrupted session. It fails open in both directions that matter: a validator crash forwards the mutated body with a `WARN` rather than breaking the request, and a missing stash forwards as-is and says so loudly. **This ships as two extension files and is one feature:** `output-guard-stash` at order 55 clones the body before the first mutator, `output-guard` at order 690 validates and restores from that clone. The pipeline loads one default export per file and the two halves must run at opposite ends of it; the split is scoping, not two features. **It guards against us, not against Claude Code** — the failure it exists to catch is our own extension chain producing a body the client never wrote. + +- **`usage-log` emits `ttl_tier` and `duration_ms` ([#320](https://github.com/cnighswonger/claude-code-cache-fix/pull/320)).** Behind `CACHE_FIX_USAGE_LOG_EXTENDED=on`, **default off.** Consumers need [claude-code-meter](https://github.com/cnighswonger/claude-code-meter) v0.9.1 or later, whose row schema is a `strictObject` — an older meter silently drops any row carrying the new keys rather than erroring. Off by default means the blast radius is bounded to operators who explicitly opt in and have upgraded. + +- **`tier-advisor` CLI ([#244](https://github.com/cnighswonger/claude-code-cache-fix/pull/244)).** Recommends cache TTL tier upgrades or downgrades from measured session behavior rather than guesswork. Closes [#63](https://github.com/cnighswonger/claude-code-cache-fix/issues/63). + +- **`session-budget-breaker` ([#262](https://github.com/cnighswonger/claude-code-cache-fix/pull/262)).** Opt-in per-session hard spend ceiling that short-circuits `/v1/messages` locally once a session crosses a token, dollar, or rate limit — capping the [anthropics/claude-code#68285](https://github.com/anthropics/claude-code/issues/68285) single-session fan-out before it reaches Anthropic. Fails open on every uncertain path. + +### Fixed + +- **Extension-mutated headers now actually reach upstream ([#274](https://github.com/cnighswonger/claude-code-cache-fix/pull/274); contributed by [@Gunther-Schulz](https://github.com/Gunther-Schulz)).** They never did. Any extension that set a header was writing to an object the forward path did not read. + +- **`thinking-block-sanitize` protects continuations by shape, not by tail distance ([#279](https://github.com/cnighswonger/claude-code-cache-fix/pull/279); contributed by [@Gunther-Schulz](https://github.com/Gunther-Schulz)).** Distance from the tail is a proxy for "is this an active tool continuation" that fails whenever the conversation grows between turns. + +- **A growing conversation is not an upstream change ([#282](https://github.com/cnighswonger/claude-code-cache-fix/pull/282); contributed by [@Gunther-Schulz](https://github.com/Gunther-Schulz)).** `upstream-change-detection` treated ordinary conversation growth as a signal that Claude Code had changed under us, which is the one thing it must not do — the detector fires an alarm operators are meant to act on. + +- **A supervised stop exits 0 ([#277](https://github.com/cnighswonger/claude-code-cache-fix/pull/277); contributed by [@Gunther-Schulz](https://github.com/Gunther-Schulz)).** Lingering streams are forced closed rather than held open until a service manager reports a clean shutdown as a failure. + +- **`--remote-control` no longer clobbers another component's `NODE_EXTRA_CA_CERTS`, and the `ca-trust.pem` guard asks node's loader instead of predicting it ([#283](https://github.com/cnighswonger/claude-code-cache-fix/pull/283), [#296](https://github.com/cnighswonger/claude-code-cache-fix/pull/296); contributed by [@codeslake](https://github.com/codeslake)).** Both entries in full under **[Unreleased]** above, which this release promotes. The second is worth reading as a method: a regex modelling node's CA loader took five review rounds and was still wrong in *both* directions on real bundles, and the fix was to stop modelling and start asking — spawn a child with the variable set, stand up a TLS server, connect to it. ~25 ms, once per launch. + +- **`--remote-control` no longer routes localhost traffic through the proxy ([#257](https://github.com/cnighswonger/claude-code-cache-fix/pull/257)).** Forward-proxy mode set `HTTPS_PROXY` without `NO_PROXY`, so every client connection — including to HTTP/SSE-transport MCP servers on `127.0.0.1` — went to the cache-fix proxy, which 404s anything that is not `api.anthropic.com`. stdio-transport MCP servers were unaffected, which is why this survived v4.3.0: the common case kept working. `127.0.0.1,localhost,::1` are now merged into any existing `NO_PROXY`/`no_proxy` in forward mode. **A v4.3.0 regression** — anyone who adopted the release's headline feature with a local HTTP MCP server hit it. + +- **RFC 7230 absolute-form request-targets are honored in forward-proxy mode ([#261](https://github.com/cnighswonger/claude-code-cache-fix/pull/261); contributed by [@codeslake](https://github.com/codeslake)).** + +- **Bounded `strlcpy`/`snprintf` in the VS Code wrapper ([#294](https://github.com/cnighswonger/claude-code-cache-fix/pull/294); contributed by [@anupamme](https://github.com/anupamme)).** First contribution — a security hardening fix in C we had not caught ourselves. + +- **Model pricing refreshed; the cost ceiling stops pricing unknown models at zero ([#259](https://github.com/cnighswonger/claude-code-cache-fix/pull/259)).** `tools/rates.json` prices `session-budget-breaker`'s `_COST_USD` lever, which for direct-API-key users is a literal dollar ceiling. `claude-opus-5` was absent from it entirely, so traffic on that model priced at zero and the ceiling never tripped. Adds current pricing for the Opus 5 / 4.8 / 4.7, Fable 5, Sonnet 5, Mythos 5 and bare Haiku 4.5 identifiers, plus a `tools/update-rates.mjs` fetcher that **fails closed on every uncertainty** — a required live-traffic model missing, an ambiguous or not-yet-effective row, a price outside a sane band, cache prices contradicting the documented multipliers — because a plausible-looking wrong price on a spend limit is worse than no update. Fetches to a reviewable PR, never a silent write. Closes [#258](https://github.com/cnighswonger/claude-code-cache-fix/issues/258). + +### Documentation + +- **Machine-assisted caveat on every translated README, and all three regenerated ([#311](https://github.com/cnighswonger/claude-code-cache-fix/pull/311), [#312](https://github.com/cnighswonger/claude-code-cache-fix/pull/312), [#313](https://github.com/cnighswonger/claude-code-cache-fix/pull/313), [#315](https://github.com/cnighswonger/claude-code-cache-fix/pull/315)).** `README.zh.md` and `README.ko.md` were pinned at 2026-06-15 — thirteen commits and +458/−2 behind English, still advertising v3.0.3 against a current v4.x — and `docs/guia-pt-br.md` was older still, documenting a preload mode superseded since v3.0.0. The caveat header is **permanent policy**, not a flag to remove once a native speaker reviews: a machine-assisted translation stays machine-assisted. Original translators keep their attribution — [@VictorSun92](https://github.com/VictorSun92) (zh), [@ArkNill](https://github.com/ArkNill) (ko), [@thepiper18](https://github.com/thepiper18) (pt-br); regeneration updates their work, it does not replace it. + +- **Benchmarking methodology for external evaluators ([#317](https://github.com/cnighswonger/claude-code-cache-fix/pull/317)),** including a Limitations section stating where this proxy is the wrong tool, meant to be read before adoption rather than after. + +- **Pre-publication guards directive ([#302](https://github.com/cnighswonger/claude-code-cache-fix/pull/302)).** Directive only; implementation deferred to v4.4.1. Records why the hook is a chain wrapper and explicitly **not** `core.hooksPath` — which would blow away a maintainer host's existing `post-merge`/`post-checkout` hooks — so the next person to reach for it finds the reason instead of rediscovering it. + +- **Label semantics split: `approved-by-lead` is a review record, `ready-for-merge` is the merge gate ([#323](https://github.com/cnighswonger/claude-code-cache-fix/pull/323)).** The two used to be one label meaning two things, and stamping the first silently satisfied the second. `ready-for-merge` is now the maintainer's alone; its absence alongside the agent approvals is the normal waiting-on-human state, not an oversight to be corrected. + +- **Two capture-derived-data closures ([#307](https://github.com/cnighswonger/claude-code-cache-fix/pull/307), [#319](https://github.com/cnighswonger/claude-code-cache-fix/pull/319)).** A transcript-shape fixture rebuilt from known-safe parts, and two real session UUIDs scrubbed from committed review artifacts. + ## [4.3.0] - 2026-07-17 Headline: **Remote Control works through the proxy.** Claude Code ≥ 2.1.196 disables Remote Control / mobile session visibility (and `/schedule`, claude.ai MCP connectors) whenever `ANTHROPIC_BASE_URL` is set — which is exactly how reverse-proxy mode routes the client. This release adds an opt-in **forward-proxy mode** that keeps the client first-party (`ANTHROPIC_BASE_URL` unset, `HTTPS_PROXY` set) so those features keep working while the proxy still sees and transforms `/v1/messages`. All changes are additive and backward-compatible; every new mode is opt-in and defaults are unchanged. diff --git a/README.md b/README.md index 19ad040e..5d62ac6f 100644 --- a/README.md +++ b/README.md @@ -1483,7 +1483,10 @@ Constraints that assume you've already decided to run this: - **[@ojura](https://github.com/ojura)** — Opus 4.7 thinking-summaries root-cause analysis: filed [anthropics/claude-code#59844](https://github.com/anthropics/claude-code/issues/59844) with the CLI-binary decode (`!getIsNonInteractiveSession()` gate at offset 230510599 in v2.1.142) and the two-stacked-special-cases framing, which made the `thinking-display` extension (v3.6.1) a clean proxy-side complement to the proposed upstream fix - **[@yurukusa](https://github.com/yurukusa)** — [Cluster taxonomy](https://yurukusa.github.io/cc-safe-setup/cluster-tracker.html#cluster-extended-thinking-wedge) for [anthropics/claude-code#63147](https://github.com/anthropics/claude-code/issues/63147) thinking-desync wedge; the 13E (ToolSearch) sub-pattern synthesis that made the `thinking-block-sanitize` v2 directive predicate tractable (cache-fix #171, shipped behind `=v2` opt-in in v4.0.0) - **[@schuay](https://github.com/schuay)** — `quota-statusline.sh` enhancements: 10-cell quota bar with elapsed-time tick and exhaust-vs-reset projection replacing the prior `%/min` burn-rate display (PR #140, v3.6.2), and d/h vs h/m time-format autoselect plus named time-unit and burn-warmup constants (PR #143, v3.7.0) -- **[@codeslake](https://github.com/codeslake)** — Opt-in forward-proxy mode (HTTP `CONNECT` + selective MITM of the upstream host) that keeps Remote Control / mobile session visibility working through the proxy, resolving the `ANTHROPIC_BASE_URL`-disables-RC breakage on CC >= 2.1.196 (PR #251, implements #248); and honoring `CLAUDE_CONFIG_DIR` for all on-disk proxy state so multiple config roots don't clobber each other's credentials/state (PR #246) +- **[@codeslake](https://github.com/codeslake)** — Opt-in forward-proxy mode (HTTP `CONNECT` + selective MITM of the upstream host) that keeps Remote Control / mobile session visibility working through the proxy, resolving the `ANTHROPIC_BASE_URL`-disables-RC breakage on CC >= 2.1.196 (PR #251, implements #248); and honoring `CLAUDE_CONFIG_DIR` for all on-disk proxy state so multiple config roots don't clobber each other's credentials/state (PR #246); RFC 7230 absolute-form request-target handling in forward-proxy mode (PR #261); and the `ca-trust.d` rendezvous that lets the launcher coexist with another MITM on the same host instead of clobbering its `NODE_EXTRA_CA_CERTS` (PR #283), with the follow-up that replaced a regex model of node's CA loader with an actual probe — spawn a child, stand up a TLS server, connect to it — after the regex proved wrong in both directions on real bundles (PR #296) +- **[@Gunther-Schulz](https://github.com/Gunther-Schulz)** — The attribution series: `capture` (PR #275), the pre-pipeline record of what Claude Code actually sent, which is what makes "is this bust ours or upstream's" answerable at all; `prefix-diff` (PR #280) reworked from head-only preview into full cache-key attribution — marker/tail windows, every cache-keyed param and beta header, per-tenant baselines; `insertion-normalization` (PR #272) pinning volatile reminder blocks so mid-history rewrites land behind the cache boundary; `deferred-tool-rewrite` (PR #273) holding `tools[]` byte-stable and announcing mid-session tool additions through Anthropic's `mid-conversation-tool-changes` beta rather than mutating the array; and `output-guard` (PR #278), the last-line response invariant that restores rather than corrupts. Plus the forwarding fix for extension-mutated headers (PR #274), continuation protection by shape rather than tail distance (PR #279), a growing conversation no longer read as an upstream change (PR #282), a supervised stop exiting 0 (PR #277), and the transcript-shape fixture rebuild (PR #307) +- **[@anupamme](https://github.com/anupamme)** — Bounded `strlcpy`/`snprintf` in `claude-vscode-wrapper.c` (PR #294), a memory-safety hardening in the C wrapper we had not caught ourselves +- **[@thepiper18](https://github.com/thepiper18)** — Original Brazilian Portuguese translation (`docs/guia-pt-br.md`, #109 era); regenerated against v4.x reality in PR #315, which updates their work rather than replacing it If you contributed to the community effort on these issues and aren't listed here, please open an issue or PR — we want to credit everyone properly. diff --git a/docs/releases/v4.4.0-beta-promote-criteria.md b/docs/releases/v4.4.0-beta-promote-criteria.md new file mode 100644 index 00000000..c6ed1048 --- /dev/null +++ b/docs/releases/v4.4.0-beta-promote-criteria.md @@ -0,0 +1,166 @@ +# v4.4.0-beta.0 → v4.4.0 promote criteria + +Written and agreed before the soak starts, so nothing lives only in a chat +message that a compaction or a shift-change could lose. This document IS the +pass condition. Silence promotes only if these five criteria hold; evidence +against any one of them holds promotion pending investigation, not "let's +decide." + +## Baseline (v4.3.0, measured this session) + +The instrument that measures the beta is this session's own telemetry against +the currently-running v4.3.0 proxy on the dogfood host (up since 2026-07-28 11:48Z). + +- **Hit rate:** 99.9–100.0% sustained across ~5,250 requests today +- **`cache_creation` tokens per turn:** ~126 steady-state on typical fires +- **Warmer:** fires every ~30 min, both halves of the 1h TTL tier are + observable + +If this session's warmer keeps firing and the numbers keep looking like +that, criterion 3 is continuously satisfied for free. If the warmer goes +quiet, that is not a pass — it is a missing instrument. + +## Prerequisite + +**The dogfood-host operator restarts the proxy onto the beta build.** The +current process has ten days of uptime predating every feature in this +release; nothing measures until this restart. The 24–48h soak clock starts +at the restart, not at merge, not at publish. + +## The five promote criteria + +Each must hold across the full soak window (24–48h). A tripped criterion +holds promotion for investigation, not "let's decide." + +### 1. Proxy uptime + +- No crashes, no unbounded restarts. +- `/health` responds throughout. +- `bytesTransferred` counter is monotonic (a reset means a silent respawn + the criterion missed). + +### 2. `output-guard` restore-path never fires on real traffic + +The guard exists to catch a broken **outgoing request body** produced by +our own mutating extensions, and to forward Claude Code's original bytes +instead. A fire is a real defect in one of those extensions — not in +Claude Code, and not in anything Anthropic sent back. + +- **Zero fires over the soak = pass**, but only after step (a) below. +- **≥ 1 fire = hold** for immediate triage; do not promote. +- Telemetry at + `${CLAUDE_CONFIG_DIR:-~/.claude}/cache-fix-snapshots/-guard-events.jsonl` + carries class + count + sid, never body content. + +**Caveat (a):** zero fires is also what a guard that never runs produces +— the `#cache-proxy` push-path bug from 2026-08-07 was a component that +looked healthy because nothing exercised it. Before treating zero as a +pass, confirm the extension is loaded and reached by **one deliberate +synthetic fire early in the soak, then zero for the remainder.** A guard +proved to fire on demand and then quiet is a much stronger claim than +one silent throughout. + +### 3. No cache-hit-rate regression vs. v4.3.0 baseline + +- Hit rate holds **≥ 99.0%** on the same session shape. `usage.jsonl` + `cache_read_input_tokens / (cache_read_input_tokens + cache_creation_input_tokens)` + per turn is the number. +- `cache_creation` tokens per turn hold **near ~126/turn steady-state**. + +The creation-token floor is why this criterion has two halves rather than +one. A hit rate can look fine at 99.5% while creation tokens climb from +126 to four figures per turn — the ratio hides the movement behind the +larger denominator. If `cache_creation` moves upward materially at a +steady hit rate, `insertion-normalization` (#272) or +`deferred-tool-rewrite` (#273) is busting the prefix and the ratio will +not surface it. Watch both. + +**Caveat (a): a quota block longer than the cache TTL produces this +exact signature with no defect present.** Before treating a +`cache_creation` climb as a regression, check `usage.jsonl` for a gap +immediately preceding it. If the gap exceeds the session's TTL tier +(1h on this host), the spike is reconnection cost, not a bust. + +Measured on the dogfood host during the 2026-08-07 16:50Z window, before +the soak began: + +| | | +|---|---| +| Fleet blocked (Q5h exhausted) | 14:57:50Z → 16:50:24Z — **1h53m** | +| Cache TTL tier | **1h** — every prefix expired mid-block | +| Cold-start turns on reconnect | **9**, within 4½ minutes | +| Their `cache_creation` | 895k, 823k, 802k, 705k, 682k, 632k, 447k, 370k, 150k | +| Hit rate on 7 of the 9 | **0%** | +| Share of the window's total `cache_creation` | **5.5M of 5.6M** | + +Read through the two halves of this criterion in isolation, that is an +unambiguous prefix-corruption regression: creation tokens four orders of +magnitude above the ~126 floor, hit rate at zero. It was a quota block. +No extension was involved; the proxy was running v4.3.0 at the time. + +The block is self-reinforcing, which is why this is not a rare edge: a +block longer than the TTL guarantees the cold restart, because **the +warmer cannot fire without quota either.** The instrument goes down with +the thing it measures. Any Q5h exhaustion over an hour produces this, +and the fleet exhausted Q5h twice on 2026-08-07 alone. + +**How to apply during triage:** + +1. Note the timestamp of the first turn showing the spike. +2. `grep '"ts":"T' ~/.claude/usage.jsonl` and look backward + for a gap in rows. +3. Gap longer than the TTL tier immediately before the spike → **discard + that window and resume measuring once hit rate recovers.** Not a + criterion-3 trip, not a hold. +4. Gap absent, or the elevated `cache_creation` persists after hit rate + has recovered → **that is the real signal.** Hold the promote. + +The distinguishing feature is persistence, not magnitude. Reconnection +cost is a spike that resolves within a few turns as prefixes re-warm; a +prefix bust is a sustained elevation that survives a healthy hit rate. + +**This exclusion does not soften the criterion.** It removes one known +false positive with a named, checkable signature. Anything that does not +match that signature still trips. + +### 4. No new 400/500 pattern + +- Especially not the pre-#278 last-line-invariant class. +- Absence of 500s alone is not sufficient — pre-existing traffic may + have been noisy. The criterion is **no NEW class of upstream error + that doesn't appear in the v4.3.0 baseline for the same operator's + traffic shape.** + +### 5. `#320` env-var stays default-off + +- `CACHE_FIX_USAGE_LOG_EXTENDED` is not the beta gate. Flipping it on + the dogfood host mid-soak is a separate opt-in probe (needs meter + v0.9.1+ installed), not the soak criterion itself. + +## What a tripped criterion means + +Hold the promote. Investigate. If the investigation resolves the cause +without needing a code change, document the resolution in this file and +restart the soak clock. If it needs a code change, cut a `beta.1`, restart +the soak clock from that publish. + +## Waivers + +**#272 `insertion-normalization` carries `needs-sim-validation`.** +Applied 2026-07-31, never removed. The fork-context case cannot be +sim'd locally — the beta soak IS its sim validation. Waiver landed as a +comment on [#272](https://github.com/cnighswonger/claude-code-cache-fix/pull/272) +before this release publishes; label removed in the same action. + +## What this document is not + +- Not exhaustive. If a criterion missed here surfaces during the soak + and is obvious (a memory leak, a file-descriptor exhaustion), of + course hold the promote and add the criterion here retroactively. +- Not a release-notes document. AITL owns those, separately. +- Not a substitute for `sys_admin`'s judgment on the observed system. + If they see something worrying and none of these criteria named it, + their observation still counts. + +— Proxy Builder (mechanical); AITL (criteria 3 + 4 refinements + waiver +policy); @chris (go / hold gate). diff --git a/docs/reviews/pr-325-round-1-codex.md b/docs/reviews/pr-325-round-1-codex.md new file mode 100644 index 00000000..50f7f3c2 --- /dev/null +++ b/docs/reviews/pr-325-round-1-codex.md @@ -0,0 +1,56 @@ +# Review: PR #325 v4.4.0-beta.0 release artifacts + +Date: 2026-08-07 +Reviewed: PR #325 at `4076938` against `origin/main` `8d6fa93` +Round: 1 +Label applied: `changes-requested` + +## What Is Correct + +The release PR is correctly scoped to release artifacts: `CHANGELOG.md`, `README.md`, `package.json`, and `docs/releases/v4.4.0-beta-promote-criteria.md`. There are no source-code changes in the PR diff. + +The semver bump is valid for a prerelease: `4.3.0` -> `4.4.0-beta.0`. I found no tracked lockfile, and the new version string is consistent across `package.json`, the changelog heading, and the promote-criteria title. Remaining `4.3.0` strings outside the release note are historical directive/docs references, not stale package version constants. + +Spot checks on `deferred-tool-rewrite` and `usage-log` match the merged code. `proxy/extensions/deferred-tool-rewrite.mjs` is runtime-gated by `CACHE_FIX_TOOL_REWRITE=1`, holds known `tools[]` entries byte-stable, suppresses tool-addition announcements for unsupported models, and only uses the beta path for allowlisted/overridden models. `proxy/extensions/usage-log.mjs` emits `ttl_tier` and `duration_ms` only when `CACHE_FIX_USAGE_LOG_EXTENDED === "on"`, with schema guards and default-off behavior intact. + +Contributor credit is mostly aligned with the release range. The new README entries cover the human authors/contributors I saw in `git log v4.3.0..origin/main`, and the markdown uses `@VictorSun92`, not the distinct `@Victor-Sun` account. + +The promote criteria are mostly falsifiable. Criterion 2 explicitly avoids treating a silent `output-guard` as proof by requiring one deliberate synthetic fire before the zero-fire window. Criterion 1 similarly uses `bytesTransferred` monotonicity to distinguish uptime from a silent respawn. Those clauses succeed at making a non-firing instrument observable. + +## Blockers + +1. `CHANGELOG.md:35` describes `output-guard` as a response-path/upstream-response guard, but the implementation is a request-body guard before forwarding upstream. The changelog says it asks whether "what we are about to send" is still "the response the upstream sent", validates the "outbound response", and restores the original on violation. The merged code does not inspect upstream responses at all: `proxy/extensions/output-guard-stash.mjs:20` to `proxy/extensions/output-guard-stash.mjs:25` stashes the pre-mutation request body on `onRequest`, and `proxy/extensions/output-guard.mjs:147` to `proxy/extensions/output-guard.mjs:178` validates `ctx.body` on `onRequest` and restores the original client body. This is a material release-note accuracy bug because operators will believe response corruption is covered when the shipped guard only covers request-body mutations before Anthropic receives them. + +2. `CHANGELOG.md:35` also claims `output-guard` had "first 243 live firings", while `CHANGELOG.md:25` says none of the beta features has executed against live traffic anywhere during the development window. Given the commissioning context that the dogfood host stayed on v4.3.0 and no beta feature has live-traffic evidence, the 243-live-firings sentence is inconsistent with the release's own beta warning and should be removed or rewritten to a true pre-release/test/prototype provenance claim. + +3. The changelog omits a user-visible fix merged after v4.3.0: `0ed2ab5` / PR #257, `fix(launcher): exclude localhost from proxy in --remote-control`. That commit fixes a v4.3.0 `--remote-control` regression where local HTTP/SSE MCP servers on `127.0.0.1` were routed through the cache-fix proxy and 404ed. This is release-note material for users of the v4.3.0 headline feature and should appear in the v4.4.0-beta.0 section or be explicitly accounted for if intentionally excluded. + +## What Needs Attention + +The `deferred-tool-rewrite` changelog paragraph is directionally correct but compressed enough to over-read. The implementation does not always "announce mid-session additions through the beta channel"; unsupported models intentionally pass through the changed `tools[]` and pay the cache bust. The top-level "no new env vars means no wire change" sentence and the merged code's runtime gate make this non-blocking, but the bullet would be more precise if it named the allowlist/suppression behavior. + +The changelog leaves detailed entries under `[Unreleased]` while also saying this release promotes them. That may be intentional for this PR's editorial shape, but it is unusual release hygiene: after cutting a release, readers generally expect `[Unreleased]` to contain only future changes. + +## Bloat / Non-Functional + +None. + +## Recommendations + +Rewrite the `output-guard` bullet to say request path / outgoing request body / original client body, not response path / upstream response. Delete or qualify the "243 live firings" claim unless there is a true non-dogfood provenance that can coexist with the beta warning. + +Add PR #257 to the `Fixed` section. Consider also deciding whether the pricing refresh in PR #259 needs a short mention because it affects `session-budget-breaker`'s dollar ceiling by adding current model prices, including models previously priced at zero. + +## Verification + +Inspected `gh pr diff 325 --repo cnighswonger/claude-code-cache-fix`. + +Compared `git log v4.3.0..origin/main` against the changelog's PR list and contributor additions. + +Ran targeted tests from an extracted archive of PR head `4076938`: `node --test test/deferred-tool-rewrite.test.mjs test/output-guard.test.mjs test/proxy-usage-log.test.mjs` passed 118/118. + +## Bottom Line + +Request changes. The release shape is close, but the `output-guard` release note currently describes the wrong traffic direction and claims live firings that contradict the beta evidence statement. The omitted `--remote-control` localhost fix also leaves a v4.3.0 regression fix out of the release notes. + +— Codex, cross-LLM review, round 1 diff --git a/docs/reviews/pr-325-round-2-codex.md b/docs/reviews/pr-325-round-2-codex.md new file mode 100644 index 00000000..e2365ed5 --- /dev/null +++ b/docs/reviews/pr-325-round-2-codex.md @@ -0,0 +1,54 @@ +# Review: PR #325 v4.4.0-beta.0 release artifacts + +Date: 2026-08-07 +Reviewed: PR #325 at `d9bf822` against `origin/main` `8d6fa93` +Round: 2 +Label applied: `changes-requested` + +## What Is Correct + +The PR remains release-artifact scoped. The current head adds review artifacts after the release-note fixes; I found no source-code change in this PR. + +Round-1 blocker 1 is resolved on traffic direction and restore semantics. `CHANGELOG.md:35` now describes `output-guard` as an outgoing-request guard behind `CACHE_FIX_OUTPUT_GUARD=1`, default off, restoring the pre-mutation body Claude Code originally sent. That matches `proxy/extensions/output-guard-stash.mjs:20` through `proxy/extensions/output-guard-stash.mjs:25`, which clones the request body before mutators, and `proxy/extensions/output-guard.mjs:147` through `proxy/extensions/output-guard.mjs:189`, which validates on `onRequest`, restores the stash when available, passes through on validator crash, and passes through loudly when the stash is missing. + +Round-1 blocker 2 is resolved. The "first 243 live firings" claim is gone from the PR diff, and I found no equivalent replacement provenance claim. The changelog still deliberately states that the dogfood host ran v4.3.0 for the development window and that none of the beta features executed against live traffic; I did not treat that absence of production evidence as a defect. + +Round-1 blocker 3 is resolved. The new PR #257 entry in `CHANGELOG.md:55` matches `bin/claude-via-proxy.mjs:609` through `bin/claude-via-proxy.mjs:625`: forward-proxy mode sets `HTTPS_PROXY`, merges `127.0.0.1,localhost,::1` into an existing `NO_PROXY` or `no_proxy`, and writes both cases so clients reading either variable bypass the cache-fix proxy for localhost. + +The promoted PR #259 entry is materially accurate. `tools/rates.json:90` through `tools/rates.json:138` adds pricing for `claude-fable-5`, `claude-mythos-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-sonnet-5`, `claude-haiku-4-5`, and `claude-opus-5`; `tools/update-rates.mjs:44` through `tools/update-rates.mjs:85` maps those models and requires the live-traffic set. The fetcher fails closed on missing required models, ambiguous/effective-date uncertainty, sane-band violations, and cache-multiplier contradictions at `tools/update-rates.mjs:18` through `tools/update-rates.mjs:25`, `tools/update-rates.mjs:194` through `tools/update-rates.mjs:209`, and `tools/update-rates.mjs:276` through `tools/update-rates.mjs:317`. + +The fourth item is fixed. `docs/releases/v4.4.0-beta-promote-criteria.md:44` through `docs/releases/v4.4.0-beta-promote-criteria.md:47` now says the guard catches a broken outgoing request body produced by our mutating extensions and forwards Claude Code's original bytes. That matches the implementation. + +The two deliberately unaddressed non-blocking items can remain non-blocking. I still think the `deferred-tool-rewrite` entry could be more precise about unsupported models passing changed `tools[]` through, but the cost of adding that caveat to a release-note paragraph is plausibly higher than the precision gained. I also do not object to leaving the CA-trust entries under `[Unreleased]` until the final v4.4.0 tag commit, given the beta-cut workflow stated in the PR discussion. + +## Blockers + +1. `CHANGELOG.md:35` still overstates the `output-guard` validators by saying the guard validates "role alternation." The five intended validators do exist in `findViolation`: tool adjacency, marker budget, roles, content presence, and assistant-terminal (`proxy/extensions/output-guard.mjs:40`, `proxy/extensions/output-guard.mjs:59`, `proxy/extensions/output-guard.mjs:67`, `proxy/extensions/output-guard.mjs:80`, `proxy/extensions/output-guard.mjs:105`, `proxy/extensions/output-guard.mjs:116` through `proxy/extensions/output-guard.mjs:124`). But the implementation's `checkRoles` rejects invalid roles and a system message at `messages[0]`; it explicitly permits mid-conversation `system` messages and does not enforce strict user/assistant alternation (`proxy/extensions/output-guard.mjs:64` through `proxy/extensions/output-guard.mjs:78`). The changelog should say `roles` or `role validity / placement`, not `role alternation`. + +## What Needs Attention + +None beyond the blocker above. + +## Bloat / Non-Functional + +None. + +## Recommendations + +Change the `output-guard` bullet's invariant list from "role alternation" to "roles" or "role validity / placement." That would align the release note with the implementation without changing the broader paragraph. + +## Verification + +Inspected `gh pr diff 325 --repo cnighswonger/claude-code-cache-fix`. + +Ran targeted tests: + +`node --test test/output-guard.test.mjs test/tools-update-rates.test.mjs test/proxy-session-budget-breaker.test.mjs` passed 68/68. + +`node --test --test-name-pattern="--remote-control (excludes localhost|merges localhost|honors lowercase no_proxy|does not duplicate)" test/proxy-wrapper.test.mjs` passed 3/3 after installing npm dependencies with a writable `/tmp` npm cache. + +## Bottom Line + +Request changes. The requested round-2 fixes are correct except for one narrow documentation accuracy issue: the release note still says `output-guard` validates role alternation, while the implementation validates roles / placement. + +— Codex, cross-LLM review, round 2 diff --git a/docs/reviews/pr-325-round-3-codex.md b/docs/reviews/pr-325-round-3-codex.md new file mode 100644 index 00000000..43c9412a --- /dev/null +++ b/docs/reviews/pr-325-round-3-codex.md @@ -0,0 +1,66 @@ +# Review: PR #325 v4.4.0-beta.0 release artifacts + +Date: 2026-08-07 +Reviewed: PR #325 at `2ddf692` +Round: 3 +Label applied: `approved-by-codex-agent`, `reviewed-by-codex-agent` + +## What Is Correct + +The round-2 blocker is resolved. `CHANGELOG.md:35` now says the +`output-guard` invariant list includes "role validity and system-message +placement," not "role alternation." That matches `checkRoles` in +`proxy/extensions/output-guard.mjs:64` through +`proxy/extensions/output-guard.mjs:78`: the validator rejects roles +outside `user`, `assistant`, and `system`; rejects `system` at +`messages[0]`; and explicitly permits mid-conversation `system` messages. +It does not enforce user/assistant alternation, and the release note no +longer claims it does. + +The new criterion-3 caveat in +`docs/releases/v4.4.0-beta-promote-criteria.md:78` through +`docs/releases/v4.4.0-beta-promote-criteria.md:124` does not make the +cache-hit-rate criterion unfalsifiable. It names a specific exclusion +signature: a `usage.jsonl` gap immediately before the spike, longer than +the applicable TTL tier. It also preserves a falsifiable hold condition: +if the gap is absent, or if elevated `cache_creation` persists after hit +rate recovers, the promote remains held. + +The caveat is appropriately aimed at a false positive where magnitude is +misleading. A TTL-expiring quota block can produce exactly the scary +numbers criterion 3 was written to catch, while involving no beta code at +all. The inserted text distinguishes that case by observable timing and +recovery behavior rather than by operator discretion. + +## Blockers + +None. + +## What Needs Attention + +None. + +## Bloat / Non-Functional + +None. + +## Recommendations + +None. + +## Verification + +Inspected `gh pr diff 325 --repo cnighswonger/claude-code-cache-fix` and +the narrow diff `8de01d7..2ddf692`. + +Ran targeted tests: + +`node --test test/output-guard.test.mjs` passed 15/15. + +## Bottom Line + +Approve. The remaining round-2 blocker is closed, and the new quota-gap +exclusion remains checkable rather than becoming an escape hatch for a +real sustained cache regression. + +— Codex, cross-LLM review, round 3 diff --git a/package.json b/package.json index e4b9a452..bf367fb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-code-cache-fix", - "version": "4.3.0", + "version": "4.4.0-beta.0", "description": "Cache optimization proxy and interceptor for Claude Code. Fixes prompt cache bugs, stabilizes prefix, reduces quota burn.", "type": "module", "exports": {