diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index ebd1367ce4..6fdeea4bbc 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -2562,6 +2562,8 @@ def ensure_gateway_service(context: str = "setup") -> bool: try: if _is_service_running(): return True + if _served_profile_needs_no_service(): + return True if not _is_service_installed(): if supports_systemd and has_conflicting_systemd_units(): # Both units would fight over bot tokens; don't pile a fresh install onto a conflicted state. @@ -4471,6 +4473,22 @@ def named_profile_served_by_running_multiplexer(profile_name: str | None = None) return False +def _served_profile_needs_no_service() -> bool: + """Print the "already served" note and return True when a setup flow must not install a standalone + service: a live multiplexing default gateway already serves this named profile, so the unit/plist it + would register can only sit dead (the start guard refuses it) or double-bind its platforms. + Shared by ``hermes setup gateway`` / ``hermes setup`` / ``hermes import`` (``ensure_gateway_service``) + and the ``hermes gateway setup`` wizard. See #111958.""" + if not named_profile_served_by_running_multiplexer(): + return False + print_success( + f"Profile '{_current_profile_name()}' is already served by the default multiplexer." + ) + print_info(" (served now by the running multiplexed gateway — add its bot token and it connects)") + print_info(" No standalone gateway service was installed or started.") + return True + + def _named_profile_refused_under_multiplexer(force: bool = False) -> bool: """Print the served-profile refusal and return True when a named-profile gateway must not start: a multiplexing default gateway already serves it (a second one would double-bind its platforms: two @@ -5783,11 +5801,7 @@ def _wizard_post_setup() -> None: """Offer to install/start/restart the gateway once at least one platform has progress.""" print() print(color("─" * 58, Colors.DIM)) - if named_profile_served_by_running_multiplexer(): - print_success( - f"Profile '{_current_profile_name()}' is already served by the default multiplexer." - ) - print_info("No standalone gateway service was installed or started.") + if _served_profile_needs_no_service(): return service_installed = _is_service_installed() service_running = _is_service_running() diff --git a/tests/hermes_cli/test_gateway_multiplex_served_record.py b/tests/hermes_cli/test_gateway_multiplex_served_record.py index bb1526ffad..e391227e6f 100644 --- a/tests/hermes_cli/test_gateway_multiplex_served_record.py +++ b/tests/hermes_cli/test_gateway_multiplex_served_record.py @@ -96,6 +96,29 @@ def test_setup_wizard_skips_service_install_for_profile_served_by_multiplexer( assert "already served by the default multiplexer" in capsys.readouterr().out +def test_setup_gateway_service_step_skips_install_for_served_profile(served_root, monkeypatch, capsys): + """``hermes -p setup gateway`` (and ``hermes setup`` / ``hermes import``) reach the service + step through ``ensure_gateway_service``: a served profile gets the multiplexer note and no unit/plist, + while a profile the live record does not list is still installed (#111958).""" + import hermes_cli.gateway as gw + + calls: list[str] = [] + monkeypatch.setattr(gw, "supports_systemd_services", lambda: True) + monkeypatch.setattr(gw, "_is_service_running", lambda: False) + monkeypatch.setattr(gw, "_is_service_installed", lambda: False) + monkeypatch.setattr(gw, "has_conflicting_systemd_units", lambda: False) + monkeypatch.setattr(gw, "systemd_install", lambda **kwargs: calls.append("install")) + monkeypatch.setattr(gw, "systemd_start", lambda *args, **kwargs: calls.append("start")) + + assert gw.ensure_gateway_service(context="setup") is True + assert calls == [] + assert "already served by the default multiplexer" in capsys.readouterr().out + + monkeypatch.setenv("HERMES_HOME", str(served_root / "profiles" / "other")) # not in the live record + assert gw.ensure_gateway_service(context="setup") is True + assert calls == ["install", "start"] + + def test_recycled_pid_does_not_lend_a_stale_record_its_served_profiles(served_root): """A stale default record whose PID now belongs to an unrelated process (start time differs, command line is not a gateway's) must not make its ``served_profiles`` authoritative: bare PID existence diff --git a/website/docs/user-guide/multi-profile-gateways.md b/website/docs/user-guide/multi-profile-gateways.md index f21142980a..5709b16ebb 100644 --- a/website/docs/user-guide/multi-profile-gateways.md +++ b/website/docs/user-guide/multi-profile-gateways.md @@ -147,6 +147,13 @@ default home's `gateway_state.json`), so it stays correct when the multiplexer w enabled only through `GATEWAY_MULTIPLEX_PROFILES` in the default profile's environment, or when profiles were added after the gateway started. +The setup flows follow the same rule: `hermes -p coder setup gateway`, `hermes -p coder setup`, +`hermes -p coder gateway setup` and `hermes -p coder import` configure the profile's bots but +skip the "install the gateway background service" step for a served profile, printing +*"Profile 'coder' is already served by the default multiplexer"* instead of registering a +stray unit or plist that could only sit dead. Add the bot token and the running multiplexer +picks it up. + The multiplexer is the single inbound process; a second profile gateway would double-bind that profile's platforms. Pass `--force` (accepted by `run`, `start`, `install` and `restart`) only if you deliberately want a separate process for that