openai / openai/codex-plugin-cc

SessionEnd cannot find the broker when CLAUDE_PLUGIN_DATA differs between spawn and teardown — same cwd, same hash, different state root

Open
#636 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

resolveStateDir() picks the state root from CLAUDE_PLUGIN_DATA, falling back to
$TMPDIR/codex-companion when that variable is absent. The workspace slug and hash are
identical in both cases — only the root differs. When a broker is registered while
the variable is unset and SessionEnd later runs with it set (or vice versa),
loadBrokerSession(cwd) reads the other root, finds nothing, and the broker is
orphaned permanently.

This is not #380. There, a different cwd produces a different hash. Here the cwd and
the hash are identical — wienerdog-cc76dfe898ef0cd4 exists in both roots on my
machine — and the lookup still misses.

Mechanism

scripts/lib/state.mjs:

const FALLBACK_STATE_ROOT_DIR = path.join(os.tmpdir(), "codex-companion");
// ...
const pluginDataDir = process.env[PLUGIN_DATA_ENV];      // CLAUDE_PLUGIN_DATA
const stateRoot = pluginDataDir ? path.join(pluginDataDir, "state") : FALLBACK_STATE_ROOT_DIR;
return path.join(stateRoot, `${slug}-${hash}`);

Same cwd, two answers:

$ CLAUDE_PLUGIN_DATA=~/.claude/plugins/data/codex-openai-codex \
    node -e 'import("…/lib/state.mjs").then(m=>console.log(m.resolveStateDir("…/wienerdog")))'
/Users/…/.claude/plugins/data/codex-openai-codex/state/wienerdog-cc76dfe898ef0cd4

$ env -u CLAUDE_PLUGIN_DATA \
    node -e 'import("…/lib/state.mjs").then(m=>console.log(m.resolveStateDir("…/wienerdog")))'
/var/folders/…/T/codex-companion/wienerdog-cc76dfe898ef0cd4

There is no second chance. handleSessionEnd falls back to the endpoint env var:

const brokerSession =
  loadBrokerSession(cwd) ??
  (process.env[BROKER_ENDPOINT_ENV] ? { endpoint: process.env[BROKER_ENDPOINT_ENV], … } : null);

but CODEX_COMPANION_APP_SERVER_ENDPOINT is never exported to the hook environment —
handleSessionStart propagates only CODEX_COMPANION_SESSION_ID,
CODEX_COMPANION_TRANSCRIPT_PATH and CLAUDE_PLUGIN_DATA via appendEnvVar. So once
the root lookup misses, endpoint is null, no broker/shutdown is sent, and
teardownBrokerSession gets nothing to kill.

Evidence from a real machine

One workspace, split across both roots:

A) $TMPDIR/codex-companion/wienerdog-cc76dfe898ef0cd4/
   broker.json   Jul 19 02:12     <- pid 68646
   jobs/         Jul 19 19:58     (64 entries)
   state.json    Jul 19 20:01     (32 KB)

B) ~/.claude/plugins/data/codex-openai-codex/state/wienerdog-cc76dfe898ef0cd4/
   jobs/         Aug 10 00:35     (empty)
   state.json    Aug 10 00:35     (80 bytes)
   (no broker.json)

broker.json in root A still records "pid": 68646, and that process is alive after
23 days
, with zero clients connected (verified via lsof on its unix socket: only the
broker itself holds the listening fd).

The 64 job entries and the 32 KB state.json in root A show this was not a
first-run artifact — real work ran against the fallback root for ~18 hours.

Inferred sequence (consistent with all timestamps, not directly observed): the
workspace was revisited on Aug 10 with CLAUDE_PLUGIN_DATA set. loadBrokerSession(cwd)
resolved to root B, found nothing, so existing was nullensureBrokerSession
skipped the teardown branch entirely and spawned a second broker. That one was reaped
normally at SessionEnd (no broker.json remains in root B), while pid 68646 in root A
was never referenced again by any code path.

So the root split does not merely orphan a broker — it also causes a duplicate broker to
be spawned for the same workspace, since ensureBrokerSession cannot see the existing one.

Why the existing issues don't cover this

  • #380 — cwd mismatch → different hash. Here the hash matches exactly; the root differs.
  • #543 / #108 — broker never self-terminates / no idle timeout. Correct and complementary:
    an idle timer would eventually reap this orphan, but the lookup defect would still cause
    duplicate brokers and split job state on every affected session.
  • #288sendBrokerShutdown has no timeout. Different failure point; here the shutdown
    is never attempted at all because endpoint is null.

Suggested fixes

  1. Make the state root deterministic. Resolve it once and record the chosen root inside
    broker.json, or drop the CLAUDE_PLUGIN_DATA conditional and always use a fixed
    location. A root that depends on ambient environment cannot be relied on across a
    process boundary.
  2. Fall back across roots on lookup. loadBrokerSession(cwd) could check the alternate
    root when the primary misses. Cheap, and fixes existing orphans on the next session.
  3. Export the broker endpoint to the hook environment so the BROKER_ENDPOINT_ENV
    fallback in handleSessionEnd is actually reachable. Today it is dead code for hooks.
  4. Independently, the idle self-exit proposed in #543 would bound the damage from this and
    every other lookup-side failure.

Environment

  • macOS (darwin 25.5.0), Node v24.18.0
  • codex plugin 1.0.6 (openai/codex-plugin-cc)
  • 9 orphaned broker chains total on this machine, ~5.1 GB RSS, 0 clients on all 9

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/lib/state.mjs, especially resolveStateDir() and loadBrokerSession(cwd), then trace handleSessionStart, handleSessionEnd, and appendEnvVar to understand how state roots and broker endpoints move between hooks. Done means a broker registered under either environment setup is found and shut down at SessionEnd without creating a duplicate broker.

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
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.