DM Messages Lack Sender Attribution in LLM Context (BodyForAgent)
Direct message conversations appear as undifferentiated text streams to the LLM because sender identification is not propagated through the inbound pipeline and BodyForAgent lacks sender prefixes.
๐ Symptoms
Primary Manifestation
When the agent processes DM conversations, the LLM receives messages without sender context. Consider this DM exchange:
What the LLM actually receives (current behavior):
Hey, are you free tonight?
Yes, I'll be there at 8
Great, see you then!
Looking forward to it!What the LLM should receive (expected behavior):
[Alice]: Hey, are you free tonight?
[Agent]: Yes, I'll be there at 8
[Alice]: Great, see you then!
[Agent]: Looking forward to it!Technical Observations
The fromMe flag exists at the protocol adapter level but is not available to the LLM:
// At adapter level (WhatsApp example):
msg.key.fromMe // Boolean - correctly identifies sender
// At LLM input level (BodyForAgent):
params.msg.body // "Hey, are you free tonight?" โ no sender contextDiagnostic Command
To inspect the current state of inboundMessage:
# Enable debug logging to observe inbound message structure
DEBUG=openclaw:inbound node agent.js
# Expected debug output showing missing fields:
# inboundMessage {
# body: "Hey, are you free tonight?",
# from: "+1234567890",
# pushName: "Alice",
# chatType: "direct",
# // fromMe: undefined โ MISSING
# // senderName: undefined โ NOT PROPAGATED
# }Version-Specific Behavior
This issue manifests specifically since v2026.3.1 because the framework changed from passing Body to passing BodyForAgent to the LLM. Changes to formatInboundEnvelope only affect Body and are invisible to the LLM.
๐ง Root Cause
Architectural Gap: The Missing Propagation Path
The root cause is a broken data flow between the protocol adapter and the LLM context. The fromMe flag and senderName are available early in the pipeline but are not propagated through the entire chain to BodyForAgent.
Data Flow Analysis
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ CURRENT DATA FLOW โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ โ โ Protocol Adapter โ โ โโโ msg.key.fromMe = true/false โ AVAILABLE โ โ โโโ msg.pushName = “Alice” โ AVAILABLE โ โ โโโ msg.chatType = “direct” โ AVAILABLE โ โ โ โ โ โผ โ โ inboundMessage CONSTRUCTOR โ โ โโโ fromMe field NOT ADDED โ DROPPED HERE โ โ โ โ โ โผ โ โ processMessage โ buildInboundLine โ formatInboundEnvelope โ โ โโโ fromMe still undefined โ NOT FORWARDED โ โ โ โ โ โผ โ โ finalizeInboundContext caller โ โ โโโ BodyForAgent lacks [senderName]: prefix โ LLM RECEIVES AMBIGUOUS DATA โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Code-Level Analysis
1. inboundMessage Construction (Drop Point #1)
javascript // Current implementation - missing fromMe field 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(), // MISSING: fromMe: Boolean(msg.key?.fromMe) // MISSING: senderName: extractSenderName(msg) }; }
2. formatInboundEnvelope (Does Not Affect LLM)
javascript
// This function modifies Body, which is NOT what the LLM receives
function formatInboundEnvelope(params) {
const selfMarker = params.fromMe ? “[You]: " : “”;
return {
Body: ${selfMarker}${params.body},
// BodyForAgent is NOT set here โ LLM receives the raw value
};
}
3. finalizeInboundContext Caller (Drop Point #2)
The final transformation that builds BodyForAgent does not include sender prefix logic:
javascript // Current implementation const BodyForAgent = params.msg.body; // Raw text, no attribution
Why Group Messages Work Correctly
Group messages inherently include sender attribution because the message format already contains the sender name:
javascript // Group messages already have this structure from the protocol: “[GroupName] @Alice: message content” // or “[Alice]: message content”
DM messages lack this structural prefix, making sender identification impossible without explicit handling.
Version Regression Analysis
| Version | LLM Input | Behavior |
|---|---|---|
| < v2026.3.1 | Body | Could be modified by formatInboundEnvelope |
| โฅ v2026.3.1 | BodyForAgent | Cannot be modified by formatInboundEnvelope |
The v2026.3.1 refactor introduced a direct BodyForAgent field that bypasses the formatInboundEnvelope transformation, creating this gap.
๐ ๏ธ Step-by-Step Fix
This fix requires modifications to five functions across the inbound pipeline. Apply changes in the order listed to maintain data integrity.
Phase 1: Propagate fromMe Through the Pipeline
Step 1.1: Add fromMe to inboundMessage Construction
File: src/core/message/inbound-message.js
Before: 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(), // MISSING }; }
After: 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), // ADD: Propagate fromMe flag senderName: msg.pushName || msg.sender?.first_name || msg.sender?.username || “Unknown”, // ADD: Sender name extraction }; }
Step 1.2: Pass fromMe Through processMessage
File: src/core/message/process-message.js
Before: javascript async function processMessage(msg, chatId, context) { const inbound = inboundMessage(msg, chatId);
// … other processing …
await buildInboundLine(inbound, context); }
After: javascript async function processMessage(msg, chatId, context) { const inbound = inboundMessage(msg, chatId);
// … other processing …
await buildInboundLine(inbound, context, { fromMe: inbound.fromMe }); }
Step 1.3: Destructure and Forward fromMe in buildInboundLine
File: src/core/message/build-inbound-line.js
Before: javascript async function buildInboundLine(inbound, context) { const { body, from, pushName, chatType, timestamp } = inbound;
// … processing …
await formatInboundEnvelope({ msg: { body, from, pushName, chatType, timestamp }, conversation: context.conversation, }); }
After: javascript async function buildInboundLine(inbound, context, options = {}) { const { body, from, pushName, chatType, timestamp, fromMe, senderName } = inbound;
// … processing …
await formatInboundEnvelope({ msg: { body, from, pushName, chatType, timestamp, fromMe, senderName }, conversation: context.conversation, fromMe: options.fromMe ?? fromMe, }); }
Step 1.4: Add Self Marker to formatInboundEnvelope (For Body)
File: src/core/message/format-inbound-envelope.js
Before: javascript function formatInboundEnvelope(params) { return { Body: params.msg.body, // … other fields }; }
After: javascript function formatInboundEnvelope(params) { const selfMarker = params.fromMe ? “[You]: " : “”;
return {
Body: ${selfMarker}${params.msg.body},
// … other fields
};
}
Phase 2: Add Sender Prefix to BodyForAgent
Step 2.1: Modify finalizeInboundContext Caller
File: src/core/context/finalize-inbound-context.js
Before: javascript function finalizeInboundContext(params) { // … other processing …
const BodyForAgent = params.msg.body;
return { // … other fields BodyForAgent, }; }
After: javascript function finalizeInboundContext(params) { // … other processing …
// Add sender prefix for DMs only; group messages already have attribution
const dmPrefix = params.msg.chatType !== “group”
? [${params.msg.senderName || params.msg.from || "Unknown"}]: : “”;
const BodyForAgent = ${dmPrefix}${params.msg.body};
return { // … other fields BodyForAgent, }; }
Phase 3: Verification Checklist
After applying all changes, verify the following modifications:
| File | Change | Verification |
|---|---|---|
inbound-message.js | Added fromMe and senderName fields | Check inbound.fromMe is boolean |
process-message.js | Passes fromMe to buildInboundLine | Check third argument present |
build-inbound-line.js | Destructures and forwards fromMe, senderName | Check params passed correctly |
format-inbound-envelope.js | Adds [You]: self marker to Body | Check Body format in logs |
finalize-inbound-context.js | Adds [Name]: prefix for DMs | Check BodyForAgent format |
๐งช Verification
Verification Method 1: Unit Test Validation
Create and run the following test to validate the propagation chain:
// 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');
});
});
Run the tests:
npm test -- test/inbound-propagation.test.jsExpected output:
โ 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 trueVerification Method 2: Integration Test with Mock Protocol Adapter
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) => { // Simulate incoming DM from Alice handler({ key: { fromMe: false }, body: “Hey, are you free tonight?”, from: “+1111111111”, pushName: “Alice”, chatType: “direct”, timestamp: Date.now(), });
// Simulate outbound DM (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();
// Capture the LLM input const llmInput = captureLLMInput();
console.log(‘LLM received BodyForAgent:’); console.log(llmInput.BodyForAgent);
// Expected output: // [Alice]: Hey, are you free tonight? // [Agent]: Yes, I’ll be there at 8
await agent.stop(); }
testDMSenderAttribution();
Verification Method 3: Manual Debug Logging
Enable verbose logging to inspect the full pipeline:
# Set environment variables
export DEBUG=openclaw:inbound,openclaw:context
export LOG_LEVEL=debug
# Run the agent
node agent.js 2>&1 | grep -E "(fromMe|senderName|BodyForAgent|\[.*\]:)"
# Expected log output for DM messages:
# [debug] inboundMessage.fromMe: false
# [debug] inboundMessage.senderName: "Alice"
# [debug] BodyForAgent: "[Alice]: Hey, are you free tonight?"Verification Method 4: Database State Inspection
If using persistent context, verify the stored messages:
# Query the messages table
SELECT id, sender_name, from_me, body, body_for_agent
FROM messages
WHERE chat_type = 'direct'
ORDER BY timestamp DESC LIMIT 5;
-- Expected result:
-- | 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 |โ ๏ธ Common Pitfalls
Pitfall 1: Protocol Adapter Field Name Variance
Problem: Different messaging platforms expose fromMe under varying field names.
- WhatsApp:
msg.key.fromMe - Telegram:
msg.from.is_bot(inverted logic) ormsg.outgoing - Signal:
msg.direction === "outgoing" - Discord:
msg.author.id === msg.client.user.id
Mitigation: Create adapter-specific field mappers in the inboundMessage constructor:
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; } }
Pitfall 2: Missing senderName Fallback Chain
Problem: pushName may be unavailable (user has privacy settings enabled, or first message before push name is cached).
Mitigation: Implement robust fallback chain:
javascript function extractSenderName(msg) { return ( msg.pushName || msg.sender?.first_name || msg.sender?.username || msg.from?.split(’@’)[0] || // Use JID/phone number as last resort “Unknown” ); }
Pitfall 3: Double Prefix in Group Messages
Problem: If group messages already include sender attribution in the protocol payload, adding another prefix creates duplication.
Example of bad output:
[#general] @Alice: [Alice]: Message content // Double attribution
Mitigation: Check the existing message format before adding prefix:
javascript function shouldAddPrefix(msg, chatType) { if (chatType !== ‘direct’) { // Check if group message already has attribution pattern const existingPattern = /^[[^]]+]\s*@\w+:/; return !existingPattern.test(msg.body); } return true; }
Pitfall 4: Platform-Specific Sender Name Formats
Problem: Different platforms format sender names differently.
- WhatsApp: Display name (e.g., "John Smith")
- Telegram: First name + optional last name
- Discord: Username + discriminator (e.g., "User#1234")
Mitigation: Normalize sender names before using them in prefixes:
javascript function normalizeSenderName(name, platform) { if (!name) return “Unknown”;
let normalized = name.trim();
if (platform === ‘discord’) { // Remove discriminator if present normalized = normalized.split(’#’)[0]; }
// Remove special characters that might break parsing return normalized.replace(/[[]]/g, ‘’).substring(0, 50); }
Pitfall 5: Version Compatibility After v2026.3.1
Problem: If the codebase has conditional logic based on whether BodyForAgent or Body is used, the fix may not apply uniformly.
Mitigation: Verify which field the LLM adapter actually consumes:
javascript // Check your LLM adapter configuration const llmAdapter = config.llm?.adapter;
// If using Body directly (old behavior), the formatInboundEnvelope fix applies // If using BodyForAgent (new behavior), the finalizeInboundContext fix applies // Some adapters may use both โ ensure consistency
Pitfall 6: Race Condition in Parallel Message Processing
Problem: When processing multiple DMs simultaneously, the fromMe state may be stale or incorrectly associated with a different message context.
Mitigation: Ensure fromMe is bound to the specific message context at construction time, not retrieved globally:
javascript // WRONG: Global state reference inboundMessage.fromMe = globalLastMessageFromMe; // Race condition
// CORRECT: Message-specific extraction inboundMessage.fromMe = Boolean(msg.key?.fromMe); // Isolated per message
๐ Related Errors
Related Issue #32060: Include Outbound DMs in Context
Symptom: The agent only receives incoming DMs but not outbound messages sent by the agent itself, making conversations appear one-sided.
Connection: This issue is the complement to the current fix. While #32060 ensures outbound messages are stored in context, this fix ensures both inbound and outbound messages have proper sender attribution.
Resolution dependency: The fromMe propagation implemented in this fix is required for proper implementation of #32060.
Related Issue #32059: DM Context Window Overflow
Symptom: Long DM conversations consume excessive context window tokens because each message lacks efficient sender identification.
Connection: The [Name]: prefix format introduced by this fix is intentionally concise to minimize token overhead while providing necessary attribution.
Related Error: undefined senderName in BodyForAgent
Error pattern:
TypeError: Cannot read property 'senderName' of undefined
at finalizeInboundContext (finalize-inbound-context.js:42)
at processMessage (process-message.js:87)Cause: senderName is accessed before inboundMessage propagation is complete.
Resolution: Ensure inboundMessage construction includes the senderName field before finalizeInboundContext is called.
Related Warning: fromMe is not a boolean
Warning pattern:
Warning: BodyForAgent received non-boolean fromMe value: "true"
Warning: Self-marker logic may behave unexpectedlyCause: fromMe was stored as a string (“true”/“false”) rather than a boolean.
Resolution: Ensure Boolean(msg.key?.fromMe) coercion in the inboundMessage constructor.
Related Configuration: dm.senderPrefix.enabled
Configuration key: openclaws.dm.senderPrefix.enabled
Purpose: Toggle sender prefix in DMs for testing or user preference.
Default: true
Interaction: When disabled, DMs revert to ambiguous format; fromMe propagation still functions for other purposes (analytics, filtering).
Related Log Category: openclaw:inbound:fromme
Debug flag: DEBUG=openclaw:inbound:fromme
Output: Logs fromMe value at each pipeline stage, useful for tracing propagation failures.
Historical Note: Pre-v2026.3.1 Behavior
Context: Before v2026.3.1, the framework used Body (modified by formatInboundEnvelope) as LLM input. A self-marker fix applied there would have been visible to the LLM.
Change: v2026.3.1 introduced BodyForAgent as a separate field, bypassing the envelope transformation.
Impact: This architectural change created the propagation gap that this fix addresses.