fix(ui): deduplicate assistant messages on chat.status final event#86646
fix(ui): deduplicate assistant messages on chat.status final event#86646SebTardif wants to merge 10 commits into
Conversation
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. Close as superseded: this older WebChat duplicate-final branch is not on main, and the newer open replacement PR is the clean, proof-positive canonical path for the same root cause. Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Canonical path: Keep review and landing focused on #96081, then close the linked WebChat duplicate-final issue from that merged implementation. So I’m closing this here and keeping the remaining discussion on #96081. Review detailsBest possible solution: Keep review and landing focused on #96081, then close the linked WebChat duplicate-final issue from that merged implementation. Do we have a high-confidence way to reproduce the issue? Yes, source-level: current main appends active-run final assistant payloads without checking whether refreshed history already supplied the same assistant tail. I did not run a live browser reproduction in this read-only review. Is this the best way to solve the issue? No. This branch is a plausible local patch, but the newer PR is the stronger canonical fix because it centralizes assistant-tail reconciliation and covers sibling history/stream cases with proof and tests. Security review: Security review cleared: The diff is limited to WebChat controller state logic and tests, with no dependency, workflow, credential, package, or code-execution surface change. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against c6ade83a5ccb. |
|
ClawSweeper PR egg 🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat. Where did the egg go?
|
2a38a59 to
91d6cc9
Compare
2087bee to
133e5ef
Compare
Guard every append point in handleChatEvent with a tail-duplicate check using messageDisplaySignature. When a final, aborted, or stream-fallback assistant message matches the last entry already in chatMessages, the append is skipped. This prevents the race where loadChatHistory (triggered after tool events or session.message) returns history that already includes the assistant message, and the optimistic append from the final event creates a second copy. Closes openclaw#85771 Signed-off-by: Sebastien Tardif <[email protected]>
Signed-off-by: Sebastien Tardif <[email protected]>
Remove the isTailDuplicate guard from the stale-run final path so sub-agent or separate-run messages with identical text are always stored. The renderer already collapses consecutive identical messages via duplicateCount instead of silently dropping them. Same-turn dedup (current-run final/aborted paths) is preserved to catch the loadChatHistory race condition.
Signed-off-by: Sebastien Tardif <[email protected]>
Signed-off-by: Seb Tardif <[email protected]>
The previous isTailDuplicate used messageDisplaySignature which only compared text content, causing false positives when two messages had the same text but different content structure (e.g., text-only vs text+canvas). Switch to JSON.stringify comparison of content arrays so messages with different block types or counts are not incorrectly deduplicated. This fixes the upstream test 'keeps repeated assistant final text within the same turn' which expects both a text-only and a text+canvas message to be preserved even when they share the same display text. Signed-off-by: Sebastien Tardif <[email protected]>
c9c7710 to
6a452a8
Compare
|
ClawSweeper applied the proposed close for this PR.
|
|
The core fix from this PR landed in main via #88786 (merged 2026-06-03), which addresses the same root cause: terminal Note: ClawSweeper cited #96081 as the covering PR, but that one is still open and covers a different variant (external reply event deduplication). The fix that actually shipped is #88786. |
Fixes #85755
Summary
Fix duplicate assistant messages appearing in WebChat when
loadChatHistoryand thefinalchat event race.Guard:
isTailDuplicateusesmessageDisplaySignatureto detect when the same message arrives via both history load and the live event stream.Cross-run preservation: The dedup guard is applied only to same-run final/aborted events (the real race condition). Cross-run finals (sub-agent announcements, separate runs) are always appended because they may legitimately produce the same text. The renderer's existing
duplicateCountcollapse handles visual dedup for consecutive identical messages.Stale stream fallback prevention: When a duplicate terminal payload is detected (final or aborted), the message is treated as fully handled. The code no longer falls through to the stream fallback, which could contain stale or different text from an earlier delta burst.
Changed files
ui/src/ui/controllers/chat.ts-isTailDuplicatehelper, guards on same-run final/aborted/stream-fallback paths; cross-run path always appends; duplicate terminal payloads skip stream fallbackui/src/ui/controllers/chat.test.ts- 7 new tests: same-run dedup, cross-run preservation, stream-fallback dedup, aborted dedup, stale-stream-on-final-duplicate, stale-stream-on-aborted-duplicateVerification
Behavioral checks:
Closes #85771
Real behavior proof
Behavior addressed: (1) Duplicate assistant messages appear in WebChat when
loadChatHistoryand thefinalchat event race. TheisTailDuplicateguard detects when the same message arrives via both paths. (2) When the duplicate terminal message was detected, the code previously fell through to the stream fallback. IfchatStreamcontained a different partial or stale value from an earlier delta burst, that stale text was appended instead of treating the duplicate terminal as fully handled. Both thefinalandabortedbranches had this issue.Real environment tested: macOS 15.5, Node 26.0.0, OpenClaw source checkout at
/tmp/wt-86646. UI code exercises the productionhandleChatEventfunction fromui/src/ui/controllers/chat.tsvianpx tsx.Exact steps or command run after this patch:
Step 1. Ran the full test suite to confirm all 98 tests pass (including 2 new stale-stream tests):
Step 2. Ran a proof script that imports the production
handleChatEventand exercises three scenarios:The proof script creates real
ChatStateobjects with stalechatStreamvalues and duplicate terminal payloads, then asserts the stale stream text is never appended.Test suite output (98 tests, all passing):
Production proof output:
Observed result after fix: The production
handleChatEventcorrectly handles duplicate terminal payloads without falling through to the stale stream fallback. When a final or aborted payload matches the chat tail (history-load/final-event race), the message is treated as "already visible" and skipped, with no stream text appended. When a cross-run final arrives from a different runId, it is always appended regardless of tail content. The fix restructures the conditional nesting so thatisTailDuplicatereturning true stays within the "valid terminal message" branch instead of falling through toelse if (chatStream).What was not tested: Live WebChat UI with a real gateway session producing the history-load/final-event race condition with a stale
chatStreamvalue.