Bug
apps/desktop/src/main/index.ts before-quit handler called processManager?.killAll() synchronously and then returned. killAll() iterates the registry and invokes pty.kill() on each — but pty.kill() only sends SIGHUP; the actual child exit is async.
Result on Cmd+Q with active pipelines:
- Electron main process exits within milliseconds.
claude / codex / gh pty children never get a chance to exit.
- They become orphaned subprocesses owned by
init, still consuming CPU + holding worktree file locks.
There was also no SIGKILL escalation — a wedged pty would survive forever.
Fix (already merged on develop)
packages/agents/src/process-manager.ts — added killAllAndWait(graceMs = 5000):
- Send
pty.kill() (SIGHUP) to every active process.
- Wait for each to emit
exit.
- After
graceMs, escalate any holdouts to pty.kill('SIGKILL').
- Final 1s cap, then resolve.
apps/desktop/src/main/index.ts — rewrote before-quit:
- Always
event.preventDefault() on the first pass so we can wait.
await processManager?.killAllAndWait(5000) before exiting.
app.exit(0) (synchronous, bypasses before-quit) once children are gone.
killInFlight guard prevents the dialog logic from re-running on the second pass.
New tests in packages/agents/src/process-manager.test.ts cover: resolve-after-all-exit, no-op when nothing active, and SIGKILL escalation under fake timers.
Worktree cleanup audit
Considered cleaning worktrees on quit. Decided against it — worktrees are keyed by threadId and intentionally persist across sessions per .agents/memory/worktrees.md. Auto-cleanup would orphan threads.
Verification
bun run test src/process-manager.test.ts # 7/7 pass
bun run typecheck # repo-wide clean
Bug
apps/desktop/src/main/index.tsbefore-quithandler calledprocessManager?.killAll()synchronously and then returned.killAll()iterates the registry and invokespty.kill()on each — butpty.kill()only sends SIGHUP; the actual child exit is async.Result on Cmd+Q with active pipelines:
claude/codex/ghpty children never get a chance to exit.init, still consuming CPU + holding worktree file locks.There was also no SIGKILL escalation — a wedged pty would survive forever.
Fix (already merged on
develop)packages/agents/src/process-manager.ts— addedkillAllAndWait(graceMs = 5000):pty.kill()(SIGHUP) to every active process.exit.graceMs, escalate any holdouts topty.kill('SIGKILL').apps/desktop/src/main/index.ts— rewrotebefore-quit:event.preventDefault()on the first pass so we can wait.await processManager?.killAllAndWait(5000)before exiting.app.exit(0)(synchronous, bypassesbefore-quit) once children are gone.killInFlightguard prevents the dialog logic from re-running on the second pass.New tests in
packages/agents/src/process-manager.test.tscover: resolve-after-all-exit, no-op when nothing active, and SIGKILL escalation under fake timers.Worktree cleanup audit
Considered cleaning worktrees on quit. Decided against it — worktrees are keyed by
threadIdand intentionally persist across sessions per.agents/memory/worktrees.md. Auto-cleanup would orphan threads.Verification