openai / openai/codex-plugin-cc
codex-rescue agent's contradictory background contract runs the companion in foreground — job orphaned at subagent turn end, permanently stuck at 'running'
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 33.3k
- Forks
- 2.3k
- PR merge metrics
- No merged PRs in 30d
Description
Bug report: codex-rescue agent's contradictory background contract runs the companion in foreground — job orphaned at subagent turn end, permanently stuck at running
Component: openai-codex Claude Code plugin v1.0.6 (commit db52e28), agents/codex-rescue.md + scripts/codex-companion.mjs + scripts/lib/{tracked-jobs,job-control}.mjs
Platform: macOS (darwin 24), Node v20.19.6, codex-cli 0.144.1, Claude Code harness
Severity: High — any substantial task routed through the codex:codex-rescue agent silently loses its result; the job record is stuck at status: "running" forever and /codex:result can never retrieve it.
Related: #478 (no PID-liveness reaping — the persistence half of this failure), #458 (state.json race). This issue is about the trigger: the rescue agent's execution contract makes the orphan inevitable.
What happened
A Claude Code session dispatched a large read-only PR review to the codex:codex-rescue subagent. Three attempts in one session, all lost the same way:
| Attempt | Lifetime | How it died |
|---|---|---|
| 1 | ~117s | foreground task call killed at the host's Bash tool timeout (~120s) |
| 2 | ~56s+ | same, killed before the next attempt started |
| 3 (pid 3602) | ~174s | agent wrapped the foreground task call in a host-side background shell, then ended its turn; the harness reaped the shell and the companion with it |
Attempt 3 completed its entire read phase (git diff, every file) and died mid-turn before any final message. Per the agent's instructions it reported nothing about attempts 1–2, and for attempt 3 told its caller "I'll be notified when it completes" — a notification mechanism that does not exist across a completed subagent's boundary.
End state: job JSON frozen at status: "running" with a dead pid, /codex:status shows a phantom running job indefinitely, /codex:result throws Job … is still running forever.
Forensic evidence that the foreground path was used
The orphaned job record has no request field and never passed through queued — both are written only by enqueueBackgroundTask() (scripts/codex-companion.mjs ~L684–697). And the host-side task log contains the [codex]-prefixed stderr progress stream that only runForegroundCommand() emits (stderr: !options.json, ~L658–663). So the properly daemonized worker path — spawnDetachedTaskWorker() with detached: true, stdio: "ignore", unref() (~L671–682), which would have survived every teardown — was never engaged.
Root cause: every instruction-compliant path fails
agents/codex-rescue.md gives the model a contract with no valid move for a long task:
- L23–24: prefer foreground for small tasks, "prefer background execution" for complicated/long ones.
- But
--backgroundis defined as Claude-side control (commands/rescue.md~L16) and the runtime skill orders it stripped before callingtask(skills/codex-cli-runtime/SKILL.md~L26) — so the agent never passes--backgroundto the companion. - L27–28: the agent must not "monitor progress, poll status, fetch results" and must not call
status,result, orcancel— so the plugin's own launch → poll → fetch machinery (status --wait,/codex:result) is forbidden to it. - L42: "If the Bash call fails or Codex cannot be invoked, return nothing" — which silently swallows the timeout kills.
Foreground blocking dies at the host Bash timeout for any real task; companion-level background is stripped; polling is banned. The model improvises the one remaining option — host-side backgrounding of a foreground companion — which is the worst case: lifetime-coupled to the subagent's turn, with nobody left to poll.
Two mechanical defects then make the loss permanent:
- No signal handling in the companion.
runTrackedJob()(scripts/lib/tracked-jobs.mjs~L142–203) reaches a terminal status only via normal completion or a caught exception; SIGTERM bypasses the only writer ofstatus: "failed". - No dead-worker reconciliation (as in #478):
resolveResultJob()(scripts/lib/job-control.mjs~L256–271) accepts onlycompleted/failed/cancelledand reports active records as "still running" — permanently, since nothing ever flips them.
A related race worth noting while in this code: enqueueBackgroundTask() spawns the detached worker before persisting the job/request file, so the worker can win the race, find no request, and exit — leaving a permanently queued record.
Suggested fix
Short term (documentation-level, no runtime changes):
- Rewrite
agents/codex-rescue.mdso the agent launches non-trivial tasks withtask --background, then loops boundedstatus <jobId> --wait --timeout-ms 120000calls until the job is terminal, then returnsresult <jobId>output verbatim. Permitstatus/resultfor the job the agent itself launched. Replace "return nothing" with returning the job id plus failure detail. Explicitly forbid host-side backgrounding of a foregroundtaskcall.
Longer term (runtime):
- Make every task execute in a durable (detached) worker; "foreground" should mean wait on that worker, so the caller never owns execution. Persist the job/request atomically before spawn and require a startup ack.
- Install SIGTERM/SIGINT handlers that terminalize the job record (and best-effort interrupt the Codex turn) before exit.
- Reconcile dead workers on the
status/result/resumeread paths (per #478), guarding against pid reuse.
Workaround for affected users
The Codex thread usually survives the companion's death. Read threadId from the stuck job JSON under ~/.claude/plugins/data/codex-openai-codex/state/<workspace-hash>/jobs/ and resume it directly:
codex exec --skip-git-repo-check --sandbox read-only -o /tmp/out.md --color never resume <threadId> "Finish the task and emit your final answer."
For write-capable jobs, confirm the original turn is actually dead first (a broker-hosted turn can outlive its companion) to avoid racing it.
Diagnosed with Claude Code against the v1.0.6 source and on-disk job records; analysis independently cross-checked with the Codex CLI (codex exec, read-only) against the same source. Line numbers are from the v1.0.6 release and marked approximate (~) where the reviewer should re-anchor.
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 agents/codex-rescue.md, then read commands/rescue.md and skills/codex-cli-runtime/SKILL.md to trace how task arguments are handled. Inspect scripts/codex-companion.mjs and the tracked-jobs and job-control modules for the failure paths. Done means non-trivial tasks use durable background execution with bounded status/result handling, without host-side backgrounding or permanently running job records.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100