Fix recurring EXC_BREAKPOINT/SIGTRAP crash (heap OOM from file-search-index rebuilds) + merge outstanding fix branches#633
Open
temperatio wants to merge 20 commits into
Conversation
_extensionManifestCache and _extensionBundleCodeCache were plain Maps with no size limit, so a long session opening many distinct extensions accumulated their full bundled text in memory indefinitely. Cap both at a bounded LRU (200 manifests, 40 bundles).
callWindowManagerWorker() already forks the worker lazily on first use via ensureWindowManagerWorker(). Most sessions never touch window management, so eagerly forking a full Node child process at app.whenReady() just to save latency on a feature most users never hit isn't worth the constant RAM cost.
The parakeet/qwen3/whisper.cpp persistent serve-mode processes keep a model loaded in memory for fast repeat transcription, but previously stayed alive until the app quit once started even once. Add a shared idle-shutdown scheduler that kills each one after 10 minutes without use, reset on every real invocation.
discoverInstalledExtensionCommands() now yields to the event loop every 5 extensions instead of scanning the whole install in one uninterrupted synchronous burst, and initCommandsCache()/loadCommandsDiskCache() move to fs.promises with icon re-attachment done concurrently via Promise.all instead of one fs.readFileSync per command — both run right before createWindow() on every launch.
node-window-manager requires both at runtime to load its native addon and resolve file icons, but only node-window-manager itself was listed in asarUnpack. In any packaged build the require() call crossed from the unpacked filesystem location back into the still-packed asar and failed, so window-manager-worker responded "window manager unavailable" to every request — breaking window management, "restore previous app" capture, and window layout commands in every distributable build. Found by actually running a packaged .dmg and reproducing the failure with ELECTRON_RUN_AS_NODE=1, not from code review.
Extension bundling (esbuild + fs) and script command execution (child_process) run real Node async work on the main process. If one of those callbacks completed after Electron began tearing down the V8/Node environment during quit, Node aborted natively (EXC_BREAKPOINT/SIGTRAP) instead of throwing a catchable JS error — reproduced repeatedly in production crash reports (SuperCmd-2026-07-13-110438.ips and others, all identical signature: CrBrowserMain thread, CrShutdownDetector present, no ASI message). Track every getExtensionBundle/executeScriptCommand/buildAllCommands call in a registry. before-quit now intercepts the first quit attempt, waits up to 2s for tracked work to settle, and only then lets the app actually quit — bounded so a stuck extension can't block shutdown indefinitely. See the ADR stored for this project (Users-cgomez-orca-workspaces- SuperCmd-Fix-shoutdown) via codebase-memory for the full crash diagnosis and the alternatives considered.
@esbuild/darwin-arm64 and @esbuild/darwin-x64 were pinned as regular dependencies, so npm install failed with EBADPLATFORM on any single- architecture Mac (e.g. arm64-only) instead of just skipping the mismatched one. The postinstall script already backfills whichever platform binary is missing for universal builds, so the hard pin was both redundant and breaking fresh installs.
Adds four native "fun" system commands modeled after Raycast's built-in Confetti extension, each rendering via a shared transparent always-on-top overlay window helper (showFullScreenOverlayEffect): - Confetti: multi-burst rectangle particles, tuned for wide screen coverage - Fireworks: staggered rocket launches with trails that explode into radial showers - Snow: continuously-recycled snowfall with sway, density scaled to screen size - Rain: wind-angled streaks with ground splashes, same density scaling Bursts stack instead of replacing one another — retriggering a command while a previous burst is still animating layers a new overlay window on top rather than closing the old one, matching Raycast's behavior.
…menu The menu bar tray only offered Open/Quit. Adds direct access to SuperCmd Settings, the Extension Store, a manual "Check for Updates..." action, and a "Launch at Login" checkbox toggle — all standard menu-bar app affordances. Extracted the app-updater check/download/restart flow and the open-at-login apply+persist pairing into standalone functions (runAppUpdaterCheckAndInstall, setOpenAtLogin) so the tray items and their existing IPC handlers share the same code instead of duplicating it.
ensureXServer() (parakeet/qwen3/whisper.cpp) bumped the 10-minute idle timer once at the start, before the request was even sent, and nothing re-armed it while a request was in flight. A transcription running past 10 minutes (long audio, slow hardware) got its server killed and its pending request rejected mid-flight, even though the server was busy, not idle. createIdleShutdownScheduler now takes an isBusy() predicate; when the timer fires while a request is still pending it defers instead of killing, and only shuts the server down once it's genuinely idle.
lastBuildError only ever grew via .set() on build failure and was never evicted on a later successful rebuild of the same command, so it retained stale error strings for the lifetime of the process.
Gated behind SC_HEAP_DEBUG=1 so it's inert for regular users. Logs process.memoryUsage() every 5 minutes and registers Cmd+Alt+Shift+H to write a heap snapshot, to help confirm whether the main-process heap grows unbounded during a long session (investigating the 2026-07-17 EXC_BREAKPOINT crash on CrBrowserMain).
The window-manager worker was forked with stdio fully ignored, so any underlying error (e.g. node-window-manager failing to load) was silently dropped — callers only ever saw the generic "window manager unavailable" message. Pipe stdout/stderr back into the main process console instead.
node-window-manager requires extract-file-icon (which loads its native binding via node-gyp-build) for window icon extraction. Both are hoisted to the root node_modules, so the node_modules/node-window-manager/** asarUnpack glob never captured them — the window-manager worker (which runs from the real unpacked-on-disk location, not through Electron's asar-aware require) failed to resolve either module and silently fell back to "window manager unavailable" on every window-capture call. Confirmed via the window-manager stdio fix in the previous commit, which surfaced the underlying "Cannot find module 'extract-file-icon'" error that had been swallowed until now.
Reproduced the original crash signature live: after a long session the
main-process heap climbed toward V8's default old-space ceiling
(~2GB), Mark-Compact GC could no longer reclaim enough, and the process
aborted with SIGTRAP — the same signal from the original crash report.
Raising --max-old-space-size via app.commandLine gives it headroom on
machines that have it to spare, buying time while the underlying
retainer is tracked down separately.
Also switch the SC_HEAP_DEBUG snapshot capture to a SIGUSR2 handler (in
addition to the existing hotkey) so a snapshot can be requested from a
script/terminal without the app needing focus, and write snapshots to
os.tmpdir() instead of app.getPath('logs') — the logs dir has been
observed losing files to unrelated cleanup shortly after capture.
The previous fix (app.commandLine.appendSwitch('js-flags', ...)) does not
reach this process at all — that switch only configures Chromium-managed
renderer processes. Confirmed by a real crash recurring (identical SIGTRAP
signature, same crashing address, ~48min uptime) with that switch in
place. v8.setFlagsFromString() was tried as a runtime alternative and also
confirmed ineffective: heap_size_limit is fixed at isolate creation and
does not change afterward, verified empirically (before/after identical).
The only mechanism that reaches this process's own V8 instance is
--js-flags on the actual argv at process creation. Re-exec once via
child_process.spawn with the flag prepended and an env marker to avoid
looping. Verified live: heap_size_limit now reports ~4096MB for the
running main process instead of the ~2000MB default it was hitting.
…dexing Root cause of the recurring EXC_BREAKPOINT/SIGTRAP crashes: rebuilding the file search index (up to 1.2M entries under the whole home directory, every 8 minutes) built the new snapshot fully before swapping it in, so the old and new snapshot (each easily 1-2GB for a large home dir) coexisted for the whole scan — a single rebuild could spike heap usage by several GB regardless of any configured V8 heap ceiling. Confirmed live: heapUsed jumped ~1.7GB in one interval, coincident with a "[FileIndex] Rebuilt" log line, ending in "JavaScript heap out of memory". - Drop the old snapshot before building the new one instead of after — searches return empty during the rebuild window rather than doubling peak memory for the whole scan. - Cap indexed entries at 400k (was 1.2M) and space full rebuilds out to 60 minutes (was 8) so both the steady-state size and the spike frequency drop. - Add a "Rebuild File Search Index" command for an on-demand manual refresh instead of relying solely on the interval.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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
Root-cause fix for a recurring
EXC_BREAKPOINT/SIGTRAPcrash onCrBrowserMain(main process), reported in production crash logs. Also folds in the two other outstanding fix branches (#630, #631) plus a few more fixes found while investigating, sincemainhad been stalled and none of this had landed yet.Root cause
file-search-index.tsindexes the whole home directory (up to 1.2M entries) and rebuilt the index every 8 minutes. The rebuild built the entire new snapshot before swapping it in, so the old (~1-2GB) and new snapshot stayed in memory simultaneously for the whole scan — confirmed live via[HeapDebug]logging: heapUsed jumped ~1.7GB in a single 5-minute window, coincident with a[FileIndex] Rebuiltlog line, ending inOOM error in V8: ... JavaScript heap out of memory.Fixes in this PR
file-search-index.ts: drop the old snapshot before building the new one (not after) — eliminates the double-memory spike. Cap entries at 400k (was 1.2M) and space full rebuilds to 60min (was 8min). Added a manual "Rebuild File Search Index" command for on-demand refresh.main.ts: raise the main process's V8 old-space ceiling via a one-time re-exec with--js-flags=--max-old-space-size=4096on argv — the only mechanism that actually reaches this process (verifiedapp.commandLine.appendSwitch('js-flags', ...)andv8.setFlagsFromString()do not affect it; both were tried and confirmed ineffective before landing on re-exec).main.ts: stop silencing the window-manager worker's stdout/stderr — this surfaced a real packaging bug (see next).package.json: unpackextract-file-icon/node-gyp-buildfrom asar — without this,node-window-managersilently failed to load in every packaged build (window manager unavailableon every hotkey invocation).extension-runner.ts:lastBuildErrormap is now cleared on a successful rebuild instead of growing forever.SC_HEAP_DEBUG=1diagnostics: periodic memory logging + on-demand heap snapshot capture (hotkey orSIGUSR2), used throughout this investigation.feat/Fix-shoutdown(PR Fix shutdown crash (EXC_BREAKPOINT/SIGTRAP) and reduce startup/RAM footprint #630 — LRU cache eviction, non-blocking discovery, idle transcription server shutdown, drain in-flight extension/script work before quit) andfeat/feat-confetti(PR Add fun visual-effect commands (Confetti, Fireworks, Snow, Rain) + expanded tray menu #631 — expanded tray menu, Confetti/Fireworks/Snow/Rain commands, esbuild as optionalDependencies).How this was tested
SC_HEAP_DEBUG=1runs, correlating[HeapDebug]/[FileIndex]logs against~/Library/Logs/DiagnosticReports/*.ipscrash reports.v8.getHeapStatistics().heap_size_limitbefore/after, then confirmed the working re-exec approach live (~4096MBreported).extract-file-iconpackaging bug by reproducing it withELECTRON_RUN_AS_NODE=1against the packaged, unpacked asar path.grep'd400000indist/main/file-search-index.js/ the packagedapp.asar) before each test round, after hitting stale-build/stale-process confusion a few times mid-investigation.npm run build:mainclean after every commit.Test plan for reviewers
SC_HEAP_DEBUG=1for 2+ hours and confirm noEXC_BREAKPOINT/SIGTRAPcrash and no new.ipsreport..dmg, not justnpm run dev) works — was silently broken before the asarUnpack fix.