@joe-broadhead/messaging 0.7.0 → 0.8.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 (97) hide show
  1. package/CHANGELOG.md +259 -2
  2. package/dist/bin/messaging-mcp.js +5484 -3858
  3. package/dist/bin/messaging-relay.js +4728 -1136
  4. package/dist/bin/messaging.js +4616 -2807
  5. package/dist/bin/messagingd.js +16451 -9444
  6. package/dist/lib/modules/core/agent/operations.js +19 -327
  7. package/dist/lib/modules/core/agent/present.js +15 -55
  8. package/dist/lib/modules/core/agent-tools.js +46 -143
  9. package/dist/lib/modules/core/consent.js +1 -1
  10. package/dist/lib/modules/core/conversation/context.js +80 -0
  11. package/dist/lib/modules/core/conversation/event.js +125 -0
  12. package/dist/lib/modules/core/conversation/guest-invitation.js +42 -0
  13. package/dist/lib/modules/core/conversation/ids.js +38 -0
  14. package/dist/lib/modules/core/conversation/link.js +35 -0
  15. package/dist/lib/modules/core/conversation/log.js +988 -0
  16. package/dist/lib/modules/core/conversation/ratchet.js +243 -0
  17. package/dist/lib/modules/core/conversation/request-log.js +158 -0
  18. package/dist/lib/modules/core/conversation/request-seal.js +41 -0
  19. package/dist/lib/modules/core/conversation/requests.js +87 -0
  20. package/dist/lib/modules/core/conversation/seal.js +251 -0
  21. package/dist/lib/modules/core/conversation-agent-tools.js +69 -0
  22. package/dist/lib/modules/core/federation/directory-proof.js +83 -0
  23. package/dist/lib/modules/core/federation/proof-state.js +167 -0
  24. package/dist/lib/modules/core/federation/roster-proof.js +74 -0
  25. package/dist/lib/modules/core/index.js +16 -2
  26. package/dist/lib/modules/core/jelly-host.js +5 -5
  27. package/dist/lib/modules/core/schema/common.js +85 -3
  28. package/dist/lib/modules/core/schema/consent.js +5 -4
  29. package/dist/lib/modules/core/schema/contact-request.js +22 -0
  30. package/dist/lib/modules/core/schema/conv-frames.js +94 -0
  31. package/dist/lib/modules/core/schema/conversation.js +326 -0
  32. package/dist/lib/modules/core/schema/envelope.js +8 -24
  33. package/dist/lib/modules/core/schema/errors.js +42 -2
  34. package/dist/lib/modules/core/schema/guest-invitation.js +13 -0
  35. package/dist/lib/modules/core/schema/index.js +6 -0
  36. package/dist/lib/modules/core/schema/ipc/consent.js +13 -9
  37. package/dist/lib/modules/core/schema/ipc/conversation-views.js +80 -0
  38. package/dist/lib/modules/core/schema/ipc/conversations.js +128 -0
  39. package/dist/lib/modules/core/schema/ipc/device.js +2 -0
  40. package/dist/lib/modules/core/schema/ipc/jelly.js +3 -38
  41. package/dist/lib/modules/core/schema/ipc/shapes.js +14 -72
  42. package/dist/lib/modules/core/schema/ipc/surfaces.js +12 -195
  43. package/dist/lib/modules/core/schema/ipc/tasks.js +31 -0
  44. package/dist/lib/modules/core/schema/ipc.js +18 -44
  45. package/dist/lib/modules/core/testing.js +1 -0
  46. package/dist/lib/modules/core/version.js +1 -1
  47. package/dist/lib/modules/ipc/client.js +10 -2
  48. package/dist/lib/modules/ipc/server.js +14 -5
  49. package/dist/lib/modules/ipc/testing.js +13 -23
  50. package/dist/spec/messaging-v1.md +835 -380
  51. package/dist/third-party-notices.md +1 -1
  52. package/dist/third-party.json +1 -1
  53. package/dist/types/core/agent/operations.d.ts +5 -52
  54. package/dist/types/core/agent/present.d.ts +6 -21
  55. package/dist/types/core/agent-tools.d.ts +277 -438
  56. package/dist/types/core/conversation/context.d.ts +56 -0
  57. package/dist/types/core/conversation/event.d.ts +36 -0
  58. package/dist/types/core/conversation/guest-invitation.d.ts +8 -0
  59. package/dist/types/core/conversation/ids.d.ts +18 -0
  60. package/dist/types/core/conversation/link.d.ts +16 -0
  61. package/dist/types/core/conversation/log.d.ts +157 -0
  62. package/dist/types/core/conversation/ratchet.d.ts +95 -0
  63. package/dist/types/core/conversation/request-log.d.ts +24 -0
  64. package/dist/types/core/conversation/request-seal.d.ts +6 -0
  65. package/dist/types/core/conversation/requests.d.ts +14 -0
  66. package/dist/types/core/conversation/seal.d.ts +70 -0
  67. package/dist/types/core/conversation-agent-tools.d.ts +918 -0
  68. package/dist/types/core/federation/directory-proof.d.ts +8 -0
  69. package/dist/types/core/federation/proof-state.d.ts +114 -0
  70. package/dist/types/core/federation/roster-proof.d.ts +7 -0
  71. package/dist/types/core/index.d.ts +15 -3
  72. package/dist/types/core/jelly-host.d.ts +1 -1
  73. package/dist/types/core/schema/common.d.ts +85 -4
  74. package/dist/types/core/schema/consent.d.ts +7 -7
  75. package/dist/types/core/schema/contact-request.d.ts +264 -0
  76. package/dist/types/core/schema/conv-frames.d.ts +1200 -0
  77. package/dist/types/core/schema/conversation.d.ts +2803 -0
  78. package/dist/types/core/schema/envelope.d.ts +89 -130
  79. package/dist/types/core/schema/errors.d.ts +40 -0
  80. package/dist/types/core/schema/guest-invitation.d.ts +51 -0
  81. package/dist/types/core/schema/index.d.ts +6 -0
  82. package/dist/types/core/schema/ipc/consent.d.ts +89 -49
  83. package/dist/types/core/schema/ipc/conversation-views.d.ts +1023 -0
  84. package/dist/types/core/schema/ipc/conversations.d.ts +3987 -0
  85. package/dist/types/core/schema/ipc/device.d.ts +10 -0
  86. package/dist/types/core/schema/ipc/jelly.d.ts +2 -105
  87. package/dist/types/core/schema/ipc/shapes.d.ts +16 -95
  88. package/dist/types/core/schema/ipc/surfaces.d.ts +28 -424
  89. package/dist/types/core/schema/ipc/tasks.d.ts +208 -0
  90. package/dist/types/core/schema/ipc.d.ts +2684 -523
  91. package/dist/types/core/schema/relay.d.ts +20 -4
  92. package/dist/types/core/testing.d.ts +1 -0
  93. package/dist/types/core/version.d.ts +1 -1
  94. package/dist/types/ipc/client.d.ts +4 -1
  95. package/dist/types/ipc/server.d.ts +10 -1
  96. package/dist/types/ipc/testing.d.ts +21 -8
  97. 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.
@@ -16,8 +79,8 @@ other, exchange messages, and delegate A2A tasks, through a relay that only sees
16
79
  (`urn:messaging:binding:relay:v1`), presence and session addressing, delivery receipts, and the
17
80
  extensions `urn:messaging:ext:provenance:v1` and `urn:messaging:ext:presence:v1`.
18
81
 
19
- Not in v1: hole-punched peer-to-peer transport, federation between relays, payments, and group
20
- (multicast) encryption.
82
+ Not in v1: hole-punched peer-to-peer transport, federation between relays, and payments. Conversations
83
+ (§16) encrypt to many members with sender keys; MLS is left to a later protocol version.
21
84
 
22
85
  ## 2. Encodings
23
86
 
@@ -54,8 +117,12 @@ Not in v1: hole-punched peer-to-peer transport, federation between relays, payme
54
117
  `messaging/1/invite\n`, `messaging/1/invite-join\n`, `messaging/1/delegation\n`, `messaging/1/grant\n`,
55
118
  `messaging/1/card\n`, and for hosted realms (§15) the person hash prefix
56
119
  `messaging/1/person\n`, `messaging/1/dir-read\n`, `messaging/1/device-log\n`, `messaging/1/device-keys\n`,
57
- `messaging/1/device-link\n`, `messaging/1/epoch\n` and `messaging/1/dir-key\n`). A signature or hash for
58
- one purpose never verifies for another.
120
+ `messaging/1/device-link\n`, `messaging/1/epoch\n` and `messaging/1/dir-key\n`, and for conversations (§16) the
121
+ event signature prefix `messaging/1/conv-event\n` and the hash prefixes `messaging/1/conv-genesis\n`,
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.
59
126
  - **Hash:** SHA-256, base64url.
60
127
 
61
128
  ## 3. Identity
@@ -474,7 +541,8 @@ Only the executor changes state (see `canTransition` in core).
474
541
  still run, and the person is away from that device. The host says so with `person.attached`; a Jelly host's device is
475
542
  away until its first `person.attached {attached: true}`, and no other device ever says `away`. A receiver shows that
476
543
  device as away (`people.get`), and an `away` older than the online window (§15.9: 150 s) means nothing.
477
- - `features` advertises optional capabilities. v1 defines `"tasks"`. A receiver ignores a feature it does not know.
544
+ - `features` advertises optional capabilities. v1 defines `"tasks"` and `"conversations"` (the daemon reads conversation
545
+ logs; `conv.caps` reads it at the relay). A receiver ignores a feature it does not know.
478
546
  - `label` must equal the sender's roster label. Receivers ignore a presence whose label differs, and
479
547
  always show the roster's label.
480
548
  - Like every envelope, presence carries the sender's `anchor` in its header (§5). Presence is
@@ -540,7 +608,7 @@ forwards a credential it was handed.
540
608
  - `exp`, the earliest along the chain.
541
609
 
542
610
  The requester's record holds the chain it sent.
543
- - **Delegating (§8.2).** A session delegates with `msg.send {kind: "task", onBehalfOf}`. Only
611
+ - **Delegating (§8.2).** A session delegates with `task.create {onBehalfOf}`. Only
544
612
  the sending daemon signs, and clients never see or build grants.
545
613
  - Without `onBehalfOf`, a task starts a new chain whose root is the sending session.
546
614
  - A root grant made over IPC is unrestricted: no `scope`, and `maxDepth = LIMITS.delegationDepth`.
@@ -574,107 +642,26 @@ forwards a credential it was handed.
574
642
  code: fenced in a model context, `terminalSafe` in a terminal, `cleanPeerText` in a permission
575
643
  prompt. A client shows a chain only when it adds something beyond `from`: depth > 1 or a scope.
576
644
 
577
- ### 6.8 Chats: DMs and channels
578
-
579
- A chat is a direct human message (room `dm:<person>`) or a channel (room `channel:<name>`). A
580
- channel has an id (a ULID chosen by its creator), a name (`ChannelName`), its creator, and a list of
581
- persons (`PersonId`s: roster person names, or hosted `p_…` ids in a directory realm), at most
582
- `LIMITS.channelPersons` including the creator. There is no group key and no chat
583
- between sessions: agent mail stays in the inbox and tasks.
584
-
585
- - **Wire.** A chat envelope is a sealed `message` with `kind: "message"`, addressed to a machine
586
- with no agent (`to: {person, device}`) (§5, "Meaning"). A DM line carries no `conv`; a channel change
587
- or post carries `conv: {kind: "channel", op, id, name, …}`. `message.messageId` is a ULID, the same on
588
- every copy. A reply in a thread sets `message.contextId` to the `messageId` of the thread's first
589
- line. A chat envelope is never `kind: "task"`, never has a `taskId`, and a
590
- `message` to an agent never has a `conv` (`invalid_request`). `from.agent` is the session that
591
- posted the line; a person's own line has no `from.agent` (§5, "Ends"). `name` on a channel op is
592
- always the channel's own name.
593
- - A DM line: one copy to each machine of the person, and none to the sender's own machines.
594
- - A DM to oneself (`dm:<own person>`, the person's own conversation, "me"): one copy to each of the
595
- person's other machines. A line one of the person's agents wrote there is for the person, so every
596
- machine files it as incoming, unread; a line the person wrote is outgoing. Its lines carry no
597
- delivery `state`: there is no other person to deliver them to.
598
- - `create {id, name, persons, creator}`: the full record, one copy to each member machine but the
599
- sender's. `persons` includes the sender; `creator` is the person who created the channel, so an
600
- invitee learns it from the inviter's `create`.
601
- - `invite {id, name, persons}`: the persons added, to the machines of the existing members. Each
602
- machine of an added person gets a `create` with the full record instead. Persons with no
603
- machine left on the roster are not in that record.
604
- - `leave {id, name}`: the sender left, to every member machine.
605
- - `remove {id, name, persons}`: the persons removed, to every member machine, the removed
606
- persons' included.
607
- - `post {id, name}`: a line, to every member machine.
608
- - The text of a channel op is not filed. Each machine files its own notice line for an op it
609
- applies, written by the daemon from the op's fields, with the op's `messageId` (§8.2 `notice`).
610
- - **Receiver rules.** Channels are looked up by `id`, never by name, and an op's `name` must match
611
- the channel's own name (`not_found`). Where a rule fails, nothing is stored and the sender gets a
612
- `rejected` receipt.
613
- - `create` is accepted only for an unknown `id`, only when the sender and this machine's person are
614
- both in `persons` (`forbidden`), and not for an id this machine
615
- left or was removed from within the last `LIMITS.ttlMs + LIMITS.clockSkewMs` (`forbidden`). A
616
- machine holds at most a policy number of channels (the reference daemon: 200); a `create`
617
- beyond it is `rate_limited`. If this machine already holds a channel with that name, it keeps
618
- the new one under a local name: the name cut to 28 characters, a hyphen and the lowest free
619
- number from 2 (`eng-2`), or `rate_limited` when 2 to 99 are all taken. Room references
620
- (`channel:<name>`), `conv.*`, `search`, `person.send` and `channel.*` use the local name; envelopes always carry the
621
- channel's own name. This also covers two people creating the same name at once.
622
- - A `create` or `invite` naming a person this machine cannot admit is `not_found`: in a roster realm a
623
- person with no current machine; in a directory realm any id that is not a hosted person. A directory
624
- home takes persons it has not verified yet, then looks them up and watches them (§15.9).
625
- - `invite` is accepted only from a current member, and only when the channel stays within the
626
- limit (`forbidden`, `invalid_request`).
627
- - `leave` removes only the sender. When this machine's person leaves (from another of its
628
- machines), the channel and its lines are deleted here.
629
- - `remove` is accepted only from a member who is the channel's creator or a team admin on the
630
- verified roster (`forbidden`), never naming the sender (`invalid_request`: use `leave`), and only
631
- for current members (`not_found`). A machine whose person is removed deletes the channel and its
632
- lines, as on `leave`, and refuses a `create` for that id as above. Other machines remove those
633
- persons from the channel.
634
- - `post` is filed only when the sender and this machine's person are both members (`forbidden`).
635
- - Every name in `persons` must be a person on the roster (`not_found`). All of this is checked
636
- before anything is stored.
637
- - A DM line from the receiver's own person is filed in its own conversation, as above.
638
- - A person who has no machine left on the roster is no longer a member of any channel; each
639
- machine drops them when it applies the removal.
640
- - **Delivery.** An accepted chat envelope gets a `delivered` receipt. It creates no inbox item, does
641
- not count against the inbox quota (§8.3) and never wakes a session. Consent (§6.9) and the inbound
642
- rate limit still apply. The sender records each chat envelope it routes with its line and
643
- recipient. A receipt from that recipient's machine, a relay nack, the recipient leaving the roster
644
- or expiry settles the record, once. An outgoing line is `delivered` once every other person has a
645
- delivered copy, and `refused` once some person's every copy was rejected or expired; until then
646
- it is `sent`. When every copy of a channel change to one person is refused, the sender files a
647
- notice line in that channel naming the person and the code. Notice lines are never sent.
648
- - **Transcript.** Each daemon keeps one row per line, keyed by `messageId`; a later envelope with a
649
- `messageId` this machine already holds is not filed and changes nothing. The thread key is the
650
- first line's `messageId`. Text is the line's text parts joined by newlines, cut to
651
- `LIMITS.chatChars` UTF-16 code units without splitting a surrogate pair. The sender files its own
652
- line with the plaintext it sealed; that copy is not on the wire. A person's line that arrives from
653
- another of their machines is filed as outgoing. Retention deletes lines after
654
- `retention.transcriptDays` (default 30), and a per-person cap deletes a sender's oldest lines
655
- first once they hold more than a policy number of lines or bytes (the reference daemon: 10,000
656
- lines or 16 MiB of UTF-8 text).
657
- - **Order and read state.** Each daemon numbers its chat changes with one counter that only
658
- rises and is never reused (`seq`, from 1): every filed line takes the next number, and so does
659
- every later change of a line (its delivery state) and every channel deleted here. A line keeps the
660
- number it was filed with as its `seq`, its place in this machine's order; another machine numbers
661
- the same line differently. Paging and read cursors use it. Read state is per machine and per room:
662
- a cursor (`readUpTo`, 0 at first) that only moves forward and never past the newest number here.
663
- A line is unread when another person's line, or the daemon's notice of another person's change,
664
- was filed after the cursor; this person's own lines never are. Leaving a channel, or being removed
665
- from it, deletes its cursor with its lines. Nothing syncs read state between machines.
666
- - **Joining later.** A person invited later does not receive earlier lines. Membership is each
667
- machine's own view: concurrent invites or removals can leave machines with different member
668
- lists, and nothing reconciles them. A post refused because of that shows as `refused` to its
669
- sender.
670
- - **Reading.** `conv.list`, `conv.read` and `search` (§8.2) see the DMs this machine holds and
671
- the channels its person is in. Agent-facing clients fence chat text as peer content and ask
672
- before reading it.
645
+ ### 6.8 Conversations: DMs, groups and channels
646
+
647
+ Conversations use the signed log and sender-key encryption in §16. A `cid` identifies a conversation across devices.
648
+ The daemon verifies each prefix before it exposes membership, content or delivery state.
649
+ Legacy chat envelopes and room-name APIs are removed. Existing database tables remain for store compatibility.
650
+
651
+ - **Participants.** A participant is a person or a stable `person/agent` address. Verified bindings identify each signing device.
652
+ - **Admission.** An invitee receives authenticated controls and message digests before acceptance, but no ciphertext or keys.
653
+ Agent-DM admission follows the receiving person's `ask` level (§6.9).
654
+ - **History.** The signed history policy controls access after acceptance. Recovery replaces matching invitation digests with authenticated entries.
655
+ It survives restart and does not restore content deleted by later retention.
656
+ - **Delivery.** Conversation sends return `pending` until verified append evidence arrives. Missing receipts never establish an outgoing consent hold.
657
+ - **Reading.** `conv.list`, `conv.read`, `conv.search` and `conv.context` expose current verified projections.
658
+ Agent reads require a registered session, access checks and the audience guard. Clients fence peer content (§12).
659
+ - **Read state.** Lane 4 omits read positions, unread counts, preferences and follow state. Their methods remain staged until later lanes implement them.
673
660
 
674
661
  ### 6.9 Consent
675
662
 
676
663
  Consent belongs to the receiving person. A person's **grants** say what each other person, and the
677
- members of each channel, may send them and their agents. A device of the person signs each grant,
664
+ participants of each conversation, may send them and their agents. A device of the person signs each grant,
678
665
  only the person's own devices hold it, and the receiving daemon enforces it. Agents never sign or
679
666
  change a grant.
680
667
 
@@ -693,7 +680,7 @@ A roster realm has only `self` and `org`.
693
680
 
694
681
  ```ts
695
682
  { v, realm, id: Ulid, person, by: MachineId, ts,
696
- subject: { person } | { channel: <channel id> },
683
+ subject: { person } | { conv: ConvId },
697
684
  chat?: "open" | "muted" | "blocked", // lines from the subject (person subjects only)
698
685
  agentMail?: "open" | "blocked", // lines from the subject's agents
699
686
  ask?: "open" | "request" | "no", // mail and tasks from the subject or their agents to the person's agents
@@ -705,8 +692,8 @@ A roster realm has only `self` and `org`.
705
692
 
706
693
  - `sig` = Ed25519(`by`, `"messaging/1/grant\n"` ‖ JCS(grant without `sig`)). `by` is a device of `person`.
707
694
  - `person` and a person subject are `PersonId`s: a roster person name, or a hosted realm's `p_…` id (§15.1).
708
- - A channel grant sets only `ask` and `agentMail`. It applies to a sender only while the sender is a
709
- member of that channel in the receiver's own view (§6.8).
695
+ - A conversation grant sets only `ask` and `agentMail`. Both persons must be active participants in the locally verified conversation.
696
+ For an event-specific decision, membership is evaluated at that event's position.
710
697
  - `trustedUntil` needs `ask: "open"` and is at most `ts + LIMITS.trustMaxMs` (90 days). Trust is set
711
698
  for `LIMITS.trustDefaultMs` (30 days) when the person names no end. An expired `trustedUntil` reads
712
699
  as absent.
@@ -741,7 +728,7 @@ the person:
741
728
  **Effective policy.** For a sender S, each level is, in order:
742
729
 
743
730
  1. the person grant for S, where it sets the level;
744
- 2. otherwise, for `ask` and `agentMail`, the most permissive among the grants of channels S is in;
731
+ 2. otherwise, for `ask` and `agentMail`, the most permissive grant of a verified conversation shared with S;
745
732
  3. otherwise the default for the relation:
746
733
 
747
734
  | Relation | `chat` | `agentMail` | `ask` | trust |
@@ -766,7 +753,7 @@ from S.
766
753
  - `sig` = Ed25519(`by`, `"messaging/1/card\n"` ‖ JCS(card without `sig`)). `by` is a device of `from`; `from` and
767
754
  `to` are different persons. `trustedUntil` needs `ask: "open"`. A card is at most `LIMITS.cardBytes` of JCS.
768
755
  - `can` is the person grant for S over the defaults of S's relation to R (core's `cardCan`): `chat` is true for
769
- `open` and `muted`, `agentMail` for `open`. Channel grants are not on a card: S knows its own channels.
756
+ `open` and `muted`, `agentMail` for `open`. Conversation grants are not on a card.
770
757
  - `seq` rises with every card R's devices send S; S keeps the card with the highest `seq`.
771
758
  - Core's `verifyContactCard` checks the schema, the rules, the size, the realm, both persons and the signature over
772
759
  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
@@ -823,40 +810,27 @@ from S.
823
810
  A delivered request then passes the inbound policy of §8.3 as any other;
824
811
  - a request ends unanswered at its expiry: the sender's own expiry settles its side. A request
825
812
  whose sender has left the realm ends at the answer, with nothing sent.
826
- - **Message requests** (a directory realm only). A person of one org first writes to a person of another with a
827
- message request: a `message` of class `request` with `conv: {kind: "request"}` and one text part of at most
828
- `LIMITS.requestChars` (§5, "Meaning"). Only a person sends one, with the human-tier `contact.request` (§8.2);
829
- agents never send or answer one.
813
+ - **Message requests** (a directory realm only). Human first contact across orgs uses `contact.request` and a portable request proof (§16).
814
+ Its `message` envelope carries `conv: {kind: "request", proof}` and one text part, bounded by `LIMITS.requestChars`.
815
+ The envelope expiry must equal the signed descriptor expiry. Agents cannot send or answer contact requests.
830
816
  - **Addressing.** The person gives an exact email. The daemon resolves it at the directory (`dir.resolve
831
817
  {email}`, §7); no answer is `not_found`, "no Messaging user with that email accepts requests", the same for an
832
818
  unknown and an undiscoverable email, so it cannot tell them apart. Several persons for one email is
833
819
  `ambiguous_address`.
834
- - **Sending.** The request goes to every current device of that person, one copy each, and is filed as the
835
- person's own line in `dm:<person>`. Its delivery state (§6.8) is `held` while each copy not yet delivered is
836
- held for the other person, `delivered` once they accept, `refused` once they decline. Before it sends, the
837
- daemon sets the person's own grant for the other to `chat: "open"` when it sets no chat level, so the answer,
838
- the card an accept sends and the other's lines reach the person; a person who blocked the other is refused
839
- `invalid_state`. The daemon paces before it sends (`rate_limited`): one request waits per person until that
840
- person writes back (or for `LIMITS.ttlMs`), and at most `LIMITS.requestsPerDay` persons are asked a day. The
841
- relay paces the same by the header (§15.10); its `rate_limited` nack of a request is final, not retried, and
842
- refuses the line.
843
- - **Receiving.** `decide` holds a request from a `stranger`; from anyone else it is a DM line by the sender, and
844
- from a blocked sender it is `refused`. A held request is stored with its envelope and opened payload (§8.8,
845
- `contact_requests`), one per sending person and at most `LIMITS.heldTotal` in all (`rate_limited`), answered with a
846
- `held` receipt and filed as a `request` notice (§8.2). It reaches no agent and wakes nothing. A request in a
847
- roster realm is `invalid_request`: a roster realm is one org.
848
- - **Answering** (`request.answer`): `accept` sets the sender's grant to `chat: "open"` (keeping its other
849
- levels), which makes the sender a `contact` and sends them a card, then files the request's text as their
850
- first line in `dm:<sender>` and answers `delivered`; `decline` answers `rejected {code: "declined"}`; `block`
851
- sets `chat: "blocked"`, then declines. A request ends unanswered at its expiry. Every device of the person
852
- holds its own copy: a grant for the sender that another device of the person signed, arriving by "Sync", settles
853
- the copy held here as its answer did (`chat: "open"` accepts it, `blocked` declines it).
820
+ - **Sending.** The daemon sends one copy to each current recipient device and returns `{id,cid,person}`.
821
+ It stores the request separately from conversation history. An authenticated recipient receipt establishes an outgoing hold.
822
+ A missing receipt means pending delivery, not a hold. One request can wait per person, subject to `LIMITS.requestsPerDay`.
823
+ - **Receiving.** The daemon verifies the portable proof, envelope binding and expiry before it stores the request.
824
+ A stranger's request waits for the person. An allowed contact or same-org request follows the signed acceptance and import path automatically.
825
+ A blocked sender is refused. The opening reaches no agent and triggers no wake before or after import.
826
+ - **Answering.** `request.answer {answer:"accept"}` settles a signed mailbox decision, grants chat access, and imports exactly one opening into the deterministic DM.
827
+ The import uses the descriptor ID and permanent completion evidence. Concurrent accepting devices reconcile against the same verified operations and completion.
828
+ `decline` settles a signed decline. `block` also sets `chat:"blocked"`. A grant or card alone is not a terminal decision.
829
+ The daemon resumes interrupted acceptance after restart and sends a contact card after successful import.
854
830
  - **The sender's card is the directory's.** Clients show the sender of a request by the realm's verified profile
855
831
  (name, email, org, §15.1), never by the request's text.
856
- - **Agents.** An agent that needs a contact in another org asks its own person: a line to the person with
857
- `request: {contact: <email>}` (`msg.send`, §8.2). The daemon files the line and a suggestion, listed by
858
- `request.list` and filed as a `request` notice; the person sends the request with `contact.request`
859
- (`suggestion` dismisses it), or dismisses it with `request.answer {answer: "decline"}`.
832
+ - **Agents.** Legacy contact suggestions remain dismissible. The removed `msg.send` method cannot create new suggestions.
833
+ Agent-to-agent DMs use recipient invitation admission instead of human contact requests.
860
834
  - **Cards on the daemon.** A device sends a card (a `message` with `conv: {kind: "card", what: "card"}`) to each
861
835
  current device of a person S when this person's grant for S changes on this device (`grant.set`, an answer to
862
836
  a request, a re-sign under "Signers"), and when S asks with `card.get`. It sends none to a `stranger` and none to
@@ -867,14 +841,7 @@ from S.
867
841
  person; it answers nothing. A `rejected` receipt from a person with `refused`, `request_required` or `declined`
868
842
  drops their card and asks for a fresh one. The `seq` of a device's cards is its clock in milliseconds, raised
869
843
  past the last one it used. A card is not a first message for the freshness bound (§15.9).
870
- - **Pacing.** Lines from one person's agents to another person (A2H), summed over those agents, are
871
- limited to `LIMITS.a2hPerHour` (30) an hour and `LIMITS.a2hPerDay` (100) a day:
872
- - the sending daemon refuses an agent's DM line or channel post at send time (`msg.send` to a person
873
- or `#channel`), before it stores or sends anything, when it would take any recipient person past either
874
- count, with `rate_limited` and when to try again; the person's own lines are not paced;
875
- - the receiving daemon refuses an agent's line past either count from one sending person with a
876
- `rejected` receipt, `rate_limited`, for a sender that does not pace itself;
877
- - the reference daemon counts in memory, so a restart starts the windows again.
844
+ - **Pacing.** Conversation append limits replace legacy chat-envelope pacing (§16). Retained task envelopes still follow the relay and inbound limits (§7, §8.3).
878
845
  - **Sync.** A device sends each grant it signs or re-signs to each of its person's other current
879
846
  devices: a sealed `message` to the device with no agent, `conv: {kind: "self", what: "grant"}`,
880
847
  whose one data part is `{grants}`. A device of the person that is new to the sender (one it has
@@ -1259,14 +1226,22 @@ Jelly ID, which wakes the host.
1259
1226
  It replaces the device's earlier ticket, and is kept in the relay's store (`dir_wakes`, §8.8) until it is
1260
1227
  used, disarmed, its device is unbound or erased, or it expires. At most 60 arms a device in a burst, then one a
1261
1228
  minute (`rate_limited` with `retryAfterMs`). `wake.disarm {}` forgets the device's ticket.
1262
- - **When the relay presents it.** Only when the relay has just taken (§7 "Routing": first seen, queued) an envelope for
1263
- the device while the device has no connection, and the envelope is:
1264
- - the person's own mail to an armed agent: header type `message`, `from.person` = `to.person`, and `to.agent` in
1265
- `agents`; or
1266
- - an answer the device awaits: header type `task.status` or `task.artifact` whose `replyTo` is in `awaiting`.
1267
-
1268
- Lines and posts (no `to.agent`), receipts, presence, task requests and cancels, anyone else's mail, a message that
1269
- merely names an awaited request, and anything for a connected device never wake. A request the recipient's person
1229
+ - **When the relay presents it.** Only while the device has no connection, and only for one of these:
1230
+ - **a conversation line by the device's own person to an armed agent.** The relay has just stored (`conv.ok`, §7
1231
+ "Conversation frames") a `msg` event whose `wake` hint (§16.2) names a session in `agents`. The person is the one
1232
+ of the authenticated device that appended the event (which `by.person` must match), never the event's claims
1233
+ alone, and only that person's current devices are considered. A person's own mail to their agent is such a line
1234
+ (design §8.2, "Wake"): their daemon sets `wake` to the sessions of their own agents a line addresses, a dm whose
1235
+ other end is one of them or a mention of one, and never names another person's session. A `wake` hint on another
1236
+ person's event reaches only that person's own devices, so it never wakes anyone else's host. A repeated append
1237
+ (`conv.ok` again for the same event) does not present again. The reason logged is `own`;
1238
+ - **an envelope** the relay has just taken (§7 "Routing": first seen, queued) for the device that is:
1239
+ - the person's own mail to an armed agent: header type `message`, `from.person` = `to.person`, and `to.agent` in
1240
+ `agents` (a task request, which keeps its envelope); or
1241
+ - an answer the device awaits: header type `task.status` or `task.artifact` whose `replyTo` is in `awaiting`.
1242
+
1243
+ Conversation events without a `wake` hint (posts, receipts, another person's lines), presence, task cancels, anyone
1244
+ 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
1270
1245
  must answer (§6.9) is held by the receiving daemon, which the relay cannot see: such mail never wakes unless it is
1271
1246
  the person's own.
1272
1247
  - **Presenting.** `POST <url>` with the form `client_assertion_type=urn:jelly:wake&ticket=<ticket>`, no redirect
@@ -1291,6 +1266,72 @@ Jelly ID, which wakes the host.
1291
1266
  names them), and the ids of the task requests it awaits (envelope ids it already routed). A malicious relay can also
1292
1267
  fail to wake a host, as it can drop any envelope; that delays the person's mail, it loses none.
1293
1268
 
1269
+ **Conversation frames** (`schema/conv-frames.ts`; §16). After `hello`, in a roster realm and in a directory realm
1270
+ alike, the relay sequences conversation logs. It is never trusted for authority: it sequences and filters, and
1271
+ members verify every entry (§16.4, §16.7). Every frame is strict; a refusal is `nack {id: req, code, message}`.
1272
+
1273
+ - **`conv.append {req, event}`**, an `op` or a `msg` (a `keys` event is `invalid_request`: shares go in
1274
+ `conv.share`). The relay checks, in order:
1275
+ 1. the frame's schema and `LIMITS.convEventBytes` (`too_large`); `by.device` is the authenticated device
1276
+ (`forbidden`);
1277
+ 2. the rate limits below (`rate_limited` with `retryAfterMs`);
1278
+ 3. a `create` against an empty log first, so a person who could not create it learns nothing of an existing one;
1279
+ then a `create` of an existing conversation is `conv_conflict`;
1280
+ 4. a person who is not a participant person of an existing conversation gets `not_found`, as for a conversation
1281
+ that does not exist (no existence oracle for derived dm ids), except for a `join`;
1282
+ 5. `ConvLog.apply` against the relay's copy of the log at its head, with the live checks (§16.7, step 4): `ts`
1283
+ within the clock skew, `by.device` a current device of `by.person`, and the org of a person for `join` and
1284
+ `add`. In a directory realm the devices and orgs are the directory's (§15.10); in a roster realm they are the
1285
+ roster's, and the realm is one org, its team id. An `op` whose `opHead` is not the current op head is
1286
+ `conv_conflict`; a `msg` under another epoch, or while a rekey is due, is `stale_epoch`;
1287
+ 6. every person a `create` or `add` names has a current device in the realm (`not_found`), and no person would
1288
+ take part in more than `LIMITS.convsPerDevice` (5000) conversations (`too_large`).
1289
+
1290
+ It then assigns `seq`, stores the entry and answers `conv.ok {req, cid, seq, hash}` once it is committed (the
1291
+ group commit, as for `send`), and pushes `conv.push {cid, entry}` to the other online devices of every participant
1292
+ person before and after the entry, so a removed person's devices see their removal. A repeat of an event the log
1293
+ holds (the same JCS) is answered `conv.ok` again, with its first `seq`; another event with a used `id` is
1294
+ `duplicate`.
1295
+ - **`conv.share {req, events}`**: 1 to `LIMITS.convSharesPerFrame` (100) key shares (§16.6), all of one
1296
+ conversation and by the authenticated device. Each must pass `ConvLog.checkShare` live, with `opHead` the current
1297
+ op head (`conv_conflict`), so a person removed since gets none. All or none are stored; the relay answers
1298
+ `conv.shared {req, cid, count}` and pushes `conv.push {cid, share: {n, event}}` to each recipient device online,
1299
+ and to no other device. A previously stored share repeated with identical JCS is acknowledged without storing or
1300
+ pushing it again, even if the op head has since advanced. A used id with different content is `duplicate`. A new
1301
+ share must still pass all current checks; the repeat rule never delivers an old share to a removed device.
1302
+ - **`conv.fetch {req, cid, after, limit?, sharesAfter?}`** → `conv.entries {req, cid, head, entries, shares, more}`:
1303
+ the entries after `after` in `seq` order, at most `limit` (`LIMITS.convFetchEntries` (500)) and as many as fit in
1304
+ three quarters of a frame (at least one). An expired `msg` is served as `{seq, prev, digest}`; an op never is. With
1305
+ `sharesAfter`, also this device's shares after that `n`; no other device's share is ever served. `more` says
1306
+ there is more of either. Only a device of a participant person may fetch (`not_found` otherwise).
1307
+ - **`conv.head {req, cids? | after?}`** → `conv.heads {req, heads: [{cid, head, opHead, shares}], more}`: the heads
1308
+ of the named conversations this device's person takes part in, or, without `cids`, of all of them by `cid` after
1309
+ `after`, at most `LIMITS.convHeads` (500) each way. `shares` is the last share `n` for this device (0: none). A
1310
+ device that comes back online asks this first, then fetches what moved.
1311
+ - **`conv.caps {req, persons}`** → `conv.capabilities {req, outdated}`: of at most `LIMITS.convOpParticipants` named
1312
+ persons, those every one of whose current devices with a stored, unexpired presence names no `conversations`
1313
+ feature: a daemon from before conversations, which reads no log. A person with no such presence is not named, since
1314
+ no presence is no evidence. It is a hint from the relay and can only make a sender refuse: before it creates a dm,
1315
+ group or channel with that person, adds them, or sends them a message request, a daemon asks, and refuses with
1316
+ `peer_outdated` ("<handle> needs to update Messaging"), creating and sending nothing. It reveals one bit about a
1317
+ person of the realm, whom the asker can already name.
1318
+ - **Retention.** Once a minute the relay prunes the bodies of `msg` entries older than the conversation's
1319
+ `retainDays` (as of its head), by the time it sequenced them, keeping each entry's `seq`, `prev` and digest
1320
+ (§16.4), and deletes the key shares older than it. It never prunes an op.
1321
+ - **Limits** (per conversation and device, in memory; a restart refills them):
1322
+ - appends by one device to one conversation: a burst of `LIMITS.convAppendBurst` (20), refilled at
1323
+ `LIMITS.convAppendsPerMinute` (60) a minute; by all its devices, `LIMITS.convAppendsPerConvPerMinute` (600) a
1324
+ minute;
1325
+ - `rekey` ops whose cause the log cannot prove (`device`, `schedule`): `LIMITS.convRekeysPerHour` (4) per device
1326
+ per conversation an hour;
1327
+ - creates: `LIMITS.convCreatesPerHour` (60) per device an hour;
1328
+ - key shares: `LIMITS.convSharesPerMinute` (10,000) per device per conversation a minute, and at most
1329
+ `LIMITS.convSharesPerPair` (256) kept from one sender device for one recipient device in one conversation
1330
+ (`rate_limited`), so one member cannot fill another's shares.
1331
+ - **What the relay learns.** The plaintext ops (participants, roles, settings, epochs, policies), the kind of each
1332
+ sealed op (`meta`, or a `setting`'s kind), sizes and timing. Never a message body, a name, a topic, a setting's
1333
+ content or a key.
1334
+
1294
1335
  **What the relay can see.** Team ids, machine ids, persons, session names, timing, and presence:
1295
1336
  its session names, agent kinds, states and tiers, and the repo, branch and doing text a machine
1296
1337
  chooses to share (§6.5, none by default). It never sees message, task or receipt bodies.
@@ -1354,6 +1395,30 @@ chooses to share (§6.5, none by default). It never sees message, task or receip
1354
1395
 
1355
1396
  ### 8.2 IPC API
1356
1397
 
1398
+ **Conversation render schemas.** Core exports `ParticipantView`, `NoticeView`, `FileView`, `TaskView`, `EventView`, `ConvView`, `ActivityBody` and `ConvStreamEvent` (§16).
1399
+ Names and content are untrusted peer data. A `FileView` exposes no attachment key or nonce.
1400
+ A deleted `EventView` contains neither text nor files. Lane 4 omits read positions, preferences and unread counts.
1401
+ Stream records carry a daemon-local `pos`. An `event.update` names the conversation and event of its embedded projection.
1402
+ Stream record types are `event`, `event.update`, `conv`, `member`, `read`, `typing`, `activity` and `gap`.
1403
+ An `event.update` change is one of `edit`, `delete`, `react`, `pin`, `replies`, `delivery` and `task`; lane 4 emits `edit`, `delete`, `react`, `pin` and `delivery`.
1404
+ A `request.waiting` item's state is one of `held` and `pending-delivery`.
1405
+ The daemon re-reads verified state before it sends a projection.
1406
+
1407
+ **Staged conversation methods.** These contracts have no live handler: `conv.catalogue`, `conv.publish`, `conv.markRead`, `conv.markUnread`, `conv.prefs`, `conv.follow`, `conv.save`, `conv.saved`, `conv.draft`, `conv.schedule`, `conv.scheduled`, `conv.unschedule`, `conv.typing`, `context.get`, `context.set`, `context.set.preview`, `conv.localRetention`, `file.get`, `session.activity`, `turn.cancel`.
1408
+
1409
+ **Live conversation methods.** `LIVE_CONVERSATION_METHODS` is part of `IPC_METHODS`. The tier table applies on both sockets.
1410
+ Batch `people.get` accepts up to 100 distinct person IDs and returns `{people, missing}` without device lists.
1411
+ `people.get` and `sessions.list` work without a connected conversation engine.
1412
+ Human-only `request.waiting` reports authenticated outgoing holds separately from pending delivery. `request.list` reports incoming requests.
1413
+ A `conv.send` names exactly one of `cid` and `to`. `broadcast` requires a thread.
1414
+ A page chooses at most one position. Agent calls require a session registered on their connection.
1415
+ The daemon enforces access, audience consent and own-line restrictions after asynchronous work too.
1416
+ Creation requires two DM participants or 3–50 group participants. Only channels have visibility. Only agents can be seated.
1417
+ `conv.access` pages accessible conversations. `conv.sync` waits at most 25 seconds and returns at most 50 events.
1418
+ A restart or expired replay position produces a gap. Clients then reload their accessible conversations.
1419
+ `conv.backfill` returns `complete: false` until indexing coverage accounts for missing keys and expired entries.
1420
+ The method schemas and their error arrays are normative. Malformed JSON-RPC and params use JSON-RPC errors.
1421
+
1357
1422
  The API is the method table `IPC_METHODS` in `schema/ipc.ts`: JSON-RPC 2.0 NDJSON on two Unix
1358
1423
  sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (for example
1359
1424
  `contacts.list`, `item.read` and `task.get`), the TSDoc in that file and in `schema/ipc/*.ts` is normative.
@@ -1366,8 +1431,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1366
1431
  - **Two sockets.** The daemon serves the one API on two sockets, and itself enforces what each
1367
1432
  serves (its tiers). A client's own method list is a second check, not the boundary.
1368
1433
  - `run/agent.sock` (the agent tier) serves only `AGENT_METHODS`. Any other method fails with
1369
- `forbidden` before its params are read. A line as the person (`person.send`, no session) is not
1370
- one of them (§6.8). Agent-facing clients connect here: the OpenCode plugin's server part
1434
+ `forbidden` before its params are read. Conversation calls on this socket require a bound session.
1435
+ They cannot send as the person. Agent-facing clients connect here: the OpenCode plugin's server part
1371
1436
  and `messaging-mcp`.
1372
1437
  - `run/admin.sock` (the admin tier) serves every method. Human clients connect here: the `messaging`
1373
1438
  CLI and the `/messaging` TUI.
@@ -1421,7 +1486,7 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1421
1486
  - **What the tiers are, and what they are not.** Where the daemon, OpenCode and the user's shell
1422
1487
  run as one OS user, as on a laptop, the tiers are not a security boundary: any process of that
1423
1488
  user can open `run/admin.sock`. They keep `team.*`, `identity.rotateEnc`, `daemon.stop`,
1424
- `channel.*`, `conv.subscribe`, `conv.markRead` and the person's own chat lines off every tool path. A tool that
1489
+ conversation administration and the person's own chat lines off every tool path. A tool that
1425
1490
  speaks to the agent socket cannot reach them. A shell that runs the `messaging` CLI is still a shell,
1426
1491
  and only the host's shell permissions govern it. The tiers are a boundary only when `messagingd`
1427
1492
  runs as its own user and the engine runs as another user in `ipc.agentGroup` (the layout is in
@@ -1430,23 +1495,25 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1430
1495
  as them and call every method, including `team.*` and `daemon.stop`. Agent-facing clients still
1431
1496
  must not expose `team.*`. The OpenCode TUI is a human surface and may call them. It must not
1432
1497
  place an invite ticket in a session, a log, or a model context.
1433
- - Key-scoped methods (`session.update`, `session.unregister`, `events.subscribe`, `msg.send`,
1434
- `inbox.*`, `item.read`, `task.*`, `conv.read` with a key) work only for a key that the calling connection registered (`not_found`
1435
- otherwise).
1498
+ - Key-scoped methods (`session.update`, `session.unregister`, `events.subscribe`, `inbox.*`, `item.read`, `task.*`,
1499
+ `conv.list`, `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`)
1500
+ work only for a key that the calling connection registered. Human conversation calls can omit the key on the admin socket.
1436
1501
  - A session key is therefore a bearer credential for its session: whoever knows it can register it
1437
1502
  and act as that session.
1438
1503
  - `AGENT_METHODS` (with its type `AgentMethod`) lists the methods an agent-facing client may call,
1439
1504
  and exactly the methods `run/agent.sock` serves. Anything else is for human clients: the
1440
1505
  `messaging` CLI and the `/messaging` TUI.
1441
- - **Tiers.** Every method is in one tier list in `schema/ipc.ts`, and `conv.read` in two:
1506
+ - **Tiers.** Conversation methods available to agents also appear in the human list:
1442
1507
  `AGENT_METHODS` (the agent tier: sessions, `device.status`, `device.bind`, `contacts.list`,
1443
- `msg.send`, `inbox.list`, `inbox.get`, `inbox.ack`, `item.read`, `conv.read`, `search`, `task.*`,
1508
+ `inbox.list`, `inbox.get`, `inbox.ack`, `item.read`, `conv.list`, `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`, `task.*`,
1444
1509
  `events.subscribe`, `daemon.status`, `daemon.health`); `HUMAN_METHODS` (acting as the person: `conv.list`,
1445
- `conv.read`, `conv.markRead`, `conv.subscribe`, `person.send`, `people.search`, `people.get`,
1510
+ `conv.read`, `conv.send`, `conv.react`, `conv.edit`, `conv.delete`, `conv.search`, `conv.context`, `conv.access`, `conv.sync`,
1511
+ `conv.get`, `conv.members`, `conv.threads`, `conv.pin`, `conv.create`, `conv.add`, `conv.remove`, `conv.role`, `conv.seat`,
1512
+ `conv.settings`, `conv.meta`, `conv.archive`, `conv.leave`, `conv.join`, `conv.accept`, `conv.policy`, `conv.backfill`, `people.search`, `people.get`,
1446
1513
  `grant.list`, `grant.set`, `grant.set.preview`, `request.list`, `request.answer`, `request.answer.preview`, `contact.request`,
1447
- `contact.request.preview`, `card.get`, `agents.list`,
1448
- `agents.read`, `notice.subscribe`, `notice.list`, `notice.ack`, `device.approve.preview`,
1449
- `device.remove.preview`, `identity.replace.preview`, `directory.erase.preview`, `channel.*`); and `ADMIN_METHODS`
1514
+ `contact.request.preview`, `card.get`, `sessions.list`, `request.waiting`,
1515
+ `notice.subscribe`, `notice.list`, `notice.ack`, `device.approve.preview`,
1516
+ `device.remove.preview`, `identity.replace.preview`, `directory.erase.preview`); and `ADMIN_METHODS`
1450
1517
  (this device and the daemon: `daemon.stop`,
1451
1518
  `daemon.woke`, `person.attached`, `daemon.snapshot`, `wake.arm`, `wake.disarm`,
1452
1519
  `device.list`, `device.remove`, `device.link`, `device.login`, `device.login.status`, `device.login.cancel`,
@@ -1561,12 +1628,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1561
1628
  `PeerRef` (inbox items, task records), provenance (§6.6), directory entries (whose client kind is
1562
1629
  `client`) and chat lines. `daemon.status` names this device in `identity` (its `device`), and a divergence alarm
1563
1630
  the signer's `device`. Team administration (`team.*`) keeps the roster's `machine`.
1564
- - **Persons in the API.** Wherever the API names a person (addresses, `PeerRef`, provenance,
1565
- directory entries, chat lines and their `refused`, `conv` events, `daemon.status`'s identity,
1566
- `contacts.list`, `people.*`, a `dm:` or `agent:` conversation, and a line's `to` in `msg.send`), it
1567
- takes a `PersonId`: a roster person name or a hosted `p_…` id. Channel membership (`channel.*`, and the
1568
- `persons` and `creator` of a channel op, §6.8) takes `PersonId`s too. Team administration (`team.*`) takes
1569
- roster person names.
1631
+ - **Persons in the API.** Person references use `PersonId`: a roster person name or a hosted `p_…` ID.
1632
+ Conversation participants use person IDs or stable `person/agent` addresses. Team administration uses roster person names.
1570
1633
  - **`session.register`.** Names a session.
1571
1634
  - The requested name is suffixed `-2`, `-3`, ... if it is taken on this machine.
1572
1635
  - Registering an existing key again updates it; this is idempotent.
@@ -1607,24 +1670,14 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1607
1670
  expired `trustedUntil` is left out. So an agent sees whom it can reach without a human-tier call. A `reach` also
1608
1671
  carries `devicesChangedAt` while a device of that person was added, removed or replaced less than 7 days ago, as
1609
1672
  their verified log (or the roster) shows it; a rekey or an attest is not a change, and it carries no other text. Entries
1610
- carry `expect`: the hint a `msg.send` to that session now, with the wake hint set, would return
1673
+ carry `expect`: the hint a `task.create` to that session, with the wake hint set, returns
1611
1674
  (below), computed from the same view. Agent clients show it before anything is sent. It is absent
1612
1675
  when no presence lists the session.
1613
- - **`msg.send`.** One send from the session `key`. Its `to` decides what it is:
1614
- - **To `<person>` or `#<channel>`: a line** in the person's DM with them, or a post in a channel
1615
- this person is in, attributed to the session (§6.8). It takes `kind: "message"`, one text part of
1616
- at most `LIMITS.chatChars`, and `replyTo` as the thread (the `id` of any line of it); any other
1617
- A2A field (`contextId`, `taskId`, `messageId`, `metadata`, `extensions`, `referenceTaskIds`,
1618
- `onBehalfOf`, `wake`) is `invalid_request`, and so is a `to` that is neither a person
1619
- name nor `#` and a channel name. It fails with `not_found` for a person not on the team, a
1620
- channel this person is not in, or an unknown thread, and `too_large` for a longer line. It writes
1621
- the line as `person.send` does (below), and `idempotencyKey` is the line's `clientId`. Its one
1622
- result is `{to, envelopeId: <line id>, messageId, contextId, stage: "accepted"}`. With `request:
1623
- {contact: <email>}`, only on a line to the session's own person (otherwise `invalid_request`), the daemon
1624
- also files a suggestion for the person (§6.9, "Message requests").
1625
- - **To `person/session[@label]`: mail or a task** to that session, as the rest of this item says.
1676
+ - **`task.create` and `task.continue`.** These methods retain task envelopes until lane 7.
1677
+ The destination is `person/session[@label]`. Creation assigns a task ID and signs a delegation chain.
1678
+ Continuation requires the existing task ID and its bound executor. Both require the calling session's `key`.
1626
1679
  - Refuses content that holds a credential (`secret_refused`, §8.7), as do `task.update`,
1627
- `task.cancel` and `person.send`.
1680
+ `task.cancel`, `conv.send` and `conv.edit`.
1628
1681
  - Resolves the address against the directory, including offline sessions.
1629
1682
  - Builds one envelope per recipient and stores it in the outbox (`accepted`).
1630
1683
  - Sends it when the relay is connected.
@@ -1650,14 +1703,12 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1650
1703
  `idempotencyKey` returns the hint computed for the original call.
1651
1704
  - `idempotencyKey` returns the original results for a repeated call from the same key.
1652
1705
  - `wake` defaults to true.
1653
- - The daemon assigns ULID task ids for `kind: "task"`, and the `contextId` when it is omitted.
1654
- - A `kind: "task"` send signs a root grant for each recipient (§6.7).
1706
+ - Creation assigns ULID task IDs and a `contextId` when omitted, and signs a root grant for each recipient (§6.7).
1655
1707
  - A chain this daemon already knows the recipient would refuse is not sent. The result is
1656
1708
  `stage: "rejected"` with that check's code, and the requester's task is stored
1657
1709
  `TASK_STATE_REJECTED` with no grants.
1658
1710
  - **`onBehalfOf {taskId, scope?, maxDepth?}`** delegates on behalf of a task that the calling key
1659
- **executes**, and only with `kind: "task"`. Otherwise it fails with `not_found` (any other task)
1660
- or `invalid_request` (`kind` is not `"task"`), before anything is stored. The daemon:
1711
+ **executes**, and only with creation. Any other task fails with `not_found` before storage. The daemon:
1661
1712
  1. refuses a terminal task (`invalid_state`) before anything is stored;
1662
1713
  2. signs the next grant of the stored chain for each recipient, narrowed to `scope` and
1663
1714
  `maxDepth` when given;
@@ -1680,101 +1731,40 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1680
1731
  - **`task.cancel`** is allowed only for the requester.
1681
1732
  - **`task` events** carry `origin`:
1682
1733
  - `local` when the event follows the subscriber key's own call: `task.update`, `task.cancel`,
1683
- or the `msg.send {kind: "task"}` that created the record;
1734
+ or the `task.create` call that created the record;
1684
1735
  - `peer` when it follows an envelope from the other side, a receipt, or a daemon timeout.
1685
1736
  - When the event follows a status or artifact update, `update` carries that delta, including
1686
1737
  `append` and `lastChunk`.
1687
- - **`conv.read {key?, conversation, thread?, before?, after?, limit?}`** returns one page of a
1688
- conversation, oldest first, as `{kind, …, more}`. `conversation` is `dm:<person>` or
1689
- `channel:<name>` (`kind: "lines"`, below under Chats), or `agent:<person>/<agent>` (`kind:
1690
- "agent"`), which needs the session `key`: what the session `key` exchanged with one other session,
1691
- on any machine of that person: incoming mail,
1692
- outgoing mail and tasks either way, as `ConvEntry` rows in time order. A task request appears
1693
- once, as its task (dated when it was created, with its current state), not again as mail. A
1694
- cursor is an entry's `{at, id}` with an agent, and a line's `seq` in a DM or a channel; the other
1695
- form is `invalid_request`, and so is `thread` with an agent. With no cursor it is the latest `limit` entries (default 30, at
1696
- most 100); with `before` the latest `limit` before that place; with `after` the earliest `limit`
1697
- after it. Entries come oldest first, and `more` says entries remain past the page in the
1698
- direction read; `before` and `after` together are invalid params. Incoming mail carries its
1699
- preview and stage, a task the start of its request, the session's role and the task's state.
1700
- Outgoing mail carries its stage but no text: the daemon keeps no copy of what it sent. It changes
1701
- no stage and sends no receipt. Agent clients build their conversation view on it (§11).
1738
+ - **`conv.read {key?, cid, thread?, before?, after?, around?, limit?, cursor?}`** returns current event projections, oldest first, or an explicit context hold.
1739
+ Pages contain at most 50 events. Before and after positions are log sequence numbers. An opaque cursor binds to the query and session.
1740
+ Reads expose no legacy mail or fabricated task events. Retained tasks use their own methods.
1702
1741
  - **`task.list`:** most recently updated first. It filters by `role`, `state`, `contextId`,
1703
1742
  `peer` (a qualified address) and `updatedAfter` (at or after, ≥), pages with an opaque `cursor`/`nextCursor`, and reports `total`.
1704
- - **`msg.send` A2A passthrough** (mail and tasks): `messageId` (generated if omitted), `metadata` (at most
1743
+ - **Task A2A passthrough:** `messageId` (generated if omitted), `metadata` (at most
1705
1744
  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
1706
1745
  side), `extensions`, and `referenceTaskIds`.
1707
- - **Chats (§6.8).** `conv.read` (of a DM or a channel) and `search` need no session key. They are
1708
- the only chat methods in `AGENT_METHODS`; an agent's own line goes through `msg.send`.
1709
- - `conv.list` (human tier) returns at most `limit` conversations (default 100, at most 200), newest
1710
- first. A channel held under a local name carries its own `name`. Each carries its read state
1711
- (§6.8): `readUpTo`, `unread`, and `lastSeq`, the newest line's `seq` (absent in a channel with no
1712
- lines).
1713
- - `conv.read` of a DM or a channel returns a page of its lines (or of one thread), oldest first, and `more`. With
1714
- no cursor it is the latest `limit` lines (default 50, at most 100); with `before: seq` the latest
1715
- `limit` lines below that `seq`, to page back; with `after: seq` the earliest `limit` lines above
1716
- it, to catch up. `more` says that lines remain past the page in the direction read. `before` and
1717
- `after` together are invalid params.
1718
- - `conv.markRead {conversation, upTo}` moves the conversation's read cursor to `upTo`, a line's `seq`, if that is
1719
- later, clamped to the newest `seq` here. It returns `{readUpTo, unread}`. It is a human-client
1720
- method that is not in `AGENT_METHODS`: read state is the person's, and an agent reading a
1721
- conversation does not mark it read. It fails with `not_found` for a channel this person is not in.
1722
- - A `ChatLine` this machine sent to other persons carries `state` (`sent`, `delivered` or
1723
- `refused`) and, when refused, `refused` (the persons). A line the daemon wrote itself, about a
1724
- channel change or a refusal of one, carries `notice: true`.
1725
- - `person.send {conversation, text, thread?, clientId?}` (human tier) writes the person's own line
1726
- and its envelopes in one transaction and sends them at once, with no session. It fails with
1727
- `not_found` for a person not on the team or a channel this person is not in. A DM to oneself
1728
- is the person's own conversation (§6.8).
1729
- - `search {query, conversation?, limit?}` finds lines in the DMs and channels this person holds,
1730
- newest first, as `hits`, each with its `conversation`.
1731
- - **Pacing.** The sender keeps its chat envelopes to one recipient machine below that machine's
1732
- chat budget (§8.3): the reference daemon allows 50 a minute, ten below
1733
- `LIMITS.inboundPerMachinePerMinute`, for clock and transit jitter. Channel changes count too. A
1734
- line (`person.send`, or `msg.send` to a person or a channel) that would go over it for any machine
1735
- it goes to fails with `rate_limited`, a
1736
- message that says when to try again, and nothing stored or sent. Pacing cannot cover a backlog:
1737
- lines queued at the relay for an offline machine reach it together when it connects. The
1738
- receiver defers those past its budget and the relay paces them (§7 "Delivery", §8.3), so every
1739
- line is delivered within minutes; only one that would expire first is refused.
1740
- - A line with a `clientId` (`msg.send`'s `idempotencyKey`) is idempotent per `clientId` and
1741
- sender: the session `key`, or the person for `person.send`. A repeat with the same room, thread and text returns the first call's
1742
- result and files and sends nothing. The same `clientId` for another line is `invalid_request`.
1743
- The daemon keeps a `clientId` for `LIMITS.ttlMs`.
1744
- - `channel.create`, `channel.invite`, `channel.leave` and `channel.remove` are for the human
1745
- `/messaging` TUI. They take a channel name as this machine holds it (its local name). Each checks
1746
- before it stores anything. `channel.create` refuses a name already held, more than the
1747
- channels a machine holds (`invalid_request`), an unknown person, or more than
1748
- `LIMITS.channelPersons` persons with the creator. `channel.remove` refuses a caller who is not
1749
- a member (`not_found`), who is neither the creator nor a team admin (`forbidden`), a list that
1750
- names the caller (`invalid_request`: use `channel.leave`), and a person who is not a member
1751
- (`not_found`).
1752
- - Mail and tasks (`msg.send` to an agent) never write the transcript.
1753
- - **`conv` events** go only to connections that called `conv.subscribe`, a human-client method
1754
- that is not in `AGENT_METHODS`. They are never sent to `events.subscribe` keys. A `conv` event
1755
- names the conversation, the line's `id` and its person, never its text. It is raised for a filed line,
1756
- a channel change, and a change of an outgoing line's delivery state (then `person` is this
1757
- machine's person). Each carries the `seq` of its change (§6.8), so events rise in `seq`.
1758
- - **Resuming.** `conv.subscribe {since}`, where `since` is the `seq` of the last event the client
1759
- handled, first sends one `conv` event per line that changed after `since`, oldest change first,
1760
- each with the `seq` of the line's latest change. Then live events follow, with none between.
1761
- It returns `{seq, gap}`: the newest `seq` at the call, and whether the replay could not be
1762
- complete, in which case it sends none. That is when more than a policy number of lines changed
1763
- (the reference daemon: 500), a channel was deleted here after `since`, or `since` is above
1764
- `seq` (a store set up again). After a gap the client reloads with `conv.list` and `conv.read`.
1765
- Without `since`, `gap` is false and no events are replayed.
1746
+ - **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`.
1747
+ Every agent call uses its bound session. Human calls use the admin socket.
1748
+ A `conv.send` returns `{cid, id, clientId, stage: "pending"}` or an explicit audience hold.
1749
+ The durable outbox retries identical signed events. A repeated `clientId` returns the first event ID.
1750
+ A queued key share whose recipient device the conversation's own verified log removes (an accepted `remove` of its
1751
+ person or the person's own `leave`, with the person no longer a participant, or a `rekey {cause: device}` naming
1752
+ the device) is dropped, never sent, and logged by conversation, device and event id. Any other share to a device
1753
+ that is no longer current, such as a directory miss, holds the outbox until the state is verified.
1754
+ Reuse with different content or a different destination fails with `invalid_request`.
1755
+ Conversation administration uses the human-tier methods in the tier list. Internal notifications carry identifiers only.
1756
+ The stream re-renders current verified state, including edits and deletions, for each reader.
1766
1757
  - **Consent (§6.9).** `grant.list`, `grant.set`, `grant.set.preview`, `request.list`,
1767
1758
  `request.answer`, `request.answer.preview`, `contact.request`, `contact.request.preview` and `card.get` are human-client methods that are
1768
1759
  not in `AGENT_METHODS`: agents never read or change a person's grants, answer a held request, or send a message
1769
1760
  request.
1770
1761
  - `grant.list {subject?}` returns this person's grants as this device holds them, newest first,
1771
1762
  each with its levels, `id`, `ts`, `by` and, when re-signed, `resignedAt`, and the review list:
1772
- the widening levels that lapsed with their signer, with when they lapsed. A channel subject
1773
- carries the channel id and, while this person is in it, its local name.
1763
+ the widening levels that lapsed with their signer, with when they lapsed. A conversation subject carries its canonical `cid`.
1774
1764
  - `grant.set {subject, chat?, agentMail?, ask?, trustedUntil?, trust?, discoverable?}` signs the
1775
1765
  whole new grant for the subject with this device: a level left out is the relation's default,
1776
- and no level at all is a clear. `subject` is `{person}` or `{channel}`, a channel by its local
1777
- name. `trust: true` without `trustedUntil` trusts for `LIMITS.trustDefaultMs`. It stores the
1766
+ and no level at all is a clear. `subject` is `{person}` or `{conv: cid}`.
1767
+ `trust: true` without `trustedUntil` trusts for `LIMITS.trustDefaultMs`. It stores the
1778
1768
  grant, takes the subject off the review list and sends the grant to this person's other devices
1779
1769
  in one transaction. It fails with `not_found` for a person the realm does not know or a channel
1780
1770
  this person is not in, `invalid_request` for a grant that breaks a rule of §6.9, and
@@ -1842,27 +1832,12 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1842
1832
  `daemon.snapshot {done: true}` deletes it and returns `{released}` (false when none was held). One is held at a time
1843
1833
  (`invalid_state` while one is); the daemon releases it after an hour (`until`), when it stops, and one a previous run
1844
1834
  left when it starts.
1845
- - **Your agents.** `agents.list` and `agents.read` (human tier, read-only) are the person's view of their
1846
- own agents' traffic on this device.
1847
- - `agents.list` returns each local session (registered or kept) as `{agent, conversations, unread,
1848
- lastAt?, state}`: how many other agents it has mail or tasks with, the mail it has not read, its last
1849
- traffic, and `idle`, `busy` or `kept`; most recent first.
1850
- - `agents.read {agent, conversation?, before?, limit?}` returns, without `conversation`, `{kind:
1851
- "conversations", conversations}`: each `agent:<person>/<agent>` it has traffic with, with its unread
1852
- mail and last time, most recent first. With `conversation`, it returns `{kind: "entries",
1853
- conversation, entries, more}`: a page of entries as `conv.read` gives them to that agent (default
1854
- 30, latest first, `before` an entry's `{at, id}`). It changes no stage and sends no receipt.
1855
- `not_found` for an agent this device has no session of. Peer text is untrusted; a client cleans it
1856
- for display (`cleanPeerText`) and never shows it to a model.
1835
+ - **Your agents.** Human-tier `sessions.list` returns local sessions as `{agent, state, seats}`.
1836
+ State is `idle`, `busy` or `kept`. Seats are verified conversation IDs. No unread count is fabricated.
1857
1837
  - **Notices.** What the person is told about, in three fixed categories, as `{id, category,
1858
1838
  conversation?, from?: {name, email?, org}, line?, at}`:
1859
- - `message`: a line filed for the person in a DM or a channel from another person or their agent, or
1860
- from one of the person's own agents in their own conversation; never the person's own line, a line
1861
- about a channel change, or a change of a line's delivery state. Its `conversation` is the DM or the
1862
- channel, and `from` the sender's person as the realm verified them.
1863
- - `request`: a request held for the person (§6.9), with `conversation` the sender's
1864
- `agent:<person>/<agent>` (`dm:<person>` when the person sent it themself, as for a message request); or a
1865
- suggestion of one of the person's own agents, with `conversation` `agent:<own person>/<agent>`.
1839
+ - `message`: a retained legacy notice, without transcript text. Live conversation updates use the conversation stream.
1840
+ - `request`: a held request or retained contact suggestion (§6.9). New notices contain no legacy room reference.
1866
1841
  - `alert`: an alarm of this device (`keys_missing`, `restored`, `directory`), with fixed text in `line`; a
1867
1842
  change of another device of the person (below), with `device`; or trust the person gave someone that ends
1868
1843
  within 7 days (§6.9), with `from` that person and `line` "Trust you gave this person ends on <date>; renew it
@@ -1887,13 +1862,11 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1887
1862
  was replaced by 'desk2' (jelly-host), ...", "'phone' (laptop) was removed from your account.".
1888
1863
  - Each notice is filed once, when its source is, under an id derived from the source (`m_<line id>`,
1889
1864
  `r_<request id>`, `a_<alarm and time>`, `a_dev<log seq>`, `a_trust<grant id>`), so a replay repeats the same ids. A notice copies no text:
1890
- with `lines: true`, a message's `line` is the first line of its text read from the transcript,
1891
- through `cleanPeerText`, at most 120 characters with its "…".
1865
+ `lines: true` can expose fixed alert text. It never restores legacy transcript content.
1892
1866
  - `notice.subscribe {after?, lines?}` sends the unacknowledged notices filed after the notice `after`
1893
1867
  (all of them without it, at most 500), oldest first, as `notice` events on this connection, then each
1894
1868
  new one as it is filed; it returns `{replayed}`. `notice.list {lines?, limit?}` returns the
1895
- unacknowledged notices, newest first (default 50), and `counts`: `unread`, the lines from others not
1896
- yet marked read over every conversation, and `requests`, the asks, message requests and suggestions held. `notice.ack {ids}` marks
1869
+ unacknowledged notices, newest first (default 50), and `counts.requests`: the asks, message requests and suggestions held. `notice.ack {ids}` marks
1897
1870
  notices seen; unknown ids are ignored. It never answers, releases or uncounts a request: a request stays held, in
1898
1871
  `request.list` and `counts.requests`, until `request.answer` (or its expiry). Notices go with the transcript's retention, and acknowledged ones
1899
1872
  with the read mail's.
@@ -1928,8 +1901,7 @@ delegation chain is verified after these (§6.7). A person refuses another perso
1928
1901
  - Receipts, `task.status` and `task.artifact` answer the recipient's own traffic, so they have a
1929
1902
  budget of their own: four times that.
1930
1903
  - Presence has a budget of its own, also four times that.
1931
- - Chat envelopes (a `message` to the machine with no session, §6.8) have a budget of their own,
1932
- `LIMITS.inboundPerMachinePerMinute`, apart from agent mail.
1904
+ - Person-addressed controls (contact requests, cards and grants) have their own budget of `LIMITS.inboundPerMachinePerMinute`.
1933
1905
  - Over the limit, an envelope is **deferred**: the recipient withholds its `recv.ack` and does not
1934
1906
  mark it seen, so the relay sends it again later (§7 "Delivery") and it is taken when the budget
1935
1907
  has room. A backlog that arrives at once is so taken at the budget's pace, all of it, not
@@ -1938,9 +1910,7 @@ delegation chain is verified after these (§6.7). A person refuses another perso
1938
1910
  can wait.
1939
1911
  - An envelope over the limit that would expire before the budget has room for it, with a minute
1940
1912
  to spare for the relay's next redelivery, can never be taken. It is dropped (acked). A message
1941
- sender gets at most one `rate_limited` receipt a minute. A chat sender gets a `rejected` receipt
1942
- with `rate_limited` for every such chat envelope, up to four times the chat budget a minute, so
1943
- its line is `refused` at once instead of `sent` until it expires.
1913
+ sender gets at most one `rate_limited` receipt a minute. Person-addressed controls get individual refusals, bounded by the response budget.
1944
1914
  - Presence over its budget is dropped: it is never queued, and the next one supersedes it.
1945
1915
  - **Quota.** A recipient may cap the inbox items and bytes it holds from one sender machine
1946
1916
  (`rate_limited` past it).
@@ -1953,6 +1923,21 @@ delegation chain is verified after these (§6.7). A person refuses another perso
1953
1923
  - **`wake` on an inbox item.** This is the sender's hint, AND-ed with the session's tier, which must be
1954
1924
  `push`. Only what consent delivers becomes an inbox item, so a held request never wakes a session.
1955
1925
 
1926
+ - **Mixed versions** (0.7.1 and the conversations release). Neither side shims the other, and neither leaves a
1927
+ half-open contact:
1928
+ - A daemon from before conversations sends a chat line, plain mail or a message request without a portable proof
1929
+ as an envelope. A current daemon refuses each with a `rejected` receipt, code `refused`, and the `note` "The
1930
+ recipient runs a newer Messaging that takes conversations only: update Messaging to reach them." 0.7.1 records
1931
+ only the code: it shows `refused` with its hint "do not retry; tell your person", the clearest it can show, since
1932
+ none of its hints names an update. The note is for later senders and the logs.
1933
+ - A current daemon asks the relay (`conv.caps`, §7) before it creates a dm, group or channel with a person, adds
1934
+ one, or sends a message request. A person whose known devices all lack the `conversations` presence feature is
1935
+ refused with `peer_outdated`, "<handle> needs to update Messaging", and nothing is created or sent. With no
1936
+ presence for that person in the envelope TTL, the relay has no evidence, and the call goes ahead.
1937
+ - Tasks keep their envelopes and work in both directions.
1938
+ - Rollout: the relay first (it serves 0.7.1 unchanged), then every daemon of a realm together
1939
+ (`docs/distribution.md`, "Versions and compatibility").
1940
+
1956
1941
  ### 8.4 Delivery stages (sender view)
1957
1942
 
1958
1943
  - The normal sequence is `accepted → relayed → delivered → handed_to_agent → read`. A request the
@@ -2035,8 +2020,8 @@ then lives in plaintext in the sender's and recipient's stores for their retenti
2035
2020
  and in the recipient's model context. The sending daemon therefore refuses such content before it
2036
2021
  stores or seals anything. The relay never sees plaintext, so this check exists only on the sender.
2037
2022
 
2038
- - **Where.** `msg.send` (lines, messages, replies and task requests), `task.update` (status message
2039
- and artifact), `task.cancel` (`reason`) and `person.send` (`text`). The daemon reads every string in
2023
+ - **Where.** `task.create`, `task.continue`, `conv.send`, `conv.edit`, `task.update` (status message
2024
+ and artifact), and `task.cancel` (`reason`). The daemon reads every string in
2040
2025
  the call's params except the session `key`: values and object keys at any depth, including
2041
2026
  `metadata` and `data` parts, and the base64-decoded bytes of every `raw` field, read as UTF-8.
2042
2027
  - **When.** After the key and task-role checks (`not_found`, `invalid_state` for a wrong role), and
@@ -2075,29 +2060,62 @@ stores or seals anything. The relay never sees plaintext, so this check exists o
2075
2060
 
2076
2061
  ### 8.8 Store schema
2077
2062
 
2078
- The daemon's store (`messaging.db`) and the relay's database each have one baseline, schema 1. Messaging resets
2079
- its stores now and grows them later (owner, 2026-11-08):
2080
-
2081
- - **Until the first release another person uses**, a store change edits the baseline instead of adding a
2082
- migration, and raises the store's generation. The release that carries it resets the stores and says so in its
2083
- notes. Release 0.7.0 is the first such reset: it reads no store of 0.1.0 to 0.6.0.
2084
- - **From that release on**, stores only grow, so that a host can roll a daemon or relay back by moving the
2085
- binary back and keeping the data. Migrations then follow the rules under "When stores grow" below.
2063
+ The daemon's store (`messaging.db`) and the relay's database each have one baseline, schema 1, as release 0.7.0
2064
+ created it (owner, 2026-11-08):
2065
+
2066
+ - **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
2067
+ person uses (Jelly's hosted users), so no later release resets a store or raises a generation.
2068
+ - **From 0.7.0 on, stores only grow**, so that a host can roll a daemon or relay back by moving the binary back
2069
+ and keeping the data. A store change adds an idempotent migration and a new schema number, under the rules of
2070
+ "When stores grow" below; the baseline is never edited.
2071
+ - The daemon store is at **schema 4** and the relay database at **schema 3**, each the baseline plus its migrations:
2072
+ - daemon schema 2 adds `conv_logs`, `conv_entries`, `conv_shares`, `conv_cursors`, `conv_outbox`, `conv_keys`,
2073
+ `conv_alarms` and `conv_fts`; schema 3 adds `conv_held`; schema 4 adds `conv_evidence`;
2074
+ - relay schema 2 adds `conv_logs`, `conv_entries`, `conv_members` and `conv_shares` (§7 "Conversation frames");
2075
+ schema 3 adds `conv_request_decisions`.
2076
+ Each keeps `min_reader` 1, so a 0.7.x daemon or relay still opens the store after a rollback and ignores the new
2077
+ tables.
2086
2078
 
2087
2079
  **Records.** The daemon's store keeps `generation`, `schema` and `min_reader`, the lowest schema that can read
2088
2080
  it, in its `kv` table. The relay keeps `generation` and `min_reader` in its `meta` table, and `schema` in
2089
- `PRAGMA user_version`. A new store records generation 2, schema 1 and `min_reader` 1. The stores of 0.1.0 to
2090
- 0.6.0 have no generation (generation 1).
2081
+ `PRAGMA user_version`. Every store from 0.7.0 on records generation 2 and `min_reader` 1; a new daemon store records
2082
+ schema 4, and a new relay database schema 3. The stores of 0.1.0 to 0.6.0 have no generation (generation 1).
2091
2083
 
2092
- **Opening.** A store with no tables is new: the daemon or relay writes the baseline and its records. For the relay,
2084
+ **Opening.** A store with no tables is new: the daemon or relay writes the baseline, its records and every migration. For the relay,
2093
2085
  the tables Litestream adds when it replicates the database (`_litestream_*`) do not count. It opens
2094
2086
  any other store only when the store records its own generation and a `min_reader` at most its own schema. It
2095
- refuses every other store and changes nothing in it, and the error says how to start over: stop the daemon or
2096
- relay, move the home (or the relay database, with its `-wal` and `-shm` files) aside, and set it up again
2097
- (`messaging-relay` logs it as `relay.store_refused {db, reason}` and exits 1). A
2098
- daemon checks the store before it reads or writes anything else in its home. Nothing deletes a home or a
2087
+ refuses every other store and changes nothing in it. The error gives the reason and its remedy (below).
2088
+ Only `pre-reset` calls for starting over: stop the daemon or relay, move the home (or the relay database,
2089
+ with its `-wal` and `-shm` files) aside, and set it up again. For `newer`, upgrade the reader and keep the data.
2090
+ A daemon checks the store before it reads or writes anything else in its home. Nothing deletes a home or
2099
2091
  database by itself.
2100
2092
 
2093
+ **A refused store** is machine-readable, so that a client never matches the wording. Every refusal has one of two
2094
+ **reasons**, and the remedy depends on it:
2095
+
2096
+ | Reason | The store | Remedy |
2097
+ |---|---|---|
2098
+ | `pre-reset` | records no store generation, or another generation than the reader's | Start over: move the home (or the relay database) aside and set it up again. |
2099
+ | `newer` | its `min_reader` is above the reader's schema: a newer messagingd or relay wrote it | Upgrade the daemon or relay, and keep the home or database as it is. Never move it aside for this reason. |
2100
+
2101
+ - `messagingd` exits with status **78** (`EX_CONFIG` of sysexits(3); `STORE_REFUSED_EXIT` in `@messaging/core`)
2102
+ for either reason, and prints one line on stderr, `messagingd: store_refused: <reason>: <why, and what to do>`.
2103
+ No other failure exits 78.
2104
+ - `messaging-relay` exits 78 and logs one JSON event, `relay.store_refused {db, reason, message}`: `reason` is
2105
+ `pre-reset` or `newer`, and `message` is the text.
2106
+ - A client that starts the daemon and sees it exit 78 reports the error code `store_refused` (§13), with the reason
2107
+ in a field of its own (`StoreRefusedError.reason`; `reason` in `messaging --json` errors and in agent tool
2108
+ errors). It reads the reason from the two tokens after `messagingd:` on the line that start printed
2109
+ (`storeRefusedReason` in `@messaging/core`) and never parses the text after them, which may change between
2110
+ releases. A line without a reason token gives `store_refused` without a reason. `messaging daemon start` does
2111
+ this.
2112
+ - A client offers to start over (moving the home aside) **only for `pre-reset`**. For `newer`, and for a refusal
2113
+ without a reason, it tells the person to upgrade or to read messagingd's line, and moves nothing.
2114
+ - A home moved aside is named `<home>.pre-reset-<UTC timestamp>`, the timestamp as `YYYYMMDDTHHMMSSZ` (for example
2115
+ `~/.messaging.pre-reset-20261108T171500Z`). The name carries no version, so it holds for every reset, and a
2116
+ client that denies its agents the home also denies `<home>.pre-reset-*`: the moved home keeps the old keys and
2117
+ messages. The messagingd that wrote it still opens it.
2118
+
2101
2119
  **When stores grow.**
2102
2120
 
2103
2121
  - A daemon or relay opens any store of its generation whose `min_reader` is at most its own schema. It
@@ -2111,10 +2129,13 @@ database by itself.
2111
2129
  - A breaking change needs a new store file with an explicit export and import, never an in-place change.
2112
2130
  `min_reader` is raised only in an emergency, and hosts that pin the daemon are told first.
2113
2131
 
2114
- **The test.** `store-schemas/1.json` in `packages/daemon` and `packages/relay` is the shape of the baseline
2115
- (tables, columns, indexes, constraints, triggers). `test/release/store-schema.test.ts` in each package fails
2116
- when HEAD's store differs from it, so a store change is seen in review. When stores grow, each released schema
2117
- gets its snapshot, and the test holds HEAD to every one of them.
2132
+ **The test.** `store-schemas/<n>.json` in `packages/daemon` and `packages/relay` is the shape of schema n as
2133
+ released (tables, columns, indexes, constraints, triggers); `1.json` is the baseline as 0.7.0 created it.
2134
+ `test/release/store-schema.test.ts` in each package holds the baseline to `1.json` and HEAD's new store to the last
2135
+ snapshot, so a store change is seen in review. Each also opens a database the schema-1 release itself wrote
2136
+ (`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
2137
+ changes nothing, and that a schema-1 reader still opens the result: its records allow it, and every table, column
2138
+ and index of schema 1 is unchanged inside it.
2118
2139
 
2119
2140
  The wire rule (§14) is separate; a rolled-back daemon must still speak to newer peers within `messaging/1`.
2120
2141
 
@@ -2231,9 +2252,9 @@ and names each session an engine may register.
2231
2252
  - `exp - iat` is in (0, 60] seconds, `iat` at most 5 s ahead of the daemon's clock and `exp` at most 5 s
2232
2253
  behind it;
2233
2254
  - `cls` is required, and is the method's action class (`ACTION_CLASSES`, `actionClass` in `@messaging/core`):
2234
- `chat` for `person.send`, `conv.markRead` and `notice.ack`; `view` for the reads a host's section makes, the
2235
- read-only human-tier methods and `device.list` (`conv.list`, `conv.read`, `conv.subscribe`, `people.search`,
2236
- `people.get`, `grant.list`, `request.list`, `agents.list`, `agents.read`, `notice.subscribe`, `notice.list`,
2255
+ `chat` for `conv.send`, `conv.edit`, `conv.delete`, `conv.react` and `notice.ack`; `view` for the reads a host's section makes, the
2256
+ read-only human-tier methods and `device.list` (`conv.list`, `conv.read`, `conv.sync`, `conv.get`, `conv.members`, `conv.threads`, `conv.search`, `conv.context`, `conv.access`, `request.waiting`, `people.search`,
2257
+ `people.get`, `grant.list`, `request.list`, `sessions.list`, `notice.subscribe`, `notice.list`,
2237
2258
  `device.list`); `local` for every other method, the previews and every other admin-tier method included.
2238
2259
  (`device.status` is agent tier: it needs no grant.) So `view` is refused on any method that writes, and a preview, which tells what
2239
2260
  a `local` action would do, takes `local`. A plugin's `jelly` declaration must give each action and read the same
@@ -2247,8 +2268,8 @@ and names each session an engine may register.
2247
2268
  `person.attached`, `daemon.snapshot`, `wake.arm` and `wake.disarm`.
2248
2269
  It is bound to the first connection that presents it, and may be presented again on that connection until
2249
2270
  it expires; on another connection, or after a restart, it is `grant_invalid`. Under a host-scope grant
2250
- `conv.list` returns counts only: each conversation's `conversation`, `kind`, `readUpTo` and `unread`, with
2251
- no channel name, time or seq. A `notice.subscribe` under a host-scope grant ends when the grant does: the
2271
+ `conv.list` is refused before content is read. Lane 4 exposes no placeholder counts.
2272
+ A `notice.subscribe` under a host-scope grant ends when the grant does: the
2252
2273
  host subscribes again with a fresh grant and `after`.
2253
2274
  - **Refusals.** No grant is `grant_required`; any failure above is `grant_invalid`. The agent tier needs no
2254
2275
  grant on either socket. Both are checked before the params are read. The daemon logs each refusal with
@@ -2319,6 +2340,10 @@ callers rely on.
2319
2340
  It answers from what this machine has verified, synchronously; a caller catches up before it
2320
2341
  verifies. `Roster` implements it: `realm` is the team id, `device` is `member`, `anchor()` is
2321
2342
  `rosterAnchor`.
2343
+ - Conversations (§16): `checkVia`, `currentLinks`, `signConvEvent`, `verifyConvEvent`, `sealConvMsg`, `openConvMsg`, `sealConvMeta`,
2344
+ `openConvMeta`, `sealKeyShares`, `openKeyShares`, `ConvLog.apply`, `ConvLog.applyExpired`, `ConvLog.relate`
2345
+ and `resolveContext` throw only `MessagingError`. `ConvLog.at(seq)` is the state a log holding only entries 1
2346
+ to `seq` has. `ReceiverChains.messageKey` hands out each message key once.
2322
2347
  - `createGrant` narrows its parent and never widens it (`forbidden`); an expired parent is `expired`.
2323
2348
  - Consent (§6.9): `signConsentGrant` refuses a grant that breaks a rule (`invalid_request`) or is
2324
2349
  over `LIMITS.grantBytes` (`too_large`); `verifyConsentGrant` checks the schema, the rules, the
@@ -2470,30 +2495,38 @@ daemon owned by the uid in `MESSAGING_DAEMON_UID` (§8.1).
2470
2495
 
2471
2496
  ### Agent tools
2472
2497
 
2473
- The OpenCode plugin and `messaging-mcp` offer an agent the same eight tools, with the same input and
2474
- output schemas (`AgentTool` in `@messaging/core`, normative like `schema/**`): `contacts`, `send`,
2475
- `inbox`, `read`, `search`, `tasks`, `task_update` and `task_cancel`. Their names are
2498
+ **Conversation tools.** `ConversationTools` is the shared schema registry for the conversation-model
2499
+ cut-over. Its ten verbs are `contacts`,
2500
+ `conversations`, `send`, `inbox`, `read`, `search`, `context`, `tasks`, `task_update` and `task_cancel`.
2501
+ `conversations` uses `conv.access`; `send`, `read`, `search` and `context` use their `conv.*` counterparts.
2502
+ Results identify a line by the stable citation `messaging:<cid>/<id>`. The client fences peer text and names,
2503
+ neutralizes fence closers, and labels own text as ours. List results contain at most 50 events and 32 KiB of text;
2504
+ each fenced event also has the existing per-part cap, `LIMITS.toolTextChars` (32,768).
2505
+ Long bodies have `[truncated: N bytes; read the item for the full text]`, where N counts omitted UTF-8 bytes.
2506
+ The stored body stays intact. `read({source, offset})` continues at the returned `nextOffset` until the full body is shown.
2507
+ A held result waits for consent and does not disclose the person's answer. Tools grant no human-tier action.
2508
+
2509
+ The OpenCode plugin and `messaging-mcp` use the same ten-tool contract in `@messaging/core`, normative like `schema/**`.
2510
+ Their names are
2476
2511
  `messaging_<verb>`; in OpenCode's Code Mode, `tools.messaging.<verb>`. No tool grants, adds
2477
2512
  contacts, changes devices or administers a team or channel.
2478
2513
 
2479
- - **Addresses.** `me` (the session's own person), `me/<agent>`, `<person>`, `<person>/<agent>`,
2480
- `<person>/<agent>@<label>` and `#<channel>` (a channel the person is in, by its local name). An
2481
- agent is a session (§3). Malformed text is `invalid_request` naming the field, and so is a group
2482
- form (`person/*`, `repo:…`, `*`): there are none (§3). A person, agent or channel the daemon does not know
2483
- (`not_found` for a person or channel, `no_recipients` for an agent, from `msg.send` or `conv.read`) is `unknown_address`; an agent
2484
- 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
2485
- conversation, `dm:<self>` (§6.8): `send` writes a line there (unread for the person), `read` and
2486
- `search` read it, and a guarded peer turn may write there too. An `ask` to `me` (a request card to
2487
- one's own person) is `invalid_request`: it is not in this build.
2488
- - **Conversations.** `person:<person>` is the person's DM room with them (§6.8), seen from the agent;
2489
- `agent:<person>/<agent>` is the session's mail and tasks with that session on any machine
2490
- (`conv.read`, §8.2); `channel:<name>` is a channel room. `read({with})` pages one of them, oldest
2491
- first, with opaque `before`/`after` cursors. Mail and task entries carry previews; outgoing mail
2492
- carries no text, because the daemon keeps no copy of what it sent.
2514
+ - **Addresses.** Lines accept a canonical `cid` or a person or agent destination.
2515
+ `#<channel>` resolves through accessible conversations. An agent sends to a conversation it is seated in, or to another
2516
+ agent (`person/agent`). With no dm yet, its person's device signs the create of the agent dm, whose only participants
2517
+ are the two agents; the recipient's `ask` admits the invited agent (`open` at once, `request` after its person
2518
+ accepts, `no` never, silently). Until the seat the line waits on the sending device, keeps the id the send returned,
2519
+ shows in `request.waiting` as `pending-delivery`, and lapses after 7 days. An agent cannot start a dm with a person,
2520
+ and cannot add or seat anyone. The sender-side consent is the client's first-send confirmation for the address (the
2521
+ plugin's `messaging.send` permission), as for 0.7.1 mail; the daemon has no separate signal.
2522
+ Retained tasks use `person/agent[@label]`. Malformed addresses are `invalid_request`.
2523
+ An ambiguous task address requires a device label. Human contact requests remain human-only.
2524
+ - **Conversations.** `read({cid})` pages current verified events with opaque cursors.
2525
+ Conversation identity is stable across devices. Legacy room names are not aliases.
2493
2526
  - **`contacts`** lists `me` (address, person, agent, device label, relay `connected` or `offline`), the
2494
- persons in the directory and in DM rooms, each with their agents (state, `expect` from
2495
- `contacts.list` (§8.2), repo, branch and doing), and the channels `contacts.list` names, with their unread
2496
- counts. `org` is `same` for every teammate. A person's `can` is the daemon's `reach` for them (§8.2
2527
+ persons in the directory and in conversations, each with their agents (state, `expect` from
2528
+ `contacts.list` (§8.2), repo, branch and doing), and channels allowed by `conv.access`.
2529
+ Counts and read preferences are omitted when unavailable. A person's `can` is the daemon's `reach` for them (§8.2
2497
2530
  `contacts.list`): `source: "card"` with `card`, when their person signed it, or `source: "default"`, the
2498
2531
  defaults for the org or for a stranger; `can.chat` is false for one's own person and when the client offers no
2499
2532
  DMs. An agent's `can` is its person's, except that `can.chat` is false and the inbound policy narrows it (§8.3: a
@@ -2503,28 +2536,21 @@ contacts, changes devices or administers a team or channel.
2503
2536
  refused. `can: "ask"` lists those whose `can.ask` is not `no`. A channel's `can` is `default`. `presence` is
2504
2537
  `online`,
2505
2538
  `offline` or (for a channel) `unknown`. No field is invented.
2506
- - **`send`** is one `msg.send`: a chat line to `<person>` or `#<channel>`, or mail or, with `ask`, a
2507
- task to an agent. `replyTo` names a line (the thread), a message from that agent
2508
- (`replyTo` and its `contextId`), or a task the session requested (`taskId`: input for it, to its
2509
- executor only). `onBehalfOf` (with `ask`) passes `msg.send {onBehalfOf: {taskId}}` after
2510
- checking that the session runs that task and it is not finished. `files` (only to an agent) become
2511
- file parts. `key` makes a repeated call send once (`idempotencyKey`);
2512
- without it the client derives one from the tool call. Results carry each `SendResult.expect` and a
2513
- fixed `note`. A result at stage `held` (the recipient's person answers a request card first, §6.9)
2514
- carries `expect: "on_consent"` and a note saying so. `request: {contact: <email>}`, only with `to: "me"`
2515
- (otherwise `invalid_request`), passes `msg.send {request}`: the line goes to the agent's own person with a
2516
- suggestion to send a message request to that exact email (§6.9, "Message requests"); the agent cannot send one,
2517
- and its note says it is not told what the person decides. A send refused `request_required` tells the agent to
2518
- ask its person this way.
2539
+ - **Sources.** An item target or result names `source: {kind: "conv", cid, event}` or `source: {kind: "task", taskId}`.
2540
+ A task never receives a fabricated conversation id. Conversation item identity includes both `cid` and `event`.
2541
+ - **`send`** uses `conv.send` for lines, including agent DMs. `ask` uses `task.create`.
2542
+ A task continuation uses `task.continue`. Task sends retain `files`, `replyTo`, `onBehalfOf` and `key`.
2543
+ Delegation requires an unfinished task that this session executes. `key` or `clientId` makes retries idempotent.
2544
+ A line returns its conversation source and `pending`. A task returns its task source and delivery results.
2545
+ An authenticated hold carries `expect: "on_consent"`. Missing receipts do not imply a hold.
2519
2546
  - **`inbox`** lists mail and task requests (`inbox.list`), unread by default, and acks
2520
2547
  `handed_to_agent` for the previews it shows. `consent` says how a request from the sender runs
2521
2548
  here: `self` (the session's own person), `trusted` or `guarded`. It is the inbox item's `consent`, the
2522
2549
  daemon's decision (§6.9); a client decides it only for a task record or an item stored before
2523
2550
  consent, which carry none.
2524
- - **`read({id})`** looks the id up as an inbox item (`inbox.get`, then `item.read`: marked read, read
2525
- receipt), a task (`task.get`), or an envelope the session sent (`item.read`, as `delivery`).
2526
- - **`search`** covers chat lines (`search`) with persons and in channels; agent mail is not
2527
- searched.
2551
+ - **`read({source})`** reads the named conversation event or retained task. It preserves the daemon's consent decision.
2552
+ A held event read acknowledges nothing. `read({cid})` returns a conversation page with opaque cursors.
2553
+ - **`search`** uses `conv.search` within the daemon's access rules.
2528
2554
  - **`tasks`** lists `task.list` with roles `mine` (requester) and `asked` (executor) and short state
2529
2555
  names (`working`, `input_required`, …); `state` also takes `open` and `done`.
2530
2556
  - **`task_update`** (executor) and **`task_cancel`** (requester) map to `task.update` and
@@ -2563,12 +2589,12 @@ contacts, changes devices or administers a team or channel.
2563
2589
 
2564
2590
  ### Clients
2565
2591
 
2566
- - **OpenCode plugin.** Tools under namespace `messaging`: `contacts`, `send`, `inbox`, `read`,
2567
- `search`, `tasks`, `task_update` and `task_cancel`. Every send asserts `messaging.send` with the
2592
+ - **OpenCode plugin.** Tools under namespace `messaging`: `contacts`, `conversations`, `send`, `inbox`, `read`,
2593
+ `search`, `context`, `tasks`, `task_update` and `task_cancel`. Every send asserts `messaging.send` with the
2568
2594
  address as its resource (`person/agent`, `#channel`, or `person` and `person/`), and every read of
2569
2595
  a conversation asserts `messaging.read` with the conversation (a `person:` one asks first by
2570
2596
  default). Inbound content is framed as untrusted data. Details are in the plugin's repository, `joe-broadhead/opencode-messaging` (`docs/`).
2571
- - It never calls `conv.markRead`: read state is the person's.
2597
+ - It never changes human read state.
2572
2598
  - With wakes on, it registers each session with `host: {kind: "opencode", location, sessionID,
2573
2599
  pid}`: its location directory, the OpenCode session id, and the server's pid.
2574
2600
  - Its RPC `messaging.opencode/wake {sessionID, envelopeIds}` serves the wake bridge (§8.6). It checks
@@ -2592,16 +2618,10 @@ contacts, changes devices or administers a team or channel.
2592
2618
  item only after the host recorded the tool result with the note intact; until then the item is
2593
2619
  only offered, and an offer that fails or is cut goes back to waiting. No wire change.
2594
2620
  - **MCP server.** `messaging-mcp` (stdio) for other agents. It registers one `pull`-tier session and
2595
- subscribes to its events. Its tools are `messaging_contacts`, `messaging_send`, `messaging_inbox`,
2596
- `messaging_read`, `messaging_search`, `messaging_tasks`, `messaging_task_update` and
2597
- `messaging_task_cancel`, over `daemon.status`, `contacts.list`, `msg.send`, `item.read`,
2598
- `inbox.list/get/ack`, `conv.read`, `task.list/get/update/cancel` and `search`.
2599
- - MCP has no per-conversation approval of its own, so chats are opt-in: the user names channels
2600
- when configuring the server, with `--channel <name>` (repeatable). `send`, `read` and `search`
2601
- reach those channels only, and are refused (`forbidden`) for any other channel before any daemon
2602
- call. Conversations with persons (DM lines) are never available over MCP: a line to a person, or
2603
- `read`/`search` with one, is `forbidden`. It never calls `conv.subscribe`, `conv.markRead`,
2604
- `person.send` or `channel.*`.
2621
+ 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`.
2622
+ Lines and conversation reads use `conv.*`. Retained tasks use `task.create/continue/list/get/update/cancel` and `inbox.list/get/ack`.
2623
+ - The daemon controls access through `conv.access`. Contacts expose only accessible channel cards.
2624
+ MCP uses the agent socket and never calls human-tier methods or changes human read state.
2605
2625
  - It has no peer-turn guard: its agent reads every request itself, with its own tools. It reports
2606
2626
  the daemon's `consent` for each item; where the daemon gives none (a task record), `self` for the
2607
2627
  session's own person and `trusted` for everyone else.
@@ -2738,7 +2758,7 @@ contacts, changes devices or administers a team or channel.
2738
2758
  `prompt` when a dialog on every daemon start is acceptable.
2739
2759
  - **Local IPC.** The sockets trust users, not processes (§8.2).
2740
2760
  - The daemon enforces two sockets: `run/agent.sock` serves only `AGENT_METHODS`, so never a line as
2741
- the person (`person.send`); `run/admin.sock` serves everything.
2761
+ the person; `run/admin.sock` serves everything. Agent conversation calls require a registered session.
2742
2762
  - Under one OS user that is no boundary: any process of the user can open `run/admin.sock`, call
2743
2763
  `team.*` and `daemon.stop`, and act for any session whose key it knows: session keys are bearer
2744
2764
  credentials. The tiers keep those methods off agent tool paths, nothing more.
@@ -2783,6 +2803,10 @@ contacts, changes devices or administers a team or channel.
2783
2803
 
2784
2804
  ## 13. Error codes
2785
2805
 
2806
+ Tool errors use the codes in this section for both conversation and task sources (§11).
2807
+ An unknown source is `not_found`. A source that combines task and conversation identifiers is `invalid_request`.
2808
+ Oversized model-visible text is truncated with the explicit marker in §11, rather than reported as `too_large`.
2809
+
2786
2810
  | Code | Meaning |
2787
2811
  |---|---|
2788
2812
  | invalid_request | malformed input or schema violation |
@@ -2795,6 +2819,7 @@ contacts, changes devices or administers a team or channel.
2795
2819
  | refused | recipient policy refuses this sender |
2796
2820
  | declined | the recipient's person declined a held request (§6.9) |
2797
2821
  | request_required | the recipient's person has not accepted this sender: another org's person needs a message request first (§6.9) |
2822
+ | 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 |
2798
2823
  | rate_limited | too many envelopes; retry later |
2799
2824
  | loop_guard | agent-to-agent loop guard tripped |
2800
2825
  | too_large | size limit exceeded |
@@ -2808,10 +2833,12 @@ contacts, changes devices or administers a team or channel.
2808
2833
  | 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) |
2809
2834
  | roster_conflict | roster entry does not extend the current head |
2810
2835
  | invite_invalid | invite unknown, expired, revoked, used or mismatched |
2811
- | no_identity | the daemon has not created or joined a team |
2836
+ | no_identity | the daemon has no usable identity: no team, or a directory device not yet bound with published keys (§15.9) |
2812
2837
  | binding_invalid | a device binding, a device-log entry or its authority failed verification (§15) |
2813
- | jelly_unavailable | Jelly ID's key set or key history could not be fetched (§15.2); never reported as `binding_invalid` |
2838
+ | 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` |
2814
2839
  | log_conflict | a device-log entry does not extend the log's head (§15.3); resync and retry |
2840
+ | conv_conflict | a conversation entry does not extend the log, or an event's op head is not the current op (§16.7); re-read and retry |
2841
+ | 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 |
2815
2842
  | device_limit | the person already has `LIMITS.devicesPerPerson` current devices (§15.3) |
2816
2843
  | admission_required | `dir.bind` of an entry that is not a device binding itself with a `jelly` authority (§15.10) |
2817
2844
  | admission_invalid | `dir.bind` whose Jelly token is not live now, or not for this directory's issuer, resource and person (§15.10) |
@@ -2826,6 +2853,7 @@ contacts, changes devices or administers a team or channel.
2826
2853
  | ticket_invalid | `deployment: "jelly"`: the session ticket did not verify, is out of its lifetime, or names another session or agent (§8.11) |
2827
2854
  | daemon_unavailable | a client cannot reach the daemon |
2828
2855
  | daemon_mismatch | the daemon on the socket is another release or IPC protocol than the client, or did not say which (§8.2) |
2856
+ | store_refused | messagingd refused the home's store and changed nothing; it exits 78 (§8.8). Its `reason` field says why: `pre-reset` (no or another store generation: start over by moving the home aside) or `newer` (a `min_reader` above its schema: upgrade, keep the home); absent when messagingd named none. A client code: a client that starts the daemon reports it, never the daemon over IPC |
2829
2857
  | internal | unexpected failure (details stay in local logs) |
2830
2858
 
2831
2859
  ## 14. Versioning
@@ -3327,11 +3355,9 @@ when a restore does not reach it: keep it on other storage than the database, or
3327
3355
  who accepts the fork on purpose moves the witness aside and starts again.
3328
3356
 
3329
3357
  **Resolution.** From each person's newest verified Jelly token the directory keeps their name, their email when
3330
- Jelly ID proved it, and from that email their **handle** and **org** (`emailHandle`, `emailOrg`):
3358
+ Jelly ID proved it, and from that email their **handle** and **org** (`hostedHandle`, `emailOrg`):
3331
3359
 
3332
- - the handle is the email's local part lower-cased, each run of characters outside `[a-z0-9]` made one `-`, `-`
3333
- trimmed from both ends, cut to 30 characters (trimmed again), and `u-` put in front when it does not start with
3334
- a letter;
3360
+ - the handle uses `hostedHandle`, exactly as §15.8 defines it;
3335
3361
  - the org is `domain:<d>` when the email's domain `d` is in the operator's `orgDomains` (`--org-domains`), and
3336
3362
  none otherwise: such a person is an individual, in no org.
3337
3363
 
@@ -3424,3 +3450,432 @@ five minutes, until it answers, so a new directory becomes ready without waiting
3424
3450
  `dir_wakes`: each Jelly host's device's armed wake ticket, its `agents` and `awaiting`, its `exp` and when it may
3425
3451
  next be presented (§7, "Wake tickets"). At start the directory rebuilds the map and the epoch log from them
3426
3452
  and refuses to start if they disagree with the last tree head.
3453
+
3454
+ ## 16. Conversations
3455
+
3456
+ **Guest acceptance (X3-1).** An invited participant is not a content reader.
3457
+ The relay serves authenticated control history and heads to invitees, with message digests instead of ciphertext and a zero share cursor.
3458
+ It pushes invitation operations, never message content or keys, to a person represented only by invited seats.
3459
+ `ConvLog.checkShare` requires an accepted recipient. For a group or history `none`, acceptance (including seating an invited
3460
+ agent) is the effective addition: it updates `since` and requires an `added` rekey before messages or shares.
3461
+ Inviting alone does not start that epoch. Shared channels keep their declared shared-history exposure after acceptance.
3462
+ A guest cannot invite or change to another role, even via an intermediate member role. Agents inherit their owner's
3463
+ guest restriction; a guest agent needs an accepted, visible guest owner and that owner's signed seat. Live org checks
3464
+ cover agent principals too; two persons without an org are not implicitly one org.
3465
+
3466
+ A guest's `accept` carries `invitation`, a `GuestInvitation` signed under `DOMAIN.guestInvitation`.
3467
+ The preview binds the invitation ID, inviter device, realm, conversation, invitee, exact op head, expiry, current audience and exposure policies.
3468
+ The device that signed the original add or create signs this bounded preview. The inviter seals it to the invitee without history or keys.
3469
+ The accept event signs the same preview. A changed audience, policy or op head requires a fresh preview.
3470
+ After acceptance, the invitee fetches the log and verifies the invitation independently before content or key use.
3471
+ `guestHistoryExposure` supplies explicit text for shared history and history from the acceptance boundary.
3472
+
3473
+ **Agent-DM admission.** The recipient's `ask` policy controls an agent-only DM invitation, including invitations across orgs.
3474
+ `open` seats the recipient agent, `request` holds the invitation for the person, and `no` silently declines it locally.
3475
+ `request.list` exposes a held invitation as `{kind:"agent-invitation",id,cid,from,to,at}`, without content or keys.
3476
+ `request.answer` accepts `accept` or `decline`. Its preview identifies the sender and target agent and reports the affected count.
3477
+ With `all`, the answer applies to held agent invitations from the same person.
3478
+
3479
+ The daemon records invitation-only history gaps durably. After acceptance, it fetches those entries again and verifies each signature against its stored digest.
3480
+ It replaces only matching gaps and applies the conversation's history policy before content use.
3481
+ This recovery survives restart. It never restores messages that local retention expired after acceptance.
3482
+
3483
+ A conversation is one hash-chained log of signed events that the relay sequences and members verify. Membership,
3484
+ roles, epochs and settings are a function of the log. Message bodies are sealed under per-device sender keys, so a
3485
+ post is one ciphertext whatever the number of members. The schemas are in `schema/conversation.ts`, the rules in
3486
+ `conversation/` of core, and the vectors in `docs/spec/vectors/conversation.json`. The relay carries the logs in
3487
+ the conversation frames of §7. The daemon's storage and the IPC surface are not specified yet;
3488
+ `docs/design/conversations.md` describes them. Until they land, chats are as §6.8 says.
3489
+
3490
+ ### 16.1 Identifiers
3491
+
3492
+ - A **participant** (`ParticipantId`) is a person (§3, §15.1) or one agent of a person: `person/agent`, where
3493
+ `agent` is a session name (§3). Every agent belongs to one person, its **person**.
3494
+ - A conversation id (`ConvId`) is one of:
3495
+ - a **dm**: `"d_" + base32lower(SHA-256("messaging/1/dm\n" || JCS([a, b] sorted)))[0..26]`, where `a` and `b`
3496
+ are its two participants, sorted by UTF-16 code units. Both ends derive the same id. `a` may equal `b`: a
3497
+ person's note to themselves.
3498
+ - a person's **self** conversation: `"s_" + base32lower(SHA-256("messaging/1/self\n" || JCS(person)))[0..26]`.
3499
+ - a **group** or **channel**: `"c_"` and a ULID its creator chooses.
3500
+
3501
+ base32lower is RFC 4648 §6 in lower case without padding, as for `PersonId` (§15.1).
3502
+ - Kinds (`ConvKind`): `dm`, `group`, `channel` and `self`. Only a channel has a visibility (`public` or
3503
+ `private`). The history policy (`none` or `shared`) says whether a late joiner may read earlier messages.
3504
+ - Roles (`ConvRole`): `owner`, `admin`, `member` and `guest`. A participant's state is `member`, or `invited`
3505
+ until it accepts or is seated (§16.7).
3506
+
3507
+ ### 16.2 Events and the log
3508
+
3509
+ Every event has a header and a body. The header is:
3510
+
3511
+ ```text
3512
+ {v: "messaging/1", realm, cid, id, by: {person, device, agent?}, ts, opHead: {seq, hash}, seen?: {seq, hash},
3513
+ class, epoch?, idx?, wake?}
3514
+ ```
3515
+
3516
+ - `id` is a ULID the author chooses. It is unique in the conversation: the log refuses an event whose `id` it
3517
+ already holds (`duplicate`). Other events refer to an event by `id`.
3518
+ - `by` names the signing device, its person and, for an agent's event, the agent. `by.device` must be a usable
3519
+ Ed25519 key (§3).
3520
+ - `opHead` is the last op the author applied: its `seq` and `h(seq)`, or `{0, h(0)}` before any.
3521
+ - `seen` is the head the author had, for gossip (§16.4).
3522
+ - `class` is:
3523
+ - `op`: a control op (`ConvOp`), because the relay routes and checks authority by it. Every piece of state is an
3524
+ op, and ops never expire (§16.4); an op whose content must stay private carries it sealed (§16.5);
3525
+ - `msg`: a sealed `MsgBody`, with the `epoch` and the ratchet index `idx` of its key (§16.5). `wake` (at most
3526
+ `LIMITS.wakeHints` (5)) names sessions of `by.person` the event may wake; it is the only plaintext hint. The
3527
+ author's daemon sets it only on a line that addresses the author's own agents, and the relay acts on it only for
3528
+ the authenticated author's person's devices (§7, "Wake tickets");
3529
+ - `keys`: a sender-key share sealed to one device (§16.6). It is signed like every event but is **not an entry
3530
+ of the log**: the relay keeps it for its recipient alone.
3531
+ - `sig` covers the whole event: `Ed25519(by.device, "messaging/1/conv-event\n" || JCS(event without sig))`.
3532
+ - An event is at most `LIMITS.convEventBytes` (256 KiB) of JCS (`too_large`). Every object is strict: an unknown
3533
+ member is `invalid_request`.
3534
+
3535
+ The relay stores and serves **entries**: `{seq, prev, event}` of ops and msgs, where `seq` counts from 1 and `prev`
3536
+ is `h(seq − 1)`. The relay does not sign entries; members verify the chain (§16.4).
3537
+
3538
+ **Ops** (`ConvOp`): `create`, `add`, `accept`, `join`, `leave`, `remove`, `role`, `seat`, `settings`,
3539
+ `archive`, `rekey`, `policy`, `meta` and `setting`.
3540
+
3541
+ - `create` states the kind, the visibility (channels), the history policy, `retainDays` (1 to
3542
+ `LIMITS.retainDaysMax` (365)), the org a public channel is open to, the participants as seats `{p, role,
3543
+ device?, respondTo?}`, the permits `post`, `invite` and `pins` (`all` or `admins`; absent is `all`), and
3544
+ `sealedMeta` (§16.5).
3545
+ - `add` names 1 to `LIMITS.convOpParticipants` (100) seats; `remove` names as many participants.
3546
+ - `seat` sets an agent participant's `device` (the device that hosts it) and `respondTo` (`self`, `members` or
3547
+ `none`), and accepts its invite. Only an agent's seat carries `device` or `respondTo`.
3548
+ - `settings` changes the visibility, history, retention or permits. `archive {on}` archives or restores.
3549
+ - `rekey {epoch, cause}` starts the next epoch; `cause` is `removed`, `device`, `added`, `settings` or
3550
+ `schedule`. A device rekey also names the removed `device` and may give `until`, a chain position strictly before
3551
+ the rekey. Other causes carry neither field.
3552
+ - `policy {agentContext?, bridges?}` sets one or both, in plaintext (§16.8, §16.10). In a dm or a group it is the
3553
+ author's vote.
3554
+ - `meta {epoch, idx, sealed}`: a group's or channel's `{name?, topic?, purpose?}`, sealed (§16.5).
3555
+ - `setting {kind, epoch, idx, sealed}`: one of the person's settings in their `self` conversation, sealed;
3556
+ `kind` is `prefs`, `grant`, `context`, `localRetention` or `link` (§16.3).
3557
+
3558
+ ### 16.3 Message bodies
3559
+
3560
+ The plaintext of a `msg` event is one `MsgBody`, at most `LIMITS.convPlaintextBytes` (64 KiB) of JCS
3561
+ (`too_large`):
3562
+
3563
+ - `line`: `text` (at most `LIMITS.lineChars` (40,000) UTF-16 code units), `format` (`plain` or `md`), and
3564
+ optionally `thread` (the root's event id), `broadcast`, `mentions` (at most `LIMITS.mentions` (50)), `files`
3565
+ (at most `LIMITS.files` (10) attachments), `parts` (up to 8 A2A data parts), `answers` (the mention it answers)
3566
+ `grants` (at most `LIMITS.agentMentions` (5) delegation grants, §6.7) and `via` (§16.10).
3567
+ - `edit {target, text, mentions?}`, `delete {target}`, `react {target, key, on}` and `pin {target, on}`.
3568
+ - `task {task}`: a task as a thread. `request {to, message, delegation}` is the root, to an agent participant;
3569
+ `status` and `artifact` carry the A2A update events (§6.2) and `cancel` and `followUp` the requester's, each
3570
+ naming the root.
3571
+ - `receipt {target, stage}`: `delivered`, `handed_to_agent` or `read`.
3572
+ - `sync {sync}`: a body of the person's `self` conversation, and only there.
3573
+
3574
+ A conversation's name, topic and purpose, and its policy, are not message bodies: they are state, so they are ops
3575
+ (`meta`, `policy`), which never expire.
3576
+
3577
+ A mention is `{p}` (a participant), `{all: "channel"}` or `{all: "here"}`. An attachment names a blob, its own
3578
+ AES-256-GCM key and nonce, its SHA-256 and size (at most `LIMITS.blobBytes` (25 MiB)), a media type and a name.
3579
+
3580
+ **The self conversation.** A person's `self` conversation has one participant, the person, and history `shared`.
3581
+ It holds two kinds of state:
3582
+
3583
+ - **Passing state**, in `sync` bodies (`SyncBody`), which expire with the retention: `read {cid, upTo, thread?}`,
3584
+ `unread {cid, from}`, `follow`, `save`, `draft`, `joined` and `keys {cid, shares}` (at most `LIMITS.keyShares`
3585
+ (512) chain keys).
3586
+ - **Settings**, in `setting` ops (`SettingBody`, sealed), which never expire: `prefs {cid, notify, muteUntil?,
3587
+ receipts?}`, `grant` (a consent grant, §6.9, verified over its raw value), `context {rules}` (§16.8),
3588
+ `localRetention {cid, days}` and `link {link}` (§16.10). The op's `kind` is the body's `s`.
3589
+
3590
+ The last writer wins by `seq`.
3591
+
3592
+ ### 16.4 The hash chain and gossip
3593
+
3594
+ - `h(0) = SHA-256("messaging/1/conv-genesis\n" || JCS(cid))`.
3595
+ - `h(seq) = SHA-256(h(seq − 1) || SHA-256(JCS(event)))`, over the raw 32-byte digests.
3596
+ - A member checks `prev = h(seq − 1)` and the signature over the raw received event before it reads any field.
3597
+ - **Gossip.** A member compares an event's `seen` with its own log (`ConvLog.relate`): `ahead` past its head (it
3598
+ fetches up to `seen.seq` first), `consistent` when its `h(seen.seq)` is `seen.hash`, `diverged` otherwise. A
3599
+ divergence proves that the relay showed two members different logs; the signed event is the evidence. `seen`
3600
+ at or past the event's own `seq` is `invalid_request`.
3601
+ - **Device cuts.** A `rekey` with cause `device` ends the named device's authority in this conversation: later
3602
+ entries by that device are invalid. If `until` is present, known entries by that device after `until` and before
3603
+ the rekey are cut from state. An absent `until` makes no earlier cut. The position need not name that device's
3604
+ entry: an expired message is a digest with no author or state effect. Replay collects the cuts, verifies every
3605
+ chain link, and does not apply cut entries; a cut rekey cannot cut another device. A cut that removes genesis or
3606
+ leaves later entries unauthorized is refused. A live verifier checks that the named device has been removed in
3607
+ its verified realm. A cut never extends a removed device's authority.
3608
+ - **Retention.** Only `msg` entries expire. When the relay prunes a `msg` entry older than the conversation's
3609
+ `retainDays` it keeps the entry's `seq`, `prev` and the digest `SHA-256(JCS(event))` and serves those
3610
+ (`ConvExpiredEntry`), so the chain stays verifiable (`ConvLog.applyExpired`). **Ops never expire**: membership,
3611
+ roles, epochs, settings, policy votes, sealed meta and the `self` conversation's settings are ops, so a member
3612
+ that replays the log after pruning, or a joiner reading it from genesis, computes the same state as a member
3613
+ that kept every body. The create (seq 1) never expires (`invalid_request`), and an op head that names an
3614
+ expired entry is `conv_conflict`, so a relay that withholds an op as expired is caught by the next op. A key
3615
+ share (§16.6) is not an entry; the relay deletes it after the retention.
3616
+
3617
+ ### 16.5 Sender keys
3618
+
3619
+ - **Chains.** A sender device keeps one chain per (conversation, epoch). From a chain key `ck_i`:
3620
+ - the message key `mk_i = HKDF-SHA256(ikm = ck_i, salt = empty, info = "messaging/1/conv-msg", 32 bytes)`;
3621
+ - the next chain key `ck_{i+1} = HKDF-SHA256(ikm = ck_i, salt = empty, info = "messaging/1/conv-chain", 32
3622
+ bytes)`.
3623
+
3624
+ The first chain of an epoch starts at index 0 from 32 random bytes.
3625
+ - **Sealing a body.** `body = base64url(nonce || AES-256-GCM(mk_idx, nonce, JCS(MsgBody), aad = JCS(header)))`,
3626
+ with a fresh 12-byte nonce and the 16-byte tag last; the header is the event without `body` and `sig`. The
3627
+ event's `epoch` and `idx` name the key. A message key seals one body only.
3628
+ - **Sealed meta.** `create.sealedMeta` is `JCS({name?, topic?, purpose?})` sealed the same way under the creator
3629
+ device's epoch-0 key `mk_0`, with the create event's header as AAD. The creator's first message uses index 1.
3630
+ - **Rotation.** A sender starts a new chain in the same epoch after `LIMITS.chainMessages` (500) messages on one
3631
+ chain, or `LIMITS.chainMaxMs` (7 days) after starting it. The new chain starts from fresh random bytes at the
3632
+ next index, so `(epoch, device, idx)` names every key: the share with the greatest index at or below `idx`
3633
+ derives it.
3634
+ - **Receiving.** A receiver derives forward only and uses each message key once. To open messages that arrive out
3635
+ of order it keeps at most `LIMITS.skippedKeys` (1000) derived but unused keys per (epoch, sender device),
3636
+ dropping the lowest indexes first; a message further than that past the chain is `too_large`, and one whose key
3637
+ is used or dropped is `not_found`. It keeps each share's chain key at the share's index, to share history (§16.6).
3638
+ - **Sealed ops.** A `meta` or `setting` op seals its content (`ConvMeta`, or a `SettingBody` of the op's `kind`)
3639
+ the same way, under the author device's key at the op's (`epoch`, `idx`), with `aad = JCS(event without sig, the
3640
+ op without sealed)`, so the AAD binds the op's kind and key index too. The plaintext is at most
3641
+ `LIMITS.convPlaintextBytes`. A sealed op takes one index of the author's chain, as a message does.
3642
+ - **Epochs.** A `rekey` op starts the next epoch. Each sender then starts a new chain and shares it only with the
3643
+ devices of the participant persons at the rekey's `seq`.
3644
+ - Authenticity comes from the event signature, not from the key: a member who holds a sender key still cannot
3645
+ write as another member. Within an epoch there is no post-compromise security: a copy of a device's state reads
3646
+ every chain it held until the next rotation or epoch.
3647
+
3648
+ ### 16.6 Key shares
3649
+
3650
+ A `keys` event goes to **one recipient device**. Its body is `{to: {person, device}, epk, nonce, ct}`; a sender
3651
+ sends one event per recipient device. `ct` seals `JCS({shares: [KeyShare]})`, where a `KeyShare` is `{epoch,
3652
+ device, idx, ck}` (at most `LIMITS.keyShares` (512)):
3653
+
3654
+ 1. Generate an ephemeral X25519 key pair; `epk` is its raw public key, `nonce` 12 random bytes.
3655
+ 2. `shared = X25519(eph_priv, recipient.enc)`, the recipient device's key as the realm verified it (§4, §15.7).
3656
+ 3. `key = HKDF-SHA256(ikm = shared, salt = epk || recipient.enc, info = "messaging/1/seal", 32 bytes)`, as for
3657
+ an envelope (§5, "Sealing").
3658
+ 4. `ct = AES-256-GCM(key, nonce, JCS({shares}), aad = JCS({header, to}))`, with the tag last.
3659
+
3660
+ The AAD binds a share to its event and its recipient: moving it to another event or device makes it fail. A
3661
+ receiver keeps the first share for a given (epoch, device, idx); a later one with another key changes nothing.
3662
+
3663
+ **Not an entry.** A `keys` event is not in the hash chain (`ConvLog.apply` refuses one, `invalid_request`). The
3664
+ relay keeps it for its recipient and serves it to no one else (`ConvShareEntry {n, event}`, §7): each device
3665
+ fetches only its own shares, so a rekey costs a device one share per sender, not every share of every sender. A
3666
+ recipient checks a share with `ConvLog.checkShare`, in the state at the share's `opHead`:
3667
+
3668
+ 1. I-JSON, size and the schema; the realm and the `cid` (`invalid_request`).
3669
+ 2. The signature over the raw event (`bad_signature`).
3670
+ 3. Live: `ts` within `LIMITS.clockSkewMs` (`expired`), and `by.device` a current device of `by.person`
3671
+ (`unknown_device`).
3672
+ 4. `by.agent` absent (`forbidden`); `opHead` names an op of the log (`conv_conflict`).
3673
+ 5. At `opHead`, the author's person is a member participant person (`not_member`) and `to.person` a participant
3674
+ person (`forbidden`); live, `to.device` a current device of `to.person` (`unknown_device`).
3675
+
3676
+ The relay also requires `opHead` to be the current op head (§7), so no share reaches a device whose person was
3677
+ removed since. A relay that withholds a share is not hidden: the recipient cannot open that sender's messages,
3678
+ and shows that it waits for keys.
3679
+
3680
+ ### 16.7 Authority
3681
+
3682
+ `ConvLog.apply` checks each entry, in order, against the state after every earlier entry, and a failed check
3683
+ changes nothing:
3684
+
3685
+ 1. I-JSON (§2, `invalid_request`) and size (`too_large`); the schema, the realm and the `cid` (`invalid_request`).
3686
+ 2. The chain: `seq = head + 1` and `prev = h(head)` (`conv_conflict`).
3687
+ 3. The signature over the raw event (`bad_signature`).
3688
+ 4. When appending live (the relay, or a member checking a pushed event): `ts` within `LIMITS.clockSkewMs` of now
3689
+ (`expired`), and `by.device` a current device of `by.person` in the realm (`unknown_device`).
3690
+ 5. `id` not already in the log (`duplicate`); `seen` before the event (`invalid_request`).
3691
+ 6. The first entry is `create`, and there is only one (`conv_conflict`).
3692
+ 7. For an `op`: `opHead` is the current op (`conv_conflict`); `by.agent` is absent: an agent never writes an op
3693
+ (`forbidden`). For a `msg`: `opHead` names an op of this log, or `{0, h(0)}` (`conv_conflict`). A `keys` event
3694
+ is never an entry (`invalid_request`, §16.6).
3695
+ 8. The rules below. An author who is not a participant is `not_member`, one who lacks the authority `forbidden`, a
3696
+ target that is not a message of this log `not_found`, and a target in the wrong state `invalid_state`.
3697
+
3698
+ A participant **person** is the person of any participant, so the devices of a person whose agent sits in a
3699
+ conversation append its keys and rekeys. "Admin" below means `owner` or `admin`; an author's role is its own
3700
+ participant's.
3701
+
3702
+ | Event | Allowed when |
3703
+ |---|---|
3704
+ | `create` | A **dm**: exactly two seats, both `member`, no visibility, org, permits or meta, history `none`, `cid` the derived one, and the author is the person of one seat. A **self**: one seat, the author as `owner`, `cid` the author's, history `shared`. A **group**: 3 to `LIMITS.groupParticipants` (50) distinct seats, no visibility or org. A **channel**: a visibility, and an org if public, at most `LIMITS.convParticipants` (1000) seats. In a group or channel the author is a seat as `owner`. |
3705
+ | `add` | A group or channel (never a dm or self). The author is a member, not a guest, and an admin when `invite` is `admins`. No seat is a current participant; only an owner adds an owner or an admin; the kind's limit holds (`too_large`). |
3706
+ | `accept` | The author's own participant is `invited`. |
3707
+ | `join` | A public channel the author is not in; live, the author's person is in the channel's org. |
3708
+ | `leave` | A group or channel; the author is a participant. Their agents leave with them. The last owner cannot leave while another person remains. |
3709
+ | `remove` | A group or channel. The author is an admin, and an admin never removes an owner; or the target is the author's own agent (whether or not the author is a participant). Removing a person removes their agents. The last owner cannot be removed. |
3710
+ | `role` | A group or channel. The author is an owner, or an admin setting `member` or `guest` on a participant who is not an owner. The last owner cannot be demoted. |
3711
+ | `seat` | The target is an agent participant whose person is the author. It becomes `member`. |
3712
+ | `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`). |
3713
+ | `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. |
3714
+ | `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. |
3715
+ | `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`). |
3716
+ | `setting` | Only in a `self` conversation; the author is its person. Its `epoch` is the current epoch and no rekey is due (`stale_epoch`). |
3717
+ | any `msg` | The author's participant is a member; `epoch` is the current epoch and no rekey is due (`stale_epoch`). |
3718
+ | `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. |
3719
+ | `edit` | Not archived; the target is a line, not deleted, by the same participant (person and agent). |
3720
+ | `delete` | The target is a line or a task event, not deleted, and the author wrote it or is an admin. |
3721
+ | `react` | As `line`; the target is not deleted; at most `LIMITS.reactionsPerEvent` (50) keys per target (`too_large`). |
3722
+ | `pin` | Not archived; an admin when `pins` is `admins`; the target is not deleted; at most `LIMITS.pins` (100) pinned (`too_large`). |
3723
+ | `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`). |
3724
+ | `receipt` | The target is a message of the log. |
3725
+ | `sync` | Only in a `self` conversation, where no other body goes (`invalid_request`). |
3726
+ | `keys` | Not an entry: §16.6 (`ConvLog.checkShare`). |
3727
+
3728
+ **State.** A participant starts as `member`, except an agent whose person is not the author (`invited` until its
3729
+ person seats it) and a `guest` (`invited` until they accept). Live, adding a person of another org as anything but a
3730
+ guest is `forbidden`. A rekey is **due** after a removal or a leave (cause `removed`), an add to a group (`added`),
3731
+ and a change of the history policy (`settings`); a channel does not rekey on add. While a rekey is due, every `msg`
3732
+ is `stale_epoch`: no message is sealed under a key a removed device holds. A `rekey` clears every due cause.
3733
+
3734
+ `ConvLog` keeps the state after every entry: `at(seq)` is the state of a log holding only entries 1 to `seq`. Only
3735
+ ops change it, so the relay, which cannot open bodies, and a member whose message bodies expired compute the same
3736
+ state. The rules that read a message body (edit, delete, reactions, pins, tasks) run only when the caller passes
3737
+ the opened body.
3738
+
3739
+ ### 16.8 Agents and history as context
3740
+
3741
+ A person decides which of their agents may mine which of their conversations as context, and a conversation's
3742
+ policy decides whether members' agents may mine it at all.
3743
+
3744
+ - **Policy** (`AgentContext`): `members` (members' agents may, each under its person's rules), `seats` (only
3745
+ agents seated in it, by their seats) or `none`. The default is `members`. In a channel an admin sets it with a
3746
+ `policy` body; in a dm or group each participant person votes, and the strictest vote counts (`none` <
3747
+ `seats` < `members`). A person's vote goes when they leave. The policy is a `policy` op, in plaintext, so a joiner
3748
+ that holds no earlier keys knows it too.
3749
+ - **Rules** (`ContextRule`): `{agent, scope, access, lookbackDays?}`, where `agent` is a session name or `*`,
3750
+ `scope` is `{cid}` or `{kind}` with `kind` one of `own`, `dm`, `group`, `channel.private` and
3751
+ `channel.public`, and `access` is `allow`, `ask` or `off`. A person holds at most `LIMITS.contextRules` (500),
3752
+ one per (agent, scope), synced in their `self` conversation. A conversation is `own` when its participant persons
3753
+ are the person alone.
3754
+ - **Resolution** (`resolveContext`) for agent A of person P reading conversation C as context:
3755
+ 1. a `self` conversation is never mined;
3756
+ 2. C's policy `seats` or `none` forbids the read, whatever P's rules say;
3757
+ 3. else the most specific rule of P applies (a `cid` scope over a `kind` scope, then A by name over `*`), or
3758
+ with none, the default for C's scope:
3759
+
3760
+ | Scope | Access | Lookback |
3761
+ |---|---|---|
3762
+ | `own` | `allow` | the retention |
3763
+ | `channel.public` | `allow` | 90 days |
3764
+ | `channel.private` | `ask` | 30 days |
3765
+ | `group` | `ask` | 30 days |
3766
+ | `dm` | `ask` | 14 days |
3767
+
3768
+ `off` forbids, `ask` holds the read for P to answer, `allow` serves it;
3769
+ 4. the lookback is the smallest of the rule's (or default's) lookback, C's `retainDays` and P's local
3770
+ retention for C.
3771
+ - An agent seated in C reads C from its seat by its seat, not by P's rules, unless C's policy is `none`; then it
3772
+ reads only what is addressed to it and the threads that is in (`seatReads`).
3773
+ - Agents of every client follow these rules, MCP clients included; each daemon enforces them for its own person's
3774
+ agents.
3775
+
3776
+ ### 16.9 Limits
3777
+
3778
+ | Limit | Value |
3779
+ |---|---|
3780
+ | `LIMITS.convParticipants` | 1000 participants (persons and agents) in a channel |
3781
+ | `LIMITS.groupParticipants` | 50 in a group |
3782
+ | `LIMITS.convOpParticipants` | 100 per `add` or `remove` |
3783
+ | `LIMITS.convEventBytes` | 256 KiB per event |
3784
+ | `LIMITS.convPlaintextBytes` | 64 KiB per message body |
3785
+ | `LIMITS.lineChars` | 40,000 UTF-16 code units |
3786
+ | `LIMITS.mentions`, `LIMITS.files`, `LIMITS.agentMentions` | 50, 10 and 5 per line |
3787
+ | `LIMITS.blobBytes` | 25 MiB per file |
3788
+ | `LIMITS.reactionsPerEvent`, `LIMITS.pins` | 50 keys per target, 100 pins per conversation |
3789
+ | `LIMITS.retainDaysDefault`, `LIMITS.retainDaysMax` | 90 days, 365 days |
3790
+ | `LIMITS.skippedKeys`, `LIMITS.chainMessages`, `LIMITS.chainMaxMs` | 1000, 500, 7 days |
3791
+ | `LIMITS.rekeyScheduleMs` | 30 days |
3792
+ | `LIMITS.keyShares` | 512 chain keys per share |
3793
+ | `LIMITS.wakeHints`, `LIMITS.contextRules` | 5, 500 |
3794
+
3795
+ ### 16.10 Linked channels (reserved)
3796
+
3797
+ A person may link an outside chat account (Telegram, then Slack) as an alias of themselves, to reach their own
3798
+ agents from a phone. The fields below are part of the schema and are validated, but no daemon acts on them yet;
3799
+ `docs/design/conversations.md` §17 describes the plan.
3800
+
3801
+ - **`via {channel}`** on a `line`: the person wrote it from a linked channel, `telegram` or `slack`. It is sealed and
3802
+ signed with the event, so every member sees it. Only a person writes it (`forbidden` on an agent's line, §16.7).
3803
+ The person's own devices also check it against the person's links (`checkVia`): a link of that channel, hosted by
3804
+ the authoring device, in force. Other members cannot see the links and do not check that.
3805
+ - **`link`**, a setting of the person's `self` conversation (a sealed `setting` op of kind `link`, which never
3806
+ expires): `{id, channel, account, host, proof, authority, linkedAt, revokedAt?}`.
3807
+ - `account` is the channel's id for the person's account; it is never in the device log, the directory or
3808
+ presence.
3809
+ - `host` is the device that runs the bridge.
3810
+ - `proof` is `{method: "code-sas", codeDigest, sas}`: the digest of the one-time code and the six-digit code the
3811
+ person matched on both sides.
3812
+ - `authority` is a non-empty subset of `chat` and `view` (§8.11), and never more.
3813
+ - For each `id` the record written last is the one in force, unless it has `revokedAt` (`currentLinks`).
3814
+ - **`bridges`** in the `policy` op: who may mirror the conversation to a linked channel: `none` (the default), `admins` or
3815
+ `members`. In a channel an admin sets it. In a dm or group the strictest vote of the participant persons counts,
3816
+ and a person who has not voted counts as `none`, so bridging needs every participant person's vote.
3817
+ - **Reader ids** (`ReaderId`): a person, or `ext:<channel>:<link id>` for a link's channel provider. They are not on
3818
+ the wire; the audience guard's reader sets use them.
3819
+ - A foreign sender on a bridged channel (`via.sender`) is not part of v1: the schema refuses it.
3820
+
3821
+ ### 16.11 Same-realm contact request cut-over
3822
+
3823
+ This profile replaces the first-message import rules of §6.9 for conversation DMs. It does not introduce cross-realm addresses.
3824
+ The normative shapes are in `schema/contact-request.ts`. The request plaintext uses `RequestImport`, separate from the ordinary `MsgBody` parser.
3825
+ The cut-over engine uses `openRequestImport` and `applyRequestEntry` for this profile. Source attribution comes from verified request evidence, never the wrapper author.
3826
+
3827
+ **Portable source proof.** A new request contains a sender-signed descriptor and a sealed opening:
3828
+
3829
+ - The descriptor binds `v`, `realm`, `cid`, `from`, `to`, `id`, `issuedAt`, `expiresAt`, `commitment` and `by`.
3830
+ - The sender signs its raw JCS bytes without `sig`, under `DOMAIN.request` (`messaging/1/contact-request\n`).
3831
+ - The opening contains a fresh random 256-bit `salt` and 1–500 UTF-16 code units of `text`.
3832
+ - The commitment is SHA-256 of `DOMAIN.requestContent` and JCS of the opening.
3833
+ - The descriptor digest is SHA-256 of `DOMAIN.requestDigest` and JCS of the signed descriptor.
3834
+ - Both device signatures require bindings from retained, verified identity history. An absent binding fails closed.
3835
+
3836
+ The request names the deterministic DM of two distinct persons. The consent window cannot exceed `LIMITS.ttlMs`.
3837
+ Every device copy carries the same descriptor and opening. Only sealed envelopes carry the opening before acceptance.
3838
+ Requests expose no agent address, task, mention, attachment, context-index entry or wake hint.
3839
+ The envelope payload has `conv: {kind: "request", proof: PortableRequest}`. Its one text part and message ID match the signed opening and request ID.
3840
+ 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.
3841
+ The optional field in the stored payload schema permits reading retained old records. It does not authorize old plaintext for a conversation import.
3842
+
3843
+ **Terminal decision.** The recipient signs `RequestDecision` under `DOMAIN.requestDecision` (`messaging/1/contact-request-decision\n`).
3844
+ The decision binds the descriptor digest, request ID, realm, DM, both persons, answer and decision time.
3845
+ The answer is `accept` or `decline`. A local block sends `decline` and keeps the blocking grant private.
3846
+ The decision time must be within the original consent window. Retries preserve the signed decision and its timestamp.
3847
+
3848
+ `request.settle {req, descriptor, decision}` stores the first valid terminal decision in the recipient mailbox.
3849
+ Only the connected recipient device can submit its own signed decision. A later conflicting decision returns the first decision.
3850
+ `request.state {req, descriptor}` returns that decision to either named person, or no decision before settlement.
3851
+ Both frames answer `request.settled {req, id, decision?}`. They expose neither salt nor text to the relay.
3852
+ Relay schema 3 adds `conv_request_decisions`. Generation stays 2 and the minimum reader stays 1.
3853
+
3854
+ **Permanent activation.** An external human DM declares `create.admission: "request"`.
3855
+ The relay refuses an external DM without this declaration. Same-org DMs retain implicit org admission.
3856
+ Before activation, content and key shares fail with `request_required`. The recipient appends `activate {proof: {descriptor, decision}}` after mailbox reconciliation.
3857
+ Device-removal rekeys can precede activation. They still require the normal removed-device and log-authority checks, and grant no content access.
3858
+ The activation is a permanent op. It reserves the request ID for exactly one import.
3859
+
3860
+ The recipient seals `RequestImport {t: "request.import", descriptor, decision, opening}` under its conversation sender key.
3861
+ The wrapper has `id` and `request` equal to the reserved request ID. It has no agent author or wake hint.
3862
+ Members verify both proofs and the opening before they attribute text to the original sender.
3863
+ The recipient then appends `request.complete {request, seq, digest}`. The digest is SHA-256 of JCS of the exact imported event.
3864
+ This permanent completion survives deletion and retention. Ordinary messages wait until all accepted reservations have completed imports.
3865
+
3866
+ Duplicate importers fetch the winning event and verify its exact proofs and opening. A different body at the same ID is a conflict.
3867
+ The source sender need not be online. Crossed requests use one DM but keep separate IDs, answers and imports.
3868
+ The import is immutable. Only its original sender can delete its text. The wrapper author gains no source-author permissions.
3869
+ After retention, members verify the permanent activation and completion without reconstructing text.
3870
+
3871
+ **Existing contacts.** Each person signs a conversation-bound `ContactAuthorization` under `DOMAIN.contactAuthorization`.
3872
+ An activation with `proof.contacts` requires both reciprocal signatures. The daemon signs only under an existing open or muted chat grant.
3873
+ This path needs no new human prompt and imports no request text. A contact card cannot substitute for either signature.
3874
+
3875
+ **Durable local lifecycle.** `ConversationRequests` stores each request and its state in namespaced rows of the existing `kv` table.
3876
+ Queued sends, submitted requests, decisions and imports survive restart. `waitingOnThem()` exposes only explicit submitted requests, not missing receipts.
3877
+ Acceptance opens only chat. A later block prevents local import and key use even after permanent activation.
3878
+ The sender receives the same error for an unknown or undiscoverable exact email.
3879
+
3880
+ Old pending requests retain their local records and expose `resend`. The sender must send a new request with portable evidence.
3881
+ The cut-over never signs old plaintext for its sender or silently resends it.