fix(index): yield during resolution so the liveness watchdog can't kill a valid large index (#1091) (#1105)

The #850 liveness watchdog SIGKILLs a process whose main-thread event loop
stalls past its window (60s default). It was extended to `index`/`init` in
#999, but reference resolution and callback-edge synthesis run synchronously
on that same thread — so on a large repo a legitimate, in-progress index gets
killed, and users had to disable the watchdog entirely (CODEGRAPH_NO_WATCHDOG=1).

Make the long synchronous spans yield cooperatively so the heartbeat keeps
firing during real work, while a genuinely wedged span (which never reaches a
yield) still trips the watchdog:

- synthesizeCallbackEdges yields between its whole-graph passes, and the heavy
  scanners (closure-collection, event-emitter, JSX-child, object-registry,
  field-channel) yield within their loops;
- batched resolution sub-chunks each batch with yields;
- the deferred chained-call and this-member post-passes yield per ref.

Behaviour-preserving — only timing changes; node/edge counts are identical.

Validated end-to-end with the real watchdog armed at the default 60s: the
released build is SIGKILLed partway through indexing the Swift compiler (27k
files, ~1.1M edges) and the TypeScript compiler, while the fixed build indexes
both to completion.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-01 12:13:42 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 6dd5512d8d
commit ed39233f1a
7 changed files with 258 additions and 45 deletions
+10
View File
@@ -17,6 +17,16 @@
* available to a one-shot command. Best-effort and self-disabling: a missing
* watchdog never blocks the command from running. Both honour the same env
* switches as `serve` (`CODEGRAPH_NO_WATCHDOG`, `CODEGRAPH_PPID_POLL_MS=0`).
*
* Unlike the daemon — whose main thread only does fast, bounded work — the
* `index`/`init` path runs reference resolution and dynamic-edge synthesis
* SYNCHRONOUSLY on this thread, and on a large repo that is legitimately many
* seconds of work. So those spans yield cooperatively to the event loop
* (`src/resolution/cooperative-yield.ts`) to keep the heartbeat alive; without
* that the watchdog would SIGKILL a valid, in-progress index (#1091). The
* distinction it must preserve — kill a TRUE wedge, spare slow-but-progressing
* work — is exactly what cooperative yielding buys: a genuinely stuck span never
* reaches its next yield, so it still trips the timeout.
*/
import { installMainThreadWatchdog } from '../mcp/liveness-watchdog';
import { supervisionLostReason, parsePpidPollMs, parseHostPpid } from '../mcp/ppid-watchdog';