Skip to content

@ahpd/push ​

npmCIlicense MITnode >=22Agent Host Protocol 0.9.0

Push notifications for the @ahpd/server daemon. A phone registers its Expo push token once, and hears about a session that needs a person while its app is closed.

It is two halves. A push: resource provider keeps the registered devices, and the plugin watches the host's own moments and sends to the devices whose client created or opened the session that started waiting or finished a turn.

Part of ahpd. The source is in packages/push.

Install ​

bash
ahpd plugin install @ahpd/push
ahpd restart

Update, remove and one-run flags such as --plugin are in Plugins.

Options ​

In the configuration file, with every option set:

json
{
  "plugins": [
    "@ahpd/agent-claude",
    {
      "name": "@ahpd/push",
      "options": {
        "title": "Build box",
        "accessToken": { "$secret": "host:expo" }
      }
    }
  ]
}

No option is required. A daemon that names none still pushes, titled with its own name and without an access token.

OptionDefaultWhat it does
titlethe daemon's nameWhat a notification is titled. A blank one falls back to the daemon's name
accessTokennoneAn Expo access token, where the project has push security on. Given here as a { "$secret": "<name>" }, the vault is read at each send rather than at load

accessToken is writeOnly, so reading the options back answers <set> rather than the token, and secretAtUse, so the name in { "$secret": ... } reaches the plugin unresolved and is read with host.secret when a send happens. A name the vault does not hold fails that send, with one line in the log and nothing else.

The push: scheme ​

A device is addressed by its install id, which is the URI path:

CommandWhat it does
resourceWrite push://devices/<id>Registers a device, replacing what the id held
resourceRead push://devices/<id>The record, as JSON, without its token
resourceResolve push://devices/<id>Its metadata. A device that is not there resolves to the shape of one, so a client can draw the form before it writes
resourceList push://devicesThe registered device ids
resourceDelete push://devices/<id>Unregisters. A device that is not there is refused -32008

The body is JSON with a non-empty token and a platform of ios or android; lang is optional and kept with the device. Anything else is refused -32602. A write with createOnly against an id that is already there is refused -32010, and a URI of another scheme is refused -32602 rather than read as a path.

push:// and push://devices are directories, and nothing below push://devices/<id> names anything.

A registration is gated like any other write, so it needs file:write: a guest with read only cannot register. A client finds out whether a host can push from _meta.ahpd.resourceProviders, the same way it finds computer.

What a notification says ​

TitleThe title option, or the daemon's name
BodyA session is waiting for your answer, or A session is waiting for your approval for toolConfirmation, or A session finished when the session's own chat completes a turn
Data{ uri: <session>, kind }, which only the app reads. kind is the request's kind, or turnComplete

A notification never repeats what the session is asking. The text passes through Expo's service and sits on a lock screen, and neither is where a prompt belongs; which session it is travels in data, which a client opens.

Which sessions a device hears ​

A device hears only about the sessions its own client created or opened, which is the rule the plugin is built around: a device is registered by a connection, the host names the client that created a session and each client that subscribed to one, and a device written by a connection that never introduced itself is kept and sent nothing.

Three more things keep a phone from being woken twice:

  • One message per (session, id) pair. The protocol's action is an upsert keyed by id, so the same entry set again sends nothing; the same id from another session is a different pair and sends.
  • A session that stops waiting sends nothing. There is no second notification that clears the first, so the pair is forgotten and a session that asks the same thing again is a new wait.
  • A turn that was stopped sends nothing, and neither does a turn of a worker's chat. A worker finishes its own turns inside one of the session's.
  • Nothing is sent for a request that is not a person being asked. toolClientExecution is a client running a call that already cleared its gate, so it is not one.

Where devices are kept ​

One file, push-devices.json, under the daemon's configuration directory, written whole and read when the daemon starts. It is written 0600, because a push token is a credential to that device's notifications.

A device Expo answers DeviceNotRegistered for is removed: the token has been retired, and a message sent there is wasted from then on. The receipts are read at the next send rather than on a timer, so a daemon nobody is waiting on reads nothing.

Documentation ​

PLUGINS.mdWriting and loading a plugin, and the events a plugin may watch
GET-STARTED.mdRunning the daemon

License ​

MIT.