perf(config): mtime-cache load_config() and read_raw_config() (#17041)

load_config() and read_raw_config() now cache their result keyed on
the config file's (mtime_ns, size). On cache hit they return a deepcopy
of the cached value, skipping yaml.safe_load + deep-merge + normalize +
env-var expansion entirely. save_config() + migrate_config() write via
atomic_yaml_write which produces a fresh inode, so stat() sees a new
mtime_ns and the next load repopulates automatically — no explicit
invalidation hook needed.

Measured per-call cost:
  load_config() cold:   13.3 ms
  load_config() cached:  0.23 ms  (57x faster)
  read_raw_config() cached: 0.13 ms

A single gateway turn hits the config 5-15 times (session context,
auxiliary client resolution, memory config, plugin hooks, approval
lookups, per-tool settings). That's 65-200 ms/turn of pure YAML
re-parsing on main. After this change: 1-3 ms/turn.

Also migrates gateway/run.py's 6 direct yaml.safe_load(config.yaml)
call sites through _load_gateway_config, which now shares the
read_raw_config cache when _hermes_home agrees with the canonical
config path. The direct-read fallback is retained for tests that
monkeypatch gateway_run._hermes_home without touching HERMES_HOME.

Safety:
- load_config() returns a deepcopy on every call; the 67+ call sites
  that mutate the result (cfg["model"]["default"] = ..., etc.) can't
  corrupt the cache.
- save_config() / atomic_yaml_write bump mtime, naturally invalidating
  the cache for the next reader.
- Cache is keyed on str(config_path), so HERMES_HOME profile switches
  don't collide.

Verified:
- 112 config tests pass (test_config, test_config_env_expansion,
  test_config_env_refs, test_config_drift, test_config_validation,
  test_aux_config).
- 87 gateway tests pass (test_verbose_command, test_session_info,
  test_compress_focus, test_runtime_footer, test_resume_command,
  test_reasoning_command, test_approve_deny_commands,
  test_run_progress_interrupt).
- Live hermes chat smoke — 2 turns + /model switch + tool calls,
  zero errors in agent.log.

Co-authored-by: teknium1 <teknium@users.noreply.github.com>
This commit is contained in:
Teknium 2026-04-28 07:06:35 -07:00 committed by GitHub
parent 42be5e49b0
commit df51ad7973
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 93 additions and 48 deletions

View file

@ -643,15 +643,31 @@ def _platform_config_key(platform: "Platform") -> str:
def _load_gateway_config() -> dict:
"""Load and parse ~/.hermes/config.yaml, returning {} on any error."""
"""Load and parse ~/.hermes/config.yaml, returning {} on any error.
Uses the module-level ``_hermes_home`` (so tests that monkeypatch it
still see their fixture) and shares the mtime-keyed raw-yaml cache
from ``hermes_cli.config.read_raw_config`` when the paths match.
"""
config_path = _hermes_home / 'config.yaml'
try:
from hermes_cli.config import get_config_path, read_raw_config
# Fast path: if _hermes_home agrees with the canonical config
# location, reuse the shared cache. Otherwise fall through to a
# direct read (keeps test fixtures with a monkeypatched
# _hermes_home working).
if config_path == get_config_path():
return read_raw_config()
except Exception:
pass
try:
config_path = _hermes_home / 'config.yaml'
if config_path.exists():
import yaml
with open(config_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f) or {}
except Exception:
logger.debug("Could not load gateway config from %s", _hermes_home / 'config.yaml')
logger.debug("Could not load gateway config from %s", config_path)
return {}
@ -4585,9 +4601,7 @@ class GatewayRunner:
# Read privacy.redact_pii from config (re-read per message)
_redact_pii = False
try:
import yaml as _pii_yaml
with open(_config_path, encoding="utf-8") as _pf:
_pcfg = _pii_yaml.safe_load(_pf) or {}
_pcfg = _load_gateway_config()
_redact_pii = bool((_pcfg.get("privacy") or {}).get("redact_pii", False))
except Exception:
pass
@ -4737,12 +4751,8 @@ class GatewayRunner:
_hyg_api_key = None
_hyg_data = {}
try:
_hyg_cfg_path = _hermes_home / "config.yaml"
if _hyg_cfg_path.exists():
import yaml as _hyg_yaml
with open(_hyg_cfg_path, encoding="utf-8") as _hyg_f:
_hyg_data = _hyg_yaml.safe_load(_hyg_f) or {}
_hyg_data = _load_gateway_config()
if _hyg_data:
# Resolve model name (same logic as run_sync)
_model_cfg = _hyg_data.get("model", {})
if isinstance(_model_cfg, str):
@ -5513,11 +5523,8 @@ class GatewayRunner:
custom_provs = None
try:
cfg_path = _hermes_home / "config.yaml"
if cfg_path.exists():
import yaml as _info_yaml
with open(cfg_path, encoding="utf-8") as f:
data = _info_yaml.safe_load(f) or {}
data = _load_gateway_config()
if data:
model_cfg = data.get("model", {})
if isinstance(model_cfg, dict):
raw_ctx = model_cfg.get("context_length")
@ -6116,9 +6123,8 @@ class GatewayRunner:
custom_provs = None
config_path = _hermes_home / "config.yaml"
try:
if config_path.exists():
with open(config_path, encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
cfg = _load_gateway_config()
if cfg:
model_cfg = cfg.get("model", {})
if isinstance(model_cfg, dict):
current_model = model_cfg.get("default", "")
@ -6423,20 +6429,14 @@ class GatewayRunner:
async def _handle_personality_command(self, event: MessageEvent) -> str:
"""Handle /personality command - list or set a personality."""
import yaml
from hermes_constants import display_hermes_home
args = event.get_command_args().strip().lower()
config_path = _hermes_home / 'config.yaml'
try:
if config_path.exists():
with open(config_path, 'r', encoding="utf-8") as f:
config = yaml.safe_load(f) or {}
personalities = config.get("agent", {}).get("personalities", {})
else:
config = {}
personalities = {}
config = _load_gateway_config()
personalities = config.get("agent", {}).get("personalities", {}) if config else {}
except Exception:
config = {}
personalities = {}
@ -7430,17 +7430,13 @@ class GatewayRunner:
``display.platforms.<platform>.tool_progress`` so each channel can
have its own verbosity level independently.
"""
import yaml
config_path = _hermes_home / "config.yaml"
platform_key = _platform_config_key(event.source.platform)
# --- check config gate ------------------------------------------------
try:
user_config = {}
if config_path.exists():
with open(config_path, encoding="utf-8") as f:
user_config = yaml.safe_load(f) or {}
user_config = _load_gateway_config()
gate_enabled = user_config.get("display", {}).get("tool_progress_command", False)
except Exception:
gate_enabled = False
@ -7502,7 +7498,6 @@ class GatewayRunner:
are respected but not modified here edit config.yaml directly for
per-platform control.
"""
import yaml
from gateway.runtime_footer import resolve_footer_config
config_path = _hermes_home / "config.yaml"
@ -7520,11 +7515,8 @@ class GatewayRunner:
arg = ""
# --- load config ----------------------------------------------------
user_config: dict = {}
try:
if config_path.exists():
with open(config_path, encoding="utf-8") as f:
user_config = yaml.safe_load(f) or {}
user_config: dict = _load_gateway_config()
except Exception as e:
return f"⚠️ Could not read config.yaml: {e}"