{"repo":"NousResearch/hermes-agent","public_id":"51435","display_label":"Delegation limits not reflecting live config changes","summary":"These items address a bug where changes to maximum subagent or delegation limits made mid-session are ignored, and the fixes make the active configuration authoritative by reloading it before reading those limits.","subsystem":"agent","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":60577,"merged_at":"2026-07-08T00:17:39+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-07-08T00:17:39+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":18946,"pr_number":60577,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-08T00:17:39+00:00"},{"issue_number":51435,"pr_number":60577,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-08T00:17:39+00:00"},{"issue_number":55380,"pr_number":60577,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-08T00:17:39+00:00"},{"issue_number":57930,"pr_number":60577,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-08T00:17:39+00:00"}],"members":[{"kind":"issue","number":51435,"title":"[Bug]: Maximum subagents limit is ignored when changed mid-session","url":null,"state":"closed"},{"kind":"pr","number":51516,"title":"fix(delegation): reload live config before reading limits","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#60577","number":60577},{"kind":"pr","value":"#59982","number":59982}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":60577,"title":"fix(delegation): make active config authoritative for delegation limits","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_delegate.py","tools/delegate_tool.py"],"fix_hunks":[{"file":"tests/tools/test_delegate.py","hunks":["@@ -13,6 +13,7 @@\n import os\n import threading\n import time\n+import types\n import unittest\n from unittest.mock import MagicMock, patch\n ","@@ -21,6 +22,7 @@\n     DELEGATE_TASK_SCHEMA,\n     DelegateEvent,\n     _get_max_concurrent_children,\n+    _load_config,\n     _LEGACY_EVENT_MAP,\n     MAX_DEPTH,\n     check_delegate_requirements,","@@ -2390,6 +2392,71 @@ def test_progress_callback_task_progress_not_misrendered(self):\n class TestConcurrencyDefaults(unittest.TestCase):\n     \"\"\"Tests for the concurrency default and no hard ceiling.\"\"\"\n \n+    def test_load_config_prefers_active_persistent_config_over_cli_defaults(self):\n+        stale_cli = types.ModuleType(\"cli\")\n+        stale_cli.CLI_CONFIG = {\n+            \"delegation\": {\n+                \"max_iterations\": 45,\n+                \"model\": \"\",\n+                \"provider\": \"\",\n+                \"base_url\": \"\",\n+                \"api_key\": \"\",\n+            }\n+        }\n+        active_config = {\n+            \"delegation\": {\n+                \"max_iterations\": 50,\n+                \"max_concurrent_children\": 50,\n+                \"max_spawn_depth\": 10,\n+            }\n+        }\n+\n+        with patch.dict(\"sys.modules\", {\"cli\": stale_cli}):\n+            with patch(\n+                \"hermes_cli.config.load_config_readonly\", return_value=active_config\n+            ):\n+                self.assertEqual(_load_config()[\"max_concurrent_children\"], 50)\n+                self.assertEqual(_get_max_concurrent_children(), 50)\n+\n+    def test_load_config_falls_back_to_cli_config_when_persistent_load_fails(self):\n+        fallback_cli = types.ModuleType(\"cli\")\n+        fallback_cli.CLI_CONFIG = {\n+            \"delegation\": {\n+                \"max_iterations\": 45,\n+                \"max_concurrent_children\": 8,\n+            }\n+        }\n+\n+        with patch.dict(\"sys.modules\", {\"cli\": fallback_cli}):\n+            with patch(\n+                \"hermes_cli.config.load_config_readonly\",\n+                side_effect=RuntimeError(\"boom\"),\n+            ):\n+                self.assertEqual(_load_config()[\"max_concurrent_children\"], 8)\n+\n+    def test_load_config_prefers_cli_config_when_user_config_ignored(self):\n+        # `hermes chat --ignore-user-config` sets HERMES_IGNORE_USER_CONFIG=1,\n+        # which only load_cli_config() honors. The delegation loader must keep\n+        # CLI_CONFIG authoritative under the flag so user config.yaml\n+        # delegation keys stay suppressed.\n+        ignoring_cli = types.ModuleType(\"cli\")\n+        ignoring_cli.CLI_CONFIG = {\n+            \"delegation\": {\n+                \"max_iterations\": 45,\n+                \"max_concurrent_children\": 4,\n+            }\n+        }\n+        user_config = {\"delegation\": {\"max_concurrent_children\": 50}}\n+\n+        with patch.dict(\"sys.modules\", {\"cli\": ignoring_cli}):\n+            with patch.dict(os.environ, {\"HERMES_IGNORE_USER_CONFIG\": \"1\"}):\n+                with patch(\n+                    \"hermes_cli.config.load_config_readonly\",\n+                    return_value=user_config,\n+                ) as mock_loader:\n+                    self.assertEqual(_load_config()[\"max_concurrent_children\"], 4)\n+                    mock_loader.assert_not_called()\n+\n     @patch(\"tools.delegate_tool._load_config\", return_value={})\n     def test_default_is_three(self, mock_cfg):\n         # Clear env var if set"]},{"file":"tools/delegate_tool.py","hunks":["@@ -3098,26 +3098,40 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:\n \n \n def _load_config() -> dict:\n-    \"\"\"Load delegation config from CLI_CONFIG or persistent config.\n-\n-    Checks the runtime config (cli.py CLI_CONFIG) first, then falls back\n-    to the persistent config (hermes_cli/config.py load_config()) so that\n-    ``delegation.model`` / ``delegation.provider`` are picked up regardless\n-    of the entry point (CLI, gateway, cron).\n+    \"\"\"Load delegation config from the active Hermes config.\n+\n+    Prefer the shared persistent loader because it follows the active\n+    HERMES_HOME/profile. ``cli.CLI_CONFIG`` is a legacy fallback for entry\n+    points that cannot import the shared loader; importing it first can return\n+    an old default ``delegation`` block and hide user-set keys such as\n+    ``max_concurrent_children``.\n+\n+    Uses ``load_config_readonly()``: every consumer of this dict is read-only\n+    (``.get()`` lookups), and this runs on each ``get_definitions()`` schema\n+    rebuild via ``_get_max_concurrent_children``, so skipping the defensive\n+    deepcopy matters. Do NOT mutate the returned dict.\n+\n+    ``HERMES_IGNORE_USER_CONFIG=1`` (``hermes chat --ignore-user-config``) is\n+    only honored by the legacy ``cli`` loader, not the shared one, so when the\n+    flag is set we keep ``cli.CLI_CONFIG`` authoritative to preserve the\n+    flag's contract of suppressing user config.yaml settings.\n     \"\"\"\n+    prefer_legacy = os.environ.get(\"HERMES_IGNORE_USER_CONFIG\") == \"1\"\n+    if not prefer_legacy:\n+        try:\n+            from hermes_cli.config import load_config_readonly\n+\n+            full = load_config_readonly()\n+            cfg = full.get(\"delegation\") or {}\n+            if isinstance(cfg, dict):\n+                return cfg\n+        except Exception:\n+            pass\n     try:\n         from cli import CLI_CONFIG\n \n         cfg = CLI_CONFIG.get(\"delegation\") or {}\n-        if cfg:\n-            return cfg\n-    except Exception:\n-        pass\n-    try:\n-        from hermes_cli.config import load_config\n-\n-        full = load_config()\n-        return full.get(\"delegation\") or {}\n+        return cfg if isinstance(cfg, dict) else {}\n     except Exception:\n         return {}\n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}