Skip to content

BUG: Control UI (webchat) double-records assistant messages in session JSONL #39469

@kAIborg24

Description

@kAIborg24

Bug type

Regression (worked before, now fails)

Summary

The Control UI (webchat provider) records two assistant messages per LLM turn in the session JSONL file, causing duplicate responses in the conversation context and visible double-messages in the UI.

Steps to reproduce

  1. Start the OpenClaw gateway with the Control UI enabled (default)
  2. Open the dashboard at http://127.0.0.1:<port>/#token=<token>
  3. Send any message via the Control UI chat input
  4. Observe the assistant response — it appears twice in the chat

Minimal repro: Any message sent via the Control UI triggers the issue. No special configuration required.

Expected behavior

Each LLM turn should produce exactly one assistant message entry in the session JSONL file, and the response should appear once in the Control UI.

Actual behavior

Each LLM turn produces two assistant message entries in the session JSONL file, approximately 400–900ms apart. The two entries:

  • Have different id and timestamp fields
  • Have separate usage and stopReason metadata
  • Contain nearly identical content (differ by ~2 characters of leading whitespace — first has \n\n prefix, second does not)

Because both entries are written to the session file, subsequent LLM turns see the duplicated assistant message in their context window. This causes the model to sometimes mirror the duplication pattern, producing visibly repeated text in responses.

This does NOT occur with the Telegram channel. In the same session, messages originating from Telegram produce exactly one assistant entry. The issue is isolated to the webchat/Control UI provider.

OpenClaw version

2026.3.2 (85377a2)

Operating system

Ubuntu 24.04.4 LTS (linux-surface 6.18.7-surface-1, x64)

Install method

npm global (~/.npm-global/lib/node_modules/openclaw)

Logs, screenshots, and evidence

### Quantified evidence from a single session

Analyzed session JSONL (`<session-id>.jsonl`):

| Message source | Consecutive assistant message pairs (duplicates) |
|---|---|
| Control UI (webchat) | **80** |
| Telegram | **3** (not true duplicates — these were multi-part messages split across tool calls) |

### Example duplicate pair from JSONL


[2026-03-08T00:56:25.107Z] assistant (len=69): "\n\nPerfect — right there in Starred, one click away. You're all set. 👍"
[2026-03-08T00:56:26.003Z] assistant (len=67): "Perfect — right there in Starred, one click away. You're all set. 👍"


Note: ~900ms apart, content differs only by leading `\n\n`.

Impact and severity

Severity: Medium

  • Context pollution: Duplicate assistant messages inflate the context window, reducing effective context capacity and increasing token costs
  • Model behavior degradation: The model sees its own duplicated responses in context and sometimes mirrors the pattern, producing visibly repeated text — degrading response quality
  • User confusion: Users see double messages in the Control UI chat, creating the impression of a buggy or broken system
  • Session integrity: Session JSONL files contain spurious entries that pollute session history and any downstream processing (memory search indexing, transcript exports, etc.)

Frequency: 100% — every single Control UI message triggers this. Not intermittent.

Context exhaustion: Duplicate recording doubles token consumption, causing sessions to hit the 200K context limit at 2× the normal rate. In testing, a session that would have been ~87K tokens on Telegram reached 174K on the Control UI due to this bug.

Cross-channel spam: Because dmScope: "main" shares a single session across all channels, duplicate messages recorded by the Control UI are also delivered to Telegram. During testing, single responses were delivered 4-6× to Telegram when the conversation originated from the Control UI. Switching to Telegram-native messages immediately produced clean single responses, confirming the duplication originates in the webchat provider's recording path, not the delivery pipeline.

Compounding feedback loop: The duplication escalates over the course of a session. The base bug always records 2× assistant messages per turn. However, as duplicates accumulate in the context window, the model begins pattern-matching against its own duplicated history and mirrors the repetition within its responses. This creates a feedback loop:

Session stage Observed copies per response Cause
Early turns Base bug (webchat double-recording)
Mid session 3–4× Model begins mirroring duplication pattern from context
Late session 6–8× Feedback loop fully compounding — context saturated with duplicates

This explains the variable duplication count observed across different messages. The escalation resets when the session is reset or when the user switches to a non-affected channel (e.g., Telegram), which breaks the feedback loop by producing clean single entries.

Not a config issue: Full config audit confirmed the duplication is not caused by dmScope, sendPolicy, or any other user-configurable setting. The webchat provider is built-in and has no user-facing configuration to control this behavior. The dmScope: "main" setting amplifies the blast radius (cross-channel delivery of duplicates) but is the correct configuration for a single-user deployment and should not be changed as a workaround.

Workaround: Use Telegram (or presumably any other channel) instead of the Control UI for chat. The Control UI is still usable for monitoring — only the chat input is affected.

Additional information

  • The issue persists across gateway restarts and session resets
  • Streaming is set to "off" — unclear if streaming modes (partial, block) are also affected
  • The duplicate does not appear to be a client-side rendering issue — the two entries exist in the server-side session JSONL file with distinct IDs and timestamps
  • The webchat provider source is minified in the dist build, so the exact code path could not be traced from the installed package

Root cause (source-confirmed)

The duplicates are delivery-mirror entries — an intentional OpenClaw feature that records cross-channel message delivery in the session transcript. The bug is not that they're written, but that they're included in the LLM context window as regular assistant messages, creating the appearance of duplicated responses.

How it works (traced through source)

  1. User sends message via Control UI (webchat provider)
  2. LLM processes and responds → assistant message written to session JSONL by the session manager (provider: anthropic, model: claude-opus-4-6)
  3. Response delivered back to Control UI via WebSocket dispatcher — no mirror written (same channel)
  4. Response ALSO delivered to Telegram (cross-channel, because dmScope: "main" shares the session) → deliverOutboundPayloads() in deliver-*.js calls appendAssistantMessageToSessionTranscript() which writes a second assistant message with model: "delivery-mirror", provider: "openclaw", api: "openai-responses"
  5. Next LLM turn: the session manager loads ALL assistant messages from the JSONL — including delivery-mirror entries — into the context window. The LLM sees two near-identical assistant messages and begins mirroring the pattern.

Evidence from JSONL analysis

Message source Anthropic (real) responses delivery-mirror entries Mirror ratio
Control UI (44 messages) 101 62 0.61 per response
Telegram (29 messages) 65 2 0.03 per response
System/cron (9 messages) 44 0 0.00 per response

This confirms:

  • Control UI → high mirror count because responses are cross-delivered to Telegram
  • Telegram → near-zero mirrors because webchat is not a routable channel (isRoutableChannel() returns false for webchat/gateway)
  • Cron → zero mirrors because internal channel is not routable

Source code references (OpenClaw 2026.3.2, build 85377a2)

  • sessions-DNn6Jbbx.js: appendAssistantMessageToSessionTranscript() — writes delivery-mirror entries
  • deliver-FEreRY0J.js: deliverOutboundPayloadsCore() — triggers mirror write when params.mirror && results.length > 0
  • pi-embedded-CtM2Mrrj.js: isRoutableChannel() — webchat returns false, so webchat-originated replies go through dispatcher.sendFinalReply() (no mirror), but cross-channel

Environment

  • Node.js: v22.22.1
  • Gateway auth: token mode
  • Streaming: off (channels.telegram.streaming: "off")
  • Model: anthropic/claude-opus-4-6

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingregressionBehavior that previously worked and now fails

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions