TUI: quick-cancelled optimistic prompt remains selectable but has no persisted UserMessage
What version of Codex CLI is running?
codex-cli 0.147.0
What subscription do you have?
ChatGPT subscription (exact tier omitted; this appears to be a local TUI persistence race)
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)?
Ghostty 1.3.1
Codex doctor report
Not included because it contains unrelated local paths. The relevant persisted state is summarized below.
What issue are you seeing?
If a newly submitted prompt is cancelled very quickly, it can remain visible in the TUI transcript and in prompt recall even though it never becomes a persisted UserMessage. Esc-Esc then offers that optimistic-only prompt as a backtrack target, but selecting it fails with:
Failed to branch before the selected prompt: the selected prompt was not found in the persisted thread
This is narrower than #37421 and does not appear to be covered by #37622. That fix reconstructs turns whose UserMessage already exists in buffered server notifications. In this reproduction, no user-message item exists in the rollout, replayable thread items, or thread database.
Observed state after the failure:
- Cross-session prompt history contains the submitted text.
- The TUI transcript contains a selectable user-history cell for it.
- The rollout contains
task_startedfollowed byturn_aborted, with no userresponse_itemfor the submitted prompt. - The corresponding
thread_turnsrow isinterrupted, with an emptyfirst_user_item_id. - There is no matching
userMessageinthread_items.
The local timing was:
- Prompt submission received.
- Interrupt received approximately 743 ms later.
- The task did not complete gracefully within the subsequent 100 ms cancellation window and was forcibly aborted.
The source appears to permit this split state:
input_submission.rsrenders the prompt optimistically before submitting the operation and separately records prompt recall: https://github.com/openai/codex/blob/rust-v0.147.0/codex-rs/tui/src/chatwidget/input_submission.rs#L359-L405app_backtrack.rsselects from visibleUserHistoryCellentries, then resolves the selection against persistedThreadItem::UserMessageentries and emits the error above if none matches: https://github.com/openai/codex/blob/rust-v0.147.0/codex-rs/tui/src/app_backtrack.rs#L463-L573
Five UserPromptSubmit hooks were configured in the reproducing environment. The logs do not identify the exact pre-persistence stage active at cancellation, so this does not establish that any specific hook caused the failure; hook processing may simply widen the timing window.
What steps can reproduce the bug?
- Start a new interactive Codex CLI session.
- Submit a short prompt such as
Hello. - Immediately press Esc once, before the turn has durably recorded its user-message item.
- With the composer empty, press Esc twice.
- Select the just-cancelled prompt and press Enter.
- Observe that Codex restores the prompt but reports that it was not found in the persisted thread.
This is timing-sensitive interactively. A deterministic test could delay pre-persistence startup or a UserPromptSubmit hook, cancel before record_user_prompt_and_emit_turn_item, and then exercise Esc-Esc against the optimistic transcript cell.
What is the expected behavior?
The backtrack selector should never offer a prompt that cannot serve as a branch anchor. After quick cancellation, Codex should do one of the following:
- Persist the submitted user message before completing cancellation, allowing normal branching; or
- Mark/filter the optimistic-only transcript cell so Esc-Esc cannot select it; or
- Gracefully restore it for editing without attempting an impossible branch and without displaying an internal consistency error.
Additional information
#37622 fixes the related paginated-thread case where live turns exist in the replay buffer but not yet in the snapshot. Its added test supplies an ItemCompleted(UserMessage) notification. This reproduction has no such item to merge, so the new reconstruction path appears unable to resolve it.
No thread or session identifier is included because this is a public issue; sanitized additional diagnostics can be provided if needed.
1 Comment
Same root cause as #38395 (analyzed there with code refs): the user prompt is persisted by
run_hooks_and_record_inputsat turn start, which sits after cancellable awaits (pre-input hooks, MCP refresh) — a quick Esc cancels the turn future beforerecord_pending_inputruns, so the TUI's optimistic echo has no persistedUserMessagebehind it. Your selectable-but-unpersisted history entry is the composer-side view of that same gap. Details: https://github.com/openai/codex/issues/38395#issuecomment-5322992551Proposed fixes there cover this issue too: persist input as the first durable action (or shield the record step from cancellation), and on early abort restore the prompt to the composer instead of leaving a phantom entry.