Skip to content

fix(update): refresh stale launchd plists and restart all profile gateways on macOS#38627

Open
joelbrilliant1-beep wants to merge 1 commit into
NousResearch:mainfrom
joelbrilliant1-beep:fix/macos-update-launchd-refresh
Open

fix(update): refresh stale launchd plists and restart all profile gateways on macOS#38627
joelbrilliant1-beep wants to merge 1 commit into
NousResearch:mainfrom
joelbrilliant1-beep:fix/macos-update-launchd-refresh

Conversation

@joelbrilliant1-beep

Copy link
Copy Markdown

What does this PR do?

On macOS, hermes update left running gateways on stale launchd service definitions and only restarted the current profile's LaunchAgent, leaving named-profile gateways pinned to the old code.

Two root causes:

  1. Stale definitions are never reloaded. launchd does not hot-reload a plist — a changed definition is only applied by bootout/bootstrap. The update postflight called launchd_restart(), which kickstarts the cached definition, so a service whose plist changed across the update came back running the old one. launchd_start() self-heals via refresh_launchd_plist_if_needed(); the update path did not, forcing users to manually run hermes gateway start.
  2. Only the current profile was restarted. The macOS branch inspected a single get_launchd_label() / get_launchd_plist_path(). The Linux/systemd branch right above it already enumerates all hermes-gateway* units; launchd was the outlier.

Fix

New restart_launchd_gateways_for_update() in hermes_cli/gateway.py:

  • Discovers every ~/Library/LaunchAgents/ai.hermes.gateway*.plist (the launchd analogue of systemctl list-units hermes-gateway* — all profiles share one LaunchAgents dir).
  • Reads each plist's own recorded HERMES_HOME and re-enters that profile's context via the existing _HERMES_HOME_OVERRIDE ContextVar, so each plist is regenerated for the correct profile.
  • For each loaded service: graceful-restarts a current definition, or (for a stale one) drains in-flight agent runs with SIGUSR1 first, then refresh_launchd_plist_if_needed() does the bootout/bootstrap reload.

The macOS update postflight now delegates to it. launchd_restart() is unchanged, so its existing graceful-restart contract (and tests) are preserved.

Related Issue

Fixes #38053

This is distinct from the other open macOS launchd-restart PRs: it specifically addresses multi-profile enumeration and stale-plist refresh in the hermes update postflight, rather than KeepAlive/missing-plist/throttle behaviour in launchd_restart() itself (which is left untouched).

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✅ Tests (adding or improving test coverage)

Changes Made

  • hermes_cli/gateway.py: add _launchd_agents_dir, _iter_hermes_launchd_plists, _hermes_home_from_launchd_plist, _launchd_service_is_loaded, and restart_launchd_gateways_for_update(). No existing function is modified (only import plistlib is added).
  • hermes_cli/main.py: replace the single-profile macOS restart branch in _cmd_update_impl with a delegation to a small helper _restart_macos_launchd_gateways().
  • tests/hermes_cli/test_gateway_service.py: TestLaunchdUpdatePostflight (11 tests).
  • tests/hermes_cli/test_cmd_update.py: TestRestartMacosLaunchdGatewaysPostflight (2 tests).

How to Test

All side effects (launchctl, signals, subprocess) are mocked — no live gateway commands run.

pytest tests/hermes_cli/test_gateway_service.py::TestLaunchdUpdatePostflight \
       tests/hermes_cli/test_cmd_update.py::TestRestartMacosLaunchdGatewaysPostflight -q

Coverage includes: stale plist rewritten + bootout/bootstrap reload; a named profile regenerated in its own context (keeps its --profile arg); unloaded agents skipped; unreadable plists skipped; one profile failing does not abort the rest; and an assertion that every subprocess invocation is launchctl (proving no live hermes commands are needed).

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(update): ...)
  • I searched for existing PRs to make sure this isn't a duplicate (this targets the unclaimed [Bug]: macOS launchd: hermes update does not restart profile gateways #38053; see Related Issue)
  • My PR contains only changes related to this fix
  • I've run the gateway + update test suites and they pass — note: a full pytest tests/ -q on macOS has pre-existing systemd/seed_supervise env failures unrelated to this change (CI runs the full suite on Linux)
  • I've added tests for my changes (13)
  • I've tested on my platform: macOS (Darwin 25.5)

Documentation & Housekeeping

  • Docstrings added; README/docs/ — N/A
  • No config keys added/changed — cli-config.yaml.example N/A
  • No architecture/workflow change — CONTRIBUTING.md/AGENTS.md N/A
  • Cross-platform impact considered — the launchd path is macOS-only; systemd/Windows untouched
  • No tool behaviour/schema change — N/A

🤖 Generated with Claude Code

…eways on macOS

`hermes update` on macOS left gateways on stale launchd service
definitions and only ever restarted the current profile's LaunchAgent.

launchd never hot-reloads a plist (only bootout/bootstrap applies a
changed definition), but the update postflight called launchd_restart(),
which kickstarts the cached definition, so a service whose plist changed
came back running the old one. `hermes gateway start` self-heals via
refresh_launchd_plist_if_needed(); the update path did not, forcing a
manual `hermes gateway start` per profile.

Add restart_launchd_gateways_for_update(): discover every
ai.hermes.gateway*.plist, re-enter each plist's recorded HERMES_HOME via
the _HERMES_HOME_OVERRIDE ContextVar, and for each loaded gateway either
reload a stale definition (draining in-flight runs with SIGUSR1 first) or
graceful-restart a current one. The macOS postflight now delegates to it,
so named-profile gateways are handled too. launchd_restart() is unchanged,
preserving its graceful-restart contract.

Tests mock launchctl/subprocess throughout, so no live gateway commands run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists labels Jun 4, 2026
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 comp/gateway Gateway runner, session dispatch, delivery 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.

[Bug]: macOS launchd: hermes update does not restart profile gateways

2 participants