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
|
# Register atexit cleanup so resources are freed even on unexpected exit
|
||||||
atexit.register(_run_cleanup)
|
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
|
# Install a custom asyncio exception handler that suppresses the
|
||||||
# "Event loop is closed" RuntimeError from httpx transport cleanup.
|
# "Event loop is closed" RuntimeError from httpx transport cleanup.
|
||||||
# This is defense-in-depth — the primary fix is neuter_async_httpx_del
|
# This is defense-in-depth — the primary fix is neuter_async_httpx_del
|
||||||
@@ -7470,7 +7484,7 @@ class HermesCLI:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
app.run()
|
app.run()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt, BrokenPipeError):
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
self._should_exit = True
|
self._should_exit = True
|
||||||
@@ -7509,6 +7523,23 @@ class HermesCLI:
|
|||||||
self._session_db.end_session(self.agent.session_id, "cli_close")
|
self._session_db.end_session(self.agent.session_id, "cli_close")
|
||||||
except (Exception, KeyboardInterrupt) as e:
|
except (Exception, KeyboardInterrupt) as e:
|
||||||
logger.debug("Could not close session in DB: %s", 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()
|
_run_cleanup()
|
||||||
self._print_exit_summary()
|
self._print_exit_summary()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user