From a44230f3dff6f3c46b1025792cbb3901f2928b3d Mon Sep 17 00:00:00 2001 From: qbit-mirror-bot Date: Thu, 2 Jul 2026 17:29:50 +0000 Subject: [PATCH] fix(gateway): skip home-channel shutdown broadcast when idle --- gateway/run.py | 13 +++++++++++++ tests/gateway/test_gateway_shutdown.py | 25 +++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index ed257607fe80..66181276c95a 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -5548,6 +5548,19 @@ async def _notify_active_sessions_of_shutdown(self) -> None: logger.debug("Skipping home-channel shutdown notifications for in-chat restart") return + if not active: + # With no running agents the "your current task will be + # interrupted" warning is false, so the home-channel ping is pure + # noise — scheduled systemd/cron restarts of an idle gateway hit + # this on every cycle (#20103, #29846). Mirrors the idle in-chat + # /restart suppression above. The drain-marker gate below still + # covers force-interrupt drains, which reach here with active + # sessions. + logger.info( + "Skipping home-channel shutdown notification: no active sessions to interrupt" + ) + return + # Suppress ONLY the home-channel broadcast when the drain that is ending # in this shutdown asked us to be quiet (e.g. a NAS auto-update image # migration — drain-gated, then the machine is recreated). On the diff --git a/tests/gateway/test_gateway_shutdown.py b/tests/gateway/test_gateway_shutdown.py index 9910f3923aa2..9a50cdf69462 100644 --- a/tests/gateway/test_gateway_shutdown.py +++ b/tests/gateway/test_gateway_shutdown.py @@ -138,7 +138,7 @@ async def test_gateway_stop_interrupts_after_drain_timeout(): @pytest.mark.asyncio -async def test_gateway_stop_systemd_service_restart_exits_cleanly(tmp_path, monkeypatch): +async def test_gateway_stop_systemd_service_restart_uses_tempfail(tmp_path, monkeypatch): monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path) runner, adapter = make_restart_runner() adapter.disconnect = AsyncMock() @@ -149,7 +149,12 @@ async def test_gateway_stop_systemd_service_restart_exits_cleanly(tmp_path, monk await runner.stop(restart=True, service_restart=True) runner._launch_systemd_restart_shortcut.assert_called_once_with() - assert runner._exit_code == 0 + # Exit 75 (EX_TEMPFAIL) so RestartForceExitStatus=75 in the unit + # file revives the gateway via Restart=on-failure, even when the + # planned-restart helper fails (Polkit denial, missing user bus, + # headless box, or operator-managed unit using on-failure instead + # of always). StartLimitBurst still bounds accidental loops. + assert runner._exit_code == GATEWAY_SERVICE_RESTART_EXIT_CODE assert (tmp_path / ".restart_pending.json").exists() @@ -242,6 +247,22 @@ async def test_idle_in_chat_restart_does_not_send_interruption_warning(): assert adapter.sent_calls == [] +@pytest.mark.asyncio +async def test_idle_external_shutdown_skips_home_channel_notification(): + """External stop/restart (systemd, cron) with no running agents must not + broadcast the interruption warning to home channels (#20103, #29846).""" + runner, adapter = make_restart_runner() + runner.config.platforms[Platform.TELEGRAM].home_channel = HomeChannel( + platform=Platform.TELEGRAM, + chat_id="home-chat", + name="Telegram Home", + ) + + await runner._notify_active_sessions_of_shutdown() + + assert adapter.sent_calls == [] + + @pytest.mark.asyncio async def test_in_chat_restart_does_not_write_home_startup_marker(tmp_path, monkeypatch): monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)