# Notification router The swarm's `swarm-notify-router` service turns forge events into todos on the agents they concern. No hive is in the path, and the agent can be on any hive: swarm-controller keeps the events, the router decides, and each agent's harness takes its own todos off the queue. Where it runs and how it's enabled: [`services.md`](services.md#notification-router). It runs beside `hive-forge-notify`, which still delivers forge notifications the way it did. An agent can get the same news from both. ## What an agent gets | Flow | Event | The agent's todo | | ------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------- | | `assigned` | someone assigns an agent to an issue or PR | `you are assigned to issue #: (<url>)`, key `forge:<repo>#<n>:assignment` | | `unassigned` | someone takes an agent off an issue or PR | the same key rewritten: `you are not assigned to issue <repo>#<n> anymore: <title> (<url>)` | | `closed` | someone closes or merges the issue or PR | the router clears every router todo it queued for it, on every agent that holds one | The todos are subsystem `router` in the agent's todo list. A todo under the same key replaces the one before, so an issue gives an agent one assignment todo however often it changes hands. A new event reopens a todo the agent marked done. Only agents get todos. An assignee with no agent of that name gets nothing. An agent assigned before it first reports its status gets its todo on the issue's next assign event. Forgejo's assign events carry the assignees after the change, not which one changed. The router keeps the assignees it saw on the last assign event per issue and takes the difference. With no earlier assign event for an issue, every current assignee counts as newly assigned and nobody as removed: an router notices an unassign only on an issue it has seen an assign event for. ## The path 1. swarm-controller registers two instance-wide hooks, one for the `issues` and `issue_assign` events at `/webhook/forge/issues` and one for `pull_request` and `pull_request_assign` at `/webhook/forge/pull-request`. At each start it adds any of these events a registered hook lacks. 2. On each delivery the controller checks the HMAC and publishes the body, unchanged, to the `SWARM_EVENTS` stream on `$SWARM.events.forge.<X-Forgejo-Event-Type>`, such as `$SWARM.events.forge.issue_assign`. The stream keeps a delivery 7 days and drops a redelivery by its `X-Forgejo-Delivery` id. A delivery the stream did not keep answers 503, so the forge's delivery log shows it. 3. The router drains its durable consumer, `notify-router`, in order. For each event it runs every flow, queues the todo ops the flows emit, writes the issue's snapshot to its `notify-router-state` bucket, then acks the event. A failed step stops the batch: that event and the rest come back in stream order after 30 seconds, and the stream drops the ops queued twice by their message id. 4. Each op goes to the `ROUTER_TODOS` stream on the agent's own subject, `$SWARM.router.todo.<agent>`. Work-queue retention: an op stays until its agent acks it, up to 30 days and 1000 per agent. 5. The agent's harness creates its own durable consumer, `todo-<agent>`, applies each op to its todo list and acks it. An agent that's down, or whose harness predates this, leaves its ops waiting. The router holds no forge token and no webhook secret, and registers no hook. ## Flows A flow is a graph of nodes joined by edges: one `source` node naming the forge event types it starts from, `match` nodes that pass when every condition holds, and `sink` nodes that emit one todo op per recipient. A match node can have more than one child, and each one runs, so one event can reach several sinks. A flow must be a tree: one source, one parent for every other node, every node reachable, no children under a sink. The router ships the three flows above, each a source → match on `action` → todo sink. ## Credentials and grants | Principal | Grant | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `swarm-notify-router`, in its container | store: `read` on `swarm/services/<router client id>/oidc/client`. Queue: its own consumer on `SWARM_EVENTS` (create, pull, ack), publish on `$SWARM.router.todo.*`, create `ROUTER_TODOS`, its own `notify-router-state` bucket, and `STREAM.INFO` on the agent-status bucket, which lists the agents | | swarm-controller | publish on `$SWARM.events.forge.*` | | an agent, under its own credential | create its own consumer `todo-<agent>` in the form whose filter is `$SWARM.router.todo.<agent>`, pull from it, and ack what it delivered | No other client may publish on `$SWARM.router.todo.*` or `$SWARM.events.forge.*`, so each has one writer. The agent's create subject carries the consumer's filter, and the queue server checks it against the request, so an agent can't create a consumer that delivers another agent's todos.