Skip to content

fix(macos): clearly distinguish launchd supervision from detached fal… - #42567

Closed
Dr1985 wants to merge 1 commit into
NousResearch:mainfrom
Dr1985:main
Closed

Dr1985 wants to merge 1 commit into
NousResearch:mainfrom
Dr1985:main

Conversation

@Dr1985

@Dr1985 Dr1985 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

…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

What does this PR do?

This PR fixes ambiguous gateway status reporting on macOS 26.x. When launchd cannot supervise the gateway (exit code 5), Hermes falls back to a detached background process, but hermes gateway status previously reported "Gateway service is loaded" without mentioning that launchd isn't actually supervising, that a fallback PID is running, or that auto-start/restart are unavailable. The fix adds PID-aware launchd probing, a persistent unsupported marker, and clear status output distinguishing launchd supervision from detached fallback.

Related Issue

Fixes #42524

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_cli/gateway.py:
    • Added _parse_launchd_pid_from_list_output() to extract PID from launchctl list output
    • Fixed _probe_launchd_service_running() to require PID presence, not just exit code 0
    • Reworked launchd_status() with four clear output paths (supervised / fallback-running / fallback-unavailable / not-loaded)
    • Added persistent unsupported marker (~/.hermes/.gateway-launchd-unsupported) with write/clear/exists helpers
    • Updated _print_gateway_process_mismatch() to distinguish fallback from manual run
    • Marker written in _launchd_fallback_to_detached(), cleared on 5 bootstrap success paths
  • tests/hermes_cli/test_gateway_service.py:
    • Updated 5 existing tests to verify marker creation on fallback
    • Added 11 new tests: PID parsing (4), probe logic (2), marker lifecycle (2), status output (3)

How to Test

  1. On macOS 26.x, run hermes gateway start — observe fallback message and marker created at ~/.hermes/.gateway-launchd-unsupported
  2. Run hermes gateway status — verify output clearly shows:
    • "launchd cannot manage the gateway on this macOS version"
    • "Detached fallback process is running (PID X)"
    • "Auto-start at login and auto-restart on crash are NOT available"
  3. Stop the gateway: hermes gateway stop — verify fallback PID is cleaned up
  4. Run hermes gateway status again — verify it shows "No fallback process is running" with clear guidance
  5. On a macOS version where launchd works normally, verify gateway status shows "Gateway is supervised by launchd (PID X)"
  6. Run scripts/run_tests.sh tests/hermes_cli/test_gateway_service.py::TestLaunchdServiceRecovery -v

Checklist

Code

  • I've read the Contributing Guide

  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)

  • I searched for existing PRs to make sure this isn't a duplicate

  • My PR contains only changes related to this fix/feature (no unrelated commits)

  • I've run pytest tests/ -q and all tests pass

  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)

  • I've tested on my platform: macOS 26.x

    Documentation & Housekeeping

    • N/A: code docstrings are updated inline, no user-facing docs needed
    • N/A: no config keys changed
    • N/A: no architecture change, just a bug fix in existing code paths
    • [x]macOS-only code path (launchd is macOS-specific), already gated behind is_macos().
      No Windows/Linux impact. New functions are only called from macOS code paths.
    • N/A: no tool behavior changed

For New Skills

  • This skill is broadly useful to most users (if bundled) — see Contributing Guide
  • SKILL.md follows the standard format (frontmatter, trigger conditions, steps, pitfalls)
  • No external dependencies that aren't already available (prefer stdlib, curl, existing Hermes tools)
  • I've tested the skill end-to-end: hermes --toolsets skills -q "Use the X skill to do Y"

Screenshots / Logs

…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)
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard area/config Config system, migrations, profiles labels Jun 9, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved

Overview

This PR improves macOS launchd supervision detection by distinguishing between launchd actively supervising a process vs. a registered-but-not-running service (macOS 26+ behavior).

Changes

  • Adds _parse_launchd_pid_from_list_output() to extract PID from launchctl output
  • Updates _probe_launchd_service_running() to require a PID for active supervision
  • Adds unsupported marker persistence for launchd domain issues
  • Updates status reporting to explain when launchd cannot supervise
  • Extensive test coverage for all scenarios

Quality

  • Clean, well-scoped fix
  • Good documentation
  • No security concerns
  • Excellent test coverage

Reviewed by Hermes Agent

@kshitijk4poor

Copy link
Copy Markdown
Contributor

Salvaged into #52984 (rebased onto current main, 506 commits ahead — cherry-pick applied cleanly, 3 fixes applied for main drift: missing json/datetime imports, case-sensitivity in test assertions, temp-home guard mock). All credit to @Dr1985 for the original fix. Thanks for the contribution!

Note: PR #52983 (AUTHOR_MAP chore) must merge first.

pai-scaffolde pushed a commit to Scaffolde/hermes-agent that referenced this pull request Jun 28, 2026
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles 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

4 participants