Spec: outbound email follow-up tracker

This commit is contained in:
James 2026-02-14 04:43:50 -05:00
parent 90485d381d
commit 6d48e6a826
2 changed files with 149 additions and 1 deletions

View File

@ -56,4 +56,4 @@ triage:
base_url: https://api.fireworks.ai/inference/v1 base_url: https://api.fireworks.ai/inference/v1
api_key: ${FIREWORKS_API_KEY} api_key: ${FIREWORKS_API_KEY}
model: accounts/fireworks/models/kimi-k2p5 model: accounts/fireworks/models/kimi-k2p5
dashboard_url: http://localhost:9202/api/alerts dashboard_url: http://localhost:9200/api/news

View File

@ -0,0 +1,148 @@
# 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 🔴)