Summary
Two related problems observed with v1.5.0 (latest) on macOS (Apple Silicon):
- The background daemon crashes with
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory — 178 times in this project's daemon.log — on a small project (524 indexed files, ~22 MB codegraph.db). Memory balloons to the default ~4 GB heap cap during what looks like the startup catch-up sync, usually ~20–30 s after the daemon starts.
- After the daemon dies it leaves stale
daemon.pid / daemon.sock / lock files behind. If the OS later reuses the dead daemon's PID for an unrelated process, the daemon liveness check (PID exists ⇒ daemon alive) is permanently wrong: every new daemon exits with Another daemon (pid …) already holds the lock; exiting, and codegraph unlock does not fix it.
Environment
- codegraph 1.5.0 (latest on npm), installed globally via pnpm (bundled node from
@colbymchenry/codegraph-darwin-arm64)
- macOS, Apple Silicon
- Project: React Native 0.84 app (iOS/Android/HarmonyOS), 524 indexed files,
codegraph.db ≈ 22 MB; status --json reports pendingChanges: 0, index state complete — the index itself is healthy
Bug 1 — daemon OOM during startup catch-up sync
Typical crash pattern (most entries look like this — heap pinned at the ~4 GB default cap ~20–30 s after daemon start):
[44485:0xa4380c000] 21323 ms: Scavenge (interleaved) 4041.6 (4058.8) -> 4041.6 (4105.8) MB … allocation failure
[44485:0xa4380c000] 23695 ms: Mark-Compact (reduce) 4041.6 (4105.8) -> 4041.6 (4044.0) MB …
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
The first daemon in the log survived ~21 minutes before its first OOM (1260216 ms into the run) — consistent with the OOM being triggered by a large catch-up sync rather than steady-state serving. My hypothesis: a mass file-change event (e.g. npm install / patch-package touching many files under node_modules) makes the watcher enqueue a huge change set that is then processed in memory all at once; the watcher appears to follow the whole project tree including node_modules even though the index excludes it.
Workaround I verified — raising the heap cap makes the same startup sync survive:
NODE_OPTIONS=--max-old-space-size=12288 codegraph serve --mcp …
With 12 GB: the startup sync balloons to ~3.6 GB RSS, then completes and exits cleanly; the daemon settles at ~230 MB RSS, queries work, and no new FATALs occur. So this looks like a legitimately large transient allocation (or a leak confined to the sync pass) rather than a steady-state leak. Either way, ~4 GB for a 524-file project's sync seems well worth a look (streaming/chunking the change set, or excluding node_modules from the watcher).
Bug 2 — stale daemon.pid + OS PID reuse permanently blocks daemon startup; unlock doesn't help
After an OOM crash the daemon left daemon.pid (containing 54444), daemon.sock, and a lock file. Later, macOS assigned pid 54444 to a completely unrelated system process (cloudd). From then on:
- Every daemon start logs
[CodeGraph daemon] Another daemon (pid 54444) already holds the lock; exiting. (28 occurrences) — and this kept happening even after codegraph unlock removed the lock file, because the liveness check appears to be "does the pid in daemon.pid exist" (a bare kill(pid, 0)/process-exists check succeeds against cloudd).
codegraph daemons reports the phantom daemon as real: pid 54444 v1.5.0 up 5358m …
codegraph serve --mcp (spawned by editor/agent integrations at session start) can never become ready, so MCP clients just time out.
Only manual cleanup recovers:
rm .codegraph/daemon.pid .codegraph/daemon.sock
codegraph unlock
Suggestions
- Verify daemon liveness by process identity (match the process command line / binary path against the codegraph install, or probe
daemon.sock with a handshake) instead of relying on PID existence — PIDs get reused by the OS all the time.
- Have
codegraph unlock (or daemon startup) also invalidate daemon.pid / daemon.sock when the pid fails identity verification.
- Consider a crash/exit handler that cleans up
daemon.pid / daemon.sock / lock on exit.
Happy to provide the full 2.1 MB daemon.log if useful — it contains all 178 OOM stack traces.
Summary
Two related problems observed with v1.5.0 (latest) on macOS (Apple Silicon):
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory— 178 times in this project'sdaemon.log— on a small project (524 indexed files, ~22 MBcodegraph.db). Memory balloons to the default ~4 GB heap cap during what looks like the startup catch-up sync, usually ~20–30 s after the daemon starts.daemon.pid/daemon.sock/ lock files behind. If the OS later reuses the dead daemon's PID for an unrelated process, the daemon liveness check (PID exists ⇒ daemon alive) is permanently wrong: every new daemon exits withAnother daemon (pid …) already holds the lock; exiting, andcodegraph unlockdoes not fix it.Environment
@colbymchenry/codegraph-darwin-arm64)codegraph.db≈ 22 MB;status --jsonreportspendingChanges: 0, index statecomplete— the index itself is healthyBug 1 — daemon OOM during startup catch-up sync
Typical crash pattern (most entries look like this — heap pinned at the ~4 GB default cap ~20–30 s after daemon start):
The first daemon in the log survived ~21 minutes before its first OOM (1260216 ms into the run) — consistent with the OOM being triggered by a large catch-up sync rather than steady-state serving. My hypothesis: a mass file-change event (e.g.
npm install/ patch-package touching many files undernode_modules) makes the watcher enqueue a huge change set that is then processed in memory all at once; the watcher appears to follow the whole project tree includingnode_moduleseven though the index excludes it.Workaround I verified — raising the heap cap makes the same startup sync survive:
With 12 GB: the startup sync balloons to ~3.6 GB RSS, then completes and exits cleanly; the daemon settles at ~230 MB RSS, queries work, and no new FATALs occur. So this looks like a legitimately large transient allocation (or a leak confined to the sync pass) rather than a steady-state leak. Either way, ~4 GB for a 524-file project's sync seems well worth a look (streaming/chunking the change set, or excluding
node_modulesfrom the watcher).Bug 2 — stale
daemon.pid+ OS PID reuse permanently blocks daemon startup;unlockdoesn't helpAfter an OOM crash the daemon left
daemon.pid(containing 54444),daemon.sock, and a lock file. Later, macOS assigned pid 54444 to a completely unrelated system process (cloudd). From then on:[CodeGraph daemon] Another daemon (pid 54444) already holds the lock; exiting.(28 occurrences) — and this kept happening even aftercodegraph unlockremoved the lock file, because the liveness check appears to be "does the pid indaemon.pidexist" (a barekill(pid, 0)/process-exists check succeeds againstcloudd).codegraph daemonsreports the phantom daemon as real:pid 54444 v1.5.0 up 5358m …codegraph serve --mcp(spawned by editor/agent integrations at session start) can never become ready, so MCP clients just time out.Only manual cleanup recovers:
Suggestions
daemon.sockwith a handshake) instead of relying on PID existence — PIDs get reused by the OS all the time.codegraph unlock(or daemon startup) also invalidatedaemon.pid/daemon.sockwhen the pid fails identity verification.daemon.pid/daemon.sock/ lock on exit.Happy to provide the full 2.1 MB
daemon.logif useful — it contains all 178 OOM stack traces.