Repository navigation
Proposal: recover stuck running turns when turn/completed is lost after long command execution #917
Description
Activity
Follow-up after testing a local fork build: I found a second contributor to the same stuck-composer symptom on top of the server-side
turn/completedrecovery problem.There is also a frontend race in
apps/web/src/components/ChatView.tsxaround localsendPhasestate:isWorkingis derived fromphase === \"running\" || isSendBusy || isConnecting || isRevertingCheckpointisSendBusystays true whilesendPhase !== \"idle\"sendPhasewas only being cleared after the UI observed one of:phase === \"running\"- a pending approval
- pending user input
- a thread error
That leaves a race where a turn can start and complete quickly enough that the UI never renders the intermediate
runningphase. In that case:- the latest turn may already exist and be completed
- the session may already be back to ready
- but local
sendPhasenever resets - so
isWorkingremains true and the composer appears blocked/stuck
So the broader symptom seems to have at least two causes:
- server-side lifecycle reconciliation is needed when
turn/completedis lost/delayed - client-side send state should also self-clear once the UI has observed a newer latest turn for the current send
I patched this locally by resetting
sendPhasewhen the current send has been superseded by an observed latest turn (based on latest turn timestamps vssendStartedAt).So I still think the server-side proposal in this issue is useful, but it likely needs to be paired with a small frontend guard as well to fully eliminate the stuck input/composer symptom.
Sanitized corroborating evidence from a Codex workload on T3 Code 0.0.32:
- Three projected provider sessions remained
runningwith a non-null active turn even though the corresponding turn records already had completion timestamps and had produced no activity for roughly an hour. - A fourth session remained
startingwith no active turn after its checkpoint was captured and then went idle. - The corresponding provider processes were sleeping at approximately 0% CPU.
- The last lifecycle events still reported
running/starting; no later ready or stopped transition was present. - These were high-activity turns: individual sessions recorded 296–967 tool starts (with nearly balanced completions) and 10–46 task starts before becoming stale.
- Projection cursors were caught up, so this did not appear to be merely an unapplied event backlog.
This looks consistent with the proposed reconciliation path here: the provider completed or became idle, but the final lifecycle transition was lost or never ingested under load. A bounded reconciliation against provider state would recover these records without requiring direct database edits or terminating unrelated sessions.
Related pressure amplifier: #5719 documents per-delta projection work that can substantially increase event/transaction volume during these high-activity turns.
No thread IDs, message content, raw logs, usernames, or filesystem paths are included.
- Three projected provider sessions remained
- locked and limited conversation to collaborators
on Aug 15, 2026
Problem
We already have
#911for the Linux-specific case where the UI can get stuck in the running state because the finalturn/completedJSON line never gets emitted by Node readline when stdout closes without a trailing newline.I think there is a second, broader failure mode with a very similar user symptom:
MCP tool call complete,Command run complete)Working for ...indefinitelyThat makes this look less like a frontend rendering bug and more like a stranded turn lifecycle on the server side.
Why this looks server-side, not frontend-only
In the current UI, the loading state is driven from thread/session lifecycle state:
apps/web/src/session-logic.ts->derivePhase()apps/web/src/components/ChatView.tsx->isWorkingThe work log/tool rows can already be complete while the composer still remains blocked if the session never transitions from
runningback toready.So when the screenshot shows completed tool activity but the turn is still stuck, the most likely missing event is still
turn/completed(or an equivalent lifecycle reconciliation), not the command tool UI itself.Why long-running shell commands may make this easier to hit
Long-running / high-output command execution looks like a plausible trigger because it increases the chance that the provider finishes tool activity while the final turn lifecycle message is delayed or lost.
There is also a possibly related upstream Codex report:
openai/codex#13821-app-server command/exec can hang after outputBytesCap is reached due to back-pressureI do not think T3 Code should depend on that being fixed upstream before handling this safely on our side.
Proposal
Keep the existing
#911style readline hardening, but add a more defensive recovery path inapps/server/src/codexAppServerManager.ts:closeresidual flush so newline/buffering issues still get handled.running, start a lightweight recovery timer.thread/read.thread/readshows the active turn already has a terminal status /completedAt, synthesize the equivalent recovered completion handling server-side and clear session state.turn/completednotification if it eventually arrives.That gives T3 Code a provider-state reconciliation path instead of assuming realtime notifications are always sufficient.
Why this seems like the right layer
Local evidence
I have a local patch with:
crlfDelay: Infinityclosethread/readLocal verification passed:
bun fmtbun lintbun typecheckbun run test src/codexAppServerManager.test.ts(fromapps/server)Goal of this issue
This is intentionally filed as a proposal / design direction, not as "this exact patch must land unchanged".
The main point is:
If this direction sounds right, I can turn the patch into a cleaner PR or split it into smaller steps.