Status indicator can disappear after switching between active threads
Open 💬 2 comments Opened Aug 11, 2026 by 92645417d9e5c763259dbebc306e3e
What version of Codex CLI is running?
- codex-cli rust-v0.148.0-alpha.1.
What subscription do you have?
- ChatGPT Pro 20x.
Which model were you using?
- gpt-5.6-sol.
What terminal emulator and version are you using (if applicable)?
- Alacritty inside tmux.
Codex doctor report
- Unavailable: all log recording is disabled for performance.
What issue are you seeing?
- Switching from an active thread to a sub-agent and back can permanently hide the active thread's status indicator even though the original turn is still running.
- The failure occurs when a thread switch overlaps the deferred status restoration window after streamed commentary finishes but before its stream queues become idle.
What steps can reproduce the bug?
- Start a long-running turn that produces streamed commentary.
- Wait until a commentary item finishes while its displayed stream is still draining.
- Open a running sub-agent before the active thread's status indicator reappears.
- Return to the main thread.
- Repeat the switch during this short drain window if the first attempt does not reproduce the issue.
- Observe that the main thread remains active but its status indicator can stay hidden.
What is the expected behavior?
- A thread switch preserves both the current status visibility and any pending request to restore the indicator after streaming becomes idle.
Additional information
ChatWidget::finalize_completed_assistant_messagesetspending_status_indicator_restorewhen a commentary stream has finalized but still needs restoration after its queues drain.App::select_agent_threadthen callsApp::store_active_thread_receiver, replaces the active widget withChatWidget::new_with_app_event, and replays the selected thread snapshot.ChatWidget::capture_thread_input_stateandThreadInputStatepreserve running/input state but omitcurrent_status, indicator visibility, andpending_status_indicator_restore.- Switching during the deferred window therefore destroys the only restore intent with the old
ChatWidget; switching back reconstructs a running widget with no event left to make the status visible. - Preserving those status fields in the per-thread snapshot and invoking
maybe_restore_status_indicator_after_stream_idleafter snapshot replay addresses the issue. - This report was organized with Codex CLI assistance from observed behavior and source inspection.
2 Comments
Confirmed on current
main(7d486ffa). I reproduced this through the production path by keeping a plan stream queued while commentary completion entered the deferred-restoration window, then switching away and replaying the thread snapshot.There is one additional edge beyond the snapshot fields described above: restoring those fields alone is not sufficient. Snapshot replay processes both the in-progress turn and
TurnStarted; either path can callon_task_started()again, overwrite the restored custom status withWorking, and clear the deferred restoration state. The thread-snapshot replay therefore also needs to be idempotent once the running lifecycle has been restored.I have a focused local patch that:
ThreadInputStatefor in-flight turns;TurnStartedevent.The regression failed before the fix (
Investigating thread switchwas replaced by a hiddenWorkingstate) and now passes. The related snapshot-replay and input-restoration tests pass as well.Would a maintainer welcome a PR for this approach? I can keep the change scoped to the TUI state capture/replay path and its regression coverage.
Verified your analysis against
main(1f41cc5d92):ThreadInputStatecarries composer/steer/queue state plustask_running/agent_turn_running, but nothing fromstatus_state— nocurrent_status, no visibility, nopending_status_indicator_restore(https://github.com/openai/codex/blob/1f41cc5d92/codex-rs/tui/src/chatwidget/user_messages.rs#L124-L141). The restore intent lives only on the discarded widget (streaming.rs#L223sets it;#L110-L124consumes it), so a switch inside the drain window destroys the one signal that would re-show the indicator, exactly as you traced.Your fix direction is right; one detail worth adding when snapshotting:
turn_runtime.rs#L88clearspending_status_indicator_restoreunconditionally on turn-runtime resets, so the restored flag needs to be applied after snapshot replay finishes reconstructing the running turn (then invokemaybe_restore_status_indicator_after_stream_idleas you proposed) — otherwise the replay path can wipe it a second time. This is a sibling of #38192 (stale completion notifications across the same thread-switch replay), which suggests the broader fix is making the per-thread snapshot own all ofstatus_staterather than adding fields one at a time.