April 22, 2026 • Versión: v2026.3.1

Los mensajes directos carecen de atribución de remitente en contexto LLM - DM Messages Lack Sender Attribution in LLM Context (BodyForAgent)

Las conversaciones de mensajes directos aparecen como flujos de texto indiferenciados para el LLM porque la identificación del remitente no se propaga a través del pipeline de entrada y BodyForAgent carece de prefijos de remitente.

🔍 Síntomas

Manifestación Principal

Cuando el agente procesa conversaciones de MD, el LLM recibe mensajes sin contexto del remitente. Considera este intercambio de MD:

Lo que el LLM realmente recibe (comportamiento actual):

Hey, are you free tonight?
Yes, I'll be there at 8
Great, see you then!
Looking forward to it!

Lo que el LLM debería recibir (comportamiento esperado):

[Alice]: Hey, are you free tonight?
[Agent]: Yes, I'll be there at 8
[Alice]: Great, see you then!
[Agent]: Looking forward to it!

Observaciones Técnicas

El flag fromMe existe a nivel del adaptador de protocolo pero no está disponible para el LLM:

// A nivel del adaptador (ejemplo de WhatsApp):
msg.key.fromMe  // Booleano - identifica correctamente al remitente

// A nivel de entrada del LLM (BodyForAgent):
params.msg.body  // "Hey, are you free tonight?" — sin contexto del remitente

Comando de Diagnóstico

Para inspeccionar el estado actual de inboundMessage:

# Habilitar registro de depuración para observar la estructura del mensaje entrante
DEBUG=openclaw:inbound node agent.js

# Salida de depuración esperada mostrando campos faltantes:
# inboundMessage {
#   body: "Hey, are you free tonight?",
#   from: "+1234567890",
#   pushName: "Alice",
#   chatType: "direct",
#   // fromMe: undefined  ← FALTANTE
#   // senderName: undefined  ← NO PROPAGADO
# }

Comportamiento Específico por Versión

Este problema se manifiesta específicamente desde v2026.3.1 porque el framework cambió de pasar Body a pasar BodyForAgent al LLM. Los cambios en formatInboundEnvelope solo afectan a Body y son invisibles para el LLM.

🧠 Causa Raíz

Brecha Arquitectónica: La Ruta de Propagación Faltante

La causa raíz es un flujo de datos roto entre el adaptador de protocolo y el contexto del LLM. El flag fromMe y senderName están disponibles temprano en el pipeline pero no se propagan a través de toda la cadena hasta BodyForAgent.

Análisis del Flujo de Datos

┌─────────────────────────────────────────────────────────────────────────┐ │ FLUJO DE DATOS ACTUAL │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Adaptador de Protocolo │ │ ├── msg.key.fromMe = true/false ✓ DISPONIBLE │ │ ├── msg.pushName = “Alice” ✓ DISPONIBLE │ │ └── msg.chatType = “direct” ✓ DISPONIBLE │ │ │ │ │ ▼ │ │ Constructor de inboundMessage │ │ └── Campo fromMe NO AÑADIDO ✗ DESCARTADO AQUÍ │ │ │ │ │ ▼ │ │ processMessage → buildInboundLine → formatInboundEnvelope │ │ └── fromMe sigue siendo undefined ✗ NO REENVIADO │ │ │ │ │ ▼ │ │ Llamador de finalizeInboundContext │ │ └── BodyForAgent carece de prefijo [senderName]: ✗ LLM RECIBE DATOS AMBIGUOS │ │ └─────────────────────────────────────────────────────────────────────────┘

Análisis a Nivel de Código

1. Construcción de inboundMessage (Punto de Descarte #1)

javascript // Implementación actual - campo fromMe faltante function inboundMessage(msg, chatId) { return { body: msg.body || msg.text || “”, from: msg.from || msg.chat?.id, pushName: msg.pushName || msg.sender?.first_name, chatType: msg.chatType || (msg.chat?.isGroup ? “group” : “direct”), timestamp: msg.timestamp || Date.now(), // FALTANTE: fromMe: Boolean(msg.key?.fromMe) // FALTANTE: senderName: extractSenderName(msg) }; }

2. formatInboundEnvelope (No Afecta al LLM)

javascript // Esta función modifica Body, que NO es lo que recibe el LLM function formatInboundEnvelope(params) { const selfMarker = params.fromMe ? “[You]: " : “”; return { Body: ${selfMarker}${params.body}, // BodyForAgent NO se establece aquí — LLM recibe el valor raw }; }

3. Llamador de finalizeInboundContext (Punto de Descarte #2)

La transformación final que construye BodyForAgent no incluye la lógica de prefijo del remitente:

javascript // Implementación actual const BodyForAgent = params.msg.body; // Texto raw, sin atribución

Por Qué los Mensajes de Grupo Funcionan Correctamente

Los mensajes de grupo inherentemente incluyen atribución del remitente porque el formato del mensaje ya contiene el nombre del remitente:

javascript // Los mensajes de grupo ya tienen esta estructura desde el protocolo: “[GroupName] @Alice: message content” // o “[Alice]: message content”

Los mensajes de MD carecen de este prefijo estructural, lo que hace imposible la identificación del remitente sin manejo explícito.

Análisis de Regresión por Versión

VersiónEntrada LLMComportamiento
< v2026.3.1BodyPodía ser modificado por formatInboundEnvelope
≥ v2026.3.1BodyForAgentNo puede ser modificado por formatInboundEnvelope

La refactorización en v2026.3.1 introdujo un campo directo BodyForAgent que evita la transformación de formatInboundEnvelope, creando esta brecha.

🛠️ Solución Paso a Paso

Esta solución requiere modificaciones a cinco funciones a lo largo del pipeline entrante. Aplica los cambios en el orden indicado para mantener la integridad de los datos.

Fase 1: Propagar fromMe a Través del Pipeline

Paso 1.1: Añadir fromMe a la Construcción de inboundMessage

Archivo: src/core/message/inbound-message.js

Antes: javascript function inboundMessage(msg, chatId) { return { body: msg.body || msg.text || “”, from: msg.from || msg.chat?.id, pushName: msg.pushName || msg.sender?.first_name, chatType: msg.chatType || (msg.chat?.isGroup ? “group” : “direct”), timestamp: msg.timestamp || Date.now(), // FALTANTE }; }

Después: javascript function inboundMessage(msg, chatId) { return { body: msg.body || msg.text || “”, from: msg.from || msg.chat?.id, pushName: msg.pushName || msg.sender?.first_name, chatType: msg.chatType || (msg.chat?.isGroup ? “group” : “direct”), timestamp: msg.timestamp || Date.now(), fromMe: Boolean(msg.key?.fromMe), // AÑADIR: Propagar flag fromMe senderName: msg.pushName || msg.sender?.first_name || msg.sender?.username || “Unknown”, // AÑADIR: Extracción del nombre del remitente }; }

Paso 1.2: Pasar fromMe a Través de processMessage

Archivo: src/core/message/process-message.js

Antes: javascript async function processMessage(msg, chatId, context) { const inbound = inboundMessage(msg, chatId);

// … otro procesamiento …

await buildInboundLine(inbound, context); }

Después: javascript async function processMessage(msg, chatId, context) { const inbound = inboundMessage(msg, chatId);

// … otro procesamiento …

await buildInboundLine(inbound, context, { fromMe: inbound.fromMe }); }

Paso 1.3: Destructurar y Reenviar fromMe en buildInboundLine

Archivo: src/core/message/build-inbound-line.js

Antes: javascript async function buildInboundLine(inbound, context) { const { body, from, pushName, chatType, timestamp } = inbound;

// … procesamiento …

await formatInboundEnvelope({ msg: { body, from, pushName, chatType, timestamp }, conversation: context.conversation, }); }

Después: javascript async function buildInboundLine(inbound, context, options = {}) { const { body, from, pushName, chatType, timestamp, fromMe, senderName } = inbound;

// … procesamiento …

await formatInboundEnvelope({ msg: { body, from, pushName, chatType, timestamp, fromMe, senderName }, conversation: context.conversation, fromMe: options.fromMe ?? fromMe, }); }

Paso 1.4: Añadir Marcador de Autoresferencia a formatInboundEnvelope (Para Body)

Archivo: src/core/message/format-inbound-envelope.js

Antes: javascript function formatInboundEnvelope(params) { return { Body: params.msg.body, // … otros campos }; }

Después: javascript function formatInboundEnvelope(params) { const selfMarker = params.fromMe ? “[You]: " : “”;

return { Body: ${selfMarker}${params.msg.body}, // … otros campos }; }

Fase 2: Añadir Prefijo de Remitente a BodyForAgent

Paso 2.1: Modificar el Llamador de finalizeInboundContext

Archivo: src/core/context/finalize-inbound-context.js

Antes: javascript function finalizeInboundContext(params) { // … otro procesamiento …

const BodyForAgent = params.msg.body;

return { // … otros campos BodyForAgent, }; }

Después: javascript function finalizeInboundContext(params) { // … otro procesamiento …

// Añadir prefijo de remitente solo para MDs; los mensajes de grupo ya tienen atribución const dmPrefix = params.msg.chatType !== “group” ? [${params.msg.senderName || params.msg.from || "Unknown"}]: : “”; const BodyForAgent = ${dmPrefix}${params.msg.body};

return { // … otros campos BodyForAgent, }; }

Fase 3: Lista de Verificación

Después de aplicar todos los cambios, verifica las siguientes modificaciones:

ArchivoCambioVerificación
inbound-message.jsAñadidos campos fromMe y senderNameVerificar que inbound.fromMe es booleano
process-message.jsPasa fromMe a buildInboundLineVerificar que el tercer argumento está presente
build-inbound-line.jsDestructura y reenvía fromMe, senderNameVerificar que los params se pasan correctamente
format-inbound-envelope.jsAñade marcador [You]: a BodyVerificar formato de Body en los logs
finalize-inbound-context.jsAñade prefijo [Name]: para MDsVerificar formato de BodyForAgent

🧪 Verificación

Método de Verificación 1: Validación con Pruebas Unitarias

Crea y ejecuta la siguiente prueba para validar la cadena de propagación:

// test/inbound-propagation.test.js
const { inboundMessage } = require('../src/core/message/inbound-message');
const { processMessage } = require('../src/core/message/process-message');
const { buildInboundLine } = require('../src/core/message/build-inbound-line');
const { formatInboundEnvelope } = require('../src/core/message/format-inbound-envelope');
const { finalizeInboundContext } = require('../src/core/context/finalize-inbound-context');

describe('fromMe propagation and sender identification', () => {
  const mockMsg = {
    body: "Test message",
    from: "+1234567890",
    pushName: "Alice",
    chatType: "direct",
    timestamp: Date.now(),
    key: { fromMe: false },
  };

  const mockMsgFromMe = {
    ...mockMsg,
    key: { fromMe: true },
  };

  test('inboundMessage includes fromMe and senderName', () => {
    const result = inboundMessage(mockMsg, 'chat123');
    expect(result.fromMe).toBe(false);
    expect(result.senderName).toBe('Alice');
  });

  test('inboundMessage.fromMe is true when key.fromMe is true', () => {
    const result = inboundMessage(mockMsgFromMe, 'chat123');
    expect(result.fromMe).toBe(true);
  });

  test('BodyForAgent includes [senderName]: prefix for DMs', () => {
    const context = { conversation: [] };
    const result = finalizeInboundContext({
      msg: { body: "Test", chatType: "direct", senderName: "Alice", from: "+1234567890" },
      conversation: context.conversation,
    });
    expect(result.BodyForAgent).toBe('[Alice]: Test');
  });

  test('BodyForAgent has no prefix for group messages', () => {
    const context = { conversation: [] };
    const result = finalizeInboundContext({
      msg: { body: "Test", chatType: "group", senderName: "Alice", from: "+1234567890" },
      conversation: context.conversation,
    });
    expect(result.BodyForAgent).toBe('Test');
  });

  test('Body includes [You]: prefix when fromMe is true', () => {
    const result = formatInboundEnvelope({
      msg: { body: "My message", fromMe: true },
    });
    expect(result.Body).toBe('[You]: My message');
  });
});

Ejecutar las pruebas:

npm test -- test/inbound-propagation.test.js

Salida esperada:

✓ inboundMessage includes fromMe and senderName
✓ inboundMessage.fromMe is true when key.fromMe is true
✓ BodyForAgent includes [senderName]: prefix for DMs
✓ BodyForAgent has no prefix for group messages
✓ Body includes [You]: prefix when fromMe is true

Método de Verificación 2: Prueba de Integración con Adaptador de Protocolo Mock

javascript // test/dm-sender-attribution.test.js const { runFullPipeline } = require(’../src/core/test-helpers’);

async function testDMSenderAttribution() { const mockAdapter = { name: ‘mock’, sendMessage: jest.fn(), onMessage: (handler) => { // Simular MD entrante de Alice handler({ key: { fromMe: false }, body: “Hey, are you free tonight?”, from: “+1111111111”, pushName: “Alice”, chatType: “direct”, timestamp: Date.now(), });

  // Simular MD saliente (fromMe = true)
  handler({
    key: { fromMe: true },
    body: "Yes, I'll be there at 8",
    from: "+2222222222",
    pushName: "Agent",
    chatType: "direct",
    timestamp: Date.now(),
  });
},

};

const agent = createAgent({ adapter: mockAdapter }); await agent.start();

// Capturar la entrada del LLM const llmInput = captureLLMInput();

console.log(‘LLM received BodyForAgent:’); console.log(llmInput.BodyForAgent);

// Salida esperada: // [Alice]: Hey, are you free tonight? // [Agent]: Yes, I’ll be there at 8

await agent.stop(); }

testDMSenderAttribution();

Método de Verificación 3: Registro de Depuración Manual

Habilitar registro verboso para inspeccionar el pipeline completo:

# Establecer variables de entorno
export DEBUG=openclaw:inbound,openclaw:context
export LOG_LEVEL=debug

# Ejecutar el agente
node agent.js 2>&1 | grep -E "(fromMe|senderName|BodyForAgent|\[.*\]:)"

# Salida de registro esperada para mensajes de MD:
# [debug] inboundMessage.fromMe: false
# [debug] inboundMessage.senderName: "Alice"
# [debug] BodyForAgent: "[Alice]: Hey, are you free tonight?"

Método de Verificación 4: Inspección del Estado de la Base de Datos

Si usas contexto persistente, verifica los mensajes almacenados:

# Consultar la tabla de mensajes
SELECT id, sender_name, from_me, body, body_for_agent 
FROM messages 
WHERE chat_type = 'direct' 
ORDER BY timestamp DESC LIMIT 5;

-- Resultado esperado:
-- | id | sender_name | from_me | body            | body_for_agent                    |
-- | 1  | Alice       | false   | Hey, free?      | [Alice]: Hey, free?               |
-- | 2  | Agent       | true    | Yes, at 8       | [Agent]: Yes, at 8                |

⚠️ Errores Comunes

Error 1: Variación del Nombre del Campo en el Adaptador de Protocolo

Problema: Diferentes plataformas de mensajería exponen fromMe bajo nombres de campo variables.

  • WhatsApp: msg.key.fromMe
  • Telegram: msg.from.is_bot (lógica invertida) o msg.outgoing
  • Signal: msg.direction === "outgoing"
  • Discord: msg.author.id === msg.client.user.id

Mitigación: Crear mapeadores de campo específicos del adaptador en el constructor de inboundMessage:

javascript function extractFromMe(msg, platform) { switch (platform) { case ‘whatsapp’: return Boolean(msg.key?.fromMe); case ’telegram’: return Boolean(msg.outgoing); case ‘signal’: return msg.direction === ‘outgoing’; case ‘discord’: return msg.author?.id === msg.client?.user?.id; default: return false; } }

Error 2: Cadena de Fallback de senderName Incompleta

Problema: pushName puede no estar disponible (el usuario tiene configuraciones de privacidad habilitadas, o es el primer mensaje antes de que se almacene en caché el push name).

Mitigación: Implementar una cadena de fallback robusta:

javascript function extractSenderName(msg) { return ( msg.pushName || msg.sender?.first_name || msg.sender?.username || msg.from?.split(’@’)[0] || // Usar JID/número de teléfono como último recurso “Unknown” ); }

Error 3: Prefijo Duplicado en Mensajes de Grupo

Problema: Si los mensajes de grupo ya incluyen atribución del remitente en el payload del protocolo, añadir otro prefijo crea duplicación.

Ejemplo de salida incorrecta:

[#general] @Alice: [Alice]: Message content // Doble atribución

Mitigación: Verificar el formato del mensaje existente antes de añadir el prefijo:

javascript function shouldAddPrefix(msg, chatType) { if (chatType !== ‘direct’) { // Verificar si el mensaje de grupo ya tiene patrón de atribución const existingPattern = /^[[^]]+]\s*@\w+:/; return !existingPattern.test(msg.body); } return true; }

Error 4: Formatos de Nombre de Remitente Específicos de Plataforma

Problema: Diferentes plataformas formatean los nombres de remitente de manera diferente.

  • WhatsApp: Nombre para mostrar (ej., "John Smith")
  • Telegram: Nombre + apellido opcional
  • Discord: Nombre de usuario + discriminador (ej., "User#1234")

Mitigación: Normalizar los nombres de remitente antes de usarlos en prefijos:

javascript function normalizeSenderName(name, platform) { if (!name) return “Unknown”;

let normalized = name.trim();

if (platform === ‘discord’) { // Eliminar discriminador si está presente normalized = normalized.split(’#’)[0]; }

// Eliminar caracteres especiales que podrían romper el análisis return normalized.replace(/[[]]/g, ‘’).substring(0, 50); }

Error 5: Compatibilidad de Versión Después de v2026.3.1

Problema: Si el código base tiene lógica condicional basada en si se usa BodyForAgent o Body, la solución puede no aplicarse uniformemente.

Mitigación: Verificar qué campo consume realmente el adaptador del LLM:

javascript // Verificar la configuración del adaptador del LLM const llmAdapter = config.llm?.adapter;

// Si usa Body directamente (comportamiento antiguo), la solución de formatInboundEnvelope aplica // Si usa BodyForAgent (comportamiento nuevo), la solución de finalizeInboundContext aplica // Algunos adaptadores pueden usar ambos — asegurar consistencia

Error 6: Condición de Carrera en Procesamiento Paralelo de Mensajes

Problema: Al procesar múltiples MDs simultáneamente, el estado de fromMe puede estar obsoleto o incorrectamente asociado a un contexto de mensaje diferente.

Mitigación: Asegurar que fromMe esté vinculado al contexto del mensaje específico en el momento de la construcción, no recuperado globalmente:

javascript // INCORRECTO: Referencia a estado global inboundMessage.fromMe = globalLastMessageFromMe; // Condición de carrera

// CORRECTO: Extracción específica del mensaje inboundMessage.fromMe = Boolean(msg.key?.fromMe); // Aislado por mensaje

🔗 Errores Relacionados

Issue Relacionado #32060: Incluir MDs Salientes en el Contexto

Síntoma: El agente solo recibe MDs entrantes pero no los mensajes salientes enviados por el agente mismo, lo que hace que las conversaciones parezcan unilaterales.

Conexión: Este issue es el complemento de la solución actual. Mientras que #32060 asegura que los mensajes salientes se almacenen en el contexto, esta solución asegura que tanto los mensajes entrantes como los salientes tengan la atribución correcta del remitente.

Dependencia de resolución: La propagación de fromMe implementada en esta solución es requerida para la implementación correcta de #32060.


Issue Relacionado #32059: Desbordamiento de Ventana de Contexto en MDs

Síntoma: Las conversaciones largas de MD consumen demasiados tokens de ventana de contexto porque cada mensaje carece de identificación eficiente del remitente.

Conexión: El formato de prefijo [Name]: introducido por esta solución es intencionalmente conciso para minimizar la sobrecarga de tokens mientras proporciona la atribución necesaria.


Error Relacionado: undefined senderName in BodyForAgent

Patrón de error:

TypeError: Cannot read property 'senderName' of undefined
    at finalizeInboundContext (finalize-inbound-context.js:42)
    at processMessage (process-message.js:87)

Causa: senderName se accede antes de que la propagación de inboundMessage esté completa.

Resolución: Asegurar que la construcción de inboundMessage incluya el campo senderName antes de que se llame a finalizeInboundContext.


Advertencia Relacionada: fromMe is not a boolean

Patrón de advertencia:

Warning: BodyForAgent received non-boolean fromMe value: "true"
Warning: Self-marker logic may behave unexpectedly

Causa: fromMe se almacenó como string (“true”/“false”) en lugar de booleano.

Resolución: Asegurar la coerción Boolean(msg.key?.fromMe) en el constructor de inboundMessage.


Configuración Relacionada: dm.senderPrefix.enabled

Clave de configuración: openclaws.dm.senderPrefix.enabled

Propósito: Alternar el prefijo de remitente en MDs para pruebas o preferencia del usuario.

Valor predeterminado: true

Interacción: Cuando está deshabilitado, los MDs revierten al formato ambiguo; la propagación de fromMe aún funciona para otros propósitos (análisis, filtrado).


Categoría de Registro Relacionada: openclaw:inbound:fromme

Flag de depuración: DEBUG=openclaw:inbound:fromme

Salida: Registra el valor de fromMe en cada etapa del pipeline, útil para rastrear fallas de propagación.


Nota Histórica: Comportamiento Pre-v2026.3.1

Contexto: Antes de v2026.3.1, el framework usaba Body (modificado por formatInboundEnvelope) como entrada del LLM. Una solución de marcador de autoresferencia aplicada ahí habría sido visible para el LLM.

Cambio: v2026.3.1 introdujo BodyForAgent como un campo separado, evitando la transformación del envelope.

Impacto: Este cambio arquitectónico creó la brecha de propagación que esta solución aborda.

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.