{"repo":"NousResearch/hermes-agent","public_id":"10719","display_label":"Context compression failures silently losing conversation history","summary":"The majority of these issues and fixes concern a pattern where context compression failures (network errors, auth failures, LLM errors) silently discard conversation history instead of preserving it. A smaller number address related concerns like user notification on failure, pre-compression hooks, and an unrelated DeFi risk analysis skill addition.","subsystem":"agent","counts":{"members":22,"open_issues":2,"open_issues_distinct":2,"open_prs":0,"closed_issues":8,"merged_prs":2,"attempt_prs":10},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":16771},"resolution":{"state":"partially_resolved","by":"pr-16771,pr-51881"},"freshness":{"state":"verified","last_verified_at":"2026-06-24T14:24:22+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.08086580086580084},"fix_edges":[{"issue_number":16650,"pr_number":16771,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":25585,"pr_number":51881,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":29559,"pr_number":51881,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":10719,"title":"Context compression silently drops all conversation turns when summary generation fails","url":null,"state":"closed"},{"kind":"issue","number":11585,"title":"Bug: context_compressor drops messages when summarization fails","url":null,"state":"closed"},{"kind":"issue","number":11588,"title":"Feature idea: Preserve-on-failure principle for context compression subsystems","url":null,"state":"open"},{"kind":"issue","number":11753,"title":"[Feature]: Pre-compression task state preservation hook","url":null,"state":"closed"},{"kind":"issue","number":11914,"title":"[Bug]: Context compression causes permanent task loss — model forgets in-progress work after MiniMax 529","url":null,"state":"closed"},{"kind":"issue","number":12131,"title":"[Bug]: Context compression failure uses static placeholder instead of preserved message tail — context permanently lost","url":null,"state":"closed"},{"kind":"issue","number":16650,"title":"Gateway users not notified when compression summary generation fails and history is dropped","url":null,"state":"closed"},{"kind":"issue","number":22929,"title":"[Feature] Wire on_pre_compress into the context compression pipeline for MCP servers","url":null,"state":"open"},{"kind":"issue","number":25585,"title":"bug(compression): failed summaries should not discard conversation context","url":null,"state":"closed"},{"kind":"issue","number":29559,"title":"[Bug] Context compression silently drops summary on connection error, causing total context loss for long tasks","url":null,"state":"closed"},{"kind":"pr","number":11651,"title":"fix: preserve context when summary generation fails","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#14514","number":14514}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":11662,"title":"fix(compressor): preserve messages when summarization fails","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#11585","number":11585}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":13034,"title":"feat(optional-skills/security): add defi-risk-analysis skill","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#11649","number":11649},{"kind":"pr","value":"#11660","number":11660},{"kind":"pr","value":"#11662","number":11662},{"kind":"pr","value":"#11585","number":11585},{"kind":"pr","value":"#12091","number":12091},{"kind":"pr","value":"#12092","number":12092},{"kind":"pr","value":"#12093","number":12093}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":14514,"title":"fix(agent): preserve messages when compression summary generation fails","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#10719","number":10719},{"kind":"pr","value":"#12131","number":12131},{"kind":"pr","value":"#11914","number":11914},{"kind":"pr","value":"#11585","number":11585}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":16333,"title":"fix(compression): notify gateway users when summary generation fails","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#16771","number":16771}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":16771,"title":"fix(compression): notify gateway users when summary generation fails (salvage #16333)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/context_compressor.py","gateway/run.py","tests/agent/test_context_compressor.py","tests/gateway/test_compress_command.py","tests/gateway/test_session_hygiene.py"]},{"kind":"pr","number":25588,"title":"fix(compression): preserve context when summary fails","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#10719","number":10719},{"kind":"pr","value":"#14514","number":14514},{"kind":"pr","value":"#16771","number":16771},{"kind":"pr","value":"#14514","number":14514}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":29581,"title":"fix(compress): retry summary on connection error and preserve recent turns on fallback (#29559)","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":46587,"title":"fix(agent): flush messages before compression session rotation (#46567)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#46567","number":46567},{"kind":"pr","value":"#47215","number":47215},{"kind":"pr","value":"#47202","number":47202},{"kind":"pr","value":"#44794","number":44794},{"kind":"pr","value":"#39704","number":39704}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":47215,"title":"fix(agent): flush un-persisted messages before session rotation (#47202)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"81ff916e57","number":0},{"kind":"pr","value":"#47202","number":47202},{"kind":"pr","value":"#46567","number":46567}],"category":"merged_via","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":51881,"title":"fix(compression): abort + preserve context on transient network summary failure (#29559, #25585)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/context_compressor.py","tests/agent/test_context_compressor.py"],"fix_hunks":[{"file":"agent/context_compressor.py","hunks":["@@ -890,7 +890,15 @@ def __init__(\n         # This is independent of the abort_on_summary_failure config flag:\n         # rotating on a broken credential is never the right behavior.\n         self._last_summary_auth_failure: bool = False\n-        # When a user-configured summary model fails and we recover by\n+        # Set when summary generation ultimately fails due to a transient\n+        # network/connection error (httpx/httpcore connection drop, premature\n+        # stream close, etc.) — distinct from auth failures but treated the\n+        # same way by compress(): ABORT and preserve the session unchanged\n+        # rather than destroy the middle window for a deterministic\n+        # \"summary unavailable\" marker. Retrying once the network recovers is\n+        # strictly better than discarding context for a transient blip\n+        # (#29559, #25585). Independent of abort_on_summary_failure.\n+        self._last_summary_network_failure: bool = False\n         # retrying on the main model, record the failure so gateway /\n         # CLI callers can still warn the user even though compression\n         # succeeded.  Silent recovery would hide the broken config.","@@ -1687,6 +1695,7 @@ def _generate_summary(\n             self._summary_model_fallen_back = False\n             self._last_summary_error = None\n             self._last_summary_auth_failure = False\n+            self._last_summary_network_failure = False\n             return self._with_summary_prefix(summary)\n         except Exception as e:\n             # ``call_llm`` raises ``RuntimeError`` for two very different cases:","@@ -1819,6 +1828,15 @@ def _generate_summary(\n             if len(err_text) > 220:\n                 err_text = err_text[:217].rstrip() + \"...\"\n             self._last_summary_error = err_text\n+            # A terminal connection/network failure (we reach this branch only\n+            # after any main-model fallback has already been tried or is\n+            # unavailable). Flag it so compress() ABORTS and preserves the\n+            # session unchanged instead of destroying the middle window for a\n+            # placeholder marker — retrying once the network recovers is\n+            # strictly better than dropping context (#29559, #25585). Mirrors\n+            # the auth-failure carve-out; independent of abort_on_summary_failure.\n+            if _is_streaming_closed:\n+                self._last_summary_network_failure = True\n             logger.warning(\n                 \"Failed to generate context summary: %s. \"\n                 \"Further summary attempts paused for %d seconds.\",","@@ -2382,6 +2400,7 @@ def compress(self, messages: List[Dict[str, Any]], current_tokens: int = None, f\n         self._last_aux_model_failure_model = None\n         self._last_compress_aborted = False\n         self._last_summary_auth_failure = False\n+        self._last_summary_network_failure = False\n \n         # Manual /compress (force=True) bypasses the failure cooldown so the\n         # user can retry immediately after an auto-compress abort.  Without","@@ -2498,15 +2517,21 @@ def compress(self, messages: List[Dict[str, Any]], current_tokens: int = None, f\n         #           surface a warning.\n         # Default is False (historical behavior).\n         #\n-        # EXCEPTION — auth failures always abort. A 401/403 from the summary\n-        # call means the credential or endpoint is broken (invalid/blocked\n-        # key, or a token pointed at the wrong inference host). Rotating into\n+        # EXCEPTION — auth AND transient network failures always abort. A\n+        # 401/403 from the summary call means the credential or endpoint is\n+        # broken (invalid/blocked key, or a token pointed at the wrong\n+        # inference host). A connection/stream-close error means the network\n+        # blipped at the compaction moment (#29559). In BOTH cases rotating into\n         # a child session with a placeholder summary on a broken credential\n         # strands the user on a degraded session for zero benefit — every\n         # subsequent call fails the same way. So when the failure was an auth\n         # error we abort regardless of abort_on_summary_failure, preserving\n         # the conversation unchanged until the credential is fixed.\n-        if not summary and (self.abort_on_summary_failure or self._last_summary_auth_failure):\n+        if not summary and (\n+            self.abort_on_summary_failure\n+            or self._last_summary_auth_failure\n+            or self._last_summary_network_failure\n+        ):\n             n_skipped = compress_end - compress_start\n             self._last_summary_dropped_count = 0  # nothing actually dropped\n             self._last_summary_fallback_used = False","@@ -2521,6 +2546,15 @@ def compress(self, messages: List[Dict[str, Any]], current_tokens: int = None, f\n                         \"with /compress or start fresh with /new.\",\n                         n_skipped,\n                     )\n+                elif self._last_summary_network_failure:\n+                    logger.warning(\n+                        \"Summary generation failed with a network/connection \"\n+                        \"error — aborting compression. %d message(s) preserved \"\n+                        \"unchanged; the session was NOT rotated. This is \"\n+                        \"transient: retry with /compress once connectivity \"\n+                        \"recovers, or continue the conversation as-is.\",\n+                        n_skipped,\n+                    )\n                 else:\n                     logger.warning(\n                         \"Summary generation failed — aborting compression \""]},{"file":"tests/agent/test_context_compressor.py","hunks":["@@ -683,6 +683,47 @@ def test_non_auth_failure_still_uses_fallback_path(self):\n         assert c._last_compress_aborted is False\n         assert len(result) < len(msgs)  # middle window dropped\n \n+    def test_generate_summary_flags_network_failure(self):\n+        \"\"\"A connection/network error on the summary call flags\n+        _last_summary_network_failure (#29559).\"\"\"\n+        with patch(\"agent.context_compressor.get_model_context_length\", return_value=100000):\n+            c = ContextCompressor(model=\"test\", quiet_mode=True)\n+        with patch(\n+            \"agent.context_compressor.call_llm\",\n+            side_effect=ConnectionError(\"Connection error.\"),\n+        ):\n+            result = c._generate_summary(self._msgs())\n+        assert result is None\n+        assert c._last_summary_network_failure is True\n+        assert c._last_summary_auth_failure is False\n+\n+    def test_compress_aborts_on_network_failure_despite_flag_false(self):\n+        \"\"\"#29559/#25585: abort_on_summary_failure=False (default), but a\n+        transient connection error must ABORT — messages returned unchanged,\n+        _last_compress_aborted=True — NOT drop the middle window. Retrying once\n+        the network recovers beats discarding context for a transient blip.\"\"\"\n+        with patch(\"agent.context_compressor.get_model_context_length\", return_value=100000):\n+            c = ContextCompressor(\n+                model=\"test\",\n+                quiet_mode=True,\n+                protect_first_n=2,\n+                protect_last_n=2,\n+                abort_on_summary_failure=False,\n+            )\n+        msgs = self._msgs(12)\n+        with patch(\n+            \"agent.context_compressor.call_llm\",\n+            side_effect=ConnectionError(\"Connection error.\"),\n+        ):\n+            result = c.compress(msgs, current_tokens=[redacted:secret-assignment] force=True)\n+        # Session must NOT be compressed/rotated — same messages back.\n+        assert result == msgs\n+        assert len(result) == len(msgs)\n+        assert c._last_compress_aborted is True\n+        assert c._last_summary_network_failure is True\n+        # Did NOT fall through to the static-fallback (drop-the-middle) path.\n+        assert c._last_summary_fallback_used is False\n+\n     def test_aux_model_auth_failure_recovers_on_main_no_abort(self):\n         \"\"\"A 401 from a DISTINCT auxiliary summary_model retries on the main\n         model; if main succeeds, the auth flag is cleared and compression is\n"]}]},{"kind":"pr","number":60956,"title":"fix(gateway): compact hygiene transcripts in place","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"4d611ba0c","number":0},{"kind":"pr","value":"#60981","number":60981},{"kind":"pr","value":"#60947","number":60947},{"kind":"pr","value":"#60947","number":60947}],"category":"merged_via","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}