7a7d19e73b
refresh_launchd_plist_if_needed ran `launchctl bootout` then `launchctl bootstrap` with errors silenced (`2>/dev/null` in the detached helper, `check=False` in the direct subprocess path). Under high load or a launchd race, the bootout succeeds — removing the service from launchd — but the follow-up bootstrap fails silently. The service stays unregistered; KeepAlive can't revive a service launchd no longer knows about, so the gateway stays dark until a manual `launchctl bootstrap`. Observed incident (2026-06-26): `/restart` in chat triggered a planned drain; during the drain a separate call re-triggered the plist refresh, which bootout'd the live service. Under loadavg 9.48 the bootstrap failed silently — 2h35min offline until manual recovery. Fix: retry the bootstrap up to 5 times with 2s back-off, verify with `launchctl list <label>` afterwards, and log failures to ~/.hermes/logs/launchd-reload.log so the health watchdog can detect a persistent orphan. Mirrors the contract across both the detached helper (refresh inside gateway tree) and the direct subprocess path (refresh from external CLI). Existing tests pass: - test_refresh_defers_reload_when_running_inside_gateway_tree - test_refresh_uses_direct_reload_when_not_inside_gateway_tree Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>