openai / openai/codex-plugin-cc
app-server brokers never self-terminate → orphaned process/RAM leak (34 chains, 272 procs, ~2.2GB)
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 33.3k
- Forks
- 2.3k
- PR merge metrics
- No merged PRs in 30d
Description
Bug: app-server brokers never self-terminate → orphaned process/RAM leak
Repo: openai/codex-plugin-cc
Version: codex plugin 1.0.6
File: scripts/app-server-broker.mjs (+ context in scripts/lib/broker-lifecycle.mjs, scripts/session-lifecycle-hook.mjs)
Summary
The shared app-server broker has no idle timeout. It exits only on an explicit
broker/shutdown RPC, SIGTERM, or SIGINT. Its only automatic reaper is the
SessionEnd hook, which is fragile. When that hook fails to run, the detached
broker (child.unref()) leaks forever, holding a Codex app-server child plus
its MCP/worker subprocesses.
Observed in the wild: 34 orphaned broker chains, 272 processes, ~2.2 GB RSS,
0 clients connected. 16 chains pointed at already-deleted git worktrees.
Root cause
Two gaps compound:
-
Broker never self-exits. In
app-server-broker.mjs,socket.on("close")
just doessockets.delete(socket)+clearSocketOwnership(socket). There is
no check for "no clients left" and no idle timer. A broker with zero clients
lives indefinitely. -
The reaper is best-effort and misses common cases.
SessionEndin
session-lifecycle-hook.mjsis the intended reaper, keyed per-cwd via
loadBrokerSession(cwd). It fails to reap when:- Claude is hard-killed / crashes → hook never runs.
- The 5s hook
timeout(hooks.json) is exceeded on a busy machine → hook killed. - The worktree is deleted before SessionEnd →
loadBrokerSession(cwd)
resolves to gone state, returnsnull, no shutdown is sent → orphan.
(This matches the 16 deleted-worktree chains.)
Because brokers are spawned detached and keyed to a cwd, any hook miss orphans a
broker that nothing will ever clean up.
Fix
Give the broker an idle self-exit. It is safe: the companion opens one socket
per CodexAppServerClient.connect() and closes it when the job ends
(BrokerCodexAppServerClient.initialize/close in lib/app-server.mjs), so a
broker at zero sockets with no active request/stream is genuinely idle. Killing
an idle broker only costs a cold respawn on the next job — ensureBrokerSession
already checks endpoint readiness and respawns if dead. This makes the broker
self-healing regardless of whether any hook fires, covering hard-kills and
deleted worktrees alike.
Threshold defaults to 15 min, overridable via CODEX_COMPANION_BROKER_IDLE_MS.
Patch (scripts/app-server-broker.mjs)
Add idle state after const sockets = new Set();:
const IDLE_TIMEOUT_MS = Number(process.env.CODEX_COMPANION_BROKER_IDLE_MS ?? 15 * 60 * 1000);
let idleTimer = null;
let serverRef = null;
function isIdle() {
return sockets.size === 0 && !activeRequestSocket && !activeStreamSocket;
}
function clearIdleTimer() {
if (idleTimer) {
clearTimeout(idleTimer);
idleTimer = null;
}
}
function armIdleTimer() {
clearIdleTimer();
if (!(IDLE_TIMEOUT_MS > 0) || !serverRef || !isIdle()) {
return;
}
idleTimer = setTimeout(async () => {
if (!isIdle()) {
return;
}
await shutdown(serverRef);
process.exit(0);
}, IDLE_TIMEOUT_MS);
if (typeof idleTimer.unref === "function") {
idleTimer.unref();
}
}
Cancel the timer on connect (in net.createServer callback, after sockets.add(socket)):
clearIdleTimer();
Re-arm on disconnect (in both socket.on("close") and socket.on("error"),
after clearSocketOwnership(socket)):
armIdleTimer();
Arm at startup and hold the server ref (replace server.listen(listenTarget.path);):
serverRef = server;
server.listen(listenTarget.path, () => {
armIdleTimer();
});
Optional hardening (not required if idle-exit lands)
- Make the
SessionEndhook timeout longer than 5s, or move broker teardown off
the hook's critical path, so slow machines still reap on clean exit. - On
SessionStart, sweepos.tmpdir()for stalecxc-*/broker.pidwhose pid is
dead (leftover session dirs). Redundant once idle-exit ships, but cleans state.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in scripts/app-server-broker.mjs, focusing on socket connect, close, error, and server startup handling; then read scripts/lib/broker-lifecycle.mjs and scripts/session-lifecycle-hook.mjs for lifecycle context. Verify that an idle broker exits after the configurable timeout, remains alive while clients or requests are active, and can be respawned by ensureBrokerSession.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- backend, devtools
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 75/100