Skip to content

feat(output-guard): last-line invariant on the response path — restore, log, never corrupt - #278

Open
Gunther-Schulz wants to merge 5 commits into
cnighswonger:mainfrom
Gunther-Schulz:pr/output-guard
Open

feat(output-guard): last-line invariant on the response path — restore, log, never corrupt#278
Gunther-Schulz wants to merge 5 commits into
cnighswonger:mainfrom
Gunther-Schulz:pr/output-guard

Conversation

@Gunther-Schulz

Copy link
Copy Markdown
Contributor

Stacked on #272 (imports validateToolAdjacency). The test/fixtures/replay-classes/ files are shared with #276 — identical content, merges cleanly in either order.

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.
  • Gated off by default: 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

Gunther-Schulz and others added 2 commits July 29, 2026 11:50
…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>
Gunther-Schulz and others added 2 commits July 30, 2026 14:25
…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>
@Gunther-Schulz

Copy link
Copy Markdown
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

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
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant