{"repo":"NousResearch/hermes-agent","public_id":"2642","display_label":"HTTP 429 rate limit and Retry-After header handling","summary":"These items address a bug where HTTP 429 Too Many Requests responses were not properly handled, covering both honouring the Retry-After header and surfacing user-friendly rate limit messages.","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":3809,"merged_at":"2026-03-29T22:48:07+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"stale","last_verified_at":"2026-03-29T22:48:07+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":1826,"pr_number":3809,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-03-29T22:48:07+00:00"},{"issue_number":2642,"pr_number":3809,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-03-29T22:48:07+00:00"}],"members":[{"kind":"issue","number":2642,"title":"[Bug]: HTTP 429 Too Many Requests is not treated","url":null,"state":"closed"},{"kind":"pr","number":2643,"title":"fix: honour Retry-After header on HTTP 429 responses - Fixes #2642","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":2724,"title":"fix(api): honour Retry-After header on HTTP 429 Too Many Requests","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"ed6427e0a77215bd4220c15951f28908c745e4ba","number":0},{"kind":"pr","value":"#3809","number":3809}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":3809,"title":"fix(agent): user-friendly 429 rate limit messages with Retry-After support","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["run_agent.py"],"fix_hunks":[{"file":"run_agent.py","hunks":["@@ -7229,7 +7229,10 @@ def _stop_spinner():\n                             retry_count = 0\n                             continue\n                         _final_summary = self._summarize_api_error(api_error)\n-                        self._vprint(f\"{self.log_prefix}❌ Max retries ({max_retries}) exceeded. Giving up.\", force=True)\n+                        if is_rate_limited:\n+                            self._vprint(f\"{self.log_prefix}❌ Rate limit persisted after {max_retries} retries. Please try again later.\", force=True)\n+                        else:\n+                            self._vprint(f\"{self.log_prefix}❌ Max retries ({max_retries}) exceeded. Giving up.\", force=True)\n                         self._vprint(f\"{self.log_prefix}   💀 Final error: {_final_summary}\", force=True)\n \n                         # Detect SSE stream-drop pattern (e.g. \"Network","@@ -7289,8 +7292,22 @@ def _stop_spinner():\n                             \"error\": _final_summary,\n                         }\n \n-                    wait_time = min(2 ** retry_count, 60)  # Exponential backoff: 2s, 4s, 8s, 16s, 32s, 60s, 60s\n-                    self._emit_status(f\"⏳ Retrying in {wait_time}s (attempt {retry_count}/{max_retries})...\")\n+                    # For rate limits, respect the Retry-After header if present\n+                    _retry_after = None\n+                    if is_rate_limited:\n+                        _resp_headers = getattr(getattr(api_error, \"response\", None), \"headers\", None)\n+                        if _resp_headers and hasattr(_resp_headers, \"get\"):\n+                            _ra_raw = _resp_headers.get(\"retry-after\") or _resp_headers.get(\"Retry-After\")\n+                            if _ra_raw:\n+                                try:\n+                                    _retry_after = min(int(_ra_raw), 120)  # Cap at 2 minutes\n+                                except (TypeError, ValueError):\n+                                    pass\n+                    wait_time = _retry_after if _retry_after else min(2 ** retry_count, 60)\n+                    if is_rate_limited:\n+                        self._emit_status(f\"⏱️ Rate limit reached. Waiting {wait_time}s before retry (attempt {retry_count + 1}/{max_retries})...\")\n+                    else:\n+                        self._emit_status(f\"⏳ Retrying in {wait_time}s (attempt {retry_count}/{max_retries})...\")\n                     logger.warning(\n                         \"Retrying API call in %ss (attempt %s/%s) %s error=%s\",\n                         wait_time,\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}