Repository navigation
Codex run never completes when turn/start returns the previous run's live turn #23016
Description
What happened
On a Codex thread in the desktop nightly (0.1.2-nightly.20261021.3457, gpt-6-astra, Orchestrator V2), a run stayed running for 3.5 hours after the provider had already finished. The thread showed no assistant output for that run, and two later messages queued behind it never started. Nothing cleared on its own; a manual interrupt settled the run, and the queue drained immediately afterwards.
The work itself was done and written to disk, but the final answer never reached the thread timeline.
Diagnosis
Two adapter gaps combine here.
1. Retracting an in-flight run does not terminate its native turn. At 13:45:06Z T3 started Codex turn 01a15eb5… for run 95. At 13:45:11Z the message was retracted, rolling the run back to checkpoint 94 with restoreFiles:false, but no turn/interrupt and no thread/revert reached the provider; neither appears anywhere in the session log. Codex kept the turn inProgress and its stream continued. The rollback effect also carried the thread's placeholder provider id (pending:…:ordinal:1) instead of the Codex native thread id.
CheckpointRollbackService.execute only calls session.rollbackThread when there is something to revert, and ProjectionStore.getCheckpointRollbackContext selects only runs with status completed, interrupted, failed, or cancelled (apps/server/src/orchestration-v2/ProjectionStore.ts#L5089-L5094). A run that is still running is excluded, so a rollback whose target sits at or before the live run has nothing to revert and leaves the provider conversation untouched. Even when a rollback does run, CodexAdapterV2.rollbackThread (apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts#L7354) reverts turns with thread/revert and never interrupts a live turn. #16843 tracks the missing active-run guard; this trace shows the additional consequence of an orphaned live turn.
2. registerRootTurn silently adopts an existing turn id for a new run. At 13:48:17Z the replacement message started run 96 with turn/start. Codex returned the same turn id 01a15eb5… with status: inProgress, which is legal: one turn can stay open on a thread, and the previous prompt in it never ended. The adapter passes started.turn.id to registerRootTurn (CodexAdapterV2.ts#L6354-L6361), and registerRootTurn returns the existing active context when that id is already known (CodexAdapterV2.ts#L2007-L2017), without checking whether the context belongs to the current run. The turn/started handler has the same short-circuit (CodexAdapterV2.ts#L4457-L4466).
From then on every item and the final turn/completed carried turnId 01a15eb5…, the retracted run's turn. The new run received no items and no terminal event, so it stayed running although the provider had completed.
Steps to reproduce
Race-dependent in the UI, deterministic at the adapter seam:
- Start a Codex V2 run (run A). Codex opens turn T.
- While run A is streaming, retract its message (the edit or rewind action that rolls the run back).
- Confirm no
turn/interruptorthread/revertis sent for T, and Codex keeps T in progress. - Send the replacement message (run B).
turn/startreturns T, andregisterRootTurnreuses run A's context. - Wait for
turn/completed. It is attributed to run A's turn, so run B gets nothing and staysrunningindefinitely.
A focused adapter test can drive this without the UI timing: have turn/start return the id of a turn already active for an earlier run, then deliver items and turn/completed carrying that id, and assert that the new run settles.
Version
0.1.2-nightly.20261021.3457 (desktop nightly). Source references are against main at 680152e (2026-10-21); nightly line numbers may differ slightly.
Environment
Arch Linux (kernel 7.2.11-1-cachyos), T3 Code desktop nightly, codex-cli 0.163.0 (codex app-server), model gpt-6-astra, Orchestrator V2.
Evidence
Provider event log for the thread (01a139ae-0064-…), timestamps UTC, paths redacted:
13:45:06.857 outgoing turn/start (run 95; text ends mid-sentence at "good notifications for")
13:45:11.229 rollback effect to checkpoint 94 (restoreFiles:false, placeholder provider id)
no turn/interrupt and no thread/revert in this session
13:48:17.004 outgoing turn/start (run 96; full replacement message)
13:48:17.189 incoming result {"turn":{"id":"01a15eb5-...","status":"inProgress"}}
13:48:17.505 incoming item/started userMessage 01a15eb8-... turnId 01a15eb5-...
13:48:20..14:25:58 every item carries turnId 01a15eb5-...
14:25:59.613 incoming turn/completed turnId 01a15eb5-... with the final answer
17:17:46 manual interrupt of run 96; queued runs start at 17:17:47
The thread timeline contained no items for run 96 between the user message and the interrupt, so the assistant message was never recorded there. The same native turn id served both runs throughout.
Related issues
#16843 (related, not duplicate: asks whether to refuse a rollback; this also needs the rollback to terminate the provider turn or the next run to stop adopting it), #22510 (accepted Claude analogue: one native turn serving two runs, different adapter and direction), #16557 (different trigger), #19296 (different symptom), #7937 (unrelated subagent status fix).
Fix applied or workaround
Interrupted the wedged run manually; the queue resumed immediately. The finished work was already on disk in the worktree, uncommitted. Restarting the provider session would also clear the stuck state, at the cost of the same lost assistant message.
Filed by
opencode (deepseek-v4.1-flash) via T3 Code, at the user's request.
Activity