Skip to content

Add native iMessage poll support#125

Open
veteranbv wants to merge 4 commits into
openclaw:mainfrom
veteranbv:native-imessage-polls
Open

Add native iMessage poll support#125
veteranbv wants to merge 4 commits into
openclaw:mainfrom
veteranbv:native-imessage-polls

Conversation

@veteranbv
Copy link
Copy Markdown

@veteranbv veteranbv commented May 24, 2026

Closes #124.

Summary

This adds native Apple Messages poll support to imsg.

  • decode native Polls extension balloons from history and watch
  • decode vote update rows and tie them back to the original poll
  • expose stable JSON events for imessage.poll.created, imessage.poll.voted, and imessage.poll.unknown
  • add imsg poll send plus poll.send and messages.poll.send RPC methods
  • support native poll replies with imsg poll send --reply-to <message-guid> and the matching RPC reply fields
  • expose reply_to_guid, reply_to_text, reply_to_sender, thread_originator_guid, and thread_originator_part so clients can correlate threaded polls and replies
  • build the native Messages layout envelope needed for sent polls to render and deliver as real Apple polls
  • add fixture-based decoder tests and command/RPC coverage
  • document the JSON, history, watch, and RPC surfaces

The JSON shape is additive. Existing message fields are preserved, and non-poll messages should keep their current behavior.

Testing

  • make build-dylib
    • passed, with the existing unarchiveObjectWithData: deprecation warning
  • make test
    • 257 tests passed
  • git diff --check
    • passed
  • make lint
    • swift format lint passed
    • stopped because swiftlint is not installed locally

I also validated the native send path on a SIP-disabled macOS 26.5 machine against an existing 1:1 iMessage chat that already contained Apple Messages polls.

Manual validation covered:

  • bridge status showed v2_ready=true, advanced_features=true, and selectors.pollPayloadMessage=true
  • CLI poll send delivered and rendered as a native Messages poll
  • history decoded the sent poll as imessage.poll.created with question, options, and option IDs
  • recipient vote decoded as imessage.poll.voted with option ID, participant, original poll GUID, and poll GUID
  • a more complex five-option poll delivered, rendered, and decoded correctly
  • RPC poll.send returned ok=true and produced a delivered native poll
  • imsg poll send --reply-to <message-guid> delivered a native poll reply
  • the threaded poll row had reply_to_guid, thread_originator_guid, and thread_originator_part present, with history resolving reply_to_text and reply_to_sender

The first version of the send path produced a local poll row but did not deliver. The issue was that the payload contained the poll data but not the native layout envelope Messages expects. The final implementation includes the layout metadata seen in Apple-created poll payloads, including MSMessageTemplateLayout, liveLayoutInfo, userInfo, ldtext, and related Polls markers.

Follow-up review fixes:

  • poll payload BLOB columns are gated to poll candidate rows in message queries
  • vote rows no longer infer poll.creator from the vote sender
  • poll reply sends use the same IMMessageItem-first thread-originator path as rich message threading, rather than a separate plugin payload path

@clawsweeper
Copy link
Copy Markdown

clawsweeper Bot commented May 24, 2026

Codex review: needs maintainer review before merge. Reviewed May 27, 2026, 12:15 PM ET / 16:15 UTC.

Summary
The branch adds native Apple Messages poll decoding and sending across history/watch, CLI/RPC, injected helper payload construction, docs, and fixture-based tests.

Reproducibility: not applicable. this is a feature PR, not a bug report. The contributor supplied real macOS 26.5 terminal proof for delivered native polls, decoded created/vote events, and threaded poll replies.

Review metrics: 2 noteworthy metrics.

  • Changed surface: 35 files, +2687/-104. The PR spans core decoding, the injected helper, CLI/RPC APIs, docs, build scripts, and tests, so maintainer review should focus on the feature boundary.
  • Bridge build paths: 3 build paths add AppKit. The injected helper now links AppKit in local, universal, and notarized builds for poll preview rendering.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Risk before merge

  • Native poll send relies on private Apple Messages Polls payload shape and IMCore selectors proven on one SIP-disabled macOS 26.5 setup; maintainers need to decide whether that compatibility boundary is acceptable.
  • If Apple changes the layout envelope or threaded reply expectations, the new CLI/RPC send path could create local-only, non-rendering, or non-delivered poll rows despite the current proof.

Maintainer options:

  1. Accept private Polls support
    Merge after maintainer approval that native Polls are worth owning despite private payload and selector compatibility risk.
  2. Ask for broader upgrade proof
    Pause for additional macOS/version or fresh-install versus upgrade proof if maintainers want more confidence before taking the private API surface.
  3. Land decode before send
    If native sending is too risky, split or revise the branch to keep poll decoding/docs while deferring CLI/RPC poll sending.

Next step before merge
Remaining action is maintainer approval of the private Polls/IMCore compatibility and product boundary, not an automated code repair.

Security
Cleared: No concrete security or supply-chain issue found; the diff adds no new workflows, dependencies, or secrets handling, and poll payload decoding uses restricted structured parsing.

Review details

Best possible solution:

Land this only if maintainers are comfortable owning native Polls as a private-IMCore bridge feature, with the current additive JSON shape, fixture coverage, docs, and runtime proof preserved.

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

Not applicable: this is a feature PR, not a bug report. The contributor supplied real macOS 26.5 terminal proof for delivered native polls, decoded created/vote events, and threaded poll replies.

Is this the best way to solve the issue?

Unclear: the implementation is cohesive and I found no discrete blocking defect, but native sending relies on private Apple Polls payloads and IMCore selectors, so the best path depends on maintainer approval of that support boundary.

AGENTS.md: found and applied where relevant.

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

Label changes

Label changes:

  • add rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • remove rating: 🐚 platinum hermit: Current PR rating is rating: 🦞 diamond lobster, so this older rating label is no longer current.

Label justifications:

  • P3: This is a low-urgency feature addition with strong proof but limited immediate blast radius.
  • merge-risk: 🚨 compatibility: The PR adds private Apple Messages Polls payload construction and IMCore selector use that may vary across macOS versions.
  • merge-risk: 🚨 message-delivery: The new native poll send path can affect whether outgoing poll rows render and deliver correctly.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • feature: ✨ showcase: ClawSweeper spotlight: unusually compelling feature idea for maintainer attention. Native iMessage polls are a compelling approval primitive for mobile agent workflows and safer one-tap decisions.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): Sufficient: PR comments include screenshot plus redacted terminal and JSON output from a real SIP-disabled macOS 26.5 Messages setup showing after-fix native poll delivery, decoding, RPC send, and threaded reply metadata.
  • proof: sufficient: Contributor real behavior proof is sufficient. Sufficient: PR comments include screenshot plus redacted terminal and JSON output from a real SIP-disabled macOS 26.5 Messages setup showing after-fix native poll delivery, decoding, RPC send, and threaded reply metadata.
Evidence reviewed

What I checked:

Likely related people:

  • Omar Shahine: Introduced the BlueBubbles private-API bridge and later fixed macOS 26 bridge regressions in the injected helper and bridge command/RPC surface touched by this PR. (role: private bridge feature owner; confidence: high; commits: c56c24d488ef, 2d7b506d1736; files: Sources/IMsgHelper/IMsgInjected.m, Sources/imsg/Commands/BridgeMessagingCommands.swift, Sources/imsg/RPCServer.swift)
  • Peter Steinberger: Refactored message row selection and RPC payload mapping, added broad chat routing hints, and has recent release/history ownership around the files this PR extends. (role: message store and routing contributor; confidence: high; commits: 5a9f5441b977, 6a05484ae6b1, 327829a819fc; files: Sources/IMsgCore/MessageStore+Messages.swift, Sources/IMsgCore/Models.swift, Sources/imsg/OutputModels.swift)
  • Ru: Introduced thread_originator_guid output, which is adjacent to the threaded poll reply metadata extended here. (role: thread metadata contributor; confidence: medium; commits: 057b7c5a91a0; files: Sources/IMsgCore/MessageStore+Messages.swift, Sources/IMsgCore/Models.swift, Sources/imsg/OutputModels.swift)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f5c812dfa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +368 to +369
let payloadData = try dataValue(row, columns.payloadData)
let messageSummaryInfo = try dataValue(row, columns.messageSummaryInfo)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid copying poll blobs for every decoded message row

decodeMessageRow now unconditionally materializes payload_data and message_summary_info into Data for every row, even though MessagePollDecoder.decode immediately returns nil for non-poll rows (balloon_bundle_id not Polls and associated_message_type != 4000). In large histories/watch streams this forces unnecessary BLOB reads/copies for unrelated messages (including large extension payloads), which can noticeably increase memory and latency. Gate these reads behind a cheap poll-candidate check (or make the columns lazy) so non-poll traffic keeps previous performance characteristics.

Useful? React with 👍 / 👎.

Comment thread Sources/IMsgCore/MessagePolls.swift Outdated

let originalGUID = normalizedAssociatedGUID(associatedMessageGUID)
let senderHandle = sender.isEmpty ? nil : sender
let creator = facts.creator ?? senderHandle
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not infer poll creator from the vote sender

For vote rows that do not carry creator* fields, creator is currently backfilled from sender, which is typically the voter, not the poll creator. That emits incorrect metadata (poll.creator) and can mislead downstream routing/analytics that treat creator as poll author. Only populate creator when it is actually present in decoded poll payload facts (or when handling a creation event where that fallback is semantically valid).

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. feature: ✨ showcase ClawSweeper spotlight: unusually compelling feature idea for maintainer attention. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels May 24, 2026
@clawsweeper
Copy link
Copy Markdown

clawsweeper Bot commented May 24, 2026

ClawSweeper PR egg

✨ Hatched: 💎 rare Frosted Branchling

Hatch command

Comment @clawsweeper hatch when this PR is hatchable.

Hatchability rules:

  • Merged PRs are hatchable.
  • Open PRs are hatchable when they are status: 👀 ready for maintainer look, status: 🚀 automerge armed, or labeled clawsweeper:automerge.
  • Closed unmerged PRs are hatchable only when one of those hatchable labels is still present in the durable record.

Rarity: 💎 rare.
Trait: sleeps inside passing CI.
Image traits: location flaky test forest; accessory review stamp; palette sunrise gold and clean white; mood watchful; pose pointing at a small proof artifact; shell polished stone shell; lighting tiny status-light glow; background tiny shells and proof notes.
Share on X: post this hatch
Copy: My PR egg hatched a 💎 rare Frosted Branchling in ClawSweeper.

What is this egg doing here?
  • Eggs appear after the PR passes real-behavior proof. It is here for vibes, not verdicts: it does not change labels, ratings, merge decisions, or automation.
  • The shell reacts to review momentum: open follow-up work warms it up, re-review makes it wobble, and a clean final review lets it hatch.
  • Hatchability usually comes from sufficient real-behavior proof, no blocking P0/P1/P2 findings, no security attention needed, and clean correctness. A merged PR is already final, so merge makes the egg hatchable independently.
  • The hatch is seeded from this repository and PR number, so the same PR keeps the same creature; the reviewed head SHA can only change safe visual details.
  • Rarity is just collectible sparkle: 🥚 common, 🌱 uncommon, 💎 rare, ✨ glimmer, and 🌈 legendary.

@veteranbv
Copy link
Copy Markdown
Author

Screenshot 2026-05-24 at 00 00 16 This is a screenshot of my conversation with my OpenClaw agent "Wit" asking him to create a poll before the patch and him creating polls natively after the patch.

Copy link
Copy Markdown
Author

Runtime proof from a SIP-disabled macOS Messages setup, using the latest PR branch. No new poll was sent for this proof pass; these results come from existing rows created during earlier validation.

Environment

git log -1 --oneline
c998305 fix: address poll review feedback

sw_vers
ProductName:    macOS
ProductVersion: 26.5
BuildVersion:   25F71

csrutil status
System Integrity Protection status: disabled.

Bridge readiness

make build-dylib
Built .build/release/imsg-bridge-helper.dylib
{
  "v2_ready": true,
  "advanced_features": true,
  "sip": "disabled",
  "bridge_version": 2,
  "selectors": {
    "pollPayloadMessage": true
  },
  "rpc_methods": [
    "poll.send",
    "messages.poll.send"
  ]
}

The existing chat containing native Polls rows resolved successfully. Private chat identifiers, handles, message GUIDs, account aliases, and option IDs are redacted.

Created poll history proof

Recent created-poll rows decode as native poll events:

[
  {
    "is_from_me": true,
    "text": "",
    "poll": {
      "event": "imessage.poll.created",
      "kind": "created",
      "question": "OpenClaw RPC poll final test",
      "options": [{"id":"<redacted>","text":"A"},{"id":"<redacted>","text":"B"}],
      "metadata": {
        "payload_bytes": 7411,
        "url_scheme": "data",
        "bundle_id": "com.apple.messages.MSMessageExtensionBalloonPlugin:0000000000:com.apple.messages.Polls",
        "summary_bytes": 61,
        "associated_message_type": 0,
        "query_keys": ["c","src"]
      }
    }
  },
  {
    "is_from_me": true,
    "text": "",
    "poll": {
      "event": "imessage.poll.created",
      "kind": "created",
      "question": "OpenClaw complex poll retest: pick the best next validation",
      "options_count": 5,
      "metadata": {
        "payload_bytes": 8423,
        "url_scheme": "data",
        "summary_bytes": 61,
        "query_keys": ["c","src"]
      }
    }
  },
  {
    "is_from_me": true,
    "text": "",
    "poll": {
      "event": "imessage.poll.created",
      "kind": "created",
      "question": "OpenClaw imsg poll envelope retest",
      "options": [{"id":"<redacted>","text":"A"},{"id":"<redacted>","text":"B"}],
      "metadata": {
        "payload_bytes": 7419,
        "url_scheme": "data",
        "summary_bytes": 61,
        "query_keys": ["c","src"]
      }
    }
  }
]

Delivery state

[
  {"is_sent":1,"is_delivered":1,"is_finished":1,"error":0,"service":"iMessage","payload_bytes":7411},
  {"is_sent":1,"is_delivered":1,"is_finished":1,"error":0,"service":"iMessage","payload_bytes":8423},
  {"is_sent":1,"is_delivered":1,"is_finished":1,"error":0,"service":"iMessage","payload_bytes":7419},
  {"is_sent":1,"is_delivered":0,"is_finished":1,"error":0,"service":"iMessage","payload_bytes":1073}
]

The 1073-byte row is the known pre-fix failure. The three newer rows are the fixed native poll payloads and are delivered.

Vote history proof

[
  {
    "is_from_me": false,
    "text": " ",
    "poll": {
      "event": "imessage.poll.voted",
      "kind": "vote",
      "original_guid_present": true,
      "poll_guid_present": true,
      "vote": {
        "option_id": "<redacted>",
        "participant_present": true,
        "event_type": "selected"
      }
    }
  }
]

Multiple recent vote rows show the same decoded shape: imessage.poll.voted, original poll reference present, poll GUID present, option ID present, and participant metadata present.

Watch note

I also started the sanitized watch command and asked for a vote. No live poll event arrived during that capture window, so I stopped it. Existing history proof confirms vote decoding from durable Messages rows.

@clawsweeper re-review

@clawsweeper
Copy link
Copy Markdown

clawsweeper Bot commented May 24, 2026

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P3 Low-risk cleanup, docs, polish, ergonomics, or speculative feature. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 message-delivery 🚨 Merging this PR could drop, duplicate, misroute, suppress, or wrongly target messages. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels May 24, 2026
Copy link
Copy Markdown
Author

Fresh proof for a862fd8 feat: support threaded native polls.

This pass focused on poll replies, since non-threaded native poll send/read/vote was already proven earlier.

Local verification

make build-dylib
Built .build/release/imsg-bridge-helper.dylib
# existing unarchiveObjectWithData: deprecation warning only
make test
257 tests passed
git diff --check
# passed

make lint still cannot complete on my machine because swiftlint is not installed. swift format lint --recursive Sources Tests TestsLinux ran before that and produced no findings.

Runtime environment

Commit tested: a862fd8
macOS: 26.5 build 25F71
SIP: disabled

Bridge was rebuilt and force-reloaded before testing:

{
  "v2_ready": true,
  "advanced_features": true,
  "sip": "disabled",
  "bridge_version": 2,
  "poll_payload_message": true,
  "poll_send_present": true,
  "messages_poll_send_present": true,
  "spike_marker_present": false
}

Threaded poll validation

I created a normal parent message with send-rich --no-dd-scan so the parent GUID resolved to the actual visible row, then sent a native poll reply to that message:

imsg poll send --chat <redacted> --reply-to <redacted-parent-guid> \
  --question "OpenClaw item reply poll 20260527120031" \
  --option "Approve" \
  --option "Needs changes"

Sanitized history proof:

{
  "poll_found": true,
  "poll_event": "imessage.poll.created",
  "options_count": 2,
  "reply_to_guid_present": true,
  "reply_to_text_present": true,
  "reply_to_sender_present": true,
  "thread_originator_guid_present": true,
  "thread_originator_part_present": true
}

Sanitized DB proof for the same row:

{
  "is_sent": 1,
  "is_delivered": 1,
  "is_finished": 1,
  "error": 0,
  "payload_bytes": 7475,
  "reply_to_guid_matches_parent": true,
  "thread_originator_guid_matches_parent": true,
  "thread_originator_part_present": true,
  "associated_message_guid_present": false,
  "associated_message_type": 0
}

The reply send path now uses the same IMMessageItem-first thread-originator approach as rich message threading. I also removed the temporary plugin-payload spike from the final patch. The final bridge status confirms the spike marker is gone.

@clawsweeper re-review

@clawsweeper
Copy link
Copy Markdown

clawsweeper Bot commented May 27, 2026

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature: ✨ showcase ClawSweeper spotlight: unusually compelling feature idea for maintainer attention. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 message-delivery 🚨 Merging this PR could drop, duplicate, misroute, suppress, or wrongly target messages. P3 Low-risk cleanup, docs, polish, ergonomics, or speculative feature. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add native iMessage poll support

1 participant