WebChat filtra entradas internas/sistema al actualizar - WebChat Refresh Leaks Internal System/Heartbeat Entries
Después de actualizar WebChat, los mensajes internos (mensajes del sistema, respuestas de heartbeat, artefactos de cadena de herramientas) se filtran en la conversación visible debido al filtrado inconsistente del historial en buildChatItems.
🔍 Síntomas
Manifestaciones visuales
Después de una actualización de la página WebChat, los usuarios observan los siguientes artefactos que aparecen en la línea de tiempo de la conversación:
- Bloques de mensajes del sistema — Eventos internos del sistema renderizados como elementos de chat (ej., "Refresh filter repro")
- Texto de confirmación de latido — Cargas útiles de respuesta como
HEARTBEAT_OKvisibles para los usuarios finales - Elementos de cadena de herramientas/estados internos — Artefactos de ejecución de herramientas y mensajes de estado expuestos en la vista de conversación
Comandos CLI de reproducción
Para activar eventos internos para pruebas:
openclaw system event --mode now --text "Refresh filter repro"
Comportamiento esperado vs actual
| Estado | Esperado | Actual (Error) |
|---|---|---|
| Antes de la actualización | Conversación limpia de usuario/asistente | Conversación limpia de usuario/asistente |
| Después de la actualización | Conversación limpia de usuario/asistente | Artefactos de sistema/latido visibles |
Detalles del entorno
- Versión: OpenClaw 2026.2.26
- SO: macOS 26.1 (Darwin 25.1.0, arm64)
- Instalación: binario global de pnpm (
/Users/bell/Library/pnpm/openclaw) - Banderas de características: WebChat habilitado, latido habilitado
🧠 Causa raíz
Análisis arquitectónico
La fuga en la actualización de WebChat proviene de una inconsistencia arquitectónica de dos fases en el pipeline de renderizado del historial de chat:
Fase 1: Recarga del historial (ui/src/ui/controllers/chat.ts)
Cuando WebChat se inicializa o actualiza, el controlador de chat carga el array completo de chat.history:
// ui/src/ui/controllers/chat.ts (línea ~N)
const history = chat.history; // Carga el HISTORIAL COMPLETO incluyendo entradas internas
Esto incluye todos los tipos de mensajes independientemente de su clasificación como internos o visibles para el usuario.
Fase 2: Filtrado inadecuado (ui/src/ui/views/chat.ts)
La función buildChatItems aplica una lógica de filtrado que es insuficientemente amplia:
// ui/src/ui/views/chat.ts - función buildChatItems
function buildChatItems(history) {
return history.filter(item => {
// Lógica de filtrado actual (incompleta):
if (!item.thinking && item.type === 'toolresult') {
return false; // Solo suprime toolresult cuando thinking está desactivado
}
// FALTANTE: Sin filtro para clases de mensajes system/heartbeat/internal
return true;
});
}
Secuencia de falla
- El usuario activa un evento interno (latido, comando del sistema)
- El mensaje interno se inserta en
chat.historycon marcas de clasificación (ej.,role: 'system',internal: true,messageClass: 'heartbeat') - Ocurre la actualización de la página
chat.tsrecarga el historial completo incluyendo las entradas internasbuildChatItemsfalla al filtrar estas entradas porque la lógica de filtrado no verifica las marcas demessageClassointernal- Los artefactos internos se renderizan en la línea de tiempo visible de WebChat
Brecha en la clasificación de mensajes
La implementación actual carece de un enfoque sistemático para la clasificación de mensajes. Los mensajes internos deben llevar marcas de metadatos que la capa de renderizado pueda usar para filtrar:
// Estructura de mensaje esperada con clasificación
{
id: "msg_xxx",
role: "system",
content: "HEARTBEAT_OK",
messageClass: "heartbeat", // FALTANTE en la verificación del filtro
internal: true, // FALTANTE en la verificación del filtro
visibleInChat: false // FALTANTE en la verificación del filtro
}
🛠️ Solución paso a paso
Descripción general de la solución
Implementar un toggle de Modo Dev que controle la visibilidad de los mensajes:
- Modo Dev OFF (predeterminado): Mostrar solo elementos de conversación visibles para el usuario
- Modo Dev ON: Mostrar todos los internos con fines de depuración
Fase 1: Agregar marcas de clasificación de mensajes
Archivo: packages/core/src/types/chat.ts
// Agregar a la interfaz de mensaje
export interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
// ... campos existentes
// NUEVO: Clasificación para filtrado de UI
internal?: boolean;
messageClass?: 'user' | 'assistant' | 'system' | 'heartbeat' | 'tool' | 'status';
visibleInChat?: boolean; // Anulación explícita
}
Fase 2: Actualizar el filtrado de buildChatItems
Archivo: ui/src/ui/views/chat.ts
// ANTES (filtrado incompleto)
function buildChatItems(history, devMode = false) {
return history.filter(item => {
if (!item.thinking && item.type === 'toolresult') {
return false;
}
return true;
});
}
// DESPUÉS (filtrado completo)
function buildChatItems(history, devMode = false) {
return history.filter(item => {
// Anulación de visibilidad explícita
if (item.visibleInChat === false && !devMode) {
return false;
}
// Mensajes internos ocultos en modo producción
if (item.internal && !devMode) {
return false;
}
// Filtrado basado en clase de mensaje
const hiddenClasses = ['heartbeat', 'system', 'status'];
if (hiddenClasses.includes(item.messageClass) && !devMode) {
return false;
}
// Manejo heredado de toolresult (cuando thinking está desactivado)
if (!item.thinking && item.type === 'toolresult') {
return false;
}
return true;
});
}
Fase 3: Implementar el toggle de Modo Dev
Archivo: 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)}
/>
Modo Dev
</label>
<span className="dev-mode-indicator">
{devMode ? '🔧 Depuración' : '🚀 Producción'}
</span>
</div>
);
}
Archivo: 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('Error al persistir configuración:', key, e);
}
}
Fase 4: Conectar Modo Dev al controlador de chat
Archivo: ui/src/ui/controllers/chat.ts
// ANTES
const history = chat.history;
// DESPUÉS
import { loadSetting } from '../utils/settings';
const DEV_MODE = loadSetting('openclaw_dev_mode', false);
const history = buildChatItems(chat.history, DEV_MODE);
Fase 5: Marcar mensajes internos en el momento de inserción
Archivo: packages/heartbeat/src/handler.ts (o módulo de latido correspondiente)
// Al insertar respuesta de latido
chat.history.push({
id: generateId(),
role: 'system',
content: 'HEARTBEAT_OK',
messageClass: 'heartbeat',
internal: true,
visibleInChat: false, // Supresión explícita
timestamp: Date.now()
});
🧪 Verificación
Caso de prueba 1: Verificar vista limpia después de la actualización (Modo Dev OFF)
# 1. Iniciar WebChat con latido habilitado
openclaw start --webchat --heartbeat
# 2. Activar evento interno del sistema
openclaw system event --mode now --text "Refresh filter test"
# 3. Actualizar página de WebChat (Ctrl+Shift+R / Cmd+Shift+R)
# 4. Verificar que no haya artefactos internos en la conversación
# Esperado: Solo mensajes de usuario y respuestas del asistente visibles
# Verificar ausencia de: HEARTBEAT_OK, bloques de mensajes del sistema, cadenas de herramientas
Resultado esperado: La conversación contiene solo intercambios de usuario/asistente.
Caso de prueba 2: Verificar que Modo Dev muestra los internos
# 1. Habilitar toggle de Modo Dev en la UI de WebChat
# 2. Actualizar página
# 3. Verificar que los artefactos internos ahora son visibles
# Esperado: Mensajes del sistema, ACKs de latido, cadenas de herramientas visibles
Resultado esperado: Cadena completa de mensajes internos visible con indicadores de depuración.
Caso de prueba 3: Verificar que Modo Dev persiste entre sesiones
# 1. Habilitar Modo Dev
# 2. Cerrar pestaña de WebChat
# 3. Volver a abrir WebChat
# 4. Verificar que el estado de Modo Dev se preservó
# Verificar localStorage
window.localStorage.getItem('openclaw_ui_settings')
# Esperado: {"openclaw_dev_mode":true}
Caso de prueba 4: Verificación CLI de clasificación de mensajes
# Verificar que los mensajes tienen la clasificación correcta
openclaw chat history --format json | jq '.[] | select(.messageClass == "heartbeat")'
# Esperado: Devuelve entradas de latido (confirmando que la clasificación está configurada)
Caso de prueba 5: Prueba de regresión para filtrado de resultados de herramientas
# 1. Deshabilitar modo thinking
openclaw config set thinking false
# 2. Ejecutar una llamada de herramienta (ej., lectura de archivo)
openclaw tool run read-file --path /tmp/test.txt
# 3. Actualizar página
# 4. Verificar que los resultados de herramientas están ocultos en modo OFF, visibles en modo ON
Código de salida esperado: Todas las pruebas pasan con código de salida 0.
⚠️ Errores comunes
1. Condición de carrera en la carga del historial
Problema: El controlador de chat puede cargar el historial antes de que se lea la configuración de Modo Dev de localStorage.
Mitigación: Inicializar Modo Dev de forma síncrona desde localStorage al momento de carga del módulo, no de forma diferida.
// CORRECTO: Inicialización síncrona
const DEV_MODE = (() => {
try {
return JSON.parse(localStorage.getItem('openclaw_ui_settings') || '{}').openclaw_dev_mode ?? false;
} catch {
return false;
}
})();
// INCORRECTO: Inicialización diferida (causa carrera)
const getDevMode = async () => loadSetting(...); // NO HACER ESTO
2. Inconsistencia en la clasificación de mensajes
Problema: Algunos productores de mensajes (latido, eventos del sistema, plugins) pueden no establecer las marcas de messageClass o internal.
Mitigación: Implementar un validador de esquema en modo desarrollo:
// Agregar a buildChatItems
if (process.env.NODE_ENV === 'development') {
history.forEach(item => {
if (!item.messageClass && item.role === 'system') {
console.warn('[Dev] Mensaje del sistema sin messageClass:', item.id);
}
});
}
3. localStorage en entorno Docker/Contenedor
Problema: WebChat ejecutándose dentro de contenedores Docker puede tener comportamiento de localStorage aislado.
Solución alternativa: Asegurar que la preferencia de Modo Dev persista mediante una llamada a API del backend además de localStorage:
// Alternativa a preferencias del lado del servidor
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. Tamaño del historial y memoria
Problema: Cargar el historial completo con todos los mensajes internos puede causar problemas de memoria en conversaciones largas.
Mitigación: Implementar poda del historial para mensajes internos al guardar:
// Al persistir estado del chat
function pruneInternalMessages(chat) {
return {
...chat,
history: chat.history.filter(item =>
item.internal ? false : true // No persistir internos
)
};
}
5. Sincronización multi-pestaña
Problema: El toggle de Modo Dev en una pestaña puede no sincronizarse con otras pestañas abiertas de WebChat.
Solución alternativa: Escuchar eventos de almacenamiento:
window.addEventListener('storage', (e) => {
if (e.key === 'openclaw_ui_settings') {
// Volver a renderizar con configuración actualizada
forceUpdate();
}
});
6. Conflictos de extensiones del navegador
Problema: Extensiones que modifican localStorage o inyectan scripts pueden corromper la configuración.
Detección: Agregar verificación de integridad:
function validateSettings() {
try {
const settings = JSON.parse(localStorage.getItem('openclaw_ui_settings'));
return typeof settings.openclaw_dev_mode === 'boolean';
} catch {
return false;
}
}
🔗 Errores relacionados
Problemas lógicamente conectados
- #26461 — Corrupción del estado del historial después de ciclos rápidos de actualización (relacionado: gestión del estado durante la recarga)
- #21032 — Mensajes del sistema apareciendo en la exportación de conversación (relacionado: filtrado del historial en el límite de exportación)
- #12186 — Visibilidad de cadena de herramientas sin respetar preferencias de UI (cerrado/obsoleto pero con alcance de filtrado similar)
Patrones de error similares
| Código de error | Descripción | Conexión |
|---|---|---|
E_HEARTBEAT_LEAK | Respuestas de latido visibles en vistas面向 usuario | Síntoma directo de este error |
E_SYSTEM_MSG_LEAK | Mensajes del sistema expuestos en modo chat normal | Misma causa raíz que la fuga de latido |
E_TOOLCHAIN_VISIBLE | Cadena de ejecución de herramientas visible en UI de producción | Problema de filtrado relacionado en buildChatItems |
E_HISTORY_RELOAD_ARTIFACTS | Entradas internas obsoletas appearing después de recarga de página | Problema de temporización de recarga del historial |
Dependencias arquitectónicas
ui/src/ui/controllers/chat.ts— Lógica de carga del historialui/src/ui/views/chat.ts— Función de filtrado debuildChatItemspackages/heartbeat/src/handler.ts— Inserción de mensajes de latidopackages/core/src/types/chat.ts— Definiciones de tipos de mensaje