diff --git a/gateway/run.py b/gateway/run.py index 559adae89bf06..e6c536dc866a9 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -8116,14 +8116,19 @@ async def _handle_reset_command(self, event: MessageEvent) -> Union[str, Ephemer # previous conversation must not survive the reset. self._clear_session_boundary_security_state(session_key) - # Fire plugin on_session_finalize hook (session boundary) - try: - from hermes_cli.plugins import invoke_hook as _invoke_hook - _old_sid = old_entry.session_id if old_entry else None - _invoke_hook("on_session_finalize", session_id=_old_sid, - platform=source.platform.value if source.platform else "") - except Exception: - pass + # Fire plugin on_session_finalize hook only when a prior session + # actually existed. On a first-message /new there is nothing to + # finalize, and emitting session_id=None creates a fake boundary event. + if old_entry is not None: + try: + from hermes_cli.plugins import invoke_hook as _invoke_hook + _invoke_hook( + "on_session_finalize", + session_id=old_entry.session_id, + platform=source.platform.value if source.platform else "", + ) + except Exception: + pass # Emit session:end hook (session is ending) await self.hooks.emit("session:end", { diff --git a/tests/gateway/test_session_boundary_hooks.py b/tests/gateway/test_session_boundary_hooks.py index 255795492fc70..ee73c93b782e6 100644 --- a/tests/gateway/test_session_boundary_hooks.py +++ b/tests/gateway/test_session_boundary_hooks.py @@ -99,6 +99,30 @@ async def test_reset_fires_reset_hook(mock_invoke_hook): ) +@pytest.mark.asyncio +@patch("hermes_cli.plugins.invoke_hook") +async def test_reset_without_existing_session_skips_finalize_hook(mock_invoke_hook): + """First-session /new should not emit a fake finalize event with None.""" + runner = _make_runner() + session_key = build_session_key(_make_source()) + runner.session_store._entries = {} + runner.session_store.reset_session.return_value = None + + await runner._handle_reset_command(_make_event("/new")) + + finalize_calls = [ + call for call in mock_invoke_hook.call_args_list if call[0][0] == "on_session_finalize" + ] + assert finalize_calls == [] + runner.session_store.reset_session.assert_called_once_with(session_key) + runner.session_store.get_or_create_session.assert_called_once_with( + _make_source(), force_new=True + ) + mock_invoke_hook.assert_any_call( + "on_session_reset", session_id="sess-new", platform="telegram" + ) + + @pytest.mark.asyncio @patch("hermes_cli.plugins.invoke_hook") async def test_finalize_before_reset(mock_invoke_hook):