Skip to content
Closed
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: 7 additions & 6 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ def _restart_notification_pending() -> bool:
sys.path.insert(0, str(Path(__file__).parent.parent))

# Resolve Hermes home directory (respects HERMES_HOME override)
from hermes_constants import get_hermes_home
from hermes_constants import get_hermes_home, is_container
from utils import atomic_json_write, atomic_yaml_write, base_url_host_matches, is_truthy_value
_hermes_home = get_hermes_home()

Expand Down Expand Up @@ -8737,12 +8737,13 @@ async def _handle_restart_command(self, event: MessageEvent) -> Union[str, Ephem
logger.debug("Failed to write restart dedup marker: %s", e)

active_agents = self._running_agent_count()
# When running under a service manager (systemd/launchd), use the
# When running under a service manager or a container, use the
# service restart path: exit with code 75 so the service manager
# restarts us. The detached subprocess approach (setsid + bash)
# doesn't work under systemd because KillMode=mixed kills all
# processes in the cgroup, including the detached helper.
_under_service = bool(os.environ.get("INVOCATION_ID")) # systemd sets this
# or container restart policy restarts us. The detached subprocess
# approach (setsid + bash) doesn't work under systemd because
# KillMode=mixed kills all processes in the cgroup, and it doesn't
# survive a Docker PID 1 exit either.
_under_service = bool(os.environ.get("INVOCATION_ID")) or is_container()
if _under_service:
self.request_restart(detached=False, via_service=True)
else:
Expand Down
1 change: 1 addition & 0 deletions tests/gateway/test_restart_drain.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ async def test_restart_command_while_busy_requests_drain_without_interrupt(monke
# Ensure INVOCATION_ID is NOT set — systemd sets this in service mode,
# which changes the restart call signature.
monkeypatch.delenv("INVOCATION_ID", raising=False)
monkeypatch.setattr(gateway_run, "is_container", lambda: False)
runner, _adapter = make_restart_runner()
runner.request_restart = MagicMock(return_value=True)
event = MessageEvent(
Expand Down
24 changes: 24 additions & 0 deletions tests/gateway/test_restart_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,29 @@ async def test_restart_command_uses_service_restart_under_systemd(tmp_path, monk
"""Under systemd (INVOCATION_ID set), /restart uses via_service=True."""
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
monkeypatch.setenv("INVOCATION_ID", "abc123")
monkeypatch.setattr(gateway_run, "is_container", lambda: False)

runner, _adapter = make_restart_runner()
runner.request_restart = MagicMock(return_value=True)

source = make_restart_source(chat_id="42")
event = MessageEvent(
text="/restart",
message_type=MessageType.TEXT,
source=source,
message_id="m1",
)

await runner._handle_restart_command(event)
runner.request_restart.assert_called_once_with(detached=False, via_service=True)


@pytest.mark.asyncio
async def test_restart_command_uses_service_restart_in_container(tmp_path, monkeypatch):
"""In Docker/Podman, /restart exits for the container restart policy."""
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
monkeypatch.delenv("INVOCATION_ID", raising=False)
monkeypatch.setattr(gateway_run, "is_container", lambda: True)

runner, _adapter = make_restart_runner()
runner.request_restart = MagicMock(return_value=True)
Expand All @@ -89,6 +112,7 @@ async def test_restart_command_uses_detached_without_systemd(tmp_path, monkeypat
"""Without systemd, /restart uses the detached subprocess approach."""
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
monkeypatch.delenv("INVOCATION_ID", raising=False)
monkeypatch.setattr(gateway_run, "is_container", lambda: False)

runner, _adapter = make_restart_runner()
runner.request_restart = MagicMock(return_value=True)
Expand Down