{"repo":"NousResearch/hermes-agent","public_id":"16351","display_label":"Fix false context overflow errors in large sessions","summary":"These issues and fixes address a bug where generic 400/disconnect errors were incorrectly classified as context overflow in large-context (e.g. 1M-token) sessions. The fixes gate absolute message and token heuristics to small context windows to avoid false positives.","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":19723,"merged_at":"2026-05-04T12:04:56+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-04T12:04:56+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":16351,"pr_number":19723,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-04T12:04:56+00:00"}],"members":[{"kind":"issue","number":16351,"title":"Generic 400/disconnect errors misclassified as context_overflow in 1M-context sessions","url":null,"state":"closed"},{"kind":"pr","number":16352,"title":"fix(error_classifier): avoid large-context false overflow heuristics","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19723","number":19723}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":16380,"title":"fix(error_classifier): gate absolute msg/token heuristics to small context windows","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d29f90e89d0263d390a71b359e1afa4f5a91e1e9","number":0},{"kind":"pr","value":"#19723","number":19723}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":19723,"title":"fix(error_classifier): avoid large-context false overflow heuristics","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/error_classifier.py","tests/agent/test_error_classifier.py"],"fix_hunks":[{"file":"agent/error_classifier.py","hunks":["@@ -520,7 +520,12 @@ def _result(reason: FailoverReason, **overrides) -> ClassifiedError:\n \n     is_disconnect = any(p in error_msg for p in _SERVER_DISCONNECT_PATTERNS)\n     if is_disconnect and not status_code:\n-        is_large = approx_tokens > context_length * 0.6 or approx_tokens > 120000 or num_messages > 200\n+        # Absolute token/message-count thresholds are only a proxy for smaller\n+        # context windows.  Large-context sessions can have hundreds of\n+        # messages while still being far below their actual token budget.\n+        is_large = approx_tokens > context_length * 0.6 or (\n+            context_length <= 256000 and (approx_tokens > 120000 or num_messages > 200)\n+        )\n         if is_large:\n             return _result(\n                 FailoverReason.context_overflow,","@@ -766,7 +771,12 @@ def _classify_400(\n         if not err_body_msg:\n             err_body_msg = str(body.get(\"message\") or \"\").strip().lower()\n     is_generic = len(err_body_msg) < 30 or err_body_msg in (\"error\", \"\")\n-    is_large = approx_tokens > context_length * 0.4 or approx_tokens > 80000 or num_messages > 80\n+    # Absolute token/message-count thresholds are only a proxy for smaller\n+    # context windows.  Large-context sessions can have many messages while\n+    # still being far below their actual token budget.\n+    is_large = approx_tokens > context_length * 0.4 or (\n+        context_length <= 256000 and (approx_tokens > 80000 or num_messages > 80)\n+    )\n \n     if is_generic and is_large:\n         return result_fn("]},{"file":"tests/agent/test_error_classifier.py","hunks":["@@ -410,6 +410,24 @@ def test_400_generic_small_session_is_format_error(self):\n         result = classify_api_error(e, approx_tokens=1000, context_length=200000)\n         assert result.reason == FailoverReason.format_error\n \n+    def test_400_generic_many_messages_below_large_context_pressure_is_format_error(self):\n+        \"\"\"Large-context sessions should not overflow solely due to message count.\"\"\"\n+        e = MockAPIError(\n+            \"Error\",\n+            status_code=400,\n+            body={\"error\": {\"message\": \"Error\"}},\n+        )\n+        result = classify_api_error(\n+            e,\n+            provider=\"openai-codex\",\n+            model=\"gpt-5.5\",\n+            approx_tokens=[redacted:secret-assignment]\n+            context_length=1_000_000,\n+            num_messages=432,\n+        )\n+        assert result.reason == FailoverReason.format_error\n+        assert result.should_compress is False\n+\n     # ── Server disconnect + large session ──\n \n     def test_disconnect_large_session_context_overflow(self):","@@ -425,6 +443,20 @@ def test_disconnect_small_session_timeout(self):\n         result = classify_api_error(e, approx_tokens=5000, context_length=200000)\n         assert result.reason == FailoverReason.timeout\n \n+    def test_disconnect_many_messages_below_large_context_pressure_is_timeout(self):\n+        \"\"\"Large-context disconnects should not overflow solely due to message count.\"\"\"\n+        e = Exception(\"server disconnected without sending complete message\")\n+        result = classify_api_error(\n+            e,\n+            provider=\"openai-codex\",\n+            model=\"gpt-5.5\",\n+            approx_tokens=[redacted:secret-assignment]\n+            context_length=1_000_000,\n+            num_messages=432,\n+        )\n+        assert result.reason == FailoverReason.timeout\n+        assert result.should_compress is False\n+\n     # ── Provider-specific: Anthropic thinking signature ──\n \n     def test_anthropic_thinking_signature(self):\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}