From 7bf66ec39b0b287b27920e1f96b87faade534769 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:02:30 -0700 Subject: [PATCH] fix(compression): /compress refusal no longer reports a successful rewrite --- agent/context_compressor.py | 3 + agent/conversation_compression.py | 11 ++++ agent/manual_compression_feedback.py | 22 ++++++- cli.py | 6 +- ...est_manual_compression_refusal_feedback.py | 58 +++++++++++++++++++ 5 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 tests/agent/test_manual_compression_refusal_feedback.py diff --git a/agent/context_compressor.py b/agent/context_compressor.py index d66bee1cad..03639cf985 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -1885,6 +1885,7 @@ class ContextCompressor(ContextEngine): self._cooldown_persist_failed = False self._last_summary_error = None self._last_compress_aborted = False + self._last_compress_refused_would_grow = False self.last_real_prompt_tokens = 0 self.last_compression_rough_tokens = 0 self.last_rough_tokens_when_real_prompt_fit = 0 @@ -2167,6 +2168,7 @@ class ContextCompressor(ContextEngine): self._summary_failure_cooldown_until = 0.0 self._cooldown_persist_failed = False self._last_compress_aborted = False + self._last_compress_refused_would_grow = False self._context_probed = False self._context_probe_persistable = False self.last_real_prompt_tokens = 0 @@ -6927,6 +6929,7 @@ This compaction should PRIORITISE preserving all information related to the focu self._last_aux_model_failure_error = None self._last_aux_model_failure_model = None self._last_compress_aborted = False + self._last_compress_refused_would_grow = False self._last_compression_made_progress = False # NOTE: do NOT reset _last_summary_auth_failure or # _last_summary_network_failure here. These flags are set by diff --git a/agent/conversation_compression.py b/agent/conversation_compression.py index 97a9e3dbf2..ba142416b3 100644 --- a/agent/conversation_compression.py +++ b/agent/conversation_compression.py @@ -3395,6 +3395,17 @@ def compress_context( f"{_rough_in:,}", f"{_rough_out:,}", ) + # Flag the refusal on the compressor state so manual + # /compress feedback can report it honestly. Without this, + # the CLI compared the returned list against its pre-call + # snapshot, saw a difference (durable-snapshot adoption can + # legitimately change the count), and printed + # "✅ Compressed: 8 → 14 messages" directly under the + # refusal warning (Aug 2026 full-surface CLI QA sweep). + try: + agent.context_compressor._last_compress_refused_would_grow = True + except Exception: + pass try: agent._emit_warning( "⚠️ Compression refused: the generated summary " diff --git a/agent/manual_compression_feedback.py b/agent/manual_compression_feedback.py index b2e12d6834..b37361e6e2 100644 --- a/agent/manual_compression_feedback.py +++ b/agent/manual_compression_feedback.py @@ -53,6 +53,11 @@ def summarize_manual_compression( compression_state is not None and getattr(compression_state, "_last_compress_aborted", False) is True ) + refused_would_grow = ( + compression_state is not None + and getattr(compression_state, "_last_compress_refused_would_grow", False) + is True + ) fallback_used = ( compression_state is not None and getattr(compression_state, "_last_summary_fallback_used", False) is True @@ -65,7 +70,12 @@ def summarize_manual_compression( if not isinstance(failure_reason, str) or not failure_reason.strip(): failure_reason = None - if aborted: + if refused_would_grow: + headline = ( + f"Compression refused (summary would grow the conversation): " + f"{before_count} messages preserved" + ) + elif aborted: headline = f"Compression aborted: {before_count} messages preserved" elif fallback_used: headline = ( @@ -78,6 +88,8 @@ def summarize_manual_compression( if noop and after_tokens == before_tokens: token_line = f"Approx request size: ~{before_tokens:,} tokens (unchanged)" + elif refused_would_grow: + token_line = f"Approx request size: ~{before_tokens:,} tokens (unchanged)" else: token_line = ( f"Approx request size: ~{before_tokens:,} → " @@ -85,7 +97,12 @@ def summarize_manual_compression( ) note = None - if aborted: + if refused_would_grow: + note = ( + "The generated summary was larger than what it would replace; " + "no messages were removed." + ) + elif aborted: note = "Summary generation failed; no messages were removed." elif fallback_used: dropped_count = getattr( @@ -113,6 +130,7 @@ def summarize_manual_compression( return { "noop": noop, "aborted": aborted, + "refused_would_grow": refused_would_grow, "fallback_used": fallback_used, "headline": headline, "token_line": token_line, diff --git a/cli.py b/cli.py index 45b9b17ebc..97d5719fd4 100644 --- a/cli.py +++ b/cli.py @@ -12970,7 +12970,11 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): self.agent, "context_compressor", None ), ) - if summary.get("aborted") or summary.get("fallback_used"): + if ( + summary.get("aborted") + or summary.get("fallback_used") + or summary.get("refused_would_grow") + ): icon = "⚠️" else: icon = "🗜️" if summary["noop"] else "✅" diff --git a/tests/agent/test_manual_compression_refusal_feedback.py b/tests/agent/test_manual_compression_refusal_feedback.py new file mode 100644 index 0000000000..6ed514b6b6 --- /dev/null +++ b/tests/agent/test_manual_compression_refusal_feedback.py @@ -0,0 +1,58 @@ +"""Regression tests: manual /compress feedback for a would-grow refusal. + +The anti-growth guard (#83339 / PR #86700) can refuse the commit AFTER the +compressor produced a candidate. The CLI's feedback previously compared the +returned list against its pre-call snapshot; durable-snapshot adoption can +legitimately change that list, so a REFUSED compression printed +"✅ Compressed: 8 → 14 messages" directly under the refusal warning +(Aug 2026 full-surface CLI QA sweep). The refusal must surface as its own +honest headline. +""" + +from types import SimpleNamespace + +from agent.manual_compression_feedback import summarize_manual_compression + + +def _msgs(n): + return [{"role": "user", "content": f"m{n_i}"} for n_i in range(n)] + + +def _state(**flags): + base = { + "_last_compress_aborted": False, + "_last_compress_refused_would_grow": False, + "_last_summary_fallback_used": False, + "_last_summary_error": None, + "_last_summary_dropped_count": 0, + } + base.update(flags) + return SimpleNamespace(**base) + + +class TestWouldGrowRefusalFeedback: + def test_refusal_reports_preserved_not_compressed(self): + summary = summarize_manual_compression( + _msgs(8), _msgs(14), 22025, 22453, + compression_state=_state(_last_compress_refused_would_grow=True), + ) + assert summary["refused_would_grow"] is True + assert "refused" in summary["headline"].lower() + assert "→" not in summary["headline"] # never claims a message rewrite + assert "unchanged" in summary["token_line"] + assert "no messages were removed" in summary["note"] + + def test_normal_compression_unchanged(self): + summary = summarize_manual_compression( + _msgs(10), _msgs(4), 30000, 12000, compression_state=_state(), + ) + assert summary["refused_would_grow"] is False + assert summary["headline"].startswith("Compressed: 10 → 4") + + def test_abort_still_wins_its_own_headline(self): + summary = summarize_manual_compression( + _msgs(6), _msgs(6), 9000, 9000, + compression_state=_state(_last_compress_aborted=True), + ) + assert summary["aborted"] is True + assert summary["headline"].startswith("Compression aborted")