April 21, 2026 • Version: 2026.2.26

[WebChat-Aktualisierung verliert interne System-/Heartbeat-Einträge] - WebChat Refresh Leaks Internal System/Heartbeat Entries

Nach dem Aktualisieren von WebChat gelangen interne Nachrichten (Systemnachrichten, Heartbeat-Antworten, Tool-Ketten-Artefakte) aufgrund inkonsistenter Verlaufsfilterung in buildChatItems in die sichtbare Unterhaltung.

🔍 Symptome

Visuelle Manifestationen

Nach einer WebChat-Seitenaktualisierung beobachten Benutzer die folgenden Artefakte in der Gesprächszeitlinie:

  • Systemnachrichtenblöcke — Interne Systemereignisse, die als Chat-Elemente gerendert werden (z.B. „Refresh filter repro")
  • Heartbeat-Bestätigungstext — Antwort-Nutzdaten wie HEARTBEAT_OK, die für Endbenutzer sichtbar sind
  • Interne Werkzeug-/Status-Kettenelemente — Werkzeug-Ausführungsartefakte und Statusmeldungen, die in der Gesprächsansicht offengelegt werden

CLI-Reproduktionsbefehle

So lösen Sie interne Ereignisse zum Testen aus:

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

Erwartetes vs. tatsächliches Verhalten

ZustandErwartetAktuell (Fehler)
Vor der AktualisierungSauberes Benutzer-/Assistenten-GesprächSauberes Benutzer-/Assistenten-Gespräch
Nach der AktualisierungSauberes Benutzer-/Assistenten-GesprächSystem-/Heartbeat-Artefakte sichtbar

Umgebungsdetails

  • Version: OpenClaw 2026.2.26
  • OS: macOS 26.1 (Darwin 25.1.0, arm64)
  • Installation: pnpm global binary (/Users/bell/Library/pnpm/openclaw)
  • Feature-Flags: WebChat aktiviert, Heartbeat aktiviert

🧠 Ursache

Architekturanalyse

Das WebChat-Aktualisierungsleck resultiert aus einer Inkonsistenz in der zweiphasigen Architektur der Chat-Verlaufs-Rendering-Pipeline:

Phase 1: Verlauf neu laden (ui/src/ui/controllers/chat.ts)

Wenn WebChat initialisiert oder aktualisiert wird, lädt der Chat-Controller das vollständige chat.history-Array:

// ui/src/ui/controllers/chat.ts (Zeile ~N)
const history = chat.history; // Lädt den VOLLSTÄNDIGEN Verlauf einschließlich interner Einträge

Dies umfasst alle Nachrichtentypen, unabhängig von ihrer Klassifizierung als intern oder benutzerorientiert.

Phase 2: Unzureichende Filterung (ui/src/ui/views/chat.ts)

Die Funktion buildChatItems wendet Filterlogik an, die nicht ausreichend umfassend ist:

// ui/src/ui/views/chat.ts - buildChatItems-Funktion
function buildChatItems(history) {
  return history.filter(item => {
    // Aktuelle Filterlogik (unvollständig):
    if (!item.thinking && item.type === 'toolresult') {
      return false; // Unterdrückt toolresult nur, wenn thinking deaktiviert ist
    }
    // FEHLT: Keine Filterung für system/heartbeat/interne Nachrichtenklassen
    return true;
  });
}

Fehlerfolge

  1. Benutzer löst internes Ereignis aus (Heartbeat, Systembefehl)
  2. Interne Nachricht wird mit Klassifizierungs-Flags in chat.history eingefügt (z.B. role: 'system', internal: true, messageClass: 'heartbeat')
  3. Seitenaktualisierung erfolgt
  4. chat.ts lädt den vollständigen Verlauf einschließlich interner Einträge
  5. buildChatItems scheitert beim Filtern dieser Einträge, da die Filterlogik nicht auf messageClass oder internal-Flags prüft
  6. Interne Artefakte werden in der sichtbaren WebChat-Zeitlinie gerendert

Lücke in der Nachrichtenklassifizierung

Die aktuelle Implementierung verfügt über keinen systematischen Ansatz zur Nachrichtenklassifizierung. Interne Nachrichten sollten Metadaten-Flags mitführen, die das Rendering-Layer zur Filterung verwenden kann:

// Erwartete Nachrichtenstruktur mit Klassifizierung
{
  id: "msg_xxx",
  role: "system",
  content: "HEARTBEAT_OK",
  messageClass: "heartbeat",      // FEHLT bei Filterprüfung
  internal: true,                  // FEHLT bei Filterprüfung
  visibleInChat: false             // FEHLT bei Filterprüfung
}

🛠️ Schritt-für-Schritt-Lösung

Lösungsübersicht

Implementieren Sie einen Dev-Modus-Toggle, der die Nachrichtensichtbarkeit steuert:

  • Dev-Modus AUS (Standard): Nur benutzerorientierte Konversationselemente anzeigen
  • Dev-Modus AN: Volle Internals zu Debugging-Zwecken anzeigen

Phase 1: Nachrichtenklassifizierungs-Flags hinzufügen

Datei: packages/core/src/types/chat.ts

// Zur Nachrichtenschnittstelle hinzufügen
export interface ChatMessage {
  id: string;
  role: 'user' | 'assistant' | 'system' | 'tool';
  content: string;
  // ... vorhandene Felder
  
  // NEU: Klassifizierung für UI-Filterung
  internal?: boolean;
  messageClass?: 'user' | 'assistant' | 'system' | 'heartbeat' | 'tool' | 'status';
  visibleInChat?: boolean; // Explizite Überschreibung
}

Phase 2: buildChatItems-Filterung aktualisieren

Datei: ui/src/ui/views/chat.ts

// VORHER (unvollständige Filterung)
function buildChatItems(history, devMode = false) {
  return history.filter(item => {
    if (!item.thinking && item.type === 'toolresult') {
      return false;
    }
    return true;
  });
}

// NACHHER (umfassende Filterung)
function buildChatItems(history, devMode = false) {
  return history.filter(item => {
    // Explizite Sichtbarkeitsüberschreibung
    if (item.visibleInChat === false && !devMode) {
      return false;
    }
    
    // Interne Nachrichten im Produktionsmodus ausgeblendet
    if (item.internal && !devMode) {
      return false;
    }
    
    // Nachrichtenklassen-basierte Filterung
    const hiddenClasses = ['heartbeat', 'system', 'status'];
    if (hiddenClasses.includes(item.messageClass) && !devMode) {
      return false;
    }
    
    // Legacy-Toolresult-Handhabung (wenn thinking deaktiviert ist)
    if (!item.thinking && item.type === 'toolresult') {
      return false;
    }
    
    return true;
  });
}

Phase 3: Dev-Modus-Toggle implementieren

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

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

Phase 4: Dev-Modus mit Chat-Controller verbinden

Datei: ui/src/ui/controllers/chat.ts

// VORHER
const history = chat.history;

// NACHHER
import { loadSetting } from '../utils/settings';

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

Phase 5: Interne Nachrichten beim Einfügen markieren

Datei: packages/heartbeat/src/handler.ts (oder relevantes Heartbeat-Modul)

// Beim Einfügen der Heartbeat-Antwort
chat.history.push({
  id: generateId(),
  role: 'system',
  content: 'HEARTBEAT_OK',
  messageClass: 'heartbeat',
  internal: true,
  visibleInChat: false, // Explizite Unterdrückung
  timestamp: Date.now()
});

🧪 Verifizierung

Testfall 1: Saubere Ansicht nach Aktualisierung verifizieren (Dev-Modus AUS)

# 1. WebChat mit aktiviertem Heartbeat starten
openclaw start --webchat --heartbeat

# 2. Internes Systemereignis auslösen
openclaw system event --mode now --text "Refresh filter test"

# 3. WebChat-Seite aktualisieren (Strg+Umschalt+R / Cmd+Umschalt+R)

# 4. Keine internen Artefakte im Gespräch verifizieren
# Erwartet: Nur Benutzernachrichten und Assistentenantworten sichtbar
# Auf Abwesenheit prüfen von: HEARTBEAT_OK, Systemnachrichtenblöcke, Werkzeugketten

Erwartete Ausgabe: Das Gespräch enthält nur Benutzer-/Assistenten-Austausche.

Testfall 2: Dev-Modus zeigt Internals

# 1. Dev-Modus-Toggle in WebChat-UI aktivieren

# 2. Seite aktualisieren

# 3. Interne Artefakte jetzt sichtbar verifizieren
# Erwartet: Systemnachrichten, Heartbeat-ACKs, Werkzeugketten sichtbar

Erwartete Ausgabe: Vollständige interne Nachrichtenkette mit Debug-Indikatoren sichtbar.

Testfall 3: Dev-Modus bleibt über Sitzungen hinweg erhalten

# 1. Dev-Modus aktivieren
# 2. WebChat-Tab schließen
# 3. WebChat erneut öffnen
# 4. Dev-Modus-Zustand bleibt erhalten verifizieren

# localStorage prüfen
window.localStorage.getItem('openclaw_ui_settings')
# Erwartet: {"openclaw_dev_mode":true}

Testfall 4: CLI-Verifizierung der Nachrichtenklassifizierung

# Verifizieren, dass Nachrichten korrekte Klassifizierung haben
openclaw chat history --format json | jq '.[] | select(.messageClass == "heartbeat")'

# Erwartet: Gibt Heartbeat-Einträge zurück (bestätigt Klassifizierung ist gesetzt)

Testfall 5: Regressionstest für Werkzeugresultat-Filterung

# 1. Thinking-Modus deaktivieren
openclaw config set thinking false

# 2. Einen Werkzeugaufruf ausführen (z.B. Dateilesen)
openclaw tool run read-file --path /tmp/test.txt

# 3. Seite aktualisieren

# 4. Verifizieren, dass Werkzeugresultate im AUS-Modus ausgeblendet, im AN-Modus sichtbar sind

Erwarteter Exit-Code: Alle Tests bestehen mit Exit-Code 0.

⚠️ Häufige Fehler

1. Race-Bedingung beim Verlaufsladen

Problem: Chat-Controller lädt möglicherweise den Verlauf, bevor die Dev-Modus-Einstellung aus localStorage gelesen wird.

Gegenmaßnahme: Dev-Modus synchron aus localStorage beim Modulladezeitpunkt initialisieren, nicht verzögert.

// KORREKT: Synchronische Initialisierung
const DEV_MODE = (() => {
  try {
    return JSON.parse(localStorage.getItem('openclaw_ui_settings') || '{}').openclaw_dev_mode ?? false;
  } catch {
    return false;
  }
})();

// INCORRECT: Verzögerte Initialisierung (verursacht Race)
const getDevMode = async () => loadSetting(...); // NICHT SO MACHEN

2. Inkonsistenz bei der Nachrichtenklassifizierung

Problem: Einige Nachrichtenproduzenten (Heartbeat, Systemereignisse, Plugins) setzen möglicherweise keine messageClass- oder internal-Flags.

Gegenmaßnahme: Schema-Validator im Entwicklungsmodus implementieren:

// Zu buildChatItems hinzufügen
if (process.env.NODE_ENV === 'development') {
  history.forEach(item => {
    if (!item.messageClass && item.role === 'system') {
      console.warn('[Dev] Systemnachricht mit fehlender messageClass:', item.id);
    }
  });
}

3. Docker/Container-Umgebung localStorage

Problem: WebChat in Docker-Containern kann isoliertes localStorage-Verhalten aufweisen.

Workaround: Dev-Modus-Präferenz zusätzlich zu localStorage über einen Backend-API-Aufruf persistieren:

// Fallback zu serverseitigen Präferenzen
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. Verlaufsgröße und Speicher

Problem: Vollständigen Verlauf mit allen internen Nachrichten laden kann bei langen Gesprächen zu Speicherproblemen führen.

Gegenmaßnahme: History-Pruning für interne Nachrichten beim Speichern implementieren:

// Beim Persistieren des Chat-Zustands
function pruneInternalMessages(chat) {
  return {
    ...chat,
    history: chat.history.filter(item => 
      item.internal ? false : true // Internals nicht persistieren
    )
  };
}

5. Multi-Tab-Synchronisation

Problem: Dev-Modus-Toggle in einem Tab wird möglicherweise nicht mit anderen offenen WebChat-Tabs synchronisiert.

Workaround: Auf Storage-Events hören:

window.addEventListener('storage', (e) => {
  if (e.key === 'openclaw_ui_settings') {
    // Mit aktualisierten Einstellungen neu rendern
    forceUpdate();
  }
});

6. Browser-Erweiterung-Konflikte

Problem: Erweiterungen, die localStorage modifizieren oder Skripte injizieren, können Einstellungen beschädigen.

Erkennung: Integritätsprüfung hinzufügen:

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

🔗 Zugehörige Fehler

Logisch verbundene Probleme

  • #26461 — Verlaufszustandskorruption nach schnellen Aktualisierungszyklen (verwandt: Zustandsverwaltung während des Neuladens)
  • #21032 — Systemnachrichten erscheinen im Gesprächsexport (verwandt: Verlaufsfilterung an Exportgrenze)
  • #12186 — Werkzeugkettensichtbarkeit respektiert UI-Präferenzen nicht (geschlossen/veraltet, aber ähnlicher Filterumfang)

Ähnliche Fehlermuster

FehlercodeBeschreibungVerbindung
E_HEARTBEAT_LEAKHeartbeat-Antworten in benutzerorientierten Ansichten sichtbarDirektes Symptom dieses Fehlers
E_SYSTEM_MSG_LEAKSystemnachrichten im normalen Chat-Modus offengelegtGleiche Ursache wie Heartbeat-Leck
E_TOOLCHAIN_VISIBLEWerkzeugausführungskette in Produktions-UI sichtbarVerwandtes Filterungsproblem in buildChatItems
E_HISTORY_RELOAD_ARTIFACTSVeraltete interne Einträge erscheinen nach SeitenreloadVerlaufsneulade-Timing-Problem

Architektonische Abhängigkeiten

  • ui/src/ui/controllers/chat.ts — Verlaufslade-Logik
  • ui/src/ui/views/chat.tsbuildChatItems-Filterfunktion
  • packages/heartbeat/src/handler.ts — Heartbeat-Nachrichteneinfügung
  • packages/core/src/types/chat.ts — Nachrichtentypdefinitionen

Belege & Quellen

Diese Troubleshooting-Anleitung wurde automatisch von der FixClaw Intelligence Pipeline aus Community-Diskussionen synthetisiert.