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

Open
#540 3 comments 1 reaction 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

Environment

  • Plugin: codex@openai-codex v1.0.6 (repo openai/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:

  1. SessionEnd assumes one session per workspace. scripts/session-lifecycle-hook.mjs:101 (handleSessionEnd) calls sendBrokerShutdown() on the broker recorded in the shared per-workspace broker.json, with no check whether other live sessions (or in-flight jobs) are using it. The broker's shutdown() then calls socket.end() on every connected client (scripts/app-server-broker.mjs:104), including another session's mid-stream turn.

  2. A transport drop mid-turn is a silent exit, not an error. captureTurn awaits state.completion (scripts/lib/codex.mjs:606), a bare promise resolved only by a turn/completed notification (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/start already resolved) — so nothing rejects, no catch runs, runTrackedJob never records a failure. The closed socket was the last ref'd handle (the completion timer is unref()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).

  3. 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 shows running with growing elapsed indefinitely. Bonus: when result <job-id> is called for a running/dead job, matchJobReference throws No job found for "<id>" before the intended Job … is still running branch in resolveResultJob is reached (the predicate filters to finished jobs and matchJobReference throws 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 after Command completed: … (exit 0); recorded pid dead; job file frozen at status: running, phase: verifying; status still 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 only handleSessionEnd produces — 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.completion against the client's exit/close promise) — never a silent exit 0 that leaves status: "running".
  • status/result should detect a dead worker (pid liveness check) and report the job as failed/orphaned instead of running forever; 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 failed with an error line in the log.
  • Killing the companion process mid-turn (SIGTERM/SIGKILL) leaves a job that status reports as dead/failed after the fact, not running.
  • result <job-id> for a running job says "still running", not "No job found".
  • Regression test: broker shutdown() with a client mid-stream → client's captureTurn rejects (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

  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 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.