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
imsgreports the peer handle in bothsenderandchat_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_idwhen 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()treatedsender === chat_identifieras sufficient proof of self-chat, but currentimsgpayloads 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=trueDM rows withsender===chat_identifierbut a differentdestination_caller_idmust still take the normalfrom medrop 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
imsgprovides a differentdestination_caller_id. - That keeps those rows on the normal
from medrop 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
- Receive a normal iMessage DM where
imsgreports an outbound row with the peer handle in bothsenderandchat_identifier. - Let OpenClaw process the
is_from_me=truerow. - Observe whether it takes the self-chat path or the normal
from medrop 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 mewhendestination_caller_idpoints 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 tsxrepro againstresolveIMessageInboundDecision()for the reported DM false-positive payload shape and for the true self-chat shape; the DM case now drops asfrom meand 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
imsgpayloads that omitdestination_caller_idstill 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_idis 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_meself-chat classification only.
- Mitigation: the new regression test locks in the exact reported payload shape, and the change is scoped to
AI Disclosure
This PR was prepared with AI assistance and reviewed before submission.
Made with Cursor