fix(context): persist fallback compaction breaker

This commit is contained in:
kshitijk4poor 2026-07-14 01:50:20 +05:30 committed by kshitij
parent 5ce827cac9
commit af7dceaf77
10 changed files with 394 additions and 91 deletions

View file

@ -210,6 +210,70 @@ class TestFutilityGuard:
assert cc.awaiting_real_usage_after_compression is False
assert cc._ineffective_compression_count == 0
def test_fallback_streak_survives_ordinary_fitting_responses(self):
cc = _compressor(threshold_tokens=24_576)
cc.record_completed_compaction(used_fallback=True)
cc.update_from_response({"prompt_tokens": 20_000})
assert cc._fallback_compression_streak == 1
# Context regrows through ordinary successful turns before the next
# fallback boundary. Those turns reset real-usage effectiveness, not
# the independent summary-quality breaker.
cc.update_from_response({"prompt_tokens": 20_000})
cc.record_completed_compaction(used_fallback=True)
cc.update_from_response({"prompt_tokens": 20_000})
assert cc._fallback_compression_streak == 2
assert not cc.should_compress(33_564)
def test_usage_less_fallback_boundary_still_counts(self):
cc = _compressor(threshold_tokens=24_576)
cc.record_completed_compaction(used_fallback=True)
cc.awaiting_real_usage_after_compression = True
cc.update_from_response({})
assert cc._fallback_compression_streak == 1
assert cc._verify_compaction_cleared_threshold is False
assert cc.awaiting_real_usage_after_compression is False
def test_healthy_boundary_resets_only_fallback_streak(self):
cc = _compressor(threshold_tokens=24_576)
cc.record_completed_compaction(used_fallback=True)
cc.record_completed_compaction(used_fallback=False)
assert cc._fallback_compression_streak == 0
assert cc._verify_compaction_cleared_threshold is True
def test_model_switch_resets_and_persists_fallback_streak(self, tmp_path):
from hermes_state import SessionDB
db = SessionDB(db_path=tmp_path / "state.db")
db.create_session("s1", source="cli")
cc = _compressor(threshold_tokens=24_576)
cc.bind_session_state(db, "s1")
cc.record_completed_compaction(used_fallback=True)
cc.update_model("next-model", 100_000)
assert cc._fallback_compression_streak == 0
assert db.get_compression_fallback_streak("s1") == 0
def test_same_runtime_context_recalibration_preserves_fallback_streak(self, tmp_path):
from hermes_state import SessionDB
db = SessionDB(db_path=tmp_path / "state.db")
db.create_session("s1", source="cli")
cc = _compressor(threshold_tokens=24_576)
cc.bind_session_state(db, "s1")
cc.record_completed_compaction(used_fallback=True)
cc.update_model(cc.model, 64_000, provider=cc.provider)
assert cc._fallback_compression_streak == 1
assert db.get_compression_fallback_streak("s1") == 1
def test_a_failed_pass_records_exactly_one_strike(self):
"""A compaction that leaves the real prompt over the threshold: one strike.

View file

@ -21,6 +21,7 @@ import os
from pathlib import Path
from unittest.mock import MagicMock, patch
from agent.context_compressor import ContextCompressor
from hermes_state import SessionDB
@ -130,3 +131,96 @@ class TestPlatformForwardedAtBoundary:
kwargs = calls[-1].kwargs
assert kwargs.get("platform") == "telegram"
assert kwargs.get("boundary_reason") == "compression"
class TestFallbackStreakFollowsRotation:
def test_fallback_boundary_persists_on_child_session(self, tmp_path: Path):
db = SessionDB(db_path=tmp_path / "state.db")
parent = "PARENT_FALLBACK_ROT"
db.create_session(parent, source="telegram")
with patch(
"agent.context_compressor.get_model_context_length",
return_value=100_000,
):
compressor = ContextCompressor(
model="test/model",
threshold_percent=0.85,
protect_first_n=2,
protect_last_n=2,
quiet_mode=True,
)
compressor.bind_session_state(db, parent)
# A fallback streak must survive the session-id rotation itself. The
# boundary then records the just-completed fallback on the child row.
compressor.record_completed_compaction(used_fallback=True)
assert db.get_compression_fallback_streak(parent) == 1
db.create_session(
"CHILD_FALLBACK_ROT",
source="telegram",
parent_session_id=parent,
)
compressor.on_session_start(
"CHILD_FALLBACK_ROT",
session_db=db,
boundary_reason="compression",
old_session_id=parent,
)
assert compressor._fallback_compression_streak == 1
compressor.record_completed_compaction(used_fallback=True)
assert compressor._fallback_compression_streak == 2
assert db.get_compression_fallback_streak("CHILD_FALLBACK_ROT") == 2
resumed = ContextCompressor(
model="test/model",
threshold_percent=0.85,
protect_first_n=2,
protect_last_n=2,
quiet_mode=True,
)
resumed.bind_session_state(db, "CHILD_FALLBACK_ROT")
assert resumed._fallback_compression_streak == 2
def test_real_rotation_records_fallback_after_lifecycle_rebind(self, tmp_path: Path):
db = SessionDB(db_path=tmp_path / "state.db")
parent = "PARENT_REAL_FALLBACK_ROT"
db.create_session(parent, source="telegram")
agent = _build_agent_with_db(db, parent, platform="telegram")
with patch(
"agent.context_compressor.get_model_context_length",
return_value=100_000,
):
compressor = ContextCompressor(
model="test/model",
threshold_percent=0.85,
protect_first_n=2,
protect_last_n=2,
quiet_mode=True,
)
compressor.bind_session_state(db, parent)
compressed = [
{"role": "user", "content": "[CONTEXT COMPACTION] fallback"},
{"role": "assistant", "content": "tail"},
]
def _fallback_compress(*_args, **_kwargs):
compressor._last_summary_error = "empty summary"
compressor._last_summary_fallback_used = True
compressor._last_compression_made_progress = True
return compressed
with patch.object(
compressor,
"compress",
side_effect=_fallback_compress,
):
compressor.compression_count = 1
setattr(agent, "context_compressor", compressor)
agent._compress_context(_msgs(), "sys", approx_tokens=120_000)
child = getattr(agent, "session_id")
assert child != parent
assert compressor._fallback_compression_streak == 1
assert db.get_compression_fallback_streak(child) == 1

View file

@ -65,62 +65,6 @@ 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):
compressor.threshold_tokens = 85_000

View file

@ -165,6 +165,7 @@ def test_reset_session_state_rebinds_builtin_compressor_after_session_switch(tmp
db.create_session("old-sid", source="cli")
db.create_session("new-sid", source="cli")
db.record_compression_failure_cooldown("old-sid", 4_000_000_000.0, "old-timeout")
db.set_compression_fallback_streak("old-sid", 2)
monkeypatch.setattr(
"agent.context_compressor.get_model_context_length",
@ -188,7 +189,9 @@ def test_reset_session_state_rebinds_builtin_compressor_after_session_switch(tmp
assert compressor._session_id == "new-sid"
assert compressor.get_active_compression_failure_cooldown() is None
assert compressor._fallback_compression_streak == 0
assert db.get_compression_failure_cooldown("old-sid") is not None
assert db.get_compression_fallback_streak("old-sid") == 2
compressor._record_compression_failure_cooldown(30.0, "new-timeout")