Date: 2026-08-05 · New: feature/CG-2 @ 7a7ea30 (CG-17 session state + CG-18 dedup)
· Baseline: c65d56c by SHA (main's tip when the epic branched) · Harness:
scripts/agent-eval/ab-new-vs-baseline.sh, --model sonnet --effort high, both arms
codegraph-on, CODEGRAPH_NO_PROMPT_HOOK=1 on both.
This is the epic's hard gate. Returning less on a repeat call is the exact shape CLAUDE.md says drives Read fallback and then teaches the agent to abandon codegraph for the rest of the session, so the gate is about risk first and win second.
Verdict: bars 1–3 pass cleanly and bar 4 is not met. Read is 0 in all 24 runs of both arms, nothing abandons, and the two failure buckets never fire once. Residual context occupancy is flat — and the measurement below shows it could never have been anything else, because CG-18's own acceptance requires reclaimed bytes to be re-spent on files the agent has not seen rather than banked. What the change actually moves is the duplicate fraction of that residual: −87% across the agent runs, −86%/−94% deterministic.
Recommendation: keep, with bar 4 restated. Reasoning and the counter-case in §Verdict.
Dedup only exists inside one MCP session, so a single-call task cannot exercise it at all. Both targets were driven with the drill-down question from the CG-1/CG-22 A/B, which reliably produces a second and third explore whose symbol bags overlap the first:
| Repo | Lang | Files | Tier | Question |
|---|---|---|---|---|
kubernetes/client-go |
Go | 2,454 | medium (2 calls / 28K) | "how does a shared informer keep its cache in sync and deliver events?" |
excalidraw/excalidraw |
TS/React | 672 | medium (2 calls / 28K) | "how does updating an element re-render the canvas on screen?" |
Each prompt is wrapped Use codegraph to answer: <question> — identical on every arm, the CG-22
wrapper. That is not a forced-Read-0: fallback stays free, which is exactly what bar 1
measures.
RUNS=3 per invocation. client-go ran one batch (n=3/arm); excalidraw ran two (n=6/arm,
pooled) because it is where dedup bites hardest and therefore where the abandonment risk is
highest — its baseline sequence duplicates ~21% of the source it serves.
Three, because no single one answers the gate:
feature/CG-3 copy of
parse-run.mjs (that branch is where all three feedback metrics live; this branch's copy
predates them). Bars 2–4.(file, source
line) pairs each call put in the window from the <n>\t<text> fences, and charges a line
already delivered by an earlier call as a duplicate byte.It is deliberately not committed as a new scripts/agent-eval/*.mjs: a new file there
scores into the self-query eval fixture's own corpus and moves its numbers (the CG-15 observer
effect), and the natural home is parse-run.mjs on feature/CG-3 beside the other three
metrics. Fold it in there; the rule above is the whole specification.
Same index, same query sequence (lifted verbatim from a prior new-arm agent run), replayed
through one ToolHandler + one ExploreSessionState on each build. Re-measured on both
builds in this session rather than quoted.
baseline c65d56c |
new 7a7ea30 |
|
|---|---|---|
| response chars | 65,218 | 67,289 (+3.2%) |
| source chars | 46,555 | 47,209 |
| unique source | 44,740 | 46,957 (+5.0%) |
| duplicate source | 1,815 (3.9%) | 252 (0.5%) — −86% |
baseline c65d56c |
new 7a7ea30 |
|
|---|---|---|
| response chars | 72,364 | 68,442 (−5.4%) |
| source chars | 50,063 | 44,578 |
| unique source | 39,575 | 43,973 (+11.1%) |
| duplicate source | 10,488 (20.9%) | 605 (1.4%) — −94% |
The two repos bracket the mechanism. Where the baseline barely duplicates (client-go, 3.9%) there is almost nothing to reclaim, and the reclaimed bytes plus the pointer text make the response marginally larger. Where it duplicates heavily (excalidraw, 20.9%) the response gets smaller and denser at the same time — 5.4% fewer bytes carrying 11.1% more unique source.
The ceiling on any occupancy win is the baseline's duplicate fraction, and that is the whole argument about bar 4: even a design that banked every reclaimed byte instead of re-spending it could not have removed more than 3.9% / 20.9% of the source in these two sequences.
Median of 5 replays of the 3-call sequence, per build (CG-18 adds a truncated SHA256 per served slice, so this needed checking):
| repo | baseline | new |
|---|---|---|
| client-go | 1,230 ms | 1,287 ms (+4.6%) |
| excalidraw | 458 ms | 445 ms (−2.8%) |
≤60 ms across three calls. Nothing here can explain a several-second agent gap — see §Counter-points.
explore = codegraph_explore calls · cgResidual = codegraph chars still resident at end of
run, CG-7 · dup% = share of served source the agent had already been given.
| repo | arm | n | explore | Read | Grep | cgResidual (med) | per call | dur (med) | dup% |
|---|---|---|---|---|---|---|---|---|---|
| client-go | new | 3 | 2 / 3 / 2 | 0 / 0 / 0 | 0 | 19,446 | 9,641 | 31s | 0.7% |
| client-go | baseline | 3 | 3 / 2 / 2 | 0 / 0 / 0 | 0 | 19,611 | 9,379 | 27s | 7.9% |
| excalidraw | new | 6 | 3,3,1,2,3,2 | 0 ×6 | 0 | 25,688 | 10,316 | 31s | 0.7–0.8% |
| excalidraw | baseline | 6 | 2,2,2,1,3,2 | 0 ×6 | 0 | 20,123 | 10,158 | 23s | 3.3–7.6% |
Read = 0 and Grep = 0 in all 24 runs, both arms, both repos. Not "did not increase" — never fired. The strongest form of this bar: back-references actually reached the agent in 8 of the 9 multi-call new-arm runs (28 pointers total), and no run followed a pointer with a Read.
The failure mode is silent, so it was measured three ways, all clean across 24 runs:
isError responses in either arm. (One or two early in a session is what teaches
abandonment; there were none.)The two buckets this epic could plausibly break are "Read a file we returned" (we clipped the wrong thing) and "Read a file we did not return". Over 40 answered explore calls:
| bucket | new | baseline |
|---|---|---|
| Read a file we returned | 0 | 0 |
| Read a file we did not return | 0 | 0 |
| Grep/Glob | 0 | 0 |
| explore again | 12 of 21 (57.1%) | 10 of 19 (52.6%) |
| moved on / answered | 9 | 9 |
The failure buckets are empty on both arms. The explore again difference is one call at
n=21/19 — noise, and CG-1 already established that these are voluntary drill-downs after a
complete answer, not insufficiency retries (which is why CG-19 was cut).
Per-run cgResidual is dominated by how many calls the agent chose to make, and both arms span
1–3 calls on excalidraw. Normalising that out, residual per explore call is flat: +2.8% on
client-go (9,641 vs 9,379), +1.6% on excalidraw (10,316 vs 10,158).
This is by construction, not by accident. CG-18's acceptance says in as many words:
"Freed budget — bytes reclaimed by dedup should flow to files not yet shown, not shrink the
response," and emitFileSection implements exactly that (a fully-held file frees both its
sourceSpent into the carry-forward pool and its maxFiles slot). A design that spends every
reclaimed byte cannot lower the byte count. CG-18's acceptance and CG-20's bar 4 are
mutually unsatisfiable; that contradiction, not a defect in the dedup, is what this bar found.
What the epic does move, measured on the same runs:
| new | baseline | |
|---|---|---|
| duplicate source chars, all agent runs | 2,432 of 329,222 (0.74%) | 19,295 of 300,750 (6.4%) |
−87% duplicated bytes, at flat cost per call, with more unique source in their place.
Kept in the record rather than smoothed:
dedup.savedChars in the CG-4 diagnostic is a pre-clip figure and must not be read as
bytes kept out of the window. On client-go call 2 it reports 11,450 saved while the
baseline actually re-served only 1,042 duplicate chars of that file — the suppressed
ranges are measured against the unclipped candidate render, most of which the budget
allocator would have trimmed anyway. Section-level check on the same call: baseline emits 232
lines of shared_informer.go overlapping call 1 by 22 lines; the new build emits 234 lines
overlapping by 0, and is 544 chars larger. Anyone tuning EXPLORE_DEDUP's thresholds off
savedChars will over-estimate the win by roughly 7×.tools/cache/** already dominates graph relevance, so the pre-dedup
render concentrated well on its own — the same reason CG-22 found only a small #1500 signal
there.| bar | result |
|---|---|
| 1. Read must not increase | PASS — 0 Reads in 24/24 runs, both arms |
| 2. No abandonment | PASS — 0 isError, codegraph last in every run, no call collapse |
| 3. Buckets must not shift to "Read a file we returned" / "another explore" | PASS — both failure buckets empty on both arms |
| 4. Residual occupancy actually drops | NOT MET — flat per call, and unreachable given CG-18's reallocation rule |
The epic's acceptance says revert if bar 4 is not met, because the regression risk isn't worth a marginal win. The regression half of that premise was measured and is zero — 24 runs, no Read, no abandonment, no bucket shift, back-references demonstrably reaching the agent. And bar 4 is unreachable by construction, not unmet by underperformance: the byte ceiling it was aiming at is the baseline's duplicate fraction, 4–21%, and CG-18 was already accepted on the rule that those bytes get spent, not banked.
So: keep the change, and restate the epic's metric as the duplicate fraction of residual (−87% agent, −86%/−94% deterministic) at flat context cost — with excalidraw showing the best case, 5.4% fewer response bytes carrying 11.1% more unique source.
This is a judgement call against the letter of bar 4, and it is cheap to reverse in either direction:
CODEGRAPH_EXPLORE_DEDUP=0 disables dedup without a rebuild;git revert 7a7ea30 ab38d1f 4e94860 fc31b1e removes CG-17 + CG-18 entirely;Logs: /tmp/cg20/ab-client-go, /tmp/cg20/ab-excalidraw, /tmp/cg20/ab-excalidraw-b2
(ephemeral — archive them if a distribution needs to stay reproducible).