[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-preview、claude-sonnet-4の思考ブロック)を生成すると、システムは別々のメッセージとして配信する必要がある派生応答を生成する可能性があります。
障害のシーケンス
トランスクリプト追加ロジックにおける競合状態または順序の誤りにより、重複メッセージバグが発生します:
- プライマリ応答の生成:LLM(例:
gpt-5.3-codex)が推論とコンテンツを含む完全な応答を生成します。 - トランスクリプトへの追加(正常):プライマリ応答が
role: assistantでtranscript.jsonlに追加されます。 - Delivery-Mirror生成:delivery-mirrorシステムが同じ推論チェーンを処理して、「クリーン」なフォローアップメッセージを生成します。
- トランスクリプトへの追加(誤り):delivery-mirrorの応答が
provider: openclaw、model: 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-codex → anthropic)、重複排除ロジックはプロバイダに関係なく最新の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設定オプションを追加し、メッセージマッチングロジックを改善しました。