TUI: queued follow-up inputs are permanently stranded after a failed prompt-edit branch
What version of Codex CLI is running?
codex-cli 0.147.0
What platform is your computer?
Linux 5.15.120 x86_64 (also reproducible on other platforms — the bug is in the TUI prompt-edit/backtrack logic, not platform-specific)
What issue are you seeing?
When editing an earlier prompt (Esc-Esc backtrack → select a prompt → Enter), the TUI forks the thread before the selected turn. If that fork fails — e.g. the composer shows:
■ Failed to branch before the selected prompt: the selected prompt was not found in the persisted thread
— then any user inputs that were queued while the fork was in flight get permanently stranded. They stay in "Queued follow-up inputs" forever and are never submitted, even after the composer returns to the Ready state. The queue just keeps growing with every new message and nothing is ever sent to the model.
Note: the "not found in the persisted thread" guard itself is legitimate defensive behavior (the visible transcript projection can't always be matched back to a persisted turn — e.g. after history pagination, or when large context blocks are injected as user messages by a wrapper). The bug is purely in the failure-handling path, which forgets to re-drive the input queue.
Root cause
Every successful fork path re-drives the follow-up input queue via maybe_send_next_queued_input():
- the sibling
AppEvent::ForkSessionhandler (codex-rs/tui/src/app/event_dispatch.rs), - the startup path (
handle_startup_thread_started), - the thread-start path.
But the branch-failure path does not. In codex-rs/tui/src/app_backtrack.rs:
pub(crate) fn restore_backtrack_prompt_after_branch_error(
&mut self,
prompt: UserMessage,
err: impl std::fmt::Display,
) {
self.chat_widget.restore_user_message_to_composer(prompt);
self.chat_widget.add_error_message(format!(
"Failed to branch before the selected prompt: {err}"
));
// <-- missing: maybe_send_next_queued_input(); the current thread is still live,
// but queued follow-ups are never re-driven, so they stay stuck forever.
}
Since the fork failed, the original thread stays live, but the queue is never flushed, so queued inputs are stranded.
What steps can reproduce the bug?
- In a live thread, enqueue one or more follow-up inputs (e.g. queue messages while a turn is running, or via a wrapper that injects inputs).
- Trigger prompt editing (Esc-Esc backtrack) and select a prompt whose fork will fail — the reliable way to force the failure is any state where the selected transcript prompt can't be resolved back to a persisted turn (
the selected prompt was not found in the persisted thread). - Observe that after the error, the composer returns to
Ready, but the queued follow-up inputs remain in "Queued follow-up inputs" and are never submitted.
What is the expected behavior?
After a failed prompt-edit branch, since the current thread is still live, queued follow-up inputs should be submitted once the composer is idle — exactly like every successful fork path already does.
Additional information
I have a minimal fix and regression test ready (one-line fix + a test asserting the queued input is submitted after a failed prompt-edit branch, verified to fail without the fix and pass with it). openai/codex does not appear to accept PRs from external forks, so I'm filing this issue instead. Happy to share the patch/branch if useful:
pub(crate) fn restore_backtrack_prompt_after_branch_error(
&mut self,
prompt: UserMessage,
err: impl std::fmt::Display,
) {
self.chat_widget.restore_user_message_to_composer(prompt);
self.chat_widget.add_error_message(format!(
"Failed to branch before the selected prompt: {err}"
));
// The prompt edit never took effect, so the current thread stays live. Follow-up inputs
// queued while the fork was in flight would otherwise remain stuck, because unlike the
// successful fork paths this branch never re-drives the queue. Flush it here so queued
// messages are submitted once the composer is idle again.
self.chat_widget.maybe_send_next_queued_input();
}
2 Comments
same issue as in https://github.com/openai/codex/issues/37421 ?
Verified on current
main(1f41cc5d92): the analysis holds exactly as written.restore_backtrack_prompt_after_branch_errorstill ends afteradd_error_messagewith no queue re-drive (https://github.com/openai/codex/blob/1f41cc5d92/codex-rs/tui/src/app_backtrack.rs#L206-L215), while all four sibling completion paths callmaybe_send_next_queued_input()(tui/src/app/event_dispatch.rs#L117,#L167,#L203,#L349). Since the failed fork leaves the original thread live and the composer returns toReady, nothing else ever re-drives the queue — stranded exactly as described.One nuance for whoever fixes it: the restored prompt is placed back into the composer by
restore_user_message_to_composer, so flushing the queue immediately could race with the user editing that restored text. Mirroring the success paths' behavior (drive the queue only when the composer is idle/submits) — i.e. callingmaybe_send_next_queued_input()at the end of the error handler, which already gates on state — matches expectations and is a one-line fix plus a regression test around the branch-failure path.