Skip to content

fix(macos): distinguish launchd supervision from detached fallback in gateway status (#42524) - #52984

Merged
kshitijk4poor merged 1 commit into
NousResearch:mainfrom
kshitijk4poor:salvage/42567-launchd-exit5
Jun 26, 2026
Merged

kshitijk4poor merged 1 commit into
NousResearch:mainfrom
kshitijk4poor:salvage/42567-launchd-exit5

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Contributor

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 bootstrap and kickstart return exit code 5 ("Input/output error"), which Hermes handles by spawning a detached fallback process. However, gateway status was 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 warnings
  • Users couldn't tell whether auto-start at login and auto-restart on crash were available

Fix

  1. _parse_launchd_pid_from_list_output() — extracts PID from launchctl list output. When launchd is actively supervising, 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.

  3. 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.

  4. Persistent unsupported marker (~/.hermes/.gateway-launchd-unsupported) — written when _launchd_fallback_to_detached() is called. Cleared automatically when bootstrap/kickstart succeeds.

  5. Updated _print_gateway_process_mismatch() — distinguishes managed detached fallback from a genuinely manual nohup hermes gateway run.

Fixes applied during salvage (main drift)

  • Added missing import json and from datetime import datetime, timezone inside _write_launchd_unsupported_marker() — the original PR used these without importing them (module-level json was only a local import in a different function)
  • Fixed case-sensitivity in 2 test assertions: out.lower() was compared against mixed-case strings containing macOS
  • Mocked _refuse_temp_home_service_write in test_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 directories

Testing

  • 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 -> passed

Depends 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

@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists labels Jun 26, 2026
@kshitijk4poor
kshitijk4poor force-pushed the salvage/42567-launchd-exit5 branch from 938ef41 to f18bf26 Compare June 26, 2026 08:53
…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
kshitijk4poor force-pushed the salvage/42567-launchd-exit5 branch from f18bf26 to 35d38fb Compare June 26, 2026 10:05
@kshitijk4poor
kshitijk4poor merged commit e3db1ef into NousResearch:main Jun 26, 2026
27 checks passed
@kshitijk4poor
kshitijk4poor deleted the salvage/42567-launchd-exit5 branch August 5, 2026 07:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

macOS 26: gateway start/restart refreshes LaunchAgent but launchctl exits 5 and falls back to detached process

3 participants