Fix duplicate assistant transcript merge

This commit is contained in:
Frank Song
2026-05-11 13:06:11 +08:00
parent 640cf6e6a9
commit 5a445e7562
3 changed files with 77 additions and 0 deletions

View File

@@ -2,6 +2,8 @@
## [Unreleased]
- Fixed duplicate assistant messages in browser chat transcripts by ignoring adjacent assistant replay duplicates during server-side result merge while preserving identical assistant text across separate user turns (#2051).
## [v0.51.43] — 2026-05-11 — Release S (fused community PR — desktop sidebar collapse)
### Added

View File

@@ -1753,6 +1753,18 @@ def _merge_display_messages_after_agent_result(previous_display, previous_contex
# in result_messages, keep the durable checkpoint and append only
# the assistant/tool delta.
continue
if (
key is not None
and isinstance(msg, dict)
and msg.get('role') == 'assistant'
and merged
and _message_identity(merged[-1]) == key
):
# Some provider/result replay paths can include the same assistant
# message twice in the current delta. Treat only adjacent identity
# matches as replay duplicates so identical answers in separate
# user turns remain visible.
continue
if _is_context_compression_marker(msg) and key is not None and key in seen:
continue
display_msg = msg

View File

@@ -159,6 +159,69 @@ def test_deferred_turn_is_materialized_when_agent_returns_assistant_only_delta()
assert [m["content"] for m in merged[-2:]] == ["latest prompt", "current answer"]
def test_duplicate_assistant_delta_is_not_persisted_twice():
"""Provider/result merge replay must not duplicate the same assistant bubble."""
previous_display = [
{"role": "user", "content": "older prompt"},
{"role": "assistant", "content": "older answer"},
]
previous_context = list(previous_display)
result_messages = previous_context + [
{"role": "user", "content": "latest prompt"},
{"role": "assistant", "content": "current answer"},
{"role": "assistant", "content": "current answer"},
]
merged = streaming._merge_display_messages_after_agent_result(
previous_display=previous_display,
previous_context=previous_context,
result_messages=result_messages,
msg_text="latest prompt",
)
assert [m["role"] for m in merged] == [
"user",
"assistant",
"user",
"assistant",
]
assert [m["content"] for m in merged[-2:]] == ["latest prompt", "current answer"]
assert (
sum(
1
for m in merged
if m.get("role") == "assistant" and m.get("content") == "current answer"
)
== 1
)
def test_same_assistant_text_across_different_turns_is_preserved():
previous_display = [
{"role": "user", "content": "first prompt"},
{"role": "assistant", "content": "same answer"},
]
previous_context = list(previous_display)
result_messages = previous_context + [
{"role": "user", "content": "second prompt"},
{"role": "assistant", "content": "same answer"},
]
merged = streaming._merge_display_messages_after_agent_result(
previous_display=previous_display,
previous_context=previous_context,
result_messages=result_messages,
msg_text="second prompt",
)
assert [m["content"] for m in merged] == [
"first prompt",
"same answer",
"second prompt",
"same answer",
]
def test_llm_title_generated_survives_save_and_load(_isolate_state):
s = Session(
session_id="generated_title",