Repository navigation
Description
Summary
On Windows, the codegraph serve --mcp long-running worker (the --liftoff-only child) repeatedly ballooned to 38–41 GB of commit charge, wedged, was killed by the watchdog added for #850, and was respawned — then ballooned again. The result was a kill→respawn→re-blowup loop (9 different node.exe PIDs in ~22 minutes) that exhausted the system commit limit (49.7 GB) and crashed unrelated applications. The watchdog fixed the "wedged forever" CPU failure mode of #850, but it exposed a worse one under memory pressure: the respawn loop itself takes the whole machine down, because every fresh worker re-attempts the same work that blew it up in the first place.
This happened on v1.2.0; we have not yet reproduced on current versions. Filed because the loop dynamic (no respawn backoff, no memory guard, no client-visible error) is version-independent as long as the watchdog design stays as-is, and none of the open memory issues (#1553/#1583, #3027) cover it.
Environment
| Item | Value |
|---|---|
| codegraph | @colbymchenry/codegraph 1.2.0 (latest is 1.6.2; several memory fixes landed in between — #900, #2111, #2006) |
| Invocation | MCP stdio server: cmd /c codegraph serve --mcp → node.exe --liftoff-only .../lib/dist/bin/codegraph.js serve --mcp (bundled Node 24.16.0) |
| Client | Codex CLI / Codex desktop, each session spawning its own MCP stack (including a fresh codegraph worker) |
| OS | Windows 11 (build 26200), 16 GB RAM, commit limit 49.7 GB (auto-managed pagefile) |
| Index target | Private monorepo working tree, 28,592 files / 234 MB, many JSON files (largest ~370–720 KB) |
| Date | 2026-10-31, 16:46–17:08 (UTC+9) |
What happened (timeline)
-
Five client sessions were started within ~25 minutes while the target repo was being actively edited; each session start spawned a fresh codegraph worker.
-
Windows
Resource-Exhaustion-Detector(System log, event 2004) fired 11 times between 16:46:07 and 17:05:42. Every event namesnode.exeas the top committer, each time a different PID except the last, at 38–41 GB commit each:Time PID Commit (bytes) 16:46:07 38796 38,849,732,608 16:47:50 22568 40,931,942,400 16:50:03 13300 40,550,477,824 16:52:47 33408 40,462,024,704 16:56:39 31140 39,835,422,720 16:58:35 20260 40,462,966,784 17:01:45 / 17:02:29 / 17:03:13 / 17:05:42 37132 ~38–41 GB (same PID across 4 minutes, 4 events) -
The worker's watchdog child (
node -e, the codegraph cause 100% CPU #850 mitigation) SIGKILLs the main process once the stdin heartbeat stops for 60 s, "so a fresh one can start". The PID churn above is consistent with that kill→respawn cycle repeating. (Process-creation auditing was not enabled, so PID→binary mapping is inferred, not logged — codegraph was, however, the onlynode.exethat matches all three constraints: freshly spawned inside the window, equipped with the kill-and-respawn watchdog, and holding a large in-memory index.) -
Commit exhaustion took unrelated applications down with it: the MCP client itself crashed twice (16:47:51, 16:50:04), Microsoft Edge crashed twice,
dwm.execrashed once (desktop-wide UI stalls), PowerPoint hung, and even the Windows Error Reporting Service terminated with an error. To the user this looked like "the whole PC is extremely slow with fans pegged". -
The storm stopped only when the client-side retry loop stopped respawning sessions. Surviving codegraph instances then idled at 33–86 MB, so the blow-up appears to happen during active (re)indexing, not at rest.
Notably this incident did not look like the #850 CPU-peg (system CPU stayed modest throughout); it manifested as unbounded commit growth instead.
Why the watchdog alone can't cover this failure mode
The watchdog turns "wedged forever" into "wedged, killed, respawned" — strictly better for a CPU wedge. But for a memory-blowup wedge it produces a loop: every fresh worker starts (re)indexing from scratch, balloons again within ~1–4 minutes, gets killed again. When the MCP client auto-respawns the server (or the user retries), this repeats indefinitely, and every cycle degrades the whole OS — commit pressure makes unrelated processes fail allocations and crash. A single misbehaving index target ends up crashing the user's browser, shell, and window manager.
Suggestions
- Memory guard in the worker: cap the worker's heap (e.g.
--max-old-space-sizefor the--liftoff-onlychild) and monitor external/buffer memory; abort indexing with a clear error to the client instead of growing until the OS commit limit is hit. - Respawn backoff / circuit breaker: after N watchdog kills within M minutes, stop respawning and surface an error to the client, instead of looping. (This is the piece that would have capped yesterday's incident at one crash instead of nine.)
- Incremental index persistence: if a fresh worker rebuilds from scratch, persist enough state that a respawn doesn't redo the full pass that triggered the blow-up.
- Surface watchdog kills: emit the kill reason (and the last exception, if any) to stderr so MCP clients can log it. We had no local trace of the root cause — only the Resource-Exhaustion events.
- Possibly: documented ignore/scoping globs for the indexer, so users can exclude pathological inputs.
Happy to enable process auditing and reproduce with a profiler attached (or retest on 1.6.2 first and report back) — just tell me what data would help.
Related: #850 (the watchdog origin), #2111 (OOM during index rebuild, closed), #900 (large-repo indexing OOMs, closed), #1553/#1583 (in-flight memory bounding), #3027 (worker pool growth / every worker opens every project).
Activity
johnhldiver-beep commentedon Oct 31, 2026
I dug into this against current
main(2008e2f) because I cannot reproduce the Windows commit exhaustion on Linux: a 38-41 GB commit charge is a Windows commit-accounting outcome, and the report is against v1.2.0. What I can establish from the code and the incident timeline (9 node.exe PIDs in ~22 minutes) is the loop itself, and it is still present on current main.How the loop exists today
src/mcp/liveness-watchdog.tsis the codegraph cause 100% CPU #850 watchdog. Its child process writes one stderr line, thenprocess.kill(parentPid, 'SIGKILL')when the event loop stops turning. It records nothing anywhere, so the next process knows nothing about the kill.src/mcp/index.tsis what restarts the worker: the MCP client starts a freshcodegraph serve --mcp(the local-handshake proxy), and itsgetDaemonSocket()probes for a daemon, thenspawnDetachedDaemon()s a new shared daemon when none answers. Nothing on that path consults the fact that this project's daemon was just killed by the watchdog.src/mcp/proxy.tsarms one for the proxy. None reads a kill history, so every fresh process repeats the same work with no memory of the previous kill.Planned fix (no public API change)
A small persisted kill record that a fresh process can see, plus a circuit breaker on the automatic restart path:
<project>/.codegraph/watchdog-kills.jsonlimmediately before it SIGKILLs, but only when its parent is armed for a project. Kill conditions themselves are untouched, so a real CPU wedge still dies exactly as codegraph cause 100% CPU #850 requires.CODEGRAPH_NO_WATCHDOG=1and the existing timeout and disk-progress deferral behavior are unchanged.The regression tests will prove on Linux that the child writes a parseable kill record when it kills; that the window accounting trips and expires; that a tripped launcher does not spawn a daemon and a tripped proxy returns the refusal; and that a real wedge is still killed. What they cannot prove here is the Windows commit-accounting behavior itself, which I will state plainly in the PR.
Does this align with the project design?
fix(mcp): a watchdog kill is remembered so the restart cannot loop (c…