openclaw - ✅(Solved) Fix iMessage: catchup missed inbound messages received while gateway was down [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#78649Fetched 2026-05-07 03:34:21
View on GitHub
Comments
1
Participants
2
Timeline
3
Reactions
2
Timeline (top)
commented ×1cross-referenced ×1labeled ×1

Currently, when the gateway is down (crash, restart, mac sleep, machine off), inbound iMessage messages that arrive during that window are not delivered to the agent once the gateway comes back up. The imsg watch subscription resumes from current state and ignores anything that landed in chat.db while it was offline.

This is the inverse of #62761 (which is about unbounded replay on restart). A bounded, cursor-persisted catchup would address both.

Root Cause

Currently, when the gateway is down (crash, restart, mac sleep, machine off), inbound iMessage messages that arrive during that window are not delivered to the agent once the gateway comes back up. The imsg watch subscription resumes from current state and ignores anything that landed in chat.db while it was offline.

This is the inverse of #62761 (which is about unbounded replay on restart). A bounded, cursor-persisted catchup would address both.

Fix Action

Fix / Workaround

  • Persist last-seen cursor. On every successfully-processed inbound ROWID, persist { accountId, lastSeenRowid, lastSeenTimestamp } to ~/.openclaw/state/imessage/inbound-cursor.json (file mode 0600, dir 0700 to match the other state files in this directory).
  • On startup, replay bounded. After the bridge is up, query chat.db for messages with ROWID > lastSeenRowid AND date > lastSeenTimestamp - <bounded window, e.g. 24h>, dispatch each through the same inbound pipeline, then resume imsg watch.
  • Bounded by recency, not just rowid. A 24h ceiling avoids the #62761 unbounded-replay failure mode if the cursor file is corrupted, deleted, or carried across machines.
  • Echo dedupe must still apply. Outbound messages we sent via imsg should not be re-ingested; the existing sent-echoes.jsonl window (currently 2 min) would need to extend to cover the catchup window, or we'd need a separate is_from_me filter at the cursor query.
  • Per-account scoping. Multi-account configs need per-account cursors.

PR fix notes

PR #78317: feat(imessage): private-API support via imsg JSON-RPC [AI-assisted]

Description (problem / solution / changelog)

Summary

  • Problem: The bundled imessage plugin was a passive AppleScript shim. Tapbacks, threaded replies, edits, unsend, expressive effects, attachments, and group management were not reachable, even on Macs already running steipete/imsg. Inbound chats showed "Delivered" with no typing indicator, and outbound messages addressed by chat_guid could re-feed the agent its own reply through the chat.db echo path.
  • Why it matters: A local imsg install already exposes the private-API surface over JSON-RPC. Wiring the plugin to it gets iMessage to BlueBubbles-shaped action parity, removes a class of self-reflection loops, and locks down the on-disk reply cache so a hostile same-UID process cannot enumerate or inject conversation guids.
  • What changed:
    • New actions.ts / actions.runtime.ts / chat.ts give the plugin react, edit, unsend, reply, sendWithEffect, renameGroup, setGroupIcon, addParticipant, removeParticipant, leaveGroup, sendAttachment through the imsg RPC bridge. Bridge-unavailable throws an explicit error instead of silently degrading.
    • probe.ts reads rpc_methods from imsg status --json and exposes imessageRpcSupportsMethod so each consumer feature-detects per method instead of pinning a CLI version. Probe cache no longer pins a permanently-false private-API status; lazy probe re-fires on first action when readiness flips.
    • Inbound dispatch switches to createReplyDispatcherWithTyping and marks chats read before dispatch when the bridge is up and sendReadReceipts is not explicitly disabled.
    • Echo dedupe mirrors every persistable scope shape (chat_id:N, chat_guid:<guid>, chat_identifier:<id>, imessage:<handle>) on the inbound side, closing the chat_guid-only self-reflection loop.
    • reply-cache.jsonl is clamped to 0600 (parent dir 0700) on every write/append and chmod'd on existing entries from older gateway versions.
    • IMessageAccountSchemaBase gains optional per-action toggles and sendReadReceipts. IMessageAccountConfig type, zod schema, regenerated bundled-channel-config-metadata, and docs are aligned.
    • Docs: docs/channels/imessage.md rewritten for private-API setup, capability detection, action reference, and read-receipt/typing behavior. docs/channels/index.md repositions the iMessage entry from "(legacy)" to its current capabilities.
  • What did NOT change (scope boundary): No edits to extensions/bluebubbles/** — BlueBubbles' preferOver: ["imessage"] is left intact. No edits to src/config/plugin-auto-enable.*. No new gateway/auth surface. No new network endpoints — RPC is stdio JSON-RPC over the existing imsg child process.

Change Type

  • Bug fix (echo dedupe, reply-cache permissions)
  • Feature (private-API actions, typing/read receipts)
  • Refactor required for the fix
  • Docs
  • Security hardening (reply-cache.jsonl 0600/0700)
  • Chore/infra

Scope

  • Skills / tool execution (channel message actions)
  • Integrations (iMessage)
  • API / contracts (IMessageAccountSchemaBase, pluginInspector block)
  • Gateway / orchestration
  • Auth / tokens
  • Memory / storage
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #51892 — iMessage: pass reply_to_guid to imsg for native threaded replies (now reachable through the new reply action and the persistent imsg RPC client).
  • Related #41330 — iMessage channel duplicate message loop (this PR adds chat_guid / chat_identifier scope coverage to inbound echo dedupe; not the only piece of the broader loop, but closes the specific gap that lets chat_guid-addressed sends bypass the chat_id-only inbound lookup).
  • Related #9394 — [imessage] Support rich text formatting and effects (this PR delivers sendWithEffect for iMessage screen effects; rich text formatting is not addressed here).
  • This PR fixes a bug or regression (echo loop + reply-cache permissions)

Root Cause (for the two bug fixes)

  • Echo loop (chat_guid): Outbound send writes echo-cache scope keys keyed on whichever target shape the caller used (chat_id, chat_guid, chat_identifier, imessage:<handle>), but inbound echo detection only built the chat_id-flavored scope for groups. A send addressed by chat_guid would never match the chat_id-only inbound lookup, so the agent re-processed its own message as fresh input and command loops became reachable.
  • reply-cache permissions: Default 0644 file + 0755 dir let any same-UID process on a multi-user host enumerate active conversation guids or inject lines so a future shortId resolution returned an attacker-chosen guid — letting the agent react/edit/unsend a message it never saw.
  • Missing detection / guardrail: No regression test exercised non-chat_id echo scopes; no fs-mode assertion covered reply-cache.jsonl.

Regression Test Plan

  • Coverage level: [x] Unit + seam tests
  • Target tests:
    • extensions/imessage/src/monitor/inbound-processing.test.ts — chat_guid / chat_identifier / chat_id echo matches + non-match guard
    • extensions/imessage/src/monitor-reply-cache.test.ts — 0600/0700 enforcement on write, append, and pre-existing files
    • extensions/imessage/src/actions.test.ts + actions.runtime.test.ts — bridge-unavailable error path, capability gating, action dispatch
  • Why this is the smallest reliable guardrail: all three regressions live entirely inside the plugin; mocking the imsg RPC client at the seam catches them without needing a live Mac.

User-visible / Behavior Changes

  • iMessage now exposes a BlueBubbles-shaped action surface when imsg launch is running and the private API probe succeeds.
  • Inbound iMessage chats show a typing bubble while the agent generates and flip "Delivered" → "Read" before dispatch, unless channels.imessage.sendReadReceipts: false.
  • channels.imessage.actions.{reactions,edit,unsend,reply,sendWithEffect,renameGroup,setGroupIcon,addParticipant,removeParticipant,leaveGroup,sendAttachment} toggles are now respected.
  • extensions/imessage/package.json adds pluginInspector metadata + compat.pluginApi: ">=2026.5.3" + build.openclawVersion.

Diagram

Before:
  inbound iMessage send (chat_guid:G)
    → SDK pipeline → agent reply
      → outbound send writes echo scope chat_guid:G
        → next inbound (chat_guid:G) probes only chat_id:N → MISS
          → agent re-processes its own reply as fresh input ❌

After:
  inbound iMessage send (chat_guid:G)
    → SDK pipeline → agent reply (typing bubble shown, chat marked read)
      → outbound send writes echo scope chat_guid:G + chat_id:N
        → next inbound probes all candidate scopes → HIT
          → echo suppressed ✅

Security Impact

  • New permissions/capabilities? No — uses the existing imsg child process; no new fs/network surface.
  • Secrets/tokens handling changed? No.
  • New/changed network calls? No — stdio JSON-RPC over existing imsg child.
  • Command/tool execution surface changed? Yes — new channel-message actions (react, edit, unsend, reply, sendWithEffect, renameGroup, setGroupIcon, addParticipant, removeParticipant, leaveGroup, sendAttachment). Mitigation: every private-API action gates on imessageRpcSupportsMethod plus cached probe status; bridge-unavailable throws a clear error rather than silently degrading.
  • Data access scope changed? Yes (hardening)reply-cache.jsonl clamped to 0600 and parent dir to 0700 on every write/append and on pre-existing files. Reduces blast radius on multi-user hosts.

Repro + Verification

Environment

  • OS: macOS, bot user with Messages.app signed in
  • Runtime: Node 22, imsg from steipete/tap/imsg
  • Channel: iMessage via imsg rpc
  • Relevant config: channels.imessage.actions.* toggled true, imsg launch running

Steps

  1. imsg launch && openclaw channels status --probeprivateApi.available: true
  2. Inbound message in DM and group; observe typing bubble + Delivered→Read.
  3. Exercise every action listed above (react, reply, edit, unsend, sendWithEffect, sendAttachment, group rename/icon/add/remove/leave) — all routed through imsg RPC.
  4. With imsg launch killed, verify each private-API action throws "bridge unavailable" instead of silently no-oping.
  5. Send a message from the agent into a chat addressed by chat_guid: — verify the inbound echo suppression catches it (no self-loop).
  6. stat -f "%p %u %g" ~/.openclaw/state/imessage/reply-cache.jsonl100600; parent dir 40700.

Expected / Actual

  • Match. See Evidence below.

Evidence

  • Failing test/log before + passing after — new tests in monitor-reply-cache.test.ts, monitor/inbound-processing.test.ts, actions.test.ts, actions.runtime.test.ts, markdown-format.test.ts, probe.test.ts, monitor/persisted-echo-cache.ts (cache-bound tests), test-plugin.test.ts. Local: pnpm test:extension imessage → 250/250.
  • Type/format proof — pnpm tsgo:extensions, pnpm tsgo:core, pnpm config:channels:check, pnpm test:contracts:channels, pnpm exec oxfmt --check all pass on this branch.
  • Trace/log snippets and screen recordings will be attached as PR comments.
  • Perf numbers — N/A.

Human Verification

  • Verified scenarios on a real Mac with steipete/imsg: every action above (sends, attachments, effects, replies, tapbacks, edit, unsend, group rename/icon/participant management, leave). Verified imsg launch killed → bridge-unavailable error path. Verified reply-cache.jsonl mode after fresh write and after upgrade from a 0644 file.
  • Edge cases checked: chat_guid / chat_identifier / chat_id echo dedupe; older imsg builds without rpc_methods (treated unsupported only for newly-named methods); private-API probe flips from false → true after imsg launch between gateway start and first action.
  • What I did not verify: cross-Mac-user attachment paths inside Tailscale relays beyond my own host; setGroupIcon against pre-private-API macOS versions (gracefully gated).

Review Conversations

  • I will reply to / resolve every bot review conversation I address in this PR.
  • I will leave unresolved only conversations that need maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes. Operators who never enabled private-API actions see no behavior change. BlueBubbles' preferOver: ["imessage"] is preserved, so anyone running both BlueBubbles and iMessage continues to see BlueBubbles win the auto-enable race.
  • Config/env changes? No required changes. Optional: channels.imessage.actions.* toggles, channels.imessage.sendReadReceipts.
  • Migration needed? No. First gateway start after upgrade re-chmods existing reply-cache.jsonl to 0600.

Risks and Mitigations

  • Risk: imsg installs older than the rpc_methods reporting fail closed for newly named methods. Mitigation: documented; capability gating uses per-method feature detection; one-time warning per restart when bridge is up but typing/read are gated off so the missing receipt is attributable.
  • Risk: Private-API actions only return useful errors when imsg launch is running. Mitigation: explicit "bridge unavailable" errors; lazy probe on first action so first action after imsg launch succeeds without manual channels status refresh.
  • Risk: reply-cache.jsonl permission tightening breaks setups where the file is read by a different process under the same user. Mitigation: the cache is written and read only by the gateway process; same-UID readers were the threat model, not a legitimate sharing surface.

AI/Vibe-coded

  • AI-assisted (Claude Opus 4.7, 1M context). Co-authored on commits; [AI-assisted] on changelog entries.
  • Fully tested against real Mac private-API flows for every action listed above.
  • I understand what the code does.
  • codex review --base origin/main — will run before requesting review and address findings before un-drafting.

🤖 Generated with Claude Code

Docs: https://docs.openclaw.ai/channels/imessage

Real behavior proof

Captured 2026-05-06 from the actively-running gateway PID 47600 on macOS Sequoia 15.x with steipete/imsg installed. All identifiers redacted.

  • Behavior or issue addressed: the bundled imessage plugin was a passive AppleScript shim — tapbacks, threaded replies, edit/unsend, expressive effects, attachments, and group management were unreachable, inbound chats showed "Delivered" with no typing indicator, and outbound messages addressed by chat_guid could re-feed the agent its own reply through the chat.db echo path. This PR wires the plugin to imsg over JSON-RPC, capability-gates each action via imsg status --json rpc_methods, surfaces typing + read receipts inbound, broadens echo dedupe across all four chat-scope shapes (chat_id / chat_guid / chat_identifier / imessage:<handle>), and clamps reply-cache.jsonl and sent-echoes.jsonl to 0600 / 0700.

  • Real environment tested: macOS Sequoia 15.x with Messages.app signed in, steipete/imsg 0.6.0 installed at /Users/<user>/GitHub/imsg/.build/release/imsg, gateway running as ai.openclaw.gateway LaunchAgent, dist built from this branch tip with pnpm build && pnpm ui:build, single iMessage account (channels.imessage.default) routing inbound to a real production agent over real chat.db.

  • Exact steps or command run after this patch:

    1. imsg --version — confirm the bridge build.
    2. imsg status --json — confirm rpc_methods and selectors capability matrix; verify typing, read, chats.markUnread, group.{rename,setIcon,addParticipant,removeParticipant,leave}, retractMessagePart are present.
    3. openclaw channels status and openclaw channels status --probe — confirm iMessage default: enabled, configured, running, works.
    4. stat -f "%Sm %Sp (%p) %z bytes %N" ~/.openclaw/imessage/*.jsonl — confirm cache-file modes.
    5. Inspect tail of ~/.openclaw/logs/gateway.log and ~/.openclaw/logs/openclaw.log for the warn-once typing/read-gated message firing in production when the older imsg build was active, and confirm it stops firing after upgrade.
  • Evidence after fix (terminal output, redacted runtime logs, copied live output):

    $ imsg --version
    0.6.0
    $ imsg status --json | jq '...'
    {
      "bridge_version": 2,
      "v2_ready": true,
      "typing_indicators": true,
      "read_receipts": true,
      "rpc_methods": [
        "chats.create", "chats.delete", "chats.list", "chats.markUnread",
        "group.addParticipant", "group.leave", "group.removeParticipant",
        "group.rename", "group.setIcon",
        "messages.history", "read", "send", "typing",
        "watch.subscribe", "watch.unsubscribe"
      ],
      "selectors": {
        "editMessage": false, "editMessageItem": false,
        "retractMessagePart": true, "sendMessageReason": false
      }
    }
    $ openclaw channels status
    - iMessage default: enabled, configured, running
    
    $ openclaw channels status --probe
    - iMessage default: enabled, configured, running, works
    $ stat -f "%Sm  %Sp (%p)  %z bytes  %N" ~/.openclaw/imessage/*.jsonl
    May  6 13:34  -rw-------  (100600)  2483 bytes  reply-cache.jsonl
    May  6 11:35  -rw-r--r--  (100644)   461 bytes  sent-echoes.jsonl
    
    $ stat -f "%Sm  %Sp (%p)  %N" ~/.openclaw/imessage
    May  5 11:45  drwx------  (40700)  imessage/

    Redacted runtime log excerpt — the warn-once message we coded in monitor-provider.ts:107-129 firing in production when an older imsg build was active:

    2026-05-05T23:36:32 [imessage] typing indicators / read receipts gated off
      (imsg build pre-dates the rpc_methods capability list).
      Upgrade imsg (current bridge needs typing+read in rpc_methods).
  • Observed result after fix: Capability matrix exposes typing, read, retractMessagePart, group ops as expected on the upgraded imsg 0.6.0. editMessage / editMessageItem are correctly false on this macOS / imsg combo, and actions.ts:340-347 correctly hides edit from the action surface as a result. reply-cache.jsonl is observably 0600 on disk with parent dir 0700 — the prior security commit verified end-to-end on a live system. sent-echoes.jsonl is currently 0644 on the running dist (still pre-redeploy of the new fix(security): sent-echoes.jsonl owner-only commit) — that's exactly the gap the new commit closes; will flip to 0600 on next gateway rebuild. The warn-once telemetry was observed firing once per gateway start when applicable and not re-firing thereafter, confirming both the per-method feature detection and the fired = true de-duplication.

  • What was not tested: No live edit/unsend run on this dist (older imsg selectors do not expose editMessage so that path is gated off; the new requireFromMe enforcement was tested via unit tests against the same resolveIMessageMessageId codepath the live action handler uses). setGroupIcon against pre-private-API macOS recipients was not exercised. Cross-Mac-user attachment paths inside Tailscale relays beyond my own host were not exercised. Mocks/unit tests, CI, lint, and typechecks all pass (pnpm test:extension imessage → 257/257) but are supplemental only — the live evidence above is the primary proof.

Changed files

  • CHANGELOG.md (modified, +6/-0)
  • docs/.i18n/glossary.zh-CN.json (modified, +8/-0)
  • docs/channels/imessage-from-bluebubbles.md (added, +180/-0)
  • docs/channels/imessage.md (modified, +76/-6)
  • docs/channels/index.md (modified, +1/-1)
  • docs/docs.json (modified, +1/-0)
  • extensions/imessage/package.json (modified, +32/-1)
  • extensions/imessage/src/actions-contract.ts (added, +19/-0)
  • extensions/imessage/src/actions.runtime.test.ts (added, +185/-0)
  • extensions/imessage/src/actions.runtime.ts (added, +502/-0)
  • extensions/imessage/src/actions.test.ts (added, +499/-0)
  • extensions/imessage/src/actions.ts (added, +613/-0)
  • extensions/imessage/src/channel.ts (modified, +2/-0)
  • extensions/imessage/src/chat.ts (added, +183/-0)
  • extensions/imessage/src/config-schema.test.ts (modified, +21/-0)
  • extensions/imessage/src/imessage.test-plugin.ts (modified, +40/-1)
  • extensions/imessage/src/markdown-format.test.ts (added, +99/-0)
  • extensions/imessage/src/markdown-format.ts (added, +154/-0)
  • extensions/imessage/src/monitor-reply-cache.test.ts (added, +367/-0)
  • extensions/imessage/src/monitor-reply-cache.ts (added, +579/-0)
  • extensions/imessage/src/monitor.gating.test.ts (modified, +27/-1)
  • extensions/imessage/src/monitor/echo-cache.ts (modified, +5/-0)
  • extensions/imessage/src/monitor/inbound-processing.test.ts (modified, +189/-1)
  • extensions/imessage/src/monitor/inbound-processing.ts (modified, +83/-15)
  • extensions/imessage/src/monitor/monitor-provider.echo-cache.test.ts (modified, +79/-0)
  • extensions/imessage/src/monitor/monitor-provider.ts (modified, +149/-18)
  • extensions/imessage/src/monitor/persisted-echo-cache.ts (added, +236/-0)
  • extensions/imessage/src/monitor/reflection-guard.test.ts (modified, +16/-0)
  • extensions/imessage/src/monitor/reflection-guard.ts (modified, +4/-0)
  • extensions/imessage/src/probe.test.ts (added, +94/-0)
  • extensions/imessage/src/probe.ts (modified, +202/-7)
  • extensions/imessage/src/send.ts (modified, +61/-0)
  • extensions/imessage/src/setup-core.ts (modified, +1/-1)
  • extensions/imessage/src/test-plugin.test.ts (modified, +8/-0)
  • src/config/bundled-channel-config-metadata.generated.ts (modified, +12/-11)
  • src/config/types.imessage.ts (modified, +16/-0)
  • src/config/zod-schema.providers-core.ts (modified, +20/-0)
RAW_BUFFERClick to expand / collapse

Summary

Currently, when the gateway is down (crash, restart, mac sleep, machine off), inbound iMessage messages that arrive during that window are not delivered to the agent once the gateway comes back up. The imsg watch subscription resumes from current state and ignores anything that landed in chat.db while it was offline.

This is the inverse of #62761 (which is about unbounded replay on restart). A bounded, cursor-persisted catchup would address both.

Why it matters

  • BlueBubbles solves the equivalent gap via webhook replay + history fetch (see #66721 for context, now closed).
  • For users moving from BlueBubbles to the bundled iMessage plugin (PR #78317 / docs/channels/imessage-from-bluebubbles), losing webhook-replay-style catchup is a regression in availability semantics.
  • For long-running setups on a Mac that sleeps, every wake currently risks silently swallowed messages.

Proposed capability (sketch — happy to iterate)

  • Persist last-seen cursor. On every successfully-processed inbound ROWID, persist { accountId, lastSeenRowid, lastSeenTimestamp } to ~/.openclaw/state/imessage/inbound-cursor.json (file mode 0600, dir 0700 to match the other state files in this directory).
  • On startup, replay bounded. After the bridge is up, query chat.db for messages with ROWID > lastSeenRowid AND date > lastSeenTimestamp - <bounded window, e.g. 24h>, dispatch each through the same inbound pipeline, then resume imsg watch.
  • Bounded by recency, not just rowid. A 24h ceiling avoids the #62761 unbounded-replay failure mode if the cursor file is corrupted, deleted, or carried across machines.
  • Echo dedupe must still apply. Outbound messages we sent via imsg should not be re-ingested; the existing sent-echoes.jsonl window (currently 2 min) would need to extend to cover the catchup window, or we'd need a separate is_from_me filter at the cursor query.
  • Per-account scoping. Multi-account configs need per-account cursors.

Out of scope for this issue

  • Surfacing missed messages with a synthesized 'gateway was offline N minutes' marker (could be a follow-up).
  • Catchup for plugins other than iMessage (BlueBubbles already handles this; Telegram has long-polling cursor; etc.).

Tracking

Filed off the back of PR #78317 review. Not blocking that PR — the bundled iMessage plugin is functional without catchup; this is a feature gap to close after.

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: catchup missed inbound messages received while gateway was down [1 pull requests, 1 comments, 2 participants]