Clean successor to V3 (forge: lilith/atlilith). Seeded from local Mac working tree at ~/Code/@projects/@cocottetech/. node_modules and build artifacts excluded via .gitignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
P — Inboxes (unified inbound across messaging surfaces)
Goal
Quinn currently checks ≥6 inboxes by hand: iMessage, SMS, Proton email (hi@cocotte.ai-style + per-platform like myprivatedelights@protonmail.com), Tryst inbox, OF DMs, X DMs, and ad-hoc IG/TikTok/Threads. CocotteAI replaces "go check each inbox" with "ai-copilot has the queue." This brief defines the unified-inbox UX: one feed, source-labeled, with reply routing back to the originating channel and per-inbox triage policy.
Briefs C (notifications outbound) and I (audit) frame the surrounding loop; this brief is the inbound counterpart to C. Brief O — N4 enumerates the channels; this brief is their UX.
Designer skim
- Headline UX: One feed, many sources. Source-labeled per-message ("via Tryst", "via Proton: myprivatedelights@…"), reply routes back to origin channel transparently. Threaded by
prospect_idonceprospect-resolver(P4) catches up. - Sections (5): P1 inbox sources (channels + N mailboxes) · P2 unified feed · P3 per-source triage posture · P4 inbox health + degradation · P5 threaded-by-prospect view.
- Blocking Qs: OPEN-DECISIONS.md → P-Q1 reply-as-different-channel confirm UX.
In-the-wild copy
Source label on a message row (plain — V5 inbox rule: source clarity wins):
via Tryst · 11 min ago
Source label, per-mailbox Proton (plain):
via Proton: myprivatedelights@protonmail.com · 2 hr ago
Reply-as-different-channel confirm (plain):
They emailed. You're replying via iMessage — that uses a different identifier they may not have. Continue / switch back to email.
Inbox degraded (plain):
Tryst inbox stalled — last fetched 47 min ago. Showing what we have. Trying again.
Triage receipt, per-source policy (hearth — receipt, not decision):
Triage tended to the Tryst inbox while you slept. Six warm, nine cold (auto-replied), two flagged for your call.
Constraints
- One feed, many sources. No "switch to Tryst inbox" affordance — the unified feed IS the inbox.
- Reply routes back to source. A message from Tryst is replied to on Tryst (via
bookings-trystadapter); a Proton email is replied via SMTP throughmail-sync:4444; an iMessage replies viamac-sync:3100. The channel never leaks into Quinn's decision-making but the system tracks it exactly. - Source-labeled, not source-grouped. Per-message badge ("via Tryst", "via Proton: myprivatedelights@…") — never a top-level "Tryst tab".
- Per-channel cadence + auto-policy is opt-in, NOT global. Quinn may want triage to auto-reply on iMessage (5-check chain ported from v2 per brief J), draft-only on email, hands-off on directory inboxes initially.
- Email account multiplicity is real. Quinn has ≥4 mailboxes in v2 user data: Proton main, per-platform Proton aliases (e.g.
myprivatedelights@protonmail.com), and at least one Gmail per [registrations.md tracker contact info]. Each is a distinct inbox to ingest. - SMS ≠ iMessage. iMessage is Apple-protocol via mac-sync; SMS is carrier-protocol. Same prospect may send via both; same number, different channel. Treat as separate inbound surfaces; cross-link at the
prospect_idresolution step (per brief I §counter-actions).
P1 — Inbox sources (the actual channel roster)
Per brief O — N4 + extended for this brief. Each row is one ingest pipeline:
Direct-messaging channels (channel_kind)
| ID | Display | Ingest mechanism | Outbound reply | Notes |
|---|---|---|---|---|
imessage |
iMessage | mac-sync:3100 send-queue + LISTEN on macsync.messages |
mac-sync send-queue |
Anchor channel. v2 engine is built around this. |
sms |
SMS | Same mac-sync pipeline (Mac → carrier) | Same | Distinct from iMessage at the protocol level. v2 conflates them; v4 separates. |
signal |
Signal | Future MCP (no v0 listener) | Manual or future bot | P3+. |
telegram |
Telegram | Bot API (future) | Bot API | P3+. |
discord |
Discord | Bot API (future, community-server use) | Bot API | Light surface; lowest priority. |
Email inboxes (mailbox_id — multiple per user)
Email is distinguished by mailbox, not just by service. Quinn may have N Proton inboxes (one per directory's verification email) + Gmail + others. Each gets its own row in mailboxes table; engagement-ingestor polls each.
| Pattern | Example | Use |
|---|---|---|
| Personal anchor inbox | quinn@cocotte.maison (canonical) |
Anchor; brand-tier. |
| Per-platform verification inbox | myprivatedelights@protonmail.com |
Per-directory KYC + transactional emails. |
| Per-brand inbox | bookings@tqftw.com, hi@adult-therapy-tours.com |
Brand-site contact forms route here. |
| Legacy / personal | Gmail, iCloud | Read-only triage; ignored for outbound. |
All Proton inboxes wrap through mail-sync:4444 (one process per Proton credential — multi-credential ingestion is a mail-sync extension flagged for engineering).
Platform-native DMs (per surface, via bookings-* / content-* adapter fetchEngagement calls)
Inbound DMs from a surface are normalized into engagement_events rows tagged with surface + kind='dm' (per migration 0001 engagement_kind ENUM). UX-wise they land in the same unified feed; backend-wise they come through the per-surface adapter, not a separate channel pipeline.
| Surface (from brief O) | DM ingest? | Notes |
|---|---|---|
tryst, ts4rent, seeking, privatedelights, tsescorts, adultsearch, adultlook, eros, eroticmonkey, skipthegames, megapersonals, ts_live, slixa |
yes — via web-session adapter | Per-directory inbox endpoint scraped on schedule. Failure surfaces per brief M. |
onlyfans, fansly |
yes — via web-session adapter | Higher cadence (every ~10 min) due to PPV-time-sensitivity. |
x, instagram, tiktok, threads, bluesky |
yes — via real API or web-session | |
reddit |
yes — DMs + comment replies | Reddit's DM API is real. |
youtube, twitch, facebook |
low priority | Either Quinn doesn't get DMs here or volume is negligible. |
P2 — Unified inbox UX (the daily-driver feed)
Default view (in chat, under chat-home brief A)
- Inbox tile at the top of chat-home, below ambient strip (brief L §L2c). One line: "Inbox: 4 waiting" + recent-sender preview.
- Tap → opens the unified inbox drawer (new sibling drawer to those enumerated in brief B).
- Quinn doesn't ever have to open the inbox; ai-copilot surfaces the highest-stakes items as approval cards directly in chat (the existing brief A engagement-card flow). The drawer is for: bulk triage, looking up old threads, searching across sources.
Unified inbox drawer (new — extends brief B)
- Top filter bar: source-multiselect (default: all on), kind-filter (DM / comment / mention / email / SMS), date range, "show blocked-sender messages" toggle.
- Threaded by prospect when
prospect_idis resolved across surfaces (per brief I + brief Jprospect-resolverworker). When the same fan emails AND iMessages Quinn within an hour, they appear as one thread with both messages under a single header. - Per-message source chip:
Tryst,Proton: myprivatedelights@…,iMessage,OF DM, etc. Tappable → opens that source's settings (cadence, auth state, last successful poll). - Per-thread quick actions: Reply (drafts via
triageper brief L §L3c), Mark resolved, Block sender (per brief K §K1), Escalate. - Reply composer: when Quinn replies, the composer shows where the reply is going: "Replying via Proton (myprivatedelights@…)" or "Replying via Tryst". One-tap reply uses the same source; "Reply somewhere else" affordance lets Quinn pick a different channel (e.g. fan asked via Tryst, Quinn wants to escalate to iMessage).
Source labeling in approval cards (brief A)
- Existing engagement-card variant adds a
via {source}line under the prospect identifier. - The card's "Reply" action defaults to the source channel. Quinn can long-press to pick a different channel for the reply.
P3 — Per-inbox triage policy
Per brief L §L3c, triage runs the 5-check eligibility chain on every inbound. Per-source posture overrides the default:
| Source | Default triage posture |
Default auto-reply | Notes |
|---|---|---|---|
imessage (warm contacts) |
Full 5-check chain; auto-reply for warm tier | yes for Quinn-line + intro |
Ported verbatim from v2 engine. |
imessage (cold inbound) |
5-check + tier classification | draft-only for cold | Lower trust until Quinn graduates. |
sms |
Same as iMessage | yes for warm | Treated as iMessage-equivalent at the policy layer. |
Proton main (quinn@cocotte.maison) |
Read-only ingest + classify | draft-only | High-value; Quinn approves every reply. |
| Proton per-platform aliases | Read-only ingest + classify | draft-only | Mostly transactional (KYC, verifications); rarely needs reply. |
| Gmail / legacy inboxes | Read-only ingest, no outbound | n/a — never reply through these | Existence acknowledged; outbound disabled. |
Surface DMs (tryst, OF, etc.) |
Per-surface specialist drafts; triage does eligibility only |
per-surface (OF content-onlyfans auto-sends ≥0.85 confidence; directories draft-only initially) |
Specialist-axis decision in brief L. |
signal / telegram / discord |
Read-only ingest until MCP ships | n/a | P3+. |
Per-source settings live alongside the brief C notification matrix in /settings/inbox.
P4 — Inbox health (degradation + observability)
Per brief M, inbox ingest failures are NOT silent:
- Per-source health badge in inbox drawer header (green / yellow / red, mirrors fleet status strip).
- "Haven't seen new mail from X in N hours" banner when a polling source goes quiet (could mean Quinn really has no new mail, OR the adapter is broken — surface both possibilities, ask Quinn to verify if uncertain).
- Daily digest entry (brief I §I1) includes: "Pulled N messages from 6 sources today" + per-source breakdown.
- Inbox audit row: every fetch produces an
agent_actionsrow withaction_type='fetch_inbox',target_kind='mailbox'|'channel'|'surface', success/failure recorded.
P5 — Search across the unified inbox
Quinn searches by:
- Sender name / identifier (handles cross-surface fuzzy match via
prospect_id) - Free-text content
- Date range
- Source
- Has-draft / replied / unread / escalated state
Search runs against engagement_events.payload_json + full message bodies stored in engagement_events.body_text (TBD migration — needs a tsvector index for performance).
States to design
- Inbox tile in chat-home — idle (0 waiting), 1–9 waiting (badge), 10+ waiting (overflow badge), red-dot (urgent item per K3 stakes), source-down (one or more sources degraded).
- Unified inbox drawer — empty, dense with mixed sources, single-source filter active, threaded view (multi-message-same-prospect), search results, blocked-sender expander.
- Reply composer — single-source default, "reply somewhere else" picker open, draft-from-
triageready, error (channel offline → fallback suggestion). - Per-source settings —
/settings/inboxpage: one row per ingest source with status + cadence + posture toggles. - Inbox-degradation banner — one source down, multiple sources down.
- First-run — onboarding asks Quinn to connect inboxes (Proton creds, mac-sync paired, surface logins). UX echoes brief D persona-seed flow.
Out of scope
- Public anonymous email forms —
bookings@tqftw.comweb form on a brand site is a posting surface, not an inbox in the messaging sense. Brand sites POST to platform.api which writes toengagement_events; inbox UX shows them like any other inbound. - Sending bulk email — newsletters and digests go via
notifier(brief C) andcontent-newsletter(brief L), not the inbox. - Calendar/meeting invites parsed from email — future enhancement; defer until a user need lands.
- Schema migration for
mailboxes+channel_kindENUM — engineering follow-up, not design (flagged in P1).
Open questions
- Multi-mailbox auth on Proton —
mail-synccurrently wraps one Proton Bridge credential. Quinn needs ≥4 Proton inboxes. Does mail-sync run one process per credential (cleanest), or extend to multi-credential (more code, one process)? Engineering call. - Cross-channel prospect dedup latency — when a fan emails AND iMessages within 30s, does the unified feed wait to coalesce them, or show both immediately and re-thread when
prospect-resolvercatches up? Lean: show immediately with a small "resolving…" pip, then merge. - Reply-as-different-channel UX — fan emails, Quinn replies via iMessage. Should the system prompt her to confirm ("this fan emailed you — replying via iMessage exposes a different identifier"), or just do it? Lean: silent-but-logged for warm/known prospects; confirm for unknown.
- Surface-DM cadence per-directory — OF every 10min vs directories every hour vs others. Default policy here or per-surface specialist config? Lean: per-specialist config;
bookings-directoriescarries default cadences inpersonas.facets[directory_id].inbox_poll_seconds. - iMessage vs SMS in v2 — v2 collapses both as "iMessage". Does v4's separation create migration friction with the v2 prospect history? Defer to brief J's P0 verdict (a) — freeze v2 history, fresh
agent_actionsstart.
Related
- brief A — inbox tile + engagement-card source labeling lives in chat-home.
- brief B — unified inbox drawer is a new sibling to B1–B6.
- brief C — outbound-notifications counterpart; inbox UX defines inbound, C defines outbound.
- brief I — every inbox fetch + reply lands in audit; prospect dedup powers the threaded view.
- brief J —
engagement-ingestorworker + 5-check eligibility chain are the ported v2 plumbing this brief consumes. - brief K §K1 — prospect blocklist suppresses messages here (collapsed under "blocked messages today — review?" card).
- brief L §L3c —
triagespecialist owns inbound classification + draft replies. - brief M — inbox degradation behaviors.
- brief O — N4 — channel + brand-site surfaces feeding the inbox.