Skip to content

Commit 47e78d0

Browse files
committed
fix(s08): keep the active user request visible after snip_compact
snip_compact archives the middle section of the history. Once a task accumulates many tool calls, the current user request moves into that archived middle, and because the context then fits under the limit, compact_history never runs — so the model loses sight of the active request and can drift back to an older one (reproduced in #577 by entering the three README example prompts in one conversation). prepare() now checks whether snip_compact archived messages and the active request is no longer present verbatim; only then is the request reinserted as a dedicated "Current user request" message, in the same format compact_history and reactive_compact already use. Histories without an archive marker and requests still visible in the kept head or tail are left untouched. Adds two regression tests: one drives the request into the archived middle behind earlier turns plus a tool-call burst, the other asserts no duplicate request message appears when nothing was archived. Fixes #577
1 parent 0dcafa2 commit 47e78d0

2 files changed

Lines changed: 66 additions & 0 deletions

File tree

‎s08_context_compact/code.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,20 @@ def reactive_compact(self, messages: list, active_request: str) -> list:
509509
def prepare(self, messages: list, active_request: str) -> list:
510510
messages = self.tool_result_budget(messages)
511511
messages = self.snip_compact(messages)
512+
if active_request and any(
513+
self.is_archive_marker(message) for message in messages
514+
) and not any(
515+
isinstance(message.get("content"), str)
516+
and message["content"] == active_request
517+
for message in messages
518+
if message.get("role") == "user"
519+
):
520+
# snip_compact archives the middle section, which may contain the
521+
# current user request once a task accumulates many tool calls.
522+
# Reinsert it in the same format used by compact_history /
523+
# reactive_compact so the model never loses the active task.
524+
messages.insert(0, {"role": "user", "content":
525+
f"Current user request:\n{active_request}"})
512526
if self.estimate_chars(messages) > self.CONTEXT_CHAR_LIMIT:
513527
target = int(self.CONTEXT_CHAR_LIMIT * 0.8)
514528
messages = self.micro_compact(messages, target)

‎tests/test_s08_context_compact.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,55 @@ def test_prepare_persists_oversized_unseen_result_before_full_compact(
136136
saved_line = next(line for line in content.splitlines()
137137
if line.startswith("Full output: "))
138138
assert Path(saved_line.removeprefix("Full output: ")).read_text() == output
139+
140+
141+
def test_prepare_reinserts_active_request_archived_by_snip_compact(
142+
tmp_path, monkeypatch):
143+
lesson = load_lesson(monkeypatch, tmp_path)
144+
request = "compare s08 and s09 context handling"
145+
# Earlier conversation turns occupy the head of the history, so the
146+
# active request sits before a long tool-call burst — the shape that
147+
# lets snip_compact archive it into the middle section.
148+
messages = [
149+
{"role": "user", "content": "earlier question one"},
150+
{"role": "assistant", "content": [{"type": "text", "text": "answer one"}]},
151+
{"role": "user", "content": "earlier question two"},
152+
{"role": "assistant", "content": [{"type": "text", "text": "answer two"}]},
153+
{"role": "user", "content": request},
154+
{"role": "assistant", "content": [{"type": "text", "text": "starting"}]},
155+
]
156+
for index in range(26):
157+
tool_id = f"tool-{index}"
158+
messages.extend([
159+
{"role": "assistant", "content": [
160+
{"type": "tool_use", "id": tool_id, "name": "bash", "input": {}}
161+
]},
162+
{"role": "user", "content": [
163+
{"type": "tool_result", "tool_use_id": tool_id, "content": f"out-{index}"}
164+
]},
165+
])
166+
167+
prepared = lesson["COMPACTOR"].prepare(list(messages), request)
168+
rendered = "\n".join(
169+
message["content"] for message in prepared
170+
if message.get("role") == "user"
171+
and isinstance(message.get("content"), str)
172+
)
173+
assert "Current user request" in rendered
174+
assert request in rendered
175+
176+
177+
def test_prepare_does_not_duplicate_active_request_visible_in_tail(
178+
tmp_path, monkeypatch):
179+
lesson = load_lesson(monkeypatch, tmp_path)
180+
request = "summarize the naming pattern"
181+
messages = [{"role": "user", "content": request}]
182+
183+
prepared = lesson["COMPACTOR"].prepare(list(messages), request)
184+
185+
assert len([message for message in prepared
186+
if message.get("role") == "user"
187+
and message.get("content") == request]) == 1
188+
assert not any(isinstance(message.get("content"), str)
189+
and message["content"].startswith("Current user request")
190+
for message in prepared)

0 commit comments

Comments
 (0)