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'

Open
#486 3 comments 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

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 --background is defined as Claude-side control (commands/rescue.md ~L16) and the runtime skill orders it stripped before calling task (skills/codex-cli-runtime/SKILL.md ~L26) — so the agent never passes --background to the companion.
  • L27–28: the agent must not "monitor progress, poll status, fetch results" and must not call status, result, or cancel — 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:

  1. 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 of status: "failed".
  2. No dead-worker reconciliation (as in #478): resolveResultJob() (scripts/lib/job-control.mjs ~L256–271) accepts only completed/failed/cancelled and 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.md so the agent launches non-trivial tasks with task --background, then loops bounded status <jobId> --wait --timeout-ms 120000 calls until the job is terminal, then returns result <jobId> output verbatim. Permit status/result for the job the agent itself launched. Replace "return nothing" with returning the job id plus failure detail. Explicitly forbid host-side backgrounding of a foreground task call.

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/resume read 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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.