Context compression failures silently losing conversation history

NousResearch/hermes-agent · cluster 10719 · agent

Data as of

2 open items. Fix merged in #16771. Check it covers your case.

The short answer

2open issues
0open PRs
22items grouped
Known?
Yes. 22 related items grouped here, 8 issues already closed.
Fix event
A fix was merged to main in PR #16771. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PRs #16771, #51881. That is separate from the fix event.
Still open
2 issues and 0 PRs.

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.

Open issues2

  • Issue#11588Feature idea: Preserve-on-failure principle for context compression subsystems
  • Issue#22929[Feature] Wire on_pre_compress into the context compression pipeline for MCP servers

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #16771 fix(compression): notify gateway users when summary generation fails (salvage #16333)

    merged to main

    files: 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

  • #51881 fix(compression): abort + preserve context on transient network summary failure (#29559, #25585)

    merged to main

    files: agent/context_compressor.py, tests/agent/test_context_compressor.py

    Show the diff
    • agent/context_compressor.py
      @@ -890,7 +890,15 @@ def __init__(
               # This is independent of the abort_on_summary_failure config flag:
               # rotating on a broken credential is never the right behavior.
               self._last_summary_auth_failure: bool = False
      -        # When a user-configured summary model fails and we recover by
      +        # Set when summary generation ultimately fails due to a transient
      +        # network/connection error (httpx/httpcore connection drop, premature
      +        # stream close, etc.) — distinct from auth failures but treated the
      +        # same way by compress(): ABORT and preserve the session unchanged
      +        # rather than destroy the middle window for a deterministic
      +        # "summary unavailable" marker. Retrying once the network recovers is
      +        # strictly better than discarding context for a transient blip
      +        # (#29559, #25585). Independent of abort_on_summary_failure.
      +        self._last_summary_network_failure: bool = False
               # retrying on the main model, record the failure so gateway /
               # CLI callers can still warn the user even though compression
               # succeeded.  Silent recovery would hide the broken config.
      @@ -1687,6 +1695,7 @@ def _generate_summary(
                   self._summary_model_fallen_back = False
                   self._last_summary_error = None
                   self._last_summary_auth_failure = False
      +            self._last_summary_network_failure = False
                   return self._with_summary_prefix(summary)
               except Exception as e:
                   # ``call_llm`` raises ``RuntimeError`` for two very different cases:
      @@ -1819,6 +1828,15 @@ def _generate_summary(
                   if len(err_text) > 220:
                       err_text = err_text[:217].rstrip() + "..."
                   self._last_summary_error = err_text
      +            # A terminal connection/network failure (we reach this branch only
      +            # after any main-model fallback has already been tried or is
      +            # unavailable). Flag it so compress() ABORTS and preserves the
      +            # session unchanged instead of destroying the middle window for a
      +            # placeholder marker — retrying once the network recovers is
      +            # strictly better than dropping context (#29559, #25585). Mirrors
      +            # the auth-failure carve-out; independent of abort_on_summary_failure.
      +            if _is_streaming_closed:
      +                self._last_summary_network_failure = True
                   logger.warning(
                       "Failed to generate context summary: %s. "
                       "Further summary attempts paused for %d seconds.",
      @@ -2382,6 +2400,7 @@ def compress(self, messages: List[Dict[str, Any]], current_tokens: int = None, f
               self._last_aux_model_failure_model = None
               self._last_compress_aborted = False
               self._last_summary_auth_failure = False
      +        self._last_summary_network_failure = False
       
               # Manual /compress (force=True) bypasses the failure cooldown so the
               # 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
               #           surface a warning.
               # Default is False (historical behavior).
               #
      -        # EXCEPTION — auth failures always abort. A 401/403 from the summary
      -        # call means the credential or endpoint is broken (invalid/blocked
      -        # key, or a token pointed at the wrong inference host). Rotating into
      +        # EXCEPTION — auth AND transient network failures always abort. A
      +        # 401/403 from the summary call means the credential or endpoint is
      +        # broken (invalid/blocked key, or a token pointed at the wrong
      +        # inference host). A connection/stream-close error means the network
      +        # blipped at the compaction moment (#29559). In BOTH cases rotating into
               # a child session with a placeholder summary on a broken credential
               # strands the user on a degraded session for zero benefit — every
               # subsequent call fails the same way. So when the failure was an auth
               # error we abort regardless of abort_on_summary_failure, preserving
               # the conversation unchanged until the credential is fixed.
      -        if not summary and (self.abort_on_summary_failure or self._last_summary_auth_failure):
      +        if not summary and (
      +            self.abort_on_summary_failure
      +            or self._last_summary_auth_failure
      +            or self._last_summary_network_failure
      +        ):
                   n_skipped = compress_end - compress_start
                   self._last_summary_dropped_count = 0  # nothing actually dropped
                   self._last_summary_fallback_used = False
      @@ -2521,6 +2546,15 @@ def compress(self, messages: List[Dict[str, Any]], current_tokens: int = None, f
                               "with /compress or start fresh with /new.",
                               n_skipped,
                           )
      +                elif self._last_summary_network_failure:
      +                    logger.warning(
      +                        "Summary generation failed with a network/connection "
      +                        "error — aborting compression. %d message(s) preserved "
      +                        "unchanged; the session was NOT rotated. This is "
      +                        "transient: retry with /compress once connectivity "
      +                        "recovers, or continue the conversation as-is.",
      +                        n_skipped,
      +                    )
                       else:
                           logger.warning(
                               "Summary generation failed — aborting compression "
    • tests/agent/test_context_compressor.py
      @@ -683,6 +683,47 @@ def test_non_auth_failure_still_uses_fallback_path(self):
               assert c._last_compress_aborted is False
               assert len(result) < len(msgs)  # middle window dropped
       
      +    def test_generate_summary_flags_network_failure(self):
      +        """A connection/network error on the summary call flags
      +        _last_summary_network_failure (#29559)."""
      +        with patch("agent.context_compressor.get_model_context_length", return_value=100000):
      +            c = ContextCompressor(model="test", quiet_mode=True)
      +        with patch(
      +            "agent.context_compressor.call_llm",
      +            side_effect=ConnectionError("Connection error."),
      +        ):
      +            result = c._generate_summary(self._msgs())
      +        assert result is None
      +        assert c._last_summary_network_failure is True
      +        assert c._last_summary_auth_failure is False
      +
      +    def test_compress_aborts_on_network_failure_despite_flag_false(self):
      +        """#29559/#25585: abort_on_summary_failure=False (default), but a
      +        transient connection error must ABORT — messages returned unchanged,
      +        _last_compress_aborted=True — NOT drop the middle window. Retrying once
      +        the network recovers beats discarding context for a transient blip."""
      +        with patch("agent.context_compressor.get_model_context_length", return_value=100000):
      +            c = ContextCompressor(
      +                model="test",
      +                quiet_mode=True,
      +                protect_first_n=2,
      +                protect_last_n=2,
      +                abort_on_summary_failure=False,
      +            )
      +        msgs = self._msgs(12)
      +        with patch(
      +            "agent.context_compressor.call_llm",
      +            side_effect=ConnectionError("Connection error."),
      +        ):
      +            result = c.compress(msgs, current_tokens=[redacted:secret-assignment] force=True)
      +        # Session must NOT be compressed/rotated — same messages back.
      +        assert result == msgs
      +        assert len(result) == len(msgs)
      +        assert c._last_compress_aborted is True
      +        assert c._last_summary_network_failure is True
      +        # Did NOT fall through to the static-fallback (drop-the-middle) path.
      +        assert c._last_summary_fallback_used is False
      +
           def test_aux_model_auth_failure_recovers_on_main_no_abort(self):
               """A 401 from a DISTINCT auxiliary summary_model retries on the main
               model; if main succeeds, the auth flag is cleared and compression is

10 PRs closed without merging

From the close comments: done elsewhere 3 · merged via another change 3 · superseded by another PR 2 · other 2

  • #11651 fix: preserve context when summary generation fails · superseded by another PR → #14514
  • #11662 fix(compressor): preserve messages when summarization fails · done elsewhere → #11585
  • #13034 feat(optional-skills/security): add defi-risk-analysis skill · other → #11649, #11660, #11662, #11585, #12091, #12092, #12093
  • #14514 fix(agent): preserve messages when compression summary generation fails · other → #10719, #12131, #11914, #11585
  • #16333 fix(compression): notify gateway users when summary generation fails · merged via another change → #16771
Show 5 more
  • #25588 fix(compression): preserve context when summary fails · superseded by another PR → #10719, #14514, #16771
  • #29581 fix(compress): retry summary on connection error and preserve recent turns on fallback (#29559) · done elsewhere
  • #46587 fix(agent): flush messages before compression session rotation (#46567) · done elsewhere → #46567, #47215, #47202, #44794, #39704
  • #47215 fix(agent): flush un-persisted messages before session rotation (#47202) · merged via another change → 81ff916, #47202, #46567
  • #60956 fix(gateway): compact hygiene transcripts in place · merged via another change → 4d611ba, #60981, #60947
Closed issues · 8
  • Closed#10719Context compression silently drops all conversation turns when summary generation fails
  • Closed#11585Bug: context_compressor drops messages when summarization fails
  • Closed#11753[Feature]: Pre-compression task state preservation hook
  • Closed#11914[Bug]: Context compression causes permanent task loss — model forgets in-progress work after MiniMax 529
  • Closed#12131[Bug]: Context compression failure uses static placeholder instead of preserved message tail — context permanently lost
  • Closed#16650Gateway users not notified when compression summary generation fails and history is dropped
  • Closed#25585bug(compression): failed summaries should not discard conversation context
  • Closed#29559[Bug] Context compression silently drops summary on connection error, causing total context loss for long tasks
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.08: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#16771, #51881
Closed issues
8
Attempts
10 PRs closed without merging
Fix link checked
24 Jun 2026

For your agent

The same facts, typed.