[Direktnachrichten ohne Absenderzuordnung im LLM-Kontext] - DM Messages Lack Sender Attribution in LLM Context (BodyForAgent)
Direktnachrichten-Unterhaltungen erscheinen als undifferenzierte Textströme für das LLM, da die Absenderidentifikation nicht durch die eingehende Pipeline propagiert wird und BodyForAgent Absenderpräfixe fehlen.
🔍 Symptome
Primäre Manifestation
Wenn der Agent DM-Konversationen verarbeitet, erhält das LLM Nachrichten ohne Absenderkontext. Betrachten wir diesen DM-Austausch:
Was das LLM tatsächlich empfängt (aktuelles Verhalten):
Hey, are you free tonight?
Yes, I'll be there at 8
Great, see you then!
Looking forward to it!Was das LLM empfangen sollte (erwartetes Verhalten):
[Alice]: Hey, are you free tonight?
[Agent]: Yes, I'll be there at 8
[Alice]: Great, see you then!
[Agent]: Looking forward to it!Technische Beobachtungen
Das fromMe-Flag existiert auf Protocol-Adapter-Ebene, ist aber dem LLM nicht verfügbar:
// Auf Adapter-Ebene (WhatsApp-Beispiel):
msg.key.fromMe // Boolean - identifiziert Absender korrekt
// Auf LLM-Eingabe-Ebene (BodyForAgent):
params.msg.body // "Hey, are you free tonight?" — kein AbsenderkontextDiagnosebefehl
So inspizieren Sie den aktuellen Zustand von inboundMessage:
# Debug-Logging aktivieren, um eingehende Nachrichtenstruktur zu beobachten
DEBUG=openclaw:inbound node agent.js
# Erwartete Debug-Ausgabe mit fehlenden Feldern:
# inboundMessage {
# body: "Hey, are you free tonight?",
# from: "+1234567890",
# pushName: "Alice",
# chatType: "direct",
# // fromMe: undefined ← FEHLT
# // senderName: undefined ← NICHT PROPAGIERT
# }Versionsspezifisches Verhalten
Dieses Problem tritt spezifisch seit v2026.3.1 auf, da das Framework von der Übergabe von Body zur Übergabe von BodyForAgent an das LLM gewechselt hat. Änderungen an formatInboundEnvelope betreffen nur Body und sind für das LLM unsichtbar.
🧠 Ursache
Architektonische Lücke: Der fehlende Propagierungspfad
Die Ursache ist ein unterbrochener Datenfluss zwischen dem Protocol-Adapter und dem LLM-Kontext. Das fromMe-Flag und senderName sind früh in der Pipeline verfügbar, werden aber nicht durch die gesamte Kette bis zu BodyForAgent propagiert.
Datenflussanalyse
┌─────────────────────────────────────────────────────────────────────────┐ │ AKTUELLER DATENFLUSS │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Protocol-Adapter │ │ ├── msg.key.fromMe = true/false ✓ VERFÜGBAR │ │ ├── msg.pushName = “Alice” ✓ VERFÜGBAR │ │ └── msg.chatType = “direct” ✓ VERFÜGBAR │ │ │ │ │ ▼ │ │ inboundMessage-KONSTRUKTOR │ │ └── fromMe-Feld NICHT HINZUGEFÜGT ✗ HIER VERWORFEN │ │ │ │ │ ▼ │ │ processMessage → buildInboundLine → formatInboundEnvelope │ │ └── fromMe noch undefined ✗ NICHT WEITERGELEITET │ │ │ │ │ ▼ │ │ finalizeInboundContext-Aufrufer │ │ └── BodyForAgent enthält kein [senderName]: Präfix ✗ LLM ERHÄLT MEHRDEUTIGE DATEN │ │ └─────────────────────────────────────────────────────────────────────────┘
Code-Level-Analyse
1. inboundMessage-Konstruktion (Verwerfungspunkt #1)
javascript // Aktuelle Implementierung - fehlendes fromMe-Feld 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(), // FEHLT: fromMe: Boolean(msg.key?.fromMe) // FEHLT: senderName: extractSenderName(msg) }; }
2. formatInboundEnvelope (Beeinflusst LLM nicht)
javascript
// Diese Funktion modifiziert Body, das NICHT vom LLM empfangen wird
function formatInboundEnvelope(params) {
const selfMarker = params.fromMe ? “[You]: " : “”;
return {
Body: ${selfMarker}${params.body},
// BodyForAgent wird HIER NICHT gesetzt — LLM empfängt den rohen Wert
};
}
3. finalizeInboundContext-Aufrufer (Verwerfungspunkt #2)
Die finale Transformation, die BodyForAgent erstellt, enthält keine Sender-Präfix-Logik:
javascript // Aktuelle Implementierung const BodyForAgent = params.msg.body; // Roher Text, keine Zuordnung
Warum Gruppennachrichten korrekt funktionieren
Gruppennachrichten enthalten von Natur aus Absenderzuordnung, da das Nachrichtenformat bereits den Absendernamen enthält:
javascript // Gruppennachrichten haben bereits diese Struktur vom Protokoll: “[GroupName] @Alice: message content” // oder “[Alice]: message content”
DM-Nachrichten fehlt dieses strukturelle Präfix, was die Absenderidentifikation ohne explizite Behandlung unmöglich macht.
Versions-Regressionsanalyse
| Version | LLM-Eingabe | Verhalten |
|---|---|---|
| < v2026.3.1 | Body | Konnte durch formatInboundEnvelope modifiziert werden |
| ≥ v2026.3.1 | BodyForAgent | Kann nicht durch formatInboundEnvelope modifiziert werden |
Der Refactor in v2026.3.1 führte ein direktes BodyForAgent-Feld ein, das die formatInboundEnvelope-Transformation umgeht und diese Lücke verursacht.
🛠️ Schritt-für-Schritt-Lösung
Diese Lösung erfordert Änderungen an fünf Funktionen in der eingehenden Pipeline. Wenden Sie die Änderungen in der angegebenen Reihenfolge an, um die Datenintegrität zu wahren.
Phase 1: fromMe durch die Pipeline propagieren
Schritt 1.1: fromMe zur inboundMessage-Konstruktion hinzufügen
Datei: src/core/message/inbound-message.js
Vorher: 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(), // FEHLT }; }
Nachher: 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), // HINZUFÜGEN: fromMe-Flag propagieren senderName: msg.pushName || msg.sender?.first_name || msg.sender?.username || “Unknown”, // HINZUFÜGEN: Absendername extrahieren }; }
Schritt 1.2: fromMe durch processMessage übergeben
Datei: src/core/message/process-message.js
Vorher: javascript async function processMessage(msg, chatId, context) { const inbound = inboundMessage(msg, chatId);
// … andere Verarbeitung …
await buildInboundLine(inbound, context); }
Nachher: javascript async function processMessage(msg, chatId, context) { const inbound = inboundMessage(msg, chatId);
// … andere Verarbeitung …
await buildInboundLine(inbound, context, { fromMe: inbound.fromMe }); }
Schritt 1.3: fromMe in buildInboundLine destrukturieren und weiterleiten
Datei: src/core/message/build-inbound-line.js
Vorher: javascript async function buildInboundLine(inbound, context) { const { body, from, pushName, chatType, timestamp } = inbound;
// … Verarbeitung …
await formatInboundEnvelope({ msg: { body, from, pushName, chatType, timestamp }, conversation: context.conversation, }); }
Nachher: javascript async function buildInboundLine(inbound, context, options = {}) { const { body, from, pushName, chatType, timestamp, fromMe, senderName } = inbound;
// … Verarbeitung …
await formatInboundEnvelope({ msg: { body, from, pushName, chatType, timestamp, fromMe, senderName }, conversation: context.conversation, fromMe: options.fromMe ?? fromMe, }); }
Schritt 1.4: Selbstmarkierung zu formatInboundEnvelope hinzufügen (Für Body)
Datei: src/core/message/format-inbound-envelope.js
Vorher: javascript function formatInboundEnvelope(params) { return { Body: params.msg.body, // … andere Felder }; }
Nachher: javascript function formatInboundEnvelope(params) { const selfMarker = params.fromMe ? “[You]: " : “”;
return {
Body: ${selfMarker}${params.msg.body},
// … andere Felder
};
}
Phase 2: Sender-Präfix zu BodyForAgent hinzufügen
Schritt 2.1: finalizeInboundContext-Aufrufer modifizieren
Datei: src/core/context/finalize-inbound-context.js
Vorher: javascript function finalizeInboundContext(params) { // … andere Verarbeitung …
const BodyForAgent = params.msg.body;
return { // … andere Felder BodyForAgent, }; }
Nachher: javascript function finalizeInboundContext(params) { // … andere Verarbeitung …
// Sender-Präfix nur für DMs hinzufügen; Gruppennachrichten haben bereits Zuordnung
const dmPrefix = params.msg.chatType !== “group”
? [${params.msg.senderName || params.msg.from || "Unknown"}]: : “”;
const BodyForAgent = ${dmPrefix}${params.msg.body};
return { // … andere Felder BodyForAgent, }; }
Phase 3: Verifizierungs-Checkliste
Nach dem Anwenden aller Änderungen verifizieren Sie die folgenden Modifikationen:
| Datei | Änderung | Verifizierung |
|---|---|---|
inbound-message.js | fromMe und senderName-Felder hinzugefügt | Prüfen Sie, dass inbound.fromMe ein Boolean ist |
process-message.js | Übergibt fromMe an buildInboundLine | Prüfen Sie, dass drittes Argument vorhanden ist |
build-inbound-line.js | Destrukturiert und leitet fromMe, senderName weiter | Prüfen Sie, dass Parameter korrekt übergeben werden |
format-inbound-envelope.js | Fügt [You]:-Selbstmarkierung zu Body hinzu | Prüfen Sie Body-Format in Logs |
finalize-inbound-context.js | Fügt [Name]:-Präfix für DMs hinzu | Prüfen Sie BodyForAgent-Format |
🧪 Verifizierung
Verifizierungsmethode 1: Unit-Test-Validierung
Erstellen und führen Sie den folgenden Test aus, um die Propagierungskette zu validieren:
// 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');
});
});
Führen Sie die Tests aus:
npm test -- test/inbound-propagation.test.jsErwartete Ausgabe:
✓ 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 trueVerifizierungsmethode 2: Integrationstest mit 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) => { // Simuliere eingehende DM von Alice handler({ key: { fromMe: false }, body: “Hey, are you free tonight?”, from: “+1111111111”, pushName: “Alice”, chatType: “direct”, timestamp: Date.now(), });
// Simuliere ausgehende 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();
// Erfasse die LLM-Eingabe const llmInput = captureLLMInput();
console.log(‘LLM empfing BodyForAgent:’); console.log(llmInput.BodyForAgent);
// Erwartete Ausgabe: // [Alice]: Hey, are you free tonight? // [Agent]: Yes, I’ll be there at 8
await agent.stop(); }
testDMSenderAttribution();
Verifizierungsmethode 3: Manuelles Debug-Logging
Aktivieren Sie ausführliches Logging, um die gesamte Pipeline zu inspizieren:
# Umgebungsvariablen setzen
export DEBUG=openclaw:inbound,openclaw:context
export LOG_LEVEL=debug
# Agent ausführen
node agent.js 2>&1 | grep -E "(fromMe|senderName|BodyForAgent|\[.*\]:)"
# Erwartete Log-Ausgabe für DM-Nachrichten:
# [debug] inboundMessage.fromMe: false
# [debug] inboundMessage.senderName: "Alice"
# [debug] BodyForAgent: "[Alice]: Hey, are you free tonight?"Verifizierungsmethode 4: Datenbankzustands-Inspektion
Wenn Sie persistente Kontexte verwenden, verifizieren Sie die gespeicherten Nachrichten:
# Tabelle messages abfragen
SELECT id, sender_name, from_me, body, body_for_agent
FROM messages
WHERE chat_type = 'direct'
ORDER BY timestamp DESC LIMIT 5;
-- Erwartetes Ergebnis:
-- | 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 |⚠️ Häufige Fehler
Fehler 1: Protokoll-Adapter-Feldnamensabweichung
Problem: Verschiedene Messaging-Plattformen exponieren fromMe unter unterschiedlichen Feldnamen.
- WhatsApp:
msg.key.fromMe - Telegram:
msg.from.is_bot(invertierte Logik) odermsg.outgoing - Signal:
msg.direction === "outgoing" - Discord:
msg.author.id === msg.client.user.id
Abhilfe: Erstellen Sie adapter-spezifische Feld-Mapper im inboundMessage-Konstruktor:
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; } }
Fehler 2: Fehlende senderName-Fallback-Kette
Problem: pushName ist möglicherweise nicht verfügbar (Benutzer hat Datenschutzeinstellungen aktiviert oder erste Nachricht bevor Push-Name gecacht ist).
Abhilfe: Implementieren Sie eine robuste Fallback-Kette:
javascript function extractSenderName(msg) { return ( msg.pushName || msg.sender?.first_name || msg.sender?.username || msg.from?.split(’@’)[0] || // JID/Telefonnummer als letzten Ausweg verwenden “Unknown” ); }
Fehler 3: Doppeltes Präfix in Gruppennachrichten
Problem: Wenn Gruppennachrichten bereits Absenderzuordnung im Protokoll-Payload enthalten, erzeugt das Hinzufügen eines weiteren Präfix eine Duplizierung.
Beispiel einer schlechten Ausgabe:
[#general] @Alice: [Alice]: Message content // Doppelte Zuordnung
Abhilfe: Überprüfen Sie das vorhandene Nachrichtenformat, bevor Sie ein Präfix hinzufügen:
javascript function shouldAddPrefix(msg, chatType) { if (chatType !== ‘direct’) { // Überprüfen, ob Gruppennachricht bereits Zuordnungsmuster hat const existingPattern = /^[[^]]+]\s*@\w+:/; return !existingPattern.test(msg.body); } return true; }
Fehler 4: Plattformspezifische Absendernamensformate
Problem: Verschiedene Plattformen formatieren Absendernamen unterschiedlich.
- WhatsApp: Anzeigename (z.B. "John Smith")
- Telegram: Vorname + optionaler Nachname
- Discord: Benutzername + Diskriminator (z.B. "User#1234")
Abhilfe: Normalisieren Sie Absendernamen vor der Verwendung in Präfixen:
javascript function normalizeSenderName(name, platform) { if (!name) return “Unknown”;
let normalized = name.trim();
if (platform === ‘discord’) { // Diskriminator entfernen, falls vorhanden normalized = normalized.split(’#’)[0]; }
// Sonderzeichen entfernen, die das Parsen unterbrechen könnten return normalized.replace(/[[]]/g, ‘’).substring(0, 50); }
Fehler 5: Versionskompatibilität nach v2026.3.1
Problem: Wenn der Code bedingte Logik basierend darauf enthält, ob BodyForAgent oder Body verwendet wird, wird die Lösung möglicherweise nicht einheitlich angewendet.
Abhilfe: Verifizieren Sie, welches Feld der LLM-Adapter tatsächlich konsumiert:
javascript // Überprüfen Sie Ihre LLM-Adapter-Konfiguration const llmAdapter = config.llm?.adapter;
// Wenn Body direkt verwendet wird (altes Verhalten), gilt die formatInboundEnvelope-Lösung // Wenn BodyForAgent verwendet wird (neues Verhalten), gilt die finalizeInboundContext-Lösung // Einige Adapter verwenden möglicherweise beide — stellen Sie Konsistenz sicher
Fehler 6: Race Condition bei paralleler Nachrichtenverarbeitung
Problem: Bei der gleichzeitigen Verarbeitung mehrerer DMs kann der fromMe-Zustand veraltet oder falsch mit einem anderen Nachrichtenkontext assoziiert sein.
Abhilfe: Stellen Sie sicher, dass fromMe zum Konstruktionszeitpunkt an den spezifischen Nachrichtenkontext gebunden ist, nicht global abgerufen wird:
javascript // FALSCH: Globaler Zustandsverweis inboundMessage.fromMe = globalLastMessageFromMe; // Race condition
// KORREKT: Nachrichtenspezifische Extraktion inboundMessage.fromMe = Boolean(msg.key?.fromMe); // Isoliert pro Nachricht
🔗 Zugehörige Fehler
Zugehöriges Problem #32060: Ausgehende DMs in Kontext einbeziehen
Symptom: Der Agent empfängt nur eingehende DMs, aber keine ausgehenden Nachrichten, die vom Agenten selbst gesendet wurden, was Konversationen einseitig erscheinen lässt.
Zusammenhang: Dieses Problem ist das Komplement zur aktuellen Lösung. Während #32060 sicherstellt, dass ausgehende Nachrichten im Kontext gespeichert werden, stellt diese Lösung sicher, dass sowohl eingehende als auch ausgehende Nachrichten eine ordnungsgemäße Absenderzuordnung haben.
Lösungsabhängigkeit: Die fromMe-Propagierung, die in dieser Lösung implementiert wurde, ist für die ordnungsgemäße Implementierung von #32060 erforderlich.
Zugehöriges Problem #32059: DM-Kontextfenster-Überlauf
Symptom: Lange DM-Konversationen verbrauchen übermäßig viele Kontextfenster-Token, da jede Nachricht eine effiziente Absenderidentifikation vermissen lässt.
Zusammenhang: Das [Name]:-Präfixformat, das durch diese Lösung eingeführt wurde, ist absichtlich prägnant, um den Token-Overhead zu minimieren und gleichzeitig die notwendige Zuordnung bereitzustellen.
Zugehöriger Fehler: undefined senderName in BodyForAgent
Fehlermuster:
TypeError: Cannot read property 'senderName' of undefined
at finalizeInboundContext (finalize-inbound-context.js:42)
at processMessage (process-message.js:87)Ursache: Auf senderName wird zugegriffen, bevor die inboundMessage-Propagierung abgeschlossen ist.
Lösung: Stellen Sie sicher, dass die inboundMessage-Konstruktion das senderName-Feld enthält, bevor finalizeInboundContext aufgerufen wird.
Zugehörige Warnung: fromMe is not a boolean
Warnungsmuster:
Warning: BodyForAgent received non-boolean fromMe value: "true"
Warning: Self-marker logic may behave unexpectedlyUrsache: fromMe wurde als String (“true”/“false”) statt als Boolean gespeichert.
Lösung: Stellen Sie die Boolean(msg.key?.fromMe)-Koersion im inboundMessage-Konstruktor sicher.
Zugehörige Konfiguration: dm.senderPrefix.enabled
Konfigurationsschlüssel: openclaws.dm.senderPrefix.enabled
Zweck: Sender-Präfix in DMs zum Testen oder für Benutzerpräferenzen umschalten.
Standard: true
Interaktion: Wenn deaktiviert, kehren DMs zum mehrdeutigen Format zurück; die fromMe-Propagierung funktioniert weiterhin für andere Zwecke (Analytik, Filterung).
Zugehörige Log-Kategorie: openclaw:inbound:fromme
Debug-Flag: DEBUG=openclaw:inbound:fromme
Ausgabe: Loggt fromMe-Wert in jedem Pipeline-Stadium, nützlich zum Nachverfolgen von Propagierungsfehlern.
Historischer Hinweis: Verhalten vor v2026.3.1
Kontext: Vor v2026.3.1 verwendete das Framework Body (modifiziert durch formatInboundEnvelope) als LLM-Eingabe. Eine dort angewendete Selbstmarkierungs-Lösung wäre für das LLM sichtbar gewesen.
Änderung: v2026.3.1 führte BodyForAgent als separates Feld ein, das die Envelope-Transformation umgeht.
Auswirkung: Diese architektonische Änderung schuf die Propagierungslücke, die diese Lösung adressiert.