6.1 KiB
6.1 KiB
Architecture
Host topology
plum (10.9.0.3 via WG) black (10.0.0.11 LAN) apricot (10.0.0.116 LAN)
┌────────────────────────┐ ┌──────────────────────────┐ ┌─────────────────────────────┐
│ macOS Messages.app │ │ postgres: quinn_icloud │ │ quinn-outreach (Node/bun) │
│ └─ chat.db (sqlite) │ │ macsync.messages ◀────┐ │ │ inbound-listener │
│ │ │ macsync.conversations │ │ │ watcher │
│ MacSyncApp:8765 ───────┼───▶│ outreach.* │ │ │ auto-respond-engine │
│ (mirrors chat.db) │ │ + LISTEN/NOTIFY ──────┼─┼───▶│ booking-worker │
│ │ │ message_inserted │ │ │ scheduled-send-worker │
│ Bridge:8766 ◀──────────┼────┤ ◀┼─┼────┤ + ./run CLI │
│ POST /api/imessage/ │ │ │ └─────────────────────────────┘
│ send │ └──────────────────────────┘
└────────────────────────┘
- plum runs Messages.app (chat.db is sqlite). MacSyncApp tails chat.db and inserts rows into postgres on black. Bridge accepts outbound HTTP requests and dispatches via osascript.
- macsync.messages (mac-sync v2): gained
attributed_body BYTEA+body_decoded TEXTcolumns via migration2026-05-13_message_attributed_body. Both are populated by the mac-sync server when the source iMessage row carried a non-emptyattributedBody; the FTS tsvector spansbody || ' ' || body_decodedso rich-content messages remain searchable even when chat.db'stextis empty. - black runs the postgres mirror. Trigger NOTIFY on insert to
macsync.messagesis what wakes the apricot listener. - apricot runs the engine processes (no chat.db access; reads via postgres mirror).
Inbound → gated dispatch path
prospect SMS/iMessage
│
▼ chat.db row on plum
│
▼ MacSyncApp INSERT into macsync.messages on black
│
▼ pg trigger NOTIFY message_inserted
│
▼ InboundListener.handler on apricot
│
▼ watcher.processInbound:
emit inbound_received
emit prospect_replied (wakes scheduled-send dependency check)
AddressBook lookup (TCC-gated; fail-loud → block)
evaluateEligibility (C1 spam │ C2 floor │ C3 wrong-id │ C4 prior │ C5 lookup)
│
┌────────┴─────────┐
▼ ▼
ineligible eligible → auto-respond-engine
│ │
├ C3 hard-block ▼ selectTemplate(inbound) → A│B│C│D
│ silent drop ▼ renderTemplate(id, calendar) → body
│ ▼ classifyTier → calculateFireAt
└ other → routeToQuinnApproval
│
▼ runPreFireChecks (§38) — see docs/gates.md
│
┌─────┴─────┐
▼ ▼
fire=true fire=false
│ │
scheduleSend routeToQuinnApproval (reason=pre_fire:<gate>)
│
▼ scheduled-send-worker picks up at fireAt
│
▼ mac-sync POST /api/imessage/send → bridge → osascript
(v2: per-module send-queue endpoints also exist —
`POST /admin/{calendar,reminder,note,mail}-send-queue/enqueue`
with body `{ deviceId, action, payload }`.)
│
▼ emit auto_respond_sent / scheduled_send_sent
Booking expiry → cover-story draft
booking-worker tick (every 60s)
│
▼ processExpiredDeposits(pool)
UPDATE outreach.bookings SET state='expired-no-deposit' WHERE deadline < now()
emit booking_deposit_expired (per row)
return ExpiredBookingRow[]
│
▼ for each row:
pickCoverStory('double-booked', {raincheck: 'tomorrow'})
emit cover_story_proposed { booking_id, prospect_handle, body }
│
▼ Quinn sees draft in self-thread tooling, decides send/skip
What flows through what
| Layer | Module | Reads | Writes |
|---|---|---|---|
| Inbound | inbound-listener.ts |
pg LISTEN | NotifyHandler callback |
| Triage | watcher.ts |
payload, addressbook, prospect_state | event_log: inbound_received, prospect_replied |
| Eligibility | eligibility.ts |
inbound, priors, addressbook | (pure function) |
| Template | templates.ts |
inbound, calendar slots | string body |
| Pre-fire gates | pre-fire-checks.ts |
engine-config, block-list, thread state, jargon rules, botness signals, macsync.messages | event_log: auto_respond_pre_fire |
| Schedule | scheduled-send-worker.ts |
outreach.scheduled_send | event_log: scheduled_send_sent |
| Dispatch | mac-sync-client.ts |
scheduled row | HTTP → bridge → osascript |
| Booking | booking-worker.ts |
outreach.bookings | event_log: booking_deposit_expired, cover_story_proposed |
The gating chain (pre-fire-checks) is the load-bearing piece. Everything else is plumbing.
Postgres schemas used
macsync.messages— chat.db mirror; read by §37 quinn-in-thread query (v2 also exposesattributed_body,body_decoded; FTS-indexed across both body columns)macsync.conversations— forparticipants @> jsonblookupoutreach.scheduled_send— durable send queueoutreach.bookings— booking state machine (deposit-pending, expired-no-deposit, etc.)outreach.event_log— append-only event stream (heartbeat, gate decisions, dispatches)outreach.heartbeat— worker liveness