Skip to content

[Bug]: iOS Chat briefly duplicates final assistant reply before history reconciliation #98116

Description

@ooiuuii

Summary

iOS Chat can briefly render the same assistant final reply twice when a live chat.final event and the canonical session.message transcript event both arrive for the same turn. After a later history refresh, one copy disappears, so the UI looks like it duplicated and then deleted a final answer.

Real environment observed

  • OpenClaw Gateway: 2026.6.11-beta.2
  • Client surface: official iOS OpenClaw app, Chat tab
  • Connection mode: Remote Domain over HTTPS/WSS via Tailscale Funnel to a Windows Gateway
  • Client network: iPhone on mobile data, not the same LAN as the Gateway
  • Gateway path: Windows Gateway exposed through a Tailscale Funnel HTTPS endpoint

Private setup codes, auth tokens, and the exact user transcript are intentionally omitted.

User-visible symptom

A single assistant response appears twice in the iOS Chat transcript. A moment later one copy disappears after the transcript/history state catches up. This makes the mobile chat look unstable even though the Gateway eventually stores only one final assistant turn.

Source-level repro on current main

The duplicate is consistent with the current shared iOS/macOS chat view-model event flow:

  1. OpenClawChatViewModel.handleChatEvent handles chat.state == "final" by calling appendFinalChatMessageIfPresent(chat) so the final answer is visible immediately.
  2. The same turn can also arrive through handleSessionMessageEvent as the canonical session.message transcript row.
  3. Existing dedupeMessages uses messageIdentityKey(for:), which includes the message timestamp and content metadata.
  4. The live chat.final row and canonical session.message row can carry the same assistant text but different timestamps or content metadata, so they do not dedupe before the later chat.history refresh reconciles the transcript.

That sequence produces a transient duplicate final assistant row, followed by one row disappearing when history applies.

Expected behavior

The iOS Chat UI should show one assistant final row for a turn, whether the live final event arrives before or after the canonical session transcript event.

Actual behavior

The UI can temporarily show two identical final assistant rows for one turn, then remove one during history reconciliation.

Scope / non-goals

  • This is not a Tailscale Funnel transport bug; the remote connection is established and replies are delivered.
  • This is not the older Control UI duplicate-rendering issue where stream state and persisted messages were both rendered by the web UI.
  • This report is scoped to shared OpenClawChatUI reconciliation between live chat.final and canonical session.message events.
  • The fix should not globally dedupe assistant messages by text, because users can legitimately receive the same short answer such as OK on separate turns.

Proposed fix

Treat assistant rows inserted from live chat.final as provisional for the current turn. If the canonical session.message for the same assistant content arrives, adopt/replace the provisional row instead of appending a second visible row. If the canonical session.message arrives first, skip the later matching live final row for that same turn. Keep the matching scoped to messages after the latest user turn so a stale provisional answer from an earlier turn cannot consume a later identical reply.

Metadata

Metadata

Assignees

Labels

P2Normal backlog priority with limited blast radius.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions