Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 23 additions & 2 deletions tests/gateway/test_gateway_shutdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()


Expand Down Expand Up @@ -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)
Expand Down
Loading