fix(cli): recover terminal state after interrupting a running turn (#33271) - #54058
Merged
Conversation
…ol sequence freeze When the agent is interrupted during processing, prompt_toolkit's renderer and VT100 input parser can be left in an inconsistent state. CSI 6n cursor position report responses leak as literal text (^[[19;1R) and the terminal stops accepting keyboard input. Fix: in process_loop's finally block, after an interrupted turn: - flush_stdin() to drain stray escape bytes from the OS input buffer - _force_full_redraw() to reset prompt_toolkit's renderer cache Closes #33271
Pull the #33271 post-interrupt recovery (flush_stdin + _force_full_redraw) out of process_loop's finally block into _recover_terminal_after_interrupt(), and replace the inline-logic-copy tests with ones that exercise the real helper plus a source guard that process_loop still invokes it behind the _last_turn_interrupted gate.
tonydwb
reviewed
Jun 28, 2026
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Adds terminal recovery after user-interrupted agent turns. When the user types during a running turn, prompt_toolkit's in-flight CSI 6n cursor query can leak as literal text and stall the VT100 parser. The recovery path drains stray escape bytes (flush_stdin) and forces a clean redraw (_force_full_redraw). Both steps are independently safe. Tests cover the helper, ordering, TTY gating, and process_loop wiring behind the _last_turn_interrupted guard.
Reviewed by Hermes Agent
This was referenced Jun 28, 2026
Closed
This was referenced Jul 29, 2026
19 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Interrupting a running CLI turn by typing a new message no longer freezes the terminal. After an interrupt, the CLI now drains stray escape bytes from stdin and forces a clean prompt_toolkit repaint, so a leaked
CSI 6ncursor-position reply can't stall the input parser.Closes #33271. Salvaged from #34064 (@zccyman).
Root cause
process_loop'sfinallyblock only calledapp.invalidate(). On an interrupt, prompt_toolkit could have an in-flightCSI 6nquery whose reply (ESC[<row>;<col>R) arrived on stdin after the parser tore down — leaking as literal^[[19;1Rand stalling the VT100 parser, accepting no further keystrokes. The existing CPR-suppression machinery (#13870/#48309) only engages on SSH/tunnel terminals, leaving local terminals (where the bug also reproduces) unprotected on the interrupt path.Changes
cli.py: after an interrupted turn (_last_turn_interrupted), callflush_stdin()(termios.tcflush(TCIFLUSH), no-op on non-TTY) then_force_full_redraw(). Extracted into a testable_recover_terminal_after_interrupt()helper.tests/cli/test_terminal_interrupt_recovery.py: tests exercise the real helper (drain-then-redraw order, redraw survives a flush failure, TTY-gating) plus a source guard thatprocess_loopactually invokes it behind the_last_turn_interruptedgate.Validation
app.invalidate())flush_stdin+_force_full_redrawtests/cli/test_terminal_interrupt_recovery.pytests/cli/test_cli_force_redraw.pyInfographic