April 23, 2026 • バージョン: 2026.3.2

[delivery-mirror トランスクリプトエントリによる重複アシスタント返信] - Webchat Duplicate Assistant Replies via delivery-mirror Transcript Entry

Control UI Webchatは、delivery-mirrorがプライマリモデルの応答後にゼロトークン使用量の追加トランスクリプトエントリを作成する際、1ターンにつき2つのアシスタントメッセージを表示する問題を制御します。

🔍 症状

主な症状

Control UI Webchatインターフェースで、ユーザーの1回の入力に対してアシスタントメッセージが2つ連続して表示されます。セッションの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")'

期待される出力では、ユーザーの1回の入力につきassistantメッセージが1つだけです。バグがあると、2つのエントリが作成され、2番目のエントリは必ず以下を持ちます:

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

UIレベルの観察

ユーザーから報告される症状:

  • 「考え中...」や「推論」ブロックが2回表示される
  • アシスタントの応答がチャットインターフェース内で視覚的にちらついたり、短時間重複して表示される
  • 会話履歴のスクロール量が増加するが、対応するユーザー入力がない

発生パターン

このバグはアクティブなセッション中に断続的に発生しますが、OAuthオンボーディングイベントの後に永続化します。特に2026-03-04 00:20-01:15 JST頃のセッションで、openai-codexプロバイダを使用している場合に行われます。

🧠 原因

アーキテクチャの背景

delivery-mirrorは、多段推論チェーンとフォローアップ生成を処理するために設計されたOpenClawの内部メカニズムです。モデルが拡張推論(例:o1-previewclaude-sonnet-4の思考ブロック)を生成すると、システムは別々のメッセージとして配信する必要がある派生応答を生成する可能性があります。

障害のシーケンス

トランスクリプト追加ロジックにおける競合状態または順序の誤りにより、重複メッセージバグが発生します:

  1. プライマリ応答の生成:LLM(例:gpt-5.3-codex)が推論とコンテンツを含む完全な応答を生成します。
  2. トランスクリプトへの追加(正常):プライマリ応答がrole: assistanttranscript.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)、重複排除ロジックはプロバイダに関係なく最新のassistantメッセージと比較する必要があります:

// 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重複

説明:Control UI Webchatが完全にデプロイされる前にCLIセッションに影響を与えた同じバグの以前の発生形態。

解決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

タイトル: “Fix: Deduplicate delivery-mirror entries in Webchat transcript”

ステータス: mainにマージ済み、v2026.3.3でのリリース待ち

主な変更: transcript.deduplicateDeliveryMirror設定オプションを追加し、メッセージマッチングロジックを改善しました。

エビデンスとソース

このトラブルシューティングガイドは、FixClaw Intelligence パイプラインによってコミュニティの議論から自動的に合成されました。