openclaw - ✅(Solved) Fix iMessage echo detection: DM misidentified as self-chat when sender == chat_identifier [2 pull requests, 1 comments, 2 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#63980Fetched 2026-04-10 03:41:26
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
0
Timeline (top)
referenced ×2commented ×1cross-referenced ×1

Root Cause

In iMessage's chat.db, the sender field for outgoing messages in a DM shows the other party's handle, not the local user's. For example, in a DM with +14145550000:

FieldOutgoing msgIncoming msg
is_from_metruefalse
sender+14145550000+14145550000
chat_identifier+14145550000+14145550000

The self-chat detection logic in monitor-CkCMHJdv.js is:

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

Since sender === chat_identifier for both incoming AND outgoing messages in a DM, isSelfChat is always true. This routes is_from_me messages through the echo-cache path instead of the simple "from me" drop.

Fix Action

Workaround

Patching the dist file locally:

// In monitor-CkCMHJdv.js, replace:
skipSelfChatHasCheck = true;

// With:
return { kind: "drop", reason: "from me (self-chat miss)" };

And optionally bumping SENT_MESSAGE_TEXT_TTL_MS from 4e3 to 3e4.

PR fix notes

PR #63989: fix(imessage): require destination_caller_id for self-chat classification (#63980)

Description (problem / solution / changelog)

Summary

  • Problem: In some DM SQLite rows, sender and chat_identifier match for both directions. When destination_caller_id was missing, the monitor treated the thread as self-chat (matchesSelfChatDestination was true because a missing destination was treated as a match). Outbound is_from_me messages then skipped the normal from me drop and could loop when echo cache missed.
  • Why it matters: Reported as outbound replies echoing back as inbound in DMs (#63980).
  • What changed: Self-chat classification now requires destination_caller_id to be present and normalized-equal to sender. Tests that model true self-chat include that field; new regression tests cover absent/blank destination with sender === chat_identifier.
  • What did NOT change: Group handling, echo cache TTL, and the existing path where destination_caller_id differs from sender (normal DM) remain as before.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #63980
  • Related #61619
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: matchesSelfChatDestination used destinationCallerIdNormalized == null || destinationCallerIdNormalized === senderNormalized, so a missing destination behaved like a confirmed self-chat destination match whenever sender === chat_identifier.
  • Missing detection / guardrail: Ambiguous DM rows without destination_caller_id were indistinguishable from self-chat on those two fields alone.
  • Contributing context (if known): chat.db row shapes vary; some DMs omit destination in the payload OpenClaw sees.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: extensions/imessage/src/monitor/self-chat-dedupe.test.ts
  • Scenario the test should lock in: is_from_me + sender === chat_identifier + missing or blank destination_caller_id drops as from me; true self-chat still dispatches when destination_caller_id matches sender.
  • Why this is the smallest reliable guardrail: Asserts the classification gate directly without requiring live chat.db.
  • Existing test that already covers this (if any): Partial overlap with uses destination_caller_id to avoid DM self-chat false positives (populated destination differing from sender).
  • If no new test is added, why not: N/A — new cases added.

User-visible / Behavior Changes

  • iMessage: DMs whose monitor payload omits destination_caller_id are no longer classified as self-chat solely from sender === chat_identifier. True self-chat rows should include destination_caller_id matching the sender (as in existing supported cases).

Diagram (if applicable)

N/A

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS (issue context)
  • Runtime/container: N/A
  • Model/provider: N/A
  • Integration/channel (if any): iMessage (bundled extension)
  • Relevant config (redacted): N/A

Steps

  1. pnpm test:extension imessage
  2. pnpm check

Expected

  • All iMessage extension tests pass; lint/typecheck pass.

Actual

  • Ran locally: iMessage extension tests and pnpm check green.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios: Unit tests for inbound decision; logic review against #63980.
  • Edge cases checked: Blank vs absent destination_caller_id; existing DM tests with differing destination_caller_id; self-chat fixtures updated with explicit destination matching sender.
  • What I did not verify: Live chat.db / imsg on hardware.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps:

Risks and Mitigations

  • Risk: A true self-chat row that never populates destination_caller_id could be treated as non-self-chat; outbound is_from_me would drop as from me instead of taking the self-chat branch.
    • Mitigation: Aligns with using explicit destination metadata when available; self-chat tests use populated destination_caller_id matching sender.

Made with Cursor

Changed files

  • CHANGELOG.md (modified, +5/-0)
  • extensions/imessage/src/monitor/inbound-processing.test.ts (modified, +11/-1)
  • extensions/imessage/src/monitor/inbound-processing.ts (modified, +5/-17)
  • extensions/imessage/src/monitor/self-chat-dedupe.test.ts (modified, +78/-5)

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

---

// In monitor-CkCMHJdv.js, replace:
skipSelfChatHasCheck = true;

// With:
return { kind: "drop", reason: "from me (self-chat miss)" };
RAW_BUFFERClick to expand / collapse

Problem

iMessage echo suppression fails for DMs because the conversation is incorrectly classified as a "self-chat."

Root cause

In iMessage's chat.db, the sender field for outgoing messages in a DM shows the other party's handle, not the local user's. For example, in a DM with +14145550000:

FieldOutgoing msgIncoming msg
is_from_metruefalse
sender+14145550000+14145550000
chat_identifier+14145550000+14145550000

The self-chat detection logic in monitor-CkCMHJdv.js is:

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

Since sender === chat_identifier for both incoming AND outgoing messages in a DM, isSelfChat is always true. This routes is_from_me messages through the echo-cache path instead of the simple "from me" drop.

Why the echo cache doesn't save it

  1. imsg RPC send doesn't return the message GUID (filed as steipete/imsg#85), so the 60s ID-based cache can't match
  2. The text-based cache has only a 4s TTL (SENT_MESSAGE_TEXT_TTL_MS = 4e3), which is too short for iMessage echo round-trips
  3. When the cache misses, the code sets skipSelfChatHasCheck = true and continues processing the message as inbound — this is correct for actual self-chat (Notes to Self) but wrong for DMs

Result

Every outbound reply echoes back as a new inbound message, creating an infinite loop.

Suggested fixes

  1. Improve self-chat detection: A real self-chat has the local user's handle as both sender and chat_identifier. Consider checking destination_caller_id or the account's own handle to distinguish DMs from self-chat.

  2. Increase text echo TTL fallback: 4 seconds is too tight for iMessage. 15-30 seconds would be safer as a fallback when GUID matching isn't available.

  3. Drop on cache miss for is_from_me: In the is_from_me + isSelfChat branch, a cache miss should still drop the message rather than accepting it. Real self-chat messages where the user types something new won't have is_from_me = true from the watch stream perspective (they originate locally, not from a remote echo).

Workaround

Patching the dist file locally:

// In monitor-CkCMHJdv.js, replace:
skipSelfChatHasCheck = true;

// With:
return { kind: "drop", reason: "from me (self-chat miss)" };

And optionally bumping SENT_MESSAGE_TEXT_TTL_MS from 4e3 to 3e4.

Environment

  • OpenClaw 2026.4.9 (0512059)
  • imsg v0.5.0
  • macOS 15.4 (Sequoia, arm64)

extent analysis

TL;DR

The most likely fix for the iMessage echo suppression issue in DMs is to improve self-chat detection by checking the local user's handle.

Guidance

  • Improve self-chat detection by considering the destination_caller_id or the account's own handle to distinguish DMs from self-chat.
  • Increase the text echo TTL fallback to 15-30 seconds to account for iMessage round-trip delays.
  • Drop messages on cache miss for is_from_me to prevent infinite loops.

Example

To patch the monitor-CkCMHJdv.js file locally, replace the line skipSelfChatHasCheck = true; with return { kind: "drop", reason: "from me (self-chat miss)" }; to drop messages on cache miss.

Notes

The provided workaround may not be a permanent solution and may need to be reapplied after updates. Additionally, the imsg RPC send not returning the message GUID is a known issue (steipete/imsg#85) that should be addressed.

Recommendation

Apply the workaround by patching the monitor-CkCMHJdv.js file and optionally bumping SENT_MESSAGE_TEXT_TTL_MS to 3e4, as this provides a temporary solution to the issue.

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