@joe-broadhead/messaging 0.7.1 → 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 (94) hide show
  1. package/CHANGELOG.md +364 -2
  2. package/dist/bin/messaging-mcp.js +5498 -4338
  3. package/dist/bin/messaging-relay.js +8626 -5540
  4. package/dist/bin/messaging.js +4492 -3158
  5. package/dist/bin/messagingd.js +19672 -12412
  6. package/dist/lib/modules/core/agent/operations.js +19 -327
  7. package/dist/lib/modules/core/agent/present.js +20 -55
  8. package/dist/lib/modules/core/agent-tools.js +35 -139
  9. package/dist/lib/modules/core/consent.js +54 -14
  10. package/dist/lib/modules/core/contact-card.js +3 -3
  11. package/dist/lib/modules/core/conversation/event.js +29 -4
  12. package/dist/lib/modules/core/conversation/guest-invitation.js +42 -0
  13. package/dist/lib/modules/core/conversation/log.js +287 -65
  14. package/dist/lib/modules/core/conversation/request-log.js +158 -0
  15. package/dist/lib/modules/core/conversation/request-seal.js +41 -0
  16. package/dist/lib/modules/core/conversation/requests.js +87 -0
  17. package/dist/lib/modules/core/conversation/seal.js +90 -37
  18. package/dist/lib/modules/core/conversation-agent-tools.js +69 -0
  19. package/dist/lib/modules/core/federation/directory-proof.js +83 -0
  20. package/dist/lib/modules/core/federation/proof-state.js +167 -0
  21. package/dist/lib/modules/core/federation/roster-proof.js +74 -0
  22. package/dist/lib/modules/core/index.js +9 -4
  23. package/dist/lib/modules/core/jelly-host.js +8 -5
  24. package/dist/lib/modules/core/schema/common.js +33 -5
  25. package/dist/lib/modules/core/schema/consent.js +23 -5
  26. package/dist/lib/modules/core/schema/contact-request.js +22 -0
  27. package/dist/lib/modules/core/schema/conv-frames.js +101 -0
  28. package/dist/lib/modules/core/schema/conversation.js +67 -33
  29. package/dist/lib/modules/core/schema/directory-frames.js +8 -3
  30. package/dist/lib/modules/core/schema/envelope.js +8 -24
  31. package/dist/lib/modules/core/schema/errors.js +7 -5
  32. package/dist/lib/modules/core/schema/guest-invitation.js +13 -0
  33. package/dist/lib/modules/core/schema/index.js +5 -0
  34. package/dist/lib/modules/core/schema/ipc/consent.js +97 -18
  35. package/dist/lib/modules/core/schema/ipc/conversation-views.js +87 -0
  36. package/dist/lib/modules/core/schema/ipc/conversations.js +132 -0
  37. package/dist/lib/modules/core/schema/ipc/device.js +9 -0
  38. package/dist/lib/modules/core/schema/ipc/jelly.js +17 -45
  39. package/dist/lib/modules/core/schema/ipc/shapes.js +74 -73
  40. package/dist/lib/modules/core/schema/ipc/surfaces.js +12 -195
  41. package/dist/lib/modules/core/schema/ipc/tasks.js +31 -0
  42. package/dist/lib/modules/core/schema/ipc.js +68 -46
  43. package/dist/lib/modules/core/testing.js +1 -1
  44. package/dist/lib/modules/core/version.js +1 -1
  45. package/dist/lib/modules/ipc/client.js +10 -2
  46. package/dist/lib/modules/ipc/server.js +14 -5
  47. package/dist/lib/modules/ipc/testing.js +13 -23
  48. package/dist/spec/messaging-v1.md +854 -445
  49. package/dist/third-party-notices.md +1 -1
  50. package/dist/third-party.json +1 -1
  51. package/dist/types/core/agent/operations.d.ts +5 -52
  52. package/dist/types/core/agent/present.d.ts +6 -21
  53. package/dist/types/core/agent-tools.d.ts +295 -442
  54. package/dist/types/core/consent.d.ts +30 -7
  55. package/dist/types/core/contact-card.d.ts +3 -3
  56. package/dist/types/core/conversation/event.d.ts +6 -6
  57. package/dist/types/core/conversation/guest-invitation.d.ts +8 -0
  58. package/dist/types/core/conversation/log.d.ts +28 -13
  59. package/dist/types/core/conversation/request-log.d.ts +24 -0
  60. package/dist/types/core/conversation/request-seal.d.ts +6 -0
  61. package/dist/types/core/conversation/requests.d.ts +14 -0
  62. package/dist/types/core/conversation/seal.d.ts +32 -9
  63. package/dist/types/core/conversation-agent-tools.d.ts +939 -0
  64. package/dist/types/core/federation/directory-proof.d.ts +8 -0
  65. package/dist/types/core/federation/proof-state.d.ts +114 -0
  66. package/dist/types/core/federation/roster-proof.d.ts +7 -0
  67. package/dist/types/core/index.d.ts +11 -6
  68. package/dist/types/core/jelly-host.d.ts +1 -1
  69. package/dist/types/core/schema/common.d.ts +33 -6
  70. package/dist/types/core/schema/consent.d.ts +29 -8
  71. package/dist/types/core/schema/contact-request.d.ts +264 -0
  72. package/dist/types/core/schema/conv-frames.d.ts +1204 -0
  73. package/dist/types/core/schema/conversation.d.ts +1410 -264
  74. package/dist/types/core/schema/directory-frames.d.ts +8 -3
  75. package/dist/types/core/schema/envelope.d.ts +85 -130
  76. package/dist/types/core/schema/errors.d.ts +5 -1
  77. package/dist/types/core/schema/guest-invitation.d.ts +51 -0
  78. package/dist/types/core/schema/index.d.ts +5 -0
  79. package/dist/types/core/schema/ipc/consent.d.ts +464 -62
  80. package/dist/types/core/schema/ipc/conversation-views.d.ts +1199 -0
  81. package/dist/types/core/schema/ipc/conversations.d.ts +4355 -0
  82. package/dist/types/core/schema/ipc/device.d.ts +10 -0
  83. package/dist/types/core/schema/ipc/jelly.d.ts +16 -111
  84. package/dist/types/core/schema/ipc/shapes.d.ts +211 -95
  85. package/dist/types/core/schema/ipc/surfaces.d.ts +26 -427
  86. package/dist/types/core/schema/ipc/tasks.d.ts +210 -0
  87. package/dist/types/core/schema/ipc.d.ts +3859 -890
  88. package/dist/types/core/schema/relay.d.ts +12 -4
  89. package/dist/types/core/testing.d.ts +1 -1
  90. package/dist/types/core/version.d.ts +1 -1
  91. package/dist/types/ipc/client.d.ts +4 -1
  92. package/dist/types/ipc/server.d.ts +16 -1
  93. package/dist/types/ipc/testing.d.ts +21 -8
  94. package/package.json +1 -1
@@ -1,5 +1,68 @@
1
1
  # Messaging protocol v1 (`messaging/1`)
2
2
 
3
+ ## Conversation IPC cut-over (2026-11-10)
4
+
5
+ The live local API uses `CONVERSATION_METHODS` and `CONVERSATION_TIERS` from the normative core schema.
6
+ The old `msg.send`, `person.send`, `channel.*`, `search`, `conv.subscribe`, `agents.list` and `agents.read`
7
+ methods are removed, without aliases. `conv.list`, `conv.read` and batch `people.get`
8
+ use the conversation schemas. Agent access follows design §8.5: `conv.get`, `conv.members` and `file.get`
9
+ are human-only contracts. `file.get` remains staged. `sessions.list` is human-only and returns local sessions and their conversation seats.
10
+ `request.waiting` labels an authenticated outgoing hold `held`, and missing-receipt evidence
11
+ `pending-delivery`; missing receipts do not establish consent state. Contact requests remain visible while delivery is unconfirmed.
12
+ Only a verified recipient receipt changes a contact request to `held`. Internal change notifications carry
13
+ identifiers only; `conv.sync` re-reads verified state and selects safe rendered fields. Storage tables remain.
14
+ A line first readable after its key arrives streams as an `event`; `event.update` carries only edits, deletions, reactions, pins and delivery.
15
+
16
+ The daemon binds every agent call to a session registered on that connection. It rechecks membership and
17
+ session ownership after asynchronous rendering. Agents edit/delete only their own lines. Cursors bind to
18
+ the query and session; a daemon restart or engine replacement returns a stream gap. Replay renders current
19
+ verified state, including edits and deletions, rather than caching historical plaintext. Projection refuses
20
+ prefixes above 10,000 entries and responses above 512 KiB. These are interim safety bounds pending load tests.
21
+ Only `LIVE_CONVERSATION_METHODS` is served. Later-lane contracts stay staged and have no socket handler.
22
+ `contacts.list` returns verified conversation metadata: `cid`, `kind`, optional `name`, and optional `dmWith`.
23
+ Only accepted conversations appear. Contacts carry no transcript, read position, or unread count.
24
+ Channel contacts use the canonical `cid` as their address. Agent clients filter them through `conv.access`.
25
+ Shared core operations retain contacts, inbox reads, and tasks. Conversation clients use `conv.*` for sends, pages, and search.
26
+ Lane 4 omits read positions, preferences, unread counts and thread-follow state from views until lane 6 implements them.
27
+ Filters on that state (`conv.list` `unread` and `mentions`, `conv.threads` `unread` and `following`) are refused with `invalid_request`, never answered with an empty page.
28
+ `conv.backfill` verifies available relay history and returns `complete: false`. Completion requires an indexing coverage boundary that accounts for missing keys and expired entries.
29
+ `clientId` deduplication commits atomically with a queued line and survives engine replacement; reuse with
30
+ different content or a different destination is refused. Keyless conversation calls on a Jelly admin socket
31
+ require an action grant, even when the same method is available to a bound agent session.
32
+
33
+ Conversation maintenance coalesces verified membership changes for a random 0–30 second delay before answering
34
+ shared channel history. Only accepted participants qualify. Successful responses are remembered per membership,
35
+ epoch and recipient device set; failed or keyless responses remain retryable. The daemon sweep retries the
36
+ conversation outbox, schedules due epoch rekeys, and expires local message ciphertext/plaintext and search rows
37
+ while preserving digest links and control operations. Shutdown cancels response timers and waits for active work.
38
+
39
+ Tasks and delegation retain their envelopes and lifecycle until lane 7. Agent-tier `task.create` replaces
40
+ the task-creation arm of removed `msg.send`: it takes `key`, an agent `to`, `parts`, optional A2A fields,
41
+ `wake`, `idempotencyKey`, and `onBehalfOf`. It assigns the task ID and signs the existing delegation chain.
42
+ Agent-tier `task.continue` takes the same message fields and a required `taskId`, without `onBehalfOf`.
43
+ It sends the retained task-follow-up envelope only to that session's existing task executor, preserving
44
+ context, request binding, receipts, and terminal-state checks. Both return `{results: SendResult[]}`.
45
+ `task.list/get/update/cancel` are unchanged. Plain mail uses `conv.send`, never a task envelope.
46
+ The daemon rejects plain agent-mail envelopes, including previously held mail released after cut-over;
47
+ it retains the database rows but does not expose them through inbox or item reads.
48
+
49
+ The inbox combines retained task requests/follow-ups with verified incoming lines in accepted two-agent DMs
50
+ addressed to this local session. Plain legacy mail rows are not returned. For a line, `id`, `messageId` and
51
+ `eventId` equal the event ID, and `cid` names the conversation. `from.address` is the stable participant ID;
52
+ its device is the verified signing device, and its label is absent, never synthesized. Task items retain their device provenance.
53
+ `inbox.get/ack` and `item.read` accept an optional `cid`; an ambiguous ID requires it. The full `message`
54
+ projection contains the line text, while `cid/eventId` lets the client read the safe event and file references.
55
+ Edits are rendered on every read; deleted, inaccessible and unaccepted lines are not returned. `receivedAt`
56
+ is the first local observation; `bytes` counts UTF-8 text. Stages are durable, monotonic local acknowledgments.
57
+ Acknowledgments belong to the stable recipient participant and `(cid, eventId)`, not a process session key.
58
+ An agent restart with a new session key preserves these stages and does not repeat wake hints for acknowledged lines.
59
+ They do not manufacture envelope receipts. Recipient `ask` governs agent-DM admission: `open` accepts, `request` holds, and `no` silently declines.
60
+ After admission, lines in the accepted agent DM follow the answer path. A later person block suppresses them.
61
+ Person blocks also hide that person's events from conversation reads, search, previews and replay.
62
+ The verified log remains intact. Hidden lines do not contribute reply summaries or reactions to visible lines.
63
+ The push tier governs wake hints. New eligible lines use the existing session event and wake bridge; acknowledgments do
64
+ not re-notify. Agent reads carry the same audience-label effect as conversation reads.
65
+
3
66
  Status: v1. The Zod schemas in `packages/core/src/schema/` are the
4
67
  normative wire contract; this document gives the semantics. If the two disagree, fix one of them in
5
68
  the same change, and add a test vector.
@@ -56,7 +119,10 @@ Not in v1: hole-punched peer-to-peer transport, federation between relays, and p
56
119
  `messaging/1/person\n`, `messaging/1/dir-read\n`, `messaging/1/device-log\n`, `messaging/1/device-keys\n`,
57
120
  `messaging/1/device-link\n`, `messaging/1/epoch\n` and `messaging/1/dir-key\n`, and for conversations (§16) the
58
121
  event signature prefix `messaging/1/conv-event\n` and the hash prefixes `messaging/1/conv-genesis\n`,
59
- `messaging/1/dm\n` and `messaging/1/self\n`). A signature or hash for one purpose never verifies for another.
122
+ `messaging/1/dm\n` and `messaging/1/self\n`, and for contact requests and guest invitations
123
+ `messaging/1/contact-request\n`, `messaging/1/contact-request-content\n`, `messaging/1/contact-request-digest\n`,
124
+ `messaging/1/contact-request-decision\n`, `messaging/1/contact-authorization\n` and `messaging/1/guest-invitation\n`).
125
+ A signature or hash for one purpose never verifies for another.
60
126
  - **Hash:** SHA-256, base64url.
61
127
 
62
128
  ## 3. Identity
@@ -419,6 +485,14 @@ An envelope with an unknown `type`, or a newer `v`, is acknowledged to the relay
419
485
  update without `replyTo` fails the shape check of §5 (`invalid_request`). The requester's daemon
420
486
  drops an update whose `replyTo` is not its record of the request (`invalid_request`).
421
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.
422
496
  - **What the relay sees.** It can link a task request to its updates, and count and time them. It
423
497
  learns no content and no state: the state is sealed. This is the correlation a relay needs to
424
498
  match a wake ticket to awaited answers.
@@ -475,7 +549,9 @@ Only the executor changes state (see `canTransition` in core).
475
549
  still run, and the person is away from that device. The host says so with `person.attached`; a Jelly host's device is
476
550
  away until its first `person.attached {attached: true}`, and no other device ever says `away`. A receiver shows that
477
551
  device as away (`people.get`), and an `away` older than the online window (§15.9: 150 s) means nothing.
478
- - `features` advertises optional capabilities. v1 defines `"tasks"`. 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.
479
555
  - `label` must equal the sender's roster label. Receivers ignore a presence whose label differs, and
480
556
  always show the roster's label.
481
557
  - Like every envelope, presence carries the sender's `anchor` in its header (§5). Presence is
@@ -541,7 +617,7 @@ forwards a credential it was handed.
541
617
  - `exp`, the earliest along the chain.
542
618
 
543
619
  The requester's record holds the chain it sent.
544
- - **Delegating (§8.2).** A session delegates with `msg.send {kind: "task", onBehalfOf}`. Only
620
+ - **Delegating (§8.2).** A session delegates with `task.create {onBehalfOf}`. Only
545
621
  the sending daemon signs, and clients never see or build grants.
546
622
  - Without `onBehalfOf`, a task starts a new chain whose root is the sending session.
547
623
  - A root grant made over IPC is unrestricted: no `scope`, and `maxDepth = LIMITS.delegationDepth`.
@@ -575,107 +651,26 @@ forwards a credential it was handed.
575
651
  code: fenced in a model context, `terminalSafe` in a terminal, `cleanPeerText` in a permission
576
652
  prompt. A client shows a chain only when it adds something beyond `from`: depth > 1 or a scope.
577
653
 
578
- ### 6.8 Chats: DMs and channels
579
-
580
- A chat is a direct human message (room `dm:<person>`) or a channel (room `channel:<name>`). A
581
- channel has an id (a ULID chosen by its creator), a name (`ChannelName`), its creator, and a list of
582
- persons (`PersonId`s: roster person names, or hosted `p_…` ids in a directory realm), at most
583
- `LIMITS.channelPersons` including the creator. There is no group key and no chat
584
- between sessions: agent mail stays in the inbox and tasks.
585
-
586
- - **Wire.** A chat envelope is a sealed `message` with `kind: "message"`, addressed to a machine
587
- with no agent (`to: {person, device}`) (§5, "Meaning"). A DM line carries no `conv`; a channel change
588
- or post carries `conv: {kind: "channel", op, id, name, …}`. `message.messageId` is a ULID, the same on
589
- every copy. A reply in a thread sets `message.contextId` to the `messageId` of the thread's first
590
- line. A chat envelope is never `kind: "task"`, never has a `taskId`, and a
591
- `message` to an agent never has a `conv` (`invalid_request`). `from.agent` is the session that
592
- posted the line; a person's own line has no `from.agent` (§5, "Ends"). `name` on a channel op is
593
- always the channel's own name.
594
- - A DM line: one copy to each machine of the person, and none to the sender's own machines.
595
- - A DM to oneself (`dm:<own person>`, the person's own conversation, "me"): one copy to each of the
596
- person's other machines. A line one of the person's agents wrote there is for the person, so every
597
- machine files it as incoming, unread; a line the person wrote is outgoing. Its lines carry no
598
- delivery `state`: there is no other person to deliver them to.
599
- - `create {id, name, persons, creator}`: the full record, one copy to each member machine but the
600
- sender's. `persons` includes the sender; `creator` is the person who created the channel, so an
601
- invitee learns it from the inviter's `create`.
602
- - `invite {id, name, persons}`: the persons added, to the machines of the existing members. Each
603
- machine of an added person gets a `create` with the full record instead. Persons with no
604
- machine left on the roster are not in that record.
605
- - `leave {id, name}`: the sender left, to every member machine.
606
- - `remove {id, name, persons}`: the persons removed, to every member machine, the removed
607
- persons' included.
608
- - `post {id, name}`: a line, to every member machine.
609
- - The text of a channel op is not filed. Each machine files its own notice line for an op it
610
- applies, written by the daemon from the op's fields, with the op's `messageId` (§8.2 `notice`).
611
- - **Receiver rules.** Channels are looked up by `id`, never by name, and an op's `name` must match
612
- the channel's own name (`not_found`). Where a rule fails, nothing is stored and the sender gets a
613
- `rejected` receipt.
614
- - `create` is accepted only for an unknown `id`, only when the sender and this machine's person are
615
- both in `persons` (`forbidden`), and not for an id this machine
616
- left or was removed from within the last `LIMITS.ttlMs + LIMITS.clockSkewMs` (`forbidden`). A
617
- machine holds at most a policy number of channels (the reference daemon: 200); a `create`
618
- beyond it is `rate_limited`. If this machine already holds a channel with that name, it keeps
619
- the new one under a local name: the name cut to 28 characters, a hyphen and the lowest free
620
- number from 2 (`eng-2`), or `rate_limited` when 2 to 99 are all taken. Room references
621
- (`channel:<name>`), `conv.*`, `search`, `person.send` and `channel.*` use the local name; envelopes always carry the
622
- channel's own name. This also covers two people creating the same name at once.
623
- - A `create` or `invite` naming a person this machine cannot admit is `not_found`: in a roster realm a
624
- person with no current machine; in a directory realm any id that is not a hosted person. A directory
625
- home takes persons it has not verified yet, then looks them up and watches them (§15.9).
626
- - `invite` is accepted only from a current member, and only when the channel stays within the
627
- limit (`forbidden`, `invalid_request`).
628
- - `leave` removes only the sender. When this machine's person leaves (from another of its
629
- machines), the channel and its lines are deleted here.
630
- - `remove` is accepted only from a member who is the channel's creator or a team admin on the
631
- verified roster (`forbidden`), never naming the sender (`invalid_request`: use `leave`), and only
632
- for current members (`not_found`). A machine whose person is removed deletes the channel and its
633
- lines, as on `leave`, and refuses a `create` for that id as above. Other machines remove those
634
- persons from the channel.
635
- - `post` is filed only when the sender and this machine's person are both members (`forbidden`).
636
- - Every name in `persons` must be a person on the roster (`not_found`). All of this is checked
637
- before anything is stored.
638
- - A DM line from the receiver's own person is filed in its own conversation, as above.
639
- - A person who has no machine left on the roster is no longer a member of any channel; each
640
- machine drops them when it applies the removal.
641
- - **Delivery.** An accepted chat envelope gets a `delivered` receipt. It creates no inbox item, does
642
- not count against the inbox quota (§8.3) and never wakes a session. Consent (§6.9) and the inbound
643
- rate limit still apply. The sender records each chat envelope it routes with its line and
644
- recipient. A receipt from that recipient's machine, a relay nack, the recipient leaving the roster
645
- or expiry settles the record, once. An outgoing line is `delivered` once every other person has a
646
- delivered copy, and `refused` once some person's every copy was rejected or expired; until then
647
- it is `sent`. When every copy of a channel change to one person is refused, the sender files a
648
- notice line in that channel naming the person and the code. Notice lines are never sent.
649
- - **Transcript.** Each daemon keeps one row per line, keyed by `messageId`; a later envelope with a
650
- `messageId` this machine already holds is not filed and changes nothing. The thread key is the
651
- first line's `messageId`. Text is the line's text parts joined by newlines, cut to
652
- `LIMITS.chatChars` UTF-16 code units without splitting a surrogate pair. The sender files its own
653
- line with the plaintext it sealed; that copy is not on the wire. A person's line that arrives from
654
- another of their machines is filed as outgoing. Retention deletes lines after
655
- `retention.transcriptDays` (default 30), and a per-person cap deletes a sender's oldest lines
656
- first once they hold more than a policy number of lines or bytes (the reference daemon: 10,000
657
- lines or 16 MiB of UTF-8 text).
658
- - **Order and read state.** Each daemon numbers its chat changes with one counter that only
659
- rises and is never reused (`seq`, from 1): every filed line takes the next number, and so does
660
- every later change of a line (its delivery state) and every channel deleted here. A line keeps the
661
- number it was filed with as its `seq`, its place in this machine's order; another machine numbers
662
- the same line differently. Paging and read cursors use it. Read state is per machine and per room:
663
- a cursor (`readUpTo`, 0 at first) that only moves forward and never past the newest number here.
664
- A line is unread when another person's line, or the daemon's notice of another person's change,
665
- was filed after the cursor; this person's own lines never are. Leaving a channel, or being removed
666
- from it, deletes its cursor with its lines. Nothing syncs read state between machines.
667
- - **Joining later.** A person invited later does not receive earlier lines. Membership is each
668
- machine's own view: concurrent invites or removals can leave machines with different member
669
- lists, and nothing reconciles them. A post refused because of that shows as `refused` to its
670
- sender.
671
- - **Reading.** `conv.list`, `conv.read` and `search` (§8.2) see the DMs this machine holds and
672
- the channels its person is in. Agent-facing clients fence chat text as peer content and ask
673
- before reading it.
654
+ ### 6.8 Conversations: DMs, groups and channels
655
+
656
+ Conversations use the signed log and sender-key encryption in §16. A `cid` identifies a conversation across devices.
657
+ The daemon verifies each prefix before it exposes membership, content or delivery state.
658
+ Legacy chat envelopes and room-name APIs are removed. Existing database tables remain for store compatibility.
659
+
660
+ - **Participants.** A participant is a person or a stable `person/agent` address. Verified bindings identify each signing device.
661
+ - **Admission.** An invitee receives authenticated controls and message digests before acceptance, but no ciphertext or keys.
662
+ Agent-DM admission follows the receiving person's `ask` level (§6.9).
663
+ - **History.** The signed history policy controls access after acceptance. Recovery replaces matching invitation digests with authenticated entries.
664
+ It survives restart and does not restore content deleted by later retention.
665
+ - **Delivery.** Conversation sends return `pending` until verified append evidence arrives. Missing receipts never establish an outgoing consent hold.
666
+ - **Reading.** `conv.list`, `conv.read`, `conv.search` and `conv.context` expose current verified projections.
667
+ Agent reads require a registered session, access checks and the audience guard. Clients fence peer content (§12).
668
+ - **Read state.** Lane 4 omits read positions, unread counts, preferences and follow state. Their methods remain staged until later lanes implement them.
674
669
 
675
670
  ### 6.9 Consent
676
671
 
677
672
  Consent belongs to the receiving person. A person's **grants** say what each other person, and the
678
- members of each channel, may send them and their agents. A device of the person signs each grant,
673
+ participants of each conversation, may send them and their agents. A device of the person signs each grant,
679
674
  only the person's own devices hold it, and the receiving daemon enforces it. Agents never sign or
680
675
  change a grant.
681
676
 
@@ -694,7 +689,7 @@ A roster realm has only `self` and `org`.
694
689
 
695
690
  ```ts
696
691
  { v, realm, id: Ulid, person, by: MachineId, ts,
697
- subject: { person } | { channel: <channel id> },
692
+ subject: { person } | { conv: ConvId } | { relation: "outside" | "org" },
698
693
  chat?: "open" | "muted" | "blocked", // lines from the subject (person subjects only)
699
694
  agentMail?: "open" | "blocked", // lines from the subject's agents
700
695
  ask?: "open" | "request" | "no", // mail and tasks from the subject or their agents to the person's agents
@@ -706,8 +701,10 @@ A roster realm has only `self` and `org`.
706
701
 
707
702
  - `sig` = Ed25519(`by`, `"messaging/1/grant\n"` ‖ JCS(grant without `sig`)). `by` is a device of `person`.
708
703
  - `person` and a person subject are `PersonId`s: a roster person name, or a hosted realm's `p_…` id (§15.1).
709
- - A channel grant sets only `ask` and `agentMail`. It applies to a sender only while the sender is a
710
- member of that channel in the receiver's own view (§6.8).
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`.
707
+ For an event-specific decision, membership is evaluated at that event's position.
711
708
  - `trustedUntil` needs `ask: "open"` and is at most `ts + LIMITS.trustMaxMs` (90 days). Trust is set
712
709
  for `LIMITS.trustDefaultMs` (30 days) when the person names no end. An expired `trustedUntil` reads
713
710
  as absent.
@@ -742,8 +739,9 @@ the person:
742
739
  **Effective policy.** For a sender S, each level is, in order:
743
740
 
744
741
  1. the person grant for S, where it sets the level;
745
- 2. otherwise, for `ask` and `agentMail`, the most permissive among the grants of channels S is in;
746
- 3. otherwise the default for the relation:
742
+ 2. otherwise, for `ask` and `agentMail`, the most permissive grant of a verified conversation shared with S;
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:
747
745
 
748
746
  | Relation | `chat` | `agentMail` | `ask` | trust |
749
747
  |---|---|---|---|---|
@@ -752,8 +750,70 @@ the person:
752
750
  | `contact` | open | blocked | no | off |
753
751
  | `stranger` | `request_required` | blocked | no | off |
754
752
 
755
- Only the person grant trusts. A `blocked` chat level also refuses agent mail, asks and channel changes
756
- 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.
757
817
 
758
818
  **Contact cards.** A person's card tells another person what they may send the person. The schema is
759
819
  `ContactCard` in `schema/consent.ts`:
@@ -767,7 +827,7 @@ from S.
767
827
  - `sig` = Ed25519(`by`, `"messaging/1/card\n"` ‖ JCS(card without `sig`)). `by` is a device of `from`; `from` and
768
828
  `to` are different persons. `trustedUntil` needs `ask: "open"`. A card is at most `LIMITS.cardBytes` of JCS.
769
829
  - `can` is the person grant for S over the defaults of S's relation to R (core's `cardCan`): `chat` is true for
770
- `open` and `muted`, `agentMail` for `open`. Channel grants are not on a card: S knows its own channels.
830
+ `open` and `muted`, `agentMail` for `open`. Conversation grants are not on a card.
771
831
  - `seq` rises with every card R's devices send S; S keeps the card with the highest `seq`.
772
832
  - Core's `verifyContactCard` checks the schema, the rules, the size, the realm, both persons and the signature over
773
833
  the raw value. Whether `by` is a current device of R is the caller's: a card signed by a device R no longer has
@@ -782,11 +842,11 @@ from S.
782
842
  | Anything from the receiver's own person | Deliver, `self` |
783
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` |
784
844
  | Anything else, while `chat` is `blocked` | Refuse, `refused` |
785
- | A DM line or channel post by the person S | `chat`: `open` delivers, `muted` delivers silently, `request_required` refuses with that code |
786
- | 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) |
787
- | A channel change | Deliver; a `stranger` is refused with `request_required` |
788
- | 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` |
789
- | 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 |
790
850
 
791
851
  - **Trust and the chain rule.** An ask runs `trusted` only while the person grant's `trustedUntil` is
792
852
  past now and every person in its delegation chain (§6.7) is S or the receiver. Otherwise it runs
@@ -824,40 +884,27 @@ from S.
824
884
  A delivered request then passes the inbound policy of §8.3 as any other;
825
885
  - a request ends unanswered at its expiry: the sender's own expiry settles its side. A request
826
886
  whose sender has left the realm ends at the answer, with nothing sent.
827
- - **Message requests** (a directory realm only). A person of one org first writes to a person of another with a
828
- message request: a `message` of class `request` with `conv: {kind: "request"}` and one text part of at most
829
- `LIMITS.requestChars` (§5, "Meaning"). Only a person sends one, with the human-tier `contact.request` (§8.2);
830
- agents never send or answer one.
887
+ - **Message requests** (a directory realm only). Human first contact across orgs uses `contact.request` and a portable request proof (§16).
888
+ Its `message` envelope carries `conv: {kind: "request", proof}` and one text part, bounded by `LIMITS.requestChars`.
889
+ The envelope expiry must equal the signed descriptor expiry. Agents cannot send or answer contact requests.
831
890
  - **Addressing.** The person gives an exact email. The daemon resolves it at the directory (`dir.resolve
832
891
  {email}`, §7); no answer is `not_found`, "no Messaging user with that email accepts requests", the same for an
833
892
  unknown and an undiscoverable email, so it cannot tell them apart. Several persons for one email is
834
893
  `ambiguous_address`.
835
- - **Sending.** The request goes to every current device of that person, one copy each, and is filed as the
836
- person's own line in `dm:<person>`. Its delivery state (§6.8) is `held` while each copy not yet delivered is
837
- held for the other person, `delivered` once they accept, `refused` once they decline. Before it sends, the
838
- daemon sets the person's own grant for the other to `chat: "open"` when it sets no chat level, so the answer,
839
- the card an accept sends and the other's lines reach the person; a person who blocked the other is refused
840
- `invalid_state`. The daemon paces before it sends (`rate_limited`): one request waits per person until that
841
- person writes back (or for `LIMITS.ttlMs`), and at most `LIMITS.requestsPerDay` persons are asked a day. The
842
- relay paces the same by the header (§15.10); its `rate_limited` nack of a request is final, not retried, and
843
- refuses the line.
844
- - **Receiving.** `decide` holds a request from a `stranger`; from anyone else it is a DM line by the sender, and
845
- from a blocked sender it is `refused`. A held request is stored with its envelope and opened payload (§8.8,
846
- `contact_requests`), one per sending person and at most `LIMITS.heldTotal` in all (`rate_limited`), answered with a
847
- `held` receipt and filed as a `request` notice (§8.2). It reaches no agent and wakes nothing. A request in a
848
- roster realm is `invalid_request`: a roster realm is one org.
849
- - **Answering** (`request.answer`): `accept` sets the sender's grant to `chat: "open"` (keeping its other
850
- levels), which makes the sender a `contact` and sends them a card, then files the request's text as their
851
- first line in `dm:<sender>` and answers `delivered`; `decline` answers `rejected {code: "declined"}`; `block`
852
- sets `chat: "blocked"`, then declines. A request ends unanswered at its expiry. Every device of the person
853
- holds its own copy: a grant for the sender that another device of the person signed, arriving by "Sync", settles
854
- the copy held here as its answer did (`chat: "open"` accepts it, `blocked` declines it).
894
+ - **Sending.** The daemon sends one copy to each current recipient device and returns `{id,cid,person}`.
895
+ It stores the request separately from conversation history. An authenticated recipient receipt establishes an outgoing hold.
896
+ A missing receipt means pending delivery, not a hold. One request can wait per person, subject to `LIMITS.requestsPerDay`.
897
+ - **Receiving.** The daemon verifies the portable proof, envelope binding and expiry before it stores the request.
898
+ A stranger's request waits for the person. An allowed contact or same-org request follows the signed acceptance and import path automatically.
899
+ A blocked sender is refused. The opening reaches no agent and triggers no wake before or after import.
900
+ - **Answering.** `request.answer {answer:"accept"}` settles a signed mailbox decision, grants chat access, and imports exactly one opening into the deterministic DM.
901
+ The import uses the descriptor ID and permanent completion evidence. Concurrent accepting devices reconcile against the same verified operations and completion.
902
+ `decline` settles a signed decline. `block` also sets `chat:"blocked"`. A grant or card alone is not a terminal decision.
903
+ The daemon resumes interrupted acceptance after restart and sends a contact card after successful import.
855
904
  - **The sender's card is the directory's.** Clients show the sender of a request by the realm's verified profile
856
905
  (name, email, org, §15.1), never by the request's text.
857
- - **Agents.** An agent that needs a contact in another org asks its own person: a line to the person with
858
- `request: {contact: <email>}` (`msg.send`, §8.2). The daemon files the line and a suggestion, listed by
859
- `request.list` and filed as a `request` notice; the person sends the request with `contact.request`
860
- (`suggestion` dismisses it), or dismisses it with `request.answer {answer: "decline"}`.
906
+ - **Agents.** Legacy contact suggestions remain dismissible. The removed `msg.send` method cannot create new suggestions.
907
+ Agent-to-agent DMs use recipient invitation admission instead of human contact requests.
861
908
  - **Cards on the daemon.** A device sends a card (a `message` with `conv: {kind: "card", what: "card"}`) to each
862
909
  current device of a person S when this person's grant for S changes on this device (`grant.set`, an answer to
863
910
  a request, a re-sign under "Signers"), and when S asks with `card.get`. It sends none to a `stranger` and none to
@@ -868,14 +915,7 @@ from S.
868
915
  person; it answers nothing. A `rejected` receipt from a person with `refused`, `request_required` or `declined`
869
916
  drops their card and asks for a fresh one. The `seq` of a device's cards is its clock in milliseconds, raised
870
917
  past the last one it used. A card is not a first message for the freshness bound (§15.9).
871
- - **Pacing.** Lines from one person's agents to another person (A2H), summed over those agents, are
872
- limited to `LIMITS.a2hPerHour` (30) an hour and `LIMITS.a2hPerDay` (100) a day:
873
- - the sending daemon refuses an agent's DM line or channel post at send time (`msg.send` to a person
874
- or `#channel`), before it stores or sends anything, when it would take any recipient person past either
875
- count, with `rate_limited` and when to try again; the person's own lines are not paced;
876
- - the receiving daemon refuses an agent's line past either count from one sending person with a
877
- `rejected` receipt, `rate_limited`, for a sender that does not pace itself;
878
- - the reference daemon counts in memory, so a restart starts the windows again.
918
+ - **Pacing.** Conversation append limits replace legacy chat-envelope pacing (§16). Retained task envelopes still follow the relay and inbound limits (§7, §8.3).
879
919
  - **Sync.** A device sends each grant it signs or re-signs to each of its person's other current
880
920
  devices: a sealed `message` to the device with no agent, `conv: {kind: "self", what: "grant"}`,
881
921
  whose one data part is `{grants}`. A device of the person that is new to the sender (one it has
@@ -1210,7 +1250,7 @@ hosted `hello` as a malformed one (4001).
1210
1250
  | `dir.watch {persons}`, at most `LIMITS.watchPersons` (500) | `dir.ok {}` |
1211
1251
  | `presence.watch {persons}`, at most `LIMITS.watchPersons` | `dir.ok {}` |
1212
1252
  | `dir.erase {entries, token}`: an `unbind` of each current device, at most `LIMITS.devicesPerPerson` (10), and a compact JWS | `dir.ok {}` |
1213
- | `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 {}` |
1214
1254
  | `wake.disarm {}` | `dir.ok {}` |
1215
1255
 
1216
1256
  - **`dir.sthIs`.** The STH at `size`, or the latest. `head` is its last epoch-log leaf (index `size − 1`) with
@@ -1246,7 +1286,7 @@ hosted `hello` as a malformed one (4001).
1246
1286
  daemon arms it at its relay (`wake.arm`, §8.11). While that device is offline, the relay may present the ticket to
1247
1287
  Jelly ID, which wakes the host.
1248
1288
 
1249
- - **`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:
1250
1290
  - `url` is Jelly ID's wake endpoint of this device's host, `https://<issuer host>/hosts/<hst>/wake` (`WakeUrl`): its
1251
1291
  origin must be the realm's Jelly ID issuer's (`invalid_request`), and `<hst>` the `hst` of the Jelly token of this
1252
1292
  device's latest `bind` or `attest` that names one (`forbidden` otherwise, and for a device no host bound);
@@ -1254,20 +1294,38 @@ Jelly ID, which wakes the host.
1254
1294
  unverified (`readWakeTicket`), only the header's `typ` and `alg` (`jelly-wake+jwt`, `EdDSA`), and the claims `exp`
1255
1295
  (it must be in the future and at most seven days and 60 s ahead), `aud` (when present, exactly `url`) and `sub`
1256
1296
  (when present, exactly `<hst>`); any other is `invalid_request`;
1257
- - `agents` (at most 64 session names) are the device's sessions whose own person's mail may wake the host, and
1258
- `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.
1259
1301
 
1260
1302
  It replaces the device's earlier ticket, and is kept in the relay's store (`dir_wakes`, §8.8) until it is
1261
1303
  used, disarmed, its device is unbound or erased, or it expires. At most 60 arms a device in a burst, then one a
1262
1304
  minute (`rate_limited` with `retryAfterMs`). `wake.disarm {}` forgets the device's ticket.
1263
- - **When the relay presents it.** Only when the relay has just taken (§7 "Routing": first seen, queued) an envelope for
1264
- the device while the device has no connection, and the envelope is:
1265
- - the person's own mail to an armed agent: header type `message`, `from.person` = `to.person`, and `to.agent` in
1266
- `agents`; or
1267
- - an answer the device awaits: header type `task.status` or `task.artifact` whose `replyTo` is in `awaiting`.
1268
-
1269
- Lines and posts (no `to.agent`), receipts, presence, task requests and cancels, anyone else's mail, a message that
1270
- merely names an awaited request, and anything for a connected device never wake. A request the recipient's person
1305
+ - **When the relay presents it.** Only while the device has no connection, and only for one of these:
1306
+ - **a conversation line by the device's own person to an armed agent.** The relay has just stored (`conv.ok`, §7
1307
+ "Conversation frames") a `msg` event whose `wake` hint (§16.2) names a session in `agents`. The person is the one
1308
+ of the authenticated device that appended the event (which `by.person` must match), never the event's claims
1309
+ alone, and only that person's current devices are considered. A person's own mail to their agent is such a line
1310
+ (design §8.2, "Wake"): their daemon sets `wake` to the sessions of their own agents a line addresses, a dm whose
1311
+ other end is one of them or a mention of one, and never names another person's session. A `wake` hint on another
1312
+ person's event reaches only that person's own devices, so it never wakes anyone else's host. A repeated append
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;
1322
+ - **an envelope** the relay has just taken (§7 "Routing": first seen, queued) for the device that is:
1323
+ - the person's own mail to an armed agent: header type `message`, `from.person` = `to.person`, and `to.agent` in
1324
+ `agents` (a task request, which keeps its envelope); or
1325
+ - an answer the device awaits: header type `task.status` or `task.artifact` whose `replyTo` is in `awaiting`.
1326
+
1327
+ Conversation events without a `wake` hint (posts, receipts, another person's lines) outside an armed dm, presence, task cancels, anyone
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
1271
1329
  must answer (§6.9) is held by the receiving daemon, which the relay cannot see: such mail never wakes unless it is
1272
1330
  the person's own.
1273
1331
  - **Presenting.** `POST <url>` with the form `client_assertion_type=urn:jelly:wake&ticket=<ticket>`, no redirect
@@ -1280,8 +1338,8 @@ Jelly ID, which wakes the host.
1280
1338
  - `503`, any other 5xx, or no answer: kept, and presented again a minute later if the device is still offline. That
1281
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
1282
1340
  relay's clock and its timers drift; new traffic within the minute is.
1283
- - **Never logged.** The ticket is in no log line; a presentation is logged with the device, why (`own`, `awaited`,
1284
- `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.
1285
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
1286
1344
  host once, before the ticket's `exp`: Jelly ID spends its `jti` on the wake, checks that it is that host's (its
1287
1345
  signature by the host's enrolled key, `iss`, `sub` and `aud`), and holds every host to its budget (6 an hour, 48 a
@@ -1289,9 +1347,83 @@ Jelly ID, which wakes the host.
1289
1347
  waking a host gives the presenter nothing on it: the host's daemon then reconnects and fetches its mail as usual.
1290
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
1291
1349
  learns that the host sleeps (it already sees the device go offline), which agents may wake it (presence already
1292
- 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
1293
1352
  fail to wake a host, as it can drop any envelope; that delays the person's mail, it loses none.
1294
1353
 
1354
+ **Conversation frames** (`schema/conv-frames.ts`; §16). After `hello`, in a roster realm and in a directory realm
1355
+ alike, the relay sequences conversation logs. It is never trusted for authority: it sequences and filters, and
1356
+ members verify every entry (§16.4, §16.7). Every frame is strict; a refusal is `nack {id: req, code, message}`.
1357
+
1358
+ - **`conv.append {req, event}`**, an `op` or a `msg` (a `keys` event is `invalid_request`: shares go in
1359
+ `conv.share`). The relay checks, in order:
1360
+ 1. the frame's schema and `LIMITS.convEventBytes` (`too_large`); `by.device` is the authenticated device
1361
+ (`forbidden`);
1362
+ 2. the rate limits below (`rate_limited` with `retryAfterMs`);
1363
+ 3. a `create` against an empty log first, so a person who could not create it learns nothing of an existing one;
1364
+ then a `create` of an existing conversation is `conv_conflict`;
1365
+ 4. a person who is not a participant person of an existing conversation gets `not_found`, as for a conversation
1366
+ that does not exist (no existence oracle for derived dm ids), except for a `join`;
1367
+ 5. `ConvLog.apply` against the relay's copy of the log at its head, with the live checks (§16.7, step 4): `ts`
1368
+ within the clock skew, `by.device` a current device of `by.person`, and the org of a person for `join` and
1369
+ `add`. In a directory realm the devices and orgs are the directory's (§15.10); in a roster realm they are the
1370
+ roster's, and the realm is one org, its team id. An `op` whose `opHead` is not the current op head is
1371
+ `conv_conflict`; a `msg` under another epoch, or while a rekey is due, is `stale_epoch`;
1372
+ 6. every person a `create` or `add` names has a current device in the realm (`not_found`), and no person would
1373
+ take part in more than `LIMITS.convsPerDevice` (5000) conversations (`too_large`).
1374
+
1375
+ It then assigns `seq`, stores the entry and answers `conv.ok {req, cid, seq, hash}` once it is committed (the
1376
+ group commit, as for `send`), and pushes `conv.push {cid, entry}` to the other online devices of every participant
1377
+ person before and after the entry, so a removed person's devices see their removal. A repeat of an event the log
1378
+ holds (the same JCS) is answered `conv.ok` again, with its first `seq`; another event with a used `id` is
1379
+ `duplicate`.
1380
+ - **`conv.share {req, events}`**: 1 to `LIMITS.convSharesPerFrame` (100) key shares (§16.6), all of one
1381
+ conversation and by the authenticated device. Each must pass `ConvLog.checkShare` live, with `opHead` the current
1382
+ op head (`conv_conflict`), so a person removed since gets none. All or none are stored; the relay answers
1383
+ `conv.shared {req, cid, count}` and pushes `conv.push {cid, share: {n, event}}` to each recipient device online,
1384
+ and to no other device. A previously stored share repeated with identical JCS is acknowledged without storing or
1385
+ pushing it again, even if the op head has since advanced. A used id with different content is `duplicate`. A new
1386
+ share must still pass all current checks; the repeat rule never delivers an old share to a removed device.
1387
+ - **`conv.fetch {req, cid, after, limit?, sharesAfter?}`** → `conv.entries {req, cid, head, entries, shares, more}`:
1388
+ the entries after `after` in `seq` order, at most `limit` (`LIMITS.convFetchEntries` (500)) and as many as fit in
1389
+ three quarters of a frame (at least one). An expired `msg` is served as `{seq, prev, digest}`; an op never is. With
1390
+ `sharesAfter`, also this device's shares after that `n`; no other device's share is ever served. `more` says
1391
+ there is more of either. Only a device of a participant person may fetch (`not_found` otherwise).
1392
+ - **`conv.head {req, cids? | after?}`** → `conv.heads {req, heads: [{cid, head, opHead, shares}], more}`: the heads
1393
+ of the named conversations this device's person takes part in, or, without `cids`, of all of them by `cid` after
1394
+ `after`, at most `LIMITS.convHeads` (500) each way. `shares` is the last share `n` for this device (0: none). A
1395
+ device that comes back online asks this first, then fetches what moved.
1396
+ - **`conv.caps {req, persons}`** → `conv.capabilities {req, outdated, unknown?}`: of at most `LIMITS.convOpParticipants` named
1397
+ persons, those every one of whose current devices with a stored, unexpired presence names no `conversations`
1398
+ feature: a daemon from before conversations, which reads no log. A person with no such presence is not named, since
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,
1400
+ group or channel with that person, adds them, or sends them a message request, a daemon asks, and refuses with
1401
+ `peer_outdated` ("<handle> needs to update Messaging"), creating and sending nothing. It reveals one bit about a
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.
1410
+ - **Retention.** Once a minute the relay prunes the bodies of `msg` entries older than the conversation's
1411
+ `retainDays` (as of its head), by the time it sequenced them, keeping each entry's `seq`, `prev` and digest
1412
+ (§16.4), and deletes the key shares older than it. It never prunes an op.
1413
+ - **Limits** (per conversation and device, in memory; a restart refills them):
1414
+ - appends by one device to one conversation: a burst of `LIMITS.convAppendBurst` (20), refilled at
1415
+ `LIMITS.convAppendsPerMinute` (60) a minute; by all its devices, `LIMITS.convAppendsPerConvPerMinute` (600) a
1416
+ minute;
1417
+ - `rekey` ops whose cause the log cannot prove (`device`, `schedule`): `LIMITS.convRekeysPerHour` (4) per device
1418
+ per conversation an hour;
1419
+ - creates: `LIMITS.convCreatesPerHour` (60) per device an hour;
1420
+ - key shares: `LIMITS.convSharesPerMinute` (10,000) per device per conversation a minute, and at most
1421
+ `LIMITS.convSharesPerPair` (256) kept from one sender device for one recipient device in one conversation
1422
+ (`rate_limited`), so one member cannot fill another's shares.
1423
+ - **What the relay learns.** The plaintext ops (participants, roles, settings, epochs, policies), the kind of each
1424
+ sealed op (`meta`, or a `setting`'s kind), sizes and timing. Never a message body, a name, a topic, a setting's
1425
+ content or a key.
1426
+
1295
1427
  **What the relay can see.** Team ids, machine ids, persons, session names, timing, and presence:
1296
1428
  its session names, agent kinds, states and tiers, and the repo, branch and doing text a machine
1297
1429
  chooses to share (§6.5, none by default). It never sees message, task or receipt bodies.
@@ -1355,6 +1487,63 @@ chooses to share (§6.5, none by default). It never sees message, task or receip
1355
1487
 
1356
1488
  ### 8.2 IPC API
1357
1489
 
1490
+ **Conversation render schemas.** Core exports `ParticipantView`, `NoticeView`, `FileView`, `TaskView`, `EventView`, `ConvView`, `ActivityBody` and `ConvStreamEvent` (§16).
1491
+ Names and content are untrusted peer data. A `FileView` exposes no attachment key or nonce.
1492
+ A deleted `EventView` contains neither text nor files. Lane 4 omits read positions, preferences and unread counts.
1493
+ Stream records carry a daemon-local `pos`. An `event.update` names the conversation and event of its embedded projection.
1494
+ Stream record types are `event`, `event.update`, `conv`, `member`, `read`, `typing`, `activity`, `gap` and `task` (person-level readers only, "The person's tasks").
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`.
1496
+ A `request.waiting` item's state is one of `held`, `pending-delivery`, `resend` and `outdated`.
1497
+ The daemon re-reads verified state before it sends a projection.
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
+
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`.
1530
+
1531
+ **Live conversation methods.** `LIVE_CONVERSATION_METHODS` is part of `IPC_METHODS`. The tier table applies on both sockets.
1532
+ Batch `people.get` accepts up to 100 distinct person IDs and returns `{people, missing}` without device lists.
1533
+ `people.get` and `sessions.list` work without a connected conversation engine.
1534
+ Human-only `request.waiting` reports authenticated outgoing holds separately from pending delivery. `request.list` reports incoming requests.
1535
+ A `conv.send` names exactly one of `cid` and `to`. `broadcast` requires a thread.
1536
+ A page chooses at most one position. Agent calls require a session registered on their connection.
1537
+ The daemon enforces access, audience consent and own-line restrictions after asynchronous work too.
1538
+ Creation requires two DM participants or 3–50 group participants. Only channels have visibility. Only agents can be seated.
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").
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`.
1544
+ `conv.backfill` returns `complete: false` until indexing coverage accounts for missing keys and expired entries.
1545
+ The method schemas and their error arrays are normative. Malformed JSON-RPC and params use JSON-RPC errors.
1546
+
1358
1547
  The API is the method table `IPC_METHODS` in `schema/ipc.ts`: JSON-RPC 2.0 NDJSON on two Unix
1359
1548
  sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (for example
1360
1549
  `contacts.list`, `item.read` and `task.get`), the TSDoc in that file and in `schema/ipc/*.ts` is normative.
@@ -1367,8 +1556,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1367
1556
  - **Two sockets.** The daemon serves the one API on two sockets, and itself enforces what each
1368
1557
  serves (its tiers). A client's own method list is a second check, not the boundary.
1369
1558
  - `run/agent.sock` (the agent tier) serves only `AGENT_METHODS`. Any other method fails with
1370
- `forbidden` before its params are read. A line as the person (`person.send`, no session) is not
1371
- one of them (§6.8). Agent-facing clients connect here: the OpenCode plugin's server part
1559
+ `forbidden` before its params are read. Conversation calls on this socket require a bound session.
1560
+ They cannot send as the person. Agent-facing clients connect here: the OpenCode plugin's server part
1372
1561
  and `messaging-mcp`.
1373
1562
  - `run/admin.sock` (the admin tier) serves every method. Human clients connect here: the `messaging`
1374
1563
  CLI and the `/messaging` TUI.
@@ -1422,7 +1611,7 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1422
1611
  - **What the tiers are, and what they are not.** Where the daemon, OpenCode and the user's shell
1423
1612
  run as one OS user, as on a laptop, the tiers are not a security boundary: any process of that
1424
1613
  user can open `run/admin.sock`. They keep `team.*`, `identity.rotateEnc`, `daemon.stop`,
1425
- `channel.*`, `conv.subscribe`, `conv.markRead` and the person's own chat lines off every tool path. A tool that
1614
+ conversation administration and the person's own chat lines off every tool path. A tool that
1426
1615
  speaks to the agent socket cannot reach them. A shell that runs the `messaging` CLI is still a shell,
1427
1616
  and only the host's shell permissions govern it. The tiers are a boundary only when `messagingd`
1428
1617
  runs as its own user and the engine runs as another user in `ipc.agentGroup` (the layout is in
@@ -1431,23 +1620,25 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1431
1620
  as them and call every method, including `team.*` and `daemon.stop`. Agent-facing clients still
1432
1621
  must not expose `team.*`. The OpenCode TUI is a human surface and may call them. It must not
1433
1622
  place an invite ticket in a session, a log, or a model context.
1434
- - Key-scoped methods (`session.update`, `session.unregister`, `events.subscribe`, `msg.send`,
1435
- `inbox.*`, `item.read`, `task.*`, `conv.read` with a key) work only for a key that the calling connection registered (`not_found`
1436
- otherwise).
1623
+ - Key-scoped methods (`session.update`, `session.unregister`, `events.subscribe`, `inbox.*`, `item.read`, `task.*` (but `tasks.list` and `tasks.get`),
1624
+ `conv.list`, `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`)
1625
+ work only for a key that the calling connection registered. Human conversation calls can omit the key on the admin socket.
1437
1626
  - A session key is therefore a bearer credential for its session: whoever knows it can register it
1438
1627
  and act as that session.
1439
1628
  - `AGENT_METHODS` (with its type `AgentMethod`) lists the methods an agent-facing client may call,
1440
1629
  and exactly the methods `run/agent.sock` serves. Anything else is for human clients: the
1441
1630
  `messaging` CLI and the `/messaging` TUI.
1442
- - **Tiers.** Every method is in one tier list in `schema/ipc.ts`, and `conv.read` in two:
1631
+ - **Tiers.** Conversation methods available to agents also appear in the human list:
1443
1632
  `AGENT_METHODS` (the agent tier: sessions, `device.status`, `device.bind`, `contacts.list`,
1444
- `msg.send`, `inbox.list`, `inbox.get`, `inbox.ack`, `item.read`, `conv.read`, `search`, `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`,
1445
1634
  `events.subscribe`, `daemon.status`, `daemon.health`); `HUMAN_METHODS` (acting as the person: `conv.list`,
1446
- `conv.read`, `conv.markRead`, `conv.subscribe`, `person.send`, `people.search`, `people.get`,
1447
- `grant.list`, `grant.set`, `grant.set.preview`, `request.list`, `request.answer`, `request.answer.preview`, `contact.request`,
1448
- `contact.request.preview`, `card.get`, `agents.list`,
1449
- `agents.read`, `notice.subscribe`, `notice.list`, `notice.ack`, `device.approve.preview`,
1450
- `device.remove.preview`, `identity.replace.preview`, `directory.erase.preview`, `channel.*`); and `ADMIN_METHODS`
1635
+ `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`,
1636
+ `conv.get`, `conv.members`, `conv.threads`, `conv.pin`, `conv.create`, `conv.add`, `conv.remove`, `conv.role`, `conv.seat`,
1637
+ `conv.settings`, `conv.meta`, `conv.archive`, `conv.leave`, `conv.join`, `conv.accept`, `conv.policy`, `conv.backfill`, `people.search`, `people.get`,
1638
+ `grant.list`, `grant.set`, `grant.set.preview`, `reach.get`, `reach.set`, `reach.set.preview`, `request.list`, `request.answer`, `request.answer.preview`, `contact.request`,
1639
+ `contact.request.preview`, `card.get`, `sessions.list`, `request.waiting`,
1640
+ `notice.subscribe`, `notice.list`, `notice.ack`, `tasks.list`, `tasks.get`, `device.approve.preview`,
1641
+ `device.remove.preview`, `identity.replace.preview`, `directory.erase.preview`); and `ADMIN_METHODS`
1451
1642
  (this device and the daemon: `daemon.stop`,
1452
1643
  `daemon.woke`, `person.attached`, `daemon.snapshot`, `wake.arm`, `wake.disarm`,
1453
1644
  `device.list`, `device.remove`, `device.link`, `device.login`, `device.login.status`, `device.login.cancel`,
@@ -1482,6 +1673,12 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1482
1673
  Only a directory realm has it (`invalid_state` in a roster realm).
1483
1674
  - `device.list` (admin tier) lists this device's person's devices, current first, with vouch, kind,
1484
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.
1485
1682
  - `device.remove {device, reason?, token?}` (admin tier) unbinds this device (`self`), or another
1486
1683
  current device of its person (`removed` by default, or `lost`) with a fresh Jelly token bound to this
1487
1684
  device (`invalid_request` without one). A directory realm only; a roster realm uses `team.remove`.
@@ -1562,12 +1759,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1562
1759
  `PeerRef` (inbox items, task records), provenance (§6.6), directory entries (whose client kind is
1563
1760
  `client`) and chat lines. `daemon.status` names this device in `identity` (its `device`), and a divergence alarm
1564
1761
  the signer's `device`. Team administration (`team.*`) keeps the roster's `machine`.
1565
- - **Persons in the API.** Wherever the API names a person (addresses, `PeerRef`, provenance,
1566
- directory entries, chat lines and their `refused`, `conv` events, `daemon.status`'s identity,
1567
- `contacts.list`, `people.*`, a `dm:` or `agent:` conversation, and a line's `to` in `msg.send`), it
1568
- takes a `PersonId`: a roster person name or a hosted `p_…` id. Channel membership (`channel.*`, and the
1569
- `persons` and `creator` of a channel op, §6.8) takes `PersonId`s too. Team administration (`team.*`) takes
1570
- roster person names.
1762
+ - **Persons in the API.** Person references use `PersonId`: a roster person name or a hosted `p_…` ID.
1763
+ Conversation participants use person IDs or stable `person/agent` addresses. Team administration uses roster person names.
1571
1764
  - **`session.register`.** Names a session.
1572
1765
  - The requested name is suffixed `-2`, `-3`, ... if it is taken on this machine.
1573
1766
  - Registering an existing key again updates it; this is idempotent.
@@ -1608,24 +1801,14 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1608
1801
  expired `trustedUntil` is left out. So an agent sees whom it can reach without a human-tier call. A `reach` also
1609
1802
  carries `devicesChangedAt` while a device of that person was added, removed or replaced less than 7 days ago, as
1610
1803
  their verified log (or the roster) shows it; a rekey or an attest is not a change, and it carries no other text. Entries
1611
- carry `expect`: the hint a `msg.send` to that session now, with the wake hint set, would return
1804
+ carry `expect`: the hint a `task.create` to that session, with the wake hint set, returns
1612
1805
  (below), computed from the same view. Agent clients show it before anything is sent. It is absent
1613
1806
  when no presence lists the session.
1614
- - **`msg.send`.** One send from the session `key`. Its `to` decides what it is:
1615
- - **To `<person>` or `#<channel>`: a line** in the person's DM with them, or a post in a channel
1616
- this person is in, attributed to the session (§6.8). It takes `kind: "message"`, one text part of
1617
- at most `LIMITS.chatChars`, and `replyTo` as the thread (the `id` of any line of it); any other
1618
- A2A field (`contextId`, `taskId`, `messageId`, `metadata`, `extensions`, `referenceTaskIds`,
1619
- `onBehalfOf`, `wake`) is `invalid_request`, and so is a `to` that is neither a person
1620
- name nor `#` and a channel name. It fails with `not_found` for a person not on the team, a
1621
- channel this person is not in, or an unknown thread, and `too_large` for a longer line. It writes
1622
- the line as `person.send` does (below), and `idempotencyKey` is the line's `clientId`. Its one
1623
- result is `{to, envelopeId: <line id>, messageId, contextId, stage: "accepted"}`. With `request:
1624
- {contact: <email>}`, only on a line to the session's own person (otherwise `invalid_request`), the daemon
1625
- also files a suggestion for the person (§6.9, "Message requests").
1626
- - **To `person/session[@label]`: mail or a task** to that session, as the rest of this item says.
1807
+ - **`task.create` and `task.continue`.** These methods retain task envelopes until lane 7.
1808
+ The destination is `person/session[@label]`. Creation assigns a task ID and signs a delegation chain.
1809
+ Continuation requires the existing task ID and its bound executor. Both require the calling session's `key`.
1627
1810
  - Refuses content that holds a credential (`secret_refused`, §8.7), as do `task.update`,
1628
- `task.cancel` and `person.send`.
1811
+ `task.cancel`, `conv.send` and `conv.edit`.
1629
1812
  - Resolves the address against the directory, including offline sessions.
1630
1813
  - Builds one envelope per recipient and stores it in the outbox (`accepted`).
1631
1814
  - Sends it when the relay is connected.
@@ -1651,14 +1834,12 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1651
1834
  `idempotencyKey` returns the hint computed for the original call.
1652
1835
  - `idempotencyKey` returns the original results for a repeated call from the same key.
1653
1836
  - `wake` defaults to true.
1654
- - The daemon assigns ULID task ids for `kind: "task"`, and the `contextId` when it is omitted.
1655
- - A `kind: "task"` send signs a root grant for each recipient (§6.7).
1837
+ - Creation assigns ULID task IDs and a `contextId` when omitted, and signs a root grant for each recipient (§6.7).
1656
1838
  - A chain this daemon already knows the recipient would refuse is not sent. The result is
1657
1839
  `stage: "rejected"` with that check's code, and the requester's task is stored
1658
1840
  `TASK_STATE_REJECTED` with no grants.
1659
1841
  - **`onBehalfOf {taskId, scope?, maxDepth?}`** delegates on behalf of a task that the calling key
1660
- **executes**, and only with `kind: "task"`. Otherwise it fails with `not_found` (any other task)
1661
- or `invalid_request` (`kind` is not `"task"`), before anything is stored. The daemon:
1842
+ **executes**, and only with creation. Any other task fails with `not_found` before storage. The daemon:
1662
1843
  1. refuses a terminal task (`invalid_state`) before anything is stored;
1663
1844
  2. signs the next grant of the stored chain for each recipient, narrowed to `scope` and
1664
1845
  `maxDepth` when given;
@@ -1678,104 +1859,132 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1678
1859
  `invalid_state`.
1679
1860
  - The daemon validates the transition and stores it.
1680
1861
  - It then emits `task.status` and/or `task.artifact`.
1681
- - **`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).
1682
1935
  - **`task` events** carry `origin`:
1683
1936
  - `local` when the event follows the subscriber key's own call: `task.update`, `task.cancel`,
1684
- or the `msg.send {kind: "task"}` that created the record;
1937
+ or the `task.create` call that created the record;
1685
1938
  - `peer` when it follows an envelope from the other side, a receipt, or a daemon timeout.
1686
1939
  - When the event follows a status or artifact update, `update` carries that delta, including
1687
1940
  `append` and `lastChunk`.
1688
- - **`conv.read {key?, conversation, thread?, before?, after?, limit?}`** returns one page of a
1689
- conversation, oldest first, as `{kind, …, more}`. `conversation` is `dm:<person>` or
1690
- `channel:<name>` (`kind: "lines"`, below under Chats), or `agent:<person>/<agent>` (`kind:
1691
- "agent"`), which needs the session `key`: what the session `key` exchanged with one other session,
1692
- on any machine of that person: incoming mail,
1693
- outgoing mail and tasks either way, as `ConvEntry` rows in time order. A task request appears
1694
- once, as its task (dated when it was created, with its current state), not again as mail. A
1695
- cursor is an entry's `{at, id}` with an agent, and a line's `seq` in a DM or a channel; the other
1696
- form is `invalid_request`, and so is `thread` with an agent. With no cursor it is the latest `limit` entries (default 30, at
1697
- most 100); with `before` the latest `limit` before that place; with `after` the earliest `limit`
1698
- after it. Entries come oldest first, and `more` says entries remain past the page in the
1699
- direction read; `before` and `after` together are invalid params. Incoming mail carries its
1700
- preview and stage, a task the start of its request, the session's role and the task's state.
1701
- Outgoing mail carries its stage but no text: the daemon keeps no copy of what it sent. It changes
1702
- no stage and sends no receipt. Agent clients build their conversation view on it (§11).
1941
+ - **`conv.read {key?, cid, thread?, before?, after?, around?, limit?, cursor?}`** returns current event projections, oldest first, or an explicit context hold.
1942
+ Pages contain at most 50 events. Before and after positions are log sequence numbers. An opaque cursor binds to the query and session.
1943
+ Reads expose no legacy mail or fabricated task events. Retained tasks use their own methods.
1703
1944
  - **`task.list`:** most recently updated first. It filters by `role`, `state`, `contextId`,
1704
1945
  `peer` (a qualified address) and `updatedAfter` (at or after, ≥), pages with an opaque `cursor`/`nextCursor`, and reports `total`.
1705
- - **`msg.send` A2A passthrough** (mail and tasks): `messageId` (generated if omitted), `metadata` (at most
1946
+ - **Task A2A passthrough:** `messageId` (generated if omitted), `metadata` (at most
1706
1947
  16,384 bytes of UTF-8 JSON, which for wire JSON is its JCS length; the daemon always overwrites `metadata[urn:messaging:ext:provenance:v1]` on the receiving
1707
1948
  side), `extensions`, and `referenceTaskIds`.
1708
- - **Chats (§6.8).** `conv.read` (of a DM or a channel) and `search` need no session key. They are
1709
- the only chat methods in `AGENT_METHODS`; an agent's own line goes through `msg.send`.
1710
- - `conv.list` (human tier) returns at most `limit` conversations (default 100, at most 200), newest
1711
- first. A channel held under a local name carries its own `name`. Each carries its read state
1712
- (§6.8): `readUpTo`, `unread`, and `lastSeq`, the newest line's `seq` (absent in a channel with no
1713
- lines).
1714
- - `conv.read` of a DM or a channel returns a page of its lines (or of one thread), oldest first, and `more`. With
1715
- no cursor it is the latest `limit` lines (default 50, at most 100); with `before: seq` the latest
1716
- `limit` lines below that `seq`, to page back; with `after: seq` the earliest `limit` lines above
1717
- it, to catch up. `more` says that lines remain past the page in the direction read. `before` and
1718
- `after` together are invalid params.
1719
- - `conv.markRead {conversation, upTo}` moves the conversation's read cursor to `upTo`, a line's `seq`, if that is
1720
- later, clamped to the newest `seq` here. It returns `{readUpTo, unread}`. It is a human-client
1721
- method that is not in `AGENT_METHODS`: read state is the person's, and an agent reading a
1722
- conversation does not mark it read. It fails with `not_found` for a channel this person is not in.
1723
- - A `ChatLine` this machine sent to other persons carries `state` (`sent`, `delivered` or
1724
- `refused`) and, when refused, `refused` (the persons). A line the daemon wrote itself, about a
1725
- channel change or a refusal of one, carries `notice: true`.
1726
- - `person.send {conversation, text, thread?, clientId?}` (human tier) writes the person's own line
1727
- and its envelopes in one transaction and sends them at once, with no session. It fails with
1728
- `not_found` for a person not on the team or a channel this person is not in. A DM to oneself
1729
- is the person's own conversation (§6.8).
1730
- - `search {query, conversation?, limit?}` finds lines in the DMs and channels this person holds,
1731
- newest first, as `hits`, each with its `conversation`.
1732
- - **Pacing.** The sender keeps its chat envelopes to one recipient machine below that machine's
1733
- chat budget (§8.3): the reference daemon allows 50 a minute, ten below
1734
- `LIMITS.inboundPerMachinePerMinute`, for clock and transit jitter. Channel changes count too. A
1735
- line (`person.send`, or `msg.send` to a person or a channel) that would go over it for any machine
1736
- it goes to fails with `rate_limited`, a
1737
- message that says when to try again, and nothing stored or sent. Pacing cannot cover a backlog:
1738
- lines queued at the relay for an offline machine reach it together when it connects. The
1739
- receiver defers those past its budget and the relay paces them (§7 "Delivery", §8.3), so every
1740
- line is delivered within minutes; only one that would expire first is refused.
1741
- - A line with a `clientId` (`msg.send`'s `idempotencyKey`) is idempotent per `clientId` and
1742
- sender: the session `key`, or the person for `person.send`. A repeat with the same room, thread and text returns the first call's
1743
- result and files and sends nothing. The same `clientId` for another line is `invalid_request`.
1744
- The daemon keeps a `clientId` for `LIMITS.ttlMs`.
1745
- - `channel.create`, `channel.invite`, `channel.leave` and `channel.remove` are for the human
1746
- `/messaging` TUI. They take a channel name as this machine holds it (its local name). Each checks
1747
- before it stores anything. `channel.create` refuses a name already held, more than the
1748
- channels a machine holds (`invalid_request`), an unknown person, or more than
1749
- `LIMITS.channelPersons` persons with the creator. `channel.remove` refuses a caller who is not
1750
- a member (`not_found`), who is neither the creator nor a team admin (`forbidden`), a list that
1751
- names the caller (`invalid_request`: use `channel.leave`), and a person who is not a member
1752
- (`not_found`).
1753
- - Mail and tasks (`msg.send` to an agent) never write the transcript.
1754
- - **`conv` events** go only to connections that called `conv.subscribe`, a human-client method
1755
- that is not in `AGENT_METHODS`. They are never sent to `events.subscribe` keys. A `conv` event
1756
- names the conversation, the line's `id` and its person, never its text. It is raised for a filed line,
1757
- a channel change, and a change of an outgoing line's delivery state (then `person` is this
1758
- machine's person). Each carries the `seq` of its change (§6.8), so events rise in `seq`.
1759
- - **Resuming.** `conv.subscribe {since}`, where `since` is the `seq` of the last event the client
1760
- handled, first sends one `conv` event per line that changed after `since`, oldest change first,
1761
- each with the `seq` of the line's latest change. Then live events follow, with none between.
1762
- It returns `{seq, gap}`: the newest `seq` at the call, and whether the replay could not be
1763
- complete, in which case it sends none. That is when more than a policy number of lines changed
1764
- (the reference daemon: 500), a channel was deleted here after `since`, or `since` is above
1765
- `seq` (a store set up again). After a gap the client reloads with `conv.list` and `conv.read`.
1766
- Without `since`, `gap` is false and no events are replayed.
1949
+ - **Conversations (§6.8).** The agent-accessible methods are `conv.list`, `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access` and `conv.sync`.
1950
+ Every agent call uses its bound session. Human calls use the admin socket.
1951
+ A `conv.send` returns `{cid, id, clientId, stage: "pending"}` or an explicit audience hold.
1952
+ The durable outbox retries identical signed events. A repeated `clientId` returns the first event ID.
1953
+ A queued key share whose recipient device the conversation's own verified log removes (an accepted `remove` of its
1954
+ person or the person's own `leave`, with the person no longer a participant, or a `rekey {cause: device}` naming
1955
+ the device) is dropped, never sent, and logged by conversation, device and event id. Any other share to a device
1956
+ that is no longer current, such as a directory miss, holds the outbox until the state is verified.
1957
+ Reuse with different content or a different destination fails with `invalid_request`.
1958
+ Conversation administration uses the human-tier methods in the tier list. Internal notifications carry identifiers only.
1959
+ The stream re-renders current verified state, including edits and deletions, for each reader.
1767
1960
  - **Consent (§6.9).** `grant.list`, `grant.set`, `grant.set.preview`, `request.list`,
1768
1961
  `request.answer`, `request.answer.preview`, `contact.request`, `contact.request.preview` and `card.get` are human-client methods that are
1769
1962
  not in `AGENT_METHODS`: agents never read or change a person's grants, answer a held request, or send a message
1770
- 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.
1771
1981
  - `grant.list {subject?}` returns this person's grants as this device holds them, newest first,
1772
1982
  each with its levels, `id`, `ts`, `by` and, when re-signed, `resignedAt`, and the review list:
1773
- the widening levels that lapsed with their signer, with when they lapsed. A channel subject
1774
- carries the channel id and, while this person is in it, its local name.
1983
+ the widening levels that lapsed with their signer, with when they lapsed. A conversation subject carries its canonical `cid`.
1775
1984
  - `grant.set {subject, chat?, agentMail?, ask?, trustedUntil?, trust?, discoverable?}` signs the
1776
1985
  whole new grant for the subject with this device: a level left out is the relation's default,
1777
- and no level at all is a clear. `subject` is `{person}` or `{channel}`, a channel by its local
1778
- name. `trust: true` without `trustedUntil` trusts for `LIMITS.trustDefaultMs`. It stores the
1986
+ and no level at all is a clear. `subject` is `{person}` or `{conv: cid}`.
1987
+ `trust: true` without `trustedUntil` trusts for `LIMITS.trustDefaultMs`. It stores the
1779
1988
  grant, takes the subject off the review list and sends the grant to this person's other devices
1780
1989
  in one transaction. It fails with `not_found` for a person the realm does not know or a channel
1781
1990
  this person is not in, `invalid_request` for a grant that breaks a rule of §6.9, and
@@ -1787,12 +1996,15 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1787
1996
  `agent`, `address`), the local session it is for, an untrusted `preview`, when it was held and when it
1788
1997
  expires; then the message requests (`contact`), each with its envelope `id`, the sender as the realm verified
1789
1998
  them (`person`, `device`, `label`, `name`, `email?`, `org`), its untrusted `text`, `at` and `exp`; then the
1790
- 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").
1791
2001
  - `request.answer {id, answer, all?}` answers one and returns the ids answered: an ask takes `once`, `always`,
1792
2002
  `decline` or `block` ("Holds"), a message request `accept`, `decline` or `block` ("Message requests"), a
1793
2003
  suggestion `decline`; any other answer is `invalid_request`. With `all`, the same answer goes to every request
1794
2004
  of the same kind held from that person. It fails with `not_found` for an id this device does not hold, or one
1795
- 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.
1796
2008
  - `request.answer.preview {id, answer, all?}` (`request.answer`'s input) changes nothing and returns, for the
1797
2009
  answer's confirmation, whom it reaches and what it does: `{kind, from: {name, email?, org}, agent?, to?, count,
1798
2010
  sentence}`. `from` is the realm's verified profile of the sender (of the person themself for their
@@ -1800,7 +2012,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1800
2012
  for, `count` how many requests the answer would settle (more than 1 only with `all`), and `sentence` one fixed
1801
2013
  sentence saying what `answer` does. A name or email in the sentence is
1802
2014
  cleaned to one line of at most 80 (100) characters. It refuses as `request.answer` would: `not_found` for an id
1803
- 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.
1804
2017
  - `contact.request {email, text, suggestion?}` (§6.9, "Message requests") resolves the exact `email` and sends
1805
2018
  `text` (1 to `LIMITS.requestChars` UTF-16 code units) as a message request, and returns `{id, conversation,
1806
2019
  person}`: the line's id, `dm:<person>` and the person found. It fails with `invalid_request` in a roster
@@ -1843,27 +2056,18 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1843
2056
  `daemon.snapshot {done: true}` deletes it and returns `{released}` (false when none was held). One is held at a time
1844
2057
  (`invalid_state` while one is); the daemon releases it after an hour (`until`), when it stops, and one a previous run
1845
2058
  left when it starts.
1846
- - **Your agents.** `agents.list` and `agents.read` (human tier, read-only) are the person's view of their
1847
- own agents' traffic on this device.
1848
- - `agents.list` returns each local session (registered or kept) as `{agent, conversations, unread,
1849
- lastAt?, state}`: how many other agents it has mail or tasks with, the mail it has not read, its last
1850
- traffic, and `idle`, `busy` or `kept`; most recent first.
1851
- - `agents.read {agent, conversation?, before?, limit?}` returns, without `conversation`, `{kind:
1852
- "conversations", conversations}`: each `agent:<person>/<agent>` it has traffic with, with its unread
1853
- mail and last time, most recent first. With `conversation`, it returns `{kind: "entries",
1854
- conversation, entries, more}`: a page of entries as `conv.read` gives them to that agent (default
1855
- 30, latest first, `before` an entry's `{at, id}`). It changes no stage and sends no receipt.
1856
- `not_found` for an agent this device has no session of. Peer text is untrusted; a client cleans it
1857
- for display (`cleanPeerText`) and never shows it to a model.
2059
+ - **Your agents.** Human-tier `sessions.list` returns local sessions as `{agent, state, seats}`.
2060
+ State is `idle`, `busy` or `kept`. Seats are verified conversation IDs. No unread count is fabricated.
1858
2061
  - **Notices.** What the person is told about, in three fixed categories, as `{id, category,
1859
- conversation?, from?: {name, email?, org}, line?, at}`:
1860
- - `message`: a line filed for the person in a DM or a channel from another person or their agent, or
1861
- from one of the person's own agents in their own conversation; never the person's own line, a line
1862
- about a channel change, or a change of a line's delivery state. Its `conversation` is the DM or the
1863
- channel, and `from` the sender's person as the realm verified them.
1864
- - `request`: a request held for the person (§6.9), with `conversation` the sender's
1865
- `agent:<person>/<agent>` (`dm:<person>` when the person sent it themself, as for a message request); or a
1866
- suggestion of one of the person's own agents, with `conversation` `agent:<own person>/<agent>`.
2062
+ conversation?, from?: {name, email?, org}, line?, key?, at}`:
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`.
2070
+ - `request`: a held request or retained contact suggestion (§6.9). New notices contain no legacy room reference.
1867
2071
  - `alert`: an alarm of this device (`keys_missing`, `restored`, `directory`), with fixed text in `line`; a
1868
2072
  change of another device of the person (below), with `device`; or trust the person gave someone that ends
1869
2073
  within 7 days (§6.9), with `from` that person and `line` "Trust you gave this person ends on <date>; renew it
@@ -1887,19 +2091,18 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1887
2091
  admitted by the team's admin.", "'tablet' (other) on your account is now vouched by Jelly ID: ...", "'desk'
1888
2092
  was replaced by 'desk2' (jelly-host), ...", "'phone' (laptop) was removed from your account.".
1889
2093
  - Each notice is filed once, when its source is, under an id derived from the source (`m_<line id>`,
1890
- `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:
1891
- with `lines: true`, a message's `line` is the first line of its text read from the transcript,
1892
- through `cleanPeerText`, at most 120 characters with its "…".
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:
2095
+ `lines: true` can expose fixed alert text. It never restores legacy transcript content.
1893
2096
  - `notice.subscribe {after?, lines?}` sends the unacknowledged notices filed after the notice `after`
1894
2097
  (all of them without it, at most 500), oldest first, as `notice` events on this connection, then each
1895
2098
  new one as it is filed; it returns `{replayed}`. `notice.list {lines?, limit?}` returns the
1896
- unacknowledged notices, newest first (default 50), and `counts`: `unread`, the lines from others not
1897
- yet marked read over every conversation, and `requests`, the asks, message requests and suggestions held. `notice.ack {ids}` marks
2099
+ unacknowledged notices, newest first (default 50), and `counts.requests`: the asks, message requests and suggestions held. `notice.ack {ids}` marks
1898
2100
  notices seen; unknown ids are ignored. It never answers, releases or uncounts a request: a request stays held, in
1899
2101
  `request.list` and `counts.requests`, until `request.answer` (or its expiry). Notices go with the transcript's retention, and acknowledged ones
1900
2102
  with the read mail's.
1901
2103
  - **Previews.** Each preview takes exactly its action's input, so a host that draws a confirmation calls it with
1902
- 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`
1903
2106
  `request.answer`'s `{id, answer, all?}`, `contact.request.preview` `contact.request`'s `{email, text?, suggestion?}`,
1904
2107
  `device.approve.preview` `device.approve`'s `{link, sas?}` and `device.remove.preview` `device.remove`'s `{device,
1905
2108
  reason?, token?}`. A field the preview does not read may be left out (`text`, `sas`, `suggestion`, `reason`,
@@ -1908,7 +2111,7 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1908
2111
  changes nothing. What each returns is unchanged: `device.remove.preview` (human tier, read-only, a directory realm
1909
2112
  only) returns what removing a device of this person would remove, `{label, kind?, vouch, current, self, since?}`,
1910
2113
  or `not_found`; `device.approve.preview` returns `{device, label, kind, sas}` ("Devices"); `grant.set.preview`,
1911
- `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
1912
2115
  grant names its action's class, `local` (§8.11).
1913
2116
  - **Failed delivery of a task request:**
1914
2117
  - a `rejected` receipt or nack moves the requester's record to `TASK_STATE_REJECTED`;
@@ -1929,8 +2132,7 @@ delegation chain is verified after these (§6.7). A person refuses another perso
1929
2132
  - Receipts, `task.status` and `task.artifact` answer the recipient's own traffic, so they have a
1930
2133
  budget of their own: four times that.
1931
2134
  - Presence has a budget of its own, also four times that.
1932
- - Chat envelopes (a `message` to the machine with no session, §6.8) have a budget of their own,
1933
- `LIMITS.inboundPerMachinePerMinute`, apart from agent mail.
2135
+ - Person-addressed controls (contact requests, cards and grants) have their own budget of `LIMITS.inboundPerMachinePerMinute`.
1934
2136
  - Over the limit, an envelope is **deferred**: the recipient withholds its `recv.ack` and does not
1935
2137
  mark it seen, so the relay sends it again later (§7 "Delivery") and it is taken when the budget
1936
2138
  has room. A backlog that arrives at once is so taken at the budget's pace, all of it, not
@@ -1939,9 +2141,7 @@ delegation chain is verified after these (§6.7). A person refuses another perso
1939
2141
  can wait.
1940
2142
  - An envelope over the limit that would expire before the budget has room for it, with a minute
1941
2143
  to spare for the relay's next redelivery, can never be taken. It is dropped (acked). A message
1942
- sender gets at most one `rate_limited` receipt a minute. A chat sender gets a `rejected` receipt
1943
- with `rate_limited` for every such chat envelope, up to four times the chat budget a minute, so
1944
- its line is `refused` at once instead of `sent` until it expires.
2144
+ sender gets at most one `rate_limited` receipt a minute. Person-addressed controls get individual refusals, bounded by the response budget.
1945
2145
  - Presence over its budget is dropped: it is never queued, and the next one supersedes it.
1946
2146
  - **Quota.** A recipient may cap the inbox items and bytes it holds from one sender machine
1947
2147
  (`rate_limited` past it).
@@ -1954,6 +2154,26 @@ delegation chain is verified after these (§6.7). A person refuses another perso
1954
2154
  - **`wake` on an inbox item.** This is the sender's hint, AND-ed with the session's tier, which must be
1955
2155
  `push`. Only what consent delivers becomes an inbox item, so a held request never wakes a session.
1956
2156
 
2157
+ - **Mixed versions** (0.7.1 and the conversations release). Neither side shims the other, and neither leaves a
2158
+ half-open contact:
2159
+ - A daemon from before conversations sends a chat line, plain mail or a message request without a portable proof
2160
+ as an envelope. A current daemon refuses each with a `rejected` receipt, code `refused`, and the `note` "The
2161
+ recipient runs a newer Messaging that takes conversations only: update Messaging to reach them." 0.7.1 records
2162
+ only the code: it shows `refused` with its hint "do not retry; tell your person", the clearest it can show, since
2163
+ none of its hints names an update. The note is for later senders and the logs.
2164
+ - A current daemon asks the relay (`conv.caps`, §7) before it creates a dm, group or channel with a person, adds
2165
+ one, or sends a message request. A person whose known devices all lack the `conversations` presence feature is
2166
+ refused with `peer_outdated`, "<handle> needs to update Messaging", and nothing is created or sent. With no
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.
2173
+ - Tasks keep their envelopes and work in both directions.
2174
+ - Rollout: the relay first (it serves 0.7.1 unchanged), then every daemon of a realm together
2175
+ (`docs/distribution.md`, "Versions and compatibility").
2176
+
1957
2177
  ### 8.4 Delivery stages (sender view)
1958
2178
 
1959
2179
  - The normal sequence is `accepted → relayed → delivered → handed_to_agent → read`. A request the
@@ -2036,8 +2256,8 @@ then lives in plaintext in the sender's and recipient's stores for their retenti
2036
2256
  and in the recipient's model context. The sending daemon therefore refuses such content before it
2037
2257
  stores or seals anything. The relay never sees plaintext, so this check exists only on the sender.
2038
2258
 
2039
- - **Where.** `msg.send` (lines, messages, replies and task requests), `task.update` (status message
2040
- and artifact), `task.cancel` (`reason`) and `person.send` (`text`). The daemon reads every string in
2259
+ - **Where.** `task.create`, `task.continue`, `conv.send`, `conv.edit`, `task.update` (status message
2260
+ and artifact), and `task.cancel` (`reason`). The daemon reads every string in
2041
2261
  the call's params except the session `key`: values and object keys at any depth, including
2042
2262
  `metadata` and `data` parts, and the base64-decoded bytes of every `raw` field, read as UTF-8.
2043
2263
  - **When.** After the key and task-role checks (`not_found`, `invalid_state` for a wrong role), and
@@ -2076,26 +2296,34 @@ stores or seals anything. The relay never sees plaintext, so this check exists o
2076
2296
 
2077
2297
  ### 8.8 Store schema
2078
2298
 
2079
- The daemon's store (`messaging.db`) and the relay's database each have one baseline, schema 1. Messaging resets
2080
- its stores now and grows them later (owner, 2026-11-08):
2081
-
2082
- - **Until the first release another person uses**, a store change edits the baseline instead of adding a
2083
- migration, and raises the store's generation. The release that carries it resets the stores and says so in its
2084
- notes. Release 0.7.0 is the first such reset: it reads no store of 0.1.0 to 0.6.0.
2085
- - **From that release on**, stores only grow, so that a host can roll a daemon or relay back by moving the
2086
- binary back and keeping the data. Migrations then follow the rules under "When stores grow" below.
2299
+ The daemon's store (`messaging.db`) and the relay's database each have one baseline, schema 1, as release 0.7.0
2300
+ created it (owner, 2026-11-08):
2301
+
2302
+ - **Release 0.7.0 was the last store reset.** It reads no store of 0.1.0 to 0.6.0. It is the first release another
2303
+ person uses (Jelly's hosted users), so no later release resets a store or raises a generation.
2304
+ - **From 0.7.0 on, stores only grow**, so that a host can roll a daemon or relay back by moving the binary back
2305
+ and keeping the data. A store change adds an idempotent migration and a new schema number, under the rules of
2306
+ "When stores grow" below; the baseline is never edited.
2307
+ - The daemon store is at **schema 4** and the relay database at **schema 3**, each the baseline plus its migrations:
2308
+ - daemon schema 2 adds `conv_logs`, `conv_entries`, `conv_shares`, `conv_cursors`, `conv_outbox`, `conv_keys`,
2309
+ `conv_alarms` and `conv_fts`; schema 3 adds `conv_held`; schema 4 adds `conv_evidence`;
2310
+ - relay schema 2 adds `conv_logs`, `conv_entries`, `conv_members` and `conv_shares` (§7 "Conversation frames");
2311
+ schema 3 adds `conv_request_decisions`.
2312
+ Each keeps `min_reader` 1, so a 0.7.x daemon or relay still opens the store after a rollback and ignores the new
2313
+ tables.
2087
2314
 
2088
2315
  **Records.** The daemon's store keeps `generation`, `schema` and `min_reader`, the lowest schema that can read
2089
2316
  it, in its `kv` table. The relay keeps `generation` and `min_reader` in its `meta` table, and `schema` in
2090
- `PRAGMA user_version`. A new store records generation 2, schema 1 and `min_reader` 1. The stores of 0.1.0 to
2091
- 0.6.0 have no generation (generation 1).
2317
+ `PRAGMA user_version`. Every store from 0.7.0 on records generation 2 and `min_reader` 1; a new daemon store records
2318
+ schema 4, and a new relay database schema 3. The stores of 0.1.0 to 0.6.0 have no generation (generation 1).
2092
2319
 
2093
- **Opening.** A store with no tables is new: the daemon or relay writes the baseline and its records. For the relay,
2320
+ **Opening.** A store with no tables is new: the daemon or relay writes the baseline, its records and every migration. For the relay,
2094
2321
  the tables Litestream adds when it replicates the database (`_litestream_*`) do not count. It opens
2095
2322
  any other store only when the store records its own generation and a `min_reader` at most its own schema. It
2096
- refuses every other store and changes nothing in it, and the error says how to start over: stop the daemon or
2097
- relay, move the home (or the relay database, with its `-wal` and `-shm` files) aside, and set it up again. A
2098
- daemon checks the store before it reads or writes anything else in its home. Nothing deletes a home or a
2323
+ refuses every other store and changes nothing in it. The error gives the reason and its remedy (below).
2324
+ Only `pre-reset` calls for starting over: stop the daemon or relay, move the home (or the relay database,
2325
+ with its `-wal` and `-shm` files) aside, and set it up again. For `newer`, upgrade the reader and keep the data.
2326
+ A daemon checks the store before it reads or writes anything else in its home. Nothing deletes a home or
2099
2327
  database by itself.
2100
2328
 
2101
2329
  **A refused store** is machine-readable, so that a client never matches the wording. Every refusal has one of two
@@ -2124,6 +2352,9 @@ database by itself.
2124
2352
  client that denies its agents the home also denies `<home>.pre-reset-*`: the moved home keeps the old keys and
2125
2353
  messages. The messagingd that wrote it still opens it.
2126
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
+
2127
2358
  **When stores grow.**
2128
2359
 
2129
2360
  - A daemon or relay opens any store of its generation whose `min_reader` is at most its own schema. It
@@ -2137,10 +2368,13 @@ database by itself.
2137
2368
  - A breaking change needs a new store file with an explicit export and import, never an in-place change.
2138
2369
  `min_reader` is raised only in an emergency, and hosts that pin the daemon are told first.
2139
2370
 
2140
- **The test.** `store-schemas/1.json` in `packages/daemon` and `packages/relay` is the shape of the baseline
2141
- (tables, columns, indexes, constraints, triggers). `test/release/store-schema.test.ts` in each package fails
2142
- when HEAD's store differs from it, so a store change is seen in review. When stores grow, each released schema
2143
- gets its snapshot, and the test holds HEAD to every one of them.
2371
+ **The test.** `store-schemas/<n>.json` in `packages/daemon` and `packages/relay` is the shape of schema n as
2372
+ released (tables, columns, indexes, constraints, triggers); `1.json` is the baseline as 0.7.0 created it.
2373
+ `test/release/store-schema.test.ts` in each package holds the baseline to `1.json` and HEAD's new store to the last
2374
+ snapshot, so a store change is seen in review. Each also opens a database the schema-1 release itself wrote
2375
+ (`test/fixtures/relay-schema-1.sql` and `test/fixtures/daemon-schema-1.sql`), checks that it migrates in place with every row intact except the schema marker, that migrating again
2376
+ changes nothing, and that a schema-1 reader still opens the result: its records allow it, and every table, column
2377
+ and index of schema 1 is unchanged inside it.
2144
2378
 
2145
2379
  The wire rule (§14) is separate; a rolled-back daemon must still speak to newer peers within `messaging/1`.
2146
2380
 
@@ -2247,19 +2481,21 @@ and names each session an engine may register.
2247
2481
  parent writable by no one else and owned by the daemon's user or root.
2248
2482
  - `--socket-group <gid>` makes both sockets 0660 and owned by that group, and `run/` (and the home, when
2249
2483
  `run/` is inside it) 0710 to it. It is accepted only here: elsewhere `run/admin.sock` is always 0600.
2250
- - **Action grants.** On `run/admin.sock`, every method outside `AGENT_METHODS` needs an action grant, a
2251
- 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`:
2252
2487
  - the header is `{alg: "EdDSA", typ: "jelly-act+jwt", kid}`, and the signature verifies with the action
2253
2488
  key (whose `kid`, when the key file has one, the header must name);
2254
2489
  - `aud` is `"@joe-broadhead/opencode-messaging#service"`;
2255
2490
  - `act` is the method, and `in` is `base64url(SHA-256(JCS(params)))` of the params as sent (absent
2256
- 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;
2257
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
2258
2494
  behind it;
2259
2495
  - `cls` is required, and is the method's action class (`ACTION_CLASSES`, `actionClass` in `@messaging/core`):
2260
- `chat` for `person.send`, `conv.markRead` and `notice.ack`; `view` for the reads a host's section makes, the
2261
- read-only human-tier methods and `device.list` (`conv.list`, `conv.read`, `conv.subscribe`, `people.search`,
2262
- `people.get`, `grant.list`, `request.list`, `agents.list`, `agents.read`, `notice.subscribe`, `notice.list`,
2496
+ `chat` for `conv.send`, `conv.edit`, `conv.delete`, `conv.react` and `notice.ack`; `view` for the reads a host's section makes, the
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`,
2498
+ `people.get`, `grant.list`, `reach.get`, `request.list`, `sessions.list`, `tasks.list`, `tasks.get`, `notice.subscribe`, `notice.list`,
2263
2499
  `device.list`); `local` for every other method, the previews and every other admin-tier method included.
2264
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
2265
2501
  a `local` action would do, takes `local`. A plugin's `jelly` declaration must give each action and read the same
@@ -2273,8 +2509,8 @@ and names each session an engine may register.
2273
2509
  `person.attached`, `daemon.snapshot`, `wake.arm` and `wake.disarm`.
2274
2510
  It is bound to the first connection that presents it, and may be presented again on that connection until
2275
2511
  it expires; on another connection, or after a restart, it is `grant_invalid`. Under a host-scope grant
2276
- `conv.list` returns counts only: each conversation's `conversation`, `kind`, `readUpTo` and `unread`, with
2277
- no channel name, time or seq. A `notice.subscribe` under a host-scope grant ends when the grant does: the
2512
+ `conv.list` is refused before content is read. Lane 4 exposes no placeholder counts.
2513
+ A `notice.subscribe` under a host-scope grant ends when the grant does: the
2278
2514
  host subscribes again with a fresh grant and `after`.
2279
2515
  - **Refusals.** No grant is `grant_required`; any failure above is `grant_invalid`. The agent tier needs no
2280
2516
  grant on either socket. Both are checked before the params are read. The daemon logs each refusal with
@@ -2309,7 +2545,16 @@ and names each session an engine may register.
2309
2545
  request envelope ids of those agents' requester tasks that have not ended (submitted, working, input- or
2310
2546
  auth-required), most recently updated first, at most 500. Their updates name the request in `replyTo` (§6.2), so the
2311
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
2312
- 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
2313
2558
  goes out at the next welcome (a newer `wake.arm` or `wake.disarm` replaces it; one the relay refuses is not sent
2314
2559
  again). `wake.disarm {}` forgets it, and `daemon.woke` disarms a ticket armed since the last disarm. The daemon never
2315
2560
  logs the ticket.
@@ -2481,6 +2726,9 @@ implementation:
2481
2726
  `in` digest, a method grant, a host-scope grant and a session ticket signed by it, and grants a daemon must
2482
2727
  refuse (another method, params, key, `kid`, `typ` or audience; a lifetime past 60 or 600 s; an expired grant;
2483
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`).
2484
2732
 
2485
2733
  `packages/core/scripts/vectors.ts` generates them. Run `npm run build && npm run vectors -w @messaging/core`;
2486
2734
  this uses Node's type stripping against the built package and `@messaging/core/testing`. `packages/core/test/vectors.test.ts`
@@ -2500,30 +2748,38 @@ daemon owned by the uid in `MESSAGING_DAEMON_UID` (§8.1).
2500
2748
 
2501
2749
  ### Agent tools
2502
2750
 
2503
- The OpenCode plugin and `messaging-mcp` offer an agent the same eight tools, with the same input and
2504
- output schemas (`AgentTool` in `@messaging/core`, normative like `schema/**`): `contacts`, `send`,
2505
- `inbox`, `read`, `search`, `tasks`, `task_update` and `task_cancel`. Their names are
2751
+ **Conversation tools.** `ConversationTools` is the shared schema registry for the conversation-model
2752
+ cut-over. Its ten verbs are `contacts`,
2753
+ `conversations`, `send`, `inbox`, `read`, `search`, `context`, `tasks`, `task_update` and `task_cancel`.
2754
+ `conversations` uses `conv.access`; `send`, `read`, `search` and `context` use their `conv.*` counterparts.
2755
+ Results identify a line by the stable citation `messaging:<cid>/<id>`. The client fences peer text and names,
2756
+ neutralizes fence closers, and labels own text as ours. List results contain at most 50 events and 32 KiB of text;
2757
+ each fenced event also has the existing per-part cap, `LIMITS.toolTextChars` (32,768).
2758
+ Long bodies have `[truncated: N bytes; read the item for the full text]`, where N counts omitted UTF-8 bytes.
2759
+ The stored body stays intact. `read({source, offset})` continues at the returned `nextOffset` until the full body is shown.
2760
+ A held result waits for consent and does not disclose the person's answer. Tools grant no human-tier action.
2761
+
2762
+ The OpenCode plugin and `messaging-mcp` use the same ten-tool contract in `@messaging/core`, normative like `schema/**`.
2763
+ Their names are
2506
2764
  `messaging_<verb>`; in OpenCode's Code Mode, `tools.messaging.<verb>`. No tool grants, adds
2507
2765
  contacts, changes devices or administers a team or channel.
2508
2766
 
2509
- - **Addresses.** `me` (the session's own person), `me/<agent>`, `<person>`, `<person>/<agent>`,
2510
- `<person>/<agent>@<label>` and `#<channel>` (a channel the person is in, by its local name). An
2511
- agent is a session (§3). Malformed text is `invalid_request` naming the field, and so is a group
2512
- form (`person/*`, `repo:…`, `*`): there are none (§3). A person, agent or channel the daemon does not know
2513
- (`not_found` for a person or channel, `no_recipients` for an agent, from `msg.send` or `conv.read`) is `unknown_address`; an agent
2514
- that runs on none of its person's machines is `unknown_session` (§3, "Resolution"); `ambiguous_address` asks for `@label`. `me`, and one's own person name, is the person's own
2515
- conversation, `dm:<self>` (§6.8): `send` writes a line there (unread for the person), `read` and
2516
- `search` read it, and a guarded peer turn may write there too. An `ask` to `me` (a request card to
2517
- one's own person) is `invalid_request`: it is not in this build.
2518
- - **Conversations.** `person:<person>` is the person's DM room with them (§6.8), seen from the agent;
2519
- `agent:<person>/<agent>` is the session's mail and tasks with that session on any machine
2520
- (`conv.read`, §8.2); `channel:<name>` is a channel room. `read({with})` pages one of them, oldest
2521
- first, with opaque `before`/`after` cursors. Mail and task entries carry previews; outgoing mail
2522
- carries no text, because the daemon keeps no copy of what it sent.
2767
+ - **Addresses.** Lines accept a canonical `cid` or a person or agent destination.
2768
+ `#<channel>` resolves through accessible conversations. An agent sends to a conversation it is seated in, or to another
2769
+ agent (`person/agent`). With no dm yet, its person's device signs the create of the agent dm, whose only participants
2770
+ are the two agents; the recipient's `ask` admits the invited agent (`open` at once, `request` after its person
2771
+ accepts, `no` never, silently). Until the seat the line waits on the sending device, keeps the id the send returned,
2772
+ shows in `request.waiting` as `pending-delivery`, and lapses after 7 days. An agent cannot start a dm with a person,
2773
+ and cannot add or seat anyone. The sender-side consent is the client's first-send confirmation for the address (the
2774
+ plugin's `messaging.send` permission), as for 0.7.1 mail; the daemon has no separate signal.
2775
+ Retained tasks use `person/agent[@label]`. Malformed addresses are `invalid_request`.
2776
+ An ambiguous task address requires a device label. Human contact requests remain human-only.
2777
+ - **Conversations.** `read({cid})` pages current verified events with opaque cursors.
2778
+ Conversation identity is stable across devices. Legacy room names are not aliases.
2523
2779
  - **`contacts`** lists `me` (address, person, agent, device label, relay `connected` or `offline`), the
2524
- persons in the directory and in DM rooms, each with their agents (state, `expect` from
2525
- `contacts.list` (§8.2), repo, branch and doing), and the channels `contacts.list` names, with their unread
2526
- counts. `org` is `same` for every teammate. A person's `can` is the daemon's `reach` for them (§8.2
2780
+ persons in the directory and in conversations, each with their agents (state, `expect` from
2781
+ `contacts.list` (§8.2), repo, branch and doing), and channels allowed by `conv.access`.
2782
+ Counts and read preferences are omitted when unavailable. A person's `can` is the daemon's `reach` for them (§8.2
2527
2783
  `contacts.list`): `source: "card"` with `card`, when their person signed it, or `source: "default"`, the
2528
2784
  defaults for the org or for a stranger; `can.chat` is false for one's own person and when the client offers no
2529
2785
  DMs. An agent's `can` is its person's, except that `can.chat` is false and the inbound policy narrows it (§8.3: a
@@ -2533,30 +2789,28 @@ contacts, changes devices or administers a team or channel.
2533
2789
  refused. `can: "ask"` lists those whose `can.ask` is not `no`. A channel's `can` is `default`. `presence` is
2534
2790
  `online`,
2535
2791
  `offline` or (for a channel) `unknown`. No field is invented.
2536
- - **`send`** is one `msg.send`: a chat line to `<person>` or `#<channel>`, or mail or, with `ask`, a
2537
- task to an agent. `replyTo` names a line (the thread), a message from that agent
2538
- (`replyTo` and its `contextId`), or a task the session requested (`taskId`: input for it, to its
2539
- executor only). `onBehalfOf` (with `ask`) passes `msg.send {onBehalfOf: {taskId}}` after
2540
- checking that the session runs that task and it is not finished. `files` (only to an agent) become
2541
- file parts. `key` makes a repeated call send once (`idempotencyKey`);
2542
- without it the client derives one from the tool call. Results carry each `SendResult.expect` and a
2543
- fixed `note`. A result at stage `held` (the recipient's person answers a request card first, §6.9)
2544
- carries `expect: "on_consent"` and a note saying so. `request: {contact: <email>}`, only with `to: "me"`
2545
- (otherwise `invalid_request`), passes `msg.send {request}`: the line goes to the agent's own person with a
2546
- suggestion to send a message request to that exact email (§6.9, "Message requests"); the agent cannot send one,
2547
- and its note says it is not told what the person decides. A send refused `request_required` tells the agent to
2548
- ask its person this way.
2792
+ - **Sources.** An item target or result names `source: {kind: "conv", cid, event}` or `source: {kind: "task", taskId}`.
2793
+ A task never receives a fabricated conversation id. Conversation item identity includes both `cid` and `event`.
2794
+ - **`send`** uses `conv.send` for lines, including agent DMs. `ask` uses `task.create`.
2795
+ A task continuation uses `task.continue`. Task sends retain `files`, `replyTo`, `onBehalfOf` and `key`.
2796
+ Delegation requires an unfinished task that this session executes. `key` or `clientId` makes retries idempotent.
2797
+ A line returns its conversation source and `pending`. A task returns its task source and delivery results.
2798
+ An authenticated hold carries `expect: "on_consent"`. Missing receipts do not imply a hold.
2549
2799
  - **`inbox`** lists mail and task requests (`inbox.list`), unread by default, and acks
2550
2800
  `handed_to_agent` for the previews it shows. `consent` says how a request from the sender runs
2551
2801
  here: `self` (the session's own person), `trusted` or `guarded`. It is the inbox item's `consent`, the
2552
2802
  daemon's decision (§6.9); a client decides it only for a task record or an item stored before
2553
2803
  consent, which carry none.
2554
- - **`read({id})`** looks the id up as an inbox item (`inbox.get`, then `item.read`: marked read, read
2555
- receipt), a task (`task.get`), or an envelope the session sent (`item.read`, as `delivery`).
2556
- - **`search`** covers chat lines (`search`) with persons and in channels; agent mail is not
2557
- searched.
2804
+ - **`read({source})`** reads the named conversation event or retained task. It preserves the daemon's consent decision.
2805
+ A held event read acknowledges nothing. `read({cid})` returns a conversation page with opaque cursors.
2806
+ - **`search`** uses `conv.search` within the daemon's access rules.
2558
2807
  - **`tasks`** lists `task.list` with roles `mine` (requester) and `asked` (executor) and short state
2559
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}`.
2560
2814
  - **`task_update`** (executor) and **`task_cancel`** (requester) map to `task.update` and
2561
2815
  `task.cancel`; a cancel of a finished task returns its state and sends nothing.
2562
2816
  - **Errors.** A tool error is `{error: {code, message, remedy}}`: `code` from §13, a `message` that
@@ -2593,12 +2847,12 @@ contacts, changes devices or administers a team or channel.
2593
2847
 
2594
2848
  ### Clients
2595
2849
 
2596
- - **OpenCode plugin.** Tools under namespace `messaging`: `contacts`, `send`, `inbox`, `read`,
2597
- `search`, `tasks`, `task_update` and `task_cancel`. Every send asserts `messaging.send` with the
2850
+ - **OpenCode plugin.** Tools under namespace `messaging`: `contacts`, `conversations`, `send`, `inbox`, `read`,
2851
+ `search`, `context`, `tasks`, `task_update` and `task_cancel`. Every send asserts `messaging.send` with the
2598
2852
  address as its resource (`person/agent`, `#channel`, or `person` and `person/`), and every read of
2599
2853
  a conversation asserts `messaging.read` with the conversation (a `person:` one asks first by
2600
2854
  default). Inbound content is framed as untrusted data. Details are in the plugin's repository, `joe-broadhead/opencode-messaging` (`docs/`).
2601
- - It never calls `conv.markRead`: read state is the person's.
2855
+ - It never changes human read state.
2602
2856
  - With wakes on, it registers each session with `host: {kind: "opencode", location, sessionID,
2603
2857
  pid}`: its location directory, the OpenCode session id, and the server's pid.
2604
2858
  - Its RPC `messaging.opencode/wake {sessionID, envelopeIds}` serves the wake bridge (§8.6). It checks
@@ -2622,16 +2876,10 @@ contacts, changes devices or administers a team or channel.
2622
2876
  item only after the host recorded the tool result with the note intact; until then the item is
2623
2877
  only offered, and an offer that fails or is cut goes back to waiting. No wire change.
2624
2878
  - **MCP server.** `messaging-mcp` (stdio) for other agents. It registers one `pull`-tier session and
2625
- subscribes to its events. Its tools are `messaging_contacts`, `messaging_send`, `messaging_inbox`,
2626
- `messaging_read`, `messaging_search`, `messaging_tasks`, `messaging_task_update` and
2627
- `messaging_task_cancel`, over `daemon.status`, `contacts.list`, `msg.send`, `item.read`,
2628
- `inbox.list/get/ack`, `conv.read`, `task.list/get/update/cancel` and `search`.
2629
- - MCP has no per-conversation approval of its own, so chats are opt-in: the user names channels
2630
- when configuring the server, with `--channel <name>` (repeatable). `send`, `read` and `search`
2631
- reach those channels only, and are refused (`forbidden`) for any other channel before any daemon
2632
- call. Conversations with persons (DM lines) are never available over MCP: a line to a person, or
2633
- `read`/`search` with one, is `forbidden`. It never calls `conv.subscribe`, `conv.markRead`,
2634
- `person.send` or `channel.*`.
2879
+ subscribes to its events. It registers the ten tools in `ConversationTools`: `messaging_contacts`, `messaging_conversations`, `messaging_send`, `messaging_inbox`, `messaging_read`, `messaging_search`, `messaging_context`, `messaging_tasks`, `messaging_task_update` and `messaging_task_cancel`.
2880
+ Lines and conversation reads use `conv.*`. Retained tasks use `task.create/continue/list/get/update/cancel` and `inbox.list/get/ack`.
2881
+ - The daemon controls access through `conv.access`. Contacts expose only accessible channel cards.
2882
+ MCP uses the agent socket and never calls human-tier methods or changes human read state.
2635
2883
  - It has no peer-turn guard: its agent reads every request itself, with its own tools. It reports
2636
2884
  the daemon's `consent` for each item; where the daemon gives none (a task record), `self` for the
2637
2885
  session's own person and `trusted` for everyone else.
@@ -2768,7 +3016,7 @@ contacts, changes devices or administers a team or channel.
2768
3016
  `prompt` when a dialog on every daemon start is acceptable.
2769
3017
  - **Local IPC.** The sockets trust users, not processes (§8.2).
2770
3018
  - The daemon enforces two sockets: `run/agent.sock` serves only `AGENT_METHODS`, so never a line as
2771
- the person (`person.send`); `run/admin.sock` serves everything.
3019
+ the person; `run/admin.sock` serves everything. Agent conversation calls require a registered session.
2772
3020
  - Under one OS user that is no boundary: any process of the user can open `run/admin.sock`, call
2773
3021
  `team.*` and `daemon.stop`, and act for any session whose key it knows: session keys are bearer
2774
3022
  credentials. The tiers keep those methods off agent tool paths, nothing more.
@@ -2813,6 +3061,10 @@ contacts, changes devices or administers a team or channel.
2813
3061
 
2814
3062
  ## 13. Error codes
2815
3063
 
3064
+ Tool errors use the codes in this section for both conversation and task sources (§11).
3065
+ An unknown source is `not_found`. A source that combines task and conversation identifiers is `invalid_request`.
3066
+ Oversized model-visible text is truncated with the explicit marker in §11, rather than reported as `too_large`.
3067
+
2816
3068
  | Code | Meaning |
2817
3069
  |---|---|
2818
3070
  | invalid_request | malformed input or schema violation |
@@ -2825,6 +3077,7 @@ contacts, changes devices or administers a team or channel.
2825
3077
  | refused | recipient policy refuses this sender |
2826
3078
  | declined | the recipient's person declined a held request (§6.9) |
2827
3079
  | request_required | the recipient's person has not accepted this sender: another org's person needs a message request first (§6.9) |
3080
+ | peer_outdated | the other person's devices all run a Messaging from before conversations (`conv.caps`, §7); nothing was created or sent. They need to update first |
2828
3081
  | rate_limited | too many envelopes; retry later |
2829
3082
  | loop_guard | agent-to-agent loop guard tripped |
2830
3083
  | too_large | size limit exceeded |
@@ -2838,11 +3091,12 @@ contacts, changes devices or administers a team or channel.
2838
3091
  | guarded_turn | a guarded peer turn tried to send to anyone but the person who started it (§11; a client code, never on the wire) |
2839
3092
  | roster_conflict | roster entry does not extend the current head |
2840
3093
  | invite_invalid | invite unknown, expired, revoked, used or mismatched |
2841
- | no_identity | the daemon has not created or joined a team |
3094
+ | no_identity | the daemon has no usable identity: no team, or a directory device not yet bound with published keys (§15.9) |
2842
3095
  | binding_invalid | a device binding, a device-log entry or its authority failed verification (§15) |
2843
- | jelly_unavailable | Jelly ID's key set or key history could not be fetched (§15.2); never reported as `binding_invalid` |
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` |
2844
3097
  | log_conflict | a device-log entry does not extend the log's head (§15.3); resync and retry |
2845
- | 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 |
2846
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 |
2847
3101
  | device_limit | the person already has `LIMITS.devicesPerPerson` current devices (§15.3) |
2848
3102
  | admission_required | `dir.bind` of an entry that is not a device binding itself with a `jelly` authority (§15.10) |
@@ -2852,8 +3106,8 @@ contacts, changes devices or administers a team or channel.
2852
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) |
2853
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") |
2854
3108
  | login_denied | the person denied a device sign-in at Jelly ID, or the account is disabled or being deleted (§15.9) |
2855
- | grant_required | `deployment: "jelly"`: a human- or admin-tier call carried no action grant (§8.11) |
2856
- | 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") |
2857
3111
  | ticket_required | `deployment: "jelly"`: `session.register` carried no session ticket (§8.11) |
2858
3112
  | ticket_invalid | `deployment: "jelly"`: the session ticket did not verify, is out of its lifetime, or names another session or agent (§8.11) |
2859
3113
  | daemon_unavailable | a client cannot reach the daemon |
@@ -3360,11 +3614,9 @@ when a restore does not reach it: keep it on other storage than the database, or
3360
3614
  who accepts the fork on purpose moves the witness aside and starts again.
3361
3615
 
3362
3616
  **Resolution.** From each person's newest verified Jelly token the directory keeps their name, their email when
3363
- Jelly ID proved it, and from that email their **handle** and **org** (`emailHandle`, `emailOrg`):
3617
+ Jelly ID proved it, and from that email their **handle** and **org** (`hostedHandle`, `emailOrg`):
3364
3618
 
3365
- - the handle is the email's local part lower-cased, each run of characters outside `[a-z0-9]` made one `-`, `-`
3366
- trimmed from both ends, cut to 30 characters (trimmed again), and `u-` put in front when it does not start with
3367
- a letter;
3619
+ - the handle uses `hostedHandle`, exactly as §15.8 defines it;
3368
3620
  - the org is `domain:<d>` when the email's domain `d` is in the operator's `orgDomains` (`--org-domains`), and
3369
3621
  none otherwise: such a person is an individual, in no org.
3370
3622
 
@@ -3414,9 +3666,12 @@ anchor must be an `epoch` anchor whose root is the directory's at that size (`di
3414
3666
  `rate_limited` with `retryAfterMs`. The per-day counts, and the copies of an open request, are kept in memory,
3415
3667
  from UTC midnight.
3416
3668
  - **Presence** (`to: {team: true}`) goes only to devices that `presence.watch` the person and are in the person's
3417
- **audience**: the person themselves, the same org, or persons who each sent the other an envelope other than a
3418
- receipt in the last 30 days. A
3419
- 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.
3420
3675
 
3421
3676
  **Erasure.** Erasing a person appends an `unbind` of each current device, sets their map value to the tombstone
3422
3677
  (§15.5) at the next epoch, deletes their profile, every log body, witness and keys statement and every pair they
@@ -3455,17 +3710,45 @@ five minutes, until it answers, so a new directory becomes ready without waiting
3455
3710
  `dir_keys`, `dir_epochs`, `dir_map` (each person's committed value), `dir_rotations`, `dir_pairs` (with
3456
3711
  `line_at`, when the pair last carried an envelope other than a receipt), `dir_jelly_keys`, `dir_feed`, and
3457
3712
  `dir_wakes`: each Jelly host's device's armed wake ticket, its `agents` and `awaiting`, its `exp` and when it may
3458
- 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
3459
3715
  and refuses to start if they disagree with the last tree head.
3460
3716
 
3461
3717
  ## 16. Conversations
3462
3718
 
3719
+ **Guest acceptance (X3-1).** An invited participant is not a content reader.
3720
+ The relay serves authenticated control history and heads to invitees, with message digests instead of ciphertext and a zero share cursor.
3721
+ It pushes invitation operations, never message content or keys, to a person represented only by invited seats.
3722
+ `ConvLog.checkShare` requires an accepted recipient. For a group or history `none`, acceptance (including seating an invited
3723
+ agent) is the effective addition: it updates `since` and requires an `added` rekey before messages or shares.
3724
+ Inviting alone does not start that epoch. Shared channels keep their declared shared-history exposure after acceptance.
3725
+ A guest cannot invite or change to another role, even via an intermediate member role. Agents inherit their owner's
3726
+ guest restriction; a guest agent needs an accepted, visible guest owner and that owner's signed seat. Live org checks
3727
+ cover agent principals too; two persons without an org are not implicitly one org.
3728
+
3729
+ A guest's `accept` carries `invitation`, a `GuestInvitation` signed under `DOMAIN.guestInvitation`.
3730
+ The preview binds the invitation ID, inviter device, realm, conversation, invitee, exact op head, expiry, current audience and exposure policies.
3731
+ The device that signed the original add or create signs this bounded preview. The inviter seals it to the invitee without history or keys.
3732
+ The accept event signs the same preview. A changed audience, policy or op head requires a fresh preview.
3733
+ After acceptance, the invitee fetches the log and verifies the invitation independently before content or key use.
3734
+ `guestHistoryExposure` supplies explicit text for shared history and history from the acceptance boundary.
3735
+
3736
+ **Agent-DM admission.** The recipient's `ask` policy controls an agent-only DM invitation, including invitations across orgs.
3737
+ `open` seats the recipient agent, `request` holds the invitation for the person, and `no` silently declines it locally.
3738
+ `request.list` exposes a held invitation as `{kind:"agent-invitation",id,cid,from,to,at}`, without content or keys.
3739
+ `request.answer` accepts `accept` or `decline`. Its preview identifies the sender and target agent and reports the affected count.
3740
+ With `all`, the answer applies to held agent invitations from the same person.
3741
+
3742
+ The daemon records invitation-only history gaps durably. After acceptance, it fetches those entries again and verifies each signature against its stored digest.
3743
+ It replaces only matching gaps and applies the conversation's history policy before content use.
3744
+ This recovery survives restart. It never restores messages that local retention expired after acceptance.
3745
+
3463
3746
  A conversation is one hash-chained log of signed events that the relay sequences and members verify. Membership,
3464
3747
  roles, epochs and settings are a function of the log. Message bodies are sealed under per-device sender keys, so a
3465
3748
  post is one ciphertext whatever the number of members. The schemas are in `schema/conversation.ts`, the rules in
3466
- `conversation/` of core, and the vectors in `docs/spec/vectors/conversation.json`. The relay frames, the daemon's
3467
- storage and the IPC surface that carry them are not specified yet; `docs/design/conversations.md` describes them.
3468
- Until they land, chats are as §6.8 says.
3749
+ `conversation/` of core, and the vectors in `docs/spec/vectors/conversation.json`. The relay carries the logs in
3750
+ the conversation frames of §7. The daemon's storage and the IPC surface are not specified yet;
3751
+ `docs/design/conversations.md` describes them. Until they land, chats are as §6.8 says.
3469
3752
 
3470
3753
  ### 16.1 Identifiers
3471
3754
 
@@ -3500,19 +3783,23 @@ Every event has a header and a body. The header is:
3500
3783
  - `opHead` is the last op the author applied: its `seq` and `h(seq)`, or `{0, h(0)}` before any.
3501
3784
  - `seen` is the head the author had, for gossip (§16.4).
3502
3785
  - `class` is:
3503
- - `op`: a control op in plaintext (`ConvOp`), because the relay routes and checks authority by it;
3786
+ - `op`: a control op (`ConvOp`), because the relay routes and checks authority by it. Every piece of state is an
3787
+ op, and ops never expire (§16.4); an op whose content must stay private carries it sealed (§16.5);
3504
3788
  - `msg`: a sealed `MsgBody`, with the `epoch` and the ratchet index `idx` of its key (§16.5). `wake` (at most
3505
- `LIMITS.wakeHints` (5)) names sessions of `by.person` the event may wake; it is the only plaintext hint;
3506
- - `keys`: sender-key shares, each sealed to one device (§16.6).
3789
+ `LIMITS.wakeHints` (5)) names sessions of `by.person` the event may wake; it is the only plaintext hint. The
3790
+ author's daemon sets it only on a line that addresses the author's own agents, and the relay acts on it only for
3791
+ the authenticated author's person's devices (§7, "Wake tickets");
3792
+ - `keys`: a sender-key share sealed to one device (§16.6). It is signed like every event but is **not an entry
3793
+ of the log**: the relay keeps it for its recipient alone.
3507
3794
  - `sig` covers the whole event: `Ed25519(by.device, "messaging/1/conv-event\n" || JCS(event without sig))`.
3508
3795
  - An event is at most `LIMITS.convEventBytes` (256 KiB) of JCS (`too_large`). Every object is strict: an unknown
3509
3796
  member is `invalid_request`.
3510
3797
 
3511
- The relay stores and serves **entries**: `{seq, prev, event}`, where `seq` counts from 1 and `prev` is `h(seq − 1)`.
3512
- The relay does not sign entries; members verify the chain (§16.4).
3798
+ The relay stores and serves **entries**: `{seq, prev, event}` of ops and msgs, where `seq` counts from 1 and `prev`
3799
+ is `h(seq − 1)`. The relay does not sign entries; members verify the chain (§16.4).
3513
3800
 
3514
3801
  **Ops** (`ConvOp`): `create`, `add`, `accept`, `join`, `leave`, `remove`, `role`, `seat`, `settings`,
3515
- `archive` and `rekey`.
3802
+ `archive`, `rekey`, `policy`, `meta` and `setting`.
3516
3803
 
3517
3804
  - `create` states the kind, the visibility (channels), the history policy, `retainDays` (1 to
3518
3805
  `LIMITS.retainDaysMax` (365)), the org a public channel is open to, the participants as seats `{p, role,
@@ -3523,7 +3810,13 @@ The relay does not sign entries; members verify the chain (§16.4).
3523
3810
  `none`), and accepts its invite. Only an agent's seat carries `device` or `respondTo`.
3524
3811
  - `settings` changes the visibility, history, retention or permits. `archive {on}` archives or restores.
3525
3812
  - `rekey {epoch, cause}` starts the next epoch; `cause` is `removed`, `device`, `added`, `settings` or
3526
- `schedule`.
3813
+ `schedule`. A device rekey also names the removed `device` and may give `until`, a chain position strictly before
3814
+ the rekey. Other causes carry neither field.
3815
+ - `policy {agentContext?, bridges?}` sets one or both, in plaintext (§16.8, §16.10). In a dm or a group it is the
3816
+ author's vote.
3817
+ - `meta {epoch, idx, sealed}`: a group's or channel's `{name?, topic?, purpose?}`, sealed (§16.5).
3818
+ - `setting {kind, epoch, idx, sealed}`: one of the person's settings in their `self` conversation, sealed;
3819
+ `kind` is `prefs`, `grant`, `context`, `localRetention` or `link` (§16.3).
3527
3820
 
3528
3821
  ### 16.3 Message bodies
3529
3822
 
@@ -3539,17 +3832,25 @@ The plaintext of a `msg` event is one `MsgBody`, at most `LIMITS.convPlaintextBy
3539
3832
  `status` and `artifact` carry the A2A update events (§6.2) and `cancel` and `followUp` the requester's, each
3540
3833
  naming the root.
3541
3834
  - `receipt {target, stage}`: `delivered`, `handed_to_agent` or `read`.
3542
- - `meta {name?, topic?, purpose?}` and `policy {agentContext?, bridges?}`, which sets one or both (§16.8, §16.10).
3543
3835
  - `sync {sync}`: a body of the person's `self` conversation, and only there.
3544
3836
 
3837
+ A conversation's name, topic and purpose, and its policy, are not message bodies: they are state, so they are ops
3838
+ (`meta`, `policy`), which never expire.
3839
+
3545
3840
  A mention is `{p}` (a participant), `{all: "channel"}` or `{all: "here"}`. An attachment names a blob, its own
3546
3841
  AES-256-GCM key and nonce, its SHA-256 and size (at most `LIMITS.blobBytes` (25 MiB)), a media type and a name.
3547
3842
 
3548
3843
  **The self conversation.** A person's `self` conversation has one participant, the person, and history `shared`.
3549
- Its `sync` bodies (`SyncBody`) are `read {cid, upTo, thread?}`, `unread {cid, from}`, `prefs {cid, notify,
3550
- muteUntil?, receipts?}`, `follow`, `save`, `draft`, `joined`, `grant` (a consent grant, §6.9, verified over its
3551
- raw value), `context {rules}` (§16.8), `localRetention {cid, days}` and `keys {cid, shares}` (at most
3552
- `LIMITS.keyShares` (512) chain keys) and `link {link}` (§16.10). The last writer wins by `seq`.
3844
+ It holds two kinds of state:
3845
+
3846
+ - **Passing state**, in `sync` bodies (`SyncBody`), which expire with the retention: `read {cid, upTo, thread?}`,
3847
+ `unread {cid, from}`, `follow`, `save`, `draft`, `joined` and `keys {cid, shares}` (at most `LIMITS.keyShares`
3848
+ (512) chain keys).
3849
+ - **Settings**, in `setting` ops (`SettingBody`, sealed), which never expire: `prefs {cid, notify, muteUntil?,
3850
+ receipts?}`, `grant` (a consent grant, §6.9, verified over its raw value), `context {rules}` (§16.8),
3851
+ `localRetention {cid, days}` and `link {link}` (§16.10). The op's `kind` is the body's `s`.
3852
+
3853
+ The last writer wins by `seq`.
3553
3854
 
3554
3855
  ### 16.4 The hash chain and gossip
3555
3856
 
@@ -3560,9 +3861,24 @@ raw value), `context {rules}` (§16.8), `localRetention {cid, days}` and `keys {
3560
3861
  fetches up to `seen.seq` first), `consistent` when its `h(seen.seq)` is `seen.hash`, `diverged` otherwise. A
3561
3862
  divergence proves that the relay showed two members different logs; the signed event is the evidence. `seen`
3562
3863
  at or past the event's own `seq` is `invalid_request`.
3563
- - **Retention.** When the relay expires a `msg` or `keys` entry it keeps the entry's `seq`, `prev` and the digest
3564
- `SHA-256(JCS(event))`, so the chain stays verifiable (`ConvLog.applyExpired`). Ops never expire: an op head that
3565
- names an expired entry is `conv_conflict`, so a relay that withholds an op as expired is caught by the next op.
3864
+ - **Device cuts.** A `rekey` with cause `device` ends the named device's authority in this conversation: later
3865
+ entries by that device are invalid. If `until` is present, known entries by that device after `until` and before
3866
+ the rekey are cut from state. An absent `until` makes no earlier cut. The position need not name that device's
3867
+ entry: an expired message is a digest with no author or state effect. Replay collects the cuts, verifies every
3868
+ chain link, and does not apply cut entries; a cut rekey cannot cut another device. A cut that removes genesis or
3869
+ leaves later entries unauthorized is refused. A live verifier checks that the named device has been removed in
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.
3874
+ - **Retention.** Only `msg` entries expire. When the relay prunes a `msg` entry older than the conversation's
3875
+ `retainDays` it keeps the entry's `seq`, `prev` and the digest `SHA-256(JCS(event))` and serves those
3876
+ (`ConvExpiredEntry`), so the chain stays verifiable (`ConvLog.applyExpired`). **Ops never expire**: membership,
3877
+ roles, epochs, settings, policy votes, sealed meta and the `self` conversation's settings are ops, so a member
3878
+ that replays the log after pruning, or a joiner reading it from genesis, computes the same state as a member
3879
+ that kept every body. The create (seq 1) never expires (`invalid_request`), and an op head that names an
3880
+ expired entry is `conv_conflict`, so a relay that withholds an op as expired is caught by the next op. A key
3881
+ share (§16.6) is not an entry; the relay deletes it after the retention.
3566
3882
 
3567
3883
  ### 16.5 Sender keys
3568
3884
 
@@ -3585,6 +3901,10 @@ raw value), `context {rules}` (§16.8), `localRetention {cid, days}` and `keys {
3585
3901
  of order it keeps at most `LIMITS.skippedKeys` (1000) derived but unused keys per (epoch, sender device),
3586
3902
  dropping the lowest indexes first; a message further than that past the chain is `too_large`, and one whose key
3587
3903
  is used or dropped is `not_found`. It keeps each share's chain key at the share's index, to share history (§16.6).
3904
+ - **Sealed ops.** A `meta` or `setting` op seals its content (`ConvMeta`, or a `SettingBody` of the op's `kind`)
3905
+ the same way, under the author device's key at the op's (`epoch`, `idx`), with `aad = JCS(event without sig, the
3906
+ op without sealed)`, so the AAD binds the op's kind and key index too. The plaintext is at most
3907
+ `LIMITS.convPlaintextBytes`. A sealed op takes one index of the author's chain, as a message does.
3588
3908
  - **Epochs.** A `rekey` op starts the next epoch. Each sender then starts a new chain and shares it only with the
3589
3909
  devices of the participant persons at the rekey's `seq`.
3590
3910
  - Authenticity comes from the event signature, not from the key: a member who holds a sender key still cannot
@@ -3593,9 +3913,9 @@ raw value), `context {rules}` (§16.8), `localRetention {cid, days}` and `keys {
3593
3913
 
3594
3914
  ### 16.6 Key shares
3595
3915
 
3596
- A `keys` event's body is `{shares: [{to: {person, device}, epk, nonce, ct}]}`, one entry per recipient device
3597
- (at most `LIMITS.keySharesPerEvent` (500); a larger share is split over several events). Each `ct` seals
3598
- `JCS({shares: [KeyShare]})`, where a `KeyShare` is `{epoch, device, idx, ck}` (at most `LIMITS.keyShares` (512)):
3916
+ A `keys` event goes to **one recipient device**. Its body is `{to: {person, device}, epk, nonce, ct}`; a sender
3917
+ sends one event per recipient device. `ct` seals `JCS({shares: [KeyShare]})`, where a `KeyShare` is `{epoch,
3918
+ device, idx, ck}` (at most `LIMITS.keyShares` (512)):
3599
3919
 
3600
3920
  1. Generate an ephemeral X25519 key pair; `epk` is its raw public key, `nonce` 12 random bytes.
3601
3921
  2. `shared = X25519(eph_priv, recipient.enc)`, the recipient device's key as the realm verified it (§4, §15.7).
@@ -3606,6 +3926,23 @@ A `keys` event's body is `{shares: [{to: {person, device}, epk, nonce, ct}]}`, o
3606
3926
  The AAD binds a share to its event and its recipient: moving it to another event or device makes it fail. A
3607
3927
  receiver keeps the first share for a given (epoch, device, idx); a later one with another key changes nothing.
3608
3928
 
3929
+ **Not an entry.** A `keys` event is not in the hash chain (`ConvLog.apply` refuses one, `invalid_request`). The
3930
+ relay keeps it for its recipient and serves it to no one else (`ConvShareEntry {n, event}`, §7): each device
3931
+ fetches only its own shares, so a rekey costs a device one share per sender, not every share of every sender. A
3932
+ recipient checks a share with `ConvLog.checkShare`, in the state at the share's `opHead`:
3933
+
3934
+ 1. I-JSON, size and the schema; the realm and the `cid` (`invalid_request`).
3935
+ 2. The signature over the raw event (`bad_signature`).
3936
+ 3. Live: `ts` within `LIMITS.clockSkewMs` (`expired`), and `by.device` a current device of `by.person`
3937
+ (`unknown_device`).
3938
+ 4. `by.agent` absent (`forbidden`); `opHead` names an op of the log (`conv_conflict`).
3939
+ 5. At `opHead`, the author's person is a member participant person (`not_member`) and `to.person` a participant
3940
+ person (`forbidden`); live, `to.device` a current device of `to.person` (`unknown_device`).
3941
+
3942
+ The relay also requires `opHead` to be the current op head (§7), so no share reaches a device whose person was
3943
+ removed since. A relay that withholds a share is not hidden: the recipient cannot open that sender's messages,
3944
+ and shows that it waits for keys.
3945
+
3609
3946
  ### 16.7 Authority
3610
3947
 
3611
3948
  `ConvLog.apply` checks each entry, in order, against the state after every earlier entry, and a failed check
@@ -3619,7 +3956,8 @@ changes nothing:
3619
3956
  5. `id` not already in the log (`duplicate`); `seen` before the event (`invalid_request`).
3620
3957
  6. The first entry is `create`, and there is only one (`conv_conflict`).
3621
3958
  7. For an `op`: `opHead` is the current op (`conv_conflict`); `by.agent` is absent: an agent never writes an op
3622
- (`forbidden`). For a `msg` or `keys`: `opHead` names an op of this log, or `{0, h(0)}` (`conv_conflict`).
3959
+ (`forbidden`). For a `msg`: `opHead` names an op of this log, or `{0, h(0)}` (`conv_conflict`). A `keys` event
3960
+ is never an entry (`invalid_request`, §16.6).
3623
3961
  8. The rules below. An author who is not a participant is `not_member`, one who lacks the authority `forbidden`, a
3624
3962
  target that is not a message of this log `not_found`, and a target in the wrong state `invalid_state`.
3625
3963
 
@@ -3639,6 +3977,9 @@ participant's.
3639
3977
  | `seat` | The target is an agent participant whose person is the author. It becomes `member`. |
3640
3978
  | `settings`, `archive` | A group or channel; the author is an admin. Only a channel has a visibility, and a public one needs an org (`invalid_state`). |
3641
3979
  | `rekey` | The author's person is a member participant person; `epoch` is the current one plus 1 (`invalid_request`). The cause is due (`invalid_state`), or is `device`, or is `schedule` and the epoch began at least `LIMITS.rekeyScheduleMs` (30 days) before. |
3980
+ | `policy` | Not in a `self` conversation. The author is a member; in a channel, an admin; in a dm or group, any participant person, as a vote. |
3981
+ | `meta` | A group or channel (never a dm or self); the author is a member, and in a channel an admin. Its `epoch` is the current epoch and no rekey is due (`stale_epoch`). |
3982
+ | `setting` | Only in a `self` conversation; the author is its person. Its `epoch` is the current epoch and no rekey is due (`stale_epoch`). |
3642
3983
  | any `msg` | The author's participant is a member; `epoch` is the current epoch and no rekey is due (`stale_epoch`). |
3643
3984
  | `line` | Not archived; an admin when `post` is `admins`. A guest may post. `thread` and `answers` name messages of the log. A line with `via` is a person's, never an agent's. |
3644
3985
  | `edit` | Not archived; the target is a line, not deleted, by the same participant (person and agent). |
@@ -3647,10 +3988,8 @@ participant's.
3647
3988
  | `pin` | Not archived; an admin when `pins` is `admins`; the target is not deleted; at most `LIMITS.pins` (100) pinned (`too_large`). |
3648
3989
  | `task` | A `request` as `line`, to a member agent participant (`not_found`). `status` and `artifact` from that agent; `cancel` and `followUp` from the requester. The root is a task request (`invalid_state`). |
3649
3990
  | `receipt` | The target is a message of the log. |
3650
- | `meta` | Not in a dm; in a channel, an admin. |
3651
- | `policy` | Not by an agent, and not in a `self` conversation. In a channel, an admin; in a dm or group, any participant person, as a vote. |
3652
3991
  | `sync` | Only in a `self` conversation, where no other body goes (`invalid_request`). |
3653
- | `keys` | The author's person is a member participant person and `by.agent` is absent. Every share goes to a participant person (`forbidden`); live, to a current device of that person (`unknown_device`). |
3992
+ | `keys` | Not an entry: §16.6 (`ConvLog.checkShare`). |
3654
3993
 
3655
3994
  **State.** A participant starts as `member`, except an agent whose person is not the author (`invited` until its
3656
3995
  person seats it) and a `guest` (`invited` until they accept). Live, adding a person of another org as anything but a
@@ -3658,9 +3997,10 @@ guest is `forbidden`. A rekey is **due** after a removal or a leave (cause `remo
3658
3997
  and a change of the history policy (`settings`); a channel does not rekey on add. While a rekey is due, every `msg`
3659
3998
  is `stale_epoch`: no message is sealed under a key a removed device holds. A `rekey` clears every due cause.
3660
3999
 
3661
- `ConvLog` keeps the state after every entry: `at(seq)` is the state of a log holding only entries 1 to `seq`. Ops
3662
- and `policy` change it; the rules that read a message body (edit, delete, reactions, pins, tasks) run only when the
3663
- caller passes the opened body, so the relay, which cannot open bodies, applies the header rules alone.
4000
+ `ConvLog` keeps the state after every entry: `at(seq)` is the state of a log holding only entries 1 to `seq`. Only
4001
+ ops change it, so the relay, which cannot open bodies, and a member whose message bodies expired compute the same
4002
+ state. The rules that read a message body (edit, delete, reactions, pins, tasks) run only when the caller passes
4003
+ the opened body.
3664
4004
 
3665
4005
  ### 16.8 Agents and history as context
3666
4006
 
@@ -3670,7 +4010,8 @@ policy decides whether members' agents may mine it at all.
3670
4010
  - **Policy** (`AgentContext`): `members` (members' agents may, each under its person's rules), `seats` (only
3671
4011
  agents seated in it, by their seats) or `none`. The default is `members`. In a channel an admin sets it with a
3672
4012
  `policy` body; in a dm or group each participant person votes, and the strictest vote counts (`none` <
3673
- `seats` < `members`). A person's vote goes when they leave.
4013
+ `seats` < `members`). A person's vote goes when they leave. The policy is a `policy` op, in plaintext, so a joiner
4014
+ that holds no earlier keys knows it too.
3674
4015
  - **Rules** (`ContextRule`): `{agent, scope, access, lookbackDays?}`, where `agent` is a session name or `*`,
3675
4016
  `scope` is `{cid}` or `{kind}` with `kind` one of `own`, `dm`, `group`, `channel.private` and
3676
4017
  `channel.public`, and `access` is `allow`, `ask` or `off`. A person holds at most `LIMITS.contextRules` (500),
@@ -3714,7 +4055,7 @@ policy decides whether members' agents may mine it at all.
3714
4055
  | `LIMITS.retainDaysDefault`, `LIMITS.retainDaysMax` | 90 days, 365 days |
3715
4056
  | `LIMITS.skippedKeys`, `LIMITS.chainMessages`, `LIMITS.chainMaxMs` | 1000, 500, 7 days |
3716
4057
  | `LIMITS.rekeyScheduleMs` | 30 days |
3717
- | `LIMITS.keyShares`, `LIMITS.keySharesPerEvent` | 512 chain keys per share, 500 devices per `keys` event |
4058
+ | `LIMITS.keyShares` | 512 chain keys per share |
3718
4059
  | `LIMITS.wakeHints`, `LIMITS.contextRules` | 5, 500 |
3719
4060
 
3720
4061
  ### 16.10 Linked channels (reserved)
@@ -3727,8 +4068,8 @@ agents from a phone. The fields below are part of the schema and are validated,
3727
4068
  signed with the event, so every member sees it. Only a person writes it (`forbidden` on an agent's line, §16.7).
3728
4069
  The person's own devices also check it against the person's links (`checkVia`): a link of that channel, hosted by
3729
4070
  the authoring device, in force. Other members cannot see the links and do not check that.
3730
- - **`link`**, a body of the person's `self` conversation: `{id, channel, account, host, proof, authority, linkedAt,
3731
- revokedAt?}`.
4071
+ - **`link`**, a setting of the person's `self` conversation (a sealed `setting` op of kind `link`, which never
4072
+ expires): `{id, channel, account, host, proof, authority, linkedAt, revokedAt?}`.
3732
4073
  - `account` is the channel's id for the person's account; it is never in the device log, the directory or
3733
4074
  presence.
3734
4075
  - `host` is the device that runs the bridge.
@@ -3736,9 +4077,77 @@ agents from a phone. The fields below are part of the schema and are validated,
3736
4077
  person matched on both sides.
3737
4078
  - `authority` is a non-empty subset of `chat` and `view` (§8.11), and never more.
3738
4079
  - For each `id` the record written last is the one in force, unless it has `revokedAt` (`currentLinks`).
3739
- - **`bridges`** in `policy`: who may mirror the conversation to a linked channel: `none` (the default), `admins` or
4080
+ - **`bridges`** in the `policy` op: who may mirror the conversation to a linked channel: `none` (the default), `admins` or
3740
4081
  `members`. In a channel an admin sets it. In a dm or group the strictest vote of the participant persons counts,
3741
4082
  and a person who has not voted counts as `none`, so bridging needs every participant person's vote.
3742
4083
  - **Reader ids** (`ReaderId`): a person, or `ext:<channel>:<link id>` for a link's channel provider. They are not on
3743
4084
  the wire; the audience guard's reader sets use them.
3744
4085
  - A foreign sender on a bridged channel (`via.sender`) is not part of v1: the schema refuses it.
4086
+
4087
+ ### 16.11 Same-realm contact request cut-over
4088
+
4089
+ This profile replaces the first-message import rules of §6.9 for conversation DMs. It does not introduce cross-realm addresses.
4090
+ The normative shapes are in `schema/contact-request.ts`. The request plaintext uses `RequestImport`, separate from the ordinary `MsgBody` parser.
4091
+ The cut-over engine uses `openRequestImport` and `applyRequestEntry` for this profile. Source attribution comes from verified request evidence, never the wrapper author.
4092
+
4093
+ **Portable source proof.** A new request contains a sender-signed descriptor and a sealed opening:
4094
+
4095
+ - The descriptor binds `v`, `realm`, `cid`, `from`, `to`, `id`, `issuedAt`, `expiresAt`, `commitment` and `by`.
4096
+ - The sender signs its raw JCS bytes without `sig`, under `DOMAIN.request` (`messaging/1/contact-request\n`).
4097
+ - The opening contains a fresh random 256-bit `salt` and 1–500 UTF-16 code units of `text`.
4098
+ - The commitment is SHA-256 of `DOMAIN.requestContent` and JCS of the opening.
4099
+ - The descriptor digest is SHA-256 of `DOMAIN.requestDigest` and JCS of the signed descriptor.
4100
+ - Both device signatures require bindings from retained, verified identity history. An absent binding fails closed.
4101
+
4102
+ The request names the deterministic DM of two distinct persons. The consent window cannot exceed `LIMITS.ttlMs`.
4103
+ Every device copy carries the same descriptor and opening. Only sealed envelopes carry the opening before acceptance.
4104
+ Requests expose no agent address, task, mention, attachment, context-index entry or wake hint.
4105
+ The envelope payload has `conv: {kind: "request", proof: PortableRequest}`. Its one text part and message ID match the signed opening and request ID.
4106
+ The envelope has human ends, class `request`, and the descriptor's expiry. The cut-over handler rejects a payload without `proof` and asks for resend.
4107
+ The optional field in the stored payload schema permits reading retained old records. It does not authorize old plaintext for a conversation import.
4108
+
4109
+ **Terminal decision.** The recipient signs `RequestDecision` under `DOMAIN.requestDecision` (`messaging/1/contact-request-decision\n`).
4110
+ The decision binds the descriptor digest, request ID, realm, DM, both persons, answer and decision time.
4111
+ The answer is `accept` or `decline`. A local block sends `decline` and keeps the blocking grant private.
4112
+ The decision time must be within the original consent window. Retries preserve the signed decision and its timestamp.
4113
+
4114
+ `request.settle {req, descriptor, decision}` stores the first valid terminal decision in the recipient mailbox.
4115
+ Only the connected recipient device can submit its own signed decision. A later conflicting decision returns the first decision.
4116
+ `request.state {req, descriptor}` returns that decision to either named person, or no decision before settlement.
4117
+ Both frames answer `request.settled {req, id, decision?}`. They expose neither salt nor text to the relay.
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.
4120
+
4121
+ **Permanent activation.** An external human DM declares `create.admission: "request"`.
4122
+ The relay refuses an external DM without this declaration. Same-org DMs retain implicit org admission.
4123
+ Before activation, content and key shares fail with `request_required`. The recipient appends `activate {proof: {descriptor, decision}}` after mailbox reconciliation.
4124
+ Device-removal rekeys can precede activation. They still require the normal removed-device and log-authority checks, and grant no content access.
4125
+ The activation is a permanent op. It reserves the request ID for exactly one import.
4126
+
4127
+ The recipient seals `RequestImport {t: "request.import", descriptor, decision, opening}` under its conversation sender key.
4128
+ The wrapper has `id` and `request` equal to the reserved request ID. It has no agent author or wake hint.
4129
+ Members verify both proofs and the opening before they attribute text to the original sender.
4130
+ The recipient then appends `request.complete {request, seq, digest}`. The digest is SHA-256 of JCS of the exact imported event.
4131
+ This permanent completion survives deletion and retention. Ordinary messages wait until all accepted reservations have completed imports.
4132
+
4133
+ Duplicate importers fetch the winning event and verify its exact proofs and opening. A different body at the same ID is a conflict.
4134
+ The source sender need not be online. Crossed requests use one DM but keep separate IDs, answers and imports.
4135
+ The import is immutable. Only its original sender can delete its text. The wrapper author gains no source-author permissions.
4136
+ After retention, members verify the permanent activation and completion without reconstructing text.
4137
+
4138
+ **Existing contacts.** Each person signs a conversation-bound `ContactAuthorization` under `DOMAIN.contactAuthorization`.
4139
+ An activation with `proof.contacts` requires both reciprocal signatures. The daemon signs only under an existing open or muted chat grant.
4140
+ This path needs no new human prompt and imports no request text. A contact card cannot substitute for either signature.
4141
+
4142
+ **Durable local lifecycle.** `ConversationRequests` stores each request and its state in namespaced rows of the existing `kv` table.
4143
+ Queued sends, submitted requests, decisions and imports survive restart. `waitingOnThem()` exposes only explicit submitted requests, not missing receipts.
4144
+ Acceptance opens only chat. A later block prevents local import and key use even after permanent activation.
4145
+ The sender receives the same error for an unknown or undiscoverable exact email.
4146
+
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.
4153
+ The cut-over never signs old plaintext for its sender or silently resends it.