mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
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:
parent
abd9edbea3
commit
fedd689d37
3 changed files with 45 additions and 8 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue