Skip to content

Background Notifications (Android)

Hive's in-app notifications only fire while the app is open and connected. On Android, background notifications close that gap: the app briefly wakes on a timer, connects to your cluster, and raises a native notification for anything that happened while it was closed or backgrounded - a finished terminal agent or durable chat, a scheduled task that succeeded or failed, or a session waiting on you.

This is opt-in and battery-friendly. Hive does not hold a constant connection; it connects for a moment roughly every 15 minutes, checks for missed alerts, and disconnects.

Turning it on

Open Settings -> Notifications on the Android app. Under Background notifications (this device), enable Check for missed notifications while the app is closed.

The toggle only appears on Android, where the native scheduling support exists. On desktop and in the web client the app relies on the normal in-app / OS alerts while it is running (see Agent Waiting Marker), so there is no equivalent toggle there.

What you get notified about

While the app is closed, a background check surfaces the same events the app would have alerted you to live:

  • A terminal agent or durable chat finished - a session went idle or a chat reply became ready.
  • A scheduled task succeeded or failed - honouring the per-task Notify on success / Notify on failure opt-ins (see Tasks). A task with neither flag set stays silent.
  • A session is waiting for you - the same "needs input" signal that drives the ? marker (see Agent Waiting Marker).

Each notification carries a title and body and, where relevant, targets the terminal session or chat it relates to so a tap can open it.

Timing: expect up to ~15 minutes

The ~15 minute cadence is a floor imposed by Android, not a setting. Hive uses an inexact repeating alarm, which Android is free to delay and batch, especially under Doze (the OS's idle power-saving mode). So a missed alert can arrive up to about 15 minutes late, sometimes more if the device has been idle for a long stretch.

This is a deliberate trade-off: a constant push connection would drain the battery, while a periodic wake keeps power use negligible. If you need to-the-second alerts, keep the app open in the foreground, where in-app notifications fire immediately.

No double alerts while another client is connected

If any Hive app is connected to the cluster - say Hive is open on your desktop - that client already receives every alert as a live push. The background poll stays silent for as long as such a client is connected, so your phone does not buzz for things you are already looking at elsewhere. The poll cursor still advances during the quiet period; those alerts are considered delivered and do not arrive late once the other client disconnects.

Two details:

  • Only app clients count. A transient hive CLI invocation does not suppress background notifications.
  • Answering a waiting session clears its pending attention records from the durable log immediately, so a prompt you resolved on the desktop cannot re-notify your phone on the next poll even if the desktop disconnects right after.

Battery saver: background idle sleep (mobile)

Related to, but separate from, background notifications: on phones and tablets the app has a battery saver (Settings -> Battery, mobile only). When the app has been in the background for the configured idle window (default 15 minutes), it disconnects from the daemon - stopping the Android keep-alive foreground service, its ongoing "Keeping your sessions connected" notification, and the WebSocket keepalive pings that would otherwise hold the radio awake. The app reconnects automatically the moment you return to it. After the connection and initial data recovery complete, Hive keeps the connect/reconnect loading indicator visible only briefly to avoid a flash. The artificial minimum is capped at 750 ms rather than 7.5 seconds, so recovered content becomes usable promptly after reopening or resuming the app.

On Android the countdown runs natively (a one-shot AlarmManager alarm), because Android freezes a backgrounded WebView's JS timers - a JS-only timer would never fire exactly when it matters. The alarm is a non-wakeup alarm: if the device is in deep sleep at the deadline, the disconnect happens on the next natural wake instead of waking the CPU just to save power.

While asleep the app holds no connection, so enable background notifications if you still want missed-alert delivery during that window.

Cluster-wide coverage

The background check polls a single node - whichever one your app's active profile points at. That node aggregates missed notifications from the whole cluster before answering, so a task failure, an idle terminal agent, or a completed chat on any node still reaches your phone through the one node you poll. You do not need to poll every node individually.

How it works

The feature has two halves: a durable notification log the daemon keeps, and a native Android poller that reads it.

Daemon: durable notification log

Every alert the daemon fires is appended to a durable log (a SQLite notifications table, added in schema migration v28) so a client that was offline when the alert fired can still see it on its next poll. The log is bounded: rows older than 7 days, or beyond the newest 500, are pruned after each insert.

Each record carries a stable id (clients dedupe on it across polls), the firing time in Unix milliseconds, a machine-readable kind, an optional session_id tap-to-open target, the firing node_id, and a title / body. Terminal targets store a session UUID. Chat targets store chat:<uuid> so opening the notification routes to the originating chat.

The kind is one of:

kindMeaning
agent_idleA terminal agent went idle or a durable chat reply completed
attentionA session is waiting for the user (the ? state)
task_successA scheduled task completed with exit 0
task_failureA scheduled task failed (non-zero exit or timeout)

HTTP endpoint

The Android poller reads the log over a small authenticated HTTP endpoint:

GET /notifications?since_ms=<unix_ms>
  • Authenticate with the daemon token, either as Authorization: Bearer <token> or as a ?token=<token> query parameter. The cluster token is also accepted.
  • The response is { "now_ms": <server_time>, "items": [ ...NotificationRecord ] }, where items are the notifications fired after since_ms, aggregated across the cluster.
  • The client persists the returned now_ms as its next since_ms cursor, so each poll only returns what is new since the previous one.
  • While any app client is connected anywhere in the cluster, items is empty (see "No double alerts" above); now_ms still advances the cursor.

The same aggregation is available over the WebSocket protocol via ClientMessage::GetNotifications { since_ms } -> ServerMessage::Notifications { items, now_ms, active_clients }, which the polled node uses to fan out to its peers: each peer answers with its own log slice and its local count of connected app clients, and the polled node merges the items and sums the counts (see Protocol).

Android native side

The native scheduling is generated into the Android project at build time by crates/clusto-app/scripts/patch-android-keyboard.mjs. It wires up:

  • An AlarmManager inexact-repeating alarm that wakes the app roughly every 15 minutes.
  • A BroadcastReceiver (HiveBackgroundNotifyReceiver) that runs on each alarm: it does the GET /notifications request with the stored cursor and posts a native notification for each new item.
  • A boot receiver that re-arms the alarm after the device reboots, so the feature keeps working across restarts without reopening the app.

Configuration (enabled flag, base URL, token, and the since_ms cursor) lives in Android SharedPreferences, written when you toggle the setting and read by the receiver on each wake.

Hive - remote AI coding agents over WebSocket.