Bug Description
On macOS, hermes gateway restart --all is non-atomic. It runs launchctl bootout (which unloads the service definition) and only later runs launchctl bootstrap. If the CLI process dies between the two — closed terminal pane, Ctrl+C, SIGHUP — the service stays unloaded and KeepAlive cannot revive the gateway. No error is shown.
Steps to Reproduce
hermes gateway start → confirm launchctl list | grep ai.hermes.gateway shows a PID.
- In a closable pane:
hermes gateway restart --all
- Close the pane immediately.
launchctl list | grep ai.hermes.gateway → empty. Gateway gone, no auto-recovery.
Expected
Service definition stays loaded; KeepAlive keeps the gateway alive.
Actual
launchctl bootout unloads the service. launchctl bootstrap never runs. Trace from log show --predicate 'process == "launchd"':
12:13:10.159 bootout initiated by: launchctl<-python3.11<-zsh<-zellij
12:13:10.159 signaled service: Terminated: 15
12:13:11.088 exited due to exit(1)
12:13:11.088 removing service: ai.hermes.gateway
12:13:11.088 service state: not running
No subsequent bootstrap event.
Component / Env
- Gateway
- macOS 15.4 (Darwin 25.4.0, arm64), Python 3.11.14, Hermes 0.14.0
Root Cause
hermes_cli/gateway.py:5234-5287 — the --all branch on macOS calls launchd_stop() (bootout) → kill_gateway_processes → launchd_start() (bootstrap). bootout unloads, so the window between the two is unsafe.
The non---all path (launchd_restart, gateway.py:3021) already does the right thing — launchctl kickstart -k cycles the process under the same job, no unload.
Proposed Fix
In the --all macOS branch, use launchd_restart() (kickstart -k) instead of bootout/bootstrap. Exclude the current PID from the sweep so the restart isn't killed. PR follows.
Willing to Submit a PR
Yes.
Bug Description
On macOS,
hermes gateway restart --allis non-atomic. It runslaunchctl bootout(which unloads the service definition) and only later runslaunchctl bootstrap. If the CLI process dies between the two — closed terminal pane, Ctrl+C, SIGHUP — the service stays unloaded andKeepAlivecannot revive the gateway. No error is shown.Steps to Reproduce
hermes gateway start→ confirmlaunchctl list | grep ai.hermes.gatewayshows a PID.hermes gateway restart --alllaunchctl list | grep ai.hermes.gateway→ empty. Gateway gone, no auto-recovery.Expected
Service definition stays loaded; KeepAlive keeps the gateway alive.
Actual
launchctl bootoutunloads the service.launchctl bootstrapnever runs. Trace fromlog show --predicate 'process == "launchd"':No subsequent
bootstrapevent.Component / Env
Root Cause
hermes_cli/gateway.py:5234-5287— the--allbranch on macOS callslaunchd_stop()(bootout) →kill_gateway_processes→launchd_start()(bootstrap).bootoutunloads, so the window between the two is unsafe.The non-
--allpath (launchd_restart,gateway.py:3021) already does the right thing —launchctl kickstart -kcycles the process under the same job, no unload.Proposed Fix
In the
--allmacOS branch, uselaunchd_restart()(kickstart -k) instead ofbootout/bootstrap. Exclude the current PID from the sweep so the restart isn't killed. PR follows.Willing to Submit a PR
Yes.