Conversation
… actually does Closes audit C3 and H1, and the partial L2. Two findings, one decision, because they are the same question: what is Gateway mode for? C3 — `exec` returned 401 to its own child. `runExecCommand` generated a per-run token and injected it as `TOKENDAMPER_GATEWAY_TOKEN`, a variable nothing in `src/` read and that no third-party client has heard of. The child is `aider`, `claude`, `codex` or `curl`; it sends `authorization` or `x-api-key` and nothing else. Reproduced by spawning a real child through `runExecCommand`: every request came back 401, and `exec` exited 0. The suite passed throughout, because its gateway test presented the header no real client sends — which is why the new test spawns a child instead. Loopback peers are now trusted. The server binds to 127.0.0.1, so a loopback peer was already the only peer that could connect, and the token was protecting one local process from another on the same machine — a real but narrow boundary that was being paid for with a mode nobody could use. Determined from `req.socket.remoteAddress`, never from a header, since `X-Forwarded-For` is attacker-supplied; includes `::1` and the IPv4-mapped `::ffff:127.0.0.1` form Node reports on a dual-stack listener. The token is still enforced on any non-loopback bind, where the boundary is not narrow. `HTTP_PROXY` and `HTTPS_PROXY` are no longer set. `GatewayServer` implements neither HTTP proxy semantics (absolute-form request URIs) nor the `connect` event `CONNECT` requires, so any child honouring them would have failed to reach the provider at all — a second failure, independent of the 401 and masked by it. Setting a proxy variable for a server that is not a proxy is worse than setting nothing. Base-URL interception is now the only supported mechanism. Partial L2: `?token=` query authentication is removed — a credential in a query string lands in access logs, shell history and any error that echoes the URL — and the header comparison is constant-time. H1 — the Gateway saves nothing across turns, and that is correct. Measured over real sockets on realistic two-turn conversations, where a resent history contains each block exactly once: 0 bytes saved, fallback on every turn, for code, prose and JSON tool results alike. (An earlier attempt at this measurement duplicated the block inside turn 2 and appeared to save; that is the within-payload case, not the cross-turn one.) Not a bug. `cleanup:session-dedup` marks an elision recoverable only when an intact copy survives elsewhere in the same payload (§16); a sole copy seen only in a previous turn is scored in full and refused, correctly, because the consumer is a stateless provider API with no rehydration mechanism and such a marker is deletion rather than reference. No cross-turn transform is available without provider-side resolvability, which does not exist. So the mode is documented as experimental and the saving claim is withdrawn. What it does deliver — transparent interception, the full validation pipeline, byte-faithful forwarding, metrics, within-payload dedup — is a coherent product; "Cross-turn Session Deduplication" was not. README, ARCHITECTURE.md and CLAUDE.md invariant 8 updated. The measurement is pinned by `test/integration/gateway-dedup-reality.test.ts` rather than left as prose. If a cross-turn saving ever appears, that is the signal to read: either resolvability was implemented, in which case update the test deliberately, or the drift gate was relaxed and the Gateway is deleting content the model cannot recover, in which case do not. Also resolves the rest of the audit's M4 list rather than deferring it: knapsack planning marked implemented-but-unreachable (H5), token hashing qualified as irreversible on the CLI by design, `TOKENDAMPER_RISK_TOLERANCE` marked as having no effect on optimization (H4, still open), `TOKENDAMPER_GATEWAY_TOKEN` described accurately. One existing test asserted the old 401 and was updated with its finding preserved: it passed only because it sent a header real clients do not. New tests verified to fail 3/6 against the unfixed gateway. See DECISIONS §41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes audit H6, the largest single cause of real code not being optimized —
`CONSTRAINT_DIRECTIVE_LOST` accounted for 24 of 40 fallbacks on the audit's
corpus.
The nine-keyword scan (`must`, `must not`, `never`, `always`, `only if`,
`do not`, `required`, `except when`, `make sure to`, `critical`) is written for
natural-language system prompts, and was applied to raw content of every kind. In
source, `required` and `critical` are ordinary identifiers.
Measured over a frozen 293-file corpus at `targetReductionRatio: 0.3`,
classifying every directive a run reported as dropped by where it came from:
Python 16 from comments/docstrings, 38 from code — nearly all
`logger.critical(...)`
TypeScript 38 from comments/docstrings, 13 from code — `readonly required?`,
error-message literals
That rules out both extremes. Trusting the check everywhere keeps 51 false
positives. The audit's proposed remedy — scope extraction by content type, not
`code` — discards 54 genuine constraints, and specifically the Python docstring
case that `docs/phase-1d-semantic-gate-disposition.md` measured to be the only
thing this check actually catches. What separates the two populations is not the
content type but the region: an instruction to a reader lives in a comment or a
docstring, never in an expression.
So extraction is scoped to prose regions — line comments, block comment bodies
and Python docstrings including their interior lines, plus whole content for
prose content types. Deliberately line-oriented and syntax-approximate rather
than lexed: this is a filter on what may *raise* a constraint, so over-inclusion
costs a false positive (the pre-existing behaviour) and under-inclusion costs a
missed constraint. Requiring the comment leader at the start of a trimmed line is
what excludes `logger.critical(exc)` while keeping `# never call this twice`.
Retention is now checked per item. The check collected every item's directives
into one list and tested each against the joined content of every item, so a
directive from item A was satisfied if the string happened to appear anywhere in
item B — the check could pass for content that was in fact destroyed — and a loss
anywhere failed the whole run with no way to say where. Matching by item id fixes
both and the message names the item. An item absent from `after` is skipped, on
the same reasoning `DriftTracker.findUnwitnessedItems` records: selection is not
elision, and failing here would make any prunable item carrying an imperative
unprunable.
Measured:
python file 14.98% -> 23.14% (+8.16pp)
python stdin 14.88% -> 22.66% (+7.78pp)
typescript file 23.38% -> 27.33% (+3.95pp)
20 rows changed of 586, and none regressed — no file went from reducing to
falling back. Every other bucket is byte-identical.
TypeScript now has zero remaining code-sourced directives; every surviving
`CONSTRAINT_DIRECTIVE_LOST` is a genuine imperative in a comment or docstring
that an elision would drop. Those files still fall back, and should. That is why
the category does not go to zero.
Ordering note: this had to land after §37 (C1a). The audit observed that this
check was "currently the only thing preventing markdown documents from being
deleted" — a document survived if its author happened to use one of nine words.
Narrowing it first would have widened that data loss. With §37 in place the drift
measurement gate covers markdown on its own merits, which the corpus confirms:
the prose bucket is unchanged at 28/28 fallbacks, now attributed to drift rather
than to a coincidence of vocabulary.
New tests verified to fail 9/11 against the unfixed code; the 2 that pass are the
ones pinning behaviour H6 must preserve.
See DECISIONS §42.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the ingestion half of audit H5. `optimize` now accepts multiple paths and
directories.
The architectural centerpiece could not run. `createContextBundle` produces
exactly one item, `applyCacheAwarePrefixLocking` pins everything inside the first
1,024 tokens, and `solve01Knapsack` places pinned items outside the candidate set
and always selects them — so item 0 was always pinned, `itemsPruned` was always
0, and the knapsack solver, cache-aware prefix locking, topology scoring, the
dependency graph and the git inspector could not affect any output the product
was able to produce.
Measured on `src/core` at `maxInputTokens: 4000`: 31 items, 15 pruned, 20,540
tokens saved by the planner.
`test/unit/fallback-render.test.ts` pinned the success path's `items.join('\n')`
as a latent defect and said whoever made an `emittedOutput` consumer multi-item
"should stop and read it". This is that change, so it is fixed rather than
inherited. One item renders as its content and nothing else — CLI, MCP and bench
stay byte-identical. More than one renders with a `==> path <==` header, which is
`head`/`tail`'s convention rather than a format invented here. It is not
collision-proof and nothing escapes it: the consumer is a model being given
context, legibility beats round-trip parsing, and anything needing to
machine-parse should read `finalBundle` from the trace. Fail-open is per file —
each file's original bytes, never a re-encoding — so §35 holds per item; the
stream as a whole is not byte-identical, because the headers are TokenDamper's.
Three defects this exposed, fixed here:
1. Pruning was scored as drift. `findUnwitnessedItems` had always exempted an
item absent from `after` — selection is not elision — but the ratios compared
whole bundles, so a pruned item's symbols vanished and `R_AST` read the planner
doing its job as semantic loss. The ratios now score retained items only,
guarded on ids actually corresponding: `id` is content-derived at construction
and preserved by the transforms, so a caller rebuilding its `after` bundle
independently would otherwise leave nothing to compare and report `S_k = 0` for
a gutted bundle. With no correspondence the whole bundle is compared, as
before — failing open to more measurement rather than less.
2. Whole-item elision of a symbol-bearing item is no longer attempted. Since §40,
`S_k = 1 - R_AST` for code, so destroying every symbol scores 1.0 against a
gate firing above 0.40; no threshold or flag lets it through. On a one-item
bundle that was invisible — the run fell back and emitted the input, which is
what skipping produces anyway. On a multi-item bundle two pure-`types.ts` files
were taking a 16-file batch down with them. Symbol-free items are unaffected.
3. `TD_PRESERVE:` matched its own implementation. `drift-tracker.ts` and
`cli/html-reporter.ts` each acquired a content marker they do not semantically
have; because `R_struct` is a bundle-scoped set, one phantom marker being
elided drove it to 0 and took a 16-file batch to `S_k = 0.4053` on a run whose
real symbol retention was 99.1%. Scoped to prose regions for `code` only — not
the prose types generally, because `TD_PRESERVE:` is an unambiguous token, and
the only way it appears without being a directive is as a literal inside an
expression. This also retires the `html-reporter.ts` regression §40 recorded as
left in deliberately.
Also: envelope headers were counted on the output side only, so a multi-item
fallback reported 72,973 -> 73,667 tokens — a negative reduction, the same shape
as the phantom -1.39% already diagnosed once in the Python bench harness.
Corpus, 586 rows: 1 row changed, 0 regressions. TypeScript 27.33% -> 29.55%.
Fallback counts drop sharply (prose 28 -> 9, TS stdin 57 -> 0) because doomed
elisions are no longer transformed-then-reverted; same bytes, less wasted work.
Not done, and now the binding constraint: multi-file runs still fall back on real
corpora. On the 45-file Python corpus drift is 0.0359 and AST is clean, but 26
constraint failures across 14 items revert all 45 — validation is bundle-scoped
and fallback is all-or-nothing (audit §3.1, Phase 1c, unstarted). This delivers
the mechanism; §3.1 stands between it and the outcome. Phase 1c's missing
prerequisite — attribution — now exists for constraint (§42), unwitnessed (§37)
and AST (`itemId`) failures.
Four pinned tests asserted behaviour this changes and were updated with their
findings preserved.
See DECISIONS §43.
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.
No description provided.