The #850 liveness watchdog was killing valid `codegraph init`/`index` runs
at "Resolving refs 0-2%" on large collision-heavy repos (18-25K-file Java
monorepos on slower hardware). #1105's cooperative yielding assumed a
500-ref sub-chunk is always cheap, but per-ref cost is unbounded: a
colliding method name (`execute`, `process`, ...) whose candidate set
misses the 5,000-entry name LRU re-fetches every same-named row
(unbounded SELECT + materialization, measured 8.8ms at just 4K collisions
on an M4 — linear in collision count), and receiver-type inference
re-split the whole source file per ref (~20% of total index CPU). A dense
pocket multiplied that past the 60s window and the heartbeat starved.
Three guards, no behavior change:
- resolveBatchYielding checkpoints after EVERY ref (maybeYield is a ~ns
time check when under budget), so a slow pocket can never run more than
one ref past the yield budget.
- resolveMethodOnType's ref-independent candidate filter is memoized per
(language, Type::method) on the resolver context; per-ref
disambiguation (import FQN #314, call-site file #1079) stays outside
the memo.
- Receiver inference reads lines through a per-file LRU (shared and C++
inferrers), and skips generated/minified lines >10K chars instead of
regex-scanning them per ref.
Measured on a 4,028-file synthetic Java bank repo (392K refs): mid-loop
max event-loop stall 1528ms -> 546ms under cache thrash, total init
250.9s -> 96.8s at default config.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>