From d8504df7e48b17a33e5820162e5fd5df228f8604 Mon Sep 17 00:00:00 2001 From: kshitijk4poor Date: Sat, 4 Jul 2026 20:42:12 +0530 Subject: [PATCH] refactor(compression): reuse _fresh_compaction_message_copy in user-turn guard Replace the inline dict-copy + _db_persisted pop in _ensure_compressed_has_user_turn with the canonical _fresh_compaction_message_copy helper (the same primitive the compressor's own protected-head/tail assembly uses), so the persistence-marker strip stays consistent across all compaction copy sites (#57491). Expand the docstring to record the alternation-safety and end-placement rationale. --- agent/conversation_compression.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/agent/conversation_compression.py b/agent/conversation_compression.py index a46789737a3..7e7a26dc2ed 100644 --- a/agent/conversation_compression.py +++ b/agent/conversation_compression.py @@ -392,15 +392,35 @@ def conversation_history_after_compression(agent: Any, messages: list) -> Option def _ensure_compressed_has_user_turn(original_messages: list, compressed: list) -> None: - """Preserve a real user turn when a compressor returns assistant/tool-only context.""" + """Preserve a real user turn when a compressor returns assistant/tool-only context. + + On repeated compaction the protected head decays to the system prompt only, + the middle summary can land as ``role="assistant"``, and a tool-heavy tail + can be all assistant/tool — so the compacted transcript can legitimately + contain zero user messages. Strict chat templates (LM Studio / llama.cpp + Jinja) then fail with "No user query found in messages" (#55677). + + The restored turn is appended at the END: the guard only runs when + ``compressed`` currently ends with an assistant/tool message (any existing + user turn — including a todo-snapshot append — short-circuits the + ``any()`` check), so appending a user message never creates consecutive + same-role messages. ``_fresh_compaction_message_copy`` copies the message + and strips the ``_db_persisted`` marker so the rotation/in-place flush + still persists the restored row to the new session (#57491). + + If the pre-compression transcript itself carried no user turn at all + (near-impossible — every real conversation opens with a user request — + but kept as a defensive backstop), a minimal continuation marker is + appended instead so strict templates still see a user message. + """ if any(isinstance(msg, dict) and msg.get("role") == "user" for msg in compressed): return + from agent.context_compressor import _fresh_compaction_message_copy + for msg in reversed(original_messages): if not isinstance(msg, dict) or msg.get("role") != "user": continue - restored = dict(msg) - restored.pop("_db_persisted", None) - compressed.append(restored) + compressed.append(_fresh_compaction_message_copy(msg)) return compressed.append({ "role": "user",