fix(macos): distinguish launchd supervision from detached fallback in gateway status (#42524) - #52984
Merged
kshitijk4poor merged 1 commit intoJun 26, 2026
Conversation
This was referenced Jun 26, 2026
kshitijk4poor
force-pushed
the
salvage/42567-launchd-exit5
branch
from
June 26, 2026 08:53
938ef41 to
f18bf26
Compare
…lback in gateway status
## Description
On macOS 26.x, `launchctl bootstrap` and `launchctl kickstart` return exit code 5 ("Input/output error"), which Hermes already anticipates and handles by spawning a detached fallback process. However, the gateway status reporting is ambiguous:
- `gateway status` says "Gateway service is loaded" (because `launchctl list` returns exit 0)
- But `launchctl print` shows `state = not running` — launchd isn't actually supervising anything
- The detached fallback PID running is invisible to the status command
- Users can't tell whether auto-start at login and auto-restart on crash are available
### Root Cause
Two problems in `hermes_cli/gateway.py`:
1. **`_probe_launchd_service_running()`** (line 1067): Determined launchd service liveness solely by `launchctl list <label>` exit code. On macOS 26, this returns 0 even when the service is only *registered* but not running (output lacks a `"PID"` field). This caused `GatewayRuntimeSnapshot.service_running = True` incorrectly, which suppressed the process/service mismatch warning.
2. **`launchd_status()`** (line 3569): Used the same binary "loaded/not loaded" check without inspecting whether launchd actually has a PID, whether a detached fallback is running, or whether auto-start/restart are available.
### Changes
**`hermes_cli/gateway.py`:**
1. **New `_parse_launchd_pid_from_list_output()` helper** — Extracts the PID from `launchctl list` output. When launchd is actively supervising, the output includes `"PID" = <number>;`. When only registered but not running, no PID field is present.
2. **Fixed `_probe_launchd_service_running()`** — Now requires a PID in the `launchctl list` output to confirm launchd is actually supervising. This correctly sets `service_running = False` when launchd has the service registered but `state = not running`, which triggers the existing process/service mismatch detection.
3. **Reworked `launchd_status()`** — Reports clearly separated information:
- LaunchAgent plist currentness (stale or current)
- Whether launchd is actively supervising (with PID)
- Whether a detached fallback PID is running
- Whether auto-start at login and auto-restart on crash are available
- When launchd supervision is known to be unavailable, explains why
4. **Persistent unsupported marker** (`~/.hermes/.gateway-launchd-unsupported`) — Written when `_launchd_fallback_to_detached()` is called (launchd exit 5/125). Allows `launchd_status()` to explain *why* launchd can't supervise even when no fallback process is currently running. Cleared automatically when a future bootstrap/kickstart succeeds (e.g., after an OS update fixes the issue).
5. **Updated `_print_gateway_process_mismatch()`** — Distinguishes the managed detached fallback from a genuinely manual `nohup hermes gateway run`, providing accurate guidance for each case.
### Status Output Examples
**Before** (macOS 26, fallback active):
```
Launchd plist: ~/Library/LaunchAgents/ai.hermes.gateway.plist
✓ Service definition matches the current Hermes install
✓ Gateway service is loaded
{
"Label" = "ai.hermes.gateway";
"OnDemand" = true;
...
};
```
**After** (macOS 26, fallback active):
```
Launchd plist: ~/Library/LaunchAgents/ai.hermes.gateway.plist
✓ Service definition matches the current Hermes install
⚠ Gateway service is registered but launchd is not supervising it
launchd cannot manage the gateway on this macOS version.
✓ Detached fallback process is running (PID 12345)
Cron jobs will fire. Stop with: hermes gateway stop
⚠ Auto-start at login and auto-restart on crash are NOT available.
```
**After** (normal launchd supervision):
```
Launchd plist: ~/Library/LaunchAgents/ai.hermes.gateway.plist
✓ Service definition matches the current Hermes install
✓ Gateway is supervised by launchd (PID 12345)
Auto-start at login and auto-restart on crash are available.
```
### Tests
Updated 5 existing tests and added 11 new tests in `tests/hermes_cli/test_gateway_service.py`:
- PID parsing from `launchctl list` output (with PID, without PID, empty, unquoted PID)
- `_probe_launchd_service_running()` requires PID presence
- Unsupport marker lifecycle (write, clear, persist across fallback)
- Marker cleared on successful bootstrap
- `launchd_status()` reporting: supervised, fallback-running, fallback-unavailable
- Existing fallback tests now verify marker creation
### Related Issues
- Issue NousResearch#23387 (original macOS 26 launchd workaround)
- Issue NousResearch#42524 (this issue)
kshitijk4poor
force-pushed
the
salvage/42567-launchd-exit5
branch
from
June 26, 2026 10:05
f18bf26 to
35d38fb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Salvage of #42567 by @Dr1985 (rebased onto current main, 506 commits ahead — cherry-pick applied cleanly, 3 fixes applied for main drift).
On macOS 26.x,
launchctl bootstrapandkickstartreturn exit code 5 ("Input/output error"), which Hermes handles by spawning a detached fallback process. However,gateway statuswas ambiguous:launchctl list <label>returns exit 0 even when the service is only registered but not running (no PID)_probe_launchd_service_running()returned True incorrectly, suppressing mismatch warningsFix
_parse_launchd_pid_from_list_output()— extracts PID fromlaunchctl listoutput. When launchd is actively supervising, output includes"PID" = <number>;. When only registered but not running, no PID field is present.Fixed
_probe_launchd_service_running()— now requires a PID in thelaunchctl listoutput to confirm launchd is actually supervising.Reworked
launchd_status()— reports clearly separated information: plist currentness, whether launchd is actively supervising (with PID), whether a detached fallback is running, and whether auto-start/restart are available.Persistent unsupported marker (
~/.hermes/.gateway-launchd-unsupported) — written when_launchd_fallback_to_detached()is called. Cleared automatically when bootstrap/kickstart succeeds.Updated
_print_gateway_process_mismatch()— distinguishes managed detached fallback from a genuinely manualnohup hermes gateway run.Fixes applied during salvage (main drift)
import jsonandfrom datetime import datetime, timezoneinside_write_launchd_unsupported_marker()— the original PR used these without importing them (module-leveljsonwas only a local import in a different function)out.lower()was compared against mixed-case strings containingmacOS_refuse_temp_home_service_writeintest_launchd_start_clears_unsupported_marker_on_bootstrap_success— this guard was added on main after the PR was created and blocks plist writes to temp directoriesTesting
pytest tests/hermes_cli/test_gateway_service.py-> 168 passed, 6 failed (all pre-existing systemd failures on macOS, same as origin/main)ruff check hermes_cli/gateway.py tests/hermes_cli/test_gateway_service.py-> passedDepends on
PR #52983 (AUTHOR_MAP chore — adds Dr1985 email mapping)
Closes #42524
Closes #42567
Closes #42588
Co-authored-by: Dr1985 140971685+Dr1985@users.noreply.github.com