From 314abb47b6abc237af2dbe7f5a1bfe2e29b954c5 Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Tue, 1 Sep 2026 16:45:06 +0800 Subject: [PATCH] fix(context): keep _partition in bounds when nothing is kept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``keep_recent=0`` — how a caller asks for the whole history to be summarised — made ``split_idx == len(rest)``, which is neither the forward nor the backward case of the orphan guard added in e4908c8: the forward walk has nothing to scan, and the ``else`` branch indexed ``rest[len(rest)]``. The kept window is empty there, so no orphan tool result is possible and both walks are meaningless. Leave the split alone instead. Callers that summarise a finished session (``SessionHistoryCompactor._summarize`` and apodex's explicit compaction) wrap ``compact()`` in ``try/except``, so the IndexError surfaced as "no summary available" and the whole rollup was lost silently. Co-Authored-By: Claude Opus 5 (1M context) --- agent_core/runtime/loop/compact_llm.py | 6 +++++- tests/test_compact_llm.py | 27 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/agent_core/runtime/loop/compact_llm.py b/agent_core/runtime/loop/compact_llm.py index 2f58f41..740d666 100644 --- a/agent_core/runtime/loop/compact_llm.py +++ b/agent_core/runtime/loop/compact_llm.py @@ -383,7 +383,7 @@ def _partition( forward += 1 if forward < len(rest): split_idx = forward - else: + elif split_idx < len(rest): # The whole tail is tool results — a parallel tool-call turn that # emitted at least ``keep_recent`` of them. Walking forward runs off # the end and leaves the last result orphaned (the bug this guard @@ -393,6 +393,10 @@ def _partition( # recent history survives, not a cap. while split_idx > 0 and is_tool_msg(rest[split_idx]): split_idx -= 1 + # ``split_idx == len(rest)`` (``keep_recent=0``: summarise everything) is + # neither case. The kept window is EMPTY, so no orphan is possible, and + # both walks are meaningless — the forward one has nothing to scan and + # the backward one would index one past the end. Leave the split alone. return sys_msgs, rest[:split_idx], rest[split_idx:] diff --git a/tests/test_compact_llm.py b/tests/test_compact_llm.py index 0182630..868a77f 100644 --- a/tests/test_compact_llm.py +++ b/tests/test_compact_llm.py @@ -232,6 +232,33 @@ def test_partition_still_shrinks_the_window_when_it_can() -> None: assert middle == history[1:3] +def test_partition_summarises_everything_when_nothing_is_kept() -> None: + """``keep_recent=0`` asks for the whole history to be summarised. + + The kept window is then empty, so the orphan guard has nothing to protect: + walking forward runs off the end, and the backward walk added for a + tool-result tail indexed one past it — an ``IndexError`` that callers + summarising a finished session (``keep_recent=0`` is how they ask for it) + swallowed as "no summary available", silently losing the rollup. + """ + history: list[Message] = [ + system_msg("S"), + user_msg("q"), + { + "role": "assistant", + "content": "", + "tool_calls": [{"id": "a", "function": {"name": "web_search", "arguments": "{}"}}], + }, + tool_msg("result a", "a"), + ] + + system, middle, recent = LLMSummaryCompactor._partition(history, keep_recent=0) + + assert system == [history[0]] + assert middle == history[1:] + assert recent == [] + + def test_rate_limit_naming_a_retry_delay_is_still_transient() -> None: """A bare ``400`` inside a provider message is not a status code.