refactor(config): one public version helper pair in config_backend

Review of the rebased stack found the same "version, or None when the
config is absent" shape written out at three call sites
(`config_version(p) if config_exists(p) else None`), a private
try/except copy of it in profiles_cache, and the gateway importing the
TUI's private `_version_token` from tui_gateway.methods_config.

- config_backend gains `config_version_or_none()` (one backend call, no
  exists-then-stat race) and `version_token()`, moved from
  tui_gateway/methods_config.py unchanged.
- tui_gateway (methods_config, server), the classic CLI config watcher,
  gateway/session_config and the profile roster cache use them;
  profiles_cache drops its private copy.

No behaviour change: same version tuples, same numeric token.
This commit is contained in:
kshitijk4poor authored and Teknium committed 2026-10-25 18:54:09 -07:00
1 parent cd5375ac8b
commit e39618f448
9 files changed
+44 -39

No files matched your search

+2 -4
View File
@@ -81,11 +81,10 @@ async def config_get(connection, ref, params):
def read():
from gateway.run import _profile_runtime_scope
from hermes_cli.config import load_config
from hermes_cli.config_backend import config_exists, config_version
from hermes_cli.config_backend import config_version_or_none, version_token
from hermes_cli.config_effective import load_user_config_effective
from hermes_constants import DEFAULT_INDICATOR_STYLE
from tools.approval_context import _get_approval_mode
from tui_gateway.methods_config import _version_token
home = Path(connection.authority.profile_id)
with _profile_runtime_scope(home):
cfg = load_config()
@@ -94,8 +93,7 @@ async def config_get(connection, ref, params):
# The user's own settings, not DEFAULT_CONFIG: clients read an absent key as their
# own default (the TUI streams unless display.streaming is explicitly false).
'full': lambda: {'config': _client_config(load_user_config_effective(home / 'config.yaml'))},
'mtime': lambda: {'mtime': _version_token(config_version(home / 'config.yaml'))
if config_exists(home / 'config.yaml') else 0,
'mtime': lambda: {'mtime': version_token(config_version_or_none(home / 'config.yaml')),
# Frozen tool policy is not a live MCP reload request.
'mcp_rev': mcp_revision(policy, cfg)},
'reasoning': lambda: _reasoning(agent, policy, cfg, connection, ref),
+2 -2
View File
@@ -1817,7 +1817,7 @@ class CLITuiMixin:
def _tui_init_run_state(self):
"""Reset the per-run REPL state (queues, modal states, voice state, config watcher)."""
from hermes_cli.config_backend import config_exists, config_version
from hermes_cli.config_backend import config_version_or_none
self._agent_running = False
self._pending_input = queue.Queue() # normal input (commands + new queries)
self._interrupt_queue = queue.Queue() # messages typed while the agent is running
@@ -1837,7 +1837,7 @@ class CLITuiMixin:
# Config file watcher — detect mcp_servers changes and auto-reload.
from hermes_cli.config import get_config_path as _get_config_path
_cfg_path = _get_config_path()
self._config_sig: tuple | None = config_version(_cfg_path) if config_exists(_cfg_path) else None
self._config_sig: tuple | None = config_version_or_none(_cfg_path)
self._config_mcp_servers: dict = self.config.get("mcp_servers") or {}
self._last_config_check: float = 0.0 # monotonic time of last check
+19
View File
@@ -227,6 +227,25 @@ def config_version(config_path: PathLike) -> tuple[Any, ...]:
return backend.version(home)
def config_version_or_none(config_path: PathLike) -> Optional[tuple[Any, ...]]:
""":func:`config_version`, or None when the config is absent or cannot be stat'ed."""
try:
return config_version(config_path)
except OSError:
return None
def version_token(version: Optional[tuple[Any, ...]]) -> float:
"""A number that changes with a backend ``version`` (0 = no config), for clients that only
compare successive values: the file backend's version leads with ``st_mtime_ns``, kept as seconds."""
if not version:
return 0
if isinstance(version[0], int):
return version[0] / 1_000_000_000
import zlib
return float(zlib.crc32(repr(version).encode("utf-8")) or 1)
def probe_config_readable(config_path: PathLike) -> None:
"""Raise ``OSError`` while a local config file cannot be opened; no parse. A non-file user
layer has no local read to fail."""
+4 -3
View File
@@ -24,8 +24,8 @@ from hermes_constants import (
LOCAL_RUNTIME_ROOT_DIRS, PROFILE_ID_RE, clear_named_profile_deleted, mark_named_profile_deleted,
named_profile_has_identity, named_profile_is_deleted, named_profile_is_live,
)
from hermes_cli.config_backend import config_exists, config_version, require_file_tooling
from hermes_cli.profiles_cache import _PROFILE_FILE_CACHE, _cached_profile_read, _config_version_or_none
from hermes_cli.config_backend import config_exists, config_version, config_version_or_none, require_file_tooling
from hermes_cli.profiles_cache import _PROFILE_FILE_CACHE, _cached_profile_read
logger = logging.getLogger(__name__)
@@ -737,7 +737,8 @@ def _read_config_model(profile_dir: Path) -> tuple:
pass
return None, None
return _cached_profile_read(config_path, "config-model", _read, _config_version_or_none)
# The user layer changes through the config backend, not necessarily on local disk.
return _cached_profile_read(config_path, "config-model", _read, config_version_or_none)
def _seed_model_config(profile_dir: Path) -> None:
-10
View File
@@ -3,7 +3,6 @@
from pathlib import Path
from typing import Optional
from hermes_cli.config_backend import config_version
# (path, kind) -> (file signature, the small derived value). `list_profiles` re-reads three YAML
@@ -26,15 +25,6 @@ def _profile_file_signature(path: Path) -> Optional[tuple]:
return (stat.st_mtime_ns, stat.st_size, stat.st_ino)
def _config_version_or_none(path: Path) -> Optional[tuple]:
"""The config backend's version of a profile's config.yaml (None when absent): the user layer
changes through the backend, not necessarily on local disk."""
try:
return config_version(path)
except OSError:
return None
def _cached_profile_read(path: Path, kind: str, compute, signature_of=_profile_file_signature):
"""``compute()``'s value, reused while ``signature_of(path)`` is unchanged. A missing file is never
cached: reading it costs nothing, and one created later must be picked up."""
+11
View File
@@ -65,6 +65,17 @@ class TestFileBackend:
with pytest.raises(FileNotFoundError):
cb.read_config_doc(path)
def test_version_or_none_and_token(self, home):
path = home / "config.yaml"
assert cb.config_version_or_none(path) is None
assert cb.version_token(None) == 0
path.write_text("a: 1\n", encoding="utf-8")
version = cb.config_version_or_none(path)
assert version == cb.config_version(path)
assert cb.version_token(version) == version[0] / 1_000_000_000 > 0
# An opaque non-file version still yields a stable non-zero number.
assert cb.version_token(("etag-1",)) == cb.version_token(("etag-1",)) != 0
def test_read_version_and_bom(self, home):
path = home / "config.yaml"
path.write_bytes("\ufeffmodel:\n default: x\n".encode("utf-8"))
@@ -88,11 +88,11 @@ def test_the_raw_config_reader_keeps_its_uncached_contract(profile_dir):
def test_config_model_cache_is_keyed_on_the_backend_version_not_the_local_file(profile_dir, monkeypatch):
"""The user layer changes through the config backend, which need not touch the local file."""
from hermes_cli import config_backend, profiles_cache
from hermes_cli import config_backend
assert profiles._read_config_model(profile_dir) == ("gpt-4o", "openai")
version = [("v2",)]
monkeypatch.setattr(profiles_cache, "config_version", lambda path: version[0])
monkeypatch.setattr(config_backend, "config_version", lambda path: version[0])
monkeypatch.setattr(config_backend.FileBackend, "read_user_layer", lambda self, home: config_backend.UserLayer(
doc={"model": {"provider": "anthropic", "default": "claude-opus-4.6"}}, version=version[0]))
+2 -16
View File
@@ -243,24 +243,10 @@ def _cfg_get_thinking_mode(params):
return {"value": raw}
def _version_token(version) -> float:
"""A non-zero number that changes with ``version`` (0 = no config yet, as before)."""
if not version:
return 0
if isinstance(version[0], int):
return version[0] / 1_000_000_000
import zlib
return float(zlib.crc32(repr(version).encode("utf-8")) or 1)
def _cfg_get_mtime(params):
from hermes_cli.config_backend import config_exists, config_version
cfg_path = _hermes_home / "config.yaml"
from hermes_cli.config_backend import config_version_or_none, version_token
try:
# The client only compares successive values for change: a numeric token derived from the
# backend's opaque version (the file backend's leads with st_mtime_ns, kept as seconds).
version = config_version(cfg_path) if config_exists(cfg_path) else None
mtime = _version_token(version)
mtime = version_token(config_version_or_none(_hermes_home / "config.yaml"))
except Exception:
return {"mtime": 0}
# mcp_rev: hash of the MCP-relevant sections so the poller reloads MCP servers only when
+2 -2
View File
@@ -998,12 +998,12 @@ def _load_cfg_raw() -> dict:
expansion applied here would be persisted on the next save). Behavioral reads use :func:`_load_cfg`.
Cache keyed on the resolved path so profiles don't clobber."""
global _cfg_cache, _cfg_sig, _cfg_path
from hermes_cli.config_backend import config_exists, config_version
from hermes_cli.config_backend import config_exists, config_version_or_none
from hermes_cli.config import read_user_config_raw
from hermes_cli.config_read_errors import FailedConfigRead
try:
p = _active_config_path()
sig = config_version(p) if config_exists(p) else None
sig = config_version_or_none(p)
with _cfg_lock:
if _cfg_cache is not None and _cfg_sig == sig and _cfg_path == p:
return copy.deepcopy(_cfg_cache)