{"repo":"NousResearch/hermes-agent","public_id":"31981","display_label":"systemd timeout mismatch and restart loop fixes","summary":"These issues and PRs address a systemd TimeoutStopSec mismatch that causes silent crash or restart loops in the gateway, with proposed fixes including detection improvements, default timeout changes, subprocess cleanup, and the --replace flag.","subsystem":"gateway","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":54066,"merged_at":"2026-06-28T08:14:35+00:00"},"resolution":{"state":"resolved","by":"pr-54066"},"freshness":{"state":"verified","last_verified_at":"2026-06-28T08:14:35+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":31981,"pr_number":54066,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-28T08:14:35+00:00"}],"members":[{"kind":"issue","number":31981,"title":"systemd TimeoutStopSec mismatch causes silent crash loop; suggested fix command is wrong","url":null,"state":"closed"},{"kind":"pr","number":41525,"title":"fix(gateway): stop stale systemd timeout mismatch loop","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#54066","number":54066}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":54066,"title":"fix(gateway): default restart_drain_timeout to 0 to kill systemd crash loop","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli-config.yaml.example","gateway/run.py","hermes_cli/config.py","tests/gateway/test_gateway_shutdown.py"],"fix_hunks":[{"file":"cli-config.yaml.example","hunks":["@@ -626,10 +626,10 @@ agent:\n   # gateway_timeout_warning: 900\n \n   # Graceful drain timeout for gateway stop/restart (seconds).\n-  # The gateway stops accepting new work, waits for in-flight agents to\n-  # finish, then interrupts anything still running after this timeout.\n-  # 0 = no drain, interrupt immediately.\n-  # restart_drain_timeout: 60\n+  # Default 0 = no drain: a restart interrupts in-flight agents immediately,\n+  # cleans up, and exits. Set a positive value only if you want a grace\n+  # window on /restart, and keep it well under systemd's TimeoutStopSec.\n+  # restart_drain_timeout: 0\n \n   # Max app-level retry attempts for API errors (connection drops, provider\n   # timeouts, 5xx, etc.) before the agent surfaces the failure. Lower this"]},{"file":"gateway/run.py","hunks":["@@ -5811,7 +5811,7 @@ async def start(self) -> bool:\n                 logger.warning(\n                     \"Stale systemd unit detected: %s has TimeoutStopSec=%.0fs but \"\n                     \"drain_timeout=%.0fs (expected >=%.0fs). systemd may SIGKILL the \"\n-                    \"gateway mid-drain. Run `hermes gateway service install --replace` \"\n+                    \"gateway mid-drain. Run `hermes gateway install --force` \"\n                     \"to regenerate the unit, or shorten agent.restart_drain_timeout.\",\n                     _alignment.get(\"unit\", \"(unknown)\"),\n                     _alignment[\"timeout_stop_sec\"],"]},{"file":"hermes_cli/config.py","hunks":["@@ -916,13 +916,18 @@ def _ensure_hermes_home_managed(home: Path):\n         # Graceful drain timeout for gateway stop/restart (seconds).\n         # The gateway stops accepting new work, waits for running agents\n         # to finish, then interrupts any remaining runs after the timeout.\n-        # 0 = no drain, interrupt immediately.\n+        # 0 = no drain, interrupt immediately (the default).\n         #\n-        # 180s is calibrated for realistic in-flight agent turns: a typical\n-        # coding conversation mid-reasoning runs 60–150s per call, so a 60s\n-        # budget routinely interrupted legitimate work on /restart. Raise\n-        # further in config.yaml if you run very-long-reasoning models.\n-        \"restart_drain_timeout\": 180,\n+        # Contract: if you restart the gateway, in-flight work stops. We do\n+        # not hold the restart open for a grace window — a drain timeout\n+        # large enough to \"save\" a long agent turn would have to outlast an\n+        # unbounded task (some runs take days), which is impossible, and a\n+        # drain timeout shorter than systemd's TimeoutStopSec invites a\n+        # SIGKILL-mid-cleanup race that leaves a stale lock and crash-loops\n+        # the service. 0 sidesteps both: interrupt now, clean up, exit fast.\n+        # Set a positive value in config.yaml only if you explicitly want a\n+        # grace window on /restart (and keep it well under TimeoutStopSec).\n+        \"restart_drain_timeout\": 0,\n         # Max app-level retry attempts for API errors (connection drops,\n         # provider timeouts, 5xx, etc.) before the agent surfaces the\n         # failure.  The OpenAI SDK already does its own low-level retries"]},{"file":"tests/gateway/test_gateway_shutdown.py","hunks":["@@ -94,6 +94,10 @@ async def block_forever(_event):\n @pytest.mark.asyncio\n async def test_gateway_stop_drains_running_agents_before_disconnect():\n     runner, adapter = make_restart_runner()\n+    # Opt into a grace window (the default is 0 = interrupt immediately).\n+    # This exercises the path where an agent finishes within the drain\n+    # window and must NOT be interrupted.\n+    runner._restart_drain_timeout = 5.0\n     disconnect_mock = AsyncMock()\n     adapter.disconnect = disconnect_mock\n \n"]}]},{"kind":"pr","number":60776,"title":"fix(gateway): expand Telegram fallback IPs, fix TimeoutStopSec detection, log connection success","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#31981","number":31981},{"kind":"pr","value":"#32021","number":32021}],"category":"duplicate_of","implication":"duplicate"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}