TUI: late interrupt key after pending steer commits can interrupt active turn
Correction (July 12, 2026): The initial report incorrectly attributed this race to the safety-buffering menu. A full event-flow trace and deterministic tests confirmed that the owner is the pending-steer acknowledgment transition described below. The title and body have been replaced, and the incorrect branch was deleted before any PR was opened.
What version of Codex CLI is running?
Source build from main at 9e552e9d15ba52bed7077d5357f3e18e330f8f38.
What subscription do you have?
Not applicable to this deterministic source-level TUI race.
Which model were you using?
Not model-specific.
What platform is your computer?
Darwin 24.6.0 arm64 arm
What terminal emulator and version are you using (if applicable)?
Terminal.app 455.1, without a terminal multiplexer.
Codex doctor report
Not available for this source build.
What issue are you seeing?
While an active turn is running, a submitted steer appears under “Messages to be submitted after next tool call,” with a hint to press the configured interrupt key to interrupt and send it immediately.
When core commits that steer just before the key event is routed, the TUI removes the final matching entry from InputQueueState::pending_steers and refreshes the preview. The delayed key then sees no pending steer and falls through to the ordinary turn-interrupt path, unintentionally stopping work that has already received the message.
What steps can reproduce the bug?
- Start a turn and submit a steer while Codex is still working.
- Confirm that the pending-steer preview offers the interrupt key to send it immediately.
- Press that key just as Codex accepts the steer and removes it from the preview.
- If
ItemCompleted(UserMessage)is processed immediately before the terminal key event, the key interrupts the active continuation instead of completing the now-unnecessary immediate-send action.
A deterministic regression test reproduces the ordering by submitting a real steer, processing its matching committed UserMessage, and immediately dispatching the interrupt key.
What is the expected behavior?
When the final pending steer was just committed, absorb the one late interrupt gesture rather than stopping the active continuation. Suppress repeats from that same physical press until release, then let the next distinct interrupt press work normally.
The transient footer should explain the behavior:
Message received; esc was ignored. Press esc again to interrupt.
Additional information
InputQueueState owns the submitted-but-uncommitted steer and the transient guard. ChatWidget arms the guard only when a matching live commit removes the final pending steer, binds it to that committed turn ID, and consults the existing BottomPane::should_interrupt_running_task predicate before consuming anything.
The 400 ms window is a deliberately short heuristic for the event-loop handoff after the steer commits. It is one-shot, is not a global interrupt debounce, and is cleared by other input, a new steer, turn/thread lifecycle changes, expiry, or the next distinct interrupt gesture.
The replacement fix includes coverage for the live final-steer transition, Press/Repeat/Release handling, the next distinct interrupt, expiry, a new pending steer, cross-turn isolation, and an 80-column footer snapshot. Existing tests continue to cover review warnings, Vim ownership, remapped interrupt bindings, /agent, and multiple pending steers.
Validated branch: https://github.com/Milad-Afdasta/codex/tree/fix/pending-steer-esc-race
I can submit a PR if a maintainer explicitly invites one, per the repository contribution policy.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗