`codegraph_explore` never returned `queueMessage` (L1087) or `flushQueuedMessages` (L1102) from a 1,414-line file, on a symbol bag or a prose question, even with that file at rank #1 holding 67% of the envelope — the agent got a same-stem `QueuedMessage` interface at L70 and had to Read the file for the functions it had named. Pre-existing at every build including pre-epic (controlled bisect, index held fixed). Two independent causes: 1. `buildFlowFromNamedSymbols` returns the Flow prose AND the set of node ids the agent named — and the latter is the whole guarantee, since it injects a named def into its file's cluster ranges at importance 9. Its bail-outs returned EMPTY, zeroing the identity whenever there was nothing to PRINT. Two sibling closures that never call each other produce no chain, no synth hop and no boundary, so both defs lost importance 9 and the file rendered from its head. `identityOnly()` now separates the two, gated on shape-precise tokens so a prose word that exact-matches a callable cannot promote itself. 2. The ceiling trim filled in SOURCE order, so an over-ceiling render always dropped the END of a large file first. The shrink HAD kept both symbols (1022-1121); the trim cut back to 839. `windowToCeiling` now takes the spine call site plus every importance>=9 member as focus lines, tries the full ceiling first, and splits the held-back reserve evenly with carry-forward — greedy-in-source-order reproduced the bug one level down. The shrink's loose size estimate is left alone deliberately, and the comment now says why: making it exact was built and measured WORSE (it stops at the last member that fits whole and the released bytes carry forward to lower-ranked files, costing payroll-go's `s.store.Upsert`). `bound()` clamps to the ceiling anyway, so the slack costs no bytes; it just must not pick the survivors, which is what the trim now handles. The measurement gap this closes: every existing probe is aggregate — envelope share, per-file spend, source totals, file counts — and all are green on a response that returns 25K from the right file and omits the named function. `probe-named-symbol.mjs` checks the definition LINE against the response's rendered lines, per symbol. Suite envelope byte-identical to main on all six repos; probe-allocation 4/4, no starvation flags; 180 files / 2,997 tests green. Fixture: 7/7 fail on main, 7/7 pass here, deterministic over 4 runs per arm.
125 lines
7.7 KiB
Markdown
125 lines
7.7 KiB
Markdown
# Epic resolution — explore response noise (CG-24)
|
||
|
||
Worked 2026-08-05 → 2026-08-06. Started from one bad `codegraph_explore` response
|
||
in a real session and ended with four shipped fixes, one open defect, and five
|
||
issues closed because measurement contradicted them.
|
||
|
||
**The headline: the reported symptom was not an explore bug.** It was a degraded
|
||
index. The explore defects the investigation found are real and were fixed, but
|
||
none of them caused the report.
|
||
|
||
## The report
|
||
|
||
A prose flow query returned an unusable response: the symbol the agent had named
|
||
never rendered, and a 12k-line generated Cloudflare ambient-types file took 60.7%
|
||
of the output envelope.
|
||
|
||
```
|
||
# deliv% bytes reserved score pen flags file
|
||
1 1.0% 251 10,970 87.0 1.00 named entry central <the named file>
|
||
2 60.7% 15,043 6,484 49.0 1.00 entry central worker-configuration.d.ts
|
||
4 — — — 19.9 1.00 dropped: budget <a third file>
|
||
```
|
||
|
||
## Root cause
|
||
|
||
**Index drift ([CG-33](index-drift-cg33.md)).** The live incrementally-synced index
|
||
diverged from a clean rebuild by 4.3% of distinct edges, bidirectionally,
|
||
overwhelmingly `calls`. RWR graph mass is relative and normalized, so call edges
|
||
missing elsewhere inflate an unaffected file's share — the `.d.ts` carried mass
|
||
0.24750 drifted vs 0.13119 rebuilt (~1.9×), score 49.0 vs 27.0.
|
||
|
||
Two causes, both fixed: incremental sync re-resolved only references *in* changed
|
||
files, and `getNodesByName` had no `ORDER BY`, so ties broke by rowid — i.e. by
|
||
the order files happened to be **written**. The second is why scope alone could
|
||
never converge. Stale edges dropped 671 → 2 across an 80-commit replay.
|
||
|
||
On a freshly rebuilt index the reported query answers correctly **with no explore
|
||
change at all**.
|
||
|
||
## Shipped
|
||
|
||
| | what |
|
||
|---|---|
|
||
| **CG-30** | Bounded how far an oversize cluster member may overshoot; windows on whole lines past 1.5× instead of emitting whole — or, when larger than the response ceiling, dropping the file silently. |
|
||
| **CG-31** | Gave the cluster path the `owedBelow` displacement guard the whole-file BUY arm always had, holding back only the prefix of what is owed below that the response can actually pay. |
|
||
| **CG-26** | Closed the remaining holes: whole-file arms had no displacement guard at all, section overhead was charged at a flat 200 against a real 300–500, and `owedPayableBelow` held all-or-nothing. |
|
||
| **CG-25** | Recognize `Generated by <tool> by running <command>` banners. Precision held by requiring two `by` clauses, so ordinary prose does not match. |
|
||
| **CG-28** | Damp declaration-only files that nothing in the index depends on. Does not stack with the generated penalty (`Math.min`), and naming a declaration symbol exempts its file. |
|
||
| **CG-33 / CG-35** | Incremental sync converges with a rebuild, plus a regression suite that fails when the fix is disabled. |
|
||
| **CG-36** | A later cluster is shrunk into the remainder rather than dropped whole — at selection, and again in the ceiling trim. All 8 starvation flags across the suite clear; +1,012 source chars net. `explore-cluster-starvation-cg36.md`. |
|
||
|
||
Deterministic across the 6-repo suite: no repo truncates, none loses a file,
|
||
okhttp gains one, every repo lands at or under the 25,000 hard ceiling.
|
||
|
||
## Follow-up — CG-38 (closed)
|
||
|
||
**Agent-named symbols in the tail of a large file never rendered.** On the
|
||
motivating repo, `queueMessage` (line 1087) and `flushQueuedMessages` (1102) in a
|
||
1,414-line file were absent from the response on both prose and symbol-bag
|
||
queries, even when that file won rank #1 with 67% of the envelope. The response
|
||
returned the `QueuedMessage` *interface* at line 70 — a fuzzy near-match on the
|
||
query token — instead of the function.
|
||
|
||
**Pre-existing, not caused by this epic.** A controlled bisect (index held fixed,
|
||
engine varied across every merge point) shows the pre-epic engine rendering 12
|
||
lines here and CG-36 rendering 463; the symbols render at neither. The epic
|
||
strictly improves the case. An earlier claim that the epic regressed it was
|
||
wrong — it compared runs across two different indexes.
|
||
|
||
Two independent causes, both longstanding: `buildFlowFromNamedSymbols` discarded
|
||
the named-symbol IDENTITY along with the narrative whenever the named symbols did
|
||
not form a call chain, so the importance-9 injection never ran; and the ceiling
|
||
trim cut in SOURCE order, so a named def at the end of a large file was always the
|
||
first thing dropped. Full account, plus the ranker-penalty lead (real, and
|
||
orthogonal — the defs are absent at both `generated` flag states on the old build
|
||
and present at both on the new one): `explore-tail-render-cg38.md`.
|
||
|
||
This epic's probes measure envelope share, starvation, source totals and file
|
||
counts. **None measured "did the agent-named symbol render"** — which is why this
|
||
survived the whole epic. `scripts/agent-eval/probe-named-symbol.mjs`,
|
||
`__tests__/fixtures/tail-render-ts` and
|
||
`__tests__/explore-named-symbol-render.test.ts` close that gap: per-symbol and
|
||
binary, checking the definition LINE against the response's rendered lines.
|
||
|
||
CG-36's own measurement is worth carrying forward, because the issue named the wrong
|
||
fix point: both real cases (`query.py`, `RealInterceptorChain.kt`) lost on
|
||
`maxImportance`, **not** on the density tiebreak the issue suspected. Ranking was
|
||
left alone; the never-shrink rule was the lever. Full numbers and the one cost
|
||
(okhttp trades its rank-6 file for +7,196 chars in the two that answer the
|
||
question) in `explore-cluster-starvation-cg36.md`.
|
||
|
||
## Closed because measurement contradicted them
|
||
|
||
Five, which is the story of this epic as much as the fixes are.
|
||
|
||
| | why |
|
||
|---|---|
|
||
| **CG-32** | Named file "didn't render first." Drift artifact; on a clean index it renders first and takes 89%. |
|
||
| **CG-34** | "Allocator over-reserves for low-scoring files." Filed on a runner's diagnosis without checking the numbers. The file was never over-reserved (4,314 in both arms) — it was over-*spending*, which is CG-31. |
|
||
| **CG-27** | Adding `function`/`method` to `ENVELOPE_KINDS` measured as a **large regression** — rank #1 fell from 7,539 delivered chars to 397, 7 of 11 inner closures to 0. The enclosing range was holding the file together as one cluster, inside which `shrinkCluster` already did the per-symbol ranking the issue wanted. A careful version was noise (69 vs 68 across nine queries). |
|
||
| **CG-29** | Prose-vs-symbol query gap. Inverted on measurement: prose matches symbol on django and delivers 63% more source on okhttp. The founding observation was drift. |
|
||
| **CG-37** | Duplicate of CG-36, filed without seeing it. |
|
||
|
||
## What this epic is actually a lesson in
|
||
|
||
**A confident diagnosis is worth less than a cheap measurement.** Every issue
|
||
above was filed by someone — human or agent — who had read the code and had a
|
||
plausible mechanism. Five were wrong. The ones that survived did so because a
|
||
deterministic probe disagreed with them and the probe won.
|
||
|
||
Two specific traps this cost real time on, both now guarded in tooling:
|
||
|
||
- **`.codegraph/graph.db` does not exist** — the index is `codegraph.db`, and
|
||
`sqlite3` against a mistyped path *creates* an empty database rather than
|
||
failing. An empty schema reads exactly like a stale pre-migration index. This
|
||
produced a wrong root cause. `diff-index-drift.mjs` refuses a missing path.
|
||
- **`ab-new-vs-baseline.sh` checks the engine out at the baseline ref mid-run.**
|
||
A commit made while it runs captures baseline sources and silently reverts the
|
||
fix under test. This happened during CG-30. Check the `changed:` line before
|
||
believing any A/B result.
|
||
|
||
And one measurement discipline worth keeping: **compare sets, not totals.** The
|
||
drift that started all of this shows up as +0.7% on raw edge counts, because it
|
||
is bidirectional and nets out. On distinct edge triples it is 4.3%.
|