April 27, 2026 โ€ข Version: v2026.3.1

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 context

Diagnostic 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

VersionLLM InputBehavior
< v2026.3.1BodyCould be modified by formatInboundEnvelope
โ‰ฅ v2026.3.1BodyForAgentCannot 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:

FileChangeVerification
inbound-message.jsAdded fromMe and senderName fieldsCheck inbound.fromMe is boolean
process-message.jsPasses fromMe to buildInboundLineCheck third argument present
build-inbound-line.jsDestructures and forwards fromMe, senderNameCheck params passed correctly
format-inbound-envelope.jsAdds [You]: self marker to BodyCheck Body format in logs
finalize-inbound-context.jsAdds [Name]: prefix for DMsCheck 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.js

Expected 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 true

Verification 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) or msg.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

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.


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.


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.


Warning pattern:

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

Cause: fromMe was stored as a string (“true”/“false”) rather than a boolean.

Resolution: Ensure Boolean(msg.key?.fromMe) coercion in the inboundMessage constructor.


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).


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.

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.