Skip to content

Push notifications

A paired phone can alert you when a session needs you. Notifications are the reason the companion app is worth having: an agent that stops for a permission prompt at 11pm is only useful to know about if something tells you.

CategoryNotification titleChannelFires when
input-requiredAgent needs your inputNeeds attentionThe agent stopped and is waiting on you, including permission prompts
turn-completeAgent went idleCompletionsThe agent went quiet and stayed quiet. Since v0.36.0 this waits for the session to settle rather than firing on every pause, because a conversational back-and-forth leaves gaps of tens of seconds and each one used to be its own notification
plan-completePlan completeCompletionsThe agent finished planning
session-failedSession stoppedFailuresThe session exited with an error
spawn-stalledStill preparingStallsA spawn is taking too long waiting on its git worktree

Each category maps to its own notification channel, so you can tune them independently in your phone’s system settings. Muting completions while leaving “needs attention” loud is the common setup.

A Command Terminal raises the same categories as a task’s agent. Since v0.46.0 its notification is titled with the terminal’s name, and a tap opens that terminal rather than a task. Whether the alert is for a terminal travels inside the encrypted envelope with the name, so the delivery network does not learn it.

A phone that is already connected to the desktop does not get pushed to. It is watching the session live, so a notification about what it is already showing you is noise. Kangentic decides that per device: pair two phones, leave one connected and the other in your pocket, and only the pocket one is pinged.

“Connected” here means the tunnel is actually up, not that the phone once said hello. Before v0.41.0 the check read a lower-level flag that a phone dying silently - a dropped signal, a killed app, a flat battery - left set for the rest of the keepalive window, so pushes were suppressed for up to a minute after the phone stopped being able to receive them. It now reads the same connection state the app’s own status badge reports.

A rate limit sits behind that: one notification per category per session per device every 30 seconds, so an agent flipping in and out of a state cannot ring your phone repeatedly. A push that genuinely fails to send releases its slot immediately rather than spending the whole window on a notification that never arrived.

The app asks for notification permission at runtime rather than on first launch, so the request arrives when it makes sense. Declining leaves the rest of the app fully usable; you simply have to open it to see what changed. Unpairing a device clears its push registration.

Push notifications travel through Expo’s push service and then the platform networks, which are Firebase Cloud Messaging on Android and APNs on iOS. That path is outside the encrypted tunnel your phone and desktop share, so Kangentic does not put anything meaningful in it.

The relay is not on this path at all. It forwards a live connection between two devices and has nowhere to hold a message for a phone that is not connected, which is exactly why the offline case is a push notification rather than something queued for later.

Every push carries the real content as an encrypted blob. What travels alongside it differs by platform, and since v0.36.0 the two are deliberately different:

  • Android gets a data-only message: no title, no body, no channel id. Android renders an incoming notification payload itself before handing off to the app, so anything visible in the push was drawn a second time on top of the decrypted notification the app posts. Sending nothing visible is what stops the double render, and the channel is chosen on the device from the decrypted category anyway, which only the phone can know.
  • iOS keeps a generic title, Kangentic, and a static placeholder body for the category in transit, because APNs only hands a push to the app’s notification service extension when the push carries an alert. That extension decrypts the envelope on your phone and replaces the placeholder with the real title and task name before iOS shows the alert. If it cannot decrypt, the placeholder is what you see.

The blob is sealed with XChaCha20-Poly1305 under a key your device generated, and it is opened on your device. Expo, Google, and Apple see at most the placeholder, your device’s push token, and the delivery timing. They never see the task name, the agent output, or anything else from the session.

If a payload cannot be decrypted for any reason, the app shows the generic placeholder rather than ciphertext or a partial result.

When the app is running in the background with a live connection, it can raise the same categories directly from the activity it is already watching, without a round trip through the push network. The titles and channels are shared with the push path so the two cannot drift apart.

Kangentic is free and open source. A star helps other people find it.

Star on GitHub