A Notification Is a Decision
Nothing in Wolffish notifies you automatically. No event listener fires a push when an automation ends, no watcher pings you when a download lands. When your phone buzzes, it is because the agent decided you should know — it composed a title and a body, chose how urgent the moment was, and called a tool. That deliberateness is the design: an inbox of machine-generated noise teaches you to ignore it, and a notification you ignore is worse than none. This page is the whole system: the tool the model calls, the guardrails the desktop stamps on every send, where a tap lands, how unread badges stay honest, and exactly what the relay in the middle gets to see. Pairing itself — the QR, the code, the encrypted tunnel — lives on the Mobile App page.The notify_phone Tool
With Phone notifications on, the agent carries a phone capability with one tool in it: notify_phone. The instructions it works under are narrow — send one when a long or background run finished with a result worth seeing, when something failed or surfaced a surprise, when it is blocked on your input or an approval, or when you asked to be told. Never for routine progress, and never for a turn you are sitting there watching.
The model chooses the words and the moment; each call also names a phase, and the phase decides how long the notification stays relevant before the platforms stop showing it:
An
urgency of high asks the platform to present the notification promptly; normal is the default.
Off makes the tool refuse — by removing it. The switch doesn’t ask the agent to hold back; it unregisters the
phone capability, so notify_phone vanishes from the model’s tool surface entirely. The same is true when no phone is paired: the tool’s presence is the availability check, so the agent never drafts a notification that has nowhere to go.What the Model Writes, What the Desktop Stamps
The model is trusted with the words and nothing else. Everything that decides where a notification goes and which notification it is comes from the desktop:- Identity — the notification id and the phone it routes to are stamped by the harness, never taken from the model. The id is minted fresh per call, and it is what makes retries and duplicate deliveries fold into one banner instead of two.
- Clamps — the title is cut to one line of 60 characters, the body to 180, and control characters are stripped. Model output is sanitized rather than trusted; malformed frames are rejected outright, never quietly truncated, so what arrives is always something the desktop actually composed.
- The run — the run id travels with the notification, read from the turn’s own scope rather than the model’s memory of it.
- The link — a deep link is rejected unless it uses the app’s own
wolffish://scheme and names a screen the phone actually has. A bad link fails at the tool call, with the list of real destinations in the error, rather than traveling all the way to the phone to quietly drop you on the home screen.
Where a Tap Lands
A notification can name a screen, and it can name this run’s own conversation without knowing its id:current removes the guess, and it is the destination the agent is told to prefer almost every time.
The destinations are a fixed list, shared by both apps, and cover every screen the phone actually has:
On the phone, a tap does the right thing from either temperature: a tap while the app is running pushes the screen so back returns you to where you were, and a tap that launched the app is read on the entry screen’s first render, so its destination is the boot destination rather than a second navigation racing the redirect. A conversation reached this way waits out a short grace period for the desktop instead of flashing an empty new chat, a link this build can’t resolve leaves you exactly where you are, and one tap moves the app exactly once — even when both paths see it.
Delivery: Tunnel First, Push Second
A notification takes the most private road available, and falls back only when it must:- The live tunnel. If the phone is connected, the notification rides the encrypted link as a control frame and renders in-app. The relay waits a beat for the phone to acknowledge it.
- Platform push. If the phone is offline — or the acknowledgment never comes — the relay hands the notification to the platform push service (Expo, in front of APNs on iPhone and FCM on Android), and the OS delivers the banner.
- Nowhere. If the phone never registered for push and isn’t connected, the send is reported back as dropped — the desktop hears it immediately, and the agent is told rather than left assuming you know.
Unread Badges
Every notification that names a conversation feeds a per-conversation unread count, and the counts surface everywhere a conversation does: on the rows of the conversations sheet, on the History list, summed on the floating menu button, and as the number on the app icon — capped at99+ in the lists, at 999 underneath.
Clearing is reading. Opening a conversation zeroes its bucket and dismisses its notifications from the system tray; deleting a conversation — from the phone, or from the desktop with the phone watching — takes its count with it; disconnecting clears everything. There is no “mark as read” chore, because the badge was never a separate ledger — it is a running count of what you haven’t looked at.
The subtle part is the app icon when the app is dead. A closed app can’t count anything, so the relay keeps one integer per device — incremented once per notification it routes, stamped onto every platform push so the icon is correct before the app has even woken. The moment the app runs again, the phone reports its true total and overwrites the relay’s number absolutely — the phone’s count is authoritative, the relay’s is a stand-in for when nobody’s home, and a lost or reordered frame can never make them drift apart.
What the Relay Can See
The blind relay carries conversation traffic as sealed frames it cannot read. Notifications are the one deliberate exception, and you should know its exact shape. A platform push has to leave your devices in the clear eventually — Apple and Google render the banner, so Apple and Google see its text. That makes sealing the relay leg alone a false comfort, and Wolffish doesn’t pretend otherwise: a notification travels to the relay as a readable control frame, is acted on there, and is never forwarded as tunnel traffic. What that means concretely:- The relay sees, in the moment: the title and body the model wrote, the deep link — including the conversation id when the tap targets a chat — the phase, urgency, run id, and the phone it’s addressed to.
- The relay stores none of it. Titles and bodies are never written down and never logged. What persists is bookkeeping: the device’s push token, platform and app version, the badge integer, and short-lived delivery records that age out on their own — minutes for delivery tickets, a day for the duplicate-folding ids.
- Conversation content never rides this path. Your transcripts, files and settings cross the relay only as sealed frames it cannot open. The agent’s own instructions say the same thing from the other side: a notification is a headline, not a transcript.
Registration, and Leaving No Trace
The phone registers for push under a random device id — minted once, kept in the OS keystore, and deliberately unrelated to the tunnel’s cryptographic identity, so the registration can’t leak the keys. Registration refreshes itself on every pairing, every reconnect, every return to the foreground, and every push-token rotation; the OS permission prompt appears the first time the phone connects after install. Decline it and nothing breaks — notifications simply arrive only while the app is connected, over the tunnel. Leaving reverses all of it, in the right order. Disconnecting the phone clears every badge — buckets, tray, app icon — tells the relay to zero its copy, then sendsunregister_push, and the relay deletes the device registration on the spot: the push token is forgotten, and every later notification addressed to that phone is answered dropped, not rerouted. Only then does the phone drop the link and wipe itself. Pairing again starts from a clean slate — new registration, badge at zero.
The desktop itself shows no OS notifications. Its job is deciding to send them; the phone is where they land. If you are at the desktop, the conversation is already in front of you.
The Switches
Two settings, both living in one place on each device — and the master switch is the same setting on both, not two that can disagree:Underneath,
phone is an ordinary capability — switching it off, whether you do it or the agent does, removes notify_phone the same way. The Phone notifications switch is the intended control; the capability toggle is just the same lever further down.