Fix duplicate assistant transcript merge
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user