# Handoff 06 — Outbound queue runner + channel selection + OF campaign batches

**For:** @mac-sync coder agent
**Depends on:** 03-outbox-send (the `/my/outbox` queue + Mac-client scheduler — DONE), 04-mark-read (DONE), 05-conversation-ownership (DONE)
**Why now:** The Cowork auto-handler is a stopgap that sends ≤3/run inline. The real model (Quinn's call): a Sonnet/LP session **queues everything** as outbox drafts and the **macsync outbox runner drains them paced** — first-replies, follow-ups, and the OnlyFans (OF) campaign blast all flow through one rail. This handoff makes the outbox the single outbound path and adds the two things it's missing: **automatic channel selection** and **campaign-batch support**.

---

## Goal
The outbox accepts a large queue of drafts (hundreds), picks the right channel per recipient automatically, drains them on a paced schedule with all send-safety guardrails, verifies delivery, and reports status back — so a planning session only has to *enqueue intent*, never babysit sends.

## Out of scope (stays in Executor / LP, NOT macsync)
- **What** to send and **to whom**: classification, segmentation, OF-Live RSVP funnel logic, go/no-go threshold, copy. macsync is the **delivery rail**, not the campaign brain.
- Deciding a number is a lead vs vendor vs friend (identity gate lives upstream; macsync only enforces the hard vendor/opt-out floor as defense-in-depth).

---

## Subtask 1 — Channel selection in the outbox (retire the Cowork `android-numbers.json`)
Today the Cowork handler keeps `state/android-numbers.json` (numbers that bounced iMessage error=22 → send SMS direct next time). That capability belongs in the outbox, which already sees verified send results.

Build:
- **`macsync.handle_channel_capability`** (migration) keyed `(device_id, handle)`: `imessage_capable` (bool/null=unknown), `last_error`, `source`, `updated`. `error=22` (or AppleScript "not registered with iMessage") on a confirmed-failed send → set `imessage_capable=false` monotonically (don't flip back to true automatically).
- **Channel pick on dequeue:** if a draft has no explicit `channel`, look up capability. `imessage_capable=false` → send **SMS directly** (skip the iMessage attempt entirely). Unknown → try iMessage, and on error=22 record the capability + auto-retry once via SMS (the existing recovery path), so the *next* draft to that handle goes straight to SMS.
- **Seed** from the Cowork list at cutover: import the numbers in `Executor/state/android-numbers.json` as `imessage_capable=false`.
- Expose capability in `GET /my/outbox` status + an MCP tool `handle_channel_state(handle)`.

Acceptance: a recipient bounces iMessage at most **once**; every subsequent draft to them is SMS-direct. No per-message guessing in the planning layer.

## Subtask 2 — Paced batch drain + campaign tagging
- Add optional `batch_id` and `campaign` (string) to `OutboxDraft`/`enqueueOutbox` + `/my/outbox` schema. Lets a session enqueue a whole segment in one shot and track/cancel it as a unit.
- **Pacing is declared, not per-message** (matches Quinn's stated preference): a batch carries `{gap_seconds_min, gap_seconds_max, window}` (e.g. 45–300s, daytime windows). The Mac-client scheduler already drains the queue — extend it to honor per-batch pacing + a daily send ceiling, jittered, instead of firing the batch at once.
- **Cancel/pause a batch:** `POST /my/outbox/batch/:id/{pause,resume,cancel}` + MCP tool. One-switch kill for a campaign mid-flight.
- All existing guardrails still apply per send: dedupe/cooldown, hard vendor/shortcode floor, opt-out honor, verify-in-chat.db, mark-read-on-send.

## Subtask 3 — OF campaign = ordinary batches (no special channel)
**OF is not a channel — it's message content + a state machine.** Two campaign types, both ride the same outbox:

1. **`campaign=of-redirect`** — single sends. Budget/harvester/out-of-area leads get the OnlyFans link instead of a booking. Just normal drafts whose body is the OF-redirect template. Nothing special needed beyond Subtask 2.

2. **`campaign=of-live`** — the interest-first, demand-gated funnel (brief: `Executor/content/of-live-reachout-campaign.md`). **No date/time is declared until Step 3.** It's a **3-step state machine driven by inbound replies** — the planning session owns the state; macsync just sends each step's batch and reports deliveries + surfaces replies:
   - **Step 1 — interest-gauge batch** (`stage=gauge`): asks if they'd be into her first OF Live (audience mode, vs a private FT). NO time, NO confirmed-show language. Queued as one paced batch.
   - **Step 2 — solicit batch** (`stage=solicit`): only to handles that replied yes/interested. The planning layer reads inbound (via `/my/...` + ownership/read state), classifies the reply, and enqueues step-2 drafts for the positives. macsync surfaces the inbound; it does **not** decide who's interested.
   - **Step 3 — reminder batch** (`stage=reminder`): only to the interested list, fired ~1–2h before the live **only if** the planning layer flips go (threshold met) and a time has been set. This is the FIRST batch that may contain a time. One bump.
   - macsync's only OF-Live-specific need: carry `stage` alongside `campaign` so status/reporting can show the funnel by stage, and so a stage's batch can be cancelled if it's no-go.

**Guardrail callout for OF-Live (defense-in-depth, primary enforcement upstream):** never to saved Contacts/vendors/human-owned; honor opt-outs; respect the per-batch ceiling so a bulk invite can't trip spam flagging. First-ever bulk blast SHOULD require an explicit `approved=true` flag on the batch (planning session sets it after Quinn approves segment+copy) before the runner will drain it.

---

## How the end-to-end flow reads after this handoff
1. Planning session (Sonnet/LP) classifies + segments (LP prospector corpus), drafts copy from the 🌹 Pastebin canon, and **enqueues batches** to `/my/outbox` with `campaign`, `stage`, `batch_id`, declared pacing, and `approved` where required.
2. macsync outbox runner **drains paced**, picks channel per handle (iMessage/SMS auto), verifies each send, marks-read, records capability, reports status per batch/stage.
3. Inbound replies surface through `/my` read/ownership; the planning session advances the OF-Live state machine and enqueues the next stage. Real bookings escalate to Quinn (human-owned), never auto-progressed.

Net: the planning layer expresses **intent**; macsync owns **reliable, paced, correctly-channeled delivery**.

## Acceptance (whole handoff)
- One recipient → at most one iMessage bounce, then SMS-direct forever (capability persisted). ✅ when Subtask 1 lands + Cowork list imported.
- A 200-draft batch enqueued in one call drains over its declared window with jittered pacing, pausable/cancellable, all guardrails honored. ✅
- OF-redirect sends and the 3-stage OF-Live funnel both run as outbox batches; macsync reports funnel status by stage and never decides segments/go-no-go. ✅
- `make build` + `make test` green; new repo tests written (run where the `@lilith` registry is up, per H03/H04 caveat).
