openai / openai/codex-plugin-cc
Turn capture can hang forever when the app-server connection drops mid-turn
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 33.3k
- Forks
- 2.3k
- PR merge metrics
- No merged PRs in 30d
Description
Summary
If the app-server connection drops after turn/start has returned but before the turn's completion notification arrives, captureTurn() awaits a promise that is never settled. The process stays alive, produces no further output, and never returns — the job's tracked status stays running indefinitely.
Verified by reading v1.0.6 (current main).
Mechanism
captureTurn()ends withreturn await state.completion(scripts/lib/codex.mjs:606). That promise is settled only bycompleteTurn(), which is driven by turn notifications.handleExit()(scripts/lib/app-server.mjs:163) rejects only entries inthis.pending— i.e. in-flight JSON-RPC requests — and then resolves the exit promise.turn/starthas already resolved by this point, so it is not inthis.pending. Nothing rejectsstate.completion.- The only fallback timer,
scheduleInferredCompletion()(scripts/lib/codex.mjs:373), returns early unlessstate.finalAnswerSeenis true. If the connection dies before the final answer arrives, that flag is never set, so the timer is never armed.
Net effect: connection loss in this window is unobservable to the awaiting caller. Because runTrackedJob() only writes a terminal status from its catch block, the job record is left at running forever, and /codex:status keeps reporting it as running (it reads the persisted JSON without any liveness check).
Observed impact
We hit this twice in one day on a self-hosted relay: two review turns went completely silent after their last tool call — 52 minutes and 33 minutes — at the same point, with the process alive and consuming nothing. Both had to be cancelled manually.
The failure is indistinguishable from "still working" to any caller polling status, so no one investigates; they just keep waiting.
Suggested fix
Race the completion promise against connection termination, e.g. in captureTurn():
return await Promise.race([
state.completion,
client.exitPromise.then(() => {
throw new Error("codex app-server connection closed before the turn completed.");
}),
]);
Alternatively, have handleExit() reject any registered turn-capture states in addition to this.pending. Either way the rejection propagates to runTrackedJob(), which then writes a terminal failed status through its existing path.
A wall-clock ceiling on a turn that has received no notifications at all would also bound the failure, but is a weaker fix — it converts an unbounded hang into a long one.
Caveat
We have not deterministically reproduced the connection drop; the code path above was found by reading, and it matches the observed symptom exactly. Happy to test a patch against our setup.
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 captureTurn() and scheduleInferredCompletion() in scripts/lib/codex.mjs, then trace handleExit() and exitPromise in scripts/lib/app-server.mjs. Exercise the app-server disconnect path after turn/start returns; done means captureTurn() no longer waits forever and runTrackedJob() records a terminal failed status.
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
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100