perf(config): one raw config.yaml parse per process instead of 3-4

Counted with an open()/read_text audit hook on a real 'hermes --version'
run: config.yaml was parsed 3x before load_config() even ran — once by
env_loader._load_secrets_config, once by main.py's early redact/ipv4
bridge (bespoke yaml.load), once by hermes_logging._read_logging_config.
Each raw parse is 1-3 ms with libyaml plus an open/stat — pure
duplication since all three want the same raw dict.

All three now route through read_raw_config()'s existing (mtime_ns,
size)-keyed shared cache:

- env_loader._load_secrets_config: uses the shared reader when reading
  the process HERMES_HOME (the cache key's home); other homes (profile
  seeding) keep the isolated direct parse. Parse-error isolation is
  preserved — the shared reader also swallows errors and returns {}.
- main.py early bridge: drops the bespoke yaml.load for read_raw_config
  (managed-scope overlay unchanged).
- hermes_logging._read_logging_config: prefers the shared reader,
  falls back to the direct fast_safe_load parse when hermes_cli.config
  isn't importable.

Measured: config.yaml opens per 'hermes --version' 3 -> 1.
348 targeted tests green (env_loader + secret sources + applied-homes +
bitwarden + hermes_logging + config).
This commit is contained in:
teknium1 2026-07-29 09:55:22 -07:00 committed by Teknium
parent abd9edbea3
commit fedd689d37
3 changed files with 45 additions and 8 deletions

View file

@ -504,6 +504,21 @@ def _load_secrets_config(home_path: Path) -> dict:
config_path = home_path / "config.yaml"
if not config_path.exists():
return {}
# Prefer the shared (mtime, size)-keyed raw-config cache — this is the
# first config.yaml read in a normal `hermes` startup, so populating the
# shared cache here lets main.py's early bridge and hermes_logging reuse
# the same parse (one parse per process instead of 3-4). Falls back to a
# direct isolated parse if the shared reader is unavailable, preserving
# the "malformed config can't take down dotenv loading" property (the
# shared reader also swallows parse errors and returns {}).
if home_path == _process_hermes_home():
try:
from hermes_cli.config import read_raw_config
data = read_raw_config() or {}
return data.get("secrets") or {}
except Exception:
pass
try:
import yaml # type: ignore
except ImportError:
@ -514,3 +529,13 @@ def _load_secrets_config(home_path: Path) -> dict:
except Exception: # noqa: BLE001
return {}
return data.get("secrets") or {}
def _process_hermes_home() -> Path:
"""The HERMES_HOME the shared config cache is keyed to."""
try:
from hermes_constants import get_hermes_home
return get_hermes_home()
except Exception:
return Path.home() / ".hermes"

View file

@ -697,14 +697,15 @@ load_hermes_dotenv(project_env=PROJECT_ROOT / ".env")
# `load_config()` was doing a full deep-merge for one boolean lookup).
_FORCE_IPV4_EARLY = False
try:
import yaml as _yaml_early
# Reuse read_raw_config()'s (mtime, size)-keyed cache instead of a bespoke
# yaml.load — the SAME parse then serves hermes_logging's
# _read_logging_config and any later raw reads in this process, collapsing
# 3-4 config.yaml parses per invocation into one.
from hermes_cli.config import read_raw_config as _read_raw_early
_cfg_path = get_hermes_home() / "config.yaml"
if _cfg_path.exists():
with open(_cfg_path, encoding="utf-8") as _f:
_early_cfg_raw = _yaml_early.load(
_f, Loader=getattr(_yaml_early, "CSafeLoader", None) or _yaml_early.SafeLoader
) or {}
_early_cfg_raw = _read_raw_early() or {}
# Managed scope: overlay administrator-pinned values so a managed
# security.redact_secrets / network.force_ipv4 wins here too. This early
# bridge reads config.yaml directly (before load_config is usable), so

View file

@ -765,11 +765,22 @@ def _read_logging_config():
Returns ``(level, max_size_mb, backup_count)`` any may be ``None``.
"""
try:
from utils import fast_safe_load
config_path = get_config_path()
if config_path.exists():
# Prefer the shared (mtime, size)-keyed raw-config cache so this read
# reuses the parse hermes_cli.main's early bridge already did (one
# config.yaml parse per process instead of 3-4). Fall back to a
# direct parse when hermes_cli.config isn't importable (bare
# hermes_logging consumers).
try:
from hermes_cli.config import read_raw_config as _rrc
cfg = _rrc() or {}
except Exception:
from utils import fast_safe_load
config_path = get_config_path()
if not config_path.exists():
return (None, None, None)
with open(config_path, "r", encoding="utf-8") as f:
cfg = fast_safe_load(f) or {}
if cfg:
# Managed scope: an administrator can pin logging.* too. Overlay via
# the shared helper (fail-open) since this reads config.yaml directly.
try: