hermes-agent/gateway
Z 40dbfa0e3c fix(gateway): revive gateway on /restart under Restart=on-failure units
The in-chat /restart command was leaving the gateway dead on systemd
deployments using Restart=on-failure (the default for many
operator-managed and tutorial-style unit files). The gateway drained,
exited cleanly (code 0), and was never revived — the only recovery was
a host reboot.

Root cause was a multi-layer assumption mismatch:

1. gateway/run.py:_stop_impl assumed all systemd units use
   Restart=always, so the Linux/systemd branch returned exit code 0
   and relied on a `systemd-run` transient helper to restart the unit
   immediately. Units with Restart=on-failure never see a clean exit
   as a trigger, so nothing revived the process.

2. gateway/run.py:_launch_systemd_restart_shortcut hardcoded
   `--user` scope, so it could not even locate the unit PID on
   system-level deployments (the common case for
   /etc/systemd/system/hermes-gateway.service). It silently returned
   without launching the helper.

3. Even after the scope detection was fixed, the helper could not
   actually start: non-root gateway units (User=ubunutu) hit a Polkit
   denial on `systemd-run --system` ("Interactive authentication
   required"), and `--user` requires a D-Bus user session that is
   typically absent on headless servers.

The fix is two-fold:

* `_stop_impl` now always exits with GATEWAY_SERVICE_RESTART_EXIT_CODE
  (75 / EX_TEMPFAIL) on service-managed restarts, regardless of
  platform. Combined with RestartForceExitStatus=75 in the unit file,
  systemd treats the planned restart as a controlled failure and
  revives the gateway via Restart=on-failure, with RestartSec as the
  only delay. The planned-restart helper is still attempted (for
  RestartSec=0 setups that want sub-second restarts) but is no longer
  load-bearing.

* `_launch_systemd_restart_shortcut` now probes both system and user
  scopes via MainPID equality and uses whichever scope actually owns
  the gateway process. It bails out safely if neither matches.

StartLimitBurst in the unit file still bounds accidental restart
loops, and the macOS launchd path is unchanged.

Verified end-to-end on Ubuntu 24.04 with hermes-gateway as a
/etc/systemd/system/... service running under User=ubunutu. The
unit uses Restart=on-failure, RestartSec=30, RestartForceExitStatus=75,
StartLimitIntervalSec=600, StartLimitBurst=5. /restart from Feishu now
drains cleanly, exits 75, and the gateway is back online ~30s later
without manual intervention.

Tests: tests/gateway/test_gateway_shutdown.py renamed the affected
case to test_gateway_stop_systemd_service_restart_uses_tempfail and
now asserts exit_code == GATEWAY_SERVICE_RESTART_EXIT_CODE.
14/14 tests in this module pass.
2026-07-01 05:13:03 -07:00
..
assets fix: improve telegram topic mode setup 2026-05-04 12:07:17 -07:00
builtin_hooks remove: BOOT.md built-in hook (#17093) 2026-04-28 09:50:27 -07:00
platforms fix(api-server): require auth for /health/detailed and fail closed on weak keys 2026-07-01 04:14:33 -07:00
relay refactor(relay): purge platform-specific scope terminology from the relay adapter (D-Q2.5c) (#56016) 2026-07-01 12:30:59 +10:00
__init__.py docs(gateway): mention Weixin in gateway help and docstrings 2026-05-12 17:08:51 -07:00
authz_mixin.py fix(gateway): pairing store cannot bypass configured allowlist 2026-07-01 04:56:25 -07:00
cgroup_cleanup.py fix: satisfy ruff encoding + windows-footgun lints for cgroup reaper 2026-06-28 02:05:50 -07:00
channel_directory.py docs(sessions): clarify sessions.json is the gateway routing index, not the session list (#51726) 2026-06-23 23:56:36 -07:00
code_skew.py fix(gateway): refuse model switch on stale checkout to avoid env_float ImportError 2026-06-24 04:16:54 +05:30
config.py feat(cron/slack): flat in-channel continuable cron delivery surface 2026-07-01 03:16:13 -07:00
dead_targets.py fix(gateway): skip confirmed-dead delivery targets (deleted groups, blocked bots) (#55115) 2026-06-29 13:23:29 -07:00
delivery.py refactor(gateway): share error-text blob between not_found classifiers 2026-07-01 15:11:38 +05:30
display_config.py feat(discord): render reasoning as -# subtext via display.reasoning_style (#51168) 2026-06-23 10:44:02 -07:00
drain_control.py feat(gateway): suppress home-channel shutdown broadcast on flagged drains (#54824) 2026-06-29 12:18:11 -07:00
hooks.py feat(hooks): expose thread_id and chat_type in agent:start/end context (#41672) 2026-06-07 19:16:36 -07:00
kanban_watchers.py fix(kanban): honor kanban.auto_decompose toggle live, without a gateway restart (#50358) 2026-06-21 12:43:44 -07:00
memory_monitor.py Port from cline/cline#10343: periodic gateway memory logging (#27102) 2026-05-16 12:55:23 -07:00
message_timestamps.py feat(gateway): inject stable human-readable message timestamps 2026-06-16 15:49:59 -07:00
mirror.py fix(cron): mirror continuable cron as a labelled user turn (alternation-safe) 2026-06-24 20:27:05 -07:00
pairing.py fix(gateway): preserve WhatsApp pairing approvals across JID/LID alias flips 2026-05-23 01:46:34 -07:00
platform_registry.py perf(startup): lazy-load gateway platform adapters (#54448) 2026-06-28 15:11:59 -07:00
response_filters.py fix(gateway): suppress NO_REPLY/[SILENT] markers on the streaming path 2026-06-30 23:37:04 -07:00
restart.py fix(gateway): exit 78 (EX_CONFIG) on fatal startup errors, s6 finish script stops restart loop 2026-06-24 16:34:51 +10:00
restart_loop_guard.py fix(gateway,cron): guard cron model-tool path + add auto-resume loop breaker (#30719) 2026-07-01 02:48:36 -07:00
rich_sent_store.py style(profile): frame comments around what the code does 2026-06-30 15:30:06 -07:00
run.py fix(gateway): revive gateway on /restart under Restart=on-failure units 2026-07-01 05:13:03 -07:00
runtime_footer.py chore: prune unused imports and duplicate import redefinitions 2026-05-28 22:26:25 -07:00
scale_to_zero.py feat(gateway): scale-to-zero idle detection + dormant-quiesce (Phase 0) 2026-06-24 18:47:18 -07:00
session.py fix(gateway): freshness-gate resume_pending against per-message zombies 2026-07-01 03:17:20 -07:00
session_context.py fix(gateway): close cross-session HERMES_SESSION_* leak into subprocess env 2026-07-01 15:42:19 +05:30
shutdown_forensics.py chore: ruff auto-fixes — collapsible-else-if, if-stmt-min-max, dict.fromkeys (#23926) 2026-05-11 11:03:29 -07:00
slash_access.py feat(gateway): per-platform admin/user split for slash commands (salvage of #4443) (#23373) 2026-05-10 12:33:54 -07:00
slash_commands.py fix(gateway): persist compressed transcript before repointing /compress session 2026-07-01 01:39:23 -07:00
status.py fix(windows): cover remaining console-flash spawn legs (#54417) 2026-06-28 13:49:08 -07:00
sticker_cache.py fix: guard yaml.safe_load, flock unlock, TOCTOU races, and atomic writes 2026-05-19 00:12:41 -07:00
stream_consumer.py fix: make streaming reasoning-tag filter case-insensitive 2026-07-01 03:25:02 -07:00
stream_dispatch.py feat(gateway): structured stream-event protocol + Telegram draft formatting parity (#37250) 2026-06-02 00:33:50 -07:00
stream_events.py feat(gateway): structured stream-event protocol + Telegram draft formatting parity (#37250) 2026-06-02 00:33:50 -07:00
whatsapp_identity.py fix(whatsapp): resolve LID aliases on modern platforms/ session layout 2026-06-28 02:05:26 -07:00