fix(compression): merge todo snapshot into trailing user msg to avoid consecutive user/user turns

After context compression, the preserved todo list was unconditionally
appended as a standalone user message. When the compressed transcript
already ends with a user message (common case), this creates consecutive
user/user turns — a role-alternation violation some providers reject.

Fix: fold the snapshot into the trailing user message (blank-line separated)
when one exists with plain-string content. Falls back to append when the
tail is non-user, empty, or has structured (list) content.

Rebased on current upstream/main.

Closes #53890
This commit is contained in:
Yingliang Zhang 2026-07-01 00:04:06 +08:00 committed by Teknium
parent 18d83b4da9
commit d2bb6cc251
2 changed files with 190 additions and 5 deletions

View file

@ -1532,11 +1532,29 @@ def compress_context(
todo_snapshot = agent._todo_store.format_for_injection()
if todo_snapshot:
compressed.append({
"role": "user",
"content": todo_snapshot,
"_todo_snapshot_synthetic": True,
})
# Fold the snapshot into a trailing user message so compression
# never introduces a synthetic user/user pair. When it must stand
# alone, retain the marker so the real-user preservation pass does
# not mistake todo scaffolding for human intent.
if compressed and compressed[-1].get("role") == "user":
from agent.context_compressor import _append_text_to_content
_tail = compressed[-1]
_tail_content = _tail.get("content")
_snapshot_text = (
f"\n\n{todo_snapshot}"
if isinstance(_tail_content, str) and _tail_content
else todo_snapshot
)
_tail["content"] = _append_text_to_content(
_tail_content, _snapshot_text
)
else:
compressed.append({
"role": "user",
"content": todo_snapshot,
"_todo_snapshot_synthetic": True,
})
_ensure_compressed_has_user_turn(messages, compressed)
cached_system_prompt = agent._cached_system_prompt