Skip to content

Packaged app hangs on "Restoring session..." (fsevents not bundled → chokidar EMFILE fd exhaustion) #139

Description

@matanelcohen

Summary

Installed/packaged (dist) builds hang forever on the "Restoring session..." loading screen. Running from source via npm start works fine.

Environment

  • macOS (Apple Silicon), packaged app launched from Finder/Dock
  • Account with a large number of Copilot sessions (repro seen with 4,972 ~/.copilot/session-state dirs)

Root cause

  • The session watchers use chokidar.watch(basePath, { usePolling: false, depth: 2 }).
  • On macOS chokidar only uses the efficient single FSEvents watcher when the native fsevents module is loadable.
  • fsevents is present in dev node_modules but is not bundled into the packaged app (the forge postPackage hook copies node-pty/better-sqlite3 but not fsevents).
  • Without it, chokidar with usePolling:false falls back to one fs.watch fd per directory. Watching thousands of session dirs exhausts the file-descriptor limit:
    Error: EMFILE: too many open files, watch
        at FSWatcher._handle.onchange (node:internal/fs/watchers:207:21)
    
  • The flood saturates the main process event loop, so the renderer's startup IPC stalls and isRestoring never clears → permanent "Restoring session...".
  • Finder/Dock-launched apps inherit a 256 fd soft limit (launchctl limit maxfiles), vs the high ulimit a terminal-launched npm start gets — which is why dev works and dist doesn't.

Reproduction

  1. Have many ~/.copilot/session-state/<id> dirs (hundreds–thousands).
  2. Package the app (npm run package) and launch the binary.
  3. Main process floods EMFILE: too many open files, watch; UI stays on "Restoring session...".

Fix

  • Bundle fsevents in forge.config.ts postPackage (same mechanism as node-pty/better-sqlite3).
  • Externalize fsevents in vite.main.config.ts for a runtime probe.
  • Add canUseNativeRecursiveWatch() and make the copilot/claude-code watchers fall back to bounded polling when fsevents is unavailable (defense-in-depth: degrade instead of hang).

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