openclaw - ✅(Solved) Fix iMessage echo loop: NSAttributedString garbage bytes cause echo cache miss for is_from_me messages [1 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#61821Fetched 2026-04-08 02:54:02
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Participants
Timeline (top)
cross-referenced ×1referenced ×1

Outgoing iMessages from Jarvis get reflected back as inbound messages, creating a reply loop. Root cause is a text normalization mismatch in the sentMessageCache echo cache.

Root Cause

Outgoing iMessages from Jarvis get reflected back as inbound messages, creating a reply loop. Root cause is a text normalization mismatch in the sentMessageCache echo cache.

Fix Action

Workaround

Set channels.imessage.blockStreaming: true to coalesce rapid burst replies into a single message, reducing the race window. Does not fully eliminate the issue.

PR fix notes

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

{
  "sender": "+13179650799",
  "chat_identifier": "+13179650799",
  "is_from_me": true
}

---

Stored in echo cache:  "Good news — the portal is up..."
Reported by imsg RPC:  "\xef\xbf\xbd@\x01Good news — the portal is up..."

---

function normalizeInboundBodyText(text: string): string {
  // Strip leading NSAttributedString parse artefacts (U+FFFD + control bytes)
  return text.replace(/^[\uFFFD\x00-\x1F]+/, "");
}
RAW_BUFFERClick to expand / collapse

Summary

Outgoing iMessages from Jarvis get reflected back as inbound messages, creating a reply loop. Root cause is a text normalization mismatch in the sentMessageCache echo cache.

Environment

  • OpenClaw: 2026.3.31 (213a704)
  • imsg: 0.5.0
  • macOS: Darwin 25.3.0 (arm64)
  • Chat type: iMessage DM

Root Cause

Step 1 — isSelfChat detection fires incorrectly

imsg RPC notifications report sent messages (is_from_me: true) with:

{
  "sender": "+13179650799",
  "chat_identifier": "+13179650799",
  "is_from_me": true
}

Because sender === chat_identifier, the isSelfChat check in resolveIMessageInboundDecision evaluates to true. This bypasses the simple is_from_me → drop path and instead defers to the echo cache.

Step 2 — Echo cache lookup fails due to text corruption

imsg reads iMessage messages from the attributedBody SQLite column (NSAttributedString binary format). For sent messages, it prepends garbled bytes to the parsed text:

Stored in echo cache:  "Good news — the portal is up..."
Reported by imsg RPC:  "\xef\xbf\xbd@\x01Good news — the portal is up..."

The leading \xef\xbf\xbd (U+FFFD replacement char) followed by 1–2 control bytes is an NSAttributedString parsing artefact. The text keys do not match, so sentMessageCache.has() returns false.

Step 3 — Message dispatched as inbound → loop

With a cache miss on a isSelfChat message, the code falls through to accessDecision. The sender (+13179650799) is in allowFrom, so the message is dispatched as a real inbound message. Jarvis replies. Loop begins.

Reproduction

  1. Configure iMessage channel with dmPolicy: allowlist, allowFrom: [<your_number>]
  2. Send multiple rapid replies from Jarvis to a DM conversation
  3. Each outgoing message fires an RPC notification with garbled attributedBody text
  4. Echo cache misses → messages dispatched as inbound → loop

Note: Single-message replies often work fine — the loop is most likely when multiple replies are sent in rapid succession (race between sentMessageCache.remember() and the RPC notification arrival).

Observed Pattern

Messages that loop start with bytes ef bf bd (U+FFFD) in the attributedBody-parsed text. Messages from the text column (when populated) do not exhibit this issue.

Suggested Fix

In resolveIMessageInboundDecision, normalize the inbound message text before echo cache comparison — specifically, strip leading \uFFFD and control characters (\x00\x1F) from the text before building the cache key:

function normalizeInboundBodyText(text: string): string {
  // Strip leading NSAttributedString parse artefacts (U+FFFD + control bytes)
  return text.replace(/^[\uFFFD\x00-\x1F]+/, "");
}

Alternatively, when is_from_me is true and the message is not a genuine self-chat (i.e. the sender is not the agent's own iCloud account), always drop without checking the echo cache. This could be gated on a new config key like channels.imessage.selfHandle to specify the agent's own iCloud address.

Workaround

Set channels.imessage.blockStreaming: true to coalesce rapid burst replies into a single message, reducing the race window. Does not fully eliminate the issue.

Additional Context

The destination_caller_id field in the RPC notification correctly identifies the agent's own iCloud address (e.g. [email protected]). This field could be used to reliably detect is_from_me messages that are NOT self-chats and hard-drop them without touching the echo cache.

extent analysis

TL;DR

The most likely fix for the iMessage reply loop issue is to normalize the inbound message text by stripping leading \uFFFD and control characters before echo cache comparison.

Guidance

  • Implement the normalizeInboundBodyText function to strip leading \uFFFD and control characters from the text before building the cache key.
  • Consider adding a new config key like channels.imessage.selfHandle to specify the agent's own iCloud address and gate the echo cache check on this.
  • As a temporary workaround, set channels.imessage.blockStreaming: true to coalesce rapid burst replies into a single message, reducing the race window.
  • Use the destination_caller_id field in the RPC notification to reliably detect is_from_me messages that are NOT self-chats and hard-drop them without touching the echo cache.

Example

function normalizeInboundBodyText(text: string): string {
  // Strip leading NSAttributedString parse artefacts (U+FFFD + control bytes)
  return text.replace(/^[\uFFFD\x00-\x1F]+/, "");
}

Notes

This fix assumes that the issue is caused by the text normalization mismatch in the sentMessageCache echo cache. The workaround may not fully eliminate the issue, but it can reduce the occurrence of the reply loop.

Recommendation

Apply the workaround by setting channels.imessage.blockStreaming: true to reduce the race window, and implement the normalizeInboundBodyText function to fix the text normalization issue. This approach addresses the root cause of the problem and provides a reliable 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 echo loop: NSAttributedString garbage bytes cause echo cache miss for is_from_me messages [1 pull requests, 1 participants]