openai / openai/codex-plugin-cc

app-server brokers leak permanently when SessionEnd doesn't fire

Open
#605 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
33.3k
Forks
2.3k
PR merge metrics
No merged PRs in 30d

Description

app-server brokers leak permanently when SessionEnd doesn't fire

Plugin version: openai-codex/codex 1.0.6
Platform: macOS 15 (darwin 25.4.0), Node 25.9.0
Impact: unbounded process accumulation; on one machine, 31 brokers / 162 MB RSS, oldest 8 days, across 9 checkout roots. 19 of them were serving a --cwd that no longer existed.

Summary

app-server-broker.mjs has no path to self-termination, and the only thing that reaps it is the SessionEnd hook, which is scoped to a single cwd and does not fire on abnormal termination. When a git worktree is deleted, no SessionEnd ever runs for that cwd, so the broker outlives its own working directory — permanently.

Two independent causes

1. The broker cannot end itself.

scripts/app-server-broker.mjs:225-228 — the socket close handler removes the socket from the set and clears ownership, but never checks whether the set is now empty:

socket.on("close", () => {
  sockets.delete(socket);
  clearSocketOwnership(socket);
});

There is no idle timeout and no last-client-disconnect exit. The only exits are the broker/shutdown RPC (:160-163), SIGTERM/SIGINT (:236-243), or a startup throw (:249-251). Because server.listen() on the unix socket holds the event loop open, a broker whose last client disconnected stays alive indefinitely.

2. The only reaper is SessionEnd, and it is cwd-scoped.

scripts/session-lifecycle-hook.mjs:83-113 does the teardown correctly — sendBrokerShutdown, then teardownBrokerSession({ ..., killProcess: terminateProcessTree }), then clearBrokerSession(cwd). But it only ever runs for input.cwd, and only when the hook fires at all.

handleSessionStart (:77-81) is three appendEnvVar calls — there is no startup sweep:

function handleSessionStart(input) {
  appendEnvVar(SESSION_ID_ENV, input.session_id);
  appendEnvVar(TRANSCRIPT_PATH_ENV, input.transcript_path);
  appendEnvVar(PLUGIN_DATA_ENV, process.env[PLUGIN_DATA_ENV]);
}

So any path that skips SessionEnd — a crash, a SIGKILL, machine sleep, closing the terminal, or git worktree remove deleting the cwd out from under a running broker — strands the process with nothing that will ever collect it.

Reproduction

  1. In a git repo, git worktree add .worktrees/tmp-leak -b tmp-leak
  2. Start a Claude Code session with cwd inside that worktree and invoke anything that starts the broker.
  3. Without ending the session cleanly, git worktree remove .worktrees/tmp-leak --force
  4. ps -eo pid,command | grep app-server-broker — the broker is still running, --cwd pointing at the now-deleted directory.
  5. It never exits.

Secondary impact

Tooling that refuses to remove a worktree while a process is using it will block on these forever, so the leak silently obstructs worktree hygiene. Every blocked removal we investigated turned out to be a leaked broker rather than real work.

Suggested fixes (either would be sufficient; the first is the smaller change)

a. Exit when the last client disconnects. In socket.on("close"), if sockets.size === 0, start a grace timer (say 60s) and shutdown() if nothing reconnects. An unref()'d timer keeps this from holding the loop open on its own.

b. Sweep at startup. Have handleSessionStart enumerate broker session records and tear down any whose cwd no longer exists. This also cleans up leaks from earlier crashes, which (a) cannot do retroactively.

A third, cheap defensive option: have the broker check periodically that its own --cwd still exists and exit if it does not. A broker whose working directory has been deleted can no longer be doing useful work.

Local workaround

We run a SessionStart sweeper that lists brokers, keeps only those whose --cwd no longer exists, re-verifies identity (same pid, same script, same --cwd, still missing) immediately before signalling to avoid pid-recycling hazards, and sends SIGTERM. Happy to contribute it if useful.

Note for anyone else hitting this: a broker whose --cwd still exists may be perfectly healthy but idle. "No jobs running" is not evidence a broker is dead — the deleted-cwd test is the one that actually distinguishes them.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with scripts/app-server-broker.mjs:225-228 and inspect its shutdown paths, socket lifecycle, and --cwd handling. Then read scripts/session-lifecycle-hook.mjs:77-113 to understand the existing SessionEnd teardown and session-start behavior. Done means brokers do not remain indefinitely after their last client disconnects or after their working directory is deleted, while healthy idle brokers remain unaffected.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
backend, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.