diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index 5deb6c24946..43c8089f112 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -2412,7 +2412,9 @@ class TestConcurrencyDefaults(unittest.TestCase): } with patch.dict("sys.modules", {"cli": stale_cli}): - with patch("hermes_cli.config.load_config", return_value=active_config): + 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) @@ -2426,9 +2428,35 @@ class TestConcurrencyDefaults(unittest.TestCase): } with patch.dict("sys.modules", {"cli": fallback_cli}): - with patch("hermes_cli.config.load_config", side_effect=RuntimeError("boom")): + 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 diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 58829528a69..b989b7983b1 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -3105,16 +3105,28 @@ def _load_config() -> dict: 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``. - """ - try: - from hermes_cli.config import load_config - full = load_config() - cfg = full.get("delegation") or {} - if isinstance(cfg, dict): - return cfg - except Exception: - pass + 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