Skip to content

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

Merged
omarshahine merged 20 commits intoopenclaw:mainfrom
omarshahine:feat/imsg-plugin-private-api
May 8, 2026
Merged

feat(imessage): private-API support via imsg JSON-RPC [AI-assisted]#78317
omarshahine merged 20 commits intoopenclaw:mainfrom
omarshahine:feat/imsg-plugin-private-api

Conversation

@omarshahine
Copy link
Copy Markdown
Contributor

@omarshahine omarshahine commented May 6, 2026

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

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.

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: bluebubbles Channel integration: bluebubbles channel: imessage Channel integration: imessage size: XL maintainer Maintainer-authored PR labels May 6, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 6, 2026

Codex review: needs changes before merge.

Summary
The PR adds imsg private-API actions, typing/read receipts, echo and cache hardening, config/schema metadata, migration docs, changelog entries, and iMessage regression tests.

Reproducibility: yes. at source level. Current main lacks the new iMessage action surface and has the chat_id-only echo-scope behavior, while the PR findings are visible by comparing the latest branch code to the shared upload-file contract and upstream imsg RPC docs.

Real behavior proof
Sufficient (terminal): The PR discussion contains redacted live macOS terminal/log proof showing the imsg bridge capability path, cache modes, gateway logs, and the iMessage extension lane after the change.

Next step before merge
The remaining blockers are narrow branch-local repairs that automation can attempt; maintainer review is still required afterward because the PR is protected and changes private-API behavior.

Security
Needs attention: No dependency or CI supply-chain regression was visible, but the cache-hardening threat model still overstates what 0600/0700 modes protect.

Review findings

  • [P2] Handle path-based upload-file inputs — extensions/imessage/src/actions.ts:607-615
  • [P2] Keep formatting off the RPC send path — extensions/imessage/src/send.ts:216-237
  • [P3] Add labeler coverage for the migration page — .github/labeler.yml:55-59
Review details

Best possible solution:

Keep the implementation in the iMessage plugin, repair the shared action and formatting contracts, narrow the cache threat-model wording, then have maintainers decide whether to merge the private-API direction.

Do we have a high-confidence way to reproduce the issue?

Yes at source level. Current main lacks the new iMessage action surface and has the chat_id-only echo-scope behavior, while the PR findings are visible by comparing the latest branch code to the shared upload-file contract and upstream imsg RPC docs.

Is this the best way to solve the issue?

No as-is. The plugin-owned boundary is the right direction, but the branch should support path-based upload-file inputs, avoid unsupported formatting on RPC send, update labeler coverage, and tighten the cache threat-model wording before maintainer review.

Full review comments:

  • [P2] Handle path-based upload-file inputs — extensions/imessage/src/actions.ts:607-615
    This adapter advertises upload-file, but a normal shared action call can pass media, filePath, or path with file access through mediaAccess/mediaReadFile. Requiring a base64 buffer plus filename makes those standard upload-file calls fail before imsg sees the attachment.
    Confidence: 0.9
  • [P2] Keep formatting off the RPC send path — extensions/imessage/src/send.ts:216-237
    sendMessageIMessage strips Markdown and adds params.formatting to JSON-RPC send, but upstream imsg documents formatting on send-rich, not RPC send. Plain sends like **bold** can become unstyled bold instead of preserving the original text or using the supported rich-send path.
    Confidence: 0.87
  • [P3] Add labeler coverage for the migration page — .github/labeler.yml:55-59
    This PR adds docs/channels/imessage-from-bluebubbles.md, but the iMessage label rule still only matches the existing iMessage doc. Add the migration page to the same labeler globs so future edits route to the channel owners.
    Confidence: 0.82
  • [P3] Narrow the owner-only cache threat model — extensions/imessage/src/monitor-reply-cache.ts:132-138
    The comment says owner-only modes close a same-UID read/injection vector, but another process running as the same Unix user can still read and write owner files. Narrow the claim to other local users, or add a real integrity boundary if same-UID tampering remains in scope.
    Confidence: 0.86

Overall correctness: patch is incorrect
Overall confidence: 0.88

Security concerns:

  • [low] Owner-only modes do not block same-UID tampering — extensions/imessage/src/monitor-reply-cache.ts:132
    The PR clamps iMessage JSONL state files to owner-only mode, which helps against other local users, but a process running as the same Unix user can still read and write those files; the stated same-UID attacker remains in scope unless the claim is narrowed or integrity protection is added.
    Confidence: 0.87

Acceptance criteria:

  • pnpm test:extension imessage
  • pnpm tsgo:extensions
  • pnpm tsgo:core
  • pnpm config:channels:check
  • pnpm test:contracts:channels

What I checked:

  • Open protected PR: GitHub API reports this PR open, non-draft, head f3df4d422dac34f7c9042efd1029d63d6caf25bf, mergeable=true, mergeable_state=unstable, with protected maintainer labeling in the supplied context. (f3df4d422dac)
  • Current main lacks action adapter: Current main registers the iMessage message adapter but no channel message action adapter, so the private-API action surface is new PR behavior rather than implemented on main. (extensions/imessage/src/channel.ts:302, 6cfb08680e71)
  • Current main echo-scope gap: Current main accepts only a single echo scope string and builds group echo scope from chat_id/sender, so chat_guid/chat_identifier-scoped outbound echoes can miss inbound dedupe. (extensions/imessage/src/monitor/inbound-processing.ts:93, 6cfb08680e71)
  • PR upload-file contract mismatch: The PR advertises upload-file but the handler requires filename plus base64 buffer, so normal path-based upload-file calls fail before reaching imsg. (extensions/imessage/src/actions.ts:607, f3df4d422dac)
  • Shared upload-file shape: Existing channel action code accepts media, filePath, or path and carries mediaAccess/mediaReadFile through the action context, which the PR's iMessage upload-file path does not use. (extensions/googlechat/src/actions.ts:111, 6cfb08680e71)
  • PR sends unsupported formatting over RPC send: The PR strips Markdown, adds params.formatting, and sends through JSON-RPC send; upstream imsg docs list formatting on send-rich, not the RPC send method. (extensions/imessage/src/send.ts:216, f3df4d422dac)

Likely related people:

  • steipete: Existing ClawSweeper history context routes iMessage send/channel plumbing and the imsg dependency surface here; the PR also depends on steipete/imsg private-API behavior. (role: recent maintainer and upstream dependency owner; confidence: medium; commits: 05eda57b3c72, 235d06bff13d, 95331e5cc52d; files: extensions/imessage/src/send.ts, extensions/imessage/src/channel.ts, docs/channels/imessage.md)
  • obviyus: Existing review history ties recent main work on iMessage self-chat and echo handling to the inbound-processing path changed by this PR. (role: recent inbound and echo-path maintainer; confidence: medium; commits: e3e2a19ab7f1, 1ee4a1606e9a, 4a5885df3a36; files: extensions/imessage/src/monitor/inbound-processing.ts)
  • vincentkoc: Local git history shows recent current-main iMessage plugin, config, and docs commits by Vincent Koc, including the current supported imsg setup path and BlueBubbles deprecation docs. (role: recent iMessage docs and plugin maintainer; confidence: high; commits: 4ad4be9aff37, 84638bfbb07b, 91ed1604b011; files: extensions/imessage/src/send.ts, extensions/imessage/src/probe.ts, docs/channels/imessage.md)

Remaining risk / open question:

  • Maintainer/product approval is still needed for the private-API/SIP support direction and BlueBubbles replacement framing.
  • The live proof is strong but I did not run a local macOS/imsg live validation in this read-only review; the remaining blockers are source-contract findings.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 6cfb08680e71.

@omarshahine omarshahine force-pushed the feat/imsg-plugin-private-api branch 2 times, most recently from 14eff53 to cc5e482 Compare May 6, 2026 17:44
@omarshahine
Copy link
Copy Markdown
Contributor Author

Update: rebased onto current origin/main (resolved CHANGELOG.md conflict adding our 3 entries alongside the new Telegram, hooks, Nix, and Codex entries from main) and added a migration guide.

New commit: docs(imessage): add Coming from BlueBubbles migration guide

  • New docs/channels/imessage-from-bluebubbles.md — when to migrate, prerequisites, full BB→iMessage config translation table, dry-run + cutover + verification flow, action parity matrix, dual-running caveat (BlueBubbles preferOver still wins), rollback steps.
  • Cross-linked from docs/channels/imessage.md Related section.
  • Registered in docs/docs.json sidebar nav.
  • Added iMessage + Coming from BlueBubbles to docs/.i18n/glossary.zh-CN.json so pnpm docs:check-i18n-glossary passes.

No code, no behavior, no edits to extensions/bluebubbles/**.

Local re-verification on the rebased tree:

  • pnpm test:extension imessage → 250/250
  • pnpm docs:check-i18n-glossary → green
  • pnpm exec oxfmt --check → green
  • git merge-tree origin/main HEAD → no conflicts

@omarshahine
Copy link
Copy Markdown
Contributor Author

omarshahine commented May 6, 2026

Review follow-ups landed (4 commits). Summary:

fix(security): sent-echoes.jsonl owner-only (0600/0700) — 364992c506
Same threat model as reply-cache.jsonl. Same hardening pattern: clamp file to 0600, parent dir to 0700 on every write/append, plus chmod existing entries from older gateway versions. Two regression tests: fresh-write and clamp-on-upgrade.

fix(imessage): add probeTimeoutMs to zod schema — 98b5402717
channels.imessage.probeTimeoutMs was declared on IMessageAccountConfig but missing from IMessageAccountSchemaBase, so user-supplied values were silently stripped at zod parse and the runtime always fell back to DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS. Added the field, regenerated bundled-channel-config-metadata. Pre-existing on main, called out in CHANGELOG ### Fixes.

fix(security): gate iMessage edit/unsend on isFromMe — f6e108343a
Reply cache was inbound-only — every entry came from monitor/inbound-processing.ts, representing a message received from another participant. edit/unsend resolved agent-supplied message ids through this cache and dispatched against any guid that came back, with chat-scope verification but no sender check. An agent in group chat G could call unsend with a short id pointing at a human participant's message in G.

Fix: adds isFromMe?: boolean to IMessageReplyCacheEntry, plumbs false through the inbound caller, adds an outbound caller in send.ts recording true after a successful imsg send, and adds a requireFromMe option to resolveIMessageMessageId. edit and unsend handlers pass requireFromMe: true. Missing values (legacy persisted entries) treated as not-from-me — the safe default.

Tests cover: rejection of inbound short id under requireFromMe, allow-self short id, rejection of uncached full guid, rejection of legacy persisted entries with no isFromMe field.

refactor(imessage): persisted echo cache append-only + monotonic clamp — 9210be0e26
Two related changes. (1) monitor/persisted-echo-cache.ts switches the hot path from full file rewrite on every send to appendFileSync, with periodic compaction triggered only when the on-disk file grows past 2× cap or holds entries beyond TTL. Group-chat bursts that fired 5+ outbound back-to-back used to write the entire 256-entry file 5+ times; now they're 5 line-appends plus one compaction when due. (2) monitor-reply-cache.ts:appendPersistedEntry was conditionally chmod'ing only on file creation, leaving older 0644 files untouched. Always clamps now — appendFileSync's mode only applies on creation, so chmod is the only path to tighten an existing file. Microseconds of overhead, monotonic guarantee. Adds a clamp-from-pre-existing-0644 regression test for the reply-cache hardening.

Local validation

  • pnpm tsgo:core / tsgo:extensions / tsgo:extensions:test → green
  • pnpm test:extension imessage → 257/257 (was 250 pre-review; +6 isFromMe + 1 reply-cache clamp test = +7)
  • pnpm test:contracts:channels → 29/29
  • pnpm config:channels:check → green
  • pnpm exec oxfmt --check on all 43 changed files → green
  • pnpm lint:extensions → 1 unrelated error in extensions/codex/src/app-server/run-attempt.test.ts:58 (pre-existing on origin/main, verified by checking out that file from main and lint-ing in isolation; same error)

Out of scope (filed separately)

Re-review progress:

@omarshahine
Copy link
Copy Markdown
Contributor Author

omarshahine commented May 6, 2026

Real behavior proof — live macOS Sequoia 15.x, imsg 0.6.0

Captured 2026-05-06 from the actively-running gateway (PID 47600 at capture time, dist built from this branch's tip prior to the four follow-up commits — those land on next rebuild). All identifiers below redacted.

imsg bridge capability

$ imsg --version
0.6.0

$ imsg status --json | jq '...'
{
  "bridge_version": 2,
  "v2_ready": true,
  "typing_indicators": true,
  "read_receipts": true,
  "basic_features": true,
  "advanced_features": 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
  },
  "sip": "disabled"
}

Per-method capability gating is doing its job: retractMessagePart: trueunsend is exposed; editMessage: false AND editMessageItem: falseedit is correctly hidden by actions.ts:340-347 since neither selector is available on this build/macOS combo. typing and read are present in rpc_methods so the typing-bubble + mark-read paths are active.

Cache file modes (the security hardening, on disk)

$ 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/
  • reply-cache.jsonl is 0600 + parent dir 0700 — the original fix(security): reply-cache.jsonl owner-only commit verified working on the live system.
  • sent-echoes.jsonl is currently 0644 — exactly the gap the new fix(security): sent-echoes.jsonl owner-only commit (364992c506) closes. Will flip to 0600 on next gateway rebuild.

Reply cache content shape (one entry, redacted)

{
  "accountId": "default",
  "shortId": "25",
  "messageId": "<GUID>",
  "chatGuid": "<REDACTED>",
  "chatIdentifier": "<REDACTED>",
  "chatId": "<int>",
  "timestamp": 1778049001439
}

13 live entries, allocated shortIds currently up to 27. isFromMe field absent from these (pre-dates the new outbound writer in send.ts); once the new dist is deployed, fresh entries will carry it and the legacy entries are correctly treated as not-from-me by the requireFromMe guard.

Recent log evidence — production code paths firing

The warn-once one-time message we coded in monitor-provider.ts:107-129 fired in production yesterday when an older imsg build was running:

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).

After upgrading to imsg 0.6.0, the warning has not re-fired and typing / read are present in rpc_methods (above) — confirming both the per-method feature detection and the warn-once de-duplication work as designed.

The bluebubbles plugin reply-context cache shows live test traffic from earlier in the cutover:

2026-05-04T21:10 [bluebubbles] reply-context cache hit
  replyToId=<GUID> sender=me body="OpenClaw imsg Lobster live test after echo fix. Please reply 'received'."

(That message was the canary used to verify the chat_guid-scope echo dedupe fix from fix(imessage): match echoes across chat_guid/chat_identifier scopes.)

Test summary (this branch)

$ pnpm test:extension imessage
Test Files  30 passed (30)
Tests       257 passed (257)

Pre-existing red, scoped proof

pnpm lint:extensions reports 1 error in extensions/codex/src/app-server/run-attempt.test.ts:58 (unused createParamsWithRuntimePlan). Verified pre-existing on origin/main by checking out that file's main version in isolation and re-linting — same error. Not introduced by this PR.


That's the substantive evidence. The actively-running gateway has been driving this code on a real Mac with a real imsg install and a real Messages.app session for the duration of this PR; the per-method capability gating, warn-once telemetry, and 0600 reply-cache are all observable in production state, and the only gap visible (sent-echoes 0644) is exactly what the follow-up commits address.

Re-review progress:

@omarshahine
Copy link
Copy Markdown
Contributor Author

omarshahine commented May 7, 2026

Follow-up commit beefed7d67docs(imessage): document the silent group-drop landmine.

Lobster (this PR's bundled-iMessage live-test rig) had been silently dropping every group message since the BlueBubbles → bundled-iMessage migration. Diagnosed today: the bundled iMessage plugin runs two separate group allowlist gates back-to-back, and only the first one (groupAllowFrom) was satisfied. Gate two (channels.imessage.groups registry, called from extensions/imessage/src/monitor/inbound-processing.ts:336-340) requires either a groups: { "*": { ... } } wildcard or an explicit per-chat_id entry; the BB config had carried that wildcard but the migration translation skipped it.

The drop's only signal is params.logVerbose?.(\imessage: skipping group message (${groupId}) not in allowlist`), which is invisible at the default info` log level — DMs kept working through a different code path, so nothing surfaced.

This commit documents the failure mode in two places:

  • docs/channels/imessage-from-bluebubbles.md — new "Group registry footgun" section, the cutover example config now includes groups: { "*": { "requireMention": true } }, and a new group-specific verification step in the cutover flow ("DMs and groups take different code paths — DM success does not prove groups are routing").
  • docs/channels/imessage.md — Warning callout in the Group policy tab covering the dual-gate behavior and OPENCLAW_LOG_LEVEL=debug as the first-line diagnostic.

Filed #78749 to raise the underlying log severity / surface the misconfiguration at gateway start so the next operator who hits this isn't debugging blind. The docs land first because the bug fix is a separate change that needs its own review.

Re-review progress:

@openclaw-barnacle openclaw-barnacle Bot added the channel: bluebubbles Channel integration: bluebubbles label May 7, 2026
@omarshahine omarshahine marked this pull request as ready for review May 7, 2026 05:06
@omarshahine omarshahine force-pushed the feat/imsg-plugin-private-api branch from 9b48f82 to 37832b5 Compare May 7, 2026 13:49
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 7, 2026
@omarshahine omarshahine force-pushed the feat/imsg-plugin-private-api branch from 37832b5 to 6ccd21b Compare May 7, 2026 15:41
@omarshahine omarshahine force-pushed the feat/imsg-plugin-private-api branch from d716c79 to e2305ce Compare May 7, 2026 20:54
@omarshahine omarshahine force-pushed the feat/imsg-plugin-private-api branch from 721183a to f3df4d4 Compare May 8, 2026 02:01
omarshahine added a commit to omarshahine/openclaw that referenced this pull request May 8, 2026
omarshahine and others added 20 commits May 8, 2026 02:06
Wires the bundled iMessage plugin to drive imsg over JSON-RPC instead of
treating it as a passive AppleScript shim. The plugin now reaches the
private-API surface that imsg exposes through its dylib bridge, while
remaining safe against older imsg builds that lack newer methods.

Capability gating
- probe.ts reads the rpc_methods array from imsg status --json and exposes
  imessageRpcSupportsMethod so each consumer can feature-detect per method
  instead of pinning a CLI version. Older imsg builds (no rpc_methods) are
  treated as unsupported for newly-named methods only.

Outbound action surface
- New actions.ts plus actions.runtime.ts give the plugin a full action set:
  react, edit, unsend, reply, sendWithEffect, renameGroup, setGroupIcon,
  addParticipant, removeParticipant, leaveGroup, sendAttachment.
  Private-API actions throw a clear error when the bridge is unavailable
  rather than silently degrading to AppleScript.
- send.ts gains private-API send paths and unifies how outbound media is
  staged.

Inbound dispatch
- monitor-provider switches from createReplyDispatcher to
  createReplyDispatcherWithTyping and feeds it the typingCallbacks the SDK
  pipeline produces, so a typing bubble is shown to the sender while the
  agent generates and stops cleanly on idle.
- Each accepted inbound chat is marked read before dispatch when the bridge
  is up and sendReadReceipts is not explicitly disabled, so users see
  "Read" instead of "Delivered".
- New chat.ts module hosts the typing/read/markUnread/create/delete plus
  group rename/setIcon/addParticipant/removeParticipant/leave helpers, all
  routed through the persistent imsg rpc client.

Resilience
- probe.ts no longer pins a permanently-false private-API status: the cache
  only short-circuits on confirmed availability, and a lazy probe fires on
  first action when readiness flips. monitor-provider re-runs the probe
  after watch.subscribe so the cached status reflects what actually came up.
- Reply caching, reflection-guarding, and persisted echo dedupe were tuned
  to keep the gateway from talking to itself when imsg surfaces both sides
  of a conversation through chat.db (full set of regression tests added).

Schema
- IMessageAccountSchemaBase gains optional per-action toggles and a
  sendReadReceipts boolean. Bundled channel config metadata regenerated.

Docs
- docs/channels/imessage.md updated with the private-API setup steps,
  capability discovery, and the relationship between the plugin and
  steipete/imsg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Outbound iMessage `send` writes echo-cache scope keys keyed on whichever
target shape the caller used: `chat_id:N`, `chat_guid:<guid>`,
`chat_identifier:<id>`, or `imessage:<handle>` (see
resolveOutboundEchoScope in send.ts). Inbound echo detection only built
the chat_id-flavored scope for groups (and `imessage:<sender>` for DMs),
so a send addressed by chat_guid would never match the chat_id-only
inbound lookup, the agent would re-process its own message as fresh
input, and command loops became reachable.

Mirror every persistable scope shape on the inbound side and probe all
candidate scopes inside hasIMessageEchoMatch. Existing chat_id-only
flows are unaffected (single scope, single probe). Adds regression
tests covering chat_guid-scoped, chat_identifier-scoped, and chat_id-
scoped echoes plus a non-match guard against unrelated chat_guid
entries.
reply-cache.jsonl maps gateway-allocated short-ids to message guids.
With default mode 0644 + parent dir 0755, a hostile same-UID process
on a multi-user host could (a) read the file to enumerate active
conversation guids or (b) inject lines so a future shortId resolution
returned an attacker-chosen guid — letting the agent react/edit/unsend
a message it never saw. Clamp file to 0600 and dir to 0700 on every
write/append, plus chmod existing entries from older gateway versions.
Three Unreleased entries: feature note for the imsg JSON-RPC private-API
surface, fix for chat_guid/chat_identifier echo dedupe, and security fix
for reply-cache.jsonl owner-only permissions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-fixes from `pnpm lint:extensions --fix` plus three manual cleanups:
drop the unused `readMessageId` helper (superseded by
`readMessageIdWithChatFallback`), drop the stale `WORD` regex constant,
and drop the unused `vi` import in `monitor-reply-cache.test.ts`.

No behavior change — every reformatted block was already curly-equivalent
or already collapsed a redundant `=== true` / `!== true` to direct boolean
use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New docs/channels/imessage-from-bluebubbles.md covers:
- when migration makes sense (and when to stay on BlueBubbles)
- before-you-start prerequisites (imsg install, private API probe,
  config snapshot)
- full BB to bundled-iMessage config-key translation table
- step-by-step dry-run + cutover + verification flow
- action parity matrix
- pairing/session/ACP-binding migration notes
- dual-running caveat (BlueBubbles preferOver still wins)
- rollback steps

Cross-linked from docs/channels/imessage.md Related section. Registered
in docs/docs.json sidebar nav alongside the other channel pages, and
added "iMessage" + "Coming from BlueBubbles" to docs/.i18n/glossary.zh-CN.json
so docs:check-i18n-glossary passes.

No code changes, no behavior changes, no edits to extensions/bluebubbles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sent-echoes.jsonl carries scope keys, outbound message text, and
messageIds. With default mode 0644 + parent dir 0755, a hostile same-UID
process on a multi-user host could (a) read the file to enumerate active
conversations and outbound content, or (b) inject lines so a future
inbound dedupe call wrongly suppresses a legitimate inbound message.

Mirrors the reply-cache.jsonl hardening from the prior security commit:
clamp file to 0600 and dir to 0700 on every write, plus chmod existing
entries written by older gateway versions. Adds regression tests for
fresh-write and clamp-on-upgrade paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
IMessageAccountConfig (src/config/types.imessage.ts:82) declares
probeTimeoutMs?: number, and actions.ts / probe.ts both read it as
account.config.probeTimeoutMs. But the field was missing from
IMessageAccountSchemaBase, so any value in user config was silently
stripped at zod parse time and the runtime always fell back to
DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS.

Adds the field between mediaMaxMb and textChunkLimit (matching the
placement in the TS type), regenerates bundled-channel-config-metadata,
and adds a CHANGELOG ### Fixes entry.

Also documents the sent-echoes.jsonl 0600/0700 hardening alongside the
existing security entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reply cache was previously inbound-only — every entry came from
monitor/inbound-processing.ts and represented a message the gateway
received from another participant. The edit and unsend actions resolved
agent-supplied message ids through this cache and dispatched the bridge
call against any guid that came back, with chat-scope verification but
no sender check.

That left a permission boundary gap: an agent authorized for group chat
G could call edit/unsend with a short id pointing at a message a human
participant sent in G. Messages.app enforces sender-only at the OS
level so the bridge would likely fail, but failing earlier in the
plugin produces a clean error and avoids dispatching a guaranteed-to-
fail bridge call.

Changes:
- Add `isFromMe?: boolean` to IMessageReplyCacheEntry. Persists through
  the JSONL round-trip; missing values from older gateway versions are
  treated as the safe default (not-from-me).
- monitor/inbound-processing.ts: existing call now records
  `isFromMe: false`.
- send.ts: new outbound call after a successful imsg send records
  `isFromMe: true` keyed by the resolved chat target.
- monitor-reply-cache.ts:resolveIMessageMessageId gains an optional
  `requireFromMe` flag that throws a clear "not one this agent sent"
  error when the cached entry is `isFromMe: false` (or absent).
- actions.ts: edit and unsend handlers pass `requireFromMe: true`.
  React, reply, sendWithEffect, sendAttachment, and group ops are
  unchanged — they don't have the same retroactive-mutation concern.

Adds regression tests covering: rejection of inbound short id under
requireFromMe, allow-self short id, rejection of uncached full guid,
rejection of legacy persisted entries with no isFromMe field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related changes to the iMessage state-cache plumbing:

1. persisted-echo-cache.ts: switch the hot path from full file rewrite
   on every send to appendFileSync, with periodic compaction triggered
   only when the on-disk file grows past 2x MAX_PERSISTED_ECHO_ENTRIES
   or holds entries beyond the TTL window. Group-chat bursts that fired
   5+ outbound messages back-to-back used to write the entire 256-entry
   file 5+ times; now they're 5 line-appends plus one rewrite when
   compaction is due. Mirror stays in sync via direct in-memory append
   plus the existing mtime-based invalidation on reads.

2. monitor-reply-cache.ts: appendPersistedEntry was conditionally
   chmod'ing only on file creation, leaving older 0644 files from
   pre-hardening gateway versions unchanged. Always clamp now —
   appendFileSync's `mode` parameter only applies on creation so the
   chmod is the only path to tighten an existing file. chmod is
   microseconds; doing it every append keeps the security guarantee
   monotonic instead of conditional on creation order.

Adds a clamp-from-pre-existing-0644 regression test for the reply-cache
hardening and parallels the same test for sent-echoes from the prior
security commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bundled iMessage plugin runs two separate group allowlist gates
back-to-back, and both must pass for a group message to reach the
agent:

1. Sender / chat-target allowlist via channels.imessage.groupAllowFrom.
2. Group registry via channels.imessage.groups. With
   groupPolicy: "allowlist", this gate requires either a
   groups: { "*": { ... } } wildcard entry (sets allowAll=true) or an
   explicit per-chat_id entry.

If gate 2 has nothing in it, every group message is dropped and the
rejection logs only at verbose/debug level
(extensions/imessage/src/monitor/inbound-processing.ts:336-340). At the
default info log level the drops are silent. DMs continue to work
because they take a different code path.

This is the most common BlueBubbles -> bundled-iMessage migration
failure: BB config typically carries
groups: { "*": { "requireMention": true } }, which sets allowAll=true
in the registry resolver. Operators copying the migration translation
table copy groupAllowFrom and groupPolicy but skip the groups block
because it looks like an unrelated mention setting.

- docs/channels/imessage-from-bluebubbles.md: new "Group registry
  footgun" section between the config-translation table and step-by-
  step. Cutover example config now includes the groups block. New
  group-specific verification step in the cutover flow (DM success
  doesn't prove groups are routing).
- docs/channels/imessage.md: Warning callout in the Group policy tab
  documenting the dual-gate behavior and how to debug a suspected
  silent drop with OPENCLAW_LOG_LEVEL=debug.
- docs/.i18n/glossary.zh-CN.json: BlueBubbles, Pairing, Channel Routing
  glossary entries so docs:check-i18n-glossary passes.

Tracker for raising the drop's log severity so it isn't silent at info:
openclaw#78749

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…effect aliases

Two issues caught by `codex review --base origin/main` against the PR:

1. resolveIMessageMessageId was reading the in-memory short-id maps
   without hydrating from disk first. rememberIMessageReplyCache hydrates
   on its own write path, but a resolve call that fired before any
   remember (the natural post-restart sequence: agent gets an inbound
   message with a MessageSid that was issued and persisted before the
   restart, then tries to react/reply/edit/unsend) would miss the
   persisted JSONL and throw "no longer available". Add a
   hydrateFromDiskOnce() call at the top of the resolver. New regression
   test simulates the post-restart sequence (issue, _reset, restore
   JSONL, resolve) and asserts the short id resolves.

2. effectIdFromParam advertised five screen-effect aliases — echo,
   happybirthday, shootingstar, sparkles, spotlight — in its error
   message, but those weren't in the alias map. Agents directed by our
   own error message to "Use one of: ... echo, happybirthday, ..."
   were getting "unknown effect" thrown back. Add the missing aliases
   pointing at the canonical com.apple.messages.effect.CK*Effect
   identifiers (and the dashed "happy-birthday" / "shooting-star"
   variants for forgiving input). New parametrized regression test
   covers all five aliases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… docs, add catchup gap

Three corrections to the iMessage / BlueBubbles channel docs that
land together because they're the same factual fix:

1. Both channels require disabling SIP, not just BlueBubbles.

   The previous framing claimed bundled iMessage reached the advanced
   action surface "without requiring SIP disabled". That was wrong.
   imsg's README is explicit: "Advanced features such as read, typing,
   launch, bridge-backed rich send, message mutation, and chat
   management are opt-in. They require SIP to be disabled and a helper
   dylib to be injected into Messages.app. imsg launch refuses to
   inject when SIP is enabled." The injection technique is a port of
   the BlueBubbles private-API surface into imsg's own dylib, so the
   underlying Apple constraint (no third-party access to internal
   IMCore functions with SIP on) applies identically to both channels.

   - imessage.md: drops the "does not require disabling SIP" claim
     from the side-by-side comparison Note. Adds a "**Both channels
     require disabling SIP**" paragraph spelling out the parity in
     prerequisites.
   - imessage-from-bluebubbles.md: drops the "you can re-enable SIP"
     reason for migrating, replaces with a "What does NOT change with
     this migration" section that lists SIP, FDA, Automation
     permission, and macOS-version caveats as constants between the
     two channels.
   - bluebubbles.md: reverts the "Enabling the BlueBubbles Private
     API" section that I added earlier in this PR. Per maintainer
     direction, that content belongs with the iMessage plugin docs
     (we're documenting the operational requirements once, in the
     channel that actually needs them written down here, since the
     BlueBubbles upstream guide already covers the BB side
     authoritatively).

2. New "Enabling the imsg private API" section in imessage.md.

   Sits between "Requirements and permissions (macOS)" and "Access
   control and routing". Covers the basic-mode vs Private-API-mode
   split, the macOS-version-specific SIP-disable flow (defers to the
   BlueBubbles upstream guide as the canonical step-by-step since the
   SIP procedure itself is identical), the imsg launch injection
   step, the OpenClaw-side verification flow, and a "When you can't
   disable SIP" subsection covering basic-mode fallback options.

3. Catchup gap noted as a missing-vs-BlueBubbles parity item.

   Bundled iMessage does not yet deliver inbound messages that
   arrived while the gateway was down — imsg watch resumes from
   current chat.db state. BlueBubbles handles this through webhook
   replay + history fetch. This is the most operationally
   significant parity gap for production deployments and the docs
   should be honest about it. Tracked at openclaw#78649.

   - imessage.md: new "Known gaps where bundled iMessage is not yet
     at parity with BlueBubbles" subsection in the parity Note.
   - imessage-from-bluebubbles.md: catchup row added to the action
     parity matrix with operational guidance ("if your deployment is
     sensitive to that, stay on BlueBubbles until openclaw#78649 lands").

Same-sender DM coalescing is also flagged in both places as a
not-yet-ported BlueBubbles feature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…I-assisted]

Apple's Messages app splits a single composition (`<command> <URL>`,
attachment + caption) into multiple chat.db rows ~0.8-2.0 s apart. Without
coalescing, the agent sees the command alone on turn 1 (often replies
"send me the URL") and the URL on turn 2 with the command context lost.
Same root cause BlueBubbles already fixes via coalesceSameSenderDms; the
imsg plugin reads the same chat.db so it inherits the same problem.

Wires the existing schema flag end-to-end:
- monitor-provider: opt-in DM-only key, group instant-dispatch gate, merge
  helper on flush, 2500 ms default debounce window when flag enabled with
  no explicit messages.inbound.byChannel.imessage override
- coalesce.ts: combineIMessagePayloads helper mirroring BB's caps and
  semantics (4000 char text cap, 20 attachment cap, 10 entry cap with
  first+latest preserved, GUID tracking, reply-context preference).
  Mirrors BB on purpose so a future SDK lift is mechanical.
- types.imessage.ts: matching TS field with doc comment

Group chats keep instant per-message dispatch so multi-user turn structure
is preserved. From-me messages stay on the cache-only path. Attachments
are now preserved on merge instead of nulled (the legacy onFlush dropped
them).

Also fixes a Mintlify rendering bug in docs/channels/imessage.md where
unindented code fences inside <Accordion> blocks were collapsing the
AccordionGroup into one paragraph.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lint:tmp:no-random-messaging blocked CI on this branch — withTempFile
was reaching for node:os tmpdir(). Swap to resolvePreferredOpenClawTmpDir
from openclaw/plugin-sdk/temp-path, matching the discord/zalo pattern.
@omarshahine omarshahine force-pushed the feat/imsg-plugin-private-api branch from f3df4d4 to b7d336b Compare May 8, 2026 02:14
@omarshahine omarshahine merged commit e259751 into openclaw:main May 8, 2026
110 checks passed
@omarshahine
Copy link
Copy Markdown
Contributor Author

Merged via squash.

Thanks @omarshahine!

@omarshahine omarshahine deleted the feat/imsg-plugin-private-api branch May 8, 2026 02:20
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…penclaw#78317)

Merged via squash.

Prepared head SHA: b7d336b
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
rogerdigital pushed a commit to rogerdigital/openclaw that referenced this pull request May 9, 2026
…penclaw#78317)

Merged via squash.

Prepared head SHA: b7d336b
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: bluebubbles Channel integration: bluebubbles channel: imessage Channel integration: imessage docs Improvements or additions to documentation maintainer Maintainer-authored PR proof: sufficient ClawSweeper judged the real behavior proof convincing. size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iMessage: pass reply_to_guid to imsg for native threaded replies

1 participant