{"repo":"NousResearch/hermes-agent","public_id":"61631","display_label":"Scheduler graceful-delivery exemption and budget-exhausted report delivery","summary":"The members center on one theme: the scheduler's graceful-delivery exemption appears to check an exit reason string (max_iterations_reached) that conversation_loop is said not to emit, so composed reports from budget-exhausted or verify-on-stop runs may not be delivered. One issue describes the mismatch, and three pull requests propose cron/scheduler changes to deliver or preserve those reports; their exact approaches and status are not established by the titles.","subsystem":null,"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":61900,"merged_at":"2026-07-10T06:23:59+00:00"},"resolution":{"state":"resolved","by":"pr-61900"},"freshness":{"state":"verified","last_verified_at":"2026-07-10T06:23:59+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":61631,"pr_number":61900,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-10T06:23:59+00:00"}],"members":[{"kind":"issue","number":61631,"title":"Scheduler's graceful-delivery exemption checks for exit reason max_iterations_reached(, which conversation_loop never emits — budget-exhausted runs lose their composed reports","url":null,"state":"closed"},{"kind":"pr","number":61636,"title":"fix(scheduler): match conversation_loop exit reasons in graceful-delivery exemption","url":null,"state":"closed","close_reason":{"category":"partial_fix","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":61721,"title":"fix(cron): deliver composed report when verify-on-stop exhausts budget","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#61900","number":61900}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":61900,"title":"fix(cron): preserve reports across verification budget exhaustion","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/conversation_loop.py","agent/turn_finalizer.py","tests/agent/test_turn_finalizer_iteration_limit_exit.py","tests/run_agent/test_verification_continuation_budget.py"],"fix_hunks":[{"file":"agent/conversation_loop.py","hunks":["@@ -613,6 +613,11 @@ def run_conversation(\n     truncated_response_parts: List[str] = []\n     compression_attempts = 0\n     _turn_exit_reason = \"unknown\"  # Diagnostic: why the loop ended\n+    # Last composed answer intentionally held back by a verification gate. If\n+    # that continuation consumes the remaining budget, this is the best\n+    # user-facing result available; it must not be confused with error or\n+    # recovery text produced by unrelated exit paths.\n+    _pending_verification_response = None\n \n     # Per-turn tally of consecutive successful credential-pool token refreshes,\n     # keyed by (provider, pool-entry-id). A persistent upstream 401 lets","@@ -5099,6 +5104,10 @@ def _perform_api_call(next_api_kwargs):\n                     }\n                     messages.append(continue_msg)\n                     agent._session_messages = messages\n+                    # An acknowledgment is explicitly non-final. Do not let its\n+                    # text suppress iteration-limit summarization if this\n+                    # continuation consumes the remaining budget.\n+                    final_response = None\n                     continue\n \n                 codex_ack_continuations = 0","@@ -5173,6 +5182,12 @@ def _perform_api_call(next_api_kwargs):\n                     # terminal. Keep a debug breadcrumb in agent.log for tracing.\n                     logger.debug(\"verification stop-loop nudge issued (attempt %d)\",\n                                  agent._verification_stop_nudges)\n+                    # Keep the attempted answer only as an explicit fallback for\n+                    # continuation-budget exhaustion.  ``final_response`` itself\n+                    # must be cleared so the finalizer can distinguish this gate\n+                    # from unrelated error/recovery exits. (#61631)\n+                    _pending_verification_response = final_response\n+                    final_response = None\n                     continue\n \n                 # User verification-loop gate: when the agent edited code this","@@ -5224,6 +5239,8 @@ def _perform_api_call(next_api_kwargs):\n                     agent._session_messages = messages\n                     logger.debug(\"pre_verify nudge issued (attempt %d)\",\n                                  agent._pre_verify_nudges)\n+                    _pending_verification_response = final_response\n+                    final_response = None\n                     continue\n \n                 messages.append(final_msg)","@@ -5308,6 +5325,7 @@ def _perform_api_call(next_api_kwargs):\n         original_user_message=original_user_message,\n         _should_review_memory=_should_review_memory,\n         _turn_exit_reason=_turn_exit_reason,\n+        _pending_verification_response=_pending_verification_response,\n     )\n \n "]},{"file":"agent/turn_finalizer.py","hunks":["@@ -42,6 +42,7 @@ def finalize_turn(\n     original_user_message,\n     _should_review_memory,\n     _turn_exit_reason,\n+    _pending_verification_response=None,\n ):\n     \"\"\"Run the post-loop finalization and return the turn ``result`` dict.\n ","@@ -50,10 +51,35 @@ def finalize_turn(\n     \"\"\"\n     from agent.conversation_loop import logger\n \n-    if final_response is None and (\n+    budget_exhausted = (\n         api_call_count >= agent.max_iterations\n         or agent.iteration_budget.remaining <= 0\n-    ):\n+    )\n+    budget_fallback_eligible = (\n+        budget_exhausted\n+        and not interrupted\n+        and not failed\n+        and str(_turn_exit_reason) in {\"unknown\", \"budget_exhausted\"}\n+    )\n+    continuation_budget_exhausted = (\n+        final_response is None\n+        and bool(_pending_verification_response)\n+        and budget_fallback_eligible\n+    )\n+\n+    iteration_limit_fallback = False\n+    preserved_verification_fallback = False\n+    if continuation_budget_exhausted:\n+        # A verification/continuation gate deliberately withheld a composed\n+        # answer, then consumed the remaining budget before producing a newer\n+        # one. Preserve that exact answer instead of replacing it with another\n+        # fallible model call. The explicit pending value is the provenance\n+        # guard: unrelated error/recovery exits can never enter this branch.\n+        final_response = _pending_verification_response\n+        _turn_exit_reason = f\"max_iterations_reached({api_call_count}/{agent.max_iterations})\"\n+        iteration_limit_fallback = True\n+        preserved_verification_fallback = True\n+    elif final_response is None and budget_fallback_eligible:\n         # Budget exhausted — ask the model for a summary via one extra\n         # API call with tools stripped.  _handle_max_iterations injects a\n         # user message and makes a single toolless request.","@@ -68,20 +94,18 @@ def finalize_turn(\n                 \"— requesting summary...\"\n             )\n         final_response = agent._handle_max_iterations(messages, api_call_count)\n+        iteration_limit_fallback = True\n \n+    if iteration_limit_fallback:\n         # If running as a kanban worker, signal the dispatcher that the\n         # worker could not complete (rather than treating it as a\n-        # protocol violation).  The agent loop strips tools before calling\n-        # _handle_max_iterations, so the model cannot call kanban_block\n-        # itself — we must do it on its behalf.\n+        # protocol violation). This applies whether the user-facing fallback\n+        # came from the summary call or an explicitly pending continuation;\n+        # both exhausted the task budget and must advance the failure circuit.\n         #\n         # We route through ``_record_task_failure(outcome=\"timed_out\")``\n-        # rather than ``kanban_block`` so this counts toward the\n-        # ``consecutive_failures`` counter and the dispatcher's\n-        # ``failure_limit`` circuit breaker (#29747 gap 2).  Without this,\n-        # a task whose worker keeps exhausting its budget would block\n-        # silently each run, get auto-promoted by the operator (or never\n-        # surface), and re-block in an endless loop with no signal.\n+        # rather than ``kanban_block`` so this counts toward the dispatcher's\n+        # consecutive-failure circuit breaker (#29747 gap 2).\n         _kanban_task = os.environ.get(\"HERMES_KANBAN_TASK\")\n         if _kanban_task:\n             try:","@@ -304,6 +328,7 @@ def finalize_turn(\n                 # truncated partial (the \"The\" case from #34452).\n                 _is_partial_fragment = (\n                     not _is_empty_terminal\n+                    and not preserved_verification_fallback\n                     and not str(_turn_exit_reason).startswith(\"text_response\")\n                     and len(_stripped) <= 24\n                     and _stripped[-1:] not in {\".\", \"!\", \"?\", \"。\", \"！\", \"？\", \"`\", \")\"}"]},{"file":"tests/agent/test_turn_finalizer_iteration_limit_exit.py","hunks":["@@ -0,0 +1,298 @@\n+\"\"\"Regression tests for iteration-limit exit normalization (#61631).\"\"\"\n+\n+from types import SimpleNamespace\n+from unittest.mock import MagicMock\n+\n+import pytest\n+\n+from agent.turn_finalizer import finalize_turn\n+\n+\n+class _LimitAgent:\n+    def __init__(\n+        self,\n+        *,\n+        max_iterations=60,\n+        budget_remaining=0,\n+        completion_explainer=False,\n+    ):\n+        self.max_iterations = max_iterations\n+        self.iteration_budget = SimpleNamespace(\n+            remaining=budget_remaining, used=max_iterations, max_total=max_iterations\n+        )\n+        self.quiet_mode = True\n+        self.model = \"test-model\"\n+        self.provider = \"test-provider\"\n+        self.base_url = \"\"\n+        self.session_id = \"sess-test\"\n+        self.context_compressor = SimpleNamespace(last_prompt_tokens=0)\n+        self.session_input_tokens = 0\n+        self.session_output_tokens = 0\n+        self.session_cache_read_tokens = 0\n+        self.session_cache_write_tokens = 0\n+        self.session_reasoning_tokens = 0\n+        self.session_prompt_tokens = 0\n+        self.session_completion_tokens = 0\n+        self.session_total_tokens = 0\n+        self.session_estimated_cost_usd = 0\n+        self.session_cost_status = \"unknown\"\n+        self.session_cost_source = \"test\"\n+        self._tool_guardrail_halt_decision = None\n+        self._interrupt_message = None\n+        self._response_was_previewed = False\n+        self._skill_nudge_interval = 0\n+        self._iters_since_skill = 0\n+        self.valid_tool_names = []\n+        self.persisted_messages = None\n+        self._handle_max_iterations_called = False\n+        self._completion_explainer = completion_explainer\n+\n+    def _handle_max_iterations(self, messages, api_call_count):\n+        self._handle_max_iterations_called = True\n+        return \"summary from extra call\"\n+\n+    def _emit_status(self, *_args, **_kwargs):\n+        pass\n+\n+    def _safe_print(self, *_args, **_kwargs):\n+        pass\n+\n+    def _save_trajectory(self, *_args, **_kwargs):\n+        pass\n+\n+    def _cleanup_task_resources(self, *_args, **_kwargs):\n+        pass\n+\n+    def _drop_trailing_empty_response_scaffolding(self, messages):\n+        pass\n+\n+    def _persist_session(self, messages, conversation_history):\n+        self.persisted_messages = list(messages)\n+\n+    def _file_mutation_verifier_enabled(self):\n+        return False\n+\n+    def _turn_completion_explainer_enabled(self):\n+        return self._completion_explainer\n+\n+    def _format_turn_completion_explanation(self, _reason):\n+        return \"iteration-limit explanation\"\n+\n+    def _drain_pending_steer(self):\n+        return None\n+\n+    def clear_interrupt(self):\n+        pass\n+\n+    def _sync_external_memory_for_turn(self, **_kwargs):\n+        pass\n+\n+\n+def _finalize(\n+    agent,\n+    *,\n+    final_response,\n+    exit_reason,\n+    api_call_count=60,\n+    pending_verification_response=None,\n+):\n+    return finalize_turn(\n+        agent,\n+        final_response=final_response,\n+        api_call_count=api_call_count,\n+        interrupted=False,\n+        failed=False,\n+        messages=[{\"role\": \"user\", \"content\": \"task\"}],\n+        conversation_history=[],\n+        effective_task_id=\"task\",\n+        turn_id=\"turn\",\n+        user_message=\"task\",\n+        original_user_message=\"task\",\n+        _should_review_memory=False,\n+        _turn_exit_reason=exit_reason,\n+        _pending_verification_response=pending_verification_response,\n+    )\n+\n+\n+def test_pending_verify_response_is_preserved_for_cron_delivery(monkeypatch):\n+    \"\"\"A held-back verification response survives last-turn exhaustion.\"\"\"\n+    monkeypatch.setattr(\"hermes_cli.plugins.invoke_hook\", lambda *_a, **_kw: [])\n+    agent = _LimitAgent()\n+    report = \"complete cron report body\"\n+\n+    result = _finalize(\n+        agent,\n+        final_response=None,\n+        exit_reason=\"unknown\",\n+        pending_verification_response=report,\n+    )\n+\n+    assert result[\"final_response\"] == report\n+    assert result[\"turn_exit_reason\"] == \"max_iterations_reached(60/60)\"\n+    assert agent._handle_max_iterations_called is False\n+\n+\n+def test_pending_pre_verify_response_is_preserved_on_budget_exhaustion(monkeypatch):\n+    monkeypatch.setattr(\"hermes_cli.plugins.invoke_hook\", lambda *_a, **_kw: [])\n+    agent = _LimitAgent()\n+    report = \"budget exhausted but complete\"\n+\n+    result = _finalize(\n+        agent,\n+        final_response=None,\n+        exit_reason=\"budget_exhausted\",\n+        pending_verification_response=report,\n+    )\n+\n+    assert result[\"final_response\"] == report\n+    assert result[\"turn_exit_reason\"] == \"max_iterations_reached(60/60)\"\n+    assert agent._handle_max_iterations_called is False\n+\n+\n+def test_empty_pending_verification_response_uses_summary_fallback(monkeypatch):\n+    monkeypatch.setattr(\"hermes_cli.plugins.invoke_hook\", lambda *_a, **_kw: [])\n+    agent = _LimitAgent()\n+\n+    result = _finalize(\n+        agent,\n+        final_response=None,\n+        exit_reason=\"unknown\",\n+        pending_verification_response=\"\",\n+    )\n+\n+    assert result[\"final_response\"] == \"summary from extra call\"\n+    assert result[\"turn_exit_reason\"] == \"max_iterations_reached(60/60)\"\n+    assert agent._handle_max_iterations_called is True\n+\n+\n+def test_short_generated_summary_keeps_abnormal_turn_explainer(monkeypatch):\n+    monkeypatch.setattr(\"hermes_cli.plugins.invoke_hook\", lambda *_a, **_kw: [])\n+    agent = _LimitAgent(completion_explainer=True)\n+    agent._handle_max_iterations = lambda *_args: \"The\"\n+\n+    result = _finalize(agent, final_response=None, exit_reason=\"unknown\")\n+\n+    assert result[\"final_response\"] == \"The\\n\\niteration-limit explanation\"\n+\n+\n+def test_short_preserved_verification_response_is_not_rewritten(monkeypatch):\n+    monkeypatch.setattr(\"hermes_cli.plugins.invoke_hook\", lambda *_a, **_kw: [])\n+    agent = _LimitAgent(completion_explainer=True)\n+\n+    result = _finalize(\n+        agent,\n+        final_response=None,\n+        exit_reason=\"unknown\",\n+        pending_verification_response=\"The\",\n+    )\n+\n+    assert result[\"final_response\"] == \"The\"\n+\n+\n+def test_text_response_exit_not_rewritten_at_iteration_limit(monkeypatch):\n+    monkeypatch.setattr(\"hermes_cli.plugins.invoke_hook\", lambda *_a, **_kw: [])\n+    agent = _LimitAgent(budget_remaining=5)\n+    exit_reason = \"text_response(finish_reason=stop)\"\n+\n+    result = _finalize(\n+        agent,\n+        final_response=\"normal answer\",\n+        exit_reason=exit_reason,\n+        api_call_count=59,\n+    )\n+\n+    assert result[\"turn_exit_reason\"] == exit_reason\n+    assert agent._handle_max_iterations_called is False\n+\n+\n[redacted:email](\n+    \"exit_reason\",\n+    [\n+        \"error_near_max_iterations(boom)\",\n+        \"guardrail_halt\",\n+        \"partial_stream_recovery\",\n+        \"fallback_prior_turn_content\",\n+        \"empty_response_exhausted\",\n+    ],\n+)\n+def test_unrelated_non_success_response_is_not_reclassified(monkeypatch, exit_reason):\n+    monkeypatch.setattr(\"hermes_cli.plugins.invoke_hook\", lambda *_a, **_kw: [])\n+    agent = _LimitAgent()\n+\n+    result = _finalize(\n+        agent,\n+        final_response=\"diagnostic or partial content\",\n+        exit_reason=exit_reason,\n+    )\n+\n+    assert result[\"turn_exit_reason\"] == exit_reason\n+    assert result[\"completed\"] is False\n+    assert agent._handle_max_iterations_called is False\n+\n+\n[redacted:email](\n+    (\"exit_reason\", \"interrupted\", \"failed\"),\n+    [\n+        (\"interrupted_by_user\", True, False),\n+        (\"all_retries_exhausted_no_response\", False, False),\n+        (\"provider_failure\", False, True),\n+    ],\n+)\n+def test_pending_response_does_not_mask_later_terminal_exit(\n+    monkeypatch, exit_reason, interrupted, failed\n+):\n+    monkeypatch.setattr(\"hermes_cli.plugins.invoke_hook\", lambda *_a, **_kw: [])\n+    agent = _LimitAgent()\n+\n+    result = finalize_turn(\n+        agent,\n+        final_response=None,\n+        api_call_count=60,\n+        interrupted=interrupted,\n+        failed=failed,\n+        messages=[{\"role\": \"user\", \"content\": \"task\"}],\n+        conversation_history=[],\n+        effective_task_id=\"task\",\n+        turn_id=\"turn\",\n+        user_message=\"task\",\n+        original_user_message=\"task\",\n+        _should_review_memory=False,\n+        _turn_exit_reason=exit_reason,\n+        _pending_verification_response=\"stale premature report\",\n+    )\n+\n+    assert result[\"final_response\"] is None\n+    assert result[\"turn_exit_reason\"] == exit_reason\n+    assert result[\"completed\"] is False\n+    assert agent._handle_max_iterations_called is False\n+\n+\n+def test_pending_response_records_kanban_timeout(monkeypatch):\n+    monkeypatch.setattr(\"hermes_cli.plugins.invoke_hook\", lambda *_a, **_kw: [])\n+    monkeypatch.setenv(\"HERMES_KANBAN_TASK\", \"task-123\")\n+    record = MagicMock(name=\"record_task_failure\")\n+    conn = SimpleNamespace(close=lambda: None)\n+    monkeypatch.setattr(\"hermes_cli.kanban_db.connect\", lambda: conn)\n+    monkeypatch.setattr(\"hermes_cli.kanban_db._record_task_failure\", record)\n+    agent = _LimitAgent()\n+\n+    result = _finalize(\n+        agent,\n+        final_response=None,\n+        exit_reason=\"unknown\",\n+        pending_verification_response=\"composed report\",\n+    )\n+\n+    assert result[\"turn_exit_reason\"] == \"max_iterations_reached(60/60)\"\n+    record.assert_called_once_with(\n+        conn,\n+        \"task-123\",\n+        error=(\n+            \"Iteration budget exhausted (60/60) — task could not complete \"\n+            \"within the allowed iterations\"\n+        ),\n+        outcome=\"timed_out\",\n+        release_claim=True,\n+        end_run=True,\n+        event_payload_extra={\"budget_used\": 60, \"budget_max\": 60},\n+    )"]},{"file":"tests/run_agent/test_verification_continuation_budget.py","hunks":["@@ -0,0 +1,146 @@\n+\"\"\"End-to-end regression coverage for verification budget exhaustion (#61631).\"\"\"\n+\n+from types import SimpleNamespace\n+from unittest.mock import MagicMock, patch\n+\n+import pytest\n+\n+from run_agent import AIAgent\n+\n+\n+def _response(content=\"composed report\"):\n+    message = SimpleNamespace(content=content, tool_calls=None)\n+    return SimpleNamespace(\n+        choices=[SimpleNamespace(message=message, finish_reason=\"stop\")],\n+        model=\"test/model\",\n+        usage=None,\n+    )\n+\n+\n[redacted:email]\n+def agent(tmp_path, monkeypatch):\n+    monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path / \".hermes\"))\n+    with (\n+        patch(\"run_agent.get_tool_definitions\", return_value=[]),\n+        patch(\"run_agent.check_toolset_requirements\", return_value={}),\n+        patch(\"run_agent.OpenAI\"),\n+    ):\n+        instance = AIAgent(\n+            session_id=\"verify-budget-test\",\n+            api_key=\"[redacted:secret-assignment]\",\n+            base_url=\"https://example.invalid/v1\",\n+            provider=\"openai-compat\",\n+            model=\"test/model\",\n+            max_iterations=1,\n+            quiet_mode=True,\n+            skip_context_files=True,\n+            skip_memory=True,\n+        )\n+    instance._cached_system_prompt = \"stable test prompt\"\n+    instance._session_db = None\n+    instance._session_json_enabled = False\n+    instance.save_trajectories = False\n+    instance.compression_enabled = False\n+    instance._cleanup_task_resources = lambda *_a, **_kw: None\n+    instance._save_trajectory = lambda *_a, **_kw: None\n+    return instance\n+\n+\n+def _assert_pending_response_survives(agent, result):\n+    assert result[\"final_response\"] == \"composed report\"\n+    assert result[\"turn_exit_reason\"] == \"max_iterations_reached(1/1)\"\n+    assert result[\"completed\"] is False\n+    assert agent._handle_max_iterations.call_count == 0\n+    assert [message[\"role\"] for message in result[\"messages\"]] == [\n+        \"user\",\n+        \"assistant\",\n+        \"user\",\n+        \"assistant\",\n+    ]\n+\n+\n+def test_verify_on_stop_preserves_composed_report_at_budget_limit(agent, monkeypatch):\n+    def model_call(_api_kwargs):\n+        agent._turn_file_mutation_paths = {\"changed.py\"}\n+        return _response()\n+\n+    agent._interruptible_api_call = model_call\n+    agent._handle_max_iterations = MagicMock(return_value=\"replacement summary\")\n+    monkeypatch.setenv(\"HERMES_VERIFY_ON_STOP\", \"1\")\n+\n+    with (\n+        patch(\"agent.verification_stop.build_verify_on_stop_nudge\", return_value=\"verify it\"),\n+        patch(\"hermes_cli.plugins.invoke_hook\", return_value=[]),\n+    ):\n+        result = agent.run_conversation(\"edit changed.py\")\n+\n+    _assert_pending_response_survives(agent, result)\n+    assert result[\"messages\"][1][\"_verification_stop_synthetic\"] is True\n+    assert result[\"messages\"][2][\"_verification_stop_synthetic\"] is True\n+\n+\n+def test_pre_verify_preserves_composed_report_at_budget_limit(agent, monkeypatch):\n+    def model_call(_api_kwargs):\n+        agent._turn_file_mutation_paths = {\"changed.py\"}\n+        return _response()\n+\n+    agent._interruptible_api_call = model_call\n+    agent._handle_max_iterations = MagicMock(return_value=\"replacement summary\")\n+    monkeypatch.setenv(\"HERMES_VERIFY_ON_STOP\", \"0\")\n+\n+    with (\n+        patch(\"hermes_cli.plugins.has_hook\", side_effect=lambda name: name == \"pre_verify\"),\n+        patch(\n+            \"hermes_cli.plugins.get_pre_verify_continue_message\",\n+            return_value=\"run project tests\",\n+        ),\n+        patch(\"agent.verify_hooks.max_verify_nudges\", return_value=2),\n+        patch(\"hermes_cli.plugins.invoke_hook\", return_value=[]),\n+    ):\n+        result = agent.run_conversation(\"edit changed.py\")\n+\n+    _assert_pending_response_survives(agent, result)\n+    assert result[\"messages\"][1][\"_pre_verify_synthetic\"] is True\n+    assert result[\"messages\"][2][\"_pre_verify_synthetic\"] is True\n+\n+\n+def test_intermediate_ack_uses_summary_instead_of_premature_text(agent, monkeypatch):\n+    agent.valid_tool_names = [\"web_search\"]\n+    agent._intent_ack_continuation = True\n+    agent._looks_like_codex_intermediate_ack = MagicMock(return_value=True)\n+    agent._interruptible_api_call = lambda _kwargs: _response(\"I'll inspect the files now\")\n+    agent._handle_max_iterations = MagicMock(return_value=\"verified summary.\")\n+    monkeypatch.setenv(\"HERMES_VERIFY_ON_STOP\", \"0\")\n+\n+    with (\n+        patch(\"hermes_cli.plugins.has_hook\", return_value=False),\n+        patch(\"hermes_cli.plugins.invoke_hook\", return_value=[]),\n+    ):\n+        result = agent.run_conversation(\"inspect /tmp/project\")\n+\n+    assert result[\"final_response\"] == \"verified summary.\"\n+    assert result[\"turn_exit_reason\"] == \"max_iterations_reached(1/1)\"\n+    agent._handle_max_iterations.assert_called_once()\n+\n+\n+def test_later_verified_response_supersedes_pending_report(agent, monkeypatch):\n+    agent.max_iterations = 2\n+    agent.iteration_budget.max_total = 2\n+    answers = iter([_response(\"premature report\"), _response(\"verified final report\")])\n+    agent._interruptible_api_call = lambda _kwargs: next(answers)\n+    agent._handle_max_iterations = MagicMock(return_value=\"replacement summary\")\n+    monkeypatch.setenv(\"HERMES_VERIFY_ON_STOP\", \"1\")\n+\n+    with (\n+        patch(\n+            \"agent.verification_stop.build_verify_on_stop_nudge\",\n+            side_effect=[\"verify it\", None],\n+        ),\n+        patch(\"hermes_cli.plugins.invoke_hook\", return_value=[]),\n+    ):\n+        result = agent.run_conversation(\"edit changed.py\")\n+\n+    assert result[\"final_response\"] == \"verified final report\"\n+    assert result[\"turn_exit_reason\"] == \"text_response(finish_reason=stop)\"\n+    assert result[\"completed\"] is True\n+    agent._handle_max_iterations.assert_not_called()\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}