openai / openai/codex

CLI: resume paints a permanent Working footer when the thread is Idle

Open
#43,952 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app-server bug CLI TUI
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

What version of Codex CLI is running?

codex-cli 0.153.4

What subscription do you have?

Pro

Which model were you using?

GPT-5.6-Sol

What platform is your computer?

Darwin 25.6.0 arm64 arm

What terminal emulator and version are you using (if applicable)?

tmux

Codex doctor report
{
    "note": "Full report omitted because it contains organization-
    specific paths, MCP names, and endpoint-security metadata.",
    "codexVersion": "0.153.4",
    "platform": "macos-aarch64",
    "installMethod": "Homebrew",
    "authMode": "chatgpt",
    "model": "gpt-5.6-sol",
    "modelProvider": "openai",
    "terminal": "iTerm2 3.6.11 with tmux 3.6a",
    "latestVersion": "0.153.4"
  }
What issue are you seeing?

When the TUI resumes an existing app-server thread, it can display
“Working (… • esc to interrupt)” indefinitely even though the
authoritative thread status is Idle and no turn is running.

Escape does not clear it because it sends a real turn/interrupt
request, but there is no active turn to interrupt. If a genuine turn
begins later, Escape interrupts that real turn while the stale Working
footer remains.

This is a TUI rendering-state bug rather than a hung model request.

What steps can reproduce the bug?
  1. Begin a Codex turn.
  2. End the app-server uncleanly while the turn is active, leaving a
    persisted task_started without a subsequent task_complete,
    turn_aborted, or other terminal event.
  3. Restart the app-server.
  4. Attach the TUI using:
    codex --remote ws://127.0.0.1: resume
  5. Confirm that the resumed thread is Idle and has no active turn.
  6. Observe that the TUI nevertheless displays a permanent Working
    footer.
  7. Press Escape. The footer remains because no live turn exists to
    complete or interrupt.

This lifecycle signature was verified in the affected rollout.

What is the expected behavior?

The TUI should use the resumed thread’s current ThreadStatus as the
authority for its activity indicator.

A historical InProgress turn should restore the Working footer only
when the live thread is Active. If the thread is Idle, the TUI should
render idle.

Additional information

During typed turn replay,
replay_thread_turns
unconditionally calls on_task_started() for every historical
TurnStatus::InProgress:

if matches!(status, TurnStatus::InProgress) {
    // ...
    self.on_task_started();
}

However,
AppServerStartedThread
does not carry the live ThreadStatus, and
started_thread_from_resume_response
copies response.thread.turns into the TUI bootstrap state while discarding
response.thread.status.

That lets a stale historical InProgress turn activate the local Working
widget even when the authoritative resumed thread is Idle. Since no live turn
exists, no later turn-completed event is guaranteed to clear the widget.

This looks like the same invariant addressed by #11852 / #12420 ("replayed
history must not force live active state") reappearing through the newer typed
turn replay path.

Suggested fix and regression cases

Carry the live ThreadStatus through the resumed-thread bootstrap and only
restore task-running UI from historical InProgress when the live thread is
actually Active. Also reconcile a live transition to Idle by clearing stale
task-running UI when no active turn remains.

Suggested tests:

  1. ThreadStatus::Idle + historical TurnStatus::InProgress => no Working
    widget.
  2. ThreadStatus::Active + historical TurnStatus::InProgress => Working
    widget is restored.
  3. A legitimately active resumed thread receives
    ThreadStatusChanged(Idle) => Working widget clears.
  4. Resume an idle thread, then receive a real live turn/started => Working
    widget appears normally.

#40563 is useful corroboration but a different layer: it documents the
producer-side persistence contradiction where history remains inProgress
while thread/resume reports an idle, input-capable runtime. This comment is
about the consumer-side TUI behavior when it receives that contradiction.

Contributor guide

Open the contributing guide

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 codex-rs/tui/src/chatwidget/replay.rs and codex-rs/tui/src/app_server_session.rs, tracing how resumed thread status and historical turns enter the TUI state. Add regression coverage for Idle and Active threads with historical InProgress turns, plus an Active-to-Idle transition. Done means idle resumes do not show Working, active resumes do, and later real turns still display normally.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
cli
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.