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.
This commit is contained in:
kshitijk4poor 2026-07-04 20:42:12 +05:30 committed by kshitij
parent 6e176e4c21
commit d8504df7e4

View file

@ -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",