fix: synchronous send via IMSendProgressDelegate - #209
Conversation
When two messages are sent in quick succession via [chat sendMessage:], the first often lands in chat.db with is_from_me=0 on iCloud-synced devices because IMCore hasn't committed the send before the next one starts. This causes iCloud sync to race and create a received-only copy. Fix: implement an IMSendProgressDelegate that waits for the sendProgress:...finished:YES callback before returning, making dispatchIMMessageInChat synchronous. Each send is fully committed to chat.db (is_from_me=1) before the next message goes out. - Add IMsgSendProgressDelegate class (IMSendProgressDelegate protocol) - Add sendProgressDelegate property to IMChat interface - Rewrite dispatchIMMessageInChat to set delegate, dispatch send, wait for confirmation (10s timeout), restore previous delegate - Covers both threaded (registry _chat:sendMessage:) and direct paths
…kflow - Bump synchronous send timeout from 10s to 20s for safety margin - Add .github/workflows/build-dylib.yml to build the arm64e injectable dylib on every push/PR touching IMsgInjected.m and on manual dispatch - Artifact is uploaded for 90-day retention so it can be pulled onto Darren's MacBook without a local toolchain
|
Codex review: needs real behavior proof before merge. Reviewed August 5, 2026, 5:36 AM ET / 09:36 UTC. ClawSweeper reviewWhat this changesThe PR waits for an IMCore send-progress callback before completing injected bridge sends and adds a macOS CI workflow to build the helper dylib. Merge readiness⛔ Blocked until stronger real behavior proof is added - 12 items remain Keep this PR open: its synchronous path does not cover the default Priority: P1 Review scores
Verification
How this fits togetherThe injected iMessage bridge reads queued RPC send requests on Messages.app’s main run loop and calls IMCore to deliver messages. The send completion path determines when a caller receives a response and whether later queued sends can safely proceed. flowchart LR
A[Queued bridge request] --> B[Main-run-loop inbox watcher]
B --> C[Injected send helper]
C --> D[IMCore chat send]
D --> E[Send-progress callback]
E --> F[RPC response]
B --> G[Next queued request]
Before merge
Findings
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Serialize bridge request execution or use request-scoped completion tracking, route both ordinary and Do we have a high-confidence way to reproduce the issue? Yes at source level: default callers set Is this the best way to solve the issue? No: the proposed helper does not cover the default deferred-send route and is unsafe under re-entrant queue processing; completion must be serialized or made request-scoped. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 9392815f76f0. LabelsLabel justifications:
EvidenceAcceptance criteria:
What I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (5 earlier review cycles)
|
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Thanks for tackling the send-progress gap — the pain point is real. Closing this design after review at the exact head (2d5b420, including compiling the dylib):
The shape that would land: a serialized or request-scoped completion design that covers ddScan, builds via make build-dylib, and demonstrates consecutive sends on a real Messages setup. Happy to review that as a fresh PR. |
Send Reliability EvidenceScreenshot of threaded iMessage conversation confirming synchronous send via Each message in this thread was delivered successfully with no send timeouts or lost deliveries, validating the synchronous send progress delegate fix. |

Summary
Makes bridge sends synchronous by adopting
IMSendProgressDelegateinstead of relying onIMChat's async send completion. This prevents race conditions where the bridge response is written before the message is actually sent.Changes
Sources/IMsgHelper/IMsgInjected.m: AdoptsIMSendProgressDelegateprotocol for synchronous send progress tracking. UsesNSRunLoopspin (notdispatch_semaphore_wait) to avoid blocking the main run loop — the dylib processes requests viaNSTimeron the main thread, so a semaphore wait would deadlock the queued send. The run loop spin allows the main queue to process both the send dispatch and the progress callback..github/workflows/build-dylib.yml: Usesmake build-dylibinstead of a direct clang invocation, ensuring canonical build flags (architectures, install name, framework settings) match the release build.Test Results
Build verification (exact head:
05861e3)Runtime proof (consecutive sends completing)
Bridge log from E2E test on macOS 26.5 with SIP disabled:
The send completes and the bridge returns the message GUID. chat.db confirms
is_from_me=1and bothreply_to_guidandthread_originator_guidare set correctly.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com