{"repo":"NousResearch/hermes-agent","public_id":"15561","display_label":"Run failure events and auth error handling fixes","summary":"Multiple fixes addressing incorrect or missing `run.failed` event emission, particularly for authentication errors and non-retryable client failures in the gateway and API server. Changes ensure runs with errors are properly marked and reported rather than silently misclassified.","subsystem":"gateway","counts":{"members":7,"open_issues":1,"open_issues_distinct":1,"open_prs":2,"closed_issues":1,"merged_prs":1,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":19706},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-04T11:47:37+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.2552380952380952},"fix_edges":[{"issue_number":15561,"pr_number":19706,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":15561,"title":"Gateway emits run.completed for runs that failed with a non-retryable client error (no run.failed)","url":null,"state":"closed"},{"kind":"issue","number":57899,"title":"Provider auth errors should be retryable (failed, not delivered)","url":null,"state":"open"},{"kind":"pr","number":15564,"title":"fix(api_server): emit run.failed when run_conversation returns failed=True","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19706","number":19706}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":19706,"title":"fix(api_server): emit run.failed when run_conversation returns failed=True","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/api_server.py"],"fix_hunks":[{"file":"gateway/platforms/api_server.py","hunks":["@@ -2578,21 +2578,39 @@ def _run_sync():\n                     return r, u\n \n                 result, usage = await asyncio.get_running_loop().run_in_executor(None, _run_sync)\n-                final_response = result.get(\"final_response\", \"\") if isinstance(result, dict) else \"\"\n-                q.put_nowait({\n-                    \"event\": \"run.completed\",\n-                    \"run_id\": run_id,\n-                    \"timestamp\": time.time(),\n-                    \"output\": final_response,\n-                    \"usage\": usage,\n-                })\n-                self._set_run_status(\n-                    run_id,\n-                    \"completed\",\n-                    output=final_response,\n-                    usage=usage,\n-                    last_event=\"run.completed\",\n-                )\n+                # Check for structured failure (non-retryable client errors like\n+                # 401/400 return failed=True instead of raising, so the except\n+                # block below never fires — issue #15561).\n+                if isinstance(result, dict) and result.get(\"failed\"):\n+                    error_msg = result.get(\"error\") or \"agent run failed\"\n+                    q.put_nowait({\n+                        \"event\": \"run.failed\",\n+                        \"run_id\": run_id,\n+                        \"timestamp\": time.time(),\n+                        \"error\": error_msg,\n+                    })\n+                    self._set_run_status(\n+                        run_id,\n+                        \"failed\",\n+                        error=error_msg,\n+                        last_event=\"run.failed\",\n+                    )\n+                else:\n+                    final_response = result.get(\"final_response\", \"\") if isinstance(result, dict) else \"\"\n+                    q.put_nowait({\n+                        \"event\": \"run.completed\",\n+                        \"run_id\": run_id,\n+                        \"timestamp\": time.time(),\n+                        \"output\": final_response,\n+                        \"usage\": usage,\n+                    })\n+                    self._set_run_status(\n+                        run_id,\n+                        \"completed\",\n+                        output=final_response,\n+                        usage=usage,\n+                        last_event=\"run.completed\",\n+                    )\n             except asyncio.CancelledError:\n                 self._set_run_status(\n                     run_id,\n"]}]},{"kind":"pr","number":57910,"title":"fix(gateway): mark proxy failures as failed runs","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":57975,"title":"fix: add failed flag to provider auth error response (#57899)","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#57910","number":57910},{"kind":"pr","value":"#57899","number":57899},{"kind":"pr","value":"#57910","number":57910}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":97136,"title":"fix(gateway): mark provider auth resolution failures failed","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#57975","number":57975},{"kind":"pr","value":"#57899","number":57899}],"category":"superseded_by_pr","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}