# NousResearch/hermes-agent · Stale CLI_CONFIG cache breaks delegation config reads

## Open now

- 0 issues, 1 PR open on GitHub.
- PR #50263: fix(delegation): merge CLI_CONFIG with on-disk config to honor base_url (#50199)

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.

## TL;DR

- 14 member(s): 0 open issue(s), 4 closed issue(s), 1 open PR(s), 1 merged PR(s), 8 PR(s) closed without merging.
- A fix was merged to main in PR #60577. Verify it addresses your case.
- **Recommended:** read PR #60577 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Closed / duplicate issues

- #18946: \[Bug\]: hermes config set delegation.* silently has no effect on running process (CLI_CONFIG cache stale)
- #32671: \`_load_config()\` in delegate_tool.py never reads persistent config — breaks all delegation.* settings in v0.14.0
- #50199: \[Bug\]: delegation.base_url ignored at runtime — _load_config() returns empty CLI_CONFIG block over correct config.yaml
- #57930: Bug: \`_load_config()\` in delegate_tool.py reads stale \`cli.CLI_CONFIG\` snapshot — \`hermes config set delegation.*\` ignored by running gateway

## Prior & competing PRs

- **#18978: fix(tools): prefer load_config() over stale CLI_CONFIG in delegate _load_config()**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#19007`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#60577: fix(delegation): make active config authoritative for delegation limits**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `tests/tools/test_delegate.py`, `tools/delegate_tool.py`
  - fix hunks:
  - `tests/tools/test_delegate.py`

    ```diff
    @@ -13,6 +13,7 @@
     import os
     import threading
     import time
    +import types
     import unittest
     from unittest.mock import MagicMock, patch
     
    ```

    ```diff
    @@ -21,6 +22,7 @@
         DELEGATE_TASK_SCHEMA,
         DelegateEvent,
         _get_max_concurrent_children,
    +    _load_config,
         _LEGACY_EVENT_MAP,
         MAX_DEPTH,
         check_delegate_requirements,
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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 {}
     
    ```
- **#18947: fix(delegate): always reload delegation config from disk, drop stale CLI_CONFIG cache (#18946)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#60577`
- **#18967: fix(delegate): read delegation config from disk first to avoid stale cache**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#18947`
- **#21405: fix(tools): prefer load_config() over stale CLI_CONFIG in delegate _load_config()**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#60577`
- **#50253: fix(delegation): merge CLI_CONFIG with on-disk config to honor base_url (#50199)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#50263`, `#44759`
- **#50263: fix(delegation): merge CLI_CONFIG with on-disk config to honor base_url (#50199)**. open
  - maintainer direction: category `silent_close`; implication `active`
  - files: `tests/tools/test_delegate_tool_config.py`, `tools/delegate_tool.py`
- **#57951: fix(tools): refresh delegation config from disk**. closed without merge
  - maintainer direction: category `partial_fix`; implication `needs_llm`; refs `#15540`, `#45301`, `#57930`, `#11999`
- **#58060: fix(delegate): prefer current delegation config**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#15540`
- **#58657: fix(delegate): prefer current delegation config**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#60577`, `#59982`

## Fix status

- Merged to main in PR #60577 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-08`.
- Subsystem: `agent`
