Symptom
Running hermes-gateway from a system-level systemd unit (operator deploys via NixOS / Ansible / etc., not via hermes gateway install) produces a permanent startup warning even when the actual system unit's TimeoutStopSec is correctly sized:
```
WARNING gateway.run: Stale systemd unit detected: hermes-gateway.service has
TimeoutStopSec=90s but drain_timeout=180s (expected >=210s). systemd may
SIGKILL the gateway mid-drain. Run `hermes gateway service install --replace`
to regenerate the unit, or shorten agent.restart_drain_timeout.
```
…but systemctl cat hermes-gateway.service confirms TimeoutStopSec=240s, which satisfies the documented drain_timeout + 30s headroom = 210s threshold. No mismatch in reality.
(Side note: the warning text also references the old CLI surface — should be hermes gateway install --replace, not hermes gateway service install --replace. The service sub-noun was removed.)
Root cause
gateway/shutdown_forensics.py:367-389 in 0.15.2:
```python
for flag in (["--user"], []):
try:
result = subprocess.run(
["systemctl", *flag, "show", unit_name, "--property=TimeoutStopUSec"],
capture_output=True, text=True, timeout=2.0,
)
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
continue
if result.returncode != 0:
continue
# … parses TimeoutStopUSec, sets timeout_us, breaks out
```
systemctl show is permissive: for a unit that does not exist in the targeted manager, it returns rc=0 and emits the systemd compiled-in defaults — TimeoutStopUSec=1min 30s (90s). It does not error out the way systemctl status <nonexistent> would.
When the gateway runs as User=hermes with XDG_RUNTIME_DIR=/run/user/<uid> set (mandatory for rootless podman in the same service), the user-bus is reachable. So systemctl --user show hermes-gateway.service --property=TimeoutStopUSec is the first iteration of the loop, succeeds with rc=0, returns the 90s default, parses, breaks — and the system unit (with the correct 240s) is never consulted.
Reproducer (on a host where the hermes user has no user-level units installed):
```bash
$ sudo -u hermes -H \
env XDG_RUNTIME_DIR=/run/user/1002 \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1002/bus \
systemctl --user list-unit-files | grep -i hermes
UNIT FILE STATE PRESET
57 unit files listed.
$ sudo -u hermes -H \
env XDG_RUNTIME_DIR=/run/user/1002 \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1002/bus \
systemctl --user show hermes-gateway.service \
--property=LoadState --property=TimeoutStopUSec
LoadState=not-found
TimeoutStopUSec=1min 30s # ← bogus default, since LoadState=not-found
```
Hermes blindly trusts the 90s value.
Proposed fix
Add LoadState to the same query and skip iterations where the unit isn't loaded:
```diff
for flag in (["--user"], []):
try:
result = subprocess.run(
-
[\"systemctl\", *flag, \"show\", unit_name, \"--property=TimeoutStopUSec\"],
-
[\"systemctl\", *flag, \"show\", unit_name,
-
\"--property=LoadState\",
-
\"--property=TimeoutStopUSec\"],
capture_output=True, text=True, timeout=2.0,
)
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
continue
if result.returncode != 0:
continue
-
Output: "TimeoutStopUSec=1min 30s" or "TimeoutStopUSec=90000000"
-
Skip if the unit isn't actually loaded in this manager — `systemctl
-
show` returns rc=0 and emits compiled-in defaults for nonexistent
-
units (notably TimeoutStopUSec=1min 30s), which would produce a
-
false-positive 'stale unit' warning when the gateway is deployed
-
via a system-level unit but the user manager is reachable.
- load_state: Optional[str] = None
- for line in result.stdout.splitlines():
-
if line.startswith(\"LoadState=\"):
-
load_state = line.split(\"=\", 1)[1].strip()
-
- if load_state and load_state != "loaded":
-
for line in result.stdout.splitlines():
if line.startswith("TimeoutStopUSec="):
...
```
Same selectivity, one extra property in the query, no other behaviour change.
Bonus: while touching the file, the warning text on gateway/run.py:3904 could be updated to reference hermes gateway install --replace (the service sub-noun was removed in a recent CLI refactor — the current text gives invalid choice: 'service').
Happy to send a PR.
Environment
- hermes-agent 0.15.2 (v2026.5.29.2)
- NixOS x86_64, gateway deployed via
systemd.services.hermes-gateway (system unit, NOT via hermes gateway install)
- gateway user:
hermes (linger=true, system service with XDG_RUNTIME_DIR=/run/user/<uid> set for rootless podman)
- No user-level hermes-gateway.service installed (verified via
systemctl --user list-unit-files)
Symptom
Running
hermes-gatewayfrom a system-level systemd unit (operator deploys via NixOS / Ansible / etc., not viahermes gateway install) produces a permanent startup warning even when the actual system unit'sTimeoutStopSecis correctly sized:```
WARNING gateway.run: Stale systemd unit detected: hermes-gateway.service has
TimeoutStopSec=90s but drain_timeout=180s (expected >=210s). systemd may
SIGKILL the gateway mid-drain. Run `hermes gateway service install --replace`
to regenerate the unit, or shorten agent.restart_drain_timeout.
```
…but
systemctl cat hermes-gateway.serviceconfirmsTimeoutStopSec=240s, which satisfies the documenteddrain_timeout + 30s headroom = 210sthreshold. No mismatch in reality.(Side note: the warning text also references the old CLI surface — should be
hermes gateway install --replace, nothermes gateway service install --replace. Theservicesub-noun was removed.)Root cause
gateway/shutdown_forensics.py:367-389in 0.15.2:```python
for flag in (["--user"], []):
try:
result = subprocess.run(
["systemctl", *flag, "show", unit_name, "--property=TimeoutStopUSec"],
capture_output=True, text=True, timeout=2.0,
)
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
continue
if result.returncode != 0:
continue
# … parses TimeoutStopUSec, sets timeout_us, breaks out
```
systemctl showis permissive: for a unit that does not exist in the targeted manager, it returnsrc=0and emits the systemd compiled-in defaults —TimeoutStopUSec=1min 30s(90s). It does not error out the waysystemctl status <nonexistent>would.When the gateway runs as
User=hermeswithXDG_RUNTIME_DIR=/run/user/<uid>set (mandatory for rootless podman in the same service), the user-bus is reachable. Sosystemctl --user show hermes-gateway.service --property=TimeoutStopUSecis the first iteration of the loop, succeeds with rc=0, returns the 90s default, parses, breaks — and the system unit (with the correct 240s) is never consulted.Reproducer (on a host where the hermes user has no user-level units installed):
```bash
$ sudo -u hermes -H \
env XDG_RUNTIME_DIR=/run/user/1002 \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1002/bus \
systemctl --user list-unit-files | grep -i hermes
UNIT FILE STATE PRESET
57 unit files listed.
$ sudo -u hermes -H \
env XDG_RUNTIME_DIR=/run/user/1002 \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1002/bus \
systemctl --user show hermes-gateway.service \
--property=LoadState --property=TimeoutStopUSec
LoadState=not-found
TimeoutStopUSec=1min 30s # ← bogus default, since LoadState=not-found
```
Hermes blindly trusts the 90s value.
Proposed fix
Add
LoadStateto the same query and skip iterations where the unit isn't loaded:```diff
for flag in (["--user"], []):
try:
result = subprocess.run(
continue
if result.returncode != 0:
continue
Output: "TimeoutStopUSec=1min 30s" or "TimeoutStopUSec=90000000"
Skip if the unit isn't actually loaded in this manager — `systemctl
show` returns rc=0 and emits compiled-in defaults for nonexistent
units (notably TimeoutStopUSec=1min 30s), which would produce a
false-positive 'stale unit' warning when the gateway is deployed
via a system-level unit but the user manager is reachable.
if line.startswith("TimeoutStopUSec="):
...
```
Same selectivity, one extra property in the query, no other behaviour change.
Bonus: while touching the file, the warning text on
gateway/run.py:3904could be updated to referencehermes gateway install --replace(theservicesub-noun was removed in a recent CLI refactor — the current text givesinvalid choice: 'service').Happy to send a PR.
Environment
systemd.services.hermes-gateway(system unit, NOT viahermes gateway install)hermes(linger=true, system service withXDG_RUNTIME_DIR=/run/user/<uid>set for rootless podman)systemctl --user list-unit-files)