broker: tap/untap = receive a copy of every routed message
ci / check (push) Successful in 12m13s

Tapped copies {type:tapped, message:{id,from,to,reply_to,body}, ts} are
delivered directly (never tapped themselves), dropped when the tapper's
mailbox is full, and the tap is saved in actor.json like subscriptions.
This commit is contained in:
Sausage Agent
2026-10-16 11:14:09 +00:00
parent 2ff30ecf70
commit 49453fa9af
4 changed files with 182 additions and 17 deletions
+21 -2
View File
@@ -68,7 +68,7 @@ Each instance sees three folders:
| `/actor` | read-only | the `actor/` folder of the world |
`/actor/<id>/` holds everything about an actor: `actor.wasm`, `actor.json`
(installed version, running, subscriptions), `source/`, `public/`, `data/`,
(installed version, running, subscriptions, tapped), `source/`, `public/`, `data/`,
`stderr.log`, and `versions/<n>/`. `ls /actor` lists all actors.
Files are plain POSIX files: a write is visible at once. To publish a file
@@ -110,7 +110,7 @@ Any request can be answered with `{"type": "fs_error", "errno": "no-entry"}`.
Requests to `broker`. The answer is a message whose `reply_to` is the ID of
the request: the request's `type` in past tense (`spawned`, `upgraded`,
`rolled_back`, `stopped`, `started`, `subscribed`, `unsubscribed`,
`scanned`, `exiting`), or `{"type": "error", "reason": "..."}`.
`tapping`, `untapped`, `scanned`, `exiting`), or `{"type": "error", "reason": "..."}`.
| `type` | Fields | Effect |
| --- | --- | --- |
@@ -119,11 +119,30 @@ the request: the request's `type` in past tense (`spawned`, `upgraded`,
| `rollback` | `actor`, `version` | go back to an older version |
| `stop`, `start` | `actor` | stop or start an actor |
| `subscribe`, `unsubscribe` | `actor` | receive (or not) what `actor` publishes |
| `tap`, `untap` | | receive (or not) a copy of every message the broker routes |
| `scan` | | boot new or changed `actor.wasm` files |
| `exit` | | answer `exiting`, stop every actor, and end the `sausage` process (a wrapper like `nix run .#loop` starts it again, updated) |
Paths are `/actor/...`, or the sender's `/public/...` or `/data/...`.
**Taps.** After `tap`, the sender gets a copy of every message that the
broker routes (direct messages, replies, and publishes, between any actors,
including its own), for debugging, logging, or a live view of the world:
```json
{"type": "tapped", "message": {"id": "m-17", "from": "console", "to": "agent", "reply_to": null, "body": {...}}, "ts": 1792132579067}
```
- `to` is `null` for a publish: one copy per publish, not per subscriber.
- `ts` is the time of routing, in milliseconds since the Unix epoch.
- Copies come from `broker`. They are not routed themselves, so they are
never tapped, and each message is tapped once per tapper.
- A copy that does not fit into the tapper's mailbox is dropped; routing
never waits for a tapper, and nobody gets `undeliverable` for a copy.
- Never answer a copy: the answer would be tapped again, forever.
- Like subscriptions, a tap is saved in `actor.json` (`"tapped": true`) and
survives restarts and upgrades. `untap` ends it.
**Versions.** A new version (from `spawn`, `upgrade`, or a changed
`actor.wasm` on disk) is tested first, while the old version keeps running.
Only when its tests pass does the broker copy it to `versions/<n>/`