openai / openai/codex-plugin-cc

`captureTurn` treats the `error` notification as non-terminal, so a Codex-side failure hangs the turn forever and wedges the job at `status: running`

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

When the app-server emits a bare error notification without a following turn/completed, captureTurn never settles state.completion. The awaiting call at lib/codex.mjs:606 hangs indefinitely, the companion process never exits, and the job record is left at status: "running" with phase: "failed" and pid: null.

This is distinct from #391 (Codex drops terminal events) and #598 (connection drops mid-turn): here the app-server delivers a well-formed, fully diagnostic terminal error, and the client discards it as a progress string.

Root cause

lib/codex.mjs:537-540 — the error case records the error and emits progress, then breaks:

case "error":
  state.error = message.params.error;
  emitProgress(state.onProgress, `Codex error: ${message.params.error.message}`, "failed");
  break;

completeTurn(state, ...) is only ever reached from case "turn/completed" (:552), so state.error is written but nothing resolves or rejects the promise awaited at :606:

return await state.completion;

The "failed" argument to emitProgress sets only the cosmetic phase. Job status is never transitioned, so /codex:status reports the contradictory pair running / failed indefinitely.

Deterministic reproduction

Any --base that resolves to a valid object that is not a commit triggers it, because Codex's reviewer runs merge-base internally. Git's empty-tree hash is the convenient case:

git init -b main repro && cd repro
git commit --allow-empty -m "root"
echo x > a.txt && git add -A && git commit -m "content"

node "$CLAUDE_PLUGIN_ROOT/scripts/codex-companion.mjs" review \
  --cwd "$PWD" --base 4b825dc642cb6eb9a060e54bf8d69288fbee4904 --scope branch

Observed — the command never returns:

[codex] Starting Codex review thread.
[codex] Thread ready (01a04d47-…).
[codex] Codex error: git command `git -c safe.bareRepository=explicit -c core.hooksPath=NUL merge-base <head> 4b825dc…` failed with status exit code: 128: error: object 4b825dc… is a tree, not a commit
fatal: Not a valid commit name 4b825dc…

/codex:status 11 minutes later, with no process still alive:

| review-mtearxsc-xudirz | review | running | failed | 11m 34s | … |

The persisted record (%TEMP%/codex-companion/<workspace-hash>/state.json):

{
  "id": "review-mtearxsc-xudirz",
  "status": "running",
  "phase": "failed",
  "pid": null,
  "startedAt": "2026-08-29T11:28:44.096Z",
  "updatedAt": "2026-08-29T11:29:06.360Z"
}

updatedAt freezes 22s in and never advances.

Contrast: the turn/completed path works correctly

A usage-limit failure on the same repo minutes later terminated cleanly, because that error arrives as a completed turn with a failure status rather than as a bare error notification:

[codex] Codex error: You've hit your usage limit. …
[codex] Turn failed.

and the record correctly reached "status": "failed" with a completedAt. So the defect is specific to the error-notification path, not to error handling generally.

Knock-on effects

  1. /codex:cancel is a silent no-op. The job has pid: null — nothing was ever spawned — so there is no process to terminate and the handler returns without transitioning status or printing anything. The wedged record can only be cleared by hand-editing state.json. This is adjacent to #423 and #647, but neither covers the pid: null case: those concern taskkill failing against a dead but non-null pid.
  2. The stop-review gate turns this into a session-level block. With stopReviewGate enabled, stop-review-gate-hook.mjs runs this same path under spawnSync with a 15-minute timeout. A malformed --base in a gated repo therefore blocks session exit for the full 900s before failing open — see also #611.

Suggested fix

Treat error as terminal: settle the promise in the error case, preserving state.error as the failure reason, so the job reaches failed and the process exits — for example by calling completeTurn with a synthesized failed turn, or by rejecting state.completion and letting runTrackedJob record the failure it already handles for other error paths.

Separately, handleCancel should transition a job whose pid is null to cancelled rather than returning silently, so a wedged record is recoverable without hand-editing state.

Environment

Plugin 1.0.6 (db52e28)
codex-cli 0.147.0
Node v24.18.0
Git 2.55.0.windows.2
OS Windows 11 26200
Host Claude Code

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 in lib/codex.mjs:537-540 and trace the completion awaited at line 606, then inspect the cancellation path for jobs with a null pid. Reproduce with the provided empty-tree --base command and verify that a bare error settles the turn, records a failed job, and that cancellation can recover a null-pid job.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
backend, cli
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.