15 KiB
MacSync Canonical Completion Plan — 2026-05-15
Drives @mac-sync to the architecture its docs already describe: every
module a BaseSyncManager<Stats, Error> with a SendQueueClient<Transport>
peer, every send routed through /admin/send-queue/enqueue → /client/<module>/send-queue/pending
→ a module-specific Sender. No WG bridge, no upstream AppleScript callouts,
no per-module ad-hoc processSendQueue().
This handoff is the source of truth for that consolidation. Working notes
in ~/.claude/plans/ are deprecated by this file where they conflict.
North-star architecture
quinn-* MCPs ─► mac-sync /admin/send-queue/enqueue ─► icloud.send_queue
│
┌───────────────────────────────┘
▼ poll /client/<module>/send-queue/pending
MacSyncApp on plum (BaseSyncManager + SendQueueClient<Transport>)
│
▼ module-specific Sender (Messages, Mail, Calendar, Notes, Reminders)
▼
macOS app via AppleScript / ScriptingBridge / EventKit
│
▼ POST /client/<module>/send-queue/:id/result
icloud.send_queue rows → sent / failed
Same shape for scheduled sends: outreach.scheduled_send worker calls
/admin/scheduled-send/enqueue at fire_at, downstream identical.
Single canonical wire format across all modules:
GET /client/<module>/send-queue/pending
→ { "success": true, "data": { "items": [ { "id", "toHandle"|<module-fields>, "body"|"payload", "createdAt" } ] } }
POST /client/<module>/send-queue/:id/result
← { "status": "sent" | "failed", "error": "..." }
Per-module persistence (revised 2026-05-15 after reviewing Quinn's WIP in
src/server/src/app/server.ts):
Each module has a dedicated send-item entity, NOT a shared icloud.send_queue
discriminated by service type. Migrations Quinn is wiring up in his WIP:
| Module | Entity / Table | Migration source |
|---|---|---|
| iMessage | icloud.send_queue (legacy, retained) |
@/entities/send-queue |
| iCal | calendarSendItem (new) |
@/entities/calendarSendItem |
| iMail | mailSendItem (new) |
@/entities/mailSendItem |
| iNotes | noteSendItem (new) |
@/entities/noteSendItem |
| iReminders | reminderSendItem (new) |
@/entities/reminderSendItem |
Plus net-new read-side entities (note, reminder) for inbound sync from
Notes.app / Reminders.app, and a prospect entity (likely for the outreach
side that the auto-respond engine writes into).
The iMessage table is the only one keeping the older icloud.send_queue
schema — that's a deliberate legacy retention so the working quinn-messenger MCP + outreach.scheduled_send worker paths don't break during the
migration. New modules get the cleaner per-module shape from the start.
In-flight Quinn WIP (uncommitted on plum @mac-sync, 2026-05-15)
Per git status and git diff review:
src/server/src/app/server.ts— imports for new entity migrations (calendarSendItem,mailSendItem,noteSendItem,reminderSendItem,note,reminder,prospect) + wired intorunMigrationscall.@packages/ical/Sources/ICalSync/APIClient.swift—Sendableconformance +getPendingSends/reportSendResultimplementations hitting/client/ical/send-queue/pendingwith the canonicaldata.itemsenvelope and aPendingCalendarSendpayload struct.@packages/ical/Sources/ICalSync/Reader.swift— modified (likely EventKit-side reader changes; not yet inspected).@packages/imail/Sources/IMailSync/APIClient.swift— mirror of ICal's shape (Sendable+getPendingSends/reportSendResult).@packages/shared/Sources/MacSyncShared/WebServer/LocalWebServer.swift— modified (not yet inspected).Package.swift,app.manifest.yaml,CLAUDE.md— module additions and metadata.
Do not edit any of these files from a Claude Code session. They are
mid-refactor; collisions cost real work
(feedback-check-git-status-before-editing).
Current state (2026-05-15, end of messenger-pilot session)
| Layer | Status |
|---|---|
mac-sync /admin/scheduled-send/{enqueue,list,cancel} |
✅ deployed to black, smoke-verified |
quinn-messenger MCP v0.3.0 (5 tools, HTTP-only) |
✅ deployed plum + apricot, stdio-verified |
outreach.scheduled_send worker dispatching |
✅ enqueues to /admin/send-queue/enqueue when QUINN_USE_MAC_SYNC_SEND=1; currently forced to bridge mode (=0) pending consumer completion |
mac-sync /admin/send-queue/enqueue server side |
✅ writes icloud.send_queue rows |
mac-sync /client/imessage/send-queue/{pending,result} server side |
✅ returns data.items shape, contract doc in src/server/SEND_QUEUE_CONTRACTS.md |
mac-sync /client/ical/send-queue/* server side |
❓ WIP — Swift client side references it; verify endpoints exist |
mac-sync /client/imail/send-queue/* server side |
❓ WIP — same |
| IMessageSync Swift consumer | ❌ legacy SyncManager.processSendQueue() in place; reads wrong wire format (data.messages/phoneNumber); SendQueueAdapter.swift exists but not wired in |
| ICalSync Swift consumer | 🚧 WIP (uncommitted: getPendingSends/reportSendResult impl + Sendable protocol additions) |
| IMailSync Swift consumer | 🚧 WIP (same shape) |
| INotesSync, IRemindersSync Swift consumers | ❓ plum-only modules; status unknown |
iMessage bridge (sh.lilith.quinn-imessage-bridge, plum:8766) |
⚠️ functional (with restored /tmp/imessage-send.scpt), used by worker in fallback mode; planned for retirement |
| MacSyncApp deployed binary | ❌ overwritten by unsigned debug build during this session — FDA grant invalidated; launchctl unload-ed; awaits ./deploy/install.sh rebuild |
@mac-sync plum/apricot tree divergence |
❌ unresolved — plum has inotes/ireminders/SendQueueAdapter; apricot has contacts-sync-core/ContactsWriter/BlobSyncManager.swift |
Phases
Phase 1 — Finish the iMessage canonical migration (BLOCKING)
Owner: Quinn (Swift work; do NOT edit from Claude Code sessions per feedback-check-git-status-before-editing).
- Rewrite
IMessageSync/SyncManager.swiftto match the pattern its own docs describe (docs/modules/imessage.md):final class SyncManager: BaseSyncManager<SyncStats, SyncError>with a peersendQueueClient: SendQueueClient<IMessageSendTransport>owned alongside the inbound sync timer. DeleteprocessSendQueue()(currently lines 475–504). - Fix
IMessageSync/APIClient.swiftwire format to matchsrc/server/SEND_QUEUE_CONTRACTS.md:json["data"]["messages"]→json["data"]["items"]phoneNumber→toHandle- Drop
requestedBy(not returned by server) - Update
PendingSendMessagestruct accordingly
- Wire
SendQueueAdapter+IMessageSendTransportinto MacSyncApp so the client polls + dispatches throughSender.SendService. - Run
./deploy/install.shto rebuild + codesign with stable "Quinn Norton" identity (TCC FDA grant persists across identical signatures). Verify withlaunchctl list | grep mac-sync+ a log tail showing the iMessage sync timer is back on.
Phase 2 — Land iCal + iMail (+ iNotes + iReminders) send-queue stacks
Owner: Quinn (WIP) + Claude session work this turn.
Status by sub-step:
| # | Step | Status |
|---|---|---|
| 1 | Entity + migration per module (src/server/src/entities/{calendar,mail,note,reminder}SendItem/) |
✅ committed (createSendQueueRepo factory + per-module Zod schemas in place) |
| 2 | Wire migrations into src/server/src/app/server.ts runMigrations |
🚧 WIP (Quinn) — git diff src/server/src/app/server.ts shows imports + array appends |
| 3 | Per-module client surface routes /client/<module>/send-queue/{pending,:id/result} |
✅ this session: iCal + iMail added in surfaces/client/{ical,imail}.ts mirroring ireminders.ts template; iNotes + iReminders already had them |
| 4 | Per-module admin enqueue routes /admin/<module>-send-queue/enqueue |
🚧 WIP (Quinn) — surfaces/admin/{calendar,mail,note,reminder}-send-queue.ts exist; mount in admin/index.ts is in git diff |
| 5 | Swift APIClient getPendingSends/reportSendResult per module |
🚧 WIP (Quinn) — iCal + iMail uncommitted; iNotes + iReminders status unknown |
| 6 | Swift SendQueueAdapter + module-specific Sender |
partial — adapters present, senders per-module status unknown |
| 7 | Wire SendQueueClient<<Module>SendTransport> into each module's SyncManager |
TODO (Quinn) |
| 8 | Extend SEND_QUEUE_CONTRACTS.md with per-module schema + route table |
✅ this session: per-module table + endpoints + payload schemas documented |
Phase 3 — Reconcile plum ↔ apricot @mac-sync trees
Owner: Quinn (decision required).
Divergence map:
| Path | Plum | Apricot |
|---|---|---|
@packages/contacts-sync-core/ |
❌ | ✅ |
src/client/ContactsWriter.swift |
❌ | ✅ |
src/client/ContactsBackup.swift |
❌ | ✅ |
src/client/ContactRenderPoller.swift |
❌ | ✅ |
src/client/ContactRenderModels.swift |
❌ | ✅ |
@packages/imessage/Sources/IMessageSync/BlobSyncManager.swift |
❌ | ✅ |
@packages/inotes/ (INotesSync) |
✅ | ❌ |
@packages/ireminders/ (IRemindersSync) |
✅ | ❌ |
@packages/imessage/Sources/IMessageSync/SendQueueAdapter.swift |
✅ | ❌ |
- Pick a canonical authoring host for
@mac-syncand write it into@mac-sync/CLAUDE.md. Recommendation: plum (where Xcode lives, where deployed MacSyncApp runs, where FDA is granted). Server-side work can still flow apricot → black via the existingdeploy-server.shfrom either host. - Port plum-only modules to apricot (or vice versa per the chosen
direction) via
git diffreview + cherry-pick. - Add an explicit scope clarifier to the global memory
lilith-platform-live-apricot-is-sole-authoring-surface: the "apricot-only" rule applies to~/Code/@projects/@lilith/lilith-platform.live/only.~/Code/@applications/*follows per-repo rules. (Done infeedback_source_of_truth_apricot.md.) - Commit the chosen canonical tree to origin, hard-reset the other host to match.
Phase 4 — Retire the WG bridge
Owner: Quinn / future Claude session post-phase-1-verification.
- Flip worker dispatch path: edit
/etc/quinn-ai-engine/secrets.envon apricot, removeQUINN_USE_MAC_SYNC_SEND=0(or set to1). Restartquinn-ai-auto-respond.service. - Smoke
outreach.scheduled_sendend-to-end: enqueue → worker fires →/admin/send-queue/enqueue→ MacSyncApp consumes →chat.dbrow appears. - Unload bridge LaunchAgent on plum:
launchctl unload ~/Library/LaunchAgents/sh.lilith.quinn-imessage-bridge.plist. Mark~/Applications/quinn-imessage-bridge/archived. Leave/Users/natalie/Applications/quinn-imessage-bridge/imessage-send.scptin place — harmless and useful as a documented Messages.app dispatch reference. - Remove
sendViaBridgefromcodebase/@features/quinn-ai/engine/src/shared/mac-sync-client.ts(lilith-platform.live, apricot-authored). MacSyncClient becomes single-path. Rebuild + redeploy engine. - Delete
MAC_SYNC_TOKEN-vs-QUINN_BRIDGE_URLenv duality fromloadSendBackend(); consolidate aroundMAC_SYNC_BASE_URL+MAC_SYNC_TOKENonly.
Phase 5 — Lock down delivery verification
Owner: Quinn / future session.
- Make
chat.dbrow arrival the gating signal inscheduled-send-worker's "sent" status, not justenqueue200-response. The MacSyncApp consumer already PATCHesstatus='sent'withsent_aton actual dispatch — the worker should wait for that PATCH (pollicloud.send_queue.status) rather than markingoutreach.scheduled_send.status='sent'on enqueue ack. - Surface a
delivery_confirmedboolean in theoutreach.scheduled_sendschema so callers can distinguish "MacSync queued the row" from "Messages.app dispatched and Apple ack'd". - Add a periodic janitor job that fails any
icloud.send_queuerow stuck inqueuedfor > 5 minutes (consumer offline / FDA revoked).
Phase 6 — Single-path TS client for the MCP
Owner: future session.
Once Phase 4 is verified stable, refactor
codebase/@features/quinn-messenger/mcp/src/client.ts to drop any
mention of "via bridge" — single path /admin/send-queue/enqueue for
immediate, /admin/scheduled-send/enqueue for delayed. Bump MCP to
v1.0.0 (out of pre-release).
Hard rules going forward
git statusbefore editing any@applications/@mac-syncSwift source. WIP collisions cost real work — see~/.claude/projects/.../memory/feedback_check_git_status_for_wip.md.- Never
cpa Swift build artifact directly into the MacSync.app bundle. TCC revokes FDA silently. Always./deploy/install.shso the stable "Quinn Norton" codesign is applied. - Server contract
SEND_QUEUE_CONTRACTS.mdis the canonical wire format. Swift clients are out of spec if they don't readdata.items/toHandle. Server is not changed to match Swift; Swift is brought up to match server. - Bridge / osascript are explicitly transitional. No new code
should reference
10.9.0.3:8766,imessage-send.scpt, orsendViaBridge. Phase 4 retires them. - MacSync is the only iMessage send path once Phase 1 lands.
quinn-messenger MCPandoutreach.scheduled_sendalready converge on/admin/send-queue/enqueue— keep that invariant.
Verification checklist (per phase)
Each phase is complete only when:
- All affected Swift targets compile (
swift build). - All affected TS targets typecheck (
bunx tsc --noEmit). - One smoke message round-trips end-to-end and lands in
chat.dbon plum (not juststatus='sent'in any queue table). - Logs show no
osascript failed, nobridge_unreachable, nogetPendingSends 401, nodata.messagesJSON path miss. outreach.heartbeatforscheduled-send-workershows ticks within the last 10s.outreach.scheduled_sendhas no rows stuck inpendingpast theirfire_atfor >60s.
References
docs/architecture.md— system overview, invariants.docs/adding-a-module.md— canonical module pattern this plan brings every existing module to.docs/modules/imessage.md— describes the target SyncManager shape.src/server/SEND_QUEUE_CONTRACTS.md— canonical wire format.~/.claude/projects/.../memory/project_macsync_send_consumer_gap.md~/.claude/projects/.../memory/project_imessage_bridge_scpt.md~/.claude/projects/.../memory/feedback_check_git_status_for_wip.md~/.claude/projects/.../memory/feedback_verify_delivery_first.md