diff --git a/cli.py b/cli.py index a22a836b7fa..8fdfa6faad0 100644 --- a/cli.py +++ b/cli.py @@ -13907,7 +13907,31 @@ class HermesCLI: time.sleep(_grace) except Exception: pass # never block signal handling - raise KeyboardInterrupt() + # Prefer a clean prompt_toolkit exit over `raise KeyboardInterrupt()`. + # Raising KBI from a signal handler unwinds into whatever Python + # frame the interpreter happens to be running — typically an + # `await asyncio.sleep()` inside prompt_toolkit's + # `_poll_output_size` coroutine. The KBI becomes a Task + # exception, prompt_toolkit's `_handle_exception` prints + # "Unhandled exception in event loop" + the full traceback, and + # parks the terminal on "Press ENTER to continue..." (#13710 + # variant — same root cause, different surface). + # + # `app.exit()` scheduled via `call_soon_threadsafe` lets the + # event loop unwind normally; `app.run()` returns and our + # existing `except (EOFError, KeyboardInterrupt, BrokenPipeError)` + # block at the bottom of the input loop handles the rest. + try: + from prompt_toolkit.application.current import get_app_or_none + _app = get_app_or_none() + if _app is not None: + _loop = getattr(_app, "loop", None) + if _loop is not None: + _loop.call_soon_threadsafe(_app.exit) + return # clean unwind — no traceback, no ENTER pause + except Exception: + pass + raise KeyboardInterrupt() # fallback for non-prompt_toolkit contexts try: import signal as _signal