diff --git a/agent/context_compressor.py b/agent/context_compressor.py index 27122ae0b20..ed5fc646a42 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -2854,12 +2854,6 @@ This compaction should PRIORITISE preserving all information related to the focu running so a manual ``/compress`` can retry immediately after an auto-compression abort. Auto-compress callers pass False. """ - # A compaction attempt is running: have should_compress() verify, against - # the next real reading, that it actually cleared the threshold. Set on - # every path (including the early no-op returns below) so a pass that - # changed nothing is still held to account. - self._verify_compaction_cleared_threshold = True - # Reset per-call summary failure state — callers inspect these fields # after compress() returns to decide whether to surface a warning. self._last_summary_dropped_count = 0 diff --git a/agent/conversation_compression.py b/agent/conversation_compression.py index 843960cc281..886e04795c4 100644 --- a/agent/conversation_compression.py +++ b/agent/conversation_compression.py @@ -961,6 +961,12 @@ def compress_context( agent.context_compressor.last_prompt_tokens = -1 agent.context_compressor.last_completion_tokens = 0 agent.context_compressor.awaiting_real_usage_after_compression = True + # Arm the effectiveness verdict only after the full compaction boundary + # succeeds. Exceptions and aborted/no-op attempts return before here, so + # unrelated later usage cannot be charged to an attempt that never took + # effect. External engines without the private flag stay untouched. + if hasattr(agent.context_compressor, "_verify_compaction_cleared_threshold"): + agent.context_compressor._verify_compaction_cleared_threshold = True # Clear the file-read dedup cache. After compression the original # read content is summarised away — if the model re-reads the same diff --git a/tests/agent/test_compaction_anti_thrash.py b/tests/agent/test_compaction_anti_thrash.py index a219526c68f..cd9c6d46220 100644 --- a/tests/agent/test_compaction_anti_thrash.py +++ b/tests/agent/test_compaction_anti_thrash.py @@ -70,6 +70,7 @@ def _turn(cc, msgs, real_prompt_tokens): if not cc.should_compress(real_prompt_tokens): return msgs, False msgs = cc.compress(msgs, current_tokens=real_prompt_tokens) + cc._verify_compaction_cleared_threshold = True cc.update_from_response({"prompt_tokens": real_prompt_tokens}) return msgs, True @@ -180,6 +181,7 @@ class TestFutilityGuard: assert cc.should_compress(real_prompt) msgs = cc.compress(msgs, current_tokens=real_prompt) real_after = floor + int(skew * estimate_messages_tokens_rough(msgs)) + cc._verify_compaction_cleared_threshold = True cc.update_from_response({"prompt_tokens": real_after}) # provider's real count assert real_after < cc.threshold_tokens, "compaction really did clear it" @@ -198,6 +200,7 @@ class TestFutilityGuard: assert cc.should_compress(33_564) cc.compress(msgs, current_tokens=33_564) + cc._verify_compaction_cleared_threshold = True assert cc._ineffective_compression_count == 0, "no verdict before real usage" cc.update_from_response({"prompt_tokens": 33_564}) # still over