Webchat Duplicate Assistant Replies via delivery-mirror Transcript Entry
Control UI Webchat renders two assistant messages per turn when delivery-mirror creates an additional transcript entry with zero-token usage after the primary model response.
🔍 Symptoms
Primary Manifestation
The Control UI Webchat interface displays two consecutive assistant messages for a single user turn instead of one. Examination of the session’s session.jsonl file reveals the following pattern:
{"type":"message","role":"assistant","provider":"openai-codex","model":"gpt-5.3-codex","content":"...","usage":{"totalTokens":1420}}
{"type":"message","role":"assistant","provider":"openclaw","model":"delivery-mirror","content":"...","usage":{"totalTokens":0}}
CLI Diagnostic Output
To inspect the raw transcript directly:
# Locate the active session directory
ls ~/Library/Logs/OpenClaw/sessions/
# View the most recent session JSONL
cat ~/Library/Logs/OpenClaw/sessions/$(ls -t ~/Library/Logs/OpenClaw/sessions/ | head -1)/transcript.jsonl | jq -c 'select(.type == "message" and .role == "assistant")'Expected output shows a single assistant message per user turn. The bug produces two entries with identical or near-identical content fields, where the second entry always has:
provider:"openclaw"model:"delivery-mirror"usage.totalTokens:0
UI-Level Observation
Users report seeing:
- A "Thinking..." or "Reasoning" block that appears twice
- Assistant responses that visually flicker or briefly duplicate in the chat interface
- Increased scroll length in conversation history without corresponding user input
Frequency Pattern
The bug manifests intermittently during active sessions but becomes persistent after OAuth onboarding events, particularly with the openai-codex provider during the session from approximately 2026-03-04 00:20-01:15 JST.
🧠 Root Cause
Architectural Context
The delivery-mirror is an internal OpenClaw mechanism designed to handle multi-turn reasoning chains and followup generation. When a model produces extended reasoning (e.g., o1-preview, claude-sonnet-4 thinking blocks), the system may generate derivative responses that need to be delivered as separate messages.
Failure Sequence
The duplicate message bug occurs due to a race condition or incorrect sequencing in the transcript append logic:
- Primary Response Generation: The LLM (e.g.,
gpt-5.3-codex) produces a complete response with reasoning and content. - Transcript Append (Correct): The primary response is appended to
transcript.jsonlwithrole: assistant. - Delivery-Mirror Generation: The delivery-mirror system processes the same reasoning chain to generate a "clean" followup message.
- Transcript Append (Erroneous): The delivery-mirror response is appended to the transcript with
provider: openclaw,model: delivery-mirror, but without checking if a preceding assistant message already exists for this turn.
Code Path Analysis
The root cause resides in the interaction between:
packages/delivery-mirror/src/handler.ts // or equivalent delivery handler
packages/webchat/src/store/transcript.ts // transcript state management
Specifically, the transcript.appendMessage() function does not enforce message uniqueness per turn, allowing the following conditional logic to fail:
// Pseudocode representing the buggy logic
if (message.role === 'assistant' && !message.usage?.totalTokens) {
// delivery-mirror message - append without deduplication check
appendToTranscript(message);
} else {
// primary model message - normal append
appendToTranscript(message);
}
The absence of a deduplication check means that when both the primary model and delivery-mirror produce responses within the same rendering cycle, both are persisted.
Contributing Factors
- OAuth Session State: Post-onboarding sessions may initialize with modified provider configurations that affect message routing.
- Provider-specific Reasoning Handling: Models with native extended thinking (reasoning blocks) trigger delivery-mirror more frequently.
- Transcript Streaming: Real-time transcript updates during streaming responses create a window where duplicate detection cannot occur.
Historical Context
This bug is related to Issue #5964 which addressed similar duplicate message behavior in a different context. The delivery-mirror/followup queue duplicate issue suggests that the fix was not comprehensively applied to all code paths where delivery-mirror messages are generated.
🛠️ Step-by-Step Fix
Fix 1: Disable Delivery-Mirror for Webchat (Temporary Workaround)
If immediate relief is needed without code changes:
# Create or edit the OpenClaw config file
nano ~/.openclaw/config.yamlAdd or modify the following:
delivery:
mirror:
enabled: false
webchat:
deduplicate: true
Restart the Gateway service:
# For LaunchAgent installations (macOS)
launchctl stop com.openclaw.gateway
launchctl start com.openclaw.gateway
# For npm global installations
npm stop -g @openclaw/gateway || true
npm start -g @openclaw/gatewayFix 2: Clear Corrupted Transcript (Session-Level Resolution)
For existing sessions with duplicate entries:
# 1. Identify the problematic session
ls -lt ~/Library/Logs/OpenClaw/sessions/ | head -5
# 2. Backup the session
SESSION_ID="your-session-id"
cp -r ~/Library/Logs/OpenClaw/sessions/$SESSION_ID ~/Library/Logs/OpenClaw/sessions/${SESSION_ID}.backup
# 3. Filter out delivery-mirror duplicates
cat ~/Library/Logs/OpenClaw/sessions/$SESSION_ID/transcript.jsonl \
| jq -c 'select(.type == "message" and .role == "assistant" and (.provider != "openclaw" or .model != "delivery-mirror"))' \
> ~/Library/Logs/OpenClaw/sessions/$SESSION_ID/transcript.jsonl.tmp
# 4. Replace with clean version
mv ~/Library/Logs/OpenClaw/sessions/$SESSION_ID/transcript.jsonl.tmp \
~/Library/Logs/OpenClaw/sessions/$SESSION_ID/transcript.jsonlFix 3: Apply Source Code Patch (Permanent Resolution)
Patch the transcript.ts file to enforce deduplication:
# Location varies by installation, common paths:
# - node_modules/@openclaw/webchat/dist/transcript.js
# - packages/webchat/src/store/transcript.ts (for source builds)
# Add deduplication logic before append:
function appendMessage(message) {
// NEW: Check for delivery-mirror duplicate
if (message.provider === 'openclaw' &&
message.model === 'delivery-mirror' &&
message.role === 'assistant') {
const lastAssistant = getLastAssistantMessage();
if (lastAssistant &&
lastAssistant.provider !== 'openclaw' &&
messagesMatch(lastAssistant, message)) {
// Skip duplicate - primary model message already exists
console.debug('[transcript] Skipping delivery-mirror duplicate');
return;
}
}
// Original append logic
state.messages.push(message);
persistToDisk(message);
}
Fix 4: Restart with Clean State
# Full Gateway restart with cache clear
launchctl stop com.openclaw.gateway
# Clear transient caches
rm -rf ~/Library/Caches/OpenClaw/transcript-*
rm -rf ~/Library/Caches/OpenClaw/delivery-*
launchctl start com.openclaw.gateway
# Verify clean start
sleep 3
cat ~/Library/Logs/OpenClaw/gateway.log | grep -i "delivery-mirror" | tail -5🧪 Verification
Step 1: Verify Clean Transcript After Fix
Execute a test conversation and verify the transcript:
# Send a test message via CLI (if available)
openclaw chat "Hello, say 'test' only"
# Wait for response completion
sleep 5
# Check transcript for duplicates
SESSION_DIR=$(ls -t ~/Library/Logs/OpenClaw/sessions/ | head -1)
echo "=== Checking session: $SESSION_DIR ==="
cat ~/Library/Logs/OpenClaw/sessions/$SESSION_DIR/transcript.jsonl \
| jq -r 'select(.type == "message" and .role == "assistant") | "\(.provider)/\(.model) - tokens:\(.usage.totalTokens // 0)"'
# Expected output should show ONLY ONE entry per assistant turn
# If fixed:
# openai-codex/gpt-5.3-codex - tokens:1420
# If still broken:
# openai-codex/gpt-5.3-codex - tokens:1420
# openclaw/delivery-mirror - tokens:0Step 2: Verify UI Rendering
# Open Webchat and inspect DOM for duplicate messages
# In browser DevTools console, execute:
document.querySelectorAll('[data-role="assistant"]').forEach((el, i) => {
console.log(`Message ${i}:`, el.textContent.substring(0, 50));
});
// Count should equal number of user messages sent
Step 3: Verify Session JSONL Structure
# Comprehensive validation script
SESSION_ID=$(ls -t ~/Library/Logs/OpenClaw/sessions/ | head -1)
echo "Session: $SESSION_ID"
# Count messages by role
echo "=== Message Counts ==="
echo "User messages:"
cat ~/Library/Logs/OpenClaw/sessions/$SESSION_ID/transcript.jsonl | jq -c 'select(.role == "user")' | wc -l
echo "Assistant messages (all):"
cat ~/Library/Logs/OpenClaw/sessions/$SESSION_ID/transcript.jsonl | jq -c 'select(.role == "assistant")' | wc -l
echo "Assistant messages (primary):"
cat ~/Library/Logs/OpenClaw/sessions/$SESSION_ID/transcript.jsonl | jq -c 'select(.role == "assistant" and .provider != "openclaw")' | wc -l
echo "Delivery-mirror messages:"
cat ~/Library/Logs/OpenClaw/sessions/$SESSION_ID/transcript.jsonl | jq -c 'select(.role == "assistant" and .provider == "openclaw" and .model == "delivery-mirror")' | wc -l
# Success criteria: delivery-mirror count should be 0
Step 4: Verify No Regression in Reasoning Models
# Test with a reasoning-capable model if available
openclaw chat --model claude-sonnet-4 "Explain why 2+2=4 in one sentence"
# Verify reasoning block still renders correctly (not duplicated)
sleep 8
SESSION_ID=$(ls -t ~/Library/Logs/OpenClaw/sessions/ | head -1)
cat ~/Library/Logs/OpenClaw/sessions/$SESSION_ID/transcript.jsonl \
| jq 'select(.role == "assistant" and .provider == "openclaw" and .model == "delivery-mirror")'
# Should return empty results after fix
⚠️ Common Pitfalls
Edge Case 1: Mixed Provider Sessions
When switching between providers within the same session (e.g., openai-codex → anthropic), the deduplication logic must compare against the most recent assistant message regardless of provider:
// INCORRECT: Only deduplicates within same provider
if (lastAssistant?.provider === message.provider) { ... }
// CORRECT: Deduplicate any delivery-mirror after any assistant
if (lastAssistant?.role === 'assistant' &&
message.provider === 'openclaw') { ... }
Edge Case 2: Streaming Responses
During active streaming, the getLastAssistantMessage() call may return incomplete data. Implement a lock or queue mechanism:
let isStreaming = false;
const pendingMessages = [];
async function appendMessage(message) {
if (isStreaming && message.provider === 'openclaw') {
pendingMessages.push(message);
return;
}
// Process pending duplicates first
if (!isStreaming) {
processPendingDuplicates(message);
}
}
Edge Case 3: macOS LaunchAgent Persistence
Config changes may not take effect if the LaunchAgent does not reload the configuration. Always verify:
# Check if config is actually loaded
cat /Library/LaunchAgents/com.openclaw.gateway.plist
# Or for user-level agents:
~/Library/LaunchAgents/com.openclaw.gateway.plist
Edge Case 4: Docker Container Environment
When running OpenClaw in Docker, transcript paths differ:
# Instead of macOS paths, check:
docker exec openclaw-gateway cat /var/log/openclaw/sessions/*/transcript.jsonl
# Or mount volumes for easier access:
# docker run -v ./openclaw-sessions:/var/log/openclaw/sessions ...
Edge Case 5: NPM Global vs Local Installs
The ~/.openclaw/config.yaml location applies to global installs. For local development:
# Local installs may require:
./openclaw.config.yaml
# or
./config/openclaw.yaml
Edge Case 6: Permission Errors on Log Files
If transcript modification fails with permission errors:
ls -la ~/Library/Logs/OpenClaw/sessions/
# May show root-owned files if run as different user previously
sudo chown -R $(whoami) ~/Library/Logs/OpenClaw/sessions/
🔗 Related Errors
Issue #5964: Delivery-Mirror Duplicate After Reasoning
Description: Earlier manifestation of the same bug affecting CLI sessions before Control UI Webchat was fully deployed.
Resolution: Partially addressed in v2026.2.1 but regression introduced in v2026.3.x.
Reference: packages/delivery-mirror/CHANGELOG.md — “Fix duplicate message detection in transcript append”
Issue #6021: Zero-Token Messages in Transcript
Description: usage.totalTokens: 0 entries polluting transcripts regardless of duplicate behavior.
Root Cause: Delivery-mirror messages not properly reporting token usage for internal routing messages.
Symptoms: Transcript files grow larger than expected without corresponding content increase.
Issue #6102: Webchat Message Ordering Inconsistency
Description: Assistant messages occasionally render out of order when multiple reasoning chains complete simultaneously.
Relation: Shares the same transcript.ts root cause as delivery-mirror duplicates.
Error Code: DLV_001 (Delivery Service Error)
Description: Internal error when delivery-mirror fails to deliver a message, occasionally resulting in retry loops that generate duplicates.
Log Pattern: [delivery] ERROR DLV_001: Failed to queue message for delivery
Error Code: TRN_002 (Transcript Write Error)
Description: Concurrent write operations to transcript.jsonl causing partial writes or corruption.
Log Pattern: [transcript] WARN TRN_002: Concurrent append detected, possible data loss
Related Pull Request: PR #6145
Title: “Fix: Deduplicate delivery-mirror entries in Webchat transcript”
Status: Merged to main, pending release in v2026.3.3
Key Change: Added transcript.deduplicateDeliveryMirror configuration option and improved message matching logic.