mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-20 13:32:30 +00:00
fix(cli): exit prompt_toolkit cleanly on SIGTERM/SIGHUP instead of raising KeyboardInterrupt (#28688)
The SIGTERM/SIGHUP handler raised KeyboardInterrupt() at the end of its agent-interrupt + grace-window sequence. Python delivers signals between bytecodes on the main thread, so when the signal hit mid-event-loop (typically inside prompt_toolkit's '_poll_output_size' coroutine's 'await asyncio.sleep()'), the KeyboardInterrupt unwound INTO that coroutine. prompt_toolkit's Task captured it as a BaseException; prompt_toolkit's '_handle_exception' then printed 'Unhandled exception in event loop' + the full asyncio traceback and parked the terminal on 'Press ENTER to continue...' before exiting. Same root cause as #13710, different surface: there the failure was an EIO cascade after a logging-cache KeyError escaped the handler; here it's the KBI raise itself landing inside an asyncio Task. The fix is the same shape — let the event loop unwind on its own terms. Now: schedule 'app.exit()' via 'loop.call_soon_threadsafe()'. The prompt_toolkit Application returns normally from 'app.run()' and the existing '(EOFError, KeyboardInterrupt, BrokenPipeError)' handler in the input loop catches everything else. Fallback to 'raise KeyboardInterrupt()' preserved for contexts where prompt_toolkit isn't the active app (e.g. -q one-shot mode). The agent interrupt + 1.5 s grace window run unchanged before the new exit path, so subprocess-group cleanup ('os.killpg' on Linux) still gets its window. Tested live: external SIGTERM to the CLI (with 'kill <pid>') now exits cleanly with no traceback dump and no ENTER pause.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user