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
- 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
Open PRs1
- PR#50263fix(delegation): merge CLI_CONFIG with on-disk config to honor base_url (#50199)
Fixes on record
-
#60577 fix(delegation): make active config authoritative for delegation limits
files:
tests/tools/test_delegate.py,tools/delegate_tool.pyShow 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
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.00means 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 LabAt a glance
- Subsystem
- agent
- Merged PRs
- #60577
- Closed issues
- 4
- Attempts
- 8 PRs closed without merging
- Fix link checked
- 8 Jul 2026