Add native iMessage poll support#125
Conversation
|
Codex review: needs maintainer review before merge. Reviewed May 27, 2026, 12:15 PM ET / 16:15 UTC. Summary 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.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge Security Review detailsBest 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 changesLabel changes:
Label justifications:
Evidence reviewedWhat I checked:
Likely related people:
What the crustacean ranks mean
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
|
There was a problem hiding this comment.
💡 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".
| let payloadData = try dataValue(row, columns.payloadData) | ||
| let messageSummaryInfo = try dataValue(row, columns.messageSummaryInfo) |
There was a problem hiding this comment.
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 👍 / 👎.
|
|
||
| let originalGUID = normalizedAssociatedGUID(associatedMessageGUID) | ||
| let senderHandle = sender.isEmpty ? nil : sender | ||
| let creator = facts.creator ?? senderHandle |
There was a problem hiding this comment.
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 PR egg ✨ Hatched: 💎 rare Frosted Branchling Hatch commandComment Hatchability rules:
Rarity: 💎 rare. What is this egg doing here?
|
|
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. EnvironmentBridge readiness{
"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 proofRecent 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: Watch noteI 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 |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Fresh proof for This pass focused on poll replies, since non-threaded native poll send/read/vote was already proven earlier. Local verification
Runtime environmentBridge 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 validationI created a normal parent message with 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 |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|

Closes #124.
Summary
This adds native Apple Messages poll support to imsg.
historyandwatchimessage.poll.created,imessage.poll.voted, andimessage.poll.unknownimsg poll sendpluspoll.sendandmessages.poll.sendRPC methodsimsg poll send --reply-to <message-guid>and the matching RPC reply fieldsreply_to_guid,reply_to_text,reply_to_sender,thread_originator_guid, andthread_originator_partso clients can correlate threaded polls and repliesThe JSON shape is additive. Existing message fields are preserved, and non-poll messages should keep their current behavior.
Testing
make build-dylibunarchiveObjectWithData:deprecation warningmake testgit diff --checkmake lintswift format lintpassedswiftlintis not installed locallyI 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:
v2_ready=true,advanced_features=true, andselectors.pollPayloadMessage=trueimessage.poll.createdwith question, options, and option IDsimessage.poll.votedwith option ID, participant, original poll GUID, and poll GUIDpoll.sendreturnedok=trueand produced a delivered native pollimsg poll send --reply-to <message-guid>delivered a native poll replyreply_to_guid,thread_originator_guid, andthread_originator_partpresent, with history resolvingreply_to_textandreply_to_senderThe 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.creatorfrom the vote sender