April 23, 2026 • 版本: 2026.3.2

[Webchat因delivery-mirror转录条目重复显示助手回复] - Webchat Duplicate Assistant Replies via delivery-mirror Transcript Entry

Control UI Webchat在delivery-mirror创建额外转录条目时,每轮显示两条助手消息,该额外条目为零token使用量,出现在主模型响应之后。

🔍 症状

主要表现

Control UI Webchat 界面为单个用户输入显示两条连续的助手消息,而不是一条。检查会话的 session.jsonl 文件会发现以下模式:

{"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 诊断输出

直接检查原始对话记录:

# 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")'

预期输出应显示每次用户输入只有一条 assistant 消息。此缺陷会产生两条具有相同或几乎相同 content 字段的条目,其中第二个条目始终具有:

  • provider: "openclaw"
  • model: "delivery-mirror"
  • usage.totalTokens: 0

UI 级别观察

用户报告看到:

  • "思考中..."或"推理"区块出现两次
  • 助手回复在聊天界面中视觉上闪烁或短暂重复
  • 对话历史滚动长度增加,但没有相应的用户输入

频率模式

此缺陷在活动会话期间间歇性出现,但在 OAuth 引导事件后变得持续存在,特别是在会话期间使用 openai-codex 提供程序时,大约在 2026-03-04 00:20-01:15 JST 期间。

🧠 根因分析

架构背景

delivery-mirror 是一个内部 OpenClaw 机制,旨在处理多轮推理链和后续生成。当模型产生扩展推理(例如 o1-previewclaude-sonnet-4 思考区块)时,系统可能会生成需要作为单独消息传递的派生回复。

故障序列

重复消息缺陷由于对话记录追加逻辑中的竞态条件或错误排序而发生:

  1. 主要响应生成:LLM(例如 gpt-5.3-codex)产生带有推理和内容的完整响应。
  2. 对话记录追加(正确):主要响应以 role: assistant 追加到 transcript.jsonl
  3. Delivery-Mirror 生成:delivery-mirror 系统处理相同的推理链以生成"干净"的后续消息。
  4. 对话记录追加(错误):delivery-mirror 响应以 provider: openclawmodel: delivery-mirror 追加到对话记录,但没有检查此轮是否已存在前置助手消息。

代码路径分析

根本原因位于以下交互中:

packages/delivery-mirror/src/handler.ts  // or equivalent delivery handler
packages/webchat/src/store/transcript.ts  // transcript state management

具体来说,transcript.appendMessage() 函数不强制每轮消息唯一性,导致以下条件逻辑失败:

// 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);
}

缺少去重检查意味着当主模型和 delivery-mirror 在同一渲染周期内都产生响应时,两者都会被持久化。

促成因素

  • OAuth 会话状态:引导后会话可能会以修改后的提供程序配置初始化,影响消息路由。
  • 提供程序特定的推理处理:具有本机扩展思考(推理区块)的模型更频繁地触发 delivery-mirror。
  • 对话记录流式传输:流式响应期间的实时对话记录更新创建了无法发生重复检测的时间窗口。

历史背景

此缺陷与 Issue #5964 相关,该问题在不同上下文中解决了类似的重复消息行为。delivery-mirror/后续队列重复问题表明,该修复并未全面应用于所有生成 delivery-mirror 消息的代码路径。

🛠️ 逐步修复

修复 1:禁用 Webchat 的 Delivery-Mirror(临时解决方案)

如果需要立即缓解且无需代码更改:

# Create or edit the OpenClaw config file
nano ~/.openclaw/config.yaml

添加或修改以下内容:

delivery:
  mirror:
    enabled: false
  webchat:
    deduplicate: true

重启 Gateway 服务:

# 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/gateway

修复 2:清除损坏的对话记录(会话级解决方案)

对于存在重复条目的现有会话:

# 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.jsonl

修复 3:应用源代码补丁(永久解决方案)

修补 transcript.ts 文件以强制去重:

# 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);
}

修复 4:使用干净状态重启

# 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

🧪 验证

步骤 1:修复后验证对话记录干净

执行测试对话并验证对话记录:

# 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:0

步骤 2:验证 UI 渲染

# 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

步骤 3:验证会话 JSONL 结构

# 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

步骤 4:验证推理模型无回归

# 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

⚠️ 常见陷阱

边缘情况 1:混合提供程序会话

在同一会话中切换提供程序之间时(例如 openai-codexanthropic),去重逻辑必须与最近的助手消息进行比较,无论提供程序如何:

// 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') { ... }

边缘情况 2:流式响应

在活动流式传输期间,getLastAssistantMessage() 调用可能返回不完整的数据。实现锁或队列机制:

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);
  }
}

边缘情况 3:macOS LaunchAgent 持久化

如果 LaunchAgent 不重新加载配置,则配置更改可能不会生效。始终验证:

# Check if config is actually loaded
cat /Library/LaunchAgents/com.openclaw.gateway.plist

# Or for user-level agents:
~/Library/LaunchAgents/com.openclaw.gateway.plist

边缘情况 4:Docker 容器环境

在 Docker 中运行 OpenClaw 时,对话记录路径不同:

# 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 ...

边缘情况 5:NPM 全局与本地安装

~/.openclaw/config.yaml 位置适用于全局安装。对于本地开发:

# Local installs may require:
./openclaw.config.yaml
# or
./config/openclaw.yaml

边缘情况 6:日志文件权限错误

如果对话记录修改因权限错误失败:

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/

🔗 相关错误

Issue #5964:推理后 Delivery-Mirror 重复

描述:影响 CLI 会话中相同缺陷的早期表现,早于 Control UI Webchat 完全部署。

解决方案:在 v2026.2.1 中部分解决,但在 v2026.3.x 中引入回归。

参考packages/delivery-mirror/CHANGELOG.md — “修复对话记录追加中的重复消息检测”


Issue #6021:对话记录中的零令牌消息

描述usage.totalTokens: 0 条目污染对话记录,与重复行为无关。

根本原因:Delivery-mirror 消息未正确报告内部路由消息的令牌使用情况。

症状:对话记录文件增长超出预期,但没有相应的内容增加。


Issue #6102:Webchat 消息顺序不一致

描述:当多个推理链同时完成时,助手消息偶尔会以乱序渲染。

关联:与 delivery-mirror 重复共享相同的 transcript.ts 根本原因。


错误代码:DLV_001(传递服务错误)

描述:delivery-mirror 传递消息失败时的内部错误,偶尔导致重试循环生成重复。

日志模式[delivery] ERROR DLV_001: Failed to queue message for delivery


错误代码:TRN_002(对话记录写入错误)

描述:对 transcript.jsonl 的并发写入操作导致部分写入或损坏。

日志模式[transcript] WARN TRN_002: Concurrent append detected, possible data loss


相关 Pull Request:PR #6145

标题:“修复:Webchat 对话记录中的 Delivery-Mirror 条目去重”

状态:已合并到 main,待发布于 v2026.3.3

关键更改:添加了 transcript.deduplicateDeliveryMirror 配置选项并改进了消息匹配逻辑。

依据与来源

本故障排除指南由 FixClaw 智能管线从社区讨论中自动合成。