From 4d1b2dcf652d31b99f8e6fe9b0ca4a41e3393da2 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:45:41 +0530 Subject: [PATCH] fix(compaction): tolerate content-less user item at over-budget boundary BN01 fold2 finding (2c Low): collapsing the over-budget branch in d24e5875b7 read `item["content"]` unguarded. A user message whose text is measured only via `output_text` (no `content` key) reaches that branch when it exceeds the budget and raised KeyError, aborting the prune. Use `item.get("content")` so it falls through to the non-truncatable path (keep nothing, budget reset) as before the refactor. The typed-boundary parametrize gains a row for it. --- agent/native_compaction.py | 2 +- tests/agent/test_native_compaction_typed_user_retention.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/agent/native_compaction.py b/agent/native_compaction.py index 67236ecca1..44c721d397 100644 --- a/agent/native_compaction.py +++ b/agent/native_compaction.py @@ -336,7 +336,7 @@ def prune_pre_checkpoint_items( retained_reversed.append(item) user_remaining -= cost else: - content = item["content"] + content = item.get("content") if isinstance(content, str): head = content[: user_remaining * 4] keep = bool(head.strip()) diff --git a/tests/agent/test_native_compaction_typed_user_retention.py b/tests/agent/test_native_compaction_typed_user_retention.py index 82071af960..cf300eadfe 100644 --- a/tests/agent/test_native_compaction_typed_user_retention.py +++ b/tests/agent/test_native_compaction_typed_user_retention.py @@ -58,13 +58,16 @@ _IMAGE = {"type": "input_image", "image_url": "data:image/png;base64,AAAA"} {"type": "input_text", "text": "trailing"}], 2, ("abcd", "efgh")), ([{"type": "input_text", "text": "你好世界"}], 2, ("你好",)), ([{"type": "input_text", "text": "CURRENT" * 20}, _IMAGE], 2, None), + (None, 2, None), ], - ids=["multipart-boundary", "cjk-estimator-budget", "oversized-mixed-image"], + ids=["multipart-boundary", "cjk-estimator-budget", "oversized-mixed-image", "oversized-output-text-only"], ) def test_typed_boundary_is_budgeted_and_never_substitutes_older_ask(content, budget, expected): older = {"role": "user", "content": "old"} current = {"type": "message", "role": "user", "id": "current-user", "content": deepcopy(content)} - for index, part in enumerate(current["content"]): + if content is None: # Content-less item measured only via output_text. + current = {"type": "message", "role": "user", "output_text": "CURRENT" * 20} + for index, part in enumerate(current.get("content") or []): part["metadata"] = {"index": index} items = [older, current, _CHECKPOINT] original = deepcopy(items)