April 21, 2026 • Versión: 2026.2.26

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_OK visibles 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

EstadoEsperadoActual (Error)
Antes de la actualizaciónConversación limpia de usuario/asistenteConversación limpia de usuario/asistente
Después de la actualizaciónConversación limpia de usuario/asistenteArtefactos 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

  1. El usuario activa un evento interno (latido, comando del sistema)
  2. El mensaje interno se inserta en chat.history con marcas de clasificación (ej., role: 'system', internal: true, messageClass: 'heartbeat')
  3. Ocurre la actualización de la página
  4. chat.ts recarga el historial completo incluyendo las entradas internas
  5. buildChatItems falla al filtrar estas entradas porque la lógica de filtrado no verifica las marcas de messageClass o internal
  6. 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 errorDescripciónConexión
E_HEARTBEAT_LEAKRespuestas de latido visibles en vistas面向 usuarioSíntoma directo de este error
E_SYSTEM_MSG_LEAKMensajes del sistema expuestos en modo chat normalMisma causa raíz que la fuga de latido
E_TOOLCHAIN_VISIBLECadena de ejecución de herramientas visible en UI de producciónProblema de filtrado relacionado en buildChatItems
E_HISTORY_RELOAD_ARTIFACTSEntradas internas obsoletas appearing después de recarga de páginaProblema de temporización de recarga del historial

Dependencias arquitectónicas

  • ui/src/ui/controllers/chat.ts — Lógica de carga del historial
  • ui/src/ui/views/chat.ts — Función de filtrado de buildChatItems
  • packages/heartbeat/src/handler.ts — Inserción de mensajes de latido
  • packages/core/src/types/chat.ts — Definiciones de tipos de mensaje

Evidencia y fuentes

Esta guía de solución de problemas fue sintetizada automáticamente por la tubería de inteligencia de FixClaw a partir de las discusiones de la comunidad.