Skip to content

fix: WebChat duplicates external reply events#96081

Closed
scotthuang wants to merge 4 commits into
openclaw:mainfrom
scotthuang:fix/webchat-dedupe-chat-events
Closed

fix: WebChat duplicates external reply events#96081
scotthuang wants to merge 4 commits into
openclaw:mainfrom
scotthuang:fix/webchat-dedupe-chat-events

Conversation

@scotthuang

@scotthuang scotthuang commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Related: #85771

What Problem This Solves

Fixes a WebChat-only duplicate reply bug where an externally triggered turn, such as a WeChat/AKK event, could render two identical assistant bubbles even though the durable transcript already had only one assistant message for that turn.

The duplication was visible in WebChat after live streaming completed. The affected session could receive the same assistant answer through two UI data paths:

  • chat.history refresh already returned the persisted assistant message for the current turn.
  • The still-active live stream later delivered the terminal final event for the same assistant answer.

Before this fix, WebChat appended the terminal final message even when the same visible assistant reply had already arrived through the refreshed history snapshot.

Why This Change Was Made

The final fix keeps the reconciliation in Control UI, where the duplicate is created:

  • appendTerminalAssistantMessage now treats terminal assistant finals as replacements for matching assistant messages already present after the latest user turn.
  • The matching is based on assistant-visible content. Hidden reasoning/thinking blocks are ignored, while visible non-text blocks, such as canvas-style blocks, keep the full content signature to avoid over-deduping rich replies.
  • If the active text stream has already been persisted into history, the terminal final replaces that history/stream placeholder instead of appending a second bubble.
  • loadChatHistory now drops a late optimistic assistant tail only when the refreshed current history turn already replaces that assistant text. It preserves legitimate repeated assistant replies from later user turns.

This avoids changing transcript persistence, plugin behavior, WeChat/AKK delivery, or gateway event semantics.

User Impact

WebChat no longer displays duplicate assistant bubbles when a refreshed history snapshot and the live terminal event both contain the same current-turn assistant reply.

Legitimate repeated assistant text is still preserved when it belongs to a distinct later user turn or includes visible non-text content that should render as a separate reply.

Evidence

Automated validation

pnpm test ui/src/ui/controllers/chat.test.ts

Test Files  1 passed (1)
Tests       140 passed (140)
pnpm build
pnpm format:check ui/src/ui/chat/stream-reconciliation.ts ui/src/ui/controllers/chat.ts ui/src/ui/controllers/chat.test.ts
node scripts/run-oxlint.mjs ui/src/ui/chat/stream-reconciliation.ts ui/src/ui/controllers/chat.ts ui/src/ui/controllers/chat.test.ts
git diff --check
.agents/skills/autoreview/scripts/autoreview --mode local

autoreview clean: no accepted/actionable findings reported
overall: patch is correct (0.8)

pnpm check:changed was attempted, but the local Crabbox/Testbox wrapper failed before running project checks:

[crabbox] selected binary failed basic --version/--help sanity checks

Manual WebChat repro proof

The following logs are redacted console proof from the affected WebChat session. They use counts, run ids, sequence numbers, and content hashes only; they do not include message content.

Before fix

WebChat history had already applied the persisted assistant answer for the current turn, then the terminal final event appended another visible bubble:

history before apply:
  status=before-history-apply
  sessionKey=agent:main:openclaw-weixin:direct:[wechat-contact]@im.wechat
  responseTail included assistant:persisted:len=474:hash=3d69233e

live terminal final:
  runId=899794e0-5809-40c1-b882-3054c7fdc28f
  seq=275
  state=final
  before final: chatMessages=100, chatStreamChars=474
  after final:  chatMessages=101, chatStreamChars=0

The chatMessages count increasing from 100 to 101 after the final event is the duplicate WebChat append.

After fix

With this patch, the same current-turn shape reconciles the persisted history assistant and the terminal final instead of appending a second bubble:

history before apply:
  status=before-history-apply
  sessionKey=agent:main:openclaw-weixin:direct:[wechat-contact]@im.wechat
  responseTail included assistant:persisted:len=455:hash=230f659f

live terminal final:
  runId=1c3143ee-c6ec-42fd-8bab-becf615a84bc
  seq=254
  state=final
  before final: chatMessages=100, chatStreamChars=455
  after final:  chatMessages=100, chatStreamChars=0

The chatMessages count staying at 100 after the final event proves WebChat did not append the duplicate terminal bubble.

Screenshot slots

  • Before fix duplicate WebChat reply:
before_1
  • After fix single WebChat reply:
after_1

Regression coverage added

The focused tests cover:

  • terminal final already present in the current turn
  • visible text already present from refreshed history
  • hidden reasoning/thinking blocks ignored for duplicate matching
  • active stream text already persisted by history
  • partial live assistant tail replaced by current history
  • repeated assistant text after a new user turn preserved
  • repeated assistant text with visible non-text content preserved

@scotthuang
scotthuang marked this pull request as ready for review June 23, 2026 11:08
@scotthuang

Copy link
Copy Markdown
Contributor Author

@clawsweeper review

@clawsweeper

clawsweeper Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper

clawsweeper Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 5, 2026, 10:57 AM ET / 14:57 UTC.

Summary
The PR adds WebChat chat-event frame dedupe, same-turn assistant final/history replacement logic, and focused regression tests for duplicate assistant bubbles.

PR surface: Source +217, Tests +207. Total +424 across 4 files.

Reproducibility: yes. source-reproducible: current main can append a terminal assistant final after refreshed history already contains the same visible reply, and the PR body supplies before/after WebChat evidence for that shape.

Review metrics: none identified.

Stored data model
Persistent data-model change detected: serialized state: ui/src/pages/chat/stream-reconciliation.ts. Confirm migration or upgrade compatibility proof before merge.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Mantis proof suggestion
A short browser visual proof would help maintainers confirm one assistant bubble is rendered when history and terminal final contain the same current-turn reply. A maintainer can ask Mantis to capture proof by posting this exact PR comment:

@openclaw-mantis visual task: verify WebChat shows one assistant bubble when refreshed history and the terminal final event contain the same current-turn reply.

Risk before merge

  • [P1] Merging intentionally suppresses or replaces same-turn assistant terminal messages in WebChat; if the visible-content invariant is wrong, a legitimate repeated text-only final could be hidden.
  • [P1] The per-state event-frame cache and history-tail replacement alter Control UI session-view reconciliation, so maintainers should explicitly accept that this dedupe belongs in WebChat rather than gateway or transcript persistence.

Maintainer options:

  1. Land the WebChat fix (recommended)
    Accept the tested Control UI dedupe semantics as the intended narrow fix for duplicate final/history rendering.
  2. Ask for one more browser boundary proof
    Before landing, request or run a short WebChat proof for a rich/non-text assistant reply and an identical repeated-text reply.
  3. Pause for gateway-level design
    Pause or close this branch only if maintainers want the invariant owned by gateway or transcript persistence instead of UI reconciliation.

Next step before merge

  • [P2] No repair lane is needed because no blocking code defect was found; maintainer review should accept or reject the WebChat-owned dedupe semantics before merge.

Maintainer decision needed

  • Question: Should WebChat own suppression and replacement of duplicate same-turn terminal assistant events when refreshed history already contains the visible reply?
  • Rationale: The patch is a narrow, tested UI fix, but it intentionally changes message suppression/replacement behavior and could hide a legitimate repeated text-only final if the invariant is wrong.
  • Likely owner: Onur Solmaz — Their recent stream-reconciliation work is the closest merged ownership signal for the helper and controller paths changed here.
  • Options:
    • Accept WebChat-owned dedupe (recommended): Land this branch as the narrow Control UI fix for the final/history duplicate-render race and keep broader transcript issues separate.
    • Request broader browser proof: Ask for one more live WebChat proof covering rich/non-text replies and repeated text boundaries before merge.
    • Move the invariant lower: Pause this PR only if maintainers decide duplicate final/history reconciliation must be solved in gateway or transcript persistence instead of UI state.

Security
Cleared: The diff is limited to Control UI TypeScript reconciliation logic and tests, with no dependency, workflow, package, credential, or code-execution surface change.

Review details

Best possible solution:

Close this PR as superseded by #88786.

Do we have a high-confidence way to reproduce the issue?

Yes, source-reproducible: current main can append a terminal assistant final after refreshed history already contains the same visible reply, and the PR body supplies before/after WebChat evidence for that shape.

Is this the best way to solve the issue?

Yes, this is the best narrow fix identified: it changes the Control UI reconciliation path where the duplicate bubble is created while avoiding gateway, plugin, transcript persistence, or provider changes.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 862de9f1a1c2.

Label changes

Label justifications:

  • P2: The PR addresses a normal-priority WebChat duplicate-render bug with limited blast radius to Control UI chat state reconciliation.
  • merge-risk: 🚨 message-delivery: The patch can suppress or replace assistant terminal messages in WebChat when they match current-turn history or an accepted event frame.
  • merge-risk: 🚨 session-state: The patch changes chatMessages/chatStream reconciliation and adds per-state event-frame dedupe for the Control UI session view.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (logs): The PR body provides after-fix real behavior proof with redacted WebChat console counts showing the duplicate final no longer appends, plus before/after screenshots.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides after-fix real behavior proof with redacted WebChat console counts showing the duplicate final no longer appends, plus before/after screenshots.
Evidence reviewed

PR surface:

Source +217, Tests +207. Total +424 across 4 files.

View PR surface stats
Area Files Added Removed Net
Source 3 223 6 +217
Tests 1 207 0 +207
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 4 430 6 +424

What I checked:

Likely related people:

  • Onur Solmaz: Commit 17a285f added and heavily changed the stream reconciliation and chat controller surfaces this PR now extends. (role: recent stream-reconciliation contributor; confidence: high; commits: 17a285f29825; files: ui/src/ui/chat/stream-reconciliation.ts, ui/src/ui/controllers/chat.ts, ui/src/ui/controllers/chat.test.ts)
  • scotthuang: Commit 7734a40 changed WebChat history reload behavior during active sends, and the same person authors this candidate fix. (role: adjacent WebChat history contributor and current fix author; confidence: high; commits: 7734a40a5650, 72283139bf7e; files: ui/src/ui/app-gateway.ts, ui/src/pages/chat/chat-gateway.ts, ui/src/pages/chat/chat-history.ts)
  • Peter Steinberger: Commit dc6afeb changed final-event history reload behavior, which is adjacent to the final/history race this PR addresses. (role: adjacent final-event history contributor; confidence: medium; commits: dc6afeb4f882; files: ui/src/ui/app-gateway.ts, ui/src/ui/chat-event-reload.ts)
  • zhangguiping-xydt: PR fix #71992: [Bug]: Control UI webchat duplicates every assistant reply on 2026.4.21 — regression from #5964/#39469 #88786 and commit f6244fb fixed a related Control UI duplicate-render path in the same controller/test area. (role: prior duplicate-render fix contributor; confidence: high; commits: f6244fb68085; files: ui/src/ui/controllers/chat.ts, ui/src/ui/controllers/chat.test.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (1 earlier review cycle)
  • reviewed 2026-07-03T01:06:14.291Z sha c34dd57 :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper review

@clawsweeper

clawsweeper Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper review

1 similar comment
@scotthuang

Copy link
Copy Markdown
Contributor Author

@clawsweeper review

@scotthuang
scotthuang force-pushed the fix/webchat-dedupe-chat-events branch from cc1468f to 4d6d053 Compare June 25, 2026 08:26
@clawsweeper clawsweeper Bot added rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Jul 3, 2026
@scotthuang
scotthuang force-pushed the fix/webchat-dedupe-chat-events branch from c34dd57 to 7228313 Compare July 5, 2026 14:42
@scotthuang

Copy link
Copy Markdown
Contributor Author

Closing in favor of a clean branch/PR with the same body and equivalent code changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. size: M status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant