openai / openai/codex-plugin-cc

Session end//clear tears down workspace-shared broker, silently losing other sessions' running jobs

Open
#612 1 comment 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

Summary

With multiple concurrent Claude Code sessions in one workspace, any session ending (including /clear) tears down the workspace-shared broker while other sessions' background jobs are mid-turn. The in-flight Codex turn is aborted, the detached worker then hangs forever on the dead socket, and once external cleanup reaps the orphan, waiters polling status <job-id> get No job found — the job vanishes without any terminal record. Three background review jobs were lost this way in one afternoon (plugin v1.0.6, macOS, companion state under CLAUDE_PLUGIN_DATA).

Reproduction (timeline from a live incident, timestamps UTC)

Two Claude Code sessions, A and B, same workspace.

  1. Session A dispatches a background task (codex-companion task --background), verifies it: status shows running, worker pid alive, threadId assigned. A's wait loop (status --wait --poll-interval-ms 30000) polls it healthy for 2 minutes.
  2. 19:19:25.015 — session B runs /clear.
  3. /clear fires the plugin's SessionEnd hook (session-lifecycle-hook.mjs ~83–114): sendBrokerShutdown(endpoint) → broker broker/shutdown (app-server-broker.mjs ~160–164) → appClient.close() → SIGTERM to the codex app-server (app-server.mjs ~259). The rollout file records turn_aborted reason "interrupted" at 19:19:25.146 — 131 ms after the /clear. The hook then deletes broker.json, socket, pid file, and session dir.
  4. Session A's worker process survives (spawned detached + unref()), but hangs forever on the dead broker socket — it never writes a failed record.
  5. External cleanup (a liveness-gated reaper running from a SessionStart hook) correctly identifies broker-dead + idle worker, kills the orphan, and removes the job record.
  6. Session A's next wait poll returns exit 1, No job found — from its perspective a verified, running job ceased to exist with no cause recorded.

Jobs whose runtime happened to overlap no session end//clear completed normally — every background job in a multi-session workspace is a race against any session ending during its runtime.

Underlying defects (v1.0.6 line references)

  1. SessionEnd treats shared state as session-owned. The broker and state.json are per-workspace and serve all sessions, but any single session's SessionEnd shuts the broker down and tears down the state dir without checking state.json for active jobs belonging to other sessionIds (session-lifecycle-hook.mjs ~100–113). This is the root cause.
  2. Workers do not fail fast on broker death. When the broker socket closes mid-turn, the worker hangs indefinitely instead of erroring so runTrackedJob's catch (tracked-jobs.mjs ~181–202) can write a terminal failed record. The hang means the job's fate is decided by whoever cleans up orphans later, with no trace.
  3. cancel interrupts before persisting. handleCancel calls interruptAppServerTurn before its first state/log write (codex-companion.mjs ~976–987), so a crash between the two (observed: EPERM on the log append in a restricted environment) interrupts the turn while recording nothing.

Suggested fixes

  • SessionEnd: before sendBrokerShutdown, scan state.json for running/queued jobs with a different sessionId; if any exist, skip broker/state teardown (or refcount attached sessions). Likewise cleanupSessionJobs should mark its own session's jobs failed/cancelled rather than deleting their records and files.
  • Worker: treat broker-socket closure mid-turn as a fatal error so a failed record with a cause is always written.
  • cancel: persist the cancelled state first, best-effort log appends after.

A threadId handle survives in the rollout file even when the job record is erased, which is what made this incident reconstructible — but callers only have that if they captured it before the loss, which is a fragile place to keep the only durable reference.

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

Trace session-lifecycle-hook.mjs around lines 83–114 and app-server-broker.mjs around 160–164 to understand shared broker teardown, then inspect tracked-jobs.mjs around 181–202 and codex-companion.mjs around 976–987. Reproduce the multi-session /clear timeline and verify that other sessions’ jobs remain represented, broker failure produces a terminal record, and cancel persists state before interruption.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
backend, cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.