fix: executor Mac app quit hangs (and orphaned codex processes)#76
Merged
Conversation
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>
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
Quitting "Newbro Executor.app" hung every time (beachball, never exits), and orphaned
codex app-server/newbro executorprocesses 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'sNodeProcessreader never hit EOF andstop()'squeue.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):NodeProcess): reader uses POSIXread();stop()closes the read fd to unblock it regardless of any surviving grandchild, and uses a bounded drain instead ofqueue.sync {}.quit()runsstopAll()off the main thread with a 3s deadline so the UI never freezes.newbro executor run/run_checked): now usesPopenand installs SIGTERM/SIGINT handlers that forwardterminate()→ (bounded) →kill()to the node child._serve): handles SIGTERM/SIGINT, cancels the run loop, and alwaysawait service.aclose()in afinally→CodexExecutor.aclose()→ closes the app-session → terminates thecodex app-serverchild.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 test --package-path executor-apps/macos— 118 passed, incl. a regression test where a grandchild holds the pipe open andstop()must still return..venv/bin/python -m pytest tests/unit/cli tests/unit/executors— 174 passed (CLI signal-forwarding glue,CodexExecutor.aclose,ExecutorNodeService.aclose, node_servefinally-aclose)../executor-apps/macos/package-app.sh), run a node, then Quit — app exits immediately (no beachball) andpgrep -f "codex app-server"/pgrep -f "newbro executor"return nothing. Repeat several times.🤖 Generated with Claude Code