Skip to content

fix(gateway): revive gateway on /restart under system-scope + Restart=on-failure units - #56362

Merged
teknium1 merged 2 commits into
mainfrom
hermes/hermes-d04d58a5
Jul 1, 2026
Merged

teknium1 merged 2 commits into
mainfrom
hermes/hermes-d04d58a5

Conversation

@teknium1

@teknium1 teknium1 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

/restart now revives the gateway on systemd deployments it previously left dead: system-scope units and units using Restart=on-failure rather than Restart=always.

Salvage of #50204 by @randomuser2026x, cherry-picked onto current main with authorship preserved, plus a follow-up comment fix + AUTHOR_MAP entry.

Root cause

Two independent gaps in the planned-restart path:

  1. Scope hardcoded to --user. _launch_systemd_restart_shortcut() ran systemctl --user show ... MainPID to confirm it owned the unit. On a system-unit deployment (/etc/systemd/system/hermes-gateway.service, User=/Group=) that returns an empty MainPID, the PID-equality check fails, and the helper silently no-ops — no relaunch is scheduled.
  2. Exit 0 on Linux only relaunches under Restart=always. The primary teardown path exited 0 on Linux systemd (conditional on INVOCATION_ID), which relies on Restart=always to respawn. Operator-managed units using Restart=on-failure treat exit 0 as success and never restart — gateway stays dead until a reboot. (The fallback path at the bottom of the same function already exited 75 unconditionally, so main was internally inconsistent.)

Changes

  • gateway/run.py: probe both systemctl show and systemctl --user show, pick the scope whose MainPID matches this PID (bail if neither does, rather than restart the wrong unit); thread the resolved scope through systemd-run and the reset-failed/restart commands.
  • gateway/run.py: always set the service-restart exit code to 75. RestartForceExitStatus=75 in the generated unit whitelists the planned code, so it revives under both Restart=always and Restart=on-failure; genuine crashes still exit non-zero-but-not-75.
  • scripts/release.py: AUTHOR_MAP entry for the contributor (CI gate).

Validation

Before After
System-unit /restart helper no-ops (empty --user MainPID) → dead scope resolves to system → relaunches
User-unit /restart works works (unchanged)
MainPID mismatch restarts hardcoded --user unit bails, no wrong-unit restart
Restart=on-failure unit exit 0 → stays dead exit 75 → revived
  • tests/gateway/test_gateway_shutdown.py: 20/20 pass (assertion updated to expect exit 75).
  • E2E: faked systemctl proving all three scope branches (system picked, user picked, mismatch bails).

Infographic

Gateway /restart revived

Nous Research

@teknium1
teknium1 force-pushed the hermes/hermes-d04d58a5 branch from cb610a2 to a7189f4 Compare July 1, 2026 12:01
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages P1 High — major feature broken, no workaround labels Jul 1, 2026
randomuser2026x and others added 2 commits July 1, 2026 05:11
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.
- Correct the exit-75 comment: Hermes-generated units set
  StartLimitIntervalSec=0 (rate limiting disabled), so StartLimitBurst
  does not bound loops. The real bound is that genuine crashes exit
  non-zero-but-not-75, and RestartForceExitStatus=75 only whitelists
  the planned code.
- Add randomuser2026x AUTHOR_MAP entry (CI blocks unmapped emails).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P1 High — major feature broken, no workaround sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants