diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index a70ec4126b..8333685a49 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -5402,11 +5402,11 @@ def _restart_all(system: bool, detach: bool = False) -> None: sys.exit(GATEWAY_FATAL_CONFIG_EXIT_CODE) # No live owner: stop/kill/start as the host root, not as the invoking profile (whose own # gateway the run-side guard would refuse — the host stayed down after a Desktop update). - with restart_all_verb_scope(owner): - _restart_all_as_host(owner, system, detach) + with restart_all_verb_scope(owner) as owe_online_notice: + _restart_all_as_host(owner, system, detach, owe_online_notice) -def _restart_all_as_host(owner, system: bool, detach: bool = False) -> None: +def _restart_all_as_host(owner, system: bool, detach: bool = False, owe_online_notice=lambda: None) -> None: service_stopped = _stop_installed_service(system) if owner is not None: # Stop ONLY the host process: its served set comes back with it, and any per-profile @@ -5426,8 +5426,8 @@ def _restart_all_as_host(owner, system: bool, detach: bool = False) -> None: # Retract the stopped owner's rendezvous record. Leaving it made this very function a silent # no-op: the re-entered `gateway run` below read the corpse's record and ATTACHED to it. _discard_dead_host_record() - # The retraction only invalidates when it removed a record; the pre-stop memo must go either way. - _forget_host_gateway_probe() + _forget_host_gateway_probe() # the retraction invalidates only a removed record; drop the memo anyway + owe_online_notice() # old host gone, new one not started: only the new boot can see it print("Starting gateway...") # Even without a registered task, gateway_windows.start() uses the detached launcher. diff --git a/hermes_cli/gateway_profile_lifecycle.py b/hermes_cli/gateway_profile_lifecycle.py index d4405ddb15..2c56297469 100644 --- a/hermes_cli/gateway_profile_lifecycle.py +++ b/hermes_cli/gateway_profile_lifecycle.py @@ -51,12 +51,13 @@ def restart_all_verb_scope(owner): The verb stops the host with a service call or a SIGTERM, so the old process never runs the in-band restart path that stamps the planned-restart marker; without it the next boot reads a - plain stop and every home channel stays silent — #142515's gap, on every OS. + plain stop and every home channel stays silent — #142515's gap, on every OS. Yields the stamp + for the caller to run once the old host is gone and before the start: a marker written while the + old host still drains can be replayed and unlinked by it, and the new host then boots silent. """ from gateway.restart_notice import write_planned_restart_marker with host_scope_for_all_verb(owner): - write_planned_restart_marker() - yield + yield write_planned_restart_marker def profile_lifecycle(command: str, args) -> bool: diff --git a/tests/hermes_cli/test_gateway_restart_all_marker.py b/tests/hermes_cli/test_gateway_restart_all_marker.py index 586653ee2b..1e8129d04d 100644 --- a/tests/hermes_cli/test_gateway_restart_all_marker.py +++ b/tests/hermes_cli/test_gateway_restart_all_marker.py @@ -22,7 +22,7 @@ def hermes_home(tmp_path, monkeypatch): return tmp_path -def test_restart_all_stamps_the_marker_before_the_host_is_stopped(hermes_home, monkeypatch): +def test_restart_all_stamps_the_marker_after_the_host_is_stopped(hermes_home, monkeypatch): from hermes_cli import gateway as gw marker = hermes_home / ".restart_pending.json" @@ -37,7 +37,7 @@ def test_restart_all_stamps_the_marker_before_the_host_is_stopped(hermes_home, m monkeypatch.setattr(gw, "_discard_dead_host_record", lambda: None) monkeypatch.setattr(gw, "_forget_host_gateway_probe", lambda: None) monkeypatch.setattr(gw, "_installed_service_kind_for", lambda windows: "systemd") - monkeypatch.setattr(gw, "_service_call", lambda kind, verb, system: None) + monkeypatch.setattr(gw, "_service_call", lambda kind, verb, system: seen.setdefault("marker_at_start", marker.exists())) monkeypatch.setattr(gw, "is_windows", False) owner = SimpleNamespace(pid=4242, profiles=["default"], home=hermes_home, describe=lambda: "host") @@ -47,5 +47,5 @@ def test_restart_all_stamps_the_marker_before_the_host_is_stopped(hermes_home, m # The verb itself, exactly as the CLI dispatches it. gw._restart_all(system=False) - assert seen["marker_at_stop"] is True, "the host must be stopped with the notice already owed" - assert marker.exists(), "the restarted host's boot pass reads this marker" + assert seen["marker_at_stop"] is False, "the still-draining old host must not see the marker" + assert seen["marker_at_start"] is True, "the restarted host's boot pass reads this marker" diff --git a/tests/hermes_cli/test_gateway_windows_restart_marker.py b/tests/hermes_cli/test_gateway_windows_restart_marker.py index eae2faf675..8a06c8e29c 100644 --- a/tests/hermes_cli/test_gateway_windows_restart_marker.py +++ b/tests/hermes_cli/test_gateway_windows_restart_marker.py @@ -23,13 +23,13 @@ def hermes_home(tmp_path, monkeypatch): return tmp_path +@pytest.mark.platforms("windows") def test_restart_stamps_the_marker_after_the_old_gateway_is_gone(hermes_home, monkeypatch): from hermes_cli import gateway as gw_facade from hermes_cli import gateway_windows as gw marker = hermes_home / MARKER seen: dict[str, bool] = {} - monkeypatch.setattr(gw, "_assert_windows", lambda: None) monkeypatch.setattr(gw, "_wait_for_gateway_absent", lambda timeout_s=30.0: True) monkeypatch.setattr(gw_facade, "_wait_for_api_server_port_free", lambda: None) monkeypatch.setattr(gw, "stop", lambda: seen.setdefault("marker_at_stop", marker.exists()))