Files
hermes-agent/hermes_cli/gateway_runtime_multiplex.py
Teknium a257be0ba7 fix: drive cold-start multiplex target from the gateway boot policy
gateway_runtime_multiplex picked the cold-start owner for a named profile
with its own reading of gateway.multiplex_profiles: a retired explicit
`false` was treated as an opt-out (spawning a per-profile owner the boot
would refuse/ignore), and an explicit `true` selected the root even for a
`gateway.standalone: true` secondary that profiles_to_serve never serves,
so the client waited on a multiplexer that would not serve it.

Export the boot verdict from gateway_multiplex_mode
(settled_explicit_flag, used by resolve_multiplex_mode, and
host_serves_named_profile) and have implied_host_root /
multiplexer_serves_home call it: retired false settles like unset (fall
back to the recorded served set, else the host root) and a standalone
secondary always gets its own owner.

Finding: R1/R2 minor "cold-start multiplex target selection vs policy".
(cherry picked from commit e5144060d8a49ef77ba4de5149279c1942dbe235)
2026-10-06 14:24:39 -07:00

64 lines
3.2 KiB
Python

"""Which daemon a named-profile client must start when nothing serves its home.
A ``<root>/profiles/<name>`` home that the default multiplexer serves has no daemon of its own.
When the multiplexer is down (restart, update, crash) the profile looks ``absent`` and the ensure
loop would spawn a per-profile gateway for it — a second owner that answers this client and then
blocks the multiplexer's next all-or-nothing reserve (``Cannot reserve gateway profiles``). The
client must start the multiplexer instead, so the start target is decided here from the same facts
the multiplexer boots on (``gateway_multiplex_mode.host_serves_named_profile``): a profile's own
``gateway.standalone: true``, the default profile's explicit ``true``, or — when the flag is unset or
the retired ``false`` that boot settles like unset — the served set the last multiplexer recorded in
the root's ``gateway_state.json`` (a standalone default gateway clears it).
"""
from __future__ import annotations
from pathlib import Path
def multiplexer_root_for(home: Path) -> Path | None:
"""Default root that may multiplex *home*, when *home* is a named profile under it."""
from hermes_constants import named_profile_home
if named_profile_home(home) is None:
return None
root = home.parent.parent
return root if root != home else None
def implied_host_root(home: Path) -> Path | None:
"""The root whose gateway must serve named profile *home* when nothing records a verdict.
``hermes gateway run`` refuses a ``profiles/<name>`` home a gateway of its own unless it authored
``gateway.standalone: true`` (or passes ``--force``); the retired ``multiplex_profiles: false``
no longer opts out, so only a standalone profile keeps its own daemon.
"""
root = multiplexer_root_for(home)
if root is None:
return None
from hermes_cli.gateway_multiplex_mode import host_serves_named_profile
return None if host_serves_named_profile(root, home) is False else root
def multiplexer_serves_home(home: Path) -> Path | None:
"""The root whose (possibly stopped) multiplexer serves *home*, else None.
``hermes_cli.gateway.named_profile_served_by_running_multiplexer`` answers the live question;
this is its offline twin for the moment no gateway runs, deciding from the boot policy
(:func:`hermes_cli.gateway_multiplex_mode.host_serves_named_profile`): a standalone secondary is
never served, an explicit ``true`` serves the rest, and an unset or retired ``false`` flag falls
back to the ``served_profiles`` the last multiplexer recorded. No evidence keeps it unanswered.
"""
root = multiplexer_root_for(home)
if root is None:
return None
from hermes_cli.gateway_multiplex_mode import host_serves_named_profile
verdict = host_serves_named_profile(root, home)
if verdict is not None:
return root if verdict else None
from gateway.status import read_runtime_status
from hermes_cli.profiles import normalize_profile_name
served = (read_runtime_status(root / "gateway_state.json") or {}).get("served_profiles")
if not isinstance(served, list):
return None
names = {normalize_profile_name(str(p)) for p in served if p}
return root if normalize_profile_name(home.name) in names else None