Skip to content

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
SuperCmdLabs:mainfrom
temperatio:feat/fix-review-crash
Open

Fix recurring EXC_BREAKPOINT/SIGTRAP crash (heap OOM from file-search-index rebuilds) + merge outstanding fix branches#633
temperatio wants to merge 20 commits into
SuperCmdLabs:mainfrom
temperatio:feat/fix-review-crash

Conversation

@temperatio

Copy link
Copy Markdown

Summary

Root-cause fix for a recurring EXC_BREAKPOINT/SIGTRAP crash on CrBrowserMain (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, since main had been stalled and none of this had landed yet.

Root cause

file-search-index.ts indexes 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] Rebuilt log line, ending in OOM 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=4096 on argv — the only mechanism that actually reaches this process (verified app.commandLine.appendSwitch('js-flags', ...) and v8.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: unpack extract-file-icon/node-gyp-build from asar — without this, node-window-manager silently failed to load in every packaged build (window manager unavailable on every hotkey invocation).
  • extension-runner.ts: lastBuildError map is now cleared on a successful rebuild instead of growing forever.
  • Opt-in SC_HEAP_DEBUG=1 diagnostics: periodic memory logging + on-demand heap snapshot capture (hotkey or SIGUSR2), used throughout this investigation.
  • Merges branch 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) and feat/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

  • Reproduced the exact crash signature live multiple times via SC_HEAP_DEBUG=1 runs, correlating [HeapDebug]/[FileIndex] logs against ~/Library/Logs/DiagnosticReports/*.ips crash reports.
  • Confirmed the two failed heap-limit approaches were genuinely ineffective by checking v8.getHeapStatistics().heap_size_limit before/after, then confirmed the working re-exec approach live (~4096MB reported).
  • Confirmed the extract-file-icon packaging bug by reproducing it with ELECTRON_RUN_AS_NODE=1 against the packaged, unpacked asar path.
  • Verified the compiled output contains the new constants (grep'd 400000 in dist/main/file-search-index.js / the packaged app.asar) before each test round, after hitting stale-build/stale-process confusion a few times mid-investigation.
  • npm run build:main clean after every commit.

Test plan for reviewers

  • Run a packaged build with SC_HEAP_DEBUG=1 for 2+ hours and confirm no EXC_BREAKPOINT/SIGTRAP crash and no new .ips report.
  • Confirm window management (packaged .dmg, not just npm run dev) works — was silently broken before the asarUnpack fix.
  • Confirm file search still returns results after a rebuild completes (briefly empty during the rebuild window is expected/accepted).

cegomez-gr and others added 20 commits July 16, 2026 13:54
_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>
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.

2 participants