* fix(explore): claim complete source only for sections that are complete On the tiers with a completeness signal (>= 500 indexed files) every codegraph_explore response ended with "Complete source for N files is included above — do NOT re-read them", whatever the render had cut. A windowed spine method, a shrunk or dropped cluster and the per-symbol view all went out under that line; the oversize-spine window never even set the trim flag the small tiers key off, which is how a 62-line slice of vscode's 968-line rpcProtocol.ts was called complete. The line also said "Reserve Read for a single specific line range", which explore output must never say. Completeness is now measured at emission from the ranges actually sent (plus back-referenced spans) against the symbols each section set out to deliver. A trimmed response keeps the verbatim / already-Read guarantee, names the trimmed files and the most relevant elided symbols, and steers to another codegraph_explore. The note is offered from most to least specific and fitted to the leftover room (CG-26); a trimmed note yields the pointer list's first entry. The lost-pointer, cut and truncation notes drop "complete" the same way, at no greater length, so the epilogue floor is unchanged. The small tiers' trimmed note now also fires for the cuts the flag missed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(explore): offer an elided method as Owner.member in the completeness note The note offered `as_sql` for django's elided SQLCompiler.as_sql — one of 110 definitions — so agents appended a path and line to disambiguate it, and that query shape returns none of the body. `SQLCompiler.as_sql` alone returns most of it. Methods are now offered as `Owner.member` from the indexed qualified name; everything else keeps its bare name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(explore): fit the completeness note and pointer list in one tested function A trimmed note's optional detail (the files, the elided names) never costs a pointer entry the least specific note would have left. The fit moves out of handleExplore into fitExploreEpilogue so both rules are pinned by tests; the second-try branch it had could never fire and is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(explore): completeness note wording after #2068 flags the spine window Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(explore): record the completeness-note replay and agent A/B Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(explore): address review of the completeness note - Judge completeness AFTER the dedup fold: a remainder too small to fence is sent in neither call, so a section with one is trimmed, not complete. The whole-file render now declares its symbols too (clamped to the lines it prints; the file node is not one), so a fold there registers. - Label trimmed files unique among every path the response can name, the pointer list included. - "1 file", and no "0 files" when every section was held from an earlier call. - The lost-pointer, cut and truncation notes are one exported table, EXPLORE_FALLBACK_NOTES, with the length rule tested; the lost note picks its wording from trimmedShown instead of re-joining the response. - The design doc's follow-up list marks the overclaim fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
7.6 KiB
The completeness note claims only what was sent
Date: 2026-09-28 · New: claude/nostalgic-mccarthy-31c89d (measured at 024d88b9, then
rebased onto #2071) · Baseline: main @ 724b5dee (#2068) · Harness: scripts/agent-eval/ab-new-vs-baseline.sh, --model sonnet --effort high, both arms codegraph-on, CLI blocked, CODEGRAPH_NO_PROMPT_HOOK=1, every
index rebuilt per arm.
The defect
On the tiers with includeCompletenessSignal (>= 500 indexed files), every codegraph_explore
response ended with the same line:
Complete source for N files is included above — do NOT re-read them. … Reserve Read for a single specific line range explore can't surface.
It was printed whatever the render had cut. The small tiers switch to a "trimmed for size" note on
anyFileTrimmed, but that flag is set at individual trim sites, and before #2068 one of them, the
oversize-spine window, set nothing. That is how a 62-line slice of vscode's 968-line
rpcProtocol.ts went out as complete. Even with every site flagged, the large tiers never read the
flag. The last sentence also offered Read, which explore output must never do.
The change
-
Completeness is measured, not flagged. Each file section passes the symbols it set out to deliver (the cluster members, or the per-symbol view's symbols) to
emitFileSection.elidedWantedSpanschecks them against the ranges actually sent, plus back-referenced ones. A span not fully covered marks the file trimmed. Any path that elides source is caught this way, including paths added later. -
The note says what is true. When no section was trimmed it is the old line, without the Read sentence. Otherwise it keeps the guarantee that still holds and names what was cut:
Verbatim source for 2 files is included above — treat it as already Read. Trimmed for size:
rpcProtocol.ts; gap markers and file headers name what was elided (e.g.RPCProtocol._receiveOneMessage,MessageIO.serializeReplyOK, …). For those, or anything under "Not shown above", make ANOTHER codegraph_explore with those exact names instead of reading the files — it returns their source with line numbers.Elided methods are offered as
Owner.member(django has 110as_sqls). Containers are never offered, because a class too big for one section comes back trimmed on a follow-up too. -
Fitted like the rest of the epilogue (CG-26), in
fitExploreEpilogue. The note comes in three sizes: with names, with files, and generic. The generic one (≈340 chars) is shorter than the line it replaced (401). A complete-source note keeps its old precedence. A trimmed note yields to the pointer list, which names files the response does not show at all, while a trimmed section already names its elisions in its own gap markers. It leaves the list's header and first entry, and its optional detail never costs an entry the generic note would have left. -
The fallback notes follow. The lost-pointer note, the epilogue-cut note and the truncation note drop "complete" when a section they vouch for was trimmed. Each trimmed variant is no longer than the original, so the epilogue floor is unchanged.
Deterministic replay
39 agent-shaped queries (the vscode and django queries from the named-concentration A/B, the
README questions, and pinned / stress queries), replayed through both builds on one index with a
fresh ToolHandler per query.
main @ 724b5dee |
new | |
|---|---|---|
| source sections byte-identical | — | 39 / 39 (only the epilogue changed) |
| responses claiming "Complete source" | 24 | 1 |
| … of which a section was actually trimmed | 23 | 0 |
| responses with the trimmed note | 0 | 16 (1 naming elided symbols) |
| responses with a pointer list | 23 | 33 |
| pointer entries, total | 71 | 102 (no list shorter than baseline) |
| responses offering Read | 24 | 0 |
| responses over the 25K inline cap | 0 | 0 |
The named variant is rare because the fit spends leftover room on pointer entries first. In saturated responses the names cost an entry, and the gap markers already carry them.
Re-run after rebasing onto 290e03f7 (#2071, same-named file pinning), against that build: source
identical 39 / 39, "Complete source" 23 → 1, trimmed note 16, pointer entries 67 → 93 (none
shorter), Read offered 23 → 0, nothing over the cap.
Agent A/B
Four questions, three runs per arm, against 724b5dee.
| question | Read runs (new / base) | Grep/Glob | explore calls (new / base) | duration median (s) |
|---|---|---|---|---|
| vscode: rpcProtocol serialize + dispatch | 0 / 0 | 0 / 0 | 7 / 6 | 45 / 36 |
| vscode: extension host ↔ main process | 0 / 0 | 0 / 0 | 12 / 12 | 61 / 58 |
django: SQLCompiler.as_sql / pre_sql_setup / get_select |
0 / 0 | 0 / 0 | 6 / 6 | 31 / 32 |
| django: QuerySet → SQL | 0 / 0 | 0 / 0 | 4 / 5 | 22 / 23 |
| pooled, 12 runs | 0 / 0 | 0 / 0 | 29 / 29 | 36 / 36 |
The change was exercised. Of the 29 explore responses the new arm's agents received, 20 carried the trimmed note (4 naming elided methods). The baseline agents received "Complete source … Reserve Read" on 26 of 29, and on this build all but one of those are false. One pre-explore Bash call in the new arm happens before any codegraph output and is noise. Load average was 30–95 from other work on the machine, so durations are indicative only.
Earlier round, against e63fe2ec
Before #2062 / #2063 / #2068 landed, the same four questions (the django pair run twice, 18 runs
per arm) showed Read in 8 new runs vs 7 baseline runs, Grep/Glob 0 / 0, explore 42 / 49. Every
django Read targeted SQLCompiler.as_sql, which no build returned for the query shape the agents
wrote (SQLCompiler.as_sql … compiler.py:776). Naming the file pinned it and dropped the method:
175 of 228 lines for the bare name, 0 with the path. #2063 fixed that, and on current main no run
in either arm reads.
Coverage
__tests__/explore-completeness-note.test.ts:
elidedWantedSpans: partial and absent spans, back-referenced and adjacent ranges joining, ordering, and invalid spans.shortestUniqueSuffixes.exploreCompletenessNotes: the complete case never offers Read; the trimmed case keeps the already-Read guarantee, names files andOwner.membernames, and skips containers; candidates shrink in order, and the last is shorter than the old line; "1 file", never "0 files"; a trimmed file's label stays distinct from a same-named file in the pointer list.EXPLORE_FALLBACK_NOTES: each trimmed wording drops "complete", keeps the already-Read guarantee, and is no longer than the wording the floor and the cut test were sized on.fitExploreEpilogue:- a complete note keeps precedence;
- a trimmed note leaves the first pointer entry;
- detail never costs an entry;
- with no pointer list, the most specific note that fits wins.
- End to end on a three-hop flow whose spine method the render windows: the large tier reports it trimmed, the small tier agrees, and complete flows are still called complete.
- End to end with dedup on: a second call whose new lines are too few to fence, and are folded into the "Already sent" pointer, is reported trimmed rather than complete.
Mutation-checked:
| Mutation | Red |
|---|---|
| Derived detection removed | the large-tier windowed-spine test |
| Old Read sentence restored | both Read checks |
| The entry rule dropped from the fit | "detail never costs a pointer entry" |
| The first-entry reservation dropped | "a trimmed note leaves the pointer list its first entry" |
| Completeness judged before the dedup fold | the folded-remainder test |