openclaw - ✅(Solved) Fix [Bug]: iMessage outbound messages misclassified as self-chat, bypassing 'from me' drop [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#60014Fetched 2026-04-08 02:37:24
View on GitHub
Comments
0
Participants
1
Timeline
6
Reactions
0
Author
Participants
Timeline (top)
referenced ×4cross-referenced ×2

When OpenClaw's iMessage channel sends an outbound message to a contact, the RPC watch notification for that sent message has is_from_me: true but is incorrectly classified as a "self-chat" by handleInboundIMessage(). This causes the message to bypass the simple "from me" drop path and instead enter the echo cache check, where the 4-second text TTL frequently expires before the notification arrives — allowing the agent's own sent messages to be re-ingested as inbound messages.

Root Cause

In the monitor provider (monitor-provider-*.js), the isSelfChat check compares sender against chatIdentifier:

const isSelfChat = !isGroup && chatIdentifier != null && 
    normalizeIMessageHandle(sender) === normalizeIMessageHandle(chatIdentifier);

For outbound messages in a DM, the RPC notification sets both sender and chat_identifier to the recipient's handle (e.g., +1XXXXXXXXXX), not the bot's own handle. This makes isSelfChat evaluate to true even though the bot's destination_caller_id is a completely different address (e.g., [email protected]).

When is_from_me && isSelfChat, the code enters the echo cache path instead of the unconditional "from me" drop:

if (params.message.is_from_me) {
    params.selfChatCache?.remember(selfChatLookup);
    if (isSelfChat) {
        // Echo cache check — misses if TTL (4s) expired
        // Falls through if no cache hit!
        skipSelfChatHasCheck = true;
    } else {
        return { kind: "drop", reason: "from me" };  // ← never reached
    }
}

PR fix notes

PR #60386: fix(imessage): prevent outbound DM messages from being misclassified as self-chat

Description (problem / solution / changelog)

Summary

Outbound messages in iMessage DMs are misclassified as "self-chat", bypassing the unconditional is_from_me drop and falling through to the echo cache path. When the echo cache TTL expires, the agent's own sent messages are re-ingested as inbound messages, creating echo loops.

Root Cause

On outbound DM messages, the iMessage RPC notification sets both sender and chat_identifier to the recipient's handle (e.g., +1XXXXXXXXXX), not the bot's own handle. This makes the self-chat heuristic (sender === chatIdentifier) evaluate to true.

When is_from_me && isSelfChat, the code enters the echo cache path instead of the unconditional "from me" drop at line 258. If the 4-second text TTL has expired, the message passes through as a new inbound message.

Fix

Check destination_caller_id (the bot's own handle) when present. If it differs from sender, this is a normal outbound DM — not a self-chat. The message correctly routes to the "from me" drop path.

This preserves all existing self-chat behavior:

  • Genuine self-chat (user messaging themselves): destination_caller_id matches senderisSelfChat = true → echo cache logic unchanged
  • Outbound DM (bot sending to contact): destination_caller_id differs from senderisSelfChat = false → dropped via "from me"
  • destination_caller_id absent: falls back to existing behavior (senderMatchesChatId only)

Reproduction

  1. Configure iMessage channel where the bot's iCloud account differs from the contact's handle
  2. Send a message via the agent to the contact
  3. Observe the RPC watch notification has sender === chat_identifier (both are recipient's handle)
  4. If echo cache TTL has expired, the message passes through as inbound
  5. Agent responds to its own message, creating an echo loop

Fixes #60014

Changed files

  • extensions/imessage/src/monitor/inbound-processing.test.ts (modified, +92/-0)
  • extensions/imessage/src/monitor/inbound-processing.ts (modified, +15/-1)
  • extensions/imessage/src/monitor/parse-notification.ts (modified, +1/-0)
  • extensions/imessage/src/monitor/types.ts (modified, +1/-0)

PR #62191: fix(imessage): strip U+FFFD garbage chars from echo text key normalization

Description (problem / solution / changelog)

Summary

U+FFFD replacement characters and C0/C1 control characters injected by imsg when extracting text from NSAttributedString (attributedBody column) break echo cache text-key matching, causing duplicate message delivery.

The echo cache stores clean outbound text, but the reflected inbound copy carries garbage character prefixes — normalizeEchoTextKey() only strips \r\n, so the keys never match and the echo slips through as a new inbound message.

Fix: Strip [\ufffd\ufffe\uffff\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]+ before comparison in normalizeEchoTextKey().

Previously included fixes (now upstream)

The original PR included two additional fixes that have since been merged independently by the OpenClaw team (#61619, #63868, #63980, #63989, #64000):

  • TTL 4s → 30s — shipped in 2026.4.10 (SENT_MESSAGE_TEXT_TTL_MS = 30e3)
  • destination_caller_id self-chat detection — shipped in 2026.4.10 with isSelfChat + isAmbiguousSelfThread

This PR now contains only the remaining unmerged fix.

Changes

  • extensions/imessage/src/monitor/echo-cache.ts: Strip U+FFFD/control chars in normalizeEchoTextKey

Related Issues

Fixes #61312, #61821

Test Plan

  • Confirmed echo cache miss on messages with U+FFFD prefixes (unpatched)
  • Applied patch to compiled dist — garbage-prefixed echoes now correctly matched and suppressed
  • Clean text (no U+FFFD) still matches normally — no regression

Changed files

  • extensions/imessage/src/monitor/echo-cache.ts (modified, +8/-1)

Code Example

const isSelfChat = !isGroup && chatIdentifier != null && 
    normalizeIMessageHandle(sender) === normalizeIMessageHandle(chatIdentifier);

---

if (params.message.is_from_me) {
    params.selfChatCache?.remember(selfChatLookup);
    if (isSelfChat) {
        // Echo cache check — misses if TTL (4s) expired
        // Falls through if no cache hit!
        skipSelfChatHasCheck = true;
    } else {
        return { kind: "drop", reason: "from me" };  // ← never reached
    }
}

---

{
  "message": {
    "is_from_me": true,
    "sender": "+1XXXXXXXXXX",
    "chat_identifier": "+1XXXXXXXXXX", 
    "destination_caller_id": "[email protected]",
    "text": "test message"
  }
}

---

const _senderMatchesChatId = !isGroup && chatIdentifier != null && 
    normalizeIMessageHandle(sender) === normalizeIMessageHandle(chatIdentifier);
const _destCallerId = params.message.destination_caller_id;
const _destMismatch = _destCallerId && 
    normalizeIMessageHandle(sender) !== normalizeIMessageHandle(_destCallerId);
const isSelfChat = _senderMatchesChatId && !_destMismatch;
RAW_BUFFERClick to expand / collapse

Summary

When OpenClaw's iMessage channel sends an outbound message to a contact, the RPC watch notification for that sent message has is_from_me: true but is incorrectly classified as a "self-chat" by handleInboundIMessage(). This causes the message to bypass the simple "from me" drop path and instead enter the echo cache check, where the 4-second text TTL frequently expires before the notification arrives — allowing the agent's own sent messages to be re-ingested as inbound messages.

Root Cause

In the monitor provider (monitor-provider-*.js), the isSelfChat check compares sender against chatIdentifier:

const isSelfChat = !isGroup && chatIdentifier != null && 
    normalizeIMessageHandle(sender) === normalizeIMessageHandle(chatIdentifier);

For outbound messages in a DM, the RPC notification sets both sender and chat_identifier to the recipient's handle (e.g., +1XXXXXXXXXX), not the bot's own handle. This makes isSelfChat evaluate to true even though the bot's destination_caller_id is a completely different address (e.g., [email protected]).

When is_from_me && isSelfChat, the code enters the echo cache path instead of the unconditional "from me" drop:

if (params.message.is_from_me) {
    params.selfChatCache?.remember(selfChatLookup);
    if (isSelfChat) {
        // Echo cache check — misses if TTL (4s) expired
        // Falls through if no cache hit!
        skipSelfChatHasCheck = true;
    } else {
        return { kind: "drop", reason: "from me" };  // ← never reached
    }
}

Reproduction

  1. Configure iMessage channel where the bot's iCloud account differs from the contact's handle
  2. Send a message via the message tool to the contact
  3. Observe the RPC watch notification:
{
  "message": {
    "is_from_me": true,
    "sender": "+1XXXXXXXXXX",
    "chat_identifier": "+1XXXXXXXXXX", 
    "destination_caller_id": "[email protected]",
    "text": "test message"
  }
}
  1. isSelfChat = true (sender === chat_identifier)
  2. If echo cache TTL has expired, the message passes through as a new inbound message
  3. Agent responds to its own message, creating an echo loop

Expected Behavior

Outbound messages with is_from_me: true should always be dropped when the bot is not in an actual self-chat scenario.

Suggested Fix

Include destination_caller_id in the self-chat determination. When present and different from sender, it proves this is NOT a self-chat:

const _senderMatchesChatId = !isGroup && chatIdentifier != null && 
    normalizeIMessageHandle(sender) === normalizeIMessageHandle(chatIdentifier);
const _destCallerId = params.message.destination_caller_id;
const _destMismatch = _destCallerId && 
    normalizeIMessageHandle(sender) !== normalizeIMessageHandle(_destCallerId);
const isSelfChat = _senderMatchesChatId && !_destMismatch;

This preserves the existing self-chat echo detection for genuine self-chat scenarios while correctly routing non-self-chat outbound messages to the unconditional "from me" drop.

Environment

  • OpenClaw: 2026.4.2 (npm)
  • macOS: Tahoe 26.3.0 (arm64)
  • imsg: latest
  • Node: v25.6.1

Related Issues

  • #32166 (self-chat echo with is_from_me filter) — related but different scenario (actual self-chat producing duplicate DB entries with different is_from_me values)
  • #1649, #2585 — earlier echo loop issues

extent analysis

TL;DR

The issue can be fixed by modifying the isSelfChat check to include destination_caller_id in the self-chat determination.

Guidance

  • Review the monitor-provider-*.js file and update the isSelfChat check to use the suggested fix: const isSelfChat = _senderMatchesChatId && !_destMismatch;
  • Verify that the destination_caller_id is correctly set in the RPC watch notification for outbound messages.
  • Test the updated code with the reproduction steps provided to ensure that the echo loop is resolved.
  • Consider reviewing related issues (#32166, #1649, #2585) to ensure that similar scenarios are handled correctly.

Example

The suggested fix provides an example of how to update the isSelfChat check:

const _senderMatchesChatId = !isGroup && chatIdentifier != null && 
    normalizeIMessageHandle(sender) === normalizeIMessageHandle(chatIdentifier);
const _destCallerId = params.message.destination_caller_id;
const _destMismatch = _destCallerId && 
    normalizeIMessageHandle(sender) !== normalizeIMessageHandle(_destCallerId);
const isSelfChat = _senderMatchesChatId && !_destMismatch;

Notes

The fix assumes that the destination_caller_id is correctly set in the RPC watch notification. If this is not the case, additional debugging may be required.

Recommendation

Apply the suggested workaround by updating the isSelfChat check to include destination_caller_id in the self-chat determination, as this should correctly route non-self-chat outbound messages to the unconditional "from me" drop.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: iMessage outbound messages misclassified as self-chat, bypassing 'from me' drop [2 pull requests, 1 participants]