{"repo":"NousResearch/hermes-agent","public_id":"20465","display_label":"Codex 429 quota fallback handling fixes","summary":"These items address a bug where interactive CLI sessions fail to auto-fallback when Codex returns a 429 usage-limit error, including fixes to error classification and fallback chain index reset logic.","subsystem":null,"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":27185,"merged_at":"2026-05-17T00:12:49+00:00"},"resolution":{"state":"resolved","by":"pr-27185"},"freshness":{"state":"verified","last_verified_at":"2026-05-17T00:12:49+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":20465,"pr_number":27185,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-17T00:12:49+00:00"}],"members":[{"kind":"issue","number":20465,"title":"[Bug] Interactive CLI session does not auto-fallback on Codex 429 'usage_limit_reached', while cron jobs with the same fallback chain do","url":null,"state":"closed"},{"kind":"pr","number":20793,"title":"fix(agent): reset fallback chain index when activation fails in interactive sessions","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#27185","number":27185}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":27185,"title":"fix(agent): reset _fallback_index at turn start even when no fallback activated","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["run_agent.py","tests/run_agent/test_primary_runtime_restore.py"],"fix_hunks":[{"file":"run_agent.py","hunks":["@@ -9156,6 +9156,14 @@ def _restore_primary_runtime(self) -> bool:\n         ``gateway/run.py``), so this restoration IS needed there too.\n         \"\"\"\n         if not self._fallback_activated:\n+            # Reset the chain index even when no fallback was activated this\n+            # turn.  Without this, a turn where _try_activate_fallback() was\n+            # called but returned False (chain exhausted or provider not\n+            # configured) leaves _fallback_index >= len(_fallback_chain) while\n+            # _fallback_activated stays False.  The next turn skips this block\n+            # entirely, stranding the index and silently blocking all future\n+            # fallback attempts for the session.  Fixes #20465.\n+            self._fallback_index = 0\n             return False\n \n         if getattr(self, \"_rate_limited_until\", 0) > time.monotonic():"]},{"file":"tests/run_agent/test_primary_runtime_restore.py","hunks":["@@ -123,6 +123,26 @@ def test_noop_when_not_fallback(self):\n         assert agent._fallback_activated is False\n         assert agent._restore_primary_runtime() is False\n \n+    def test_resets_index_when_fallback_not_activated(self):\n+        \"\"\"Regression for #20465: failed activation leaves _fallback_index advanced\n+        with _fallback_activated=False; the next turn's restore must reset the index.\"\"\"\n+        fbs = [{\"provider\": \"custom\", \"model\": \"gpt-oss:20b\",\n+                \"base_url\": \"http://host.docker.internal:11434/v1\", \"api_key\": \"[redacted:secret-assignment]\"}]\n+        agent = _make_agent(fallback_model=fbs)\n+\n+        # resolve_provider_client returns None → _try_activate_fallback returns False\n+        # but _fallback_index has already been incremented to 1\n+        with patch(\"agent.auxiliary_client.resolve_provider_client\", return_value=(None, None)):\n+            assert agent._try_activate_fallback() is False\n+\n+        assert agent._fallback_activated is False\n+        assert agent._fallback_index == 1  # advanced past the only entry\n+\n+        # _restore_primary_runtime must reset the index so the next turn can retry\n+        result = agent._restore_primary_runtime()\n+        assert result is False  # still no-op (primary was never left)\n+        assert agent._fallback_index == 0  # chain available again\n+\n     def test_restores_model_and_provider(self):\n         agent = _make_agent(\n             fallback_model={\"provider\": \"openrouter\", \"model\": \"anthropic/claude-sonnet-4\"},\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}