TUI /agents return can leave stale busy spinner and active-turn inhibitor after subagent completion on Linux/tmux
What version of Codex CLI is running?
v0.118.0
What subscription do you have?
Pro
Which model were you using?
gpt-5.4 high
What platform is your computer?
Linux x86_64
What terminal emulator and version are you using (if applicable)?
tmux
What issue are you seeing?
I hit a bad TUI/runtime state around subagent switching.
The important operator action is that I was switching between the subagent and the main agent via /agents.
After the child subagent completed, the parent/main thread ended up in a stale busy-looking state instead of cleanly recovering.
From the user side this looked like the app hung or crashed while leaving the screen in a state that still suggested work was ongoing.
What I observed locally on the affected tmux pane:
- the pane content still contained repeated
Waited for background terminallines from the earlier work - the final assistant response from the parent thread was already printed
- the normal prompt was visible again at the bottom
- but the tmux pane title remained stuck at
⠧ home(busy spinner title) and did not clear - the title stayed exactly
⠧ homeacross repeated checks over multiple seconds, instead of returning to a non-busy title - the Codex process remained alive
- a stale child process was still present:
systemd-inhibit --what=idle --mode=block --who codex --why "Codex is running an active turn" -- sleep 2147483647- that inhibitor was still alive more than an hour after the turn had already completed
So even if the backend believes the turn finished, the TUI/runtime busy-state cleanup did not happen correctly.
This is not just "the turn completed and I did not want it to" (although from the operator side it did feel wrong). The concrete bug is that the CLI was left in a contradictory state:
- visible prompt is back
- parent turn has completed
- but busy indicators and the active-turn idle inhibitor are still stuck as if a turn were running
What steps can reproduce the bug?
I do not yet have a minimal deterministic repro, but the sequence that triggered it was:
- Start Codex CLI in
tmux. - Use a workflow that spawns/uses a subagent.
- Switch between the child and main thread via
/agents. - Switch back to the main thread and wait for the child subagent to complete.
- Observe that after completion, the main thread can be left in a stale state:
- the screen still looks like it was working recently (
Waited for background terminalstill visible) - the tmux pane title remains stuck on a busy spinner
- an
active turnsystemd-inhibitchild remains alive - the process is alive, but the session no longer feels cleanly interactive from the user perspective
Affected top-level thread ID:
019d61b9-5b70-78e2-9a11-56ebcfd12bba
Child subagent completion was delivered into that parent thread via <subagent_notification>.
What is the expected behavior?
After subagent completion and return to the main thread, Codex should do one of these consistently:
- keep the parent turn active and visibly keep waiting, or
- complete the parent turn and fully clear all busy state
It should not leave the session in a contradictory half-finished state where:
- the prompt is back,
- the backend turn is complete,
- but the busy spinner/title and the
Codex is running an active turninhibitor are still stuck.
Additional information
Concrete local evidence from this incident:
- parent thread rollout recorded
task_completeat2026-04-06T10:55:05.491Z - affected tmux pane title remained:
⠧ home- repeated checks over ~6 seconds showed the title was still unchanged:
t0: ⠧ homet+3s: ⠧ homet+6s: ⠧ home- the stale inhibitor process remained present long after completion:
systemd-inhibit --what=idle --mode=block --who codex --why Codex is running an active turn -- sleep 2147483647
Related but not identical issues:
- #11472 (sub-agent shutdown / parent restoration)
- #12033 / #14303 / #14314 (stale Waiting/Waited for background terminal state)
- #12645 (tmux TUI/input stuck after completion)
My current guess is that this is another turn-lifecycle cleanup bug in the TUI/runtime layer, possibly triggered specifically by the /agents thread-switch path around subagent completion boundaries.
6 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I dug further into the upstream TUI code and there is now a concrete code-level hypothesis for why
/agentscan resurrect a false running state after switching back.Relevant paths:
codex-rs/tui/src/app.rsstore_active_thread_receiver()savesstore.input_state = self.chat_widget.capture_thread_input_state()when leaving the active thread.ThreadEventStore::push_notification()clearsactive_turn_idonTurnCompleted, but it does not sanitize the already-savedinput_state.codex-rs/tui/src/chatwidget.rscapture_thread_input_state()persists both:task_running: self.bottom_pane.is_task_running()agent_turn_running: self.agent_turn_runningrestore_thread_input_state()later restoresself.agent_turn_running = input_state.agent_turn_runningand immediately calls:self.turn_sleep_inhibitor.set_turn_running(self.agent_turn_running)self.update_task_running_state()So the race/bug pattern looks like this:
/agents.input_statewithagent_turn_running=true/task_running=true.TurnCompletednotification arrives.ThreadEventStore::push_notification()clears onlyactive_turn_id, but the stale savedinput_statestill says the thread is running.replay_thread_snapshot()restores the staleinput_statefirst.This would explain the exact symptom cluster I saw:
systemd-inhibit --why "Codex is running an active turn"Even if replay later can clear the state when it sees a matching completion event, this code still allows
/agentsto restore a ghost-running state from staleinput_state. If the correcting replay event is missing/missed/reordered, the false busy state can persist indefinitely.So I suspect the fix is in one of these directions:
agent_turn_running/task_runningacross thread switches as authoritative state,input_statewhenever an inactive thread receivesTurnCompleted/ThreadClosed,active_turn_id/ replayed turns) instead of the stale saved widget state.I do not have a minimal deterministic repro yet, but this looks like a real logic hole rather than just user confusion.
I did a deeper source review against a local checkout under
/home/tools/codex.Version note first: the local checkout is not literally the packaged
v0.118.0source tree.HEAD:c1d18ceb6f22ae3acd67bbd6badad0f475b31dfccodex-rs/Cargo.toml:0.0.00.118.xrelease-tagged tree available locally:rust-v0.118.0-alpha.3(a5ea426b9a867ec64a41fa2d956bae552036cd4d)HEADis 116 commits ahead of that alpha tagThat said, the relevant logic hole exists in both the current tree and the
rust-v0.118.0-alpha.3tree, so this does not look like a main-only regression.Findings
HIGH: thread-switch snapshots persist running state as authoritative and re-arm the sleep inhibitor on restore
Current main:
codex-rs/tui/src/chatwidget.rs:3136-3206capture_thread_input_state()persists:task_running: self.bottom_pane.is_task_running()agent_turn_running: self.agent_turn_runningrestore_thread_input_state()then restoresself.agent_turn_running = input_state.agent_turn_runningand immediately does:self.turn_sleep_inhibitor.set_turn_running(self.agent_turn_running)self.update_task_running_state()This means a stale snapshot from
/agentsthread switching can directly resurrect both the busy UI and Linuxsystemd-inhibitstate before canonical replay finishes.I confirmed the same capture/restore pattern already exists in
rust-v0.118.0-alpha.3as well.HIGH: inactive-thread completion clears only
active_turn_id, not the savedinput_stateCurrent main:
codex-rs/tui/src/app.rs:577-588ThreadEventStore::push_notification()clearsactive_turn_idonTurnCompleted, but does not invalidate or sanitizestore.input_statecodex-rs/tui/src/app.rs:1569-1578So if the user switches away while the main thread is running, the TUI saves
agent_turn_running=true/task_running=true. IfTurnCompletedarrives while that thread is inactive, the store only clears the cached turn id, but the saved running flags remain stale. On restore, those stale flags can still win.MEDIUM: replay order widens the race window by restoring snapshot state before draining queued live events
Current main:
codex-rs/tui/src/app.rs:1585-1593codex-rs/tui/src/app.rs:3141-3159The thread switch path restores the snapshot first:
self.replay_thread_snapshot(snapshot, !is_replay_only);and only after that drains the live receiver queue:
self.drain_active_thread_events(tui).await?;So even if a
TurnCompletedevent is already queued on the receiver at switch time, staleinput_stateis restored first. If that later completion is missed, delayed, or reordered, the false running state can persist.MEDIUM: the current tests encode the problematic behavior, but I did not find a regression test for "thread completed while inactive"
Current main:
codex-rs/tui/src/chatwidget/tests/composer_submission.rs:789-813There is an explicit test asserting that
restore_thread_input_state()should re-enable the sleep inhibitor when the snapshot saysagent_turn_running=true.That makes the behavior intentional at the unit-test level, but I did not find a corresponding regression test that covers:
/agentsBottom line
After reviewing the source, I think the issue is real and the logic hole is likely in the TUI state model itself:
That seems fully compatible with the user-visible symptom I reported:
If helpful, the next thing I can do is reduce this into a deterministic integration test around thread switching and completion ordering.
Version correction after local audit:
/usr/bin/codex->/usr/lib/node_modules/@openai/codex/bin/codex.js, and bothcodex --versionand the installed package metadata report@openai/codex 0.118.0.0.118.0) is correct for the actual runtime artifact involved in the bug./home/tools/codexnot matchingv0.118.0was about the local source checkout used for code review: that checkout does not currently have the exact0.118.0release tag fetched locally, and the nearest local0.118.xtag visible there isrust-v0.118.0-alpha.3.0.118.0; local source checkout tag availability = incomplete/misaligned.This does not change the bug diagnosis, but it does matter for version attribution, so I wanted to correct the record.
This looks like a duplicate of #16904.
I have already copied the code-level hypothesis and the latest
0.119.0-alpha.11source-level check into #16904, so I am treating #16904 as the canonical thread going forward unless there is a reason to keep both open.Relevant follow-ups now live there.
Closing as duplicate of #16904. I copied the substantive technical follow-up there, including the source-level hypothesis and the latest 0.119.0-alpha.11 check.