April 21, 2026 • バージョン: 2026.2.26

[WebChatリフレッシュ時に内部システム/ハートビートエントリがリーク] - WebChat Refresh Leaks Internal System/Heartbeat Entries

WebChatをリフレッシュすると、buildChatItemsの一貫性のない履歴フィルタリングにより、内部メッセージ(システムメッセージ、ハートビート応答、ツールチェーン生成物)が可視会話にリークします。

🔍 症状

視覚的な症状

WebChatページを更新した後、会話タイムラインに以下のアーティファクトが表示されるのをユーザーが確認します:

  • システムメッセージブロック — 内部システムイベントがチャットアイテムとしてレンダリングされる(例:「Refresh filter repro」)
  • ハートビート確認応答テキスト — エンドユーザーに表示されるHEARTBEAT_OKなどの応答ペイロード
  • 内部ツール/ステータスチェーンアイテム — 会話ビューに公開されるツール実行アーティファクトとステータスメッセージ

CLI再現コマンド

テスト用に内部イベントをトリガーするには:

openclaw system event --mode now --text "Refresh filter repro"

期待される動作と実際の動作

状態期待される動作実際の動作(バグ)
更新前クリーンなユーザー/アシスタントの会話を表示クリーンなユーザー/アシスタントの会話を表示
更新後クリーンなユーザー/アシスタントの会話を表示システム/ハートビートアーティファクトが表示される

環境詳細

  • バージョン: OpenClaw 2026.2.26
  • OS: macOS 26.1 (Darwin 25.1.0, arm64)
  • インストール: pnpm global binary (/Users/bell/Library/pnpm/openclaw)
  • フィーチャーフラグ: WebChat有効、ハートビート有効

🧠 原因

アーキテクチャ分析

WebChatの更新リークは、チャット履歴レンダリングパイプラインにおける2フェーズのアーキテクチャの不整合から発生します:

フェーズ1: 履歴の再読み込み (ui/src/ui/controllers/chat.ts)

WebChatが初期化または更新されるとき、チャットコントローラは完全なchat.history配列を読み込みます:

// ui/src/ui/controllers/chat.ts (line ~N)
const history = chat.history; // 内部エントリを含む完全な履歴を読み込む

これには、内部またはユーザー向けとして分類に関係なく、すべてのメッセージタイプが含まれます。

フェーズ2: 不十分なフィルタリング (ui/src/ui/views/chat.ts)

buildChatItems関数が適用するフィルタリングロジックは、範囲が十分に広くありません:

// ui/src/ui/views/chat.ts - buildChatItems関数
function buildChatItems(history) {
  return history.filter(item => {
    // 現在のフィルタリングロジック(不十分):
    if (!item.thinking && item.type === 'toolresult') {
      return false; // thinkingがオフの場合のみtoolresultを抑制
    }
    // 不足: system/heartbeat/internalメッセージクラスのフィルタリングがない
    return true;
  });
}

障害発生の順序

  1. ユーザーが内部イベント(ハートビート、システムコマンド)をトリガーする
  2. 内部メッセージが分類フラグ(例:role: 'system'internal: truemessageClass: 'heartbeat')とともにchat.historyに挿入される
  3. ページの更新が発生する
  4. chat.tsが内部エントリを含む完全な履歴を再読み込みする
  5. buildChatItemsmessageClassまたはinternalフラグをチェックしないため、これらのエントリをフィルタリングできなかった
  6. 内部アーティファクトが видимый WebChatタイムラインにレンダリングされる

メッセージ分類のギャップ

現在の実装には、メッセージ分類への体系的なアプローチが欠けています。内部メッセージには、レンダリング層がフィルタリングに使用できるメタデータフラグが必要です:

// 分類付きの期待されるメッセージ構造
{
  id: "msg_xxx",
  role: "system",
  content: "HEARTBEAT_OK",
  messageClass: "heartbeat",      // フィルタチェックから不足
  internal: true,                  // フィルタチェックから不足
  visibleInChat: false             // フィルタチェックから不足
}

🛠️ 解決手順

解決策の概要

メッセージの可視性を制御するDev Mode切り替えを実装します:

  • Dev Mode OFF(デフォルト): ユーザー向けの会話アイテムのみを表示
  • Dev Mode ON: デバッグ目的で内部メッセージをすべて表示

フェーズ1: メッセージ分類フラグの追加

ファイル: packages/core/src/types/chat.ts

// メッセージインターフェースに追加
export interface ChatMessage {
  id: string;
  role: 'user' | 'assistant' | 'system' | 'tool';
  content: string;
  // ... 既存のフィールド
  
  // 新規: UIフィルタリング用の分類
  internal?: boolean;
  messageClass?: 'user' | 'assistant' | 'system' | 'heartbeat' | 'tool' | 'status';
  visibleInChat?: boolean; // 明示的なオーバーライド
}

フェーズ2: buildChatItemsフィルタリングの更新

ファイル: ui/src/ui/views/chat.ts

// 変更前(不完全なフィルタリング)
function buildChatItems(history, devMode = false) {
  return history.filter(item => {
    if (!item.thinking && item.type === 'toolresult') {
      return false;
    }
    return true;
  });
}

// 変更後(包括的なフィルタリング)
function buildChatItems(history, devMode = false) {
  return history.filter(item => {
    // 明示的な可視性オーバーライド
    if (item.visibleInChat === false && !devMode) {
      return false;
    }
    
    // 内部メッセージは本番モードでは非表示
    if (item.internal && !devMode) {
      return false;
    }
    
    // メッセージクラスベースのフィルタリング
    const hiddenClasses = ['heartbeat', 'system', 'status'];
    if (hiddenClasses.includes(item.messageClass) && !devMode) {
      return false;
    }
    
    // レガシーtoolresult処理(thinkingがオフの場合)
    if (!item.thinking && item.type === 'toolresult') {
      return false;
    }
    
    return true;
  });
}

フェーズ3: Dev Mode切り替えの実装

ファイル: ui/src/ui/components/DevModeToggle.tsx

import { useState, useEffect } from 'react';
import { loadSetting, saveSetting } from '../utils/settings';

const DEV_MODE_KEY = 'openclaw_dev_mode';

export function DevModeToggle() {
  const [devMode, setDevMode] = useState(() => 
    loadSetting(DEV_MODE_KEY, false)
  );

  useEffect(() => {
    saveSetting(DEV_MODE_KEY, devMode);
  }, [devMode]);

  return (
    <div className="dev-mode-toggle">
      <label>
        <input
          type="checkbox"
          checked={devMode}
          onChange={(e) => setDevMode(e.target.checked)}
        />
        Dev Mode
      </label>
      <span className="dev-mode-indicator">
        {devMode ? '🔧 Debugging' : '🚀 Production'}
      </span>
    </div>
  );
}

ファイル: ui/src/ui/utils/settings.ts

const SETTINGS_KEY = 'openclaw_ui_settings';

export function loadSetting(key: string, defaultValue: any): any {
  try {
    const settings = JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}');
    return settings[key] ?? defaultValue;
  } catch {
    return defaultValue;
  }
}

export function saveSetting(key: string, value: any): void {
  try {
    const settings = JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}');
    settings[key] = value;
    localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
  } catch (e) {
    console.error('Failed to persist setting:', key, e);
  }
}

フェーズ4: Dev Modeをチャットコントローラに接続

ファイル: ui/src/ui/controllers/chat.ts

// 変更前
const history = chat.history;

// 変更後
import { loadSetting } from '../utils/settings';

const DEV_MODE = loadSetting('openclaw_dev_mode', false);
const history = buildChatItems(chat.history, DEV_MODE);

フェーズ5: 挿入時に内部メッセージをマーク

ファイル: packages/heartbeat/src/handler.ts(または関連するハートビートモジュール)

// ハートビート応答を挿入する場合
chat.history.push({
  id: generateId(),
  role: 'system',
  content: 'HEARTBEAT_OK',
  messageClass: 'heartbeat',
  internal: true,
  visibleInChat: false, // 明示的な非表示
  timestamp: Date.now()
});

🧪 検証

テストケース1: 更新後のクリーンなビューの確認(Dev Mode OFF)

# 1. ハートビート有効な状態でWebChatを起動
openclaw start --webchat --heartbeat

# 2. 内部システムイベントをトリガー
openclaw system event --mode now --text "Refresh filter test"

# 3. WebChatページを強制更新(Ctrl+Shift+R / Cmd+Shift+R)

# 4. 会話に内部アーティファクトがないことを確認
# 期待される結果: ユーザーメッセージとアシスタント応答のみ表示される
# 確認事項: HEARTBEAT_OK、システムメッセージブロック、ツールチェーンが存在しないこと

期待される出力: 会話にはユーザー/アシスタントの連携のみ含まれる。

テストケース2: Dev Modeが内部メッセージを表示することの確認

# 1. WebChat UIでDev Mode切り替えを有効化

# 2. ページを更新

# 3. 内部アーティファクトが теперь 表示されることを確認
# 期待される結果: システムメッセージ、ハートビートACK、ツールチェーンがデバッグインジケーター付きで表示される

期待される出力: デバッグインジケーター付きの完全な内部メッセージチェーンが表示される。

テストケース3: Dev Modeがセッション間で維持されることの確認

# 1. Dev Modeを有効化
# 2. WebChatタブを閉じる
# 3. WebChatを再度開く
# 4. Dev Modeの状態が保持されていることを確認

# localStorageを確認
window.localStorage.getItem('openclaw_ui_settings')
# 期待される結果: {"openclaw_dev_mode":true}

テストケース4: メッセージ分類のCLI検証

# メッセージが正しい分類を持っていることを確認
openclaw chat history --format json | jq '.[] | select(.messageClass == "heartbeat")'

# 期待される結果: ハートビートエントリを返す(分類が設定されていることを確認)

テストケース5: ツール結果フィルタリングの回帰テスト

# 1. thinkingモードを無効化
openclaw config set thinking false

# 2. ツールコールを実行(例: ファイル読み取り)
openclaw tool run read-file --path /tmp/test.txt

# 3. ページを更新

# 4. OFFモードではツール結果が非表示、ONモードでは表示されることを確認

期待される終了コード: すべてのテストが終了コード0で合格。

⚠️ よくある落とし穴

1. 履歴読み込み時の競合状態

問題: チャットコントローラがlocalStorageからDev Mode設定を読み取る前に履歴を読み込む可能性がある。

対策: Dev Modeを遅延ではなく、モジュール読み込み時にlocalStorageから同期的に初期化する。

// 正しい: 同期的な初期化
const DEV_MODE = (() => {
  try {
    return JSON.parse(localStorage.getItem('openclaw_ui_settings') || '{}').openclaw_dev_mode ?? false;
  } catch {
    return false;
  }
})();

// 誤り: 遅延初期化(競合を引き起こす)
const getDevMode = async () => loadSetting(...); // これは避ける

2. メッセージ分類の不整合

問題: 一部のメッセージプロデューサー(ハートビート、システムイベント、プラグイン)がmessageClassまたはinternalフラグを設定しない場合がある。

対策: 開発モードでスキーマバリデーターを実装する:

// buildChatItemsに追加
if (process.env.NODE_ENV === 'development') {
  history.forEach(item => {
    if (!item.messageClass && item.role === 'system') {
      console.warn('[Dev] System message missing messageClass:', item.id);
    }
  });
}

3. Docker/コンテナ環境のlocalStorage

問題: Dockerコンテナ内で実行されるWebChatは、分離されたlocalStorage動作を持つ場合がある。

回避策: localStorageに加えて、バックエンドAPI呼び出しでDev Mode設定が永続化されることを確認する:

// サーバーサイド設定へのフォールバック
async function getDevMode() {
  const local = loadSetting('openclaw_dev_mode', false);
  const server = await fetch('/api/user/preferences/dev-mode').catch(() => null);
  return server ? await server.json() : local;
}

4. 履歴サイズとメモリ

問題: すべての内部メッセージを含む完全な履歴を読み込むと、長い会話でメモリ問題を引き起こす可能性がある。

対策: 保存時に内部メッセージの履歴プルーニングを実装する:

// チャット状態を永続化するとき
function pruneInternalMessages(chat) {
  return {
    ...chat,
    history: chat.history.filter(item => 
      item.internal ? false : true // 内部メッセージを永続化しない
    )
  };
}

5. マルチタブ同期

問題: 1つのタブ内のDev Mode切り替えが、他の開いているWebChatタブに同期されない場合がある。

回避策: ストレージイベントをリッスンする:

window.addEventListener('storage', (e) => {
  if (e.key === 'openclaw_ui_settings') {
    // 更新された設定で再レンダリング
    forceUpdate();
  }
});

6. ブラウザ拡張機能の競合

問題: localStorageを変更하거나スクリプトを挿入する拡張機能が設定破損を引き起こす可能性がある。

検出: 完全性チェックを追加する:

function validateSettings() {
  try {
    const settings = JSON.parse(localStorage.getItem('openclaw_ui_settings'));
    return typeof settings.openclaw_dev_mode === 'boolean';
  } catch {
    return false;
  }
}

🔗 関連するエラー

論理的に関連する問題

  • #26461 — 迅速な更新サイクル後の履歴状態破損(関連: 再読み込み時の状態管理)
  • #21032 — 会話エクスポートにシステムメッセージが表示される(関連: エクスポート境界での履歴フィルタリング)
  • #12186 — ツールチェーンの可視性がUI設定を尊重しない(終了/古い Similar Filtering Scope)

類似のエラーンパターン

エラーコード説明関連性
E_HEARTBEAT_LEAKハートビート応答がユーザー向けビューに表示されるこのバグの直接的な症状
E_SYSTEM_MSG_LEAKシステムメッセージが通常チャットモードで露出するハートビートリークと同じ根本原因
E_TOOLCHAIN_VISIBLEツール実行チェーンが本番UIに表示されるbuildChatItemsの関連フィルタリング問題
E_HISTORY_RELOAD_ARTIFACTSページリロード後に古い内部エントリが表示される履歴再読み込みのタイミング問題

アーキテクチャ依存関係

  • ui/src/ui/controllers/chat.ts — 履歴読み込みロジック
  • ui/src/ui/views/chat.tsbuildChatItemsフィルタリング関数
  • packages/heartbeat/src/handler.ts — ハートビートメッセージ挿入
  • packages/core/src/types/chat.ts — メッセージ型定義

エビデンスとソース

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