openclaw - ✅(Solved) Fix iMessage plugin: self-echo infinite loop in DM chats (isSelfChat false positive) [1 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#61543Fetched 2026-04-08 02:57:34
View on GitHub
Comments
1
Participants
2
Timeline
2
Reactions
0
Author
Timeline (top)
commented ×1cross-referenced ×1

The iMessage plugin's isSelfChat heuristic false-positives on all DM chats, causing outbound messages to sometimes bypass the "from me" drop filter and leak back as inbound messages. This creates an infinite self-echo loop.

Root Cause

In monitor-provider-BaCJr_L0.js around line 917:

if (params.message.is_from_me) {
    params.selfChatCache?.remember(selfChatLookup);
    if (isSelfChat) {
        // Echo cache detection (designed for "Notes to Self" chats)
        // FALSE POSITIVE: in a DM, chatIdentifier IS the peer's handle,
        // and imsg reports sender as the peer's handle for outbound too
    } else {
        return { kind: "drop", reason: "from me" };  // correct path, never reached in DMs
    }
}

The isSelfChat check:

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

This is always true for DM outbound messages because imsg reports sender = peer handle = chatIdentifier.

Fix Action

Workaround

Reset the poisoned session transcript (archive the .jsonl, keep header line only, restart gateway). The echo loop stops once the accumulated garbage is cleared, but can recur on any DM.

PR fix notes

PR #61619: fix(imessage): avoid DM self-chat false positives

Description (problem / solution / changelog)

Summary

  • Problem: iMessage DM outbound rows can be misclassified as self-chat when imsg reports the peer handle in both sender and chat_identifier.
  • Why it matters: that sends normal DMs down the self-chat echo path, so a cache miss can leak the bot's own outbound reply back into inbound handling and start a loop.
  • What changed: the iMessage monitor now uses destination_caller_id when available to distinguish true self-chat from DM false positives, and it preserves that metadata in the parsed payload.
  • What did NOT change (scope boundary): no echo-cache redesign, no config changes, and no behavior change for older payloads that do not include destination_caller_id.

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 #61543
  • Related #59845
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: resolveIMessageInboundDecision() treated sender === chat_identifier as sufficient proof of self-chat, but current imsg payloads can use the peer handle for both fields in a normal DM outbound row.
  • Missing detection / guardrail: the monitor ignored destination_caller_id, which is the field that differentiates a real self-chat from this DM false positive.
  • Contributing context (if known): the earlier self-chat fixes addressed reflected duplicate rows, but they still depended on the older heuristic for identifying self-chat in the first place.

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, extensions/imessage/src/monitor.gating.test.ts
  • Scenario the test should lock in: is_from_me=true DM rows with sender===chat_identifier but a different destination_caller_id must still take the normal from me drop path, while real self-chat still dispatches.
  • Why this is the smallest reliable guardrail: the bug lives entirely inside inbound payload parsing and the inbound-decision branch, so focused tests on those two seams cover the real regression surface.
  • Existing test that already covers this (if any): the existing self-chat tests covered the legacy heuristic and true self-chat behavior, but not the DM false-positive shape.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • OpenClaw no longer treats normal iMessage DM outbound rows as self-chat when imsg provides a different destination_caller_id.
  • That keeps those rows on the normal from me drop path and avoids self-echo loops caused by cache misses.

Diagram (if applicable)

Before:
[normal DM outbound row]
  -> sender == chat_identifier
  -> misclassified as self-chat
  -> echo cache miss
  -> dispatched as inbound

After:
[normal DM outbound row]
  -> destination_caller_id != sender
  -> classified as normal DM
  -> dropped as from me

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 15.3 / Darwin 25.3.0
  • Runtime/container: Node.js via repo scripts
  • Model/provider: N/A
  • Integration/channel: iMessage via imsg
  • Relevant config (redacted): standard iMessage channel config; no config changes required

Steps

  1. Receive a normal iMessage DM where imsg reports an outbound row with the peer handle in both sender and chat_identifier.
  2. Let OpenClaw process the is_from_me=true row.
  3. Observe whether it takes the self-chat path or the normal from me drop path.

Expected

  • The row is recognized as a normal DM outbound event and dropped as from me.
  • Real self-chat rows still use the self-chat path.

Actual

  • Before this change, the row could be misclassified as self-chat and leak through on an echo-cache miss.
  • After this change, the same shape drops as from me when destination_caller_id points at a different local handle.

Evidence

Attach at least one:

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

Human Verification (required)

  • Verified scenarios: ran a direct node --import tsx repro against resolveIMessageInboundDecision() for the reported DM false-positive payload shape and for the true self-chat shape; the DM case now drops as from me and the true self-chat case still dispatches.
  • Edge cases checked: parser now preserves destination_caller_id; current fallback behavior remains unchanged when that field is absent.
  • What you did not verify: the heavier Vitest lanes (pnpm test, pnpm test:extension imessage, and a focused Vitest invocation) stalled after Vitest startup in this environment, so I did not get a completed test summary from those commands.

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: older imsg payloads that omit destination_caller_id still rely on the legacy heuristic.

    • Mitigation: the new logic is additive and only tightens classification when the stronger signal is present, so older payload behavior is preserved rather than reinterpreted.
  • Risk: this assumes destination_caller_id is the local sending identity for the normal DM false-positive shape.

    • Mitigation: the new regression test locks in the exact reported payload shape, and the change is scoped to is_from_me self-chat classification only.

AI Disclosure

This PR was prepared with AI assistance and reviewed before submission.

Made with Cursor

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/imessage/src/monitor.gating.test.ts (modified, +16/-0)
  • extensions/imessage/src/monitor/inbound-processing.ts (modified, +16/-6)
  • extensions/imessage/src/monitor/parse-notification.ts (modified, +1/-0)
  • extensions/imessage/src/monitor/self-chat-dedupe.test.ts (modified, +82/-3)
  • extensions/imessage/src/monitor/types.ts (modified, +1/-0)

Code Example

if (params.message.is_from_me) {
    params.selfChatCache?.remember(selfChatLookup);
    if (isSelfChat) {
        // Echo cache detection (designed for "Notes to Self" chats)
        // FALSE POSITIVE: in a DM, chatIdentifier IS the peer's handle,
        // and imsg reports sender as the peer's handle for outbound too
    } else {
        return { kind: "drop", reason: "from me" };  // correct path, never reached in DMs
    }
}

---

const isSelfChat = !isGroup && chatIdentifier != null 
    && normalizeIMessageHandle(sender) === normalizeIMessageHandle(chatIdentifier);
RAW_BUFFERClick to expand / collapse

Version

OpenClaw 2026.4.2 (d74a122) macOS 26.3.1 (arm64), imsg via Homebrew

Summary

The iMessage plugin's isSelfChat heuristic false-positives on all DM chats, causing outbound messages to sometimes bypass the "from me" drop filter and leak back as inbound messages. This creates an infinite self-echo loop.

Reproduction

  1. Have an iMessage DM chat with contact +1234567890
  2. The chat's identifier in Messages.app DB is +1234567890
  3. Send a reply via OpenClaw
  4. imsg watch picks up the new DB row with is_from_me: true, sender: "+1234567890"
  5. Plugin evaluates normalizeIMessageHandle(sender) === normalizeIMessageHandle(chatIdentifier)true
  6. Instead of return { kind: "drop", reason: "from me" }, enters echo-cache path
  7. Echo cache misses (encoding artifacts from Messages.app attributed string storage corrupt the text)
  8. Message leaks through as inbound → generates reply → infinite loop

Root Cause

In monitor-provider-BaCJr_L0.js around line 917:

if (params.message.is_from_me) {
    params.selfChatCache?.remember(selfChatLookup);
    if (isSelfChat) {
        // Echo cache detection (designed for "Notes to Self" chats)
        // FALSE POSITIVE: in a DM, chatIdentifier IS the peer's handle,
        // and imsg reports sender as the peer's handle for outbound too
    } else {
        return { kind: "drop", reason: "from me" };  // correct path, never reached in DMs
    }
}

The isSelfChat check:

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

This is always true for DM outbound messages because imsg reports sender = peer handle = chatIdentifier.

Echo Cache Failure Mode

The echo cache compares exact text, but round-tripped messages have:

  • Binary encoding artifacts (attributed string metadata bytes decoded as UTF-8)
  • Possible text chunking differences
  • The garbled prefixes (\xef\xbf\xbd, \x01, etc.) guarantee no match

Observed Impact

  • ~20+ rapid-fire self-echo messages in a single DM
  • Session transcript polluted with 295 lines of echo garbage
  • Required manual session reset + gateway restart

Suggested Fixes

  1. Simplest: For is_from_me=true in non-group, non-actual-self chats, always drop. Detect actual self-chat by comparing against the Mac's own registered handles rather than chatIdentifier.
  2. Alternative: Use message GUID tracking for echo detection instead of text comparison.
  3. Defense in depth: Add a rate limiter — if >N replies to the same chat in <M seconds, pause and flag.

Workaround

Reset the poisoned session transcript (archive the .jsonl, keep header line only, restart gateway). The echo loop stops once the accumulated garbage is cleared, but can recur on any DM.

extent analysis

TL;DR

The most likely fix is to modify the isSelfChat check to correctly identify self-chats by comparing against the Mac's own registered handles instead of chatIdentifier.

Guidance

  • Review the isSelfChat check in monitor-provider-BaCJr_L0.js and consider modifying it to use the Mac's own registered handles for comparison.
  • Implement a rate limiter to prevent rapid-fire self-echo messages by pausing and flagging if more than N replies are sent to the same chat in less than M seconds.
  • Consider using message GUID tracking for echo detection instead of text comparison to improve accuracy.
  • To verify the fix, test the modified isSelfChat check with various DM chats and ensure that outbound messages are correctly dropped and do not leak back as inbound messages.

Example

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

Note: This example assumes that macRegisteredHandle is a variable that stores the Mac's own registered handle.

Notes

The suggested fixes and workaround may not completely eliminate the issue, and further testing and refinement may be necessary to ensure a reliable solution.

Recommendation

Apply the simplest suggested fix: modify the isSelfChat check to compare against the Mac's own registered handles instead of chatIdentifier, as this approach is likely to be the most effective and efficient solution.

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 iMessage plugin: self-echo infinite loop in DM chats (isSelfChat false positive) [1 pull requests, 1 comments, 2 participants]