Skip to content

Proposal: recover stuck running turns when turn/completed is lost after long command execution #917

Description

@cyppe

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:

  1. Keep the readline close residual flush so newline/buffering issues still get handled.
  2. While a turn is marked running, start a lightweight recovery timer.
  3. If the turn is still marked running after the grace period, call thread/read.
  4. 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.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions