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

5.0 KiB
Raw Blame History

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)

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 🔴)