From 5ce827cac9f97215da9d9da019c2d06a616abdd1 Mon Sep 17 00:00:00 2001 From: LeonSGP43 Date: Sat, 11 Jul 2026 23:15:48 -0700 Subject: [PATCH] fix(context): count fallback compactions as ineffective --- agent/context_compressor.py | 28 +++++++++++-- tests/agent/test_context_compressor.py | 55 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/agent/context_compressor.py b/agent/context_compressor.py index d8f661987b94..2ffff5ef9c15 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -1186,13 +1186,23 @@ class ContextCompressor(ContextEngine): self.last_total_tokens = usage.get("total_tokens", self.last_prompt_tokens + self.last_completion_tokens) if self.last_prompt_tokens > 0: self.last_real_prompt_tokens = self.last_prompt_tokens + fallback_compaction = ( + self._verify_compaction_cleared_threshold + and self._last_summary_fallback_used + ) if self.last_prompt_tokens < self.threshold_tokens: if self.awaiting_real_usage_after_compression and self.last_compression_rough_tokens > 0: self.last_rough_tokens_when_real_prompt_fit = self.last_compression_rough_tokens # Any real provider reading below the trigger proves the prompt - # fits again. Clear the episode latch even when this response was - # not the one immediately following compaction. - self._ineffective_compression_count = 0 + # fits again, UNLESS the just-finished compaction had to fall + # back to the deterministic placeholder summary. That path + # does shrink the prompt, but it is still a degraded compaction + # attempt and must count toward the anti-thrashing strike limit. + # Otherwise repeated empty-summary failures can rotate through + # fallback markers forever while each smaller prompt reading + # resets the counter back to zero (#63008 / R1). + if not fallback_compaction: + self._ineffective_compression_count = 0 else: self.last_rough_tokens_when_real_prompt_fit = 0 @@ -1213,7 +1223,17 @@ class ContextCompressor(ContextEngine): # Keying on real usage compares like with like and fires exactly once # per compaction. if self._verify_compaction_cleared_threshold: - if self.last_prompt_tokens >= self.threshold_tokens: + if fallback_compaction: + self._ineffective_compression_count += 1 + if not self.quiet_mode: + logger.warning( + "Compaction completed with a deterministic fallback " + "summary. Counting this as a degraded attempt to " + "avoid repeated fallback-only compaction loops. " + "ineffective_compression_count=%d", + self._ineffective_compression_count, + ) + elif self.last_prompt_tokens >= self.threshold_tokens: self._ineffective_compression_count += 1 if not self.quiet_mode: logger.warning( diff --git a/tests/agent/test_context_compressor.py b/tests/agent/test_context_compressor.py index 252737e3e851..46cbff8bfa15 100644 --- a/tests/agent/test_context_compressor.py +++ b/tests/agent/test_context_compressor.py @@ -65,6 +65,61 @@ class TestUpdateFromResponse: compressor.update_from_response({}) assert compressor.last_prompt_tokens == 0 + def test_fallback_compaction_counts_as_ineffective_even_when_prompt_fits(self, compressor): + compressor.threshold_tokens = 85_000 + compressor.awaiting_real_usage_after_compression = True + compressor.last_compression_rough_tokens = 90_000 + compressor._verify_compaction_cleared_threshold = True + compressor._last_summary_fallback_used = True + + compressor.update_from_response({ + "prompt_tokens": 5_000, + "completion_tokens": 100, + "total_tokens": 5_100, + }) + + assert compressor.last_real_prompt_tokens == 5_000 + assert compressor.last_rough_tokens_when_real_prompt_fit == 90_000 + assert compressor._ineffective_compression_count == 1 + assert compressor._verify_compaction_cleared_threshold is False + assert compressor.awaiting_real_usage_after_compression is False + + def test_successful_compaction_fit_still_clears_prior_ineffective_count(self, compressor): + compressor.threshold_tokens = 85_000 + compressor.awaiting_real_usage_after_compression = True + compressor.last_compression_rough_tokens = 90_000 + compressor._verify_compaction_cleared_threshold = True + compressor._last_summary_fallback_used = False + compressor._ineffective_compression_count = 1 + + compressor.update_from_response({ + "prompt_tokens": 5_000, + "completion_tokens": 100, + "total_tokens": 5_100, + }) + + assert compressor._ineffective_compression_count == 0 + + def test_second_fallback_strike_blocks_future_compression(self, compressor): + compressor.threshold_tokens = 85_000 + compressor.last_prompt_tokens = 90_000 + assert compressor.should_compress() is True + + for expected in (1, 2): + compressor.awaiting_real_usage_after_compression = True + compressor.last_compression_rough_tokens = 90_000 + compressor._verify_compaction_cleared_threshold = True + compressor._last_summary_fallback_used = True + compressor.update_from_response({ + "prompt_tokens": 5_000, + "completion_tokens": 100, + "total_tokens": 5_100, + }) + assert compressor._ineffective_compression_count == expected + compressor.last_prompt_tokens = 90_000 + + assert compressor.should_compress() is False + class TestPreflightDeferral: def test_defers_when_recent_real_usage_fit_and_rough_growth_is_small(self, compressor):