feat(output-guard): last-line invariant on the response path — restore, log, never corrupt - #278
Open
Gunther-Schulz wants to merge 5 commits into
Open
feat(output-guard): last-line invariant on the response path — restore, log, never corrupt#278Gunther-Schulz wants to merge 5 commits into
Gunther-Schulz wants to merge 5 commits into
Conversation
…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>
…e, log, never corrupt A pipeline of body-mutating extensions needs one check that runs LAST and answers a different question: is what we are about to send still the response the upstream sent, in every way the client depends on? The guard validates structural invariants (tool_use/tool_result adjacency among them) on the outbound response; on violation it RESTORES the original stashed by output-guard-stash (order 55, before any mutator) and logs the diff — a mutation bug becomes one logged line instead of a corrupted session. Its first 243 live firings were its own missing exemption, which is the point: a guard that can restore makes that class of mistake cheap to discover and free to survive. Gated: CACHE_FIX_OUTPUT_GUARD=1. Stacked on pr/insertion-normalization (imports validateToolAdjacency). The replay-classes fixtures are shared with pr/verification-tools — identical files, merge cleanly in either order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 29, 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>
… + 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
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
… fork da9bf8c 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
Gunther-Schulz
added a commit
to Gunther-Schulz/claude-code-cache-fix
that referenced
this pull request
Jul 30, 2026
…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>
Contributor
Author
|
Pushed wave 2 for this slice — three commits. ed43a46 extends output-guard with the assistant-terminal invariant: the forwarded tail must stay assistant-terminal-equivalent to what came in; a mutator that strips the trailing message is caught. Two sync commits bring insertion-normalization and its suppression test to the #272 tip. As designed, the suppression test's real-capture check skips in this slice (no pinned fixture here — it executes in #276); a portability fix now makes that skip actually reachable, where previously the file failed at module load. Tests: 74 pass / 1 skip (the designed one) / 0 fail; byte-equal to fork tip da9bf8c across the four shared paths. 🤖 Generated with Claude Code |
Gunther-Schulz
added a commit
to Gunther-Schulz/claude-code-cache-fix
that referenced
this pull request
Aug 1, 2026
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Once a proxy runs body-mutating extensions (#272/#273, or any future one), the failure mode that matters most isn't a missed optimization — it's shipping a corrupted response. This guard is the last line: it runs after every mutator and asks a different question than any unit test can — is what we're about to send still the response upstream sent, in every way the client depends on?
What
output-guard-stash(order 55) stashes the pristine body before any mutator runs.output-guard(order 690) validates structural invariants on the outbound response (tool_use/tool_result adjacency among them). On violation it restores the stashed original and logs the diff — a mutation bug becomes one logged line instead of a broken session.CACHE_FIX_OUTPUT_GUARD=1.Evidence
10 tests, including gate 1: the guard fires zero times across every class-matrix corpus (a guard that fires on legitimate traffic trains its reader to ignore it — that's a failing guard, and its first 243 live firings on our fleet were exactly that: its own missing exemption, since fixed and pinned). Running in production alongside #272/#273 for two days, 0 safety violations across ~2.5 GB of daily replay.
🤖 Generated with Claude Code