message-center/specs/outbound-follow-up-tracker.md

149 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Outbound Email Follow-Up Tracker
*Spec — February 14, 2026*
*Status: Parked, ready to build*
---
## Problem
Johan sends emails that require responses. If no reply comes, nobody notices until he manually remembers to check. Conversations also move to phone/WhatsApp/in-person — so "no email reply" doesn't always mean "not handled."
## Solution
Triage outgoing emails the same way we triage incoming: K2.5 reads each sent email and decides if it expects a reply. If yes, track it with a deadline. If the deadline passes with no reply, push one alert to Fully. Dismiss via Fully or auto-resolve on inbound reply.
## Architecture
Everything lives in MC. Fully is just the display layer.
### IMAP Changes
Add Sent folder polling to existing connectors:
- `tj_jongsma_me` → poll `Sent` alongside `INBOX`
- `johan_jongsma_me` → poll `Sent` alongside `INBOX`
Tag outgoing messages with `direction: "outbound"` in MC's message store (inbound messages get `direction: "inbound"` or remain untagged for backwards compat).
### Schema: `follow_ups` table (orchestration.db)
```sql
CREATE TABLE follow_ups (
id TEXT PRIMARY KEY, -- MC message ID of the outbound email
to_address TEXT NOT NULL, -- recipient email
to_name TEXT, -- recipient display name
subject TEXT, -- email subject
sent_at TEXT NOT NULL, -- ISO timestamp
deadline_hours INTEGER DEFAULT 48, -- expected response window
expects_reply INTEGER DEFAULT 1, -- 1=yes, 0=no (triage decided no)
reply_received INTEGER DEFAULT 0, -- auto-set on inbound match
reply_message_id TEXT, -- MC message ID of the inbound reply
alerted_at TEXT, -- timestamp when Fully alert was pushed (NULL = not yet)
dismissed INTEGER DEFAULT 0, -- set via Fully callback
dismissed_at TEXT, -- timestamp
created_at TEXT DEFAULT (datetime('now'))
);
```
### Triage (K2.5)
Same triage engine, second pass for outbound messages. Prompt addition:
```
For OUTBOUND emails, determine:
1. Does this email expect a reply? (question, request, proposal, scheduling = yes. FYI, acknowledgment, thank you = no)
2. How urgent is the expected reply?
- urgent: 24h (time-sensitive, medical, financial)
- normal: 48h (business correspondence, requests)
- low: 7d (informational, low-priority asks)
3. Output: { "expects_reply": true/false, "deadline_hours": 24|48|168 }
```
### Alert Flow
**Check runs every hour (cron or heartbeat):**
```
SELECT * FROM follow_ups
WHERE expects_reply = 1
AND reply_received = 0
AND dismissed = 0
AND alerted_at IS NULL
AND datetime(sent_at, '+' || deadline_hours || ' hours') < datetime('now')
```
For each result:
1. POST to Fully: `{ "message": "🟠 No reply from {to_name} re: {subject} — sent {N} days ago", "priority": "warning" }`
2. SET `alerted_at = now()`
3. **Never alert again for this follow-up**
### Auto-Resolve
When an inbound email arrives, check if sender matches any active follow-up:
```
UPDATE follow_ups
SET reply_received = 1, reply_message_id = ?
WHERE to_address = ?
AND reply_received = 0
AND dismissed = 0
ORDER BY sent_at DESC
LIMIT 1
```
Match by email address. Thread matching (In-Reply-To header) is better but address matching covers 90% of cases and is simpler.
### Fully Dismiss Callback
When a follow-up alert is dismissed on Fully (long-press done or ×):
Alert dashboard POSTs to MC:
```
POST /api/follow-ups/{id}/dismiss
```
MC sets `dismissed = 1, dismissed_at = now()`.
This requires:
1. Follow-up alerts include the MC follow-up ID in their metadata
2. Alert dashboard `removeAlert` / done handler checks for follow-up ID and calls MC
### Auto-Expire
Follow-ups that were alerted but not dismissed auto-expire after 14 days:
```
UPDATE follow_ups SET dismissed = 1
WHERE alerted_at IS NOT NULL
AND dismissed = 0
AND datetime(alerted_at, '+14 days') < datetime('now')
```
Runs on the same hourly check.
## Edge Cases
- **Reply via WhatsApp/phone:** Johan dismisses the Fully alert. That's the signal.
- **Multiple emails to same person:** Each gets its own follow-up. Inbound reply resolves the most recent one.
- **CC/BCC recipients:** Only track the TO address. CC'd people rarely owe a reply.
- **Auto-replies/OOO:** Don't count as a real reply. Triage can detect these.
- **Johan replies again (bump):** If Johan sends a second email to the same person on the same thread, reset the deadline on the existing follow-up.
## Implementation Order
1. Add Sent folder to IMAP connectors
2. Add `direction` field to MC message model
3. Create `follow_ups` table
4. Add outbound triage prompt to K2.5
5. Add follow-up check to hourly cron / heartbeat
6. Add auto-resolve on inbound match
7. Add dismiss callback endpoint in MC
8. Wire alert dashboard to call MC on dismiss
9. Add auto-expire cleanup
## Not In Scope (Yet)
- WhatsApp outbound tracking (could add later, same pattern)
- Calendar meeting follow-ups (different trigger, same tracker)
- Priority escalation (if 2x deadline passes, escalate from 🟠 to 🔴)