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 remitenteComando 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ón | Entrada LLM | Comportamiento |
|---|---|---|
| < v2026.3.1 | Body | Podía ser modificado por formatInboundEnvelope |
| ≥ v2026.3.1 | BodyForAgent | No 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:
| Archivo | Cambio | Verificación |
|---|---|---|
inbound-message.js | Añadidos campos fromMe y senderName | Verificar que inbound.fromMe es booleano |
process-message.js | Pasa fromMe a buildInboundLine | Verificar que el tercer argumento está presente |
build-inbound-line.js | Destructura y reenvía fromMe, senderName | Verificar que los params se pasan correctamente |
format-inbound-envelope.js | Añade marcador [You]: a Body | Verificar formato de Body en los logs |
finalize-inbound-context.js | Añade prefijo [Name]: para MDs | Verificar 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.jsSalida 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 trueMé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) omsg.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 unexpectedlyCausa: 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.