main: bound long-running memory growth + heap-pressure watchdog#48
Merged
Conversation
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>
6 tasks
This was referenced Jun 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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] → ElectronMainconfirm 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): samplesused_heap_sizeevery 30 s and writes a.heapsnapshotto~/.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 insrc/main/index.tstoapp.whenReady/before-quit.src/main/storage/feedDebugLog.ts: the per-sessionfeedDebugWriteQueuesandlastWrittenFeedDebugIdMaps used to grow forever. Now self-reap when the queue settles (with a=== nextsuccessor check to avoid racing concurrent appends), and a newforgetFeedDebugSession()drops the cursor on session close. Wired intoSessionManager.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,refreshNownow loads the disk index ONCE per refresh whentotalOnDisk > 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()removesfeed-debug/*.jsonlfiles 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-*.tsharness scripts and thetesting/regression-*/testing/ghost-demobits — none of them are wired to CI and the project has been operating without them. The interactivetesting/rendering/harness stays (it's a dev tool, not a test suite).Test plan
npx tsc --noEmit -p tsconfig.node.jsonshows only pre-existing errors in unrelated files (claude-code-headless package types, dictation, pidusage). No errors cite the new/changed files..heapsnapshotlands in~/.config/cc-shell/heap-snapshots/. Restore the threshold before merging.feed-debug/*.jsonlpresent, launch the app, confirm a[feed-debug] pruned N stale logsline in the console.Notes for reviewers
🤖 Generated with Claude Code