fix(cli): let the early interface probe follow the profile's home

`_suppress_mouse_residue_early()` runs at import time, before
`_apply_profile_override()` sets HERMES_HOME, so the first read of
`display.interface` always saw the default home. The result was memoised
in a cache keyed on nothing, so every later caller — the Termux fast
paths and the TUI launch decision, which all run after the profile is
applied — was pinned to the default home's interface for the whole run.
`hermes -p coder` with `display.interface: tui` in that profile booted
the classic REPL, and the mirror case booted the TUI for a profile that
asked for cli.

Key the cache on the config path it read. The hot path still parses the
YAML once per home, and the first call after the process is re-homed
re-reads the file it should have read.

Fixes #116902
This commit is contained in:
rodricksz4h5
2026-09-20 13:01:50 +05:30
committed by Teknium
parent b3ae818261
commit f50c6f43a2
2 changed files with 51 additions and 10 deletions

View File

@@ -241,23 +241,31 @@ def _set_process_title() -> None:
# Cheap read of `display.interface` for the earliest hot-path decisions
# (mouse-residue suppression, Termux fast launch) that run before
# hermes_cli.config is importable. Cached so early callers don't re-parse YAML.
_EARLY_INTERFACE_CACHE: "list | None" = None
# hermes_cli.config is importable. Cached per config path so early callers
# don't re-parse YAML, and so the answer follows the home the process ends up
# in: mouse-residue suppression reads this BEFORE `_apply_profile_override()`
# sets HERMES_HOME, and a cache keyed on nothing pinned every later caller to
# the default home's interface for the whole run (#116902).
_EARLY_INTERFACE_CACHE: "tuple[str, str] | None" = None
def _early_interface_config_path() -> str:
"""config.yaml of the home this process is currently pointed at."""
home = os.environ.get("HERMES_HOME")
if home:
return os.path.join(home, "config.yaml")
return os.path.join(os.path.expanduser("~"), ".hermes", "config.yaml")
def _config_default_interface_early() -> str:
"""Return the configured default interface ("cli"/"tui") via a minimal
YAML read. Best-effort: any error falls back to "cli" (legacy behavior)."""
global _EARLY_INTERFACE_CACHE
if _EARLY_INTERFACE_CACHE is not None:
return _EARLY_INTERFACE_CACHE[0]
cfg_path = _early_interface_config_path()
if _EARLY_INTERFACE_CACHE is not None and _EARLY_INTERFACE_CACHE[0] == cfg_path:
return _EARLY_INTERFACE_CACHE[1]
value = "cli"
try:
home = os.environ.get("HERMES_HOME")
if home:
cfg_path = os.path.join(home, "config.yaml")
else:
cfg_path = os.path.join(os.path.expanduser("~"), ".hermes", "config.yaml")
if os.path.exists(cfg_path):
import yaml as _yaml_iface
@@ -272,7 +280,7 @@ def _config_default_interface_early() -> str:
value = "tui"
except Exception:
value = "cli" # best-effort — default to classic REPL on any error
_EARLY_INTERFACE_CACHE = [value]
_EARLY_INTERFACE_CACHE = (cfg_path, value)
return value

View File

@@ -125,6 +125,39 @@ class TestWantsTuiEarly:
monkeypatch.setattr(m, "_EARLY_INTERFACE_CACHE", None)
assert m._wants_tui_early([]) is False
# REGRESSION (#116902): mouse-residue suppression reads the interface
# before `_apply_profile_override()` sets HERMES_HOME, so a cache that
# ignored the home answered every later caller with the DEFAULT home's
# interface — `hermes -p <name>` booted the wrong one.
def test_reread_after_the_profile_rehomes_the_process(self, tmp_path, monkeypatch):
default_home = tmp_path / "default"
profile_home = tmp_path / "profiles" / "coder"
for home, interface in ((default_home, "cli"), (profile_home, "tui")):
home.mkdir(parents=True)
(home / "config.yaml").write_text(
f"display:\n interface: {interface}\n"
)
# The import-time read, on the home the process starts in.
_fake_tty(monkeypatch, True)
monkeypatch.setenv("HERMES_HOME", str(default_home))
m._suppress_mouse_residue_early()
assert m._config_default_interface_early() == "cli"
# What `-p coder` does, after that read already happened.
monkeypatch.setenv("HERMES_HOME", str(profile_home))
assert m._config_default_interface_early() == "tui"
assert m._wants_tui_early([]) is True
def test_same_home_is_read_only_once(self, tmp_path, monkeypatch):
(tmp_path / "config.yaml").write_text("display:\n interface: tui\n")
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
assert m._config_default_interface_early() == "tui"
# The cache still spares the hot path a second YAML parse.
(tmp_path / "config.yaml").write_text("display:\n interface: cli\n")
assert m._config_default_interface_early() == "tui"
# ---------------------------------------------------------------------------
# argument parser — flags exist at both levels and are relaunch-inherited