Skip to content

fix: executor Mac app quit hangs (and orphaned codex processes)#76

Merged
plutoless merged 12 commits into
mainfrom
fix/executor-quit-hang
Jun 8, 2026
Merged

fix: executor Mac app quit hangs (and orphaned codex processes)#76
plutoless merged 12 commits into
mainfrom
fix/executor-quit-hang

Conversation

@plutoless

Copy link
Copy Markdown
Contributor

Summary

Quitting "Newbro Executor.app" hung every time (beachball, never exits), and orphaned codex app-server / newbro executor processes accumulated. Root cause was a cross-layer deadlock: the Python node ignored SIGTERM, so on quit it died without closing its codex child; the surviving grandchild kept the inherited stdout/stderr pipe open, so the app's NodeProcess reader never hit EOF and stop()'s queue.sync {} blocked the main thread forever.

Fixed as a signal-forwarding chain + hang-proof reader (no setsid/posix_spawn, so terminal Ctrl-C is unaffected):

  • macOS app (NodeProcess): reader uses POSIX read(); stop() closes the read fd to unblock it regardless of any surviving grandchild, and uses a bounded drain instead of queue.sync {}. quit() runs stopAll() off the main thread with a 3s deadline so the UI never freezes.
  • CLI (newbro executor run / run_checked): now uses Popen and installs SIGTERM/SIGINT handlers that forward terminate() → (bounded) → kill() to the node child.
  • Node (_serve): handles SIGTERM/SIGINT, cancels the run loop, and always await service.aclose() in a finallyCodexExecutor.aclose() → closes the app-session → terminates the codex app-server child.

Spec: docs/superpowers/specs/2026-06-08-executor-quit-hang-design.md. Plan: docs/superpowers/plans/2026-06-08-executor-quit-hang.md.

Test Plan

  • Swift: swift test --package-path executor-apps/macos — 118 passed, incl. a regression test where a grandchild holds the pipe open and stop() must still return.
  • Python: .venv/bin/python -m pytest tests/unit/cli tests/unit/executors — 174 passed (CLI signal-forwarding glue, CodexExecutor.aclose, ExecutorNodeService.aclose, node _serve finally-aclose).
  • Manual: rebuild the app (./executor-apps/macos/package-app.sh), run a node, then Quit — app exits immediately (no beachball) and pgrep -f "codex app-server" / pgrep -f "newbro executor" return nothing. Repeat several times.

🤖 Generated with Claude Code

plutoless and others added 12 commits June 8, 2026 07:36
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop os.setsid/posix_spawn process-group approach (breaks terminal Ctrl-C,
heavy Swift rewrite). Three-layer signal-forwarding chain instead: Swift
read-handle-close, CLI forwards SIGTERM to node, node closes codex.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch the reader from availableData/read(upToCount:) to POSIX Darwin.read()
so stop() can close the read handle to unblock the reader even when a
grandchild (e.g. codex app-server) keeps the pipe write end open; replace
queue.sync{} drain with a bounded DispatchSemaphore wait so stop() never
blocks forever on the main thread.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace subprocess.run with Popen in run_checked; install SIGTERM+SIGINT
handlers that call _terminate_child (terminate → poll loop → kill) so the
node process is not orphaned when the Mac app stops the CLI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements best-effort async shutdown of all executors that expose an
aclose() hook. Failures in one executor do not block others or the
process exit, ensuring clean process termination in the quit-hang fix.

Implements TDD: test created first, implementation follows, all tests
pass, no new pyright errors.
…meWarning)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@plutoless
plutoless merged commit 75fe6b8 into main Jun 8, 2026
2 checks passed
@plutoless
plutoless deleted the fix/executor-quit-hang branch June 8, 2026 16:10
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.

1 participant