From fb7a38ad213b7dc8cc7a9c5bbba3326af8bd0175 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:38:31 +0530 Subject: [PATCH] fix(macos): compose launchd reload retry with _launchctl_bootstrap + drain-aware window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks @valenteff's #53277 fix per review (Teknium's 3 findings): - Route refresh_launchd_plist_if_needed's bootstrap through the existing _launchctl_bootstrap() EIO-recovery helper (canonical since #56256), wrapped in a wall-clock retry loop, instead of an ad-hoc 5x2s loop. - Window sized to agent.restart_drain_timeout (default 180s), not a fixed ~10s: the failure happens while the old gateway is still draining (finding 1). - Retry on subprocess.TimeoutExpired too, not just CalledProcessError — a bootstrap timeout after bootout otherwise escapes and leaves the service unloaded (finding 2). - Confirm success with launchctl list, not a bare bootstrap exit 0 (finding 3); mirror verify+drain-window in the detached-helper bash path. - Shared helpers _launchd_reload_log_path / _append_launchd_reload_log / _launchctl_label_registered / _retry_launchctl_bootstrap_until_registered. 3 new tests cover retry-until-listed, TimeoutExpired-retried, deadline-exhaust. E2E: real reload log + mocked launchctl — retries CalledProcessError+TimeoutExpired, verifies via launchctl list, logs failures. --- hermes_cli/gateway.py | 168 ++++++++++++++++------- tests/hermes_cli/test_gateway_service.py | 79 +++++++++++ 2 files changed, 199 insertions(+), 48 deletions(-) diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 7a9951dd5..bd970d9ca 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -3588,6 +3588,86 @@ def _launchctl_bootstrap( ) +def _launchd_reload_log_path() -> Path: + """Path the launchd reload watchdog tails for persistent-orphan detection.""" + return get_hermes_home() / "logs" / "launchd-reload.log" + + +def _append_launchd_reload_log(message: str) -> None: + """Append a timestamped line to the launchd reload log (best-effort).""" + path = _launchd_reload_log_path() + try: + path.parent.mkdir(parents=True, exist_ok=True) + from datetime import datetime as _dt + + stamp = _dt.now().astimezone().strftime("%Y-%m-%d %H:%M:%S %z") + with path.open("a", encoding="utf-8") as fh: + fh.write(f"[{stamp}] {message}\n") + except OSError: + pass + + +def _launchctl_label_registered(label: str) -> bool: + """True when ``launchctl list