April 22, 2026 • バージョン: v2026.3.1

[LLMコンテキストでDMメッセージに送信者属性がない] - DM Messages Lack Sender Attribution in LLM Context (BodyForAgent)

ダイレクトメッセージの会話は、送信者識別情報が受信パイプラインを通じて伝播されておらず、BodyForAgentに送信者プレフィックスがないため、LLMにとっては区別のないテキストストリームとして表示されます。

🔍 症状

主な症状

エージェントがDMの会話を処理する際、LLMは送信者コンテキストなしでメッセージを受信します。以下のDM交換を考えてみましょう:

LLMが実際に受信するもの(現在の動作):

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

LLMが受信すべきもの(期待される動作):

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

技術的観察

fromMeフラグはプロトコルアダプターレベルで存在しますが、LLMでは利用できません:

// アダプターレベル(WhatsAppの例):
msg.key.fromMe  // Boolean - 送信者を正しく識別

// LLM入力レベル(BodyForAgent):
params.msg.body  // "Hey, are you free tonight?" — 送信者コンテキストなし

診断コマンド

inboundMessageの現在の状態を検査するには:

# 受信メッセージ構造を観察するためにデバッグログを有効にする
DEBUG=openclaw:inbound node agent.js

# 欠落しているフィールドを示す予想デバッグ出力:
# inboundMessage {
#   body: "Hey, are you free tonight?",
#   from: "+1234567890",
#   pushName: "Alice",
#   chatType: "direct",
#   // fromMe: undefined  ← 欠落
#   // senderName: undefined  ← 伝播されていない
# }

バージョン固有の動作

この問題は、フレームワークがLLMにBodyを渡すことからBodyForAgentを渡すように変更されたため、v2026.3.1以降で具体的に発生します。formatInboundEnvelopeへの変更はBodyのみに影響し、LLMには見えません。

🧠 原因

アーキテクチャ上のギャップ:欠落している伝播パス

根本原因是、プロトコルアダプターとLLMコンテキスト間のデータフローの途絶です。fromMeフラグとsenderNameはパイプラインの早い段階で利用できますが、BodyForAgentへの整个チェーンを通じて伝播されません。

データフロー分析

┌─────────────────────────────────────────────────────────────────────────┐ │ 現在のデータフロー │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ プロトコルアダプター │ │ ├── msg.key.fromMe = true/false ✓ 利用可能 │ │ ├── msg.pushName = “Alice” ✓ 利用可能 │ │ └── msg.chatType = “direct” ✓ 利用可能 │ │ │ │ │ ▼ │ │ inboundMessage コンストラクタ │ │ └── fromMe フィールドが追加されていない ✗ ここでドロップ │ │ │ │ │ ▼ │ │ processMessage → buildInboundLine → formatInboundEnvelope │ │ └── fromMe は依然として undefined ✗ 転送されていない │ │ │ │ │ ▼ │ │ finalizeInboundContext 呼び出し元 │ │ └── BodyForAgent に [senderName]: プレフィックスがない ✗ LLMは曖昧なデータを受信 │ │ └─────────────────────────────────────────────────────────────────────────┘

コードレベルの分析

1. inboundMessage 構築(ドロップポイント #1)

javascript // 現在の実装 - fromMe フィールドが欠落 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) // 欠落: senderName: extractSenderName(msg) }; }

2. formatInboundEnvelope(LLMには影響しない)

javascript // この関数は Body を修正するが、LLMが受信するのは Body ではない function formatInboundEnvelope(params) { const selfMarker = params.fromMe ? “[You]: " : “”; return { Body: ${selfMarker}${params.body}, // BodyForAgent はここで設定されない — LLMは生の値を受け取る }; }

3. finalizeInboundContext 呼び出し元(ドロップポイント #2)

BodyForAgentを構築する最終変換には、送信者プレフィックスロジックが含まれていません:

javascript // 現在の実装 const BodyForAgent = params.msg.body; // 生テキスト、帰属情報なし

グループメッセージが正しく動作する理由

グループメッセージは、メッセージフォーマットに既に送信者名が含まれているため、帰属情報を自然に含んでいます:

javascript // グループメッセージはプロトコルから既にこの構造を持っている: “[GroupName] @Alice: message content” // または “[Alice]: message content”

DMメッセージにはこの構造的なプレフィックスがないため、明示的な処理なしでは送信者識別ができません。

バージョン回帰分析

バージョンLLM入力動作
< v2026.3.1BodyformatInboundEnvelope で修正可能だった
≥ v2026.3.1BodyForAgentformatInboundEnvelope で修正不可

v2026.3.1のリファクタリングにより、formatInboundEnvelope変換をバイパスする直接的なBodyForAgentフィールドが導入され、このギャップが発生しました。

🛠️ 解決手順

この修正には、受信パイプライン全体の5つの関数への変更が必要です。データ整合性を維持するため、記載された順序で変更を適用してください。

フェーズ1:パイプライン全体のfromMe伝播

Step 1.1: inboundMessage構築にfromMeを追加

ファイル: src/core/message/inbound-message.js

変更前: 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(), // 欠落 }; }

変更後: 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), // 追加: fromMe フラグを伝播 senderName: msg.pushName || msg.sender?.first_name || msg.sender?.username || “Unknown”, // 追加: 送信者名抽出 }; }

Step 1.2: processMessagefromMeを渡す

ファイル: src/core/message/process-message.js

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

// … 他の処理 …

await buildInboundLine(inbound, context); }

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

// … 他の処理 …

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

Step 1.3: buildInboundLinefromMeをデструкクトして転送

ファイル: src/core/message/build-inbound-line.js

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

// … 処理 …

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

変更後: javascript async function buildInboundLine(inbound, context, options = {}) { const { body, from, pushName, chatType, timestamp, fromMe, senderName } = inbound;

// … 処理 …

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

Step 1.4: formatInboundEnvelopeにセルフマーカーを追加(Body用)

ファイル: src/core/message/format-inbound-envelope.js

変更前: javascript function formatInboundEnvelope(params) { return { Body: params.msg.body, // … 他のフィールド }; }

変更後: javascript function formatInboundEnvelope(params) { const selfMarker = params.fromMe ? “[You]: " : “”;

return { Body: ${selfMarker}${params.msg.body}, // … 他のフィールド }; }

フェーズ2:BodyForAgentに送信者プレフィックスを追加

Step 2.1: finalizeInboundContext呼び出し元を修正

ファイル: src/core/context/finalize-inbound-context.js

変更前: javascript function finalizeInboundContext(params) { // … 他の処理 …

const BodyForAgent = params.msg.body;

return { // … 他のフィールド BodyForAgent, }; }

変更後: javascript function finalizeInboundContext(params) { // … 他の処理 …

// DMのみに送信者プレフィックスを追加;グループメッセージは既に帰属情報を持つ const dmPrefix = params.msg.chatType !== “group” ? [${params.msg.senderName || params.msg.from || "Unknown"}]: : “”; const BodyForAgent = ${dmPrefix}${params.msg.body};

return { // … 他のフィールド BodyForAgent, }; }

フェーズ3:検証チェックリスト

すべての変更を適用した後、以下の修正を確認してください:

ファイル変更検証項目
inbound-message.jsfromMesenderNameフィールドを追加inbound.fromMeがbooleanであることを確認
process-message.jsfromMebuildInboundLineに渡す第3引数が存在することを確認
build-inbound-line.jsfromMesenderNameをデストラクトして転送パラメータが正しく渡されていることを確認
format-inbound-envelope.jsBodyに[You]:セルフマーカーを追加ログでBody形式を確認
finalize-inbound-context.jsDMに[Name]:プレフィックスを追加BodyForAgent形式を確認

🧪 検証

検証方法1:ユニットテスト検証

伝播チェーンを検証するために以下のテストを作成して実行します:

// 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');
  });
});

テストを実行:

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

予想される出力:

✓ 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

検証方法2:モックプロトコルアダプターを使った統合テスト

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) => { // Aliceからの受信DMをシミュレート handler({ key: { fromMe: false }, body: “Hey, are you free tonight?”, from: “+1111111111”, pushName: “Alice”, chatType: “direct”, timestamp: Date.now(), });

  // 送信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();

// LLM入力をキャプチャ const llmInput = captureLLMInput();

console.log(‘LLMが受信したBodyForAgent:’); console.log(llmInput.BodyForAgent);

// 予想される出力: // [Alice]: Hey, are you free tonight? // [Agent]: Yes, I’ll be there at 8

await agent.stop(); }

testDMSenderAttribution();

検証方法3:手動デバッグログ

フルパイプラインを検査するために詳細ログを有効にします:

# 環境変数を設定
export DEBUG=openclaw:inbound,openclaw:context
export LOG_LEVEL=debug

# エージェントを実行
node agent.js 2>&1 | grep -E "(fromMe|senderName|BodyForAgent|\[.*\]:)"

# DMメッセージの予想ログ出力:
# [debug] inboundMessage.fromMe: false
# [debug] inboundMessage.senderName: "Alice"
# [debug] BodyForAgent: "[Alice]: Hey, are you free tonight?"

検証方法4:データベース状態検査

永続コンテキストを使用している場合、保存されたメッセージを確認します:

# messagesテーブルをクエリ
SELECT id, sender_name, from_me, body, body_for_agent 
FROM messages 
WHERE chat_type = 'direct' 
ORDER BY timestamp DESC LIMIT 5;

-- 予想される結果:
-- | 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                |

⚠️ よくある落とし穴

落とし穴1:プロトコルアダプターでのフィールド名の差異

問題: 異なるメッセージングプラットフォームでfromMeが様々なフィールド名で公開されています。

  • WhatsApp: msg.key.fromMe
  • Telegram: msg.from.is_bot(ロジック反転)または msg.outgoing
  • Signal: msg.direction === "outgoing"
  • Discord: msg.author.id === msg.client.user.id

対策: 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; } }

落とし穴2:欠落しているsenderNameフォールバックチェーン

問題: pushNameが利用できない場合があります(プライバシー設定が有効なユーザー、またはプッシュ名がキャッシュされる前の最初のメッセージ)。

**対策:**堅牢なフォールバックチェーンを実装します:

javascript function extractSenderName(msg) { return ( msg.pushName || msg.sender?.first_name || msg.sender?.username || msg.from?.split(’@’)[0] || // 最後の手段としてJID/電話番号を使用 “Unknown” ); }

落とし穴3:グループメッセージでの二重プレフィックス

問題: グループメッセージがプロトコルペイロードに既に送信者帰属情報を含んでいる場合、別のプレフィックスを追加すると重複が発生します。

悪い出力の例:

[#general] @Alice: [Alice]: Message content // 二重帰属

対策: プレフィックスを追加する前に既存のメッセージ形式を確認します:

javascript function shouldAddPrefix(msg, chatType) { if (chatType !== ‘direct’) { // グループメッセージが既に帰属パターンを持っているか確認 const existingPattern = /^[[^]]+]\s*@\w+:/; return !existingPattern.test(msg.body); } return true; }

落とし穴4:プラットフォーム固有の送信者名形式

問題: 異なるプラットフォームで送信者名の形式が異なります。

  • WhatsApp: 表示名(例:"John Smith")
  • Telegram: 名 + オプションの姓
  • Discord: ユーザー名 + 識別子(例:"User#1234")

対策: プレフィックスで使用する前に送信者名を正規化します:

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

let normalized = name.trim();

if (platform === ‘discord’) { // 識別子が 있으면削除 normalized = normalized.split(’#’)[0]; }

// 解析を壊す可能性のある特殊文字を削除 return normalized.replace(/[[]]/g, ‘’).substring(0, 50); }

落とし穴5:v2026.3.1以降のバージョン互換性

問題: BodyForAgentBodyかの使用に基づいて条件付きロジックがある場合、修正が均一に適用されない可能性があります。

対策: LLMアダプターが実際に消費するフィールドを確認します:

javascript // LLMアダプター設定を確認 const llmAdapter = config.llm?.adapter;

// Body を直接使用(古い動作)の場合、formatInboundEnvelope の修正が適用される // BodyForAgent を使用(新しい動作)の場合、finalizeInboundContext の修正が適用される // 一部のアダプターは両方を使用する場合がある — 一貫性を確保

落とし穴6:パラレルメッセージ処理での競合状態

問題: 複数のDMを同時に処理する際、fromMe状態が古くなっているか、異なるメッセージコンテキストに誤って関連付けられている可能性があります。

対策: fromMeが構築時にグローバルに取得されるのではなく、特定のメッセージコンテキストにバインドされていることを確認します:

javascript // 誤り:グローバル状態参照 inboundMessage.fromMe = globalLastMessageFromMe; // 競合状態

// 正しい:メッセージ固有の抽出 inboundMessage.fromMe = Boolean(msg.key?.fromMe); // メッセージごとに分離

🔗 関連するエラー

関連問題 #32060: 送信DMをコンテキストに含める

症状: エージェントは受信DMのみを受け取り、エージェント自体が送信した送信メッセージを受け取らないため、会話を一方的に見えます。

関連性: この問題は現在の修正の補完です。#32060は送信メッセージをコンテキストに保存することを保証しますが、この修正は受信と送信の両方のメッセージが適切な送信者帰属情報を持つことを保証します。

解決依存性: この修正で実装されたfromMe伝播は、#32060の適切な実装に必要です。


関連問題 #32059: DMコンテキストウィンドウオーバーフロー

症状: 長いDM会話は、各メッセージが必要な帰属識別子を失うため、過度のコンテキストウィンドウトークンを消費します。

関連性: この修正で導入された[Name]:プレフィックス形式は、必要な帰属情報を提供しながらトークンオーバーヘッドを最小限に抑えるように意図的に簡潔です。


関連エラー: BodyForAgentでsenderNameがundefined

エラーパターン:

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

原因: inboundMessage伝播が完了する前にsenderNameにアクセスされています。

解決: finalizeInboundContextが呼び出される前にinboundMessage構築にsenderNameフィールドが含まれていることを確認します。


関連する警告: fromMeはbooleanではありません

警告パターン:

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

原因: fromMeがbooleanではなく文字列(“true”/“false”)として保存されていました。

解決: inboundMessageコンストラクタでBoolean(msg.key?.fromMe)の強制変換を確保します。


関連設定: dm.senderPrefix.enabled

設定キー: openclaws.dm.senderPrefix.enabled

目的: テストまたはユーザー設定のためにDMでの送信者プレフィックスを切り替え。

デフォルト: true

相互作用: 無効にすると、DMは曖昧な形式に戻ります;fromMe伝播は他の目的(分析、フィルタリング)では引き続き機能します。


関連ログカテゴリ: openclaw:inbound:fromme

デバッグフラグ: DEBUG=openclaw:inbound:fromme

出力: 各パイプラインテンポでのfromMe値をログ出力し、伝播失敗のトレースに有用。


歴史的メモ:v2026.3.1以前の動作

コンテキスト: v2026.3.1以前、フレームワークはLLM入力としてBodyformatInboundEnvelopeで修正)を使用していました。セルフマーカー修正がそこで適用されていれば、LLMに見えていたでしょう。

変更: v2026.3.1はBodyForAgentを別フィールドとして導入し、エンベロープ変換をバイパスしました。

影響: このアーキテクチャ変更により、この修正が対処する伝播ギャップが発生しました。

エビデンスとソース

このトラブルシューティングガイドは、FixClaw Intelligence パイプラインによってコミュニティの議論から自動的に合成されました。