Files
hermes-agent/hermes_cli/gateway_profile_lifecycle.py
teknium1 f6ce02eafa fix(gateway): parking composes with gateway.standalone and the dashboard Stop/Start twin
gateway.standalone wins over the parked marker: profile_lifecycle() returns
False for an opted-out profile, so `-p X gateway stop|start` keeps addressing
X's own gateway process and never writes gateway.parked, even while a stale
host record still lists X. Every installed-roster caller now threads both
kwargs (`include_standalone=True, include_parked=True`): the host-attach peer
walk and the standalone boot notice were reading the roster without parked
profiles.

Dashboard twin (#119886 class): `/api/gateway/stop?profile=X` on a served
profile no longer answers 409 — the spawned `hermes -p X gateway stop` parks
it; `/api/gateway/start` on a parked profile is allowed while a host
multiplexer is live (the child unparks it) and still refused when nothing can
serve it. Tests trimmed to the invariants: the marker-appearing case was a
subset of the boot-and-reconcile test.
2026-09-23 08:25:28 -07:00

99 lines
4.6 KiB
Python

"""Named-profile lifecycle inside the host gateway, never a service-manager action."""
from __future__ import annotations
from hermes_constants import get_hermes_home, get_default_hermes_root
from hermes_cli.profiles import parked_marker_path, profile_is_parked, profile_is_standalone, profiles_to_serve
def _confirmed(answer, key, name):
return isinstance(answer, dict) and answer.get(key) == name and not answer.get("error")
def _failure(answer):
if isinstance(answer, dict):
return answer.get("error") or "host operation is still pending"
return "host control socket did not answer"
def profile_lifecycle(command: str, args) -> bool:
"""True when a named-profile command was handled (including an unconfirmed request)."""
from hermes_cli import gateway as gw
from gateway.control_socket import request_unserve_profile, request_serve_profile_hot
name = gw._current_profile_name()
if not name or name == "default" or getattr(args, "all", False) or getattr(args, "force", False):
return False
home = get_hermes_home()
if profile_is_standalone(home):
# gateway.standalone wins: the host never serves (or parks) an opted-out profile, so its
# verbs keep addressing its own gateway process even while a stale host record lists it.
return False
marker = parked_marker_path(home)
if command == "start":
if not profile_is_parked(home):
return False
marker.unlink()
owner = gw._host_multiplexer_for_all_verb()
if owner is None:
from hermes_cli.gateway_multiplex_served import live_default_gateway_pid
if live_default_gateway_pid() is None:
return False # Unpark even when today's normal start path must start the host first.
host_home = owner.home if owner is not None else get_default_hermes_root()
answer = request_serve_profile_hot(host_home, name)
if _confirmed(answer, "served", name):
print(f"Profile '{name}' served by the host gateway.")
else:
print(f"Profile '{name}' unparked, but serving was not confirmed: {_failure(answer)}.")
print("The host retries on its next rescan (within 30s). Check gateway status.")
return True
# A separate --force gateway still owns its normal process/service lifecycle.
if gw.find_gateway_pids():
return False
owner = gw._served_by_another_host_gateway()
if owner is None and not gw.named_profile_served_by_running_multiplexer():
return False
host_home = owner.home if owner is not None else get_default_hermes_root()
if command == "stop":
# Persist intent BEFORE teardown: the periodic rescan must not re-add this profile.
marker.touch()
answer = request_unserve_profile(host_home, name)
if _confirmed(answer, "unserved", name):
print(f"Profile '{name}' parked; its bots and cron are stopped. "
f"Start again with: hermes -p {name} gateway start")
else:
print(f"Profile '{name}' parked, but immediate stop was not confirmed: {_failure(answer)}.")
print("The host drops it on its next rescan (within 30s).")
return True
answer = request_unserve_profile(host_home, name)
if not _confirmed(answer, "unserved", name):
print(f"Profile '{name}' restart was not confirmed: {_failure(answer)}.")
return True
answer = request_serve_profile_hot(host_home, name)
if _confirmed(answer, "served", name):
print(f"Profile '{name}' restarted by the host gateway.")
else:
print(f"Profile '{name}' stopped, but serving was not confirmed: {_failure(answer)}.")
print("The host retries on its next rescan (within 30s). Check gateway status.")
return True
def print_parked_status() -> bool:
"""A parked satellite is still installed, but is not a running gateway."""
from hermes_cli import gateway as gw
name = gw._current_profile_name()
if name and name != "default" and profile_is_parked(get_hermes_home()):
print(f"Profile '{name}': parked (hermes -p {name} gateway start)")
return True
if not name or name == "default":
parked = [profile for profile, home in profiles_to_serve(True, include_parked=True)
if profile != "default" and profile_is_parked(home)]
if parked:
owner = gw.host_multiplexer_serving()
if owner is not None:
print(f"Served profiles: {', '.join(owner.profiles)}")
for profile in parked:
print(f"Profile '{profile}': parked (hermes -p {profile} gateway start)")
return False