openai / openai/codex-plugin-cc

Turn capture can hang forever when the app-server connection drops mid-turn

Open
#598 1 comment 1 reaction 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

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

  1. captureTurn() ends with return await state.completion (scripts/lib/codex.mjs:606). That promise is settled only by completeTurn(), which is driven by turn notifications.
  2. handleExit() (scripts/lib/app-server.mjs:163) rejects only entries in this.pending — i.e. in-flight JSON-RPC requests — and then resolves the exit promise. turn/start has already resolved by this point, so it is not in this.pending. Nothing rejects state.completion.
  3. The only fallback timer, scheduleInferredCompletion() (scripts/lib/codex.mjs:373), returns early unless state.finalAnswerSeen is 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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.