openai / openai/codex-plugin-cc
Ending any Claude session kills the shared broker mid-turn: concurrent sessions' tasks die silently (exit 0) and stay "running" forever
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 33.3k
- Forks
- 2.3k
- PR merge metrics
- No merged PRs in 30d
Description
Environment
- Plugin:
codex@openai-codexv1.0.6 (repoopenai/codex-plugin-cc) - Claude Code on Linux (Fedora, Node 24)
- Multiple concurrent Claude Code sessions in the same workspace (interactive sessions plus occasional short-lived/headless ones)
Summary
When any Claude session in a workspace ends, the SessionEnd hook unconditionally shuts down the workspace's shared broker. If another session's rescue task is streaming a turn through that broker at that moment, its companion process exits silently with code 0 mid-turn: no error, no failed state — the job log just stops after an ordinary event line. The job file stays status: "running" forever (nothing ever pid-checks it), and the stale record then blocks all further tasks on that session ("Task … is still running") until a manual /codex:cancel.
Three defects compose into this:
-
SessionEndassumes one session per workspace.scripts/session-lifecycle-hook.mjs:101(handleSessionEnd) callssendBrokerShutdown()on the broker recorded in the shared per-workspacebroker.json, with no check whether other live sessions (or in-flight jobs) are using it. The broker'sshutdown()then callssocket.end()on every connected client (scripts/app-server-broker.mjs:104), including another session's mid-stream turn. -
A transport drop mid-turn is a silent exit, not an error.
captureTurnawaitsstate.completion(scripts/lib/codex.mjs:606), a bare promise resolved only by aturn/completednotification (resolveCompletion,lib/codex.mjs:304-310). It is never raced against the client's exit. On socket close,handleExit(scripts/lib/app-server.mjs:163) rejects only pending request promises — during streaming there are none (turn/startalready resolved) — so nothing rejects, no catch runs,runTrackedJobnever records a failure. The closed socket was the last ref'd handle (the completion timer isunref()d,lib/codex.mjs:393), so the event loop drains and Node exits 0 mid-await. The same silent death occurs for any transport loss: direct-mode app-server child crash, broker crash, or the client being SIGTERMed (no signal handler updates job state either). -
Nothing ever detects the corpse.
status/result(scripts/lib/job-control.mjs) render whatever the job file says without checking pid liveness, so the job showsrunningwith growingelapsedindefinitely. Bonus: whenresult <job-id>is called for a running/dead job,matchJobReferencethrowsNo job found for "<id>"before the intendedJob … is still runningbranch inresolveResultJobis reached (the predicate filters to finished jobs andmatchJobReferencethrows on no match), which makes diagnosing this even more confusing.
Evidence (observed corpse, twice, different sessions)
- Job
A(session 1): created 06:52:50, log ends 06:52:57 right afterCommand completed: … (exit 0); recorded pid dead; job file frozen atstatus: running,phase: verifying;statusstill showed it "running / 12m" long after. - Job
B(session 2): created 07:18:04, log ends 07:19:13 the same way. The Bash call that ran the foreground companion returned normally after ~94 s with a clean exit (no timeout, no error output beyond relayed progress lines) — i.e., the process self-exited mid-turn. - Every
/tmp/cxc-*broker session dir created during that window was erased completely (socket + pid + log + dir +broker.json) — the full-erase combination onlyhandleSessionEndproduces — while jobs were in flight; sessions were being opened/closed concurrently in this workspace throughout. - A later failure ("Selected model is at capacity") failed loudly with
Turn failed— showing the error path works when a rejection actually reaches the turn; the silent deaths never produce any error line.
Related but distinct: #526 (idle-timeout shutdown race, broker-side), #509 (stale broker reuse → hang at Turn started), #450/#380 (orphan brokers on missing/failed SessionEnd), #286 (same-cwd state races). None of them covers the SessionEnd-of-a-concurrent-session → mid-turn client kill → silent exit-0 → permanently-running job chain.
Expected behavior
- Ending one Claude session must not kill in-flight work of other sessions sharing the workspace broker (refcount sessions, or refuse/defer shutdown while a foreign turn is streaming, or scope brokers per session).
- A transport loss mid-turn must surface as a job failure (race
state.completionagainst the client's exit/close promise) — never a silent exit 0 that leavesstatus: "running". status/resultshould detect a dead worker (pid liveness check) and report the job as failed/orphaned instead ofrunningforever; a dead job must not block subsequent tasks until manual cancel.
Acceptance criteria
- With two sessions in one workspace, ending session X while session Y streams a rescue turn: Y's task either completes normally (broker survives) or fails fast with an explicit transport error — job state transitions to
failedwith an error line in the log. - Killing the companion process mid-turn (SIGTERM/SIGKILL) leaves a job that
statusreports as dead/failed after the fact, notrunning. result <job-id>for a running job says "still running", not "No job found".- Regression test: broker
shutdown()with a client mid-stream → client'scaptureTurnrejects (no silent event-loop-drain exit).
Workaround
Setting CODEX_COMPANION_APP_SERVER_ENDPOINT to an unreachable socket forces the ECONNREFUSED → direct-mode fallback in withAppServer, giving each task a private app-server that other sessions' SessionEnd cannot kill (at the cost of per-task startup).
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 with handleSessionEnd in scripts/session-lifecycle-hook.mjs and shutdown() in scripts/app-server-broker.mjs, then trace captureTurn and handleExit in scripts/lib/codex.mjs and scripts/lib/app-server.mjs. Read job-control.mjs for status/result behavior. Use the acceptance criteria to verify concurrent-session shutdown, transport-loss failure handling, dead-worker reporting, and the running-job result message.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- backend, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100