diff --git a/hermes_cli/env_loader.py b/hermes_cli/env_loader.py index 22cb4a45854..f1e4eae9006 100644 --- a/hermes_cli/env_loader.py +++ b/hermes_cli/env_loader.py @@ -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" diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 8f13ba27d7d..485549fa4a8 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -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 diff --git a/hermes_logging.py b/hermes_logging.py index fb5065e87ce..d371af25aaf 100644 --- a/hermes_logging.py +++ b/hermes_logging.py @@ -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: