Heartbeat
Last verified: 2026-05-14
Heartbeat is user-controlled (on/off toggle, interval, active hours live in the settings UI). The AI cannot enable, disable, or reschedule it. To customise what happens on each heartbeat, the AI creates heartbeat-triggered scheduled tasks via
schedule_taskwithon_heartbeat: true— these areHEARTBEAT-trigger tasks (see tasks.md) and their prompts are appended to every heartbeat run under## Heartbeat Additions. Each addition is a first-class task the user can see, edit, and cancel.
Kai's heartbeat feature enables periodic automatic self-checks. The AI reviews pending tasks, email status, newly arrived emails, and learned memories on a configurable interval, surfacing anything that needs attention without requiring user interaction.
Concepts
Heartbeat
A silent, scheduled prompt sent to the AI during active hours. If nothing needs attention, the AI responds with "HEARTBEAT_OK" and the user sees nothing. If something requires follow-up, the response appears as an assistant message in the chat.
Active Hours
A configurable time window (default 8:00–22:00) during which heartbeats are allowed to fire. Outside this window, heartbeats are skipped regardless of interval.
Promotion
A mechanism for graduating well-established memories into the permanent soul/system prompt. Memories that have been reinforced 5 or more times become promotion candidates and are surfaced during heartbeat checks for the AI to evaluate.
Configuration
Heartbeat configuration is stored as a serialized JSON object in app settings. Values are only editable from the settings UI — there is no AI tool that can flip them:
- Enabled: true
- Interval: 30 minutes between heartbeats (UI slider offers 5m, 10m, 15m, 30m, 45m, 1h, 2h, 4h)
- Active hours start: 8 (hour, 24h format; UI range slider covers 0–23)
- Active hours end: 22 (hour, 24h format; UI range slider covers 0–23)
- Model: optional override for which service+model to use for heartbeats. When not set, the first configured service is used (default behavior). Useful for selecting a cheaper or faster model for background checks
UI validation rules:
- Interval must be at least 5 minutes
- Active hours must be in the range 0–23
Execution Flow
- The task scheduler polls every 60 seconds
- On each poll, it checks: is the overall scheduling-enabled toggle on (the tasks setting that gates all scheduled work)? Is heartbeat enabled? Is the current hour within active hours? Has the configured interval elapsed since the last heartbeat?
- If all conditions are met and no other API call is in progress, a heartbeat prompt is built and sent via
askWithTools(which includes the full tool-calling loop) - The last heartbeat timestamp is updated and a log entry is recorded
When the overall scheduling-enabled toggle is off, heartbeats do not run regardless of the heartbeat toggle, interval, or active hours.
Response Handling
- If the AI response contains "HEARTBEAT_OK", nothing is shown to the user
- Any other response is saved into a dedicated heartbeat conversation (type
heartbeat) viaaddAssistantMessage - A dismissable banner appears at the top of the chat when the heartbeat has something to report
- Android push notification: when the heartbeat produces a non-OK report and the app is not currently in the foreground, a push notification fires. Foreground state is tracked via
ProcessLifecycleOwnerinKaiApplicationand mirrored toTaskScheduler.appInForeground. Tapping the notification launches/foregrounds the app and deep-links into the heartbeat conversation via theEXTRA_OPEN_HEARTBEATintent extra;ChatViewModelconsumes the signal throughDataRepository.openHeartbeatRequestedand callsloadConversationon the heartbeat conversation id. The notification uses a fixed id so a fresh report replaces the previous unread one instead of stacking. The notification body is the heartbeat response with markdown formatting stripped and truncated to 240 characters at a word boundary (so the preview never breaks mid-word or shows raw markdown syntax). Desktop/iOS/web no-op (banner-only). - Tapping the banner loads the heartbeat conversation so the user can read the report and reply
- The X button dismisses the banner without navigating
- Heartbeat conversations are included in the chat history list with a "Heartbeat" label badge, and can also be accessed via the banner
- The heartbeat prompt is sent as a standalone message (not including user chat history as context)
- If the API call fails, a failure entry is recorded in the heartbeat log
Prompt Building
The heartbeat prompt is assembled by the pure function buildHeartbeatPrompt (in HeartbeatPromptBuilder.kt). Each conditional section is covered by HeartbeatPromptBuilderTest. Sources:
- Custom prompt — user-defined text from settings, or the default prompt if empty. The default instructs the AI to review memories and tasks, respond "HEARTBEAT_OK" if nothing needs attention, or address anything that does
- Previous heartbeat results — the last 3 responses from the heartbeat conversation, so the AI can track trends, avoid repeating notifications, and detect persistent issues (e.g. "email still unread since last check")
- Pending tasks — all tasks with status PENDING are listed with their description, id, scheduled time, and cron expression (if recurring)
- Email status — if email is enabled and accounts exist, each account's email address, unread count, and last sync time are included
- New emails — headers (subject, from, preview) for emails polled since the last heartbeat pickup. Emails are fetched in the background by the email poll loop and buffered in a pending queue (capped at 100, FIFO). The heartbeat consumes the queue: everything the heartbeat saw is removed from the queue after a successful run, while emails that arrive during the heartbeat call remain for the next run. After consumption the heartbeat also advances each account's delivery watermark, so a follow-up
check_emailcall from the user won't re-surface the same messages — Kai tracks read/unread internally and ignores the provider's\Seenflag - New SMS — SMS messages received since the last heartbeat. Consumed analogously to emails: buffered as they arrive, surfaced once under
## New SMS, and cleared from the pending queue after a successful run so the next heartbeat only sees newer arrivals - New notifications — Android notifications captured since the last heartbeat, capped at 20 newest-first. Consumed analogously to emails: buffered as they arrive, surfaced once under
## New Notifications, and cleared from the pending queue after a successful run so the next heartbeat only sees newer arrivals - Promotion candidates — memories with 5 or more hits are listed with their key, hit count, category, and content, along with a suggestion to use the
promote_learningtool
For the full contract of every prompt variation in Kai (chat remote/local, heartbeat, Splinterlands) see system-prompts.md.
Heartbeat Log
- Stores up to 5 most recent heartbeat entries
- Each entry records success/failure, a timestamp, and an optional error message
- Displayed in the settings UI under the heartbeat section
- Entries show an OK/FAIL indicator and a formatted local timestamp
- Failed entries display the error message (single line, ellipsized) below the timestamp in the error color
Promote Learning
When a memory has been reinforced 5 or more times, it becomes a promotion candidate. The promote_learning tool:
- Looks up the memory by key
- Appends the provided
soul_additiontext to the soul/system prompt - Removes the original memory from the memory store
- Returns confirmation with the promoted key and hit count
This allows well-established patterns to graduate from ephemeral memory into permanent AI behavior.
Settings UI
The heartbeat section in settings contains:
- Toggle — enables or disables heartbeat with a switch
- Interval display — shows the current interval in minutes in the section description
- Interval slider — a snap-to-preset slider with positions for 5m, 10m, 15m, 30m, 45m, 1h, 2h, 4h. Displays the formatted value (e.g. "15m", "2h") next to the label
- Active hours range slider — a dual-thumb range slider spanning 0–23 (24-hour clock). Displays "H:00 – H:00" next to the label (unpadded hours)
- Model picker — a dropdown button showing the selected service+model, or "Default" when no override is set. Opens a dropdown menu listing all configured services with their icons and model IDs. Selecting "Default" clears the override and uses the first configured service. If the previously selected service is removed, heartbeat falls back to the default automatically
- Custom prompt editor — a text field (max 4000 characters) for editing the heartbeat prompt, with a save button that appears when changes are detected. Shows the default prompt text when no custom prompt is set. A character counter (X/4000) is displayed in the editor as the user types
- Reset to default — when a custom heartbeat prompt is set, a reset button appears in the custom-prompt section header. Tapping it opens a confirmation dialog; confirming clears the custom prompt and restores the default
- Log display — when log entries exist, shows a "Recent" label followed by each entry with an OK/FAIL indicator and timestamp
- Manual refresh — a refresh icon next to the "Recent" label runs a heartbeat immediately, bypassing the active-hours window and the interval-due check. Only fires while heartbeat is enabled and scheduling is on; the icon shows a progress spinner during the call
AI Tools
| Tool | Purpose |
|---|---|
promote_learning |
Promote a reinforced memory into the soul/system prompt |
Standing additions to heartbeat behaviour are created with schedule_task(on_heartbeat=true) — see tasks.md. Those prompts are appended to the main heartbeat self-check, not replaced.
Key Files
| File | Purpose |
|---|---|
composeApp/src/commonMain/.../data/HeartbeatManager.kt |
Config, prompt building, log management |
composeApp/src/commonMain/.../tools/HeartbeatTools.kt |
AI tool definitions for heartbeat and promotion |
composeApp/src/commonMain/.../data/TaskScheduler.kt |
Poll loop that triggers heartbeat checks |
composeApp/src/commonMain/.../data/AppSettings.kt |
Persisted heartbeat config, prompt, and log storage |
composeApp/src/commonMain/.../data/RemoteDataRepository.kt |
Heartbeat conversation creation, unread flag management |
composeApp/src/commonMain/.../ui/chat/composables/HeartbeatBanner.kt |
Dismissable notification banner UI |
composeApp/src/commonMain/.../ui/settings/SettingsScreen.kt |
Heartbeat settings UI section |
composeApp/src/commonMain/.../Platform.kt |
expect fun sendHeartbeatNotification — push notification for background heartbeat reports |
composeApp/src/androidMain/.../HeartbeatNotifier.android.kt |
Android actual + EXTRA_OPEN_HEARTBEAT deep-link constant |
androidApp/src/main/kotlin/.../MainActivity.kt |
Reads EXTRA_OPEN_HEARTBEAT in onCreate/onNewIntent and calls DataRepository.requestOpenHeartbeat |