From dd35af6181e6497cd0338bc5201db65a8c941c65 Mon Sep 17 00:00:00 2001 From: Ahmed Allam Date: Sat, 25 Jul 2026 23:24:34 +0000 Subject: [PATCH] fix(context): skip compaction when the window can't fit a summary request When instructions, the reserved summary output, and any prior checkpoint already consume the model's context window, the summary-input budget is zero. Previously a doomed summary request was still submitted (and rejected). Skip summarization in that case and log, rather than issue a request guaranteed to overflow. --- strix/llm/compaction.py | 16 +++++++++++----- tests/test_compaction.py | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/strix/llm/compaction.py b/strix/llm/compaction.py index 5ad3a074..cd6126c4 100644 --- a/strix/llm/compaction.py +++ b/strix/llm/compaction.py @@ -297,13 +297,19 @@ async def maybe_compact( split = _select_split(model, items, context.keep_tokens) head, recent = items[:split], items[split:] - if not head: + previous = _previous_summary(head) + input_budget = _summary_input_budget(model, previous) + if not head or input_budget <= 0: + # Nothing to summarise, or the window can't even fit the summary + # instructions plus the reserved output allowance — any request would + # be rejected, so skip rather than submit a doomed call. + if head: + logger.warning( + "skipping compaction for %s: no room to summarise within its context window", model + ) return False - previous = _previous_summary(head) - serialized_head = _fit_to_tokens( - model, _serialize_items(head), _summary_input_budget(model, previous) - ) + serialized_head = _fit_to_tokens(model, _serialize_items(head), input_budget) summary = await _summarize( model, _build_summary_prompt(serialized_head, previous), diff --git a/tests/test_compaction.py b/tests/test_compaction.py index f25f0abf..9e56825e 100644 --- a/tests/test_compaction.py +++ b/tests/test_compaction.py @@ -139,11 +139,11 @@ async def test_maybe_compact_noop_when_within_budget(monkeypatch: pytest.MonkeyP @pytest.mark.asyncio async def test_maybe_compact_rewrites_and_keeps_pairs(monkeypatch: pytest.MonkeyPatch) -> None: - _patch_budget(monkeypatch, keep_tokens=30, window=50) + _patch_budget(monkeypatch, keep_tokens=30, window=4_000) _patch_summary(monkeypatch, "SUMMARY BODY") session = FakeSession(_turns(12)) - assert await compaction.maybe_compact(session, model="m") is True + assert await compaction.maybe_compact(session, model="m", force=True) is True items = await session.get_items() assert items[0]["role"] == "user" @@ -155,7 +155,8 @@ async def test_maybe_compact_rewrites_and_keeps_pairs(monkeypatch: pytest.Monkey @pytest.mark.asyncio async def test_maybe_compact_updates_previous_summary(monkeypatch: pytest.MonkeyPatch) -> None: - _patch_budget(monkeypatch, keep_tokens=30, window=50) + # Window large enough to leave real room for the summary instructions. + _patch_budget(monkeypatch, keep_tokens=30, window=4_000) captured: dict[str, str] = {} async def fake_acompletion(**kwargs: Any) -> Any: @@ -167,7 +168,7 @@ async def test_maybe_compact_updates_previous_summary(monkeypatch: pytest.Monkey prior = compaction._checkpoint_item("OLD SUMMARY TEXT") session = FakeSession([prior, *_turns(12)]) - assert await compaction.maybe_compact(session, model="m") is True + assert await compaction.maybe_compact(session, model="m", force=True) is True assert "OLD SUMMARY TEXT" in captured["prompt"] @@ -227,7 +228,7 @@ async def test_summary_request_fits_when_room_is_below_old_floor( @pytest.mark.asyncio async def test_maybe_compact_skips_when_summary_fails(monkeypatch: pytest.MonkeyPatch) -> None: - _patch_budget(monkeypatch, keep_tokens=30, window=50) + _patch_budget(monkeypatch, keep_tokens=30, window=4_000) async def fake_acompletion(**_kwargs: Any) -> Any: raise RuntimeError("boom") @@ -236,5 +237,28 @@ async def test_maybe_compact_skips_when_summary_fails(monkeypatch: pytest.Monkey session = FakeSession(_turns(12)) before = await session.get_items() - assert await compaction.maybe_compact(session, model="m") is False + assert await compaction.maybe_compact(session, model="m", force=True) is False + assert await session.get_items() == before + + +@pytest.mark.asyncio +async def test_maybe_compact_skips_when_no_room_to_summarise( + monkeypatch: pytest.MonkeyPatch, +) -> None: + # A window too small to fit the summary instructions plus the reserved + # output leaves no room for any head, so no (doomed) summary is attempted. + _patch_budget(monkeypatch, keep_tokens=30, window=200) + called = False + + async def fake_acompletion(**_kwargs: Any) -> Any: + nonlocal called + called = True + return SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="S"))]) + + monkeypatch.setattr("strix.llm.compaction.litellm.acompletion", fake_acompletion) + session = FakeSession(_turns(12)) + before = await session.get_items() + + assert await compaction.maybe_compact(session, model="m", force=True) is False + assert called is False assert await session.get_items() == before