{"repo":"NousResearch/hermes-agent","public_id":"5668","display_label":"Shorten 401 credential blackout duration","summary":"These items address a problem where a transient 401 error causes a credential to be blacklisted for 24 hours, with proposed fixes to shorten that cooldown period. The concern is especially acute for single-key setups where one bad response locks out the only available credential.","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":21264,"merged_at":"2026-05-07T13:15:34+00:00"},"resolution":{"state":"resolved","by":"pr-21264"},"freshness":{"state":"verified","last_verified_at":"2026-05-07T13:15:34+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":5668,"pr_number":21264,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-07T13:15:34+00:00"}],"members":[{"kind":"issue","number":5668,"title":"Transient 401 blacklists credential for 24 hours — breaks single-key setups","url":null,"state":"closed"},{"kind":"pr","number":19267,"title":"fix(auth): shorten credential 401 cooldown","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":21264,"title":"fix(auth): shorten credential 401 cooldown to 5 minutes (salvage #19267)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/credential_pool.py","tests/agent/test_credential_pool.py"],"fix_hunks":[{"file":"agent/credential_pool.py","hunks":["@@ -68,8 +68,10 @@ def _load_config_safe() -> Optional[dict]:\n }\n \n # Cooldown before retrying an exhausted credential.\n-# 429 (rate-limited) and 402 (billing/quota) both cool down after 1 hour.\n+# Transient 401 auth failures cool down briefly so single-key setups can recover.\n+# 429 (rate-limited), 402 (billing/quota), and other failures cool down after 1 hour.\n # Provider-supplied reset_at timestamps override these defaults.\n+EXHAUSTED_TTL_401_SECONDS = 5 * 60           # 5 minutes\n EXHAUSTED_TTL_429_SECONDS = 60 * 60          # 1 hour\n EXHAUSTED_TTL_DEFAULT_SECONDS = 60 * 60      # 1 hour\n ","@@ -190,6 +192,8 @@ def _is_manual_source(source: str) -> bool:\n \n def _exhausted_ttl(error_code: Optional[int]) -> int:\n     \"\"\"Return cooldown seconds based on the HTTP status that caused exhaustion.\"\"\"\n+    if error_code == 401:\n+        return EXHAUSTED_TTL_401_SECONDS\n     if error_code == 429:\n         return EXHAUSTED_TTL_429_SECONDS\n     return EXHAUSTED_TTL_DEFAULT_SECONDS"]},{"file":"tests/agent/test_credential_pool.py","hunks":["@@ -250,6 +250,42 @@ def test_exhausted_402_entry_resets_after_one_hour(tmp_path, monkeypatch):\n     assert entry.last_status == \"ok\"\n \n \n+def test_exhausted_401_entry_resets_after_five_minutes(tmp_path, monkeypatch):\n+    \"\"\"Transient auth failures should not strand single-key setups for an hour.\"\"\"\n+    monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path / \"hermes\"))\n+    _write_auth_store(\n+        tmp_path,\n+        {\n+            \"version\": 1,\n+            \"credential_pool\": {\n+                \"openrouter\": [\n+                    {\n+                        \"id\": \"cred-1\",\n+                        \"label\": \"primary\",\n+                        \"auth_type\": \"api_key\",\n+                        \"priority\": 0,\n+                        \"source\": \"manual\",\n+                        \"access_token\": \"***\",\n+                        \"base_url\": \"https://openrouter.ai/api/v1\",\n+                        \"last_status\": \"exhausted\",\n+                        \"last_status_at\": time.time() - 310,\n+                        \"last_error_code\": 401,\n+                    }\n+                ]\n+            },\n+        },\n+    )\n+\n+    from agent.credential_pool import load_pool\n+\n+    pool = load_pool(\"openrouter\")\n+    entry = pool.select()\n+\n+    assert entry is not None\n+    assert entry.id == \"cred-1\"\n+    assert entry.last_status == \"ok\"\n+\n+\n def test_explicit_reset_timestamp_overrides_default_429_ttl(tmp_path, monkeypatch):\n     monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path / \"hermes\"))\n     # Prevent auto-seeding from Codex CLI tokens on the host\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}