Merged
Size
M
Change Breakdown
Bug Fix85%
Maintenance15%
#61619fix(imessage): avoid DM self-chat false positives

iMessage self-chat false positives fixed

Normal iMessage DMs no longer trigger self-echo loops. The plugin now uses destination_caller_id to distinguish real self-chat from regular outbound messages.

The iMessage plugin was misclassifying standard direct messages as self-chat, causing outbound replies to leak back as inbound messages and create infinite loops.

When processing iMessage notifications, the plugin checked whether sender === chat_identifier to detect self-chat scenarios. In regular DMs, Apple's imsg tool reports the peer's phone number for both fields—making every DM look like a conversation with yourself. Messages then bypassed the normal "from me" drop path and entered an echo cache designed for actual self-chat scenarios like Notes to Self. Once that cache expired or missed, outbound replies slipped through as inbound, triggering a reply, which generated another outbound, and so on.

The fix introduces destination_caller_id as a discriminating signal. When this field is present and differs from the sender, the message is treated as a normal DM rather than self-chat. The logic also accounts for multi-handle self-chat aliases by checking against chat participants. Older payloads lacking destination_caller_id retain the original heuristic for backwards compatibility.

The change lives in the iMessage monitor's inbound processing pipeline.

View Original GitHub Description

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

© 2026 · via Gitpulse