@joe-broadhead/messaging 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/dist/bin/messaging-mcp.js +202 -31
  3. package/dist/bin/messaging-relay.js +291 -43
  4. package/dist/bin/messaging.js +290 -32
  5. package/dist/bin/messagingd.js +1158 -138
  6. package/dist/lib/modules/core/agent/present.js +5 -0
  7. package/dist/lib/modules/core/agent-tools.js +12 -0
  8. package/dist/lib/modules/core/consent.js +54 -14
  9. package/dist/lib/modules/core/contact-card.js +3 -3
  10. package/dist/lib/modules/core/index.js +1 -1
  11. package/dist/lib/modules/core/jelly-host.js +3 -0
  12. package/dist/lib/modules/core/schema/common.js +2 -0
  13. package/dist/lib/modules/core/schema/consent.js +20 -3
  14. package/dist/lib/modules/core/schema/conv-frames.js +10 -3
  15. package/dist/lib/modules/core/schema/directory-frames.js +8 -3
  16. package/dist/lib/modules/core/schema/errors.js +3 -2
  17. package/dist/lib/modules/core/schema/ipc/consent.js +85 -10
  18. package/dist/lib/modules/core/schema/ipc/conversation-views.js +8 -1
  19. package/dist/lib/modules/core/schema/ipc/conversations.js +5 -1
  20. package/dist/lib/modules/core/schema/ipc/device.js +7 -0
  21. package/dist/lib/modules/core/schema/ipc/jelly.js +14 -7
  22. package/dist/lib/modules/core/schema/ipc/shapes.js +60 -1
  23. package/dist/lib/modules/core/schema/ipc.js +51 -3
  24. package/dist/lib/modules/core/version.js +1 -1
  25. package/dist/spec/messaging-v1.md +321 -49
  26. package/dist/types/core/agent/present.d.ts +1 -1
  27. package/dist/types/core/agent-tools.d.ts +32 -0
  28. package/dist/types/core/consent.d.ts +30 -7
  29. package/dist/types/core/contact-card.d.ts +3 -3
  30. package/dist/types/core/conversation-agent-tools.d.ts +21 -0
  31. package/dist/types/core/index.d.ts +1 -1
  32. package/dist/types/core/schema/common.d.ts +2 -0
  33. package/dist/types/core/schema/consent.d.ts +23 -2
  34. package/dist/types/core/schema/conv-frames.d.ts +6 -2
  35. package/dist/types/core/schema/directory-frames.d.ts +8 -3
  36. package/dist/types/core/schema/envelope.d.ts +2 -0
  37. package/dist/types/core/schema/errors.d.ts +2 -0
  38. package/dist/types/core/schema/ipc/consent.d.ts +377 -15
  39. package/dist/types/core/schema/ipc/conversation-views.d.ts +176 -0
  40. package/dist/types/core/schema/ipc/conversations.d.ts +368 -0
  41. package/dist/types/core/schema/ipc/device.d.ts +6 -0
  42. package/dist/types/core/schema/ipc/jelly.d.ts +14 -6
  43. package/dist/types/core/schema/ipc/shapes.d.ts +198 -0
  44. package/dist/types/core/schema/ipc/surfaces.d.ts +1 -0
  45. package/dist/types/core/schema/ipc/tasks.d.ts +2 -0
  46. package/dist/types/core/schema/ipc.d.ts +826 -3
  47. package/dist/types/core/schema/relay.d.ts +4 -0
  48. package/dist/types/core/version.d.ts +1 -1
  49. package/dist/types/ipc/server.d.ts +6 -0
  50. package/package.json +1 -1
@@ -485,6 +485,14 @@ An envelope with an unknown `type`, or a newer `v`, is acknowledged to the relay
485
485
  update without `replyTo` fails the shape check of §5 (`invalid_request`). The requester's daemon
486
486
  drops an update whose `replyTo` is not its record of the request (`invalid_request`).
487
487
  - The requester's daemon applies them to its `TaskRecord` and publishes a `task` event.
488
+ - **Sign-in.** An executor whose task waits on a sign-in to a tool says which one with `task.update {state:
489
+ TASK_STATE_AUTH_REQUIRED, signIn: {tool}}` (`signIn` with any other state is invalid params). Its daemon puts the
490
+ tool in the status message as A2A v1 extension data: `metadata["urn:messaging:ext:sign-in:v1"] = {tool}`, with that
491
+ URI in the message's `extensions`. The message's parts are the session's `message`, or the fixed text "Sign-in
492
+ required." when it gave none, so the tool name is never copied into text. Extension metadata is how A2A v1 adds
493
+ fields to a `Message` without changing its parts, and a peer that does not know the extension still reads a valid
494
+ status. The requester's daemon stores the status as received and reads the tool only when it projects the task
495
+ (§8.2, "The task view"); nothing else in the extension is read, and whose sign-in it is never comes from the peer.
488
496
  - **What the relay sees.** It can link a task request to its updates, and count and time them. It
489
497
  learns no content and no state: the state is sealed. This is the correlation a relay needs to
490
498
  match a wake ticket to awaited answers.
@@ -541,8 +549,9 @@ Only the executor changes state (see `canTransition` in core).
541
549
  still run, and the person is away from that device. The host says so with `person.attached`; a Jelly host's device is
542
550
  away until its first `person.attached {attached: true}`, and no other device ever says `away`. A receiver shows that
543
551
  device as away (`people.get`), and an `away` older than the online window (§15.9: 150 s) means nothing.
544
- - `features` advertises optional capabilities. v1 defines `"tasks"` and `"conversations"` (the daemon reads conversation
545
- logs; `conv.caps` reads it at the relay). A receiver ignores a feature it does not know.
552
+ - `features` advertises optional capabilities. v1 defines `"tasks"`, `"conversations"` (the daemon reads conversation
553
+ logs; `conv.caps` reads it at the relay) and `"reach"` (the daemon enforces the person's reach choices, §6.9; the
554
+ person's own devices show it in `device.list`, §8.2). A receiver ignores a feature it does not know.
546
555
  - `label` must equal the sender's roster label. Receivers ignore a presence whose label differs, and
547
556
  always show the roster's label.
548
557
  - Like every envelope, presence carries the sender's `anchor` in its header (§5). Presence is
@@ -680,7 +689,7 @@ A roster realm has only `self` and `org`.
680
689
 
681
690
  ```ts
682
691
  { v, realm, id: Ulid, person, by: MachineId, ts,
683
- subject: { person } | { conv: ConvId },
692
+ subject: { person } | { conv: ConvId } | { relation: "outside" | "org" },
684
693
  chat?: "open" | "muted" | "blocked", // lines from the subject (person subjects only)
685
694
  agentMail?: "open" | "blocked", // lines from the subject's agents
686
695
  ask?: "open" | "request" | "no", // mail and tasks from the subject or their agents to the person's agents
@@ -693,6 +702,8 @@ A roster realm has only `self` and `org`.
693
702
  - `sig` = Ed25519(`by`, `"messaging/1/grant\n"` ‖ JCS(grant without `sig`)). `by` is a device of `person`.
694
703
  - `person` and a person subject are `PersonId`s: a roster person name, or a hosted realm's `p_…` id (§15.1).
695
704
  - A conversation grant sets only `ask` and `agentMail`. Both persons must be active participants in the locally verified conversation.
705
+ - A relation grant records a reach choice ("Reach choices", below): the `outside` grant sets only `chat`, `open` or
706
+ `blocked`; the `org` grant sets only `ask`.
696
707
  For an event-specific decision, membership is evaluated at that event's position.
697
708
  - `trustedUntil` needs `ask: "open"` and is at most `ts + LIMITS.trustMaxMs` (90 days). Trust is set
698
709
  for `LIMITS.trustDefaultMs` (30 days) when the person names no end. An expired `trustedUntil` reads
@@ -729,7 +740,8 @@ the person:
729
740
 
730
741
  1. the person grant for S, where it sets the level;
731
742
  2. otherwise, for `ask` and `agentMail`, the most permissive grant of a verified conversation shared with S;
732
- 3. otherwise the default for the relation:
743
+ 3. otherwise the person's reach choice for S's relation, where they made one ("Reach choices");
744
+ 4. otherwise the default for the relation:
733
745
 
734
746
  | Relation | `chat` | `agentMail` | `ask` | trust |
735
747
  |---|---|---|---|---|
@@ -738,8 +750,70 @@ the person:
738
750
  | `contact` | open | blocked | no | off |
739
751
  | `stranger` | `request_required` | blocked | no | off |
740
752
 
741
- Only the person grant trusts. A `blocked` chat level also refuses agent mail, asks and channel changes
742
- from S.
753
+ Only the person grant trusts. A `blocked` chat level from the person grant also refuses agent mail, asks and
754
+ channel changes from S.
755
+
756
+ **Reach choices.** A person answers two questions, each Open, Ask me first or Never (owner, 2026-11-10). Ask me
757
+ first is the default, and it is what the relation defaults already do. The person's devices record each answer as
758
+ a grant on a relation subject, so it is signed, merged, re-signed, lapses and syncs like any other grant:
759
+
760
+ | Question | Subject | Open | Ask me first | Never |
761
+ |---|---|---|---|---|
762
+ | "Who can message you" | `{relation: "outside"}` | `chat: "open"` | a grant with no level | `chat: "blocked"` |
763
+ | "How agents may ask yours" | `{relation: "org"}` | `ask: "open"` | `ask: "request"` | `ask: "no"` |
764
+
765
+ No grant for a subject means no choice: the defaults apply, the same as Ask me first. The choices set step 3 of
766
+ "Effective policy", under every person and conversation grant:
767
+
768
+ | S | `chat` under Open | under Ask me first or no choice | under Never |
769
+ |---|---|---|---|
770
+ | `org` | open | open | open |
771
+ | `contact` | open | open | blocked |
772
+ | `stranger` | open | `request_required` | blocked |
773
+
774
+ | S | `ask` under Open | under Ask me first or no choice | under Never |
775
+ |---|---|---|---|
776
+ | `org` | open (`guarded`) | request | no |
777
+ | `contact`, `stranger` | no | no | no |
778
+
779
+ - **"Who can message you"** is about the people outside the person's org. The org stays `open`. A `contact` has
780
+ already been let in (the person accepted their request, sent them one, or granted them something), so Ask me
781
+ first leaves them `open`, as it did before the choice existed; Never blocks one whose person grant sets no `chat`.
782
+ So no answer opens anything the person had closed, and the default changes nothing.
783
+ - **"How agents may ask yours"** is about the agents of the person's org. Agents of people outside the org stay
784
+ `no`: only a person or conversation grant lets them ask.
785
+ - **A grant set for one person or conversation wins.** A person grant's `chat` (an accepted request sets `open`)
786
+ and its `ask`, and a conversation grant's `ask` and `agentMail`, all come before the choice. So a conversation
787
+ grant with `ask: "open"` still lets the agents of that conversation's participants ask under Never: an explicit
788
+ grant for one person or conversation wins over the account-wide choice (coordinator, 2026-11-10, under the owner's
789
+ rule).
790
+ - A `blocked` chat level that comes from Never refuses S's new lines, message requests and channel changes as they
791
+ arrive. For conversations that already exist, Never hides S's lines and channel-change notices from the person's
792
+ view; it can't undo a change S makes to a shared conversation. It does not refuse agent mail or asks: they follow
793
+ their own levels, so a grant that opened them still counts.
794
+ - **In conversations that already exist** the same rule applies on the person's own view: the daemon's reads of a
795
+ conversation (`conv.read`, `conv.sync`, `conv.search`, `conv.context` and the stream) leave out the lines and
796
+ channel-change notices of S while S's effective `chat` is `blocked`, whether a person grant or Never set it. Never
797
+ leaves out only an S the realm's verified profile shows in another org (`org: "other"`); the org is never left out,
798
+ and neither is a person the realm no longer knows (a departed teammate), history included (owner, 2026-11-10). A
799
+ person grant's block still leaves out anyone. An agent of S is left out only by a person grant's block (agent
800
+ mail follows its own level). A channel change S makes is still applied to the conversation's state; only its
801
+ notice is left out of the person's view.
802
+ Like a person-grant block, this filters what the person sees and is undone when the choice changes. The
803
+ conversation's log, and the other participants' views of it, are unchanged.
804
+ - **Signers.** The rule of "Signers" applies as written: `chat: "blocked"` and `ask: "no"` are kept when their
805
+ signer is removed; `chat: "open"`, `ask: "open"` and `ask: "request"` lapse into the review list, and the choice
806
+ goes back to none. A grant with no level lapses whole.
807
+ - **Cards.** A card computes its levels with the choices (`cardCan`). A change of a choice sends no cards: a card
808
+ is advice, and S learns on its next refusal.
809
+ - **Store.** The daemon keeps relation grants and their review entries in tables of their own (§8.8, schema 5),
810
+ not in `grants`: a daemon from before schema 5 drops a row of `grants` that does not verify for it, so a host
811
+ that rolled the binary back would lose the person's choices.
812
+ - **Older daemons do not enforce the choices.** A daemon from before schema 5 keeps the relation grants it is sent
813
+ out of its view, so it applies the relation defaults: a stranger may send it a message request again, and a
814
+ teammate's ask is held there, not refused. Envelopes go to every device of the person, so after a binary rollback,
815
+ or while any of the person's devices runs an older release, a choice that narrows reach (Never) is not enforced on
816
+ that device. A client should warn while any of the person's devices reports a store schema below 5.
743
817
 
744
818
  **Contact cards.** A person's card tells another person what they may send the person. The schema is
745
819
  `ContactCard` in `schema/consent.ts`:
@@ -768,11 +842,11 @@ from S.
768
842
  | Anything from the receiver's own person | Deliver, `self` |
769
843
  | An answer to the person's own traffic: a receipt, `task.status`, `task.artifact`, `task.cancel`, or a message the counterpart binding (§5) ties to earlier traffic | Deliver, whatever the grants, so a grant change never strands a task the person started. `trusted` while the person grant trusts the sender, else `guarded` |
770
844
  | Anything else, while `chat` is `blocked` | Refuse, `refused` |
771
- | A DM line or channel post by the person S | `chat`: `open` delivers, `muted` delivers silently, `request_required` refuses with that code |
772
- | A line or channel post by an agent of S | `agentMail`: `open` delivers; otherwise refuse, `refused` (`request_required` for a `stranger` when no grant set it) |
773
- | A channel change | Deliver; a `stranger` is refused with `request_required` |
774
- | Mail or a task request to one of the person's agents (an ask) | `ask`: `open` delivers `guarded`, or `trusted` (below); `request` holds it for the person; `no` refuses, `request_required` when it is the default of a `contact` or a `stranger`, else `refused` |
775
- | A message request (class `request`, "Message requests") | From a `stranger`: hold it for the person. From anyone else: as a DM line by the person S |
845
+ | A DM line or channel post by the person S | `chat`: `open` delivers, `muted` delivers silently, `blocked` (from Never) refuses with `refused`, `request_required` refuses with that code |
846
+ | A line or channel post by an agent of S | `agentMail`: `open` delivers; otherwise refuse, `refused` (`request_required` for a `stranger` when no grant set it and the person did not choose Never) |
847
+ | A channel change | Deliver; refuse while `chat` is `blocked` (from Never), `refused`; a `stranger` is refused with `request_required` |
848
+ | Mail or a task request to one of the person's agents (an ask) | `ask`: `open` delivers `guarded`, or `trusted` (below); `request` holds it for the person; `no` refuses, `request_required` when it is the default of a `contact` or a `stranger` and the person did not choose Never for messages, else `refused` |
849
+ | A message request (class `request`, "Message requests") | While `chat` is `request_required` (a `stranger` under Ask me first): hold it for the person. While `chat` is `blocked` (from Never): refuse, `refused`. Otherwise: as a DM line by the person S |
776
850
 
777
851
  - **Trust and the chain rule.** An ask runs `trusted` only while the person grant's `trustedUntil` is
778
852
  past now and every person in its delegation chain (§6.7) is S or the receiver. Otherwise it runs
@@ -1176,7 +1250,7 @@ hosted `hello` as a malformed one (4001).
1176
1250
  | `dir.watch {persons}`, at most `LIMITS.watchPersons` (500) | `dir.ok {}` |
1177
1251
  | `presence.watch {persons}`, at most `LIMITS.watchPersons` | `dir.ok {}` |
1178
1252
  | `dir.erase {entries, token}`: an `unbind` of each current device, at most `LIMITS.devicesPerPerson` (10), and a compact JWS | `dir.ok {}` |
1179
- | `wake.arm {url, ticket, agents, awaiting}`: a Jelly host's wake ticket ("Wake tickets" below) | `dir.ok {}` |
1253
+ | `wake.arm {url, ticket, agents, awaiting, dms?}`: a Jelly host's wake ticket ("Wake tickets" below) | `dir.ok {}` |
1180
1254
  | `wake.disarm {}` | `dir.ok {}` |
1181
1255
 
1182
1256
  - **`dir.sthIs`.** The STH at `size`, or the latest. `head` is its last epoch-log leaf (index `size − 1`) with
@@ -1212,7 +1286,7 @@ hosted `hello` as a malformed one (4001).
1212
1286
  daemon arms it at its relay (`wake.arm`, §8.11). While that device is offline, the relay may present the ticket to
1213
1287
  Jelly ID, which wakes the host.
1214
1288
 
1215
- - **`wake.arm {url, ticket, agents, awaiting}`**, after `hello` (`forbidden` before it), from a current device:
1289
+ - **`wake.arm {url, ticket, agents, awaiting, dms?}`**, after `hello` (`forbidden` before it), from a current device:
1216
1290
  - `url` is Jelly ID's wake endpoint of this device's host, `https://<issuer host>/hosts/<hst>/wake` (`WakeUrl`): its
1217
1291
  origin must be the realm's Jelly ID issuer's (`invalid_request`), and `<hst>` the `hst` of the Jelly token of this
1218
1292
  device's latest `bind` or `attest` that names one (`forbidden` otherwise, and for a device no host bound);
@@ -1220,8 +1294,10 @@ Jelly ID, which wakes the host.
1220
1294
  unverified (`readWakeTicket`), only the header's `typ` and `alg` (`jelly-wake+jwt`, `EdDSA`), and the claims `exp`
1221
1295
  (it must be in the future and at most seven days and 60 s ahead), `aud` (when present, exactly `url`) and `sub`
1222
1296
  (when present, exactly `<hst>`); any other is `invalid_request`;
1223
- - `agents` (at most 64 session names) are the device's sessions whose own person's mail may wake the host, and
1224
- `awaiting` (at most 500 envelope ids) the requests whose task updates may wake it.
1297
+ - `agents` (at most 64 session names) are the device's sessions whose own person's mail may wake the host,
1298
+ `awaiting` (at most 500 envelope ids) the requests whose task updates may wake it, and `dms` (at most `WAKE_DMS`
1299
+ (256) conversation ids; absent is none) the dm conversations in which another person's events may wake it. The
1300
+ host's daemon chooses them (§8.11, "Wake tickets"); the relay checks only their form and the cap.
1225
1301
 
1226
1302
  It replaces the device's earlier ticket, and is kept in the relay's store (`dir_wakes`, §8.8) until it is
1227
1303
  used, disarmed, its device is unbound or erased, or it expires. At most 60 arms a device in a burst, then one a
@@ -1235,12 +1311,20 @@ Jelly ID, which wakes the host.
1235
1311
  other end is one of them or a mention of one, and never names another person's session. A `wake` hint on another
1236
1312
  person's event reaches only that person's own devices, so it never wakes anyone else's host. A repeated append
1237
1313
  (`conv.ok` again for the same event) does not present again. The reason logged is `own`;
1314
+ - **an event in an armed dm by its other person** (owner, 2026-11-10, opencode-messaging #19). The relay has just
1315
+ stored a `msg` event in a conversation whose kind is `dm` (its plaintext `create` op) and whose `cid` is in the
1316
+ device's `dms`, and the authenticated author's person is not the device's person. Only the current devices of
1317
+ the conversation's participant persons other than the author's are considered. The relay cannot tell a line
1318
+ from another sealed event (a reaction, an edit, a receipt), so any such `msg` may wake; the budget below bounds
1319
+ it. Ops, the person's own events (their own lines wake only by the hint above) and a dm the device did not arm
1320
+ never wake by this rule. Arming learns the relay nothing new: dm membership is plaintext to it. A repeated
1321
+ append does not present again. The reason logged is `dm`, without the conversation;
1238
1322
  - **an envelope** the relay has just taken (§7 "Routing": first seen, queued) for the device that is:
1239
1323
  - the person's own mail to an armed agent: header type `message`, `from.person` = `to.person`, and `to.agent` in
1240
1324
  `agents` (a task request, which keeps its envelope); or
1241
1325
  - an answer the device awaits: header type `task.status` or `task.artifact` whose `replyTo` is in `awaiting`.
1242
1326
 
1243
- Conversation events without a `wake` hint (posts, receipts, another person's lines), presence, task cancels, anyone
1327
+ Conversation events without a `wake` hint (posts, receipts, another person's lines) outside an armed dm, presence, task cancels, anyone
1244
1328
  else's mail, a message that merely names an awaited request, and anything for a connected device never wake. A request the recipient's person
1245
1329
  must answer (§6.9) is held by the receiving daemon, which the relay cannot see: such mail never wakes unless it is
1246
1330
  the person's own.
@@ -1254,8 +1338,8 @@ Jelly ID, which wakes the host.
1254
1338
  - `503`, any other 5xx, or no answer: kept, and presented again a minute later if the device is still offline. That
1255
1339
  retry has waited out the gap on its own timer and is not held back again by the one-a-minute rule, however the
1256
1340
  relay's clock and its timers drift; new traffic within the minute is.
1257
- - **Never logged.** The ticket is in no log line; a presentation is logged with the device, why (`own`, `awaited`,
1258
- `retry`) and the status.
1341
+ - **Never logged.** The ticket is in no log line, nor are the armed `dms`; a presentation is logged with the
1342
+ device, why (`own`, `dm`, `awaited`, `retry`) and the status.
1259
1343
  - **What a relay can and cannot do with a ticket.** A relay that keeps or leaks a ticket can, at most, wake that one
1260
1344
  host once, before the ticket's `exp`: Jelly ID spends its `jti` on the wake, checks that it is that host's (its
1261
1345
  signature by the host's enrolled key, `iss`, `sub` and `aud`), and holds every host to its budget (6 an hour, 48 a
@@ -1263,7 +1347,8 @@ Jelly ID, which wakes the host.
1263
1347
  waking a host gives the presenter nothing on it: the host's daemon then reconnects and fetches its mail as usual.
1264
1348
  Nor can a relay mint or widen one: it cannot sign for the host's key, and an `aud` names one host's URL. The relay
1265
1349
  learns that the host sleeps (it already sees the device go offline), which agents may wake it (presence already
1266
- names them), and the ids of the task requests it awaits (envelope ids it already routed). A malicious relay can also
1350
+ names them), the ids of the task requests it awaits (envelope ids it already routed), and which of its dms may
1351
+ wake it (the relay already holds every dm's membership). A malicious relay can also
1267
1352
  fail to wake a host, as it can drop any envelope; that delays the person's mail, it loses none.
1268
1353
 
1269
1354
  **Conversation frames** (`schema/conv-frames.ts`; §16). After `hello`, in a roster realm and in a directory realm
@@ -1308,13 +1393,20 @@ members verify every entry (§16.4, §16.7). Every frame is strict; a refusal is
1308
1393
  of the named conversations this device's person takes part in, or, without `cids`, of all of them by `cid` after
1309
1394
  `after`, at most `LIMITS.convHeads` (500) each way. `shares` is the last share `n` for this device (0: none). A
1310
1395
  device that comes back online asks this first, then fetches what moved.
1311
- - **`conv.caps {req, persons}`** → `conv.capabilities {req, outdated}`: of at most `LIMITS.convOpParticipants` named
1396
+ - **`conv.caps {req, persons}`** → `conv.capabilities {req, outdated, unknown?}`: of at most `LIMITS.convOpParticipants` named
1312
1397
  persons, those every one of whose current devices with a stored, unexpired presence names no `conversations`
1313
1398
  feature: a daemon from before conversations, which reads no log. A person with no such presence is not named, since
1314
1399
  no presence is no evidence. It is a hint from the relay and can only make a sender refuse: before it creates a dm,
1315
1400
  group or channel with that person, adds them, or sends them a message request, a daemon asks, and refuses with
1316
1401
  `peer_outdated` ("<handle> needs to update Messaging"), creating and sending nothing. It reveals one bit about a
1317
- person of the realm, whom the asker can already name.
1402
+ person of the realm, whom the asker can already name. So that it is no presence oracle, a person the asker does not
1403
+ already relate to (the presence audience of §15.10: the same person, the same org, envelopes both ways in 30
1404
+ days, or accepted members of one conversation; a roster realm is one org) counts against a budget of
1405
+ `LIMITS.requestsPerDay` distinct persons per asking person (not per device) per UTC day, the message-request
1406
+ budget. Related persons, and persons already asked about that day, are always answered. A new unrelated person past
1407
+ the budget is not answered: the reply names them in `unknown` (present only when not empty), and the daemon goes
1408
+ ahead for them as without an answer. Every other person in the same frame is still answered, so a daemon refuses
1409
+ with `peer_outdated` for any person the reply names in `outdated`, whatever else the frame holds.
1318
1410
  - **Retention.** Once a minute the relay prunes the bodies of `msg` entries older than the conversation's
1319
1411
  `retainDays` (as of its head), by the time it sequenced them, keeping each entry's `seq`, `prev` and digest
1320
1412
  (§16.4), and deletes the key shares older than it. It never prunes an op.
@@ -1399,11 +1491,41 @@ chooses to share (§6.5, none by default). It never sees message, task or receip
1399
1491
  Names and content are untrusted peer data. A `FileView` exposes no attachment key or nonce.
1400
1492
  A deleted `EventView` contains neither text nor files. Lane 4 omits read positions, preferences and unread counts.
1401
1493
  Stream records carry a daemon-local `pos`. An `event.update` names the conversation and event of its embedded projection.
1402
- Stream record types are `event`, `event.update`, `conv`, `member`, `read`, `typing`, `activity` and `gap`.
1494
+ Stream record types are `event`, `event.update`, `conv`, `member`, `read`, `typing`, `activity`, `gap` and `task` (person-level readers only, "The person's tasks").
1403
1495
  An `event.update` change is one of `edit`, `delete`, `react`, `pin`, `replies`, `delivery` and `task`; lane 4 emits `edit`, `delete`, `react`, `pin` and `delivery`.
1404
- A `request.waiting` item's state is one of `held` and `pending-delivery`.
1496
+ A `request.waiting` item's state is one of `held`, `pending-delivery`, `resend` and `outdated`.
1405
1497
  The daemon re-reads verified state before it sends a projection.
1406
1498
 
1499
+ **Action tokens.** A human-tier client acts on an item it shows through an action the daemon put on that item:
1500
+ `{name, token}`, where `token` is an `ActionToken` (32 random bytes, base64url, 43 characters).
1501
+ - **Minting.** The daemon mints a token when it renders an item on `run/admin.sock` for a call that carries no session
1502
+ key. Under a Jelly host (§8.11) that call is under a method grant: a host-scope grant renders no token. The agent tier
1503
+ never receives one.
1504
+ - **Binding.** A token is for one action `name`, one target and one view. The target is the request (`request.list`)
1505
+ or the task (`TaskView`). The view is the admin connection that received the token, and under a Jelly host also the
1506
+ `dev` and `mbr` of the method grant. A missing `dev` or `mbr` is bound as missing.
1507
+ - **Life.** A token is spent by its first presentation, whether or not it matches. It lives 10 minutes. The daemon holds
1508
+ tokens in memory only, so a restart invalidates every one; the store does not change. A connection's tokens end
1509
+ when it closes.
1510
+ - **Limits.** A view that reads the same item again gets the same token while more than half of the token's life is
1511
+ left. A view holds at most 256 tokens and the daemon at most 4,096. Past either limit the oldest token ends.
1512
+ - **Redemption.** Only through the action's own method, with the token as the `token` param: a request's answer
1513
+ through `request.answer {id, answer, token}`, where `answer` is the action's `name`; a task's `cancel` through
1514
+ `task.cancel {taskId, token}`. The params name the target. Only `run/admin.sock` takes a token, for a call with no
1515
+ session key: elsewhere the call is `forbidden`, and that refusal does not spend the token. A token that is forged,
1516
+ spent, expired or ended, for another name or target, or from another view or an earlier start, is `grant_invalid`,
1517
+ and the call does nothing.
1518
+ - **Logging.** The daemon logs each refusal as `action.token.refused` with the method and a fixed reason. It never logs
1519
+ a token.
1520
+ - **Where tokens are offered.** `request.list` gives each request `actions`, one per answer its kind takes, while the
1521
+ request can be answered: an ask `once`, `always`, `decline` and `block`; a pending message request `accept`,
1522
+ `decline` and `block`; a suggestion `decline`; an agent invitation `accept` and `decline`. `TaskView.cancel` is
1523
+ offered only on a task that this person's agent asked (the requester side) and that has not ended. The executor side
1524
+ never gets `cancel`. No retry method exists for a failed send (the outbox retries by itself), so a failed send has
1525
+ no recovery action.
1526
+ - **Vectors.** `docs/spec/vectors/action-tokens.json` has requests with actions and the params that redeem them, with
1527
+ negative cases. `conversation-views.json` has a `TaskView` with `cancel`.
1528
+
1407
1529
  **Staged conversation methods.** These contracts have no live handler: `conv.catalogue`, `conv.publish`, `conv.markRead`, `conv.markUnread`, `conv.prefs`, `conv.follow`, `conv.save`, `conv.saved`, `conv.draft`, `conv.schedule`, `conv.scheduled`, `conv.unschedule`, `conv.typing`, `context.get`, `context.set`, `context.set.preview`, `conv.localRetention`, `file.get`, `session.activity`, `turn.cancel`.
1408
1530
 
1409
1531
  **Live conversation methods.** `LIVE_CONVERSATION_METHODS` is part of `IPC_METHODS`. The tier table applies on both sockets.
@@ -1415,7 +1537,10 @@ A page chooses at most one position. Agent calls require a session registered on
1415
1537
  The daemon enforces access, audience consent and own-line restrictions after asynchronous work too.
1416
1538
  Creation requires two DM participants or 3–50 group participants. Only channels have visibility. Only agents can be seated.
1417
1539
  `conv.access` pages accessible conversations. `conv.sync` waits at most 25 seconds and returns at most 50 events.
1540
+ A person-level `conv.sync` (admin socket, no key) also carries `task` entries for the person's tasks (§8.2, "The person's tasks").
1418
1541
  A restart or expired replay position produces a gap. Clients then reload their accessible conversations.
1542
+ Task changes share the replay buffer (its last 10,000 changes) with conversation changes, so a burst of task
1543
+ updates moves the oldest replayable position forward for every reader, keyed or not; a reader past it gets `gap`.
1419
1544
  `conv.backfill` returns `complete: false` until indexing coverage accounts for missing keys and expired entries.
1420
1545
  The method schemas and their error arrays are normative. Malformed JSON-RPC and params use JSON-RPC errors.
1421
1546
 
@@ -1495,7 +1620,7 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1495
1620
  as them and call every method, including `team.*` and `daemon.stop`. Agent-facing clients still
1496
1621
  must not expose `team.*`. The OpenCode TUI is a human surface and may call them. It must not
1497
1622
  place an invite ticket in a session, a log, or a model context.
1498
- - Key-scoped methods (`session.update`, `session.unregister`, `events.subscribe`, `inbox.*`, `item.read`, `task.*`,
1623
+ - Key-scoped methods (`session.update`, `session.unregister`, `events.subscribe`, `inbox.*`, `item.read`, `task.*` (but `tasks.list` and `tasks.get`),
1499
1624
  `conv.list`, `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`)
1500
1625
  work only for a key that the calling connection registered. Human conversation calls can omit the key on the admin socket.
1501
1626
  - A session key is therefore a bearer credential for its session: whoever knows it can register it
@@ -1505,14 +1630,14 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1505
1630
  `messaging` CLI and the `/messaging` TUI.
1506
1631
  - **Tiers.** Conversation methods available to agents also appear in the human list:
1507
1632
  `AGENT_METHODS` (the agent tier: sessions, `device.status`, `device.bind`, `contacts.list`,
1508
- `inbox.list`, `inbox.get`, `inbox.ack`, `item.read`, `conv.list`, `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`, `task.*`,
1633
+ `inbox.list`, `inbox.get`, `inbox.ack`, `item.read`, `conv.list`, `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`, `task.create`, `task.continue`, `task.list`, `task.get`, `task.update`, `task.cancel`, `task.question`,
1509
1634
  `events.subscribe`, `daemon.status`, `daemon.health`); `HUMAN_METHODS` (acting as the person: `conv.list`,
1510
1635
  `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`,
1511
1636
  `conv.get`, `conv.members`, `conv.threads`, `conv.pin`, `conv.create`, `conv.add`, `conv.remove`, `conv.role`, `conv.seat`,
1512
1637
  `conv.settings`, `conv.meta`, `conv.archive`, `conv.leave`, `conv.join`, `conv.accept`, `conv.policy`, `conv.backfill`, `people.search`, `people.get`,
1513
- `grant.list`, `grant.set`, `grant.set.preview`, `request.list`, `request.answer`, `request.answer.preview`, `contact.request`,
1638
+ `grant.list`, `grant.set`, `grant.set.preview`, `reach.get`, `reach.set`, `reach.set.preview`, `request.list`, `request.answer`, `request.answer.preview`, `contact.request`,
1514
1639
  `contact.request.preview`, `card.get`, `sessions.list`, `request.waiting`,
1515
- `notice.subscribe`, `notice.list`, `notice.ack`, `device.approve.preview`,
1640
+ `notice.subscribe`, `notice.list`, `notice.ack`, `tasks.list`, `tasks.get`, `device.approve.preview`,
1516
1641
  `device.remove.preview`, `identity.replace.preview`, `directory.erase.preview`); and `ADMIN_METHODS`
1517
1642
  (this device and the daemon: `daemon.stop`,
1518
1643
  `daemon.woke`, `person.attached`, `daemon.snapshot`, `wake.arm`, `wake.disarm`,
@@ -1548,6 +1673,12 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1548
1673
  Only a directory realm has it (`invalid_state` in a roster realm).
1549
1674
  - `device.list` (admin tier) lists this device's person's devices, current first, with vouch, kind,
1550
1675
  and for a removed one when, why and by which device it was unbound.
1676
+ - Each entry carries `features`: the presence features (§6.5) that device advertises. This device lists its own.
1677
+ Another device of the person lists those of the last presence this device verified from it.
1678
+ - `features` is absent when no presence of that device has been seen. That means unknown, not none.
1679
+ - A client warns while any current device of the person lacks a feature it relies on. The plugin does this for
1680
+ `reach`, the reach choices of §6.9: a device without it does not enforce them.
1681
+ - Only the person's own devices are listed. No method shows another person's devices or their features.
1551
1682
  - `device.remove {device, reason?, token?}` (admin tier) unbinds this device (`self`), or another
1552
1683
  current device of its person (`removed` by default, or `lost`) with a fresh Jelly token bound to this
1553
1684
  device (`invalid_request` without one). A directory realm only; a roster realm uses `team.remove`.
@@ -1728,7 +1859,79 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1728
1859
  `invalid_state`.
1729
1860
  - The daemon validates the transition and stores it.
1730
1861
  - It then emits `task.status` and/or `task.artifact`.
1731
- - **`task.cancel`** is allowed only for the requester.
1862
+ - **`task.cancel`** is allowed only for the requester: the requester session (`{key, taskId, reason?}`), or, on the
1863
+ human tier, the person with the task's `cancel` action (`{taskId, token, reason?}`, "Action tokens"). It takes
1864
+ exactly one of `key` and `token`. A cancel by token returns the record with the owner view, as the admin socket's
1865
+ task reads do.
1866
+ - **The task view.** Every `TaskRecord` the daemon projects (`task.get`, `task.list`, `task.update`, `task.cancel` and
1867
+ `task` events) carries fields it computes as it projects the record. They are never stored and never sent to the
1868
+ peer:
1869
+ - `agent`: the counterpart agent, the session its verified envelopes name (`peer.agent`).
1870
+ - `person: {id, name}`: that agent's person, `peer.person`, with the display name the realm verified for them (the
1871
+ person id when the realm does not know them). It is never parsed from an agent's name.
1872
+ - `org`: `same`, `other` or `none`, that person's relation to this device's person, as `ParticipantView.org` computes
1873
+ it. (Jelly calls it `organisation`; the plugin maps the name.)
1874
+ - `person` and `org` are absent while the daemon cannot name the person yet: a `task` event is projected at once from
1875
+ what it last learned, and a read looks the person up first.
1876
+ - `signIn: {tool?, by}`, in `TASK_STATE_AUTH_REQUIRED` only. `by` is `you` when the executor is an agent of this
1877
+ device's person (this device runs the task, or the peer is another device of the same person), else `them`. The
1878
+ viewer's own daemon computes it; it is never read from the peer. `tool` is what the executor named (§6.2,
1879
+ "Sign-in"): plain text through `cleanPeerText`, single line, at most `TASK_SIGN_IN_TOOL_CHARS`
1880
+ (`LIMITS.previewChars`, 200) characters; absent when it named none.
1881
+ - `refusal: {reason}`, in `TASK_STATE_REJECTED` only, when the executor gave words: the text of its status message,
1882
+ through `cleanPeerText`, at most `TASK_REFUSAL_CHARS` (`LIMITS.requestChars`, 500) characters.
1883
+ - The tool and the reason are only ever the executor's own words: on the executor's side, its session's
1884
+ `task.update`; on the requester's, a status message received from the executor (it carries the provenance the
1885
+ daemon wrote, §6.6). A status the requester's daemon made itself, such as "delivery rejected: refused", gives
1886
+ neither.
1887
+ - Both are untrusted peer text, as `InboxItem.preview` is: a UI shows them as plain text, never markup, and a model
1888
+ sees them only fenced (§11). So is the `name` in `person`: the realm verifies who the person is, but the name is their own
1889
+ choice, and a UI shows it as plain text.
1890
+ - `question: {sessionId, approvalId}`: the executor host's own pending approval or question that an
1891
+ `AUTH_REQUIRED` or `INPUT_REQUIRED` task waits on. Both ids are the host engine's, opaque plain identifiers
1892
+ (`[A-Za-z0-9][A-Za-z0-9._:-]{0,199}`).
1893
+ - **Who sets it.** The session that runs the task: its client's runtime calls agent-tier
1894
+ `task.question {key, taskId, question}` from its host's own permission and question events (`question: null`
1895
+ unties it). It is never a model tool.
1896
+ - **What is checked.** `key` must be registered on the calling connection (`not_found`). The task must be one that
1897
+ session executes: another session's task, or an unknown one, is `not_found`, and the requester side is
1898
+ `invalid_state`. The task must be in one of those two states (`invalid_state`). When the session's registration
1899
+ named its host session (`session.register {host: {sessionID}}`), `sessionId` must equal it (`forbidden`).
1900
+ - **The limit of that check.** A registration without `host` (under a Jelly host, one key serves all of an
1901
+ agent's sessions) gives the daemon no host session to compare. Then `sessionId` is the runtime's word, scoped
1902
+ to that agent's own task.
1903
+ - **No human-tier form.** Only the executing session knows which approval its task waits on. An admin-socket
1904
+ client could name any approval of any task, with nothing to check it against.
1905
+ - The daemon keeps it **in memory only**: the approval it names does not survive a restart, so neither does the
1906
+ association. Any later status of the task (a `task.update`, a cancel from the requester, the 24 h timeout)
1907
+ unties it.
1908
+ - Setting it from the agent tier does not make it visible there. It is shown only on the admin socket (the
1909
+ person's own clients): in `tasks.list`, `tasks.get` and `conv.sync`'s `task` entries, and in `task.get`,
1910
+ `task.list`, `task.update` and `task.cancel` results there, for the executor's side of a task still in one of
1911
+ those states. It is never on the
1912
+ agent socket, never in a `task` event (events go to both sockets), and never in anything sent to the peer.
1913
+ - Nothing in it travels: the peer's daemon cannot learn the host's session or approval ids.
1914
+ - **The person's tasks.** `tasks.list {role?, state?, session?, cursor?, limit?}` and `tasks.get {taskId, role?}` are
1915
+ human tier (admin socket only, no session key). They read the tasks this person's agents on this device are in,
1916
+ as requester or executor, across all their sessions. They return `PersonTask`: the task view above, as the owner
1917
+ sees it, `question` included.
1918
+ - In place of the session `key` (a bearer credential), a `PersonTask` names the agent in `session`. The schema
1919
+ refuses a `key`.
1920
+ - On the requester side of a task that has not ended, it carries `cancel`, minted for this view as `request.list`'s
1921
+ actions are ("Action tokens"). The executor side never has one.
1922
+ - A device holds only its own sessions' sides, so another person's tasks are never listed, nor the person's other
1923
+ devices' tasks.
1924
+ - `tasks.list` is most recently updated first, 50 by default and at most 100 per page. A page also stops at the list
1925
+ byte budget. It reports `total` and pages with an opaque `cursor`/`nextCursor`. `session` filters to one agent by
1926
+ name; an unknown name lists nothing. `tasks.get` needs `role` only when this device holds both sides (one of its
1927
+ agents asked another); without it, `invalid_request`. An unknown task is `not_found`.
1928
+ - **Live.** A change of such a task (any `task` event, and `task.question`) adds a `task` entry to `conv.sync`'s
1929
+ stream (§12.4): `{type: "task", pos, task: PersonTask}`, rendered for each reader when it reads, so `cancel` is
1930
+ minted for that reader's view. Only a person-level reader gets these entries: the admin socket with no session
1931
+ key. The agent tier, and any `conv.sync` with a `key`, skip them. The stream is chosen over a new push kind so a
1932
+ host's request-and-response section stays live with the long poll it already makes (design §12.1). When one read
1933
+ covers several changes of the same side, it renders that side once, as it is now.
1934
+ - Action class `view` under a Jelly host (§8.11).
1732
1935
  - **`task` events** carry `origin`:
1733
1936
  - `local` when the event follows the subscriber key's own call: `task.update`, `task.cancel`,
1734
1937
  or the `task.create` call that created the record;
@@ -1757,7 +1960,24 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1757
1960
  - **Consent (§6.9).** `grant.list`, `grant.set`, `grant.set.preview`, `request.list`,
1758
1961
  `request.answer`, `request.answer.preview`, `contact.request`, `contact.request.preview` and `card.get` are human-client methods that are
1759
1962
  not in `AGENT_METHODS`: agents never read or change a person's grants, answer a held request, or send a message
1760
- request.
1963
+ request. So are `reach.get`, `reach.set` and `reach.set.preview`: agents never read or change the person's reach
1964
+ choices.
1965
+ - `reach.get {}` (read-only) returns `{revision, available, groups}`: `available` is true; `groups` are the two
1966
+ questions of "Reach choices" (§6.9), in order, each `{id, label, options, value, lapsed?}`. `id` is `message`
1967
+ ("Who can message you") or `ask` ("How agents may ask yours"); `options` are `{value, label, description}` for
1968
+ `open` (Open), `ask` (Ask me first) and `never` (Never), in that order, in fixed words (`REACH_QUESTIONS`);
1969
+ `value` is the person's choice, from the relation grant this device holds whose signer is current, or null for
1970
+ none (the defaults apply); `lapsed` is a choice that lapsed with a removed signer and waits on the review list.
1971
+ `revision` is opaque, and changes whenever a relation grant or its review entry changes on this device, also by
1972
+ sync from another device or a re-sign.
1973
+ - `reach.set {revision, changes}` takes the `revision` the caller read and `changes`, an object of at most the two
1974
+ group ids, each one of the three values: anything else is invalid params. When `revision` is not the current
1975
+ one it fails with `conflict` and changes nothing. Otherwise it signs, for each change, the relation grant of
1976
+ "Reach choices" with this device, stores it, takes the subject off the review list and sends it to this person's
1977
+ other devices, all in one transaction, and returns the new snapshot (`reach.get`'s result). An empty `changes`
1978
+ changes nothing. Under a Jelly host it is a `local` action (§8.11).
1979
+ - `reach.set.preview` takes the same params, checks them the same way (`conflict` too), changes nothing, and
1980
+ returns one fixed `sentence` saying what the changes would do, for a confirmation.
1761
1981
  - `grant.list {subject?}` returns this person's grants as this device holds them, newest first,
1762
1982
  each with its levels, `id`, `ts`, `by` and, when re-signed, `resignedAt`, and the review list:
1763
1983
  the widening levels that lapsed with their signer, with when they lapsed. A conversation subject carries its canonical `cid`.
@@ -1776,12 +1996,15 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1776
1996
  `agent`, `address`), the local session it is for, an untrusted `preview`, when it was held and when it
1777
1997
  expires; then the message requests (`contact`), each with its envelope `id`, the sender as the realm verified
1778
1998
  them (`person`, `device`, `label`, `name`, `email?`, `org`), its untrusted `text`, `at` and `exp`; then the
1779
- suggestions (`suggestion`), each with its `id`, the local `agent` that asked, the `email` and `at`.
1999
+ suggestions (`suggestion`), each with its `id`, the local `agent` that asked, the `email` and `at`. Each item
2000
+ also carries `actions` ("Action tokens").
1780
2001
  - `request.answer {id, answer, all?}` answers one and returns the ids answered: an ask takes `once`, `always`,
1781
2002
  `decline` or `block` ("Holds"), a message request `accept`, `decline` or `block` ("Message requests"), a
1782
2003
  suggestion `decline`; any other answer is `invalid_request`. With `all`, the same answer goes to every request
1783
2004
  of the same kind held from that person. It fails with `not_found` for an id this device does not hold, or one
1784
- past its expiry.
2005
+ past its expiry. `token` is the request's action for this `answer`. It is checked before anything else, it never
2006
+ comes with `all`, and under a Jelly host it is required (`grant_required` without it). So under a Jelly host
2007
+ `all` is never used: each request is answered with its own token.
1785
2008
  - `request.answer.preview {id, answer, all?}` (`request.answer`'s input) changes nothing and returns, for the
1786
2009
  answer's confirmation, whom it reaches and what it does: `{kind, from: {name, email?, org}, agent?, to?, count,
1787
2010
  sentence}`. `from` is the realm's verified profile of the sender (of the person themself for their
@@ -1789,7 +2012,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1789
2012
  for, `count` how many requests the answer would settle (more than 1 only with `all`), and `sentence` one fixed
1790
2013
  sentence saying what `answer` does. A name or email in the sentence is
1791
2014
  cleaned to one line of at most 80 (100) characters. It refuses as `request.answer` would: `not_found` for an id
1792
- this device does not hold or one past its expiry, `invalid_request` for an answer the kind does not take.
2015
+ this device does not hold or one past its expiry, `invalid_request` for an answer the kind does not take. It
2016
+ checks a `token` as `request.answer` does, but does not spend it.
1793
2017
  - `contact.request {email, text, suggestion?}` (§6.9, "Message requests") resolves the exact `email` and sends
1794
2018
  `text` (1 to `LIMITS.requestChars` UTF-16 code units) as a message request, and returns `{id, conversation,
1795
2019
  person}`: the line's id, `dm:<person>` and the person found. It fails with `invalid_request` in a roster
@@ -1835,8 +2059,14 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1835
2059
  - **Your agents.** Human-tier `sessions.list` returns local sessions as `{agent, state, seats}`.
1836
2060
  State is `idle`, `busy` or `kept`. Seats are verified conversation IDs. No unread count is fabricated.
1837
2061
  - **Notices.** What the person is told about, in three fixed categories, as `{id, category,
1838
- conversation?, from?: {name, email?, org}, line?, at}`:
2062
+ conversation?, from?: {name, email?, org}, line?, key?, at}`:
1839
2063
  - `message`: a retained legacy notice, without transcript text. Live conversation updates use the conversation stream.
2064
+ - `message` with `key: "task:<task id>"`: a task another person's agent asked of one of this person's agents. It is
2065
+ filed when this device accepts the task (when the `TaskRecord` of the executor's side is created), with `from` the
2066
+ requesting person. It copies no text of the task. It waits on the agent, not the person: it is never a `request`
2067
+ and never in `counts.requests`. An ask held for consent (§6.9, "Holds") is filed as a `request` and stays one until
2068
+ its person answers. If they let it through, the accepted task then files its `message` notice; if they decline,
2069
+ there is none. A task between this person's own agents files no notice. A host opens the notice by its `key`.
1840
2070
  - `request`: a held request or retained contact suggestion (§6.9). New notices contain no legacy room reference.
1841
2071
  - `alert`: an alarm of this device (`keys_missing`, `restored`, `directory`), with fixed text in `line`; a
1842
2072
  change of another device of the person (below), with `device`; or trust the person gave someone that ends
@@ -1861,7 +2091,7 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1861
2091
  admitted by the team's admin.", "'tablet' (other) on your account is now vouched by Jelly ID: ...", "'desk'
1862
2092
  was replaced by 'desk2' (jelly-host), ...", "'phone' (laptop) was removed from your account.".
1863
2093
  - Each notice is filed once, when its source is, under an id derived from the source (`m_<line id>`,
1864
- `r_<request id>`, `a_<alarm and time>`, `a_dev<log seq>`, `a_trust<grant id>`), so a replay repeats the same ids. A notice copies no text:
2094
+ `m_task<task id>`, `r_<request id>`, `a_<alarm and time>`, `a_dev<log seq>`, `a_trust<grant id>`), so a replay repeats the same ids. A notice copies no text:
1865
2095
  `lines: true` can expose fixed alert text. It never restores legacy transcript content.
1866
2096
  - `notice.subscribe {after?, lines?}` sends the unacknowledged notices filed after the notice `after`
1867
2097
  (all of them without it, at most 500), oldest first, as `notice` events on this connection, then each
@@ -1871,7 +2101,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1871
2101
  `request.list` and `counts.requests`, until `request.answer` (or its expiry). Notices go with the transcript's retention, and acknowledged ones
1872
2102
  with the read mail's.
1873
2103
  - **Previews.** Each preview takes exactly its action's input, so a host that draws a confirmation calls it with
1874
- the input the action will get (Jelly does): `grant.set.preview` takes `grant.set`'s params, `request.answer.preview`
2104
+ the input the action will get (Jelly does): `grant.set.preview` takes `grant.set`'s params, `reach.set.preview`
2105
+ `reach.set`'s `{revision, changes}`, `request.answer.preview`
1875
2106
  `request.answer`'s `{id, answer, all?}`, `contact.request.preview` `contact.request`'s `{email, text?, suggestion?}`,
1876
2107
  `device.approve.preview` `device.approve`'s `{link, sas?}` and `device.remove.preview` `device.remove`'s `{device,
1877
2108
  reason?, token?}`. A field the preview does not read may be left out (`text`, `sas`, `suggestion`, `reason`,
@@ -1880,7 +2111,7 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1880
2111
  changes nothing. What each returns is unchanged: `device.remove.preview` (human tier, read-only, a directory realm
1881
2112
  only) returns what removing a device of this person would remove, `{label, kind?, vouch, current, self, since?}`,
1882
2113
  or `not_found`; `device.approve.preview` returns `{device, label, kind, sas}` ("Devices"); `grant.set.preview`,
1883
- `request.answer.preview` and `contact.request.preview` are under "Consent" above. Under a Jelly host a preview's
2114
+ `reach.set.preview`, `request.answer.preview` and `contact.request.preview` are under "Consent" above. Under a Jelly host a preview's
1884
2115
  grant names its action's class, `local` (§8.11).
1885
2116
  - **Failed delivery of a task request:**
1886
2117
  - a `rejected` receipt or nack moves the requester's record to `TASK_STATE_REJECTED`;
@@ -1934,6 +2165,11 @@ delegation chain is verified after these (§6.7). A person refuses another perso
1934
2165
  one, or sends a message request. A person whose known devices all lack the `conversations` presence feature is
1935
2166
  refused with `peer_outdated`, "<handle> needs to update Messaging", and nothing is created or sent. With no
1936
2167
  presence for that person in the envelope TTL, the relay has no evidence, and the call goes ahead.
2168
+ A request that then reaches a client before conversations is filed there and answered `delivered`, which a
2169
+ current recipient never sends for a request (it holds it, and an accept activates the dm at the relay). The
2170
+ sender's daemon takes a `delivered` or `read` receipt for a request envelope from that recipient's device as that
2171
+ proof: `request.waiting` shows the request with state `outdated` (the recipient must update Messaging, and the
2172
+ request must then be sent again), instead of `held` forever.
1937
2173
  - Tasks keep their envelopes and work in both directions.
1938
2174
  - Rollout: the relay first (it serves 0.7.1 unchanged), then every daemon of a realm together
1939
2175
  (`docs/distribution.md`, "Versions and compatibility").
@@ -2116,6 +2352,9 @@ database by itself.
2116
2352
  client that denies its agents the home also denies `<home>.pre-reset-*`: the moved home keeps the old keys and
2117
2353
  messages. The messagingd that wrote it still opens it.
2118
2354
 
2355
+ **Daemon schema 5** adds `reach_grants` and `reach_reviews`: the person's relation grants (§6.9, "Reach choices") and
2356
+ their review entries, apart from `grants`. An older daemon ignores them, and so does not enforce the choices (§6.9).
2357
+
2119
2358
  **When stores grow.**
2120
2359
 
2121
2360
  - A daemon or relay opens any store of its generation whose `min_reader` is at most its own schema. It
@@ -2242,19 +2481,21 @@ and names each session an engine may register.
2242
2481
  parent writable by no one else and owned by the daemon's user or root.
2243
2482
  - `--socket-group <gid>` makes both sockets 0660 and owned by that group, and `run/` (and the home, when
2244
2483
  `run/` is inside it) 0710 to it. It is accepted only here: elsewhere `run/admin.sock` is always 0600.
2245
- - **Action grants.** On `run/admin.sock`, every method outside `AGENT_METHODS` needs an action grant, a
2246
- compact JWS in the request's top-level `grant` member, beside `params`:
2484
+ - **Action grants.** On `run/admin.sock`, every method outside `AGENT_METHODS`, and every call whose params carry an
2485
+ action `token` (§8.2, "Action tokens"), needs an action grant, a compact JWS in the request's top-level `grant`
2486
+ member, beside `params`:
2247
2487
  - the header is `{alg: "EdDSA", typ: "jelly-act+jwt", kid}`, and the signature verifies with the action
2248
2488
  key (whose `kid`, when the key file has one, the header must name);
2249
2489
  - `aud` is `"@joe-broadhead/opencode-messaging#service"`;
2250
2490
  - `act` is the method, and `in` is `base64url(SHA-256(JCS(params)))` of the params as sent (absent
2251
- params are `{}`);
2491
+ params are `{}`). The params include any action `token` (§8.2), so the host's grant signer sees the token. That
2492
+ is expected: the host's client received it in the view;
2252
2493
  - `exp - iat` is in (0, 60] seconds, `iat` at most 5 s ahead of the daemon's clock and `exp` at most 5 s
2253
2494
  behind it;
2254
2495
  - `cls` is required, and is the method's action class (`ACTION_CLASSES`, `actionClass` in `@messaging/core`):
2255
2496
  `chat` for `conv.send`, `conv.edit`, `conv.delete`, `conv.react` and `notice.ack`; `view` for the reads a host's section makes, the
2256
2497
  read-only human-tier methods and `device.list` (`conv.list`, `conv.read`, `conv.sync`, `conv.get`, `conv.members`, `conv.threads`, `conv.search`, `conv.context`, `conv.access`, `request.waiting`, `people.search`,
2257
- `people.get`, `grant.list`, `request.list`, `sessions.list`, `notice.subscribe`, `notice.list`,
2498
+ `people.get`, `grant.list`, `reach.get`, `request.list`, `sessions.list`, `tasks.list`, `tasks.get`, `notice.subscribe`, `notice.list`,
2258
2499
  `device.list`); `local` for every other method, the previews and every other admin-tier method included.
2259
2500
  (`device.status` is agent tier: it needs no grant.) So `view` is refused on any method that writes, and a preview, which tells what
2260
2501
  a `local` action would do, takes `local`. A plugin's `jelly` declaration must give each action and read the same
@@ -2304,7 +2545,16 @@ and names each session an engine may register.
2304
2545
  request envelope ids of those agents' requester tasks that have not ended (submitted, working, input- or
2305
2546
  auth-required), most recently updated first, at most 500. Their updates name the request in `replyTo` (§6.2), so the
2306
2547
  relay can match them without reading a body. Mail an agent sent and a reply it may get is not awaited: only a task
2307
- is. It returns `{relay: "armed" | "pending", awaiting}`: `pending` while the relay is not connected, and the arm then
2548
+ is. The daemon also adds `dms`: the verified dm conversations of which this person is a member whose other
2549
+ participant's person shares a `channel` with this person (both seated there as persons, not only through an
2550
+ agent, and `member`, in that channel's verified log) and passes `shouldArm` (not blocked or muted by this
2551
+ person's grants, and not hidden by their reach choices: under Never a person of another org with no chat on their
2552
+ person grant, §6.9), most recently active first, at most `WAKE_DMS`. The daemon reads the candidates' directory
2553
+ views in batches before it decides. It is the host's own list: no other member learns it, and the daemon logs only
2554
+ its count. The list is what held when the host armed: a person blocked, muted or hidden afterwards (on another of
2555
+ this person's devices, while the host sleeps) stays armed until the next `wake.arm`. That is at most one wake: the
2556
+ ticket is single-use, and the wake budget applies. It returns `{relay: "armed" | "pending", awaiting, dms}` (the counts): `pending` while the relay is not
2557
+ connected, and the arm then
2308
2558
  goes out at the next welcome (a newer `wake.arm` or `wake.disarm` replaces it; one the relay refuses is not sent
2309
2559
  again). `wake.disarm {}` forgets it, and `daemon.woke` disarms a ticket armed since the last disarm. The daemon never
2310
2560
  logs the ticket.
@@ -2476,6 +2726,9 @@ implementation:
2476
2726
  `in` digest, a method grant, a host-scope grant and a session ticket signed by it, and grants a daemon must
2477
2727
  refuse (another method, params, key, `kid`, `typ` or audience; a lifetime past 60 or 600 s; an expired grant;
2478
2728
  a `cls` of another class; host scope for a method outside its list).
2729
+ - `action-tokens.json` (§8.2, "Action tokens"): a token, held requests with their actions, the `request.answer` and
2730
+ `task.cancel` params that redeem one, and shapes to refuse (an action that is not an answer, a short or padded
2731
+ token, a token with `all`, a cancel with neither or both of `key` and `token`).
2479
2732
 
2480
2733
  `packages/core/scripts/vectors.ts` generates them. Run `npm run build && npm run vectors -w @messaging/core`;
2481
2734
  this uses Node's type stripping against the built package and `@messaging/core/testing`. `packages/core/test/vectors.test.ts`
@@ -2553,6 +2806,11 @@ contacts, changes devices or administers a team or channel.
2553
2806
  - **`search`** uses `conv.search` within the daemon's access rules.
2554
2807
  - **`tasks`** lists `task.list` with roles `mine` (requester) and `asked` (executor) and short state
2555
2808
  names (`working`, `input_required`, …); `state` also takes `open` and `done`.
2809
+ A task in `auth_required` shows `signIn {tool?, by}`, and one in `rejected` shows `refusal {reason}`, from the task
2810
+ view (§8.2). The tool and the reason are the executor's words, with the author `statusAuthor` names: fenced when the
2811
+ peer wrote them, cleaned and shown without a fence when this side did. `by` is the daemon's, never the peer's. The
2812
+ agent tier never sees `question`, and no tool sets it (the runtime calls `task.question`). `task_update` takes no sign-in tool yet: a plugin that knows which tool is waiting
2813
+ passes it with `task.update {signIn}`.
2556
2814
  - **`task_update`** (executor) and **`task_cancel`** (requester) map to `task.update` and
2557
2815
  `task.cancel`; a cancel of a finished task returns its state and sends nothing.
2558
2816
  - **Errors.** A tool error is `{error: {code, message, remedy}}`: `code` from §13, a `message` that
@@ -2837,7 +3095,8 @@ Oversized model-visible text is truncated with the explicit marker in §11, rath
2837
3095
  | binding_invalid | a device binding, a device-log entry or its authority failed verification (§15) |
2838
3096
  | jelly_unavailable | Jelly ID could not be reached or returned an unusable response during key fetching (§15.2) or device sign-in (§15.9); never reported as `binding_invalid` |
2839
3097
  | log_conflict | a device-log entry does not extend the log's head (§15.3); resync and retry |
2840
- | conv_conflict | a conversation entry does not extend the log, or an event's op head is not the current op (§16.7); re-read and retry |
3098
+ | conv_conflict | a conversation entry does not extend the log, or an event's op head is not the current op (§16.7); re-read and retry. Also a control op (`conv.add`, `conv.remove`, …) on a conversation that kept queuing events through three drains: "the conversation is busy sending; try again", which can be retried as is |
3099
+ | conflict | a person's setting changed since the caller read it: `reach.set` with a stale `revision` (§8.2); nothing changed, read again |
2841
3100
  | stale_epoch | a conversation message is sealed under an epoch that is not the current one, or a rekey is due (§16.7); re-seal and retry |
2842
3101
  | device_limit | the person already has `LIMITS.devicesPerPerson` current devices (§15.3) |
2843
3102
  | admission_required | `dir.bind` of an entry that is not a device binding itself with a `jelly` authority (§15.10) |
@@ -2847,8 +3106,8 @@ Oversized model-visible text is truncated with the explicit marker in §11, rath
2847
3106
  | directory_unavailable | the key directory could not be reached to adopt a tree head or verify a person within the freshness bound (§15.9) |
2848
3107
  | login_unsupported | Jelly ID's metadata names no device sign-in, or none for this realm's resource (§15.9, "Signing in through the browser") |
2849
3108
  | login_denied | the person denied a device sign-in at Jelly ID, or the account is disabled or being deleted (§15.9) |
2850
- | grant_required | `deployment: "jelly"`: a human- or admin-tier call carried no action grant (§8.11) |
2851
- | grant_invalid | `deployment: "jelly"`: the action grant did not verify, is for another method, params or audience, is out of its lifetime, was spent, or is a host-scope grant bound to another connection (§8.11) |
3109
+ | grant_required | `deployment: "jelly"`: a human- or admin-tier call carried no action grant (§8.11), or a `request.answer` no action token (§8.2) |
3110
+ | grant_invalid | `deployment: "jelly"`: the action grant did not verify, is for another method, params or audience, is out of its lifetime, was spent, or is a host-scope grant bound to another connection (§8.11). Any deployment: an action token that the daemon did not offer for this action, target and view, or that was spent or has ended (§8.2, "Action tokens") |
2852
3111
  | ticket_required | `deployment: "jelly"`: `session.register` carried no session ticket (§8.11) |
2853
3112
  | ticket_invalid | `deployment: "jelly"`: the session ticket did not verify, is out of its lifetime, or names another session or agent (§8.11) |
2854
3113
  | daemon_unavailable | a client cannot reach the daemon |
@@ -3407,9 +3666,12 @@ anchor must be an `epoch` anchor whose root is the directory's at that size (`di
3407
3666
  `rate_limited` with `retryAfterMs`. The per-day counts, and the copies of an open request, are kept in memory,
3408
3667
  from UTC midnight.
3409
3668
  - **Presence** (`to: {team: true}`) goes only to devices that `presence.watch` the person and are in the person's
3410
- **audience**: the person themselves, the same org, or persons who each sent the other an envelope other than a
3411
- receipt in the last 30 days. A
3412
- watcher outside it learns nothing, not even "offline". The relay sends no `peer` notices.
3669
+ **audience**: the person themselves, the same org, persons who each sent the other an envelope other than a
3670
+ receipt in the last 30 days, or persons who are both accepted members of one conversation the relay sequences
3671
+ (§7 "Conversation frames"), for as long as both are. The relay reads that from the plaintext op state it already
3672
+ holds: an invited participant does not count, a removed or departed one stops counting, and an external dm counts
3673
+ only once activated (its request accepted). A watcher outside the audience learns nothing, not even "offline". The
3674
+ relay sends no `peer` notices.
3413
3675
 
3414
3676
  **Erasure.** Erasing a person appends an `unbind` of each current device, sets their map value to the tombstone
3415
3677
  (§15.5) at the next epoch, deletes their profile, every log body, witness and keys statement and every pair they
@@ -3448,7 +3710,8 @@ five minutes, until it answers, so a new directory becomes ready without waiting
3448
3710
  `dir_keys`, `dir_epochs`, `dir_map` (each person's committed value), `dir_rotations`, `dir_pairs` (with
3449
3711
  `line_at`, when the pair last carried an envelope other than a receipt), `dir_jelly_keys`, `dir_feed`, and
3450
3712
  `dir_wakes`: each Jelly host's device's armed wake ticket, its `agents` and `awaiting`, its `exp` and when it may
3451
- next be presented (§7, "Wake tickets"). At start the directory rebuilds the map and the epoch log from them
3713
+ next be presented (§7, "Wake tickets"), and `dir_wake_dms` (relay schema 4): the dms each armed device named, with
3714
+ the `exp` of the ticket they were armed with, so a ticket armed again without them does not keep them. At start the directory rebuilds the map and the epoch log from them
3452
3715
  and refuses to start if they disagree with the last tree head.
3453
3716
 
3454
3717
  ## 16. Conversations
@@ -3604,7 +3867,10 @@ The last writer wins by `seq`.
3604
3867
  entry: an expired message is a digest with no author or state effect. Replay collects the cuts, verifies every
3605
3868
  chain link, and does not apply cut entries; a cut rekey cannot cut another device. A cut that removes genesis or
3606
3869
  leaves later entries unauthorized is refused. A live verifier checks that the named device has been removed in
3607
- its verified realm. A cut never extends a removed device's authority.
3870
+ its verified realm. A cut never extends a removed device's authority. Several device rekeys may name one device:
3871
+ an entry is cut when any of their cuts covers it. A daemon asked for a cut stricter than a rekey of the same device
3872
+ it has queued or sent sends the stricter one too, ahead of anything else queued, before the call that drains it
3873
+ returns: a queued rekey stands in for a request only when its cut is at least as strict.
3608
3874
  - **Retention.** Only `msg` entries expire. When the relay prunes a `msg` entry older than the conversation's
3609
3875
  `retainDays` it keeps the entry's `seq`, `prev` and the digest `SHA-256(JCS(event))` and serves those
3610
3876
  (`ConvExpiredEntry`), so the chain stays verifiable (`ConvLog.applyExpired`). **Ops never expire**: membership,
@@ -3850,6 +4116,7 @@ Only the connected recipient device can submit its own signed decision. A later
3850
4116
  `request.state {req, descriptor}` returns that decision to either named person, or no decision before settlement.
3851
4117
  Both frames answer `request.settled {req, id, decision?}`. They expose neither salt nor text to the relay.
3852
4118
  Relay schema 3 adds `conv_request_decisions`. Generation stays 2 and the minimum reader stays 1.
4119
+ Relay schema 4 adds `dir_wake_dms` (§7, "Wake tickets"; §15). Generation stays 2 and the minimum reader stays 1.
3853
4120
 
3854
4121
  **Permanent activation.** An external human DM declares `create.admission: "request"`.
3855
4122
  The relay refuses an external DM without this declaration. Same-org DMs retain implicit org admission.
@@ -3878,4 +4145,9 @@ Acceptance opens only chat. A later block prevents local import and key use even
3878
4145
  The sender receives the same error for an unknown or undiscoverable exact email.
3879
4146
 
3880
4147
  Old pending requests retain their local records and expose `resend`. The sender must send a new request with portable evidence.
4148
+ On the sender's side, a request sent before the cut-over that the recipient's device held for its person and nobody
4149
+ answered (its chat envelope still `sent`, held and unexpired) appears in `request.waiting` with state `resend`, until
4150
+ it lapses (`LIMITS.ttlMs` after it was sent) or a new request to that person replaces it. Nothing is sent for it:
4151
+ resending signs a new request, which needs the person's own send. A request still in flight before the cut-over,
4152
+ not yet held, cannot be told from a chat line and is not shown.
3881
4153
  The cut-over never signs old plaintext for its sender or silently resends it.