macsync/handoffs/20260515_macsync-canonical-completion.md

15 KiB
Raw Blame History

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 into runMigrations call.
  • @packages/ical/Sources/ICalSync/APIClient.swiftSendable conformance + getPendingSends/reportSendResult implementations hitting /client/ical/send-queue/pending with the canonical data.items envelope and a PendingCalendarSend payload 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).

  1. Rewrite IMessageSync/SyncManager.swift to match the pattern its own docs describe (docs/modules/imessage.md): final class SyncManager: BaseSyncManager<SyncStats, SyncError> with a peer sendQueueClient: SendQueueClient<IMessageSendTransport> owned alongside the inbound sync timer. Delete processSendQueue() (currently lines 475504).
  2. Fix IMessageSync/APIClient.swift wire format to match src/server/SEND_QUEUE_CONTRACTS.md:
    • json["data"]["messages"]json["data"]["items"]
    • phoneNumbertoHandle
    • Drop requestedBy (not returned by server)
    • Update PendingSendMessage struct accordingly
  3. Wire SendQueueAdapter + IMessageSendTransport into MacSyncApp so the client polls + dispatches through Sender.SendService.
  4. Run ./deploy/install.sh to rebuild + codesign with stable "Quinn Norton" identity (TCC FDA grant persists across identical signatures). Verify with launchctl 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
  1. Pick a canonical authoring host for @mac-sync and 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 existing deploy-server.sh from either host.
  2. Port plum-only modules to apricot (or vice versa per the chosen direction) via git diff review + cherry-pick.
  3. 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 in feedback_source_of_truth_apricot.md.)
  4. 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.

  1. Flip worker dispatch path: edit /etc/quinn-ai-engine/secrets.env on apricot, remove QUINN_USE_MAC_SYNC_SEND=0 (or set to 1). Restart quinn-ai-auto-respond.service.
  2. Smoke outreach.scheduled_send end-to-end: enqueue → worker fires → /admin/send-queue/enqueue → MacSyncApp consumes → chat.db row appears.
  3. 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.scpt in place — harmless and useful as a documented Messages.app dispatch reference.
  4. Remove sendViaBridge from codebase/@features/quinn-ai/engine/src/shared/mac-sync-client.ts (lilith-platform.live, apricot-authored). MacSyncClient becomes single-path. Rebuild + redeploy engine.
  5. Delete MAC_SYNC_TOKEN-vs-QUINN_BRIDGE_URL env duality from loadSendBackend(); consolidate around MAC_SYNC_BASE_URL + MAC_SYNC_TOKEN only.

Phase 5 — Lock down delivery verification

Owner: Quinn / future session.

  1. Make chat.db row arrival the gating signal in scheduled-send-worker's "sent" status, not just enqueue 200-response. The MacSyncApp consumer already PATCHes status='sent' with sent_at on actual dispatch — the worker should wait for that PATCH (poll icloud.send_queue.status) rather than marking outreach.scheduled_send.status='sent' on enqueue ack.
  2. Surface a delivery_confirmed boolean in the outreach.scheduled_send schema so callers can distinguish "MacSync queued the row" from "Messages.app dispatched and Apple ack'd".
  3. Add a periodic janitor job that fails any icloud.send_queue row stuck in queued for > 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

  1. git status before editing any @applications/@mac-sync Swift source. WIP collisions cost real work — see ~/.claude/projects/.../memory/feedback_check_git_status_for_wip.md.
  2. Never cp a Swift build artifact directly into the MacSync.app bundle. TCC revokes FDA silently. Always ./deploy/install.sh so the stable "Quinn Norton" codesign is applied.
  3. Server contract SEND_QUEUE_CONTRACTS.md is the canonical wire format. Swift clients are out of spec if they don't read data.items/toHandle. Server is not changed to match Swift; Swift is brought up to match server.
  4. Bridge / osascript are explicitly transitional. No new code should reference 10.9.0.3:8766, imessage-send.scpt, or sendViaBridge. Phase 4 retires them.
  5. MacSync is the only iMessage send path once Phase 1 lands. quinn-messenger MCP and outreach.scheduled_send already 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.db on plum (not just status='sent' in any queue table).
  • Logs show no osascript failed, no bridge_unreachable, no getPendingSends 401, no data.messages JSON path miss.
  • outreach.heartbeat for scheduled-send-worker shows ticks within the last 10s.
  • outreach.scheduled_send has no rows stuck in pending past their fire_at for >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