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`
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
/codex:cancelis a silent no-op. The job haspid: 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-editingstate.json. This is adjacent to #423 and #647, but neither covers thepid: nullcase: those concerntaskkillfailing against a dead but non-null pid.- The stop-review gate turns this into a session-level block. With
stopReviewGateenabled,stop-review-gate-hook.mjsruns this same path underspawnSyncwith a 15-minute timeout. A malformed--basein 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
- 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 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