Problem
We already have #911 for the Linux-specific case where the UI can get stuck in the running state because the final turn/completed JSON 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:
- tool rows show as completed (
MCP tool call complete, Command run complete)
- the UI still shows
Working for ... indefinitely
- switching away and back can recover because the app resyncs from snapshot state
That 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 -> isWorking
The work log/tool rows can already be complete while the composer still remains blocked if the session never transitions from running back to ready.
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-pressure
I do not think T3 Code should depend on that being fixed upstream before handling this safely on our side.
Proposal
Keep the existing #911 style readline hardening, but add a more defensive recovery path in apps/server/src/codexAppServerManager.ts:
- Keep the readline
close residual flush so newline/buffering issues still get handled.
- While a turn is marked
running, start a lightweight recovery timer.
- If the turn is still marked running after the grace period, call
thread/read.
- If
thread/read shows the active turn already has a terminal status / completedAt, synthesize the equivalent recovered completion handling server-side and clear session state.
- Ignore a later duplicate real
turn/completed notification 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
- preserves current frontend behavior
- makes the session lifecycle self-healing
- covers both the Linux newline-buffering case and other missing/delayed completion cases
- protects users even if Codex app-server has edge-case command/exec behavior under back-pressure or long output
Local evidence
I have a local patch with:
- readline
crlfDelay: Infinity
- residual stdout flush on readline
close
- turn completion recovery via
thread/read
- duplicate completion suppression
- regression tests for recovery + dedupe
Local verification passed:
bun fmt
bun lint
bun typecheck
bun run test src/codexAppServerManager.test.ts (from apps/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:
- the stuck input/composer symptom is likely broader than the original Linux-only newline buffering bug
- the server should probably reconcile turn completion from provider snapshot state when realtime lifecycle messages are missing
If this direction sounds right, I can turn the patch into a cleaner PR or split it into smaller steps.
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.