Skip to content

Daemon OOMs (~4 GB heap) on small project during startup catch-up sync; stale daemon.pid + OS PID reuse then blocks all daemon restarts (unlock does not help) #1553

Description

@hcg1023

Summary

Two related problems observed with v1.5.0 (latest) on macOS (Apple Silicon):

  1. The background daemon crashes with FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory178 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions