Stop orphaned Codex companion brokers#490
Conversation
A shared app-server broker outlives the Claude Code session that spawned it. When the session exits without a clean teardown the broker is reparented to init and stays listening forever, holding its `codex app-server` child open. These accumulate: on one developer machine, 8 orphaned brokers and their children held ~1.3 GB RSS, the oldest alive for 41 hours. Shut the broker down after 15 minutes with no connected clients. Foreground and background jobs hold a connection for their whole lifetime, so an active job always keeps its broker alive; `CODEX_COMPANION_BROKER_IDLE_TIMEOUT_MS` overrides the window and `0` disables the timer. `ensureBrokerSession()` already probes the endpoint and respawns when it finds none, so an idle-exited broker is transparently recreated on the next call. Also close the listener before awaiting async child cleanup, and reject connections that arrive during shutdown, so a reconnect cannot slip into the shutdown window and inherit a closing app-server client. Tests run with a 2s idle window so the suite does not leave detached helpers.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: da16256dbd
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| await shutdown(server); | ||
| process.exit(0); |
There was a problem hiding this comment.
Clear stale broker session on idle exit
In the idle-timeout path, the broker exits after removing only the socket/pid runtime files, leaving the saved broker session in broker.json. After the normal idle timeout, loadBrokerSession(cwd) still points at the deleted socket; /codex:setup then calls getCodexAuthStatus() with reuseExistingBroker: true, attempts that dead endpoint, and reports the workspace as not ready with connect ENOENT instead of starting or reporting a fresh direct runtime. Clear the broker session when this self-shutdown path runs, or make the setup/status path validate and discard stale sessions.
Useful? React with 👍 / 👎.
|
Nice — this overlaps directly with a race I already caught and patched on the fork: the shutdown window where a pid can be reused. Your "close the listener before awaiting async child cleanup" and "reject connections delivered after One thing worth pressure-testing: the interaction between this idle-timer and an active respawn. |
Problem
The shared app-server broker outlives the Claude Code session that spawned it. When a session exits without a clean teardown, the broker is reparented to
initand keeps listening forever, holding itscodex app-serverchild open with it. Nothing ever reaps them, so they accumulate for as long as the machine stays up.On one developer machine this had reached 8 orphaned brokers, ~1.3 GB RSS, the oldest alive for 41 hours — every one with
PPID=1, i.e. no session attached. Running the plugin's own test suite makes it worse, since each run leaves its helper brokers behind too.Fix
Shut the broker down after 15 minutes with no connected clients.
CODEX_COMPANION_BROKER_IDLE_TIMEOUT_MSoverrides the window;0disables the timer entirely.ensureBrokerSession()already probes the endpoint and respawns when it finds none, so an idle-exited broker is transparently recreated on the next call. A successful probe also resets the idle timer, so there is no window where a client can connect to a broker that is about to exit.Two shutdown-path corrections come with it:
server.close(), rather than adding them to the live socket set.Tests
Adds
tests/broker-idle.test.mjs, covering: exit + runtime-file cleanup after the last client disconnects; idle shutdown deferred while a client is still connected; the timeout being disabled via0; and clean shutdown while new clients race to connect.tests/fake-codex-fixture.mjsnow sets a 2s idle window so the suite stops leaving detached helpers behind.Full suite: 91 passing. The 4 failures on this branch (
status/result/resolveStateDir) reproduce identically on an unmodifiedmain, so they are pre-existing and unrelated.