7a76046a86
`hermes gateway restart` on macOS never took the graceful path, so every restart — including deliberate ones — was reported to chat as an unplanned shutdown. `launchd_restart()` diverged from `systemd_restart()` in two ways, each sufficient to break it on its own: 1. Wrong helper. It called `_request_gateway_self_restart()`, which is gated on `_is_pid_ancestor_of_current_process()`. That holds only when the CLI was spawned *by* the gateway (in-chat `/restart`). Invoked from a shell the gateway is a sibling, so the guard returns False and SIGUSR1 is never sent. `_graceful_restart_via_sigusr1()` — same job, no ancestry gate, already used by `systemd_restart()` and the updater — had no launchd call site. 2. Wrong budget. It waited `_get_restart_drain_timeout()`, which defaults to 0, so `_wait_for_gateway_exit(timeout=0.0)` could never succeed. The systemd branch uses `_get_restart_exit_wait_budget()` (drain + after_turn + 15s headroom); `resolve_restart_exit_wait_budget()` documents that callers falling back to a hard kill must cover both phases or they reintroduce #77184. The result was a bare SIGTERM followed immediately by `kickstart -k`. Since SIGTERM leaves `restart_requested` False, the gateway exited 1 instead of 75 and announced "⚠️ Gateway shutting down — Your current task will be interrupted." instead of "restarting", dropping the resume_pending handoff that lets a session resume after the bounce. Observed on macOS 27.0 / Hermes 0.20.4: → Stopping gateway (PID 49787) — draining in-flight runs (up to 0s)... ⚠ Gateway PID 49787 still running after 0.0s — restart may fail ⚠ Gateway drain timed out after 0s — forcing launchd restart Send SIGUSR1 with the exit-wait budget and return on success, leaving launchd's unconditional KeepAlive to revive the process. `kickstart -k` stays as the fallback for a genuine drain timeout, but must not run after a successful graceful exit or it would kill the replacement instance. The wedged-loop escalation (#81642) still short-circuits ahead of this, so a provably dead event loop is not handed a signal it cannot process. Tests: adds a launchd counterpart to the existing systemd graceful-restart test, asserting SIGUSR1 with the exit-wait budget and no bare SIGTERM or kickstart on success. Updates the three wedged-gateway tests, which asserted the old SIGTERM-plus-drain shape; they also now stub `_graceful_restart_via_sigusr1` so no real signal escapes to the fake PID. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>