A nudge is not durable state
The moment a client can be asleep, the backend needs a way to say something happened. cwkPippa grew one: a relay that mints its own provider token, speaks HTTP/2 to Apple's notification service, keeps a hot-reloaded token store on disk, and writes one log row per attempt.
There is no retry. That is deliberate, and it is the whole design in one decision. A push is a nudge; the record is the record. If the notification is lost, the assistant row is still in SQLite and still in JSONL, and the next time the app opens it is simply there. Retrying a nudge buys nothing and risks buzzing a phone twice about a thing that already finished.
What fires, and what deliberately does not
Three alert triggers, and the third line of each one is the interesting part.
A finished turn — one hook after the assistant row lands, in every brain's chat route, for the phone surfaces only. Note what the server does not do: it never tries to know whether the phone is still attached. Seven routes would each need to carry a detach flag, and the phone already knows what it is looking at — so the app suppresses its own banner when that conversation is open in the foreground. The knowledge lives where it already exists.
A finished scheduled job — but only the ones the heartbeat delivers itself, or ones that explicitly ask to push. Not every job that sends a message elsewhere. For most of those the brain decides whether anything was worth saying, and the scheduler cannot tell a quiet run from a loud one. A system that buzzes on every silent success teaches its owner to ignore it.
A failure — beside the existing backstop, never instead of it.
The dead-token protocol
Apple answers a send to an uninstalled app with a status that means this token is dead. The relay treats that as authoritative and drops every row carrying that token. This is the one piece of state a push system genuinely owns, and letting it rot turns a token store into a slow leak of sends to devices that no longer exist.
Live Activities, and a rule that bites
A Live Activity is the little live card on the lock screen. iOS lets an app start one only from the foreground — and the most valuable moment to start one is precisely when the app is not in the foreground, because the message came from the watch while the phone was in a pocket.
The way out is that the activity can also be started by push, from a token the app hands over in advance. So the app registers a start token; when it cannot start an activity itself it asks the backend to; the relay sends the start push carrying the app's own content state verbatim; the phone adopts the started activity and drives it locally while it is awake; and the finished turn ends it by push with the reply's first breath. A phone that slept through the whole turn still sees the answer land on its lock screen.
Two facts the bench taught that no documentation would have
Both were measured on Dad's own iPhone, and both are the kind of thing you only learn by sending real pushes to real hardware.
One device, two environments. A development build's ordinary alert token belongs to the sandbox host — but its Live Activity tokens were accepted only by the production host. Same device, same build, two different destinations. The relay now tries one host, retries the other once on a bad-token answer, and remembers which one worked for that device.
Registration order is not the order you would design. At launch the push-to-start token arrives before the ordinary alert token. So the alert registration has to merge into the existing device row rather than replace it — otherwise the start token is gone a second after it arrived, and the failure is invisible until the one moment it was needed.
The reason this lesson belongs in a quest about a chat system at all: a push layer is where a truth-first architecture is most tempted to compromise. The nudge is fast and the record is slow, and it is very easy to start treating the fast thing as the thing. Every decision above exists to keep the answer to what actually happened in exactly one place.