NousResearch/hermes-agent

Stale CLI_CONFIG cache breaks delegation config reads

Cluster 18946 · agent. Data as of

1 open item. Fix merged in #60577 on 8 Jul 2026. Check it covers your case.

The short answer

0open issues
1open PR
14items grouped
Known?
Yes. 14 related items grouped here, 4 issues already closed.
Fix event
A fix was merged to main in PR #60577. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
0 issues and 1 PR.

Multiple bug reports and fix attempts addressing that _load_config() in delegate_tool.py reads from a stale in-memory CLI_CONFIG snapshot instead of the current on-disk config, causing delegation.* settings to be silently ignored at runtime.

Open issues0

No open issues.

Open PRs1

  • PR#50263fix(delegation): merge CLI_CONFIG with on-disk config to honor base_url (#50199)

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #60577 fix(delegation): make active config authoritative for delegation limits

    merged to main

    files: tests/tools/test_delegate.py, tools/delegate_tool.py

    Show the diff
    • tests/tools/test_delegate.py
      @@ -13,6 +13,7 @@
       import os
       import threading
       import time
      +import types
       import unittest
       from unittest.mock import MagicMock, patch
       
      @@ -21,6 +22,7 @@
           DELEGATE_TASK_SCHEMA,
           DelegateEvent,
           _get_max_concurrent_children,
      +    _load_config,
           _LEGACY_EVENT_MAP,
           MAX_DEPTH,
           check_delegate_requirements,
      @@ -2390,6 +2392,71 @@ def test_progress_callback_task_progress_not_misrendered(self):
       class TestConcurrencyDefaults(unittest.TestCase):
           """Tests for the concurrency default and no hard ceiling."""
       
      +    def test_load_config_prefers_active_persistent_config_over_cli_defaults(self):
      +        stale_cli = types.ModuleType("cli")
      +        stale_cli.CLI_CONFIG = {
      +            "delegation": {
      +                "max_iterations": 45,
      +                "model": "",
      +                "provider": "",
      +                "base_url": "",
      +                "api_key": "",
      +            }
      +        }
      +        active_config = {
      +            "delegation": {
      +                "max_iterations": 50,
      +                "max_concurrent_children": 50,
      +                "max_spawn_depth": 10,
      +            }
      +        }
      +
      +        with patch.dict("sys.modules", {"cli": stale_cli}):
      +            with patch(
      +                "hermes_cli.config.load_config_readonly", return_value=active_config
      +            ):
      +                self.assertEqual(_load_config()["max_concurrent_children"], 50)
      +                self.assertEqual(_get_max_concurrent_children(), 50)
      +
      +    def test_load_config_falls_back_to_cli_config_when_persistent_load_fails(self):
      +        fallback_cli = types.ModuleType("cli")
      +        fallback_cli.CLI_CONFIG = {
      +            "delegation": {
      +                "max_iterations": 45,
      +                "max_concurrent_children": 8,
      +            }
      +        }
      +
      +        with patch.dict("sys.modules", {"cli": fallback_cli}):
      +            with patch(
      +                "hermes_cli.config.load_config_readonly",
      +                side_effect=RuntimeError("boom"),
      +            ):
      +                self.assertEqual(_load_config()["max_concurrent_children"], 8)
      +
      +    def test_load_config_prefers_cli_config_when_user_config_ignored(self):
      +        # `hermes chat --ignore-user-config` sets HERMES_IGNORE_USER_CONFIG=1,
      +        # which only load_cli_config() honors. The delegation loader must keep
      +        # CLI_CONFIG authoritative under the flag so user config.yaml
      +        # delegation keys stay suppressed.
      +        ignoring_cli = types.ModuleType("cli")
      +        ignoring_cli.CLI_CONFIG = {
      +            "delegation": {
      +                "max_iterations": 45,
      +                "max_concurrent_children": 4,
      +            }
      +        }
      +        user_config = {"delegation": {"max_concurrent_children": 50}}
      +
      +        with patch.dict("sys.modules", {"cli": ignoring_cli}):
      +            with patch.dict(os.environ, {"HERMES_IGNORE_USER_CONFIG": "1"}):
      +                with patch(
      +                    "hermes_cli.config.load_config_readonly",
      +                    return_value=user_config,
      +                ) as mock_loader:
      +                    self.assertEqual(_load_config()["max_concurrent_children"], 4)
      +                    mock_loader.assert_not_called()
      +
           @patch("tools.delegate_tool._load_config", return_value={})
           def test_default_is_three(self, mock_cfg):
               # Clear env var if set
    • tools/delegate_tool.py
      @@ -3098,26 +3098,40 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
       
       
       def _load_config() -> dict:
      -    """Load delegation config from CLI_CONFIG or persistent config.
      -
      -    Checks the runtime config (cli.py CLI_CONFIG) first, then falls back
      -    to the persistent config (hermes_cli/config.py load_config()) so that
      -    ``delegation.model`` / ``delegation.provider`` are picked up regardless
      -    of the entry point (CLI, gateway, cron).
      +    """Load delegation config from the active Hermes config.
      +
      +    Prefer the shared persistent loader because it follows the active
      +    HERMES_HOME/profile. ``cli.CLI_CONFIG`` is a legacy fallback for entry
      +    points that cannot import the shared loader; importing it first can return
      +    an old default ``delegation`` block and hide user-set keys such as
      +    ``max_concurrent_children``.
      +
      +    Uses ``load_config_readonly()``: every consumer of this dict is read-only
      +    (``.get()`` lookups), and this runs on each ``get_definitions()`` schema
      +    rebuild via ``_get_max_concurrent_children``, so skipping the defensive
      +    deepcopy matters. Do NOT mutate the returned dict.
      +
      +    ``HERMES_IGNORE_USER_CONFIG=1`` (``hermes chat --ignore-user-config``) is
      +    only honored by the legacy ``cli`` loader, not the shared one, so when the
      +    flag is set we keep ``cli.CLI_CONFIG`` authoritative to preserve the
      +    flag's contract of suppressing user config.yaml settings.
           """
      +    prefer_legacy = os.environ.get("HERMES_IGNORE_USER_CONFIG") == "1"
      +    if not prefer_legacy:
      +        try:
      +            from hermes_cli.config import load_config_readonly
      +
      +            full = load_config_readonly()
      +            cfg = full.get("delegation") or {}
      +            if isinstance(cfg, dict):
      +                return cfg
      +        except Exception:
      +            pass
           try:
               from cli import CLI_CONFIG
       
               cfg = CLI_CONFIG.get("delegation") or {}
      -        if cfg:
      -            return cfg
      -    except Exception:
      -        pass
      -    try:
      -        from hermes_cli.config import load_config
      -
      -        full = load_config()
      -        return full.get("delegation") or {}
      +        return cfg if isinstance(cfg, dict) else {}
           except Exception:
               return {}
       

8 PRs closed without merging

From the close comments: done elsewhere 3 · duplicate 2 · superseded by another PR 2 · partial fix 1

  • #18947 fix(delegate): always reload delegation config from disk, drop stale CLI_CONFIG cache (#18946) · done elsewhere → #60577
  • #18967 fix(delegate): read delegation config from disk first to avoid stale cache · duplicate → #18947
  • #18978 fix(tools): prefer load_config() over stale CLI_CONFIG in delegate _load_config() · superseded by another PR → #19007
  • #21405 fix(tools): prefer load_config() over stale CLI_CONFIG in delegate _load_config() · done elsewhere → #60577
  • #50253 fix(delegation): merge CLI_CONFIG with on-disk config to honor base_url (#50199) · superseded by another PR → #50263, #44759
Show 3 more
  • #57951 fix(tools): refresh delegation config from disk · partial fix → #15540, #45301, #57930, #11999
  • #58060 fix(delegate): prefer current delegation config · duplicate → #15540
  • #58657 fix(delegate): prefer current delegation config · done elsewhere → #60577, #59982
Closed issues · 4
  • Closed#18946[Bug]: hermes config set delegation.* silently has no effect on running process (CLI_CONFIG cache stale)
  • Closed#32671`_load_config()` in delegate_tool.py never reads persistent config — breaks all delegation.* settings in v0.14.0
  • Closed#50199[Bug]: delegation.base_url ignored at runtime — _load_config() returns empty CLI_CONFIG block over correct config.yaml
  • Closed#57930Bug: `_load_config()` in delegate_tool.py reads stale `cli.CLI_CONFIG` snapshot — `hermes config set delegation.*` ignored by running gateway
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.14: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#60577
Closed issues
4
Attempts
8 PRs closed without merging
Fix link checked
8 Jul 2026

For your agent

The same facts, typed.