Deliver notifications on the tool result, not by polling
Why the hub carries what needs an agent's attention on the next tool result it makes, and offers standing subscriptions, instead of a push channel or a per-harness poll.
Context
Agents have no push channel. The hub exposes a freshness stream
(GET /api/v1/stream, ADR 0016) for the
human's PWA, but an agent over MCP has only request and response. Until now the
only way an agent learned that its question was answered or its approval
decided was to call inbox_read or inbox_wait, and the only way it learned
of a feed event was feed_read with its own cursor.
That put the notification logic in each harness. A client that wanted to watch artifact comments wrote its own poller with its own seen-file, and every client would reinvent the same timer, cursor, and de-duplication differently. The notification mechanism belongs in the hub, which already holds the events, the threads, and the freshness signal.
Decision
Every successful MCP tool result may carry a top-level notifications member
whose pending list holds what needs the caller's attention. It is the
delivery channel, and it has two sources:
- Attention. An answer to a question the caller posted, or a decision on an approval it posted, that it has not been shown. This is on by default: an agent that never asks is still told when its own question is answered.
- Subscriptions.
notify_subscriberegisters a standing interest in feed events by kind, optionally scoped to one project, andnotify_unsubscriberemoves it. A subscription is thefeed_reada client would otherwise poll for, kept on the hub's side.
Each item is {source, kind, id, project_id, title, at}. It is a nudge, not
the record: the answer body, the decision note, and the full event stay in the
inbox and the feed, read by inbox_read, inbox_wait, or feed_read.
Each item is delivered once. Two server-side cursors, keyed on the resolved
actor and never the token, record what has been shown: one for the attention
queue, one per subscription. A delivered read advances its cursor; an empty
read moves nothing. The member is absent when there is nothing to report and on
an error result, so a caller that never has anything pending pays one indexed
read and nothing more. notify_subscribe seeds its cursor at the newest
matching event, so a subscription reports from the moment it was made rather
than replaying history, and an unscoped drain keeps to the projects the caller
may read at drain time, so an event in a project it may not read is not
consumed and arrives if a grant is later given.
The member rides on the result of a call the agent was making anyway. It is not
a push: nothing is delivered between calls, and an agent that stops calling is
not notified. inbox_wait remains for an agent that is idle and wants to block
until something lands.
Consequences
- The hub owns notification, so a harness needs no timer, no cursor, and no seen-file. Wiring a harness to the hub is one MCP connection.
- The trailer is a nudge and at most once. An agent that ignores it does not
lose the record:
inbox_readandfeed_readkeep their own cursors and return everything. This is why the item carries an id and a title rather than the body. - A tool result now varies with the caller's pending state, so a test that asserts an exact result shape must account for the member. It is absent when there is nothing pending, which is the common case.
- Two migrations (21 and 22) add the cursors and the subscriptions, both keyed on the resolved actor, so a rotated token keeps its read position.