Skip to content

main: bound long-running memory growth + heap-pressure watchdog#48

Merged
Juliusolsson05 merged 2 commits into
mainfrom
fix/main-process-oom
May 6, 2026
Merged

main: bound long-running memory growth + heap-pressure watchdog#48
Juliusolsson05 merged 2 commits into
mainfrom
fix/main-process-oom

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Summary

Closes three concrete unbounded-growth paths in the Electron main process and adds a single-shot heap-snapshot watchdog so any remaining leak leaves a forensic artifact for follow-up.

Motivated by an OOM crash (Reached heap limit Allocation failed - JavaScript heap out of memory) at ~3.84 GB / 4.09 GB after 19h 51m of uptime. Stack frames _DPSNextEvent → -[NSApplication run] → ElectronMain confirm it's the main process, not a renderer. Default v8 main heap is 4 GB on macOS; we are NOT bumping that — bandaging the cap just postpones the crash. The fix is bounded growth.

What changed

  • src/main/performance/heapWatchdog.ts (new): samples used_heap_size every 30 s and writes a .heapsnapshot to ~/.config/cc-shell/heap-snapshots/ the first time the process crosses 3 GB. Single-shot — additional samples in the same near-OOM window are no-ops. Wired in src/main/index.ts to app.whenReady/before-quit.
  • src/main/storage/feedDebugLog.ts: the per-session feedDebugWriteQueues and lastWrittenFeedDebugId Maps used to grow forever. Now self-reap when the queue settles (with a === next successor check to avoid racing concurrent appends), and a new forgetFeedDebugSession() drops the cursor on session close. Wired into SessionManager.kill() AND both natural-exit paths (claude/codex agent exit + terminal exit).
  • src/main/worktreeActivity/: the in-memory transcripts map is now an LRU bounded at 1000 entries with disk fallthrough on miss. Critically, refreshNow now loads the disk index ONCE per refresh when totalOnDisk > IN_MEMORY_MAX_ENTRIES, instead of reparsing the full ~30 MB JSON on every per-candidate LRU miss — the old shape would have churned ~120 GB of allocations per refresh on heavy users (5000 transcripts × 30 MB).
  • src/main/storage/paths.ts: pruneStaleFeedDebugLogs() removes feed-debug/*.jsonl files older than 30 days at app startup. Matches observed disk pressure (149 GB / 5007 files / largest single 9.8 GB on the affected machine).

Also drops the loose scripts/test-*.ts harness scripts and the testing/regression-* / testing/ghost-demo bits — none of them are wired to CI and the project has been operating without them. The interactive testing/rendering/ harness stays (it's a dev tool, not a test suite).

Test plan

  • npx tsc --noEmit -p tsconfig.node.json shows only pre-existing errors in unrelated files (claude-code-headless package types, dictation, pidusage). No errors cite the new/changed files.
  • Manual: launch the app, lower the watchdog threshold to ~256 MiB temporarily, open enough panes to trip it, confirm a .heapsnapshot lands in ~/.config/cc-shell/heap-snapshots/. Restore the threshold before merging.
  • Manual: with old feed-debug/*.jsonl present, launch the app, confirm a [feed-debug] pruned N stale logs line in the console.

Notes for reviewers

  • 3 GB watchdog threshold leaves headroom for the snapshot writer itself (which allocates).
  • LRU cap of 1000 was picked from observed working-set sizes; can tune up if heavy-user telemetry shows churn at that boundary.
  • Independent of the title-turn PR (different subsystems; treat them as separate landings unless heap snapshots from this PR's watchdog point at something the title-turn fix would also address).

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits May 6, 2026 12:22
Closes three concrete unbounded-growth paths that contributed to the
main-process OOM observed at ~3.84 GB after 19h 51m uptime, and adds
a single-shot heap snapshot watchdog so any remaining leak leaves a
forensic artifact for follow-up:

  * heapWatchdog samples used_heap_size every 30 s and writes a
    .heapsnapshot to ~/.config/cc-shell/heap-snapshots/ the first
    time the process crosses 3 GB. Single-shot — additional samples
    in the same near-OOM window are no-ops.

  * feedDebugLog: the per-session write-queue Map and id-cursor Map
    now self-reap when their queue settles (with === successor
    check), and a new forgetFeedDebugSession() drops the cursor on
    session close (kill + both natural-exit paths). Both Maps used
    to grow forever.

  * WorktreeActivityIndex: the in-memory transcripts map is now an
    LRU bounded at 1000 entries with disk fallthrough on miss.
    refreshNow loads the disk index ONCE per refresh when the
    on-disk set exceeds the LRU cap, replacing the per-candidate
    full-JSON reparse that would have churned ~120 GB of
    allocations on heavy users.

  * paths: added pruneStaleFeedDebugLogs() that removes
    feed-debug/*.jsonl files older than 30 days at app startup,
    matching the observed disk-pressure (149 GB / 5007 files).

Also drops the loose scripts/test-*.ts harness scripts and the
testing/regression-* / testing/ghost-demo bits that have been
accumulating as one-off check scripts. None of them are wired into
CI; the rendering harness under testing/rendering/ stays since it's
an interactive dev tool, not a test suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant