# NousResearch/hermes-agent · Context compression failures silently losing conversation history

## Open now

- 2 issues, 0 PRs open on GitHub.
- #11588: Feature idea: Preserve-on-failure principle for context compression subsystems
- #22929: \[Feature\] Wire on_pre_compress into the context compression pipeline for MCP servers

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.

## TL;DR

- 22 member(s): 2 open issue(s), 8 closed issue(s), 0 open PR(s), 2 merged PR(s), 10 PR(s) closed without merging.
- A fix was merged to main in PR #16771. Verify it addresses your case.
- **Recommended:** read PR #16771 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-16771,pr-51881`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Open related issues

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

## Closed / duplicate issues

- #10719: Context compression silently drops all conversation turns when summary generation fails
- #11585: Bug: context_compressor drops messages when summarization fails
- #11753: \[Feature\]: Pre-compression task state preservation hook
- #11914: \[Bug\]: Context compression causes permanent task loss — model forgets in-progress work after MiniMax 529
- #12131: \[Bug\]: Context compression failure uses static placeholder instead of preserved message tail — context permanently lost
- #16650: Gateway users not notified when compression summary generation fails and history is dropped
- #25585: bug(compression): failed summaries should not discard conversation context
- #29559: \[Bug\] Context compression silently drops summary on connection error, causing total context loss for long tasks

## Prior & competing PRs

- **#14514: fix(agent): preserve messages when compression summary generation fails**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#10719`, `#12131`, `#11914`, `#11585`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#16771: fix(compression): notify gateway users when summary generation fails (salvage #16333)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - 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`
- **#47215: fix(agent): flush un-persisted messages before session rotation (#47202)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `merged_via`; implication `superseded`; refs `81ff916e57`, `#47202`, `#46567`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#51881: fix(compression): abort + preserve context on transient network summary failure (#29559, #25585)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/context_compressor.py`, `tests/agent/test_context_compressor.py`
  - fix hunks:
  - `agent/context_compressor.py`

    ```diff
    @@ -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.
    ```

    ```diff
    @@ -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:
    ```

    ```diff
    @@ -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.",
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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
    ```
- **#11651: fix: preserve context when summary generation fails**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#14514`
- **#11662: fix(compressor): preserve messages when summarization fails**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#11585`
- **#13034: feat(optional-skills/security): add defi-risk-analysis skill**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#11649`, `#11660`, `#11662`, `#11585`, `#12091`, `#12092`, `#12093`
- **#16333: fix(compression): notify gateway users when summary generation fails**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#16771`
- **#25588: fix(compression): preserve context when summary fails**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#10719`, `#14514`, `#16771`, `#14514`
- **#29581: fix(compress): retry summary on connection error and preserve recent turns on fallback (#29559)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
- **#46587: fix(agent): flush messages before compression session rotation (#46567)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#46567`, `#47215`, `#47202`, `#44794`, `#39704`
- **#60956: fix(gateway): compact hygiene transcripts in place**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `4d611ba0c`, `#60981`, `#60947`, `#60947`

## Fix status

- Merged to main in PR #16771 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-24`.
- Subsystem: `agent`
