mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
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:
1 parent
cd5375ac8b
commit
e39618f448
9 files changed
+44
-39
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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]))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user