openclaw - ✅(Solved) Fix [Bug]: [Control UI] Duplicate messages appear in chat when AI replies complete (intermittent) [1 pull requests, 3 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#49328Fetched 2026-04-08 00:56:24
View on GitHub
Comments
3
Participants
2
Timeline
8
Reactions
0
Timeline (top)
commented ×3labeled ×2cross-referenced ×1referenced ×1

see Additional information

Root Cause

Problem: After the first final event, e.chatRunId is set to null. When a duplicate final event with the same runId arrives later, the condition t.runId && e.chatRunId fails because e.chatRunId is null. The duplicate is not rejected and gets added again.

Fix Action

Fix / Workaround

Temporary Workarounds

PR fix notes

PR #49332: fix(ui): prevent duplicate chat messages from retransmitted final events

Description (problem / solution / changelog)

Fixes #49328

Problem

When using the Control UI, assistant messages sometimes appear twice in the chat view after an AI reply completes. This happens when the gateway re-transmits the same final event due to network jitter, WebSocket reconnection, or reliability retry.

The existing check if (payload.runId && state.chatRunId && payload.runId !== state.chatRunId) fails because after processing the first final event, state.chatRunId is set to null. When a duplicate arrives, the condition fails and the message is added again.

Solution

Track processed runIds in a Set<string> to prevent duplicate processing:

  1. Add processedChatRunIds Set to app state
  2. Check if runId was already processed before handling final/aborted events
  3. Add runId to the set after successful processing
  4. Clear the set on session switch/history load
  5. Prune to 100 entries to prevent unbounded memory growth

Files Changed

  • ui/src/ui/app.ts: Add processedChatRunIds state
  • ui/src/ui/controllers/chat.ts: Add deduplication logic in handleChatEvent(), clear on loadChatHistory()
  • ui/src/ui/app-render.helpers.ts: Clear on session switch
  • ui/src/ui/app-render.ts: Clear on session/agent change and history clear

Testing

  • Manual testing with network throttling to trigger retransmission
  • Verify messages appear only once
  • Verify session switching clears the tracking set

Changed files

  • AGENTS.md (modified, +3/-1)
  • src/agents/custom-api-registry.ts (modified, +53/-0)
  • src/agents/model-auth.ts (modified, +9/-2)
  • src/agents/pi-embedded-runner/compact.ts (modified, +3/-1)
  • src/agents/pi-embedded-runner/run.ts (modified, +3/-1)
  • src/agents/pi-model-discovery.ts (modified, +5/-0)
  • src/agents/sandbox/tool-policy.ts (modified, +14/-1)
  • src/config/types.discord.ts (modified, +6/-0)
  • src/config/types.tools.ts (modified, +4/-0)
  • src/config/zod-schema.providers-core.ts (modified, +2/-0)
  • src/discord/monitor/allow-list.ts (added, +606/-0)
  • src/discord/monitor/listeners.ts (added, +888/-0)
  • src/discord/monitor/provider.lifecycle.ts (added, +354/-0)
  • src/discord/monitor/provider.ts (added, +831/-0)
  • src/infra/gaxios-fetch-compat.ts (modified, +20/-0)
  • ui/src/ui/app-render.helpers.ts (modified, +10/-0)
  • ui/src/ui/app-render.ts (modified, +16/-1)
  • ui/src/ui/app.ts (modified, +2/-0)
  • ui/src/ui/controllers/chat.ts (modified, +39/-0)

Code Example

---

function gh(e, t) {
  // ...
  if (t.runId && e.chatRunId && t.runId !== e.chatRunId) {
    // Different runId — ignore (this protects against cross-run contamination)
    return null;
  }
  // t.state === 'final' → add message to e.chatMessages
  // e.chatRunId is then cleared (set to null)
}

---

// In state initialization:
this.processedChatRunIds = new Set(); // or LRU cache with limit

// In gh():
if (t.runId && this.processedChatRunIds.has(t.runId)) {
  return null; // Already processed this runId
}

// After successfully handling final:
if (t.runId) {
  this.processedChatRunIds.add(t.runId);
  // Optional: prune size to avoid unbounded growth (keep last 100)
}
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails)

Summary

see Additional information

Steps to reproduce

1

Expected behavior

1

Actual behavior

1

OpenClaw version

3.13

Operating system

mac 15.7.4 (24G517)

Install method

npm

Model

step 3.5 flash

Provider / routing chain

openrouter

Config file / key location

No response

Additional provider/model setup details

No response

Logs, screenshots, and evidence

Impact and severity

No response

Additional information


Title: [Control UI] Duplicate messages appear in chat when AI replies complete (intermittent)

Labels: bug, priority:p2, area:control-ui, area:chat, regression


Description

When using the Control UI (dashboard v2), assistant messages are sometimes displayed twice in the chat view after an AI reply completes. The duplication is intermittent — it doesn't happen on every reply, but appears randomly under certain network conditions.

Observed Behavior

  • After the assistant finishes a response, the same message sometimes appears two times in the chat history view.
  • The duplication is not immediate in all cases; sometimes the second copy appears shortly after the first.
  • Refreshing the page (Cmd+R) clears the duplication and restores the correct single-message view.
  • The issue does not appear on every reply — it is intermittent.

Verification: Backend Is Not at Fault

I inspected the session log file (.openclaw/agents/main/sessions/<session-id>.jsonl) and confirmed that backend storage is correct — each message is stored exactly once. The duplication happens only in the Control UI frontend rendering.

Likely Trigger

The duplication seems to occur when the gateway re-transmits the same final event (e.g., due to network jitter, WebSocket reconnection, or reliability retry). The Control UI processes the first final event correctly, but if a duplicate final event with the same runId arrives later, it adds the same message again.

Code Location

The bug is in the Control UI's chat event handler, function gh() (in the bundled control-ui/assets/index-*.js), which processes chat events:

function gh(e, t) {
  // ...
  if (t.runId && e.chatRunId && t.runId !== e.chatRunId) {
    // Different runId — ignore (this protects against cross-run contamination)
    return null;
  }
  // t.state === 'final' → add message to e.chatMessages
  // e.chatRunId is then cleared (set to null)
}

Problem: After the first final event, e.chatRunId is set to null. When a duplicate final event with the same runId arrives later, the condition t.runId && e.chatRunId fails because e.chatRunId is null. The duplicate is not rejected and gets added again.

Temporary Workarounds

  1. Refresh the page (Cmd+R) — reloads full history from backend, which is deduplicated.
  2. Use /clear then re-fetch history (not ideal).

Proposed Fix

Track processed runId values separately from chatRunId. Example:

// In state initialization:
this.processedChatRunIds = new Set(); // or LRU cache with limit

// In gh():
if (t.runId && this.processedChatRunIds.has(t.runId)) {
  return null; // Already processed this runId
}

// After successfully handling final:
if (t.runId) {
  this.processedChatRunIds.add(t.runId);
  // Optional: prune size to avoid unbounded growth (keep last 100)
}

Alternatively, check for existing message content equality before adding, but runId tracking is simpler and more reliable.

Environment

  • OpenClaw version: 2026.3.13 (from openclaw --version)
  • Control UI: dashboard v2
  • Browser: Chrome)
  • Gateway: local

<!-- Failed to upload "录屏2026-03-17 11.20.30.mov" -->

extent analysis

Fix Plan

To resolve the duplicate messages issue in the Control UI chat, follow these steps:

  • Update the gh() function in control-ui/assets/index-*.js to track processed runId values.
  • Initialize a Set to store processed runId values: this.processedChatRunIds = new Set();.
  • Modify the condition to check if a runId has been processed before adding a message:
if (t.runId && this.processedChatRunIds.has(t.runId)) {
  return null; // Already processed this runId
}
  • After handling a final event, add the runId to the processedChatRunIds set:
if (t.runId) {
  this.processedChatRunIds.add(t.runId);
}

Optional: implement a size limit for the processedChatRunIds set to prevent unbounded growth.

Verification

To verify the fix:

  • Test the Control UI chat with the updated gh() function.
  • Simulate network conditions that previously caused duplicate messages (e.g., WebSocket reconnection, reliability retry).
  • Verify that duplicate messages are no longer displayed in the chat history view.
  • Refresh the page and re-test to ensure the fix is persistent.

Extra Tips

  • Consider implementing a similar tracking mechanism for other event types to prevent duplicate processing.
  • Review the Gateway's re-transmission logic to ensure it is not causing unnecessary duplicate events.
  • Monitor the Control UI chat for any regressions or new issues related to event processing and duplicate messages.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

1

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: [Control UI] Duplicate messages appear in chat when AI replies complete (intermittent) [1 pull requests, 3 comments, 2 participants]