fix(cli): ensure on_session_end hook fires on interrupted exits (#4159)
- Add SIGTERM/SIGHUP signal handlers for graceful shutdown - Add BrokenPipeError to exit exception handling (SSH disconnects) - Fire on_session_end plugin hook in finally block, guarded by _agent_running to avoid double-firing on normal exits (the hook already fires per-turn from run_conversation) Co-authored-by: kelsia14 <kelsia14@users.noreply.github.com>
This commit is contained in:
33
cli.py
33
cli.py
@@ -7447,6 +7447,20 @@ class HermesCLI:
|
||||
# Register atexit cleanup so resources are freed even on unexpected exit
|
||||
atexit.register(_run_cleanup)
|
||||
|
||||
# Register signal handlers for graceful shutdown on SSH disconnect / SIGTERM
|
||||
def _signal_handler(signum, frame):
|
||||
"""Handle SIGHUP/SIGTERM by triggering graceful cleanup."""
|
||||
logger.debug("Received signal %s, triggering graceful shutdown", signum)
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
try:
|
||||
import signal as _signal
|
||||
_signal.signal(_signal.SIGTERM, _signal_handler)
|
||||
if hasattr(_signal, 'SIGHUP'):
|
||||
_signal.signal(_signal.SIGHUP, _signal_handler)
|
||||
except Exception:
|
||||
pass # Signal handlers may fail in restricted environments
|
||||
|
||||
# Install a custom asyncio exception handler that suppresses the
|
||||
# "Event loop is closed" RuntimeError from httpx transport cleanup.
|
||||
# This is defense-in-depth — the primary fix is neuter_async_httpx_del
|
||||
@@ -7470,7 +7484,7 @@ class HermesCLI:
|
||||
except Exception:
|
||||
pass
|
||||
app.run()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
except (EOFError, KeyboardInterrupt, BrokenPipeError):
|
||||
pass
|
||||
finally:
|
||||
self._should_exit = True
|
||||
@@ -7509,6 +7523,23 @@ class HermesCLI:
|
||||
self._session_db.end_session(self.agent.session_id, "cli_close")
|
||||
except (Exception, KeyboardInterrupt) as e:
|
||||
logger.debug("Could not close session in DB: %s", e)
|
||||
# Plugin hook: on_session_end — safety net for interrupted exits.
|
||||
# run_conversation() already fires this per-turn on normal completion,
|
||||
# so only fire here if the agent was mid-turn (_agent_running) when
|
||||
# the exit occurred, meaning run_conversation's hook didn't fire.
|
||||
if self.agent and getattr(self, '_agent_running', False):
|
||||
try:
|
||||
from hermes_cli.plugins import invoke_hook as _invoke_hook
|
||||
_invoke_hook(
|
||||
"on_session_end",
|
||||
session_id=self.agent.session_id,
|
||||
completed=False,
|
||||
interrupted=True,
|
||||
model=getattr(self.agent, 'model', None),
|
||||
platform=getattr(self.agent, 'platform', None) or "cli",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
_run_cleanup()
|
||||
self._print_exit_summary()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user