{"repo":"NousResearch/hermes-agent","public_id":"2873","display_label":"Subagent iteration budget isolation bug and fixes","summary":"A bug where subagents share the parent agent's iteration budget, causing premature max_iterations cutoffs. Multiple fix attempts address giving subagents independent, isolated iteration budgets.","subsystem":"agent","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":3004,"merged_at":"2026-03-25T18:29:49+00:00"},"resolution":{"state":"resolved","by":"pr-3004"},"freshness":{"state":"stale","last_verified_at":"2026-03-25T18:29:49+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":2873,"pr_number":3004,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-03-25T18:29:49+00:00"}],"members":[{"kind":"issue","number":2873,"title":"[Bug]: Subagents hit max_iterations prematurely — shared IterationBudget drains parent's remaining quota","url":null,"state":"closed"},{"kind":"pr","number":2875,"title":"fix(delegation): isolate subagent iteration budgets by default (fixes #2873)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#3004","number":3004}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":2928,"title":"fix(delegate): give subagents isolated iteration budgets","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#3004","number":3004}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":3004,"title":"fix(delegate): give subagents independent iteration budgets","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/config.py","run_agent.py","tools/delegate_tool.py"],"fix_hunks":[{"file":"hermes_cli/config.py","hunks":["@@ -317,6 +317,8 @@ def ensure_hermes_home():\n         \"provider\": \"\",    # e.g. \"openrouter\" (empty = inherit parent provider + credentials)\n         \"base_url\": \"\",    # direct OpenAI-compatible endpoint for subagents\n         \"api_key\": \"\",     # API key for delegation.base_url (falls back to OPENAI_API_KEY)\n+        \"max_iterations\": 50,  # per-subagent iteration cap (each subagent gets its own budget,\n+                               # independent of the parent's max_iterations)\n     },\n \n     # Ephemeral prefill messages file — JSON list of {role, content} dicts"]},{"file":"run_agent.py","hunks":["@@ -162,11 +162,15 @@ def _install_safe_stdio() -> None:\n \n \n class IterationBudget:\n-    \"\"\"Thread-safe shared iteration counter for parent and child agents.\n+    \"\"\"Thread-safe iteration counter for an agent.\n \n-    Tracks total LLM-call iterations consumed across a parent agent and all\n-    its subagents.  A single ``IterationBudget`` is created by the parent\n-    and passed to every child so they share the same cap.\n+    Each agent (parent or subagent) gets its own ``IterationBudget``.\n+    The parent's budget is capped at ``max_iterations`` (default 90).\n+    Each subagent gets an independent budget capped at\n+    ``delegation.max_iterations`` (default 50) — this means total\n+    iterations across parent + subagents can exceed the parent's cap.\n+    Users control the per-subagent limit via ``delegation.max_iterations``\n+    in config.yaml.\n \n     ``execute_code`` (programmatic tool calling) iterations are refunded via\n     :meth:`refund` so they don't eat into the budget.","@@ -5719,7 +5723,7 @@ def run_conversation(\n             api_call_count += 1\n             if not self.iteration_budget.consume():\n                 if not self.quiet_mode:\n-                    self._safe_print(f\"\\n⚠️  Session iteration budget exhausted ({self.iteration_budget.max_total} total across agent + subagents)\")\n+                    self._safe_print(f\"\\n⚠️  Iteration budget exhausted ({self.iteration_budget.used}/{self.iteration_budget.max_total} iterations used)\")\n                 break\n \n             # Fire step_callback for gateway hooks (agent:step event)","@@ -7178,7 +7182,7 @@ def _stop_spinner():\n             or self.iteration_budget.remaining <= 0\n         ):\n             if self.iteration_budget.remaining <= 0 and not self.quiet_mode:\n-                print(f\"\\n⚠️  Session iteration budget exhausted ({self.iteration_budget.used}/{self.iteration_budget.max_total} used, including subagents)\")\n+                print(f\"\\n⚠️  Iteration budget exhausted ({self.iteration_budget.used}/{self.iteration_budget.max_total} iterations used)\")\n             final_response = self._handle_max_iterations(messages, api_call_count)\n         \n         # Determine if conversation completed successfully"]},{"file":"tools/delegate_tool.py","hunks":["@@ -191,9 +191,10 @@ def _build_child_agent(\n     # Build progress callback to relay tool calls to parent display\n     child_progress_cb = _build_child_progress_callback(task_index, parent_agent)\n \n-    # Share the parent's iteration budget so subagent tool calls\n-    # count toward the session-wide limit.\n-    shared_budget = getattr(parent_agent, \"iteration_budget\", None)\n+    # Each subagent gets its own iteration budget capped at max_iterations\n+    # (configurable via delegation.max_iterations, default 50).  This means\n+    # total iterations across parent + subagents can exceed the parent's\n+    # max_iterations.  The user controls the per-subagent cap in config.yaml.\n \n     # Resolve effective credentials: config override > parent inherit\n     effective_model = model or parent_agent.model","@@ -230,7 +231,7 @@ def _build_child_agent(\n         providers_order=parent_agent.providers_order,\n         provider_sort=parent_agent.provider_sort,\n         tool_progress_callback=child_progress_cb,\n-        iteration_budget=shared_budget,\n+        iteration_budget=None,  # fresh budget per subagent\n     )\n     # Set delegation depth so children can't spawn grandchildren\n     child._delegate_depth = getattr(parent_agent, '_delegate_depth', 0) + 1\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}