Skip to content

codex-rescue can generate duplicate pgrep -f "codex-companion.mjs" wait loops that keep each other alive on macOS (stuck background tasks) #686

Description

@corocojam

Environment

  • Plugin: codex@1.0.6 (marketplace openai/codex-plugin-cc, commit db52e28)
  • Claude Code CLI 2.1.238 on macOS Darwin 25.5.0
  • Rescue subagent model: sonnet
  • Note: the session ran with user-level config (CLAUDE.md, custom hooks). I have not yet reproduced end-to-end in a clean profile, so this report presents an observed occurrence plus an isolated reproduction of the persistence mechanism — not a deterministic repro.

Symptom

After several /codex:rescue --background ... delegations, the session accumulates background tasks that never finish. Their process shape is always:

zsh -c while pgrep -f "codex-companion.mjs" > /dev/null 2>&1; do sleep 5; done; echo "===PROCESS_DONE==="; cat "<claude tasks dir>/<id>.output"

They stay alive after the Codex job (and the companion process) has completed, each showing as a stuck "background task running" entry in the session UI. Five instances observed over two days of normal use.

Observed occurrence (timestamped, from the subagent transcript, 2026-08-25)

Time (UTC) Event
11:09:57 subagent runs codex-companion.mjs task ... (foreground Bash)
11:20:00 harness: "Command did not complete within its 600s timeout and was moved to the background (ID: bankuk08v)"
11:20:49 subagent issues wait loop #1 (while pgrep -f "codex-companion.mjs" ...; cat .../bankuk08v.output)
12:07:08 (later round) wait loop targeting .../bcjiv3vzi.output issued
12:16:59 harness: that wait loop itself "moved to the background (ID: bajyfm7mc)" — still running at this moment
12:17:01 subagent issues a second identical wait loop for the same output file — two seconds later, i.e. within a single sleep 5 iteration of the first
12:26:52 harness: second loop also "moved to the background (ID: b5wz9tl0t)"

Six such auto-backgrounded wait loops accumulated in this one session (IDs bajyfm7mc, b5wz9tl0t, bb662rqqw, by7r8ttif, b322f0nm6, byx7cqzqh), and the stuck-task symptom above matches sustained overlap.

Where the loop comes from

The plugin source at db52e28 contains no pgrep, so the command is model-generated. agents/codex-rescue.md requires the subagent to use exactly one Bash call, return the companion stdout as-is, and forbids polling status / fetching result. Once the companion call is auto-backgrounded at the Bash timeout, there is no compliant way left to "return the stdout as-is", and the model recovers by writing a pgrep wait loop — noncompliant with the letter of the contract, but a predictable failure mode of it: the prompt defines no fallback for the timeout/auto-background case, and the same improvisation recurred across independent runs (8+ wait loops in the transcript above).

Why the loops never exit (isolated mechanism reproduction)

The pgrep pattern codex-companion.mjs appears verbatim inside each wait loop's own zsh -c command line. Verified experimentally on the tested Darwin 25.5 setup:

  • A single loop did not match its own ancestor shell and exited once no companion process remained. This matches the documented BSD pgrep default: "the current pgrep or pkill process and all of its ancestors are excluded" (macOS man pgrep, -a flag description).
  • Two sibling loops matched each other and stayed alive indefinitely after every codex-related process was gone (observed over several minutes; each loop's command line satisfies the other's pgrep). pgrep -f was confirmed to match the full zsh -c argv at least to offset 2000, so the loop body is always visible to a sibling.

This establishes the persistence mechanism whenever two waiters overlap — and the timeline above shows the plugin flow producing exactly that overlap. It does not claim process-name waiting is generally safe in the single-loop case.

Untested expectation (not required for this report): on procps-based Linux, pgrep excludes only itself, so even a single loop is expected to match its own parent shell and never exit.

Potential fixes

  1. Define an explicit timeout/background fallback in the agent contract (agents/codex-rescue.md, skills/codex-cli-runtime/SKILL.md): if the Bash call times out or is moved to the background, return the job handle / harness message immediately; never issue another Bash call and never construct a waiter.
  2. Provide a job-specific blocking await and let the subagent use it (e.g. codex-companion.mjs status <job-id> --wait --timeout-ms ...). Today the "do not call status" rule closes off the safe path while the missing stdout creates the pressure.
  3. Supervise completion inside the companion: task --background self-daemonizes and writes its result to a known file, so no shell-side waiting is needed.

(The bracket pattern pgrep -f "[c]odex-companion.mjs" prevents waiter self/mutual matching, but it is globally scoped — it can wait on unrelated concurrent companion jobs — so it is only a narrow mitigation, not a fix.)

Related

Happy to provide fuller transcript excerpts (with timestamps and task IDs) or run additional experiments if that helps.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions