Heartbeat
Edward's proactive monitoring system — listening for messages, calendar events, and emails without being asked.
Overview
The heartbeat system lets Edward act on incoming information before you open the chat. It monitors iMessage, Apple Calendar, and Apple Mail, triaging each event through a multi-layer classification pipeline to decide whether to ignore, remember, respond, or alert you.
Architecture
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ iMessage │ │ Calendar │ │ Email │
│ Listener │ │ Listener │ │ Listener │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└────────────────┼────────────────┘
↓
┌───────────────────┐
│ Triage Engine │
│ L1 → L2 → L3 │
└────────┬──────────┘
↓
┌──────────────┼──────────────┐
↓ ↓ ↓
[DISMISS] [NOTE/ACT] [ESCALATE]
memory/reply reply + pushListeners
iMessage
Polls the macOS Messages database (~/Library/Messages/chat.db) every 10 seconds for new incoming messages. Uses Apple's timestamp format (nanoseconds since 2001-01-01). Only picks up messages from other people — Edward's own outbound messages are ignored.
Calendar
Polls Apple Calendar via MCP tools at a configurable interval (default 300 seconds). Looks ahead a configurable number of minutes for upcoming events. Useful for proactive reminders about meetings or appointments.
Polls Apple Mail for unread emails. Filters for messages that mention @edward to avoid noise. Only processes emails that explicitly invoke Edward.
3-Layer Triage
Every incoming event passes through up to three triage layers, stopping as soon as a definitive action is determined:
Layer 1 — Rules (Zero Cost)
Fast pattern matching with no LLM calls. Catches obvious cases like known spam senders, automated notifications, or messages from Edward himself. Events that pass Layer 1 move to Layer 2.
Layer 2 — Haiku Classification
Claude Haiku classifies the event's urgency and determines what action to take. This is the primary decision point for most messages. Classification outputs one of the action types below.
Layer 3 — Execute
Carries out the determined action — storing a memory, sending a response via chat_with_memory(), or pushing a notification.
Actions
| Action | Description |
|---|---|
DISMISS | Ignore the event — no further processing |
NOTE | Store the information as a memory for future reference |
ACT | Respond to the message using Edward's full tool access |
ESCALATE | Respond and send a push notification to alert you |
@edward Mentions
Messages containing @edward bypass the normal triage flow entirely and are routed directly to Layer 3 for immediate response. This lets anyone in a group chat summon Edward by name.
Listening Windows
When Edward responds to a message, a 5-minute follow-up window opens for that conversation. Any replies during this window are treated as continuations rather than new events, keeping the conversation coherent.
Active Chat Gating
When you're actively chatting with Edward in the web UI, the triage system pauses to avoid duplicate responses. It resumes automatically when the chat goes idle.
Briefing Injection
Pending heartbeat events that require attention are injected into Edward's system prompt. When you open the chat, Edward can proactively brief you on things that happened while you were away.
Configuration
Heartbeat settings are managed via PATCH /api/heartbeat/config or the settings UI:
| Field | Default | Description |
|---|---|---|
enabled | true | Master switch for the heartbeat system |
poll_seconds | 10 | iMessage polling interval |
calendar_enabled | true | Enable calendar listener |
calendar_poll_seconds | 300 | Calendar polling interval |
calendar_lookahead_minutes | 30 | How far ahead to look for events |
email_enabled | true | Enable email listener |
email_poll_seconds | 300 | Email polling interval |