@joe-broadhead/messaging 0.7.1 → 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 (89) hide show
  1. package/CHANGELOG.md +214 -2
  2. package/dist/bin/messaging-mcp.js +5043 -4054
  3. package/dist/bin/messaging-relay.js +8263 -5425
  4. package/dist/bin/messaging.js +5982 -4906
  5. package/dist/bin/messagingd.js +19956 -13716
  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 +23 -139
  9. package/dist/lib/modules/core/consent.js +1 -1
  10. package/dist/lib/modules/core/conversation/event.js +29 -4
  11. package/dist/lib/modules/core/conversation/guest-invitation.js +42 -0
  12. package/dist/lib/modules/core/conversation/log.js +287 -65
  13. package/dist/lib/modules/core/conversation/request-log.js +158 -0
  14. package/dist/lib/modules/core/conversation/request-seal.js +41 -0
  15. package/dist/lib/modules/core/conversation/requests.js +87 -0
  16. package/dist/lib/modules/core/conversation/seal.js +90 -37
  17. package/dist/lib/modules/core/conversation-agent-tools.js +69 -0
  18. package/dist/lib/modules/core/federation/directory-proof.js +83 -0
  19. package/dist/lib/modules/core/federation/proof-state.js +167 -0
  20. package/dist/lib/modules/core/federation/roster-proof.js +74 -0
  21. package/dist/lib/modules/core/index.js +8 -3
  22. package/dist/lib/modules/core/jelly-host.js +5 -5
  23. package/dist/lib/modules/core/schema/common.js +31 -5
  24. package/dist/lib/modules/core/schema/consent.js +5 -4
  25. package/dist/lib/modules/core/schema/contact-request.js +22 -0
  26. package/dist/lib/modules/core/schema/conv-frames.js +94 -0
  27. package/dist/lib/modules/core/schema/conversation.js +67 -33
  28. package/dist/lib/modules/core/schema/envelope.js +8 -24
  29. package/dist/lib/modules/core/schema/errors.js +4 -3
  30. package/dist/lib/modules/core/schema/guest-invitation.js +13 -0
  31. package/dist/lib/modules/core/schema/index.js +5 -0
  32. package/dist/lib/modules/core/schema/ipc/consent.js +13 -9
  33. package/dist/lib/modules/core/schema/ipc/conversation-views.js +80 -0
  34. package/dist/lib/modules/core/schema/ipc/conversations.js +128 -0
  35. package/dist/lib/modules/core/schema/ipc/device.js +2 -0
  36. package/dist/lib/modules/core/schema/ipc/jelly.js +3 -38
  37. package/dist/lib/modules/core/schema/ipc/shapes.js +14 -72
  38. package/dist/lib/modules/core/schema/ipc/surfaces.js +12 -195
  39. package/dist/lib/modules/core/schema/ipc/tasks.js +31 -0
  40. package/dist/lib/modules/core/schema/ipc.js +18 -44
  41. package/dist/lib/modules/core/testing.js +1 -1
  42. package/dist/lib/modules/core/version.js +1 -1
  43. package/dist/lib/modules/ipc/client.js +10 -2
  44. package/dist/lib/modules/ipc/server.js +14 -5
  45. package/dist/lib/modules/ipc/testing.js +13 -23
  46. package/dist/spec/messaging-v1.md +546 -409
  47. package/dist/third-party-notices.md +1 -1
  48. package/dist/third-party.json +1 -1
  49. package/dist/types/core/agent/operations.d.ts +5 -52
  50. package/dist/types/core/agent/present.d.ts +6 -21
  51. package/dist/types/core/agent-tools.d.ts +263 -442
  52. package/dist/types/core/conversation/event.d.ts +6 -6
  53. package/dist/types/core/conversation/guest-invitation.d.ts +8 -0
  54. package/dist/types/core/conversation/log.d.ts +28 -13
  55. package/dist/types/core/conversation/request-log.d.ts +24 -0
  56. package/dist/types/core/conversation/request-seal.d.ts +6 -0
  57. package/dist/types/core/conversation/requests.d.ts +14 -0
  58. package/dist/types/core/conversation/seal.d.ts +32 -9
  59. package/dist/types/core/conversation-agent-tools.d.ts +918 -0
  60. package/dist/types/core/federation/directory-proof.d.ts +8 -0
  61. package/dist/types/core/federation/proof-state.d.ts +114 -0
  62. package/dist/types/core/federation/roster-proof.d.ts +7 -0
  63. package/dist/types/core/index.d.ts +10 -5
  64. package/dist/types/core/jelly-host.d.ts +1 -1
  65. package/dist/types/core/schema/common.d.ts +31 -6
  66. package/dist/types/core/schema/consent.d.ts +7 -7
  67. package/dist/types/core/schema/contact-request.d.ts +264 -0
  68. package/dist/types/core/schema/conv-frames.d.ts +1200 -0
  69. package/dist/types/core/schema/conversation.d.ts +1410 -264
  70. package/dist/types/core/schema/envelope.d.ts +83 -130
  71. package/dist/types/core/schema/errors.d.ts +3 -1
  72. package/dist/types/core/schema/guest-invitation.d.ts +51 -0
  73. package/dist/types/core/schema/index.d.ts +5 -0
  74. package/dist/types/core/schema/ipc/consent.d.ts +89 -49
  75. package/dist/types/core/schema/ipc/conversation-views.d.ts +1023 -0
  76. package/dist/types/core/schema/ipc/conversations.d.ts +3987 -0
  77. package/dist/types/core/schema/ipc/device.d.ts +4 -0
  78. package/dist/types/core/schema/ipc/jelly.d.ts +2 -105
  79. package/dist/types/core/schema/ipc/shapes.d.ts +13 -95
  80. package/dist/types/core/schema/ipc/surfaces.d.ts +25 -427
  81. package/dist/types/core/schema/ipc/tasks.d.ts +208 -0
  82. package/dist/types/core/schema/ipc.d.ts +2671 -525
  83. package/dist/types/core/schema/relay.d.ts +8 -4
  84. package/dist/types/core/testing.d.ts +1 -1
  85. package/dist/types/core/version.d.ts +1 -1
  86. package/dist/types/ipc/client.d.ts +4 -1
  87. package/dist/types/ipc/server.d.ts +10 -1
  88. package/dist/types/ipc/testing.d.ts +21 -8
  89. package/package.json +1 -1
@@ -1,5 +1,68 @@
1
1
  # Messaging protocol v1 (`messaging/1`)
2
2
 
3
+ ## Conversation IPC cut-over (2026-11-10)
4
+
5
+ The live local API uses `CONVERSATION_METHODS` and `CONVERSATION_TIERS` from the normative core schema.
6
+ The old `msg.send`, `person.send`, `channel.*`, `search`, `conv.subscribe`, `agents.list` and `agents.read`
7
+ methods are removed, without aliases. `conv.list`, `conv.read` and batch `people.get`
8
+ use the conversation schemas. Agent access follows design §8.5: `conv.get`, `conv.members` and `file.get`
9
+ are human-only contracts. `file.get` remains staged. `sessions.list` is human-only and returns local sessions and their conversation seats.
10
+ `request.waiting` labels an authenticated outgoing hold `held`, and missing-receipt evidence
11
+ `pending-delivery`; missing receipts do not establish consent state. Contact requests remain visible while delivery is unconfirmed.
12
+ Only a verified recipient receipt changes a contact request to `held`. Internal change notifications carry
13
+ identifiers only; `conv.sync` re-reads verified state and selects safe rendered fields. Storage tables remain.
14
+ A line first readable after its key arrives streams as an `event`; `event.update` carries only edits, deletions, reactions, pins and delivery.
15
+
16
+ The daemon binds every agent call to a session registered on that connection. It rechecks membership and
17
+ session ownership after asynchronous rendering. Agents edit/delete only their own lines. Cursors bind to
18
+ the query and session; a daemon restart or engine replacement returns a stream gap. Replay renders current
19
+ verified state, including edits and deletions, rather than caching historical plaintext. Projection refuses
20
+ prefixes above 10,000 entries and responses above 512 KiB. These are interim safety bounds pending load tests.
21
+ Only `LIVE_CONVERSATION_METHODS` is served. Later-lane contracts stay staged and have no socket handler.
22
+ `contacts.list` returns verified conversation metadata: `cid`, `kind`, optional `name`, and optional `dmWith`.
23
+ Only accepted conversations appear. Contacts carry no transcript, read position, or unread count.
24
+ Channel contacts use the canonical `cid` as their address. Agent clients filter them through `conv.access`.
25
+ Shared core operations retain contacts, inbox reads, and tasks. Conversation clients use `conv.*` for sends, pages, and search.
26
+ Lane 4 omits read positions, preferences, unread counts and thread-follow state from views until lane 6 implements them.
27
+ Filters on that state (`conv.list` `unread` and `mentions`, `conv.threads` `unread` and `following`) are refused with `invalid_request`, never answered with an empty page.
28
+ `conv.backfill` verifies available relay history and returns `complete: false`. Completion requires an indexing coverage boundary that accounts for missing keys and expired entries.
29
+ `clientId` deduplication commits atomically with a queued line and survives engine replacement; reuse with
30
+ different content or a different destination is refused. Keyless conversation calls on a Jelly admin socket
31
+ require an action grant, even when the same method is available to a bound agent session.
32
+
33
+ Conversation maintenance coalesces verified membership changes for a random 0–30 second delay before answering
34
+ shared channel history. Only accepted participants qualify. Successful responses are remembered per membership,
35
+ epoch and recipient device set; failed or keyless responses remain retryable. The daemon sweep retries the
36
+ conversation outbox, schedules due epoch rekeys, and expires local message ciphertext/plaintext and search rows
37
+ while preserving digest links and control operations. Shutdown cancels response timers and waits for active work.
38
+
39
+ Tasks and delegation retain their envelopes and lifecycle until lane 7. Agent-tier `task.create` replaces
40
+ the task-creation arm of removed `msg.send`: it takes `key`, an agent `to`, `parts`, optional A2A fields,
41
+ `wake`, `idempotencyKey`, and `onBehalfOf`. It assigns the task ID and signs the existing delegation chain.
42
+ Agent-tier `task.continue` takes the same message fields and a required `taskId`, without `onBehalfOf`.
43
+ It sends the retained task-follow-up envelope only to that session's existing task executor, preserving
44
+ context, request binding, receipts, and terminal-state checks. Both return `{results: SendResult[]}`.
45
+ `task.list/get/update/cancel` are unchanged. Plain mail uses `conv.send`, never a task envelope.
46
+ The daemon rejects plain agent-mail envelopes, including previously held mail released after cut-over;
47
+ it retains the database rows but does not expose them through inbox or item reads.
48
+
49
+ The inbox combines retained task requests/follow-ups with verified incoming lines in accepted two-agent DMs
50
+ addressed to this local session. Plain legacy mail rows are not returned. For a line, `id`, `messageId` and
51
+ `eventId` equal the event ID, and `cid` names the conversation. `from.address` is the stable participant ID;
52
+ its device is the verified signing device, and its label is absent, never synthesized. Task items retain their device provenance.
53
+ `inbox.get/ack` and `item.read` accept an optional `cid`; an ambiguous ID requires it. The full `message`
54
+ projection contains the line text, while `cid/eventId` lets the client read the safe event and file references.
55
+ Edits are rendered on every read; deleted, inaccessible and unaccepted lines are not returned. `receivedAt`
56
+ is the first local observation; `bytes` counts UTF-8 text. Stages are durable, monotonic local acknowledgments.
57
+ Acknowledgments belong to the stable recipient participant and `(cid, eventId)`, not a process session key.
58
+ An agent restart with a new session key preserves these stages and does not repeat wake hints for acknowledged lines.
59
+ They do not manufacture envelope receipts. Recipient `ask` governs agent-DM admission: `open` accepts, `request` holds, and `no` silently declines.
60
+ After admission, lines in the accepted agent DM follow the answer path. A later person block suppresses them.
61
+ Person blocks also hide that person's events from conversation reads, search, previews and replay.
62
+ The verified log remains intact. Hidden lines do not contribute reply summaries or reactions to visible lines.
63
+ The push tier governs wake hints. New eligible lines use the existing session event and wake bridge; acknowledgments do
64
+ not re-notify. Agent reads carry the same audience-label effect as conversation reads.
65
+
3
66
  Status: v1. The Zod schemas in `packages/core/src/schema/` are the
4
67
  normative wire contract; this document gives the semantics. If the two disagree, fix one of them in
5
68
  the same change, and add a test vector.
@@ -56,7 +119,10 @@ Not in v1: hole-punched peer-to-peer transport, federation between relays, and p
56
119
  `messaging/1/person\n`, `messaging/1/dir-read\n`, `messaging/1/device-log\n`, `messaging/1/device-keys\n`,
57
120
  `messaging/1/device-link\n`, `messaging/1/epoch\n` and `messaging/1/dir-key\n`, and for conversations (§16) the
58
121
  event signature prefix `messaging/1/conv-event\n` and the hash prefixes `messaging/1/conv-genesis\n`,
59
- `messaging/1/dm\n` and `messaging/1/self\n`). A signature or hash for one purpose never verifies for another.
122
+ `messaging/1/dm\n` and `messaging/1/self\n`, and for contact requests and guest invitations
123
+ `messaging/1/contact-request\n`, `messaging/1/contact-request-content\n`, `messaging/1/contact-request-digest\n`,
124
+ `messaging/1/contact-request-decision\n`, `messaging/1/contact-authorization\n` and `messaging/1/guest-invitation\n`).
125
+ A signature or hash for one purpose never verifies for another.
60
126
  - **Hash:** SHA-256, base64url.
61
127
 
62
128
  ## 3. Identity
@@ -475,7 +541,8 @@ Only the executor changes state (see `canTransition` in core).
475
541
  still run, and the person is away from that device. The host says so with `person.attached`; a Jelly host's device is
476
542
  away until its first `person.attached {attached: true}`, and no other device ever says `away`. A receiver shows that
477
543
  device as away (`people.get`), and an `away` older than the online window (§15.9: 150 s) means nothing.
478
- - `features` advertises optional capabilities. v1 defines `"tasks"`. A receiver ignores a feature it does not know.
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.
479
546
  - `label` must equal the sender's roster label. Receivers ignore a presence whose label differs, and
480
547
  always show the roster's label.
481
548
  - Like every envelope, presence carries the sender's `anchor` in its header (§5). Presence is
@@ -541,7 +608,7 @@ forwards a credential it was handed.
541
608
  - `exp`, the earliest along the chain.
542
609
 
543
610
  The requester's record holds the chain it sent.
544
- - **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
545
612
  the sending daemon signs, and clients never see or build grants.
546
613
  - Without `onBehalfOf`, a task starts a new chain whose root is the sending session.
547
614
  - A root grant made over IPC is unrestricted: no `scope`, and `maxDepth = LIMITS.delegationDepth`.
@@ -575,107 +642,26 @@ forwards a credential it was handed.
575
642
  code: fenced in a model context, `terminalSafe` in a terminal, `cleanPeerText` in a permission
576
643
  prompt. A client shows a chain only when it adds something beyond `from`: depth > 1 or a scope.
577
644
 
578
- ### 6.8 Chats: DMs and channels
579
-
580
- A chat is a direct human message (room `dm:<person>`) or a channel (room `channel:<name>`). A
581
- channel has an id (a ULID chosen by its creator), a name (`ChannelName`), its creator, and a list of
582
- persons (`PersonId`s: roster person names, or hosted `p_…` ids in a directory realm), at most
583
- `LIMITS.channelPersons` including the creator. There is no group key and no chat
584
- between sessions: agent mail stays in the inbox and tasks.
585
-
586
- - **Wire.** A chat envelope is a sealed `message` with `kind: "message"`, addressed to a machine
587
- with no agent (`to: {person, device}`) (§5, "Meaning"). A DM line carries no `conv`; a channel change
588
- or post carries `conv: {kind: "channel", op, id, name, …}`. `message.messageId` is a ULID, the same on
589
- every copy. A reply in a thread sets `message.contextId` to the `messageId` of the thread's first
590
- line. A chat envelope is never `kind: "task"`, never has a `taskId`, and a
591
- `message` to an agent never has a `conv` (`invalid_request`). `from.agent` is the session that
592
- posted the line; a person's own line has no `from.agent` (§5, "Ends"). `name` on a channel op is
593
- always the channel's own name.
594
- - A DM line: one copy to each machine of the person, and none to the sender's own machines.
595
- - A DM to oneself (`dm:<own person>`, the person's own conversation, "me"): one copy to each of the
596
- person's other machines. A line one of the person's agents wrote there is for the person, so every
597
- machine files it as incoming, unread; a line the person wrote is outgoing. Its lines carry no
598
- delivery `state`: there is no other person to deliver them to.
599
- - `create {id, name, persons, creator}`: the full record, one copy to each member machine but the
600
- sender's. `persons` includes the sender; `creator` is the person who created the channel, so an
601
- invitee learns it from the inviter's `create`.
602
- - `invite {id, name, persons}`: the persons added, to the machines of the existing members. Each
603
- machine of an added person gets a `create` with the full record instead. Persons with no
604
- machine left on the roster are not in that record.
605
- - `leave {id, name}`: the sender left, to every member machine.
606
- - `remove {id, name, persons}`: the persons removed, to every member machine, the removed
607
- persons' included.
608
- - `post {id, name}`: a line, to every member machine.
609
- - The text of a channel op is not filed. Each machine files its own notice line for an op it
610
- applies, written by the daemon from the op's fields, with the op's `messageId` (§8.2 `notice`).
611
- - **Receiver rules.** Channels are looked up by `id`, never by name, and an op's `name` must match
612
- the channel's own name (`not_found`). Where a rule fails, nothing is stored and the sender gets a
613
- `rejected` receipt.
614
- - `create` is accepted only for an unknown `id`, only when the sender and this machine's person are
615
- both in `persons` (`forbidden`), and not for an id this machine
616
- left or was removed from within the last `LIMITS.ttlMs + LIMITS.clockSkewMs` (`forbidden`). A
617
- machine holds at most a policy number of channels (the reference daemon: 200); a `create`
618
- beyond it is `rate_limited`. If this machine already holds a channel with that name, it keeps
619
- the new one under a local name: the name cut to 28 characters, a hyphen and the lowest free
620
- number from 2 (`eng-2`), or `rate_limited` when 2 to 99 are all taken. Room references
621
- (`channel:<name>`), `conv.*`, `search`, `person.send` and `channel.*` use the local name; envelopes always carry the
622
- channel's own name. This also covers two people creating the same name at once.
623
- - A `create` or `invite` naming a person this machine cannot admit is `not_found`: in a roster realm a
624
- person with no current machine; in a directory realm any id that is not a hosted person. A directory
625
- home takes persons it has not verified yet, then looks them up and watches them (§15.9).
626
- - `invite` is accepted only from a current member, and only when the channel stays within the
627
- limit (`forbidden`, `invalid_request`).
628
- - `leave` removes only the sender. When this machine's person leaves (from another of its
629
- machines), the channel and its lines are deleted here.
630
- - `remove` is accepted only from a member who is the channel's creator or a team admin on the
631
- verified roster (`forbidden`), never naming the sender (`invalid_request`: use `leave`), and only
632
- for current members (`not_found`). A machine whose person is removed deletes the channel and its
633
- lines, as on `leave`, and refuses a `create` for that id as above. Other machines remove those
634
- persons from the channel.
635
- - `post` is filed only when the sender and this machine's person are both members (`forbidden`).
636
- - Every name in `persons` must be a person on the roster (`not_found`). All of this is checked
637
- before anything is stored.
638
- - A DM line from the receiver's own person is filed in its own conversation, as above.
639
- - A person who has no machine left on the roster is no longer a member of any channel; each
640
- machine drops them when it applies the removal.
641
- - **Delivery.** An accepted chat envelope gets a `delivered` receipt. It creates no inbox item, does
642
- not count against the inbox quota (§8.3) and never wakes a session. Consent (§6.9) and the inbound
643
- rate limit still apply. The sender records each chat envelope it routes with its line and
644
- recipient. A receipt from that recipient's machine, a relay nack, the recipient leaving the roster
645
- or expiry settles the record, once. An outgoing line is `delivered` once every other person has a
646
- delivered copy, and `refused` once some person's every copy was rejected or expired; until then
647
- it is `sent`. When every copy of a channel change to one person is refused, the sender files a
648
- notice line in that channel naming the person and the code. Notice lines are never sent.
649
- - **Transcript.** Each daemon keeps one row per line, keyed by `messageId`; a later envelope with a
650
- `messageId` this machine already holds is not filed and changes nothing. The thread key is the
651
- first line's `messageId`. Text is the line's text parts joined by newlines, cut to
652
- `LIMITS.chatChars` UTF-16 code units without splitting a surrogate pair. The sender files its own
653
- line with the plaintext it sealed; that copy is not on the wire. A person's line that arrives from
654
- another of their machines is filed as outgoing. Retention deletes lines after
655
- `retention.transcriptDays` (default 30), and a per-person cap deletes a sender's oldest lines
656
- first once they hold more than a policy number of lines or bytes (the reference daemon: 10,000
657
- lines or 16 MiB of UTF-8 text).
658
- - **Order and read state.** Each daemon numbers its chat changes with one counter that only
659
- rises and is never reused (`seq`, from 1): every filed line takes the next number, and so does
660
- every later change of a line (its delivery state) and every channel deleted here. A line keeps the
661
- number it was filed with as its `seq`, its place in this machine's order; another machine numbers
662
- the same line differently. Paging and read cursors use it. Read state is per machine and per room:
663
- a cursor (`readUpTo`, 0 at first) that only moves forward and never past the newest number here.
664
- A line is unread when another person's line, or the daemon's notice of another person's change,
665
- was filed after the cursor; this person's own lines never are. Leaving a channel, or being removed
666
- from it, deletes its cursor with its lines. Nothing syncs read state between machines.
667
- - **Joining later.** A person invited later does not receive earlier lines. Membership is each
668
- machine's own view: concurrent invites or removals can leave machines with different member
669
- lists, and nothing reconciles them. A post refused because of that shows as `refused` to its
670
- sender.
671
- - **Reading.** `conv.list`, `conv.read` and `search` (§8.2) see the DMs this machine holds and
672
- the channels its person is in. Agent-facing clients fence chat text as peer content and ask
673
- before reading it.
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.
674
660
 
675
661
  ### 6.9 Consent
676
662
 
677
663
  Consent belongs to the receiving person. A person's **grants** say what each other person, and the
678
- members of each channel, may send them and their agents. A device of the person signs each grant,
664
+ participants of each conversation, may send them and their agents. A device of the person signs each grant,
679
665
  only the person's own devices hold it, and the receiving daemon enforces it. Agents never sign or
680
666
  change a grant.
681
667
 
@@ -694,7 +680,7 @@ A roster realm has only `self` and `org`.
694
680
 
695
681
  ```ts
696
682
  { v, realm, id: Ulid, person, by: MachineId, ts,
697
- subject: { person } | { channel: <channel id> },
683
+ subject: { person } | { conv: ConvId },
698
684
  chat?: "open" | "muted" | "blocked", // lines from the subject (person subjects only)
699
685
  agentMail?: "open" | "blocked", // lines from the subject's agents
700
686
  ask?: "open" | "request" | "no", // mail and tasks from the subject or their agents to the person's agents
@@ -706,8 +692,8 @@ A roster realm has only `self` and `org`.
706
692
 
707
693
  - `sig` = Ed25519(`by`, `"messaging/1/grant\n"` ‖ JCS(grant without `sig`)). `by` is a device of `person`.
708
694
  - `person` and a person subject are `PersonId`s: a roster person name, or a hosted realm's `p_…` id (§15.1).
709
- - A channel grant sets only `ask` and `agentMail`. It applies to a sender only while the sender is a
710
- member of that channel in the receiver's own view (§6.8).
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.
711
697
  - `trustedUntil` needs `ask: "open"` and is at most `ts + LIMITS.trustMaxMs` (90 days). Trust is set
712
698
  for `LIMITS.trustDefaultMs` (30 days) when the person names no end. An expired `trustedUntil` reads
713
699
  as absent.
@@ -742,7 +728,7 @@ the person:
742
728
  **Effective policy.** For a sender S, each level is, in order:
743
729
 
744
730
  1. the person grant for S, where it sets the level;
745
- 2. otherwise, for `ask` and `agentMail`, the most permissive among the grants of channels S is in;
731
+ 2. otherwise, for `ask` and `agentMail`, the most permissive grant of a verified conversation shared with S;
746
732
  3. otherwise the default for the relation:
747
733
 
748
734
  | Relation | `chat` | `agentMail` | `ask` | trust |
@@ -767,7 +753,7 @@ from S.
767
753
  - `sig` = Ed25519(`by`, `"messaging/1/card\n"` ‖ JCS(card without `sig`)). `by` is a device of `from`; `from` and
768
754
  `to` are different persons. `trustedUntil` needs `ask: "open"`. A card is at most `LIMITS.cardBytes` of JCS.
769
755
  - `can` is the person grant for S over the defaults of S's relation to R (core's `cardCan`): `chat` is true for
770
- `open` and `muted`, `agentMail` for `open`. Channel grants are not on a card: S knows its own channels.
756
+ `open` and `muted`, `agentMail` for `open`. Conversation grants are not on a card.
771
757
  - `seq` rises with every card R's devices send S; S keeps the card with the highest `seq`.
772
758
  - Core's `verifyContactCard` checks the schema, the rules, the size, the realm, both persons and the signature over
773
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
@@ -824,40 +810,27 @@ from S.
824
810
  A delivered request then passes the inbound policy of §8.3 as any other;
825
811
  - a request ends unanswered at its expiry: the sender's own expiry settles its side. A request
826
812
  whose sender has left the realm ends at the answer, with nothing sent.
827
- - **Message requests** (a directory realm only). A person of one org first writes to a person of another with a
828
- message request: a `message` of class `request` with `conv: {kind: "request"}` and one text part of at most
829
- `LIMITS.requestChars` (§5, "Meaning"). Only a person sends one, with the human-tier `contact.request` (§8.2);
830
- agents never send or answer one.
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.
831
816
  - **Addressing.** The person gives an exact email. The daemon resolves it at the directory (`dir.resolve
832
817
  {email}`, §7); no answer is `not_found`, "no Messaging user with that email accepts requests", the same for an
833
818
  unknown and an undiscoverable email, so it cannot tell them apart. Several persons for one email is
834
819
  `ambiguous_address`.
835
- - **Sending.** The request goes to every current device of that person, one copy each, and is filed as the
836
- person's own line in `dm:<person>`. Its delivery state (§6.8) is `held` while each copy not yet delivered is
837
- held for the other person, `delivered` once they accept, `refused` once they decline. Before it sends, the
838
- daemon sets the person's own grant for the other to `chat: "open"` when it sets no chat level, so the answer,
839
- the card an accept sends and the other's lines reach the person; a person who blocked the other is refused
840
- `invalid_state`. The daemon paces before it sends (`rate_limited`): one request waits per person until that
841
- person writes back (or for `LIMITS.ttlMs`), and at most `LIMITS.requestsPerDay` persons are asked a day. The
842
- relay paces the same by the header (§15.10); its `rate_limited` nack of a request is final, not retried, and
843
- refuses the line.
844
- - **Receiving.** `decide` holds a request from a `stranger`; from anyone else it is a DM line by the sender, and
845
- from a blocked sender it is `refused`. A held request is stored with its envelope and opened payload (§8.8,
846
- `contact_requests`), one per sending person and at most `LIMITS.heldTotal` in all (`rate_limited`), answered with a
847
- `held` receipt and filed as a `request` notice (§8.2). It reaches no agent and wakes nothing. A request in a
848
- roster realm is `invalid_request`: a roster realm is one org.
849
- - **Answering** (`request.answer`): `accept` sets the sender's grant to `chat: "open"` (keeping its other
850
- levels), which makes the sender a `contact` and sends them a card, then files the request's text as their
851
- first line in `dm:<sender>` and answers `delivered`; `decline` answers `rejected {code: "declined"}`; `block`
852
- sets `chat: "blocked"`, then declines. A request ends unanswered at its expiry. Every device of the person
853
- holds its own copy: a grant for the sender that another device of the person signed, arriving by "Sync", settles
854
- the copy held here as its answer did (`chat: "open"` accepts it, `blocked` declines it).
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.
855
830
  - **The sender's card is the directory's.** Clients show the sender of a request by the realm's verified profile
856
831
  (name, email, org, §15.1), never by the request's text.
857
- - **Agents.** An agent that needs a contact in another org asks its own person: a line to the person with
858
- `request: {contact: <email>}` (`msg.send`, §8.2). The daemon files the line and a suggestion, listed by
859
- `request.list` and filed as a `request` notice; the person sends the request with `contact.request`
860
- (`suggestion` dismisses it), or dismisses it with `request.answer {answer: "decline"}`.
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.
861
834
  - **Cards on the daemon.** A device sends a card (a `message` with `conv: {kind: "card", what: "card"}`) to each
862
835
  current device of a person S when this person's grant for S changes on this device (`grant.set`, an answer to
863
836
  a request, a re-sign under "Signers"), and when S asks with `card.get`. It sends none to a `stranger` and none to
@@ -868,14 +841,7 @@ from S.
868
841
  person; it answers nothing. A `rejected` receipt from a person with `refused`, `request_required` or `declined`
869
842
  drops their card and asks for a fresh one. The `seq` of a device's cards is its clock in milliseconds, raised
870
843
  past the last one it used. A card is not a first message for the freshness bound (§15.9).
871
- - **Pacing.** Lines from one person's agents to another person (A2H), summed over those agents, are
872
- limited to `LIMITS.a2hPerHour` (30) an hour and `LIMITS.a2hPerDay` (100) a day:
873
- - the sending daemon refuses an agent's DM line or channel post at send time (`msg.send` to a person
874
- or `#channel`), before it stores or sends anything, when it would take any recipient person past either
875
- count, with `rate_limited` and when to try again; the person's own lines are not paced;
876
- - the receiving daemon refuses an agent's line past either count from one sending person with a
877
- `rejected` receipt, `rate_limited`, for a sender that does not pace itself;
878
- - the reference daemon counts in memory, so a restart starts the windows again.
844
+ - **Pacing.** Conversation append limits replace legacy chat-envelope pacing (§16). Retained task envelopes still follow the relay and inbound limits (§7, §8.3).
879
845
  - **Sync.** A device sends each grant it signs or re-signs to each of its person's other current
880
846
  devices: a sealed `message` to the device with no agent, `conv: {kind: "self", what: "grant"}`,
881
847
  whose one data part is `{grants}`. A device of the person that is new to the sender (one it has
@@ -1260,14 +1226,22 @@ Jelly ID, which wakes the host.
1260
1226
  It replaces the device's earlier ticket, and is kept in the relay's store (`dir_wakes`, §8.8) until it is
1261
1227
  used, disarmed, its device is unbound or erased, or it expires. At most 60 arms a device in a burst, then one a
1262
1228
  minute (`rate_limited` with `retryAfterMs`). `wake.disarm {}` forgets the device's ticket.
1263
- - **When the relay presents it.** Only when the relay has just taken (§7 "Routing": first seen, queued) an envelope for
1264
- the device while the device has no connection, and the envelope is:
1265
- - the person's own mail to an armed agent: header type `message`, `from.person` = `to.person`, and `to.agent` in
1266
- `agents`; or
1267
- - an answer the device awaits: header type `task.status` or `task.artifact` whose `replyTo` is in `awaiting`.
1268
-
1269
- Lines and posts (no `to.agent`), receipts, presence, task requests and cancels, anyone else's mail, a message that
1270
- merely names an awaited request, and anything for a connected device never wake. A request the recipient's person
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
1271
1245
  must answer (§6.9) is held by the receiving daemon, which the relay cannot see: such mail never wakes unless it is
1272
1246
  the person's own.
1273
1247
  - **Presenting.** `POST <url>` with the form `client_assertion_type=urn:jelly:wake&ticket=<ticket>`, no redirect
@@ -1292,6 +1266,72 @@ Jelly ID, which wakes the host.
1292
1266
  names them), and the ids of the task requests it awaits (envelope ids it already routed). A malicious relay can also
1293
1267
  fail to wake a host, as it can drop any envelope; that delays the person's mail, it loses none.
1294
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
+
1295
1335
  **What the relay can see.** Team ids, machine ids, persons, session names, timing, and presence:
1296
1336
  its session names, agent kinds, states and tiers, and the repo, branch and doing text a machine
1297
1337
  chooses to share (§6.5, none by default). It never sees message, task or receipt bodies.
@@ -1355,6 +1395,30 @@ chooses to share (§6.5, none by default). It never sees message, task or receip
1355
1395
 
1356
1396
  ### 8.2 IPC API
1357
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
+
1358
1422
  The API is the method table `IPC_METHODS` in `schema/ipc.ts`: JSON-RPC 2.0 NDJSON on two Unix
1359
1423
  sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (for example
1360
1424
  `contacts.list`, `item.read` and `task.get`), the TSDoc in that file and in `schema/ipc/*.ts` is normative.
@@ -1367,8 +1431,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1367
1431
  - **Two sockets.** The daemon serves the one API on two sockets, and itself enforces what each
1368
1432
  serves (its tiers). A client's own method list is a second check, not the boundary.
1369
1433
  - `run/agent.sock` (the agent tier) serves only `AGENT_METHODS`. Any other method fails with
1370
- `forbidden` before its params are read. A line as the person (`person.send`, no session) is not
1371
- one of them (§6.8). Agent-facing clients connect here: the OpenCode plugin's server part
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
1372
1436
  and `messaging-mcp`.
1373
1437
  - `run/admin.sock` (the admin tier) serves every method. Human clients connect here: the `messaging`
1374
1438
  CLI and the `/messaging` TUI.
@@ -1422,7 +1486,7 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1422
1486
  - **What the tiers are, and what they are not.** Where the daemon, OpenCode and the user's shell
1423
1487
  run as one OS user, as on a laptop, the tiers are not a security boundary: any process of that
1424
1488
  user can open `run/admin.sock`. They keep `team.*`, `identity.rotateEnc`, `daemon.stop`,
1425
- `channel.*`, `conv.subscribe`, `conv.markRead` and the person's own chat lines off every tool path. A tool that
1489
+ conversation administration and the person's own chat lines off every tool path. A tool that
1426
1490
  speaks to the agent socket cannot reach them. A shell that runs the `messaging` CLI is still a shell,
1427
1491
  and only the host's shell permissions govern it. The tiers are a boundary only when `messagingd`
1428
1492
  runs as its own user and the engine runs as another user in `ipc.agentGroup` (the layout is in
@@ -1431,23 +1495,25 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1431
1495
  as them and call every method, including `team.*` and `daemon.stop`. Agent-facing clients still
1432
1496
  must not expose `team.*`. The OpenCode TUI is a human surface and may call them. It must not
1433
1497
  place an invite ticket in a session, a log, or a model context.
1434
- - Key-scoped methods (`session.update`, `session.unregister`, `events.subscribe`, `msg.send`,
1435
- `inbox.*`, `item.read`, `task.*`, `conv.read` with a key) work only for a key that the calling connection registered (`not_found`
1436
- otherwise).
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.
1437
1501
  - A session key is therefore a bearer credential for its session: whoever knows it can register it
1438
1502
  and act as that session.
1439
1503
  - `AGENT_METHODS` (with its type `AgentMethod`) lists the methods an agent-facing client may call,
1440
1504
  and exactly the methods `run/agent.sock` serves. Anything else is for human clients: the
1441
1505
  `messaging` CLI and the `/messaging` TUI.
1442
- - **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:
1443
1507
  `AGENT_METHODS` (the agent tier: sessions, `device.status`, `device.bind`, `contacts.list`,
1444
- `msg.send`, `inbox.list`, `inbox.get`, `inbox.ack`, `item.read`, `conv.read`, `search`, `task.*`,
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.*`,
1445
1509
  `events.subscribe`, `daemon.status`, `daemon.health`); `HUMAN_METHODS` (acting as the person: `conv.list`,
1446
- `conv.read`, `conv.markRead`, `conv.subscribe`, `person.send`, `people.search`, `people.get`,
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`,
1447
1513
  `grant.list`, `grant.set`, `grant.set.preview`, `request.list`, `request.answer`, `request.answer.preview`, `contact.request`,
1448
- `contact.request.preview`, `card.get`, `agents.list`,
1449
- `agents.read`, `notice.subscribe`, `notice.list`, `notice.ack`, `device.approve.preview`,
1450
- `device.remove.preview`, `identity.replace.preview`, `directory.erase.preview`, `channel.*`); and `ADMIN_METHODS`
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`
1451
1517
  (this device and the daemon: `daemon.stop`,
1452
1518
  `daemon.woke`, `person.attached`, `daemon.snapshot`, `wake.arm`, `wake.disarm`,
1453
1519
  `device.list`, `device.remove`, `device.link`, `device.login`, `device.login.status`, `device.login.cancel`,
@@ -1562,12 +1628,8 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1562
1628
  `PeerRef` (inbox items, task records), provenance (§6.6), directory entries (whose client kind is
1563
1629
  `client`) and chat lines. `daemon.status` names this device in `identity` (its `device`), and a divergence alarm
1564
1630
  the signer's `device`. Team administration (`team.*`) keeps the roster's `machine`.
1565
- - **Persons in the API.** Wherever the API names a person (addresses, `PeerRef`, provenance,
1566
- directory entries, chat lines and their `refused`, `conv` events, `daemon.status`'s identity,
1567
- `contacts.list`, `people.*`, a `dm:` or `agent:` conversation, and a line's `to` in `msg.send`), it
1568
- takes a `PersonId`: a roster person name or a hosted `p_…` id. Channel membership (`channel.*`, and the
1569
- `persons` and `creator` of a channel op, §6.8) takes `PersonId`s too. Team administration (`team.*`) takes
1570
- roster person names.
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.
1571
1633
  - **`session.register`.** Names a session.
1572
1634
  - The requested name is suffixed `-2`, `-3`, ... if it is taken on this machine.
1573
1635
  - Registering an existing key again updates it; this is idempotent.
@@ -1608,24 +1670,14 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1608
1670
  expired `trustedUntil` is left out. So an agent sees whom it can reach without a human-tier call. A `reach` also
1609
1671
  carries `devicesChangedAt` while a device of that person was added, removed or replaced less than 7 days ago, as
1610
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
1611
- 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
1612
1674
  (below), computed from the same view. Agent clients show it before anything is sent. It is absent
1613
1675
  when no presence lists the session.
1614
- - **`msg.send`.** One send from the session `key`. Its `to` decides what it is:
1615
- - **To `<person>` or `#<channel>`: a line** in the person's DM with them, or a post in a channel
1616
- this person is in, attributed to the session (§6.8). It takes `kind: "message"`, one text part of
1617
- at most `LIMITS.chatChars`, and `replyTo` as the thread (the `id` of any line of it); any other
1618
- A2A field (`contextId`, `taskId`, `messageId`, `metadata`, `extensions`, `referenceTaskIds`,
1619
- `onBehalfOf`, `wake`) is `invalid_request`, and so is a `to` that is neither a person
1620
- name nor `#` and a channel name. It fails with `not_found` for a person not on the team, a
1621
- channel this person is not in, or an unknown thread, and `too_large` for a longer line. It writes
1622
- the line as `person.send` does (below), and `idempotencyKey` is the line's `clientId`. Its one
1623
- result is `{to, envelopeId: <line id>, messageId, contextId, stage: "accepted"}`. With `request:
1624
- {contact: <email>}`, only on a line to the session's own person (otherwise `invalid_request`), the daemon
1625
- also files a suggestion for the person (§6.9, "Message requests").
1626
- - **To `person/session[@label]`: mail or a task** to that session, as the rest of this item says.
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`.
1627
1679
  - Refuses content that holds a credential (`secret_refused`, §8.7), as do `task.update`,
1628
- `task.cancel` and `person.send`.
1680
+ `task.cancel`, `conv.send` and `conv.edit`.
1629
1681
  - Resolves the address against the directory, including offline sessions.
1630
1682
  - Builds one envelope per recipient and stores it in the outbox (`accepted`).
1631
1683
  - Sends it when the relay is connected.
@@ -1651,14 +1703,12 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1651
1703
  `idempotencyKey` returns the hint computed for the original call.
1652
1704
  - `idempotencyKey` returns the original results for a repeated call from the same key.
1653
1705
  - `wake` defaults to true.
1654
- - The daemon assigns ULID task ids for `kind: "task"`, and the `contextId` when it is omitted.
1655
- - A `kind: "task"` send signs a root grant for each recipient (§6.7).
1706
+ - Creation assigns ULID task IDs and a `contextId` when omitted, and signs a root grant for each recipient (§6.7).
1656
1707
  - A chain this daemon already knows the recipient would refuse is not sent. The result is
1657
1708
  `stage: "rejected"` with that check's code, and the requester's task is stored
1658
1709
  `TASK_STATE_REJECTED` with no grants.
1659
1710
  - **`onBehalfOf {taskId, scope?, maxDepth?}`** delegates on behalf of a task that the calling key
1660
- **executes**, and only with `kind: "task"`. Otherwise it fails with `not_found` (any other task)
1661
- or `invalid_request` (`kind` is not `"task"`), before anything is stored. The daemon:
1711
+ **executes**, and only with creation. Any other task fails with `not_found` before storage. The daemon:
1662
1712
  1. refuses a terminal task (`invalid_state`) before anything is stored;
1663
1713
  2. signs the next grant of the stored chain for each recipient, narrowed to `scope` and
1664
1714
  `maxDepth` when given;
@@ -1681,101 +1731,40 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1681
1731
  - **`task.cancel`** is allowed only for the requester.
1682
1732
  - **`task` events** carry `origin`:
1683
1733
  - `local` when the event follows the subscriber key's own call: `task.update`, `task.cancel`,
1684
- or the `msg.send {kind: "task"}` that created the record;
1734
+ or the `task.create` call that created the record;
1685
1735
  - `peer` when it follows an envelope from the other side, a receipt, or a daemon timeout.
1686
1736
  - When the event follows a status or artifact update, `update` carries that delta, including
1687
1737
  `append` and `lastChunk`.
1688
- - **`conv.read {key?, conversation, thread?, before?, after?, limit?}`** returns one page of a
1689
- conversation, oldest first, as `{kind, …, more}`. `conversation` is `dm:<person>` or
1690
- `channel:<name>` (`kind: "lines"`, below under Chats), or `agent:<person>/<agent>` (`kind:
1691
- "agent"`), which needs the session `key`: what the session `key` exchanged with one other session,
1692
- on any machine of that person: incoming mail,
1693
- outgoing mail and tasks either way, as `ConvEntry` rows in time order. A task request appears
1694
- once, as its task (dated when it was created, with its current state), not again as mail. A
1695
- cursor is an entry's `{at, id}` with an agent, and a line's `seq` in a DM or a channel; the other
1696
- form is `invalid_request`, and so is `thread` with an agent. With no cursor it is the latest `limit` entries (default 30, at
1697
- most 100); with `before` the latest `limit` before that place; with `after` the earliest `limit`
1698
- after it. Entries come oldest first, and `more` says entries remain past the page in the
1699
- direction read; `before` and `after` together are invalid params. Incoming mail carries its
1700
- preview and stage, a task the start of its request, the session's role and the task's state.
1701
- Outgoing mail carries its stage but no text: the daemon keeps no copy of what it sent. It changes
1702
- no stage and sends no receipt. Agent clients build their conversation view on it (§11).
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.
1703
1741
  - **`task.list`:** most recently updated first. It filters by `role`, `state`, `contextId`,
1704
1742
  `peer` (a qualified address) and `updatedAfter` (at or after, ≥), pages with an opaque `cursor`/`nextCursor`, and reports `total`.
1705
- - **`msg.send` A2A passthrough** (mail and tasks): `messageId` (generated if omitted), `metadata` (at most
1743
+ - **Task A2A passthrough:** `messageId` (generated if omitted), `metadata` (at most
1706
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
1707
1745
  side), `extensions`, and `referenceTaskIds`.
1708
- - **Chats (§6.8).** `conv.read` (of a DM or a channel) and `search` need no session key. They are
1709
- the only chat methods in `AGENT_METHODS`; an agent's own line goes through `msg.send`.
1710
- - `conv.list` (human tier) returns at most `limit` conversations (default 100, at most 200), newest
1711
- first. A channel held under a local name carries its own `name`. Each carries its read state
1712
- (§6.8): `readUpTo`, `unread`, and `lastSeq`, the newest line's `seq` (absent in a channel with no
1713
- lines).
1714
- - `conv.read` of a DM or a channel returns a page of its lines (or of one thread), oldest first, and `more`. With
1715
- no cursor it is the latest `limit` lines (default 50, at most 100); with `before: seq` the latest
1716
- `limit` lines below that `seq`, to page back; with `after: seq` the earliest `limit` lines above
1717
- it, to catch up. `more` says that lines remain past the page in the direction read. `before` and
1718
- `after` together are invalid params.
1719
- - `conv.markRead {conversation, upTo}` moves the conversation's read cursor to `upTo`, a line's `seq`, if that is
1720
- later, clamped to the newest `seq` here. It returns `{readUpTo, unread}`. It is a human-client
1721
- method that is not in `AGENT_METHODS`: read state is the person's, and an agent reading a
1722
- conversation does not mark it read. It fails with `not_found` for a channel this person is not in.
1723
- - A `ChatLine` this machine sent to other persons carries `state` (`sent`, `delivered` or
1724
- `refused`) and, when refused, `refused` (the persons). A line the daemon wrote itself, about a
1725
- channel change or a refusal of one, carries `notice: true`.
1726
- - `person.send {conversation, text, thread?, clientId?}` (human tier) writes the person's own line
1727
- and its envelopes in one transaction and sends them at once, with no session. It fails with
1728
- `not_found` for a person not on the team or a channel this person is not in. A DM to oneself
1729
- is the person's own conversation (§6.8).
1730
- - `search {query, conversation?, limit?}` finds lines in the DMs and channels this person holds,
1731
- newest first, as `hits`, each with its `conversation`.
1732
- - **Pacing.** The sender keeps its chat envelopes to one recipient machine below that machine's
1733
- chat budget (§8.3): the reference daemon allows 50 a minute, ten below
1734
- `LIMITS.inboundPerMachinePerMinute`, for clock and transit jitter. Channel changes count too. A
1735
- line (`person.send`, or `msg.send` to a person or a channel) that would go over it for any machine
1736
- it goes to fails with `rate_limited`, a
1737
- message that says when to try again, and nothing stored or sent. Pacing cannot cover a backlog:
1738
- lines queued at the relay for an offline machine reach it together when it connects. The
1739
- receiver defers those past its budget and the relay paces them (§7 "Delivery", §8.3), so every
1740
- line is delivered within minutes; only one that would expire first is refused.
1741
- - A line with a `clientId` (`msg.send`'s `idempotencyKey`) is idempotent per `clientId` and
1742
- sender: the session `key`, or the person for `person.send`. A repeat with the same room, thread and text returns the first call's
1743
- result and files and sends nothing. The same `clientId` for another line is `invalid_request`.
1744
- The daemon keeps a `clientId` for `LIMITS.ttlMs`.
1745
- - `channel.create`, `channel.invite`, `channel.leave` and `channel.remove` are for the human
1746
- `/messaging` TUI. They take a channel name as this machine holds it (its local name). Each checks
1747
- before it stores anything. `channel.create` refuses a name already held, more than the
1748
- channels a machine holds (`invalid_request`), an unknown person, or more than
1749
- `LIMITS.channelPersons` persons with the creator. `channel.remove` refuses a caller who is not
1750
- a member (`not_found`), who is neither the creator nor a team admin (`forbidden`), a list that
1751
- names the caller (`invalid_request`: use `channel.leave`), and a person who is not a member
1752
- (`not_found`).
1753
- - Mail and tasks (`msg.send` to an agent) never write the transcript.
1754
- - **`conv` events** go only to connections that called `conv.subscribe`, a human-client method
1755
- that is not in `AGENT_METHODS`. They are never sent to `events.subscribe` keys. A `conv` event
1756
- names the conversation, the line's `id` and its person, never its text. It is raised for a filed line,
1757
- a channel change, and a change of an outgoing line's delivery state (then `person` is this
1758
- machine's person). Each carries the `seq` of its change (§6.8), so events rise in `seq`.
1759
- - **Resuming.** `conv.subscribe {since}`, where `since` is the `seq` of the last event the client
1760
- handled, first sends one `conv` event per line that changed after `since`, oldest change first,
1761
- each with the `seq` of the line's latest change. Then live events follow, with none between.
1762
- It returns `{seq, gap}`: the newest `seq` at the call, and whether the replay could not be
1763
- complete, in which case it sends none. That is when more than a policy number of lines changed
1764
- (the reference daemon: 500), a channel was deleted here after `since`, or `since` is above
1765
- `seq` (a store set up again). After a gap the client reloads with `conv.list` and `conv.read`.
1766
- Without `since`, `gap` is false and no events are replayed.
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.
1767
1757
  - **Consent (§6.9).** `grant.list`, `grant.set`, `grant.set.preview`, `request.list`,
1768
1758
  `request.answer`, `request.answer.preview`, `contact.request`, `contact.request.preview` and `card.get` are human-client methods that are
1769
1759
  not in `AGENT_METHODS`: agents never read or change a person's grants, answer a held request, or send a message
1770
1760
  request.
1771
1761
  - `grant.list {subject?}` returns this person's grants as this device holds them, newest first,
1772
1762
  each with its levels, `id`, `ts`, `by` and, when re-signed, `resignedAt`, and the review list:
1773
- the widening levels that lapsed with their signer, with when they lapsed. A channel subject
1774
- carries the channel id and, while this person is in it, its local name.
1763
+ the widening levels that lapsed with their signer, with when they lapsed. A conversation subject carries its canonical `cid`.
1775
1764
  - `grant.set {subject, chat?, agentMail?, ask?, trustedUntil?, trust?, discoverable?}` signs the
1776
1765
  whole new grant for the subject with this device: a level left out is the relation's default,
1777
- and no level at all is a clear. `subject` is `{person}` or `{channel}`, a channel by its local
1778
- name. `trust: true` without `trustedUntil` trusts for `LIMITS.trustDefaultMs`. It stores the
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
1779
1768
  grant, takes the subject off the review list and sends the grant to this person's other devices
1780
1769
  in one transaction. It fails with `not_found` for a person the realm does not know or a channel
1781
1770
  this person is not in, `invalid_request` for a grant that breaks a rule of §6.9, and
@@ -1843,27 +1832,12 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1843
1832
  `daemon.snapshot {done: true}` deletes it and returns `{released}` (false when none was held). One is held at a time
1844
1833
  (`invalid_state` while one is); the daemon releases it after an hour (`until`), when it stops, and one a previous run
1845
1834
  left when it starts.
1846
- - **Your agents.** `agents.list` and `agents.read` (human tier, read-only) are the person's view of their
1847
- own agents' traffic on this device.
1848
- - `agents.list` returns each local session (registered or kept) as `{agent, conversations, unread,
1849
- lastAt?, state}`: how many other agents it has mail or tasks with, the mail it has not read, its last
1850
- traffic, and `idle`, `busy` or `kept`; most recent first.
1851
- - `agents.read {agent, conversation?, before?, limit?}` returns, without `conversation`, `{kind:
1852
- "conversations", conversations}`: each `agent:<person>/<agent>` it has traffic with, with its unread
1853
- mail and last time, most recent first. With `conversation`, it returns `{kind: "entries",
1854
- conversation, entries, more}`: a page of entries as `conv.read` gives them to that agent (default
1855
- 30, latest first, `before` an entry's `{at, id}`). It changes no stage and sends no receipt.
1856
- `not_found` for an agent this device has no session of. Peer text is untrusted; a client cleans it
1857
- for display (`cleanPeerText`) and never shows it to a model.
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.
1858
1837
  - **Notices.** What the person is told about, in three fixed categories, as `{id, category,
1859
1838
  conversation?, from?: {name, email?, org}, line?, at}`:
1860
- - `message`: a line filed for the person in a DM or a channel from another person or their agent, or
1861
- from one of the person's own agents in their own conversation; never the person's own line, a line
1862
- about a channel change, or a change of a line's delivery state. Its `conversation` is the DM or the
1863
- channel, and `from` the sender's person as the realm verified them.
1864
- - `request`: a request held for the person (§6.9), with `conversation` the sender's
1865
- `agent:<person>/<agent>` (`dm:<person>` when the person sent it themself, as for a message request); or a
1866
- suggestion of one of the person's own agents, with `conversation` `agent:<own person>/<agent>`.
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.
1867
1841
  - `alert`: an alarm of this device (`keys_missing`, `restored`, `directory`), with fixed text in `line`; a
1868
1842
  change of another device of the person (below), with `device`; or trust the person gave someone that ends
1869
1843
  within 7 days (§6.9), with `from` that person and `line` "Trust you gave this person ends on <date>; renew it
@@ -1888,13 +1862,11 @@ sockets in `$MESSAGING_HOME/run/`. For a method this section does not describe (
1888
1862
  was replaced by 'desk2' (jelly-host), ...", "'phone' (laptop) was removed from your account.".
1889
1863
  - Each notice is filed once, when its source is, under an id derived from the source (`m_<line id>`,
1890
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:
1891
- with `lines: true`, a message's `line` is the first line of its text read from the transcript,
1892
- through `cleanPeerText`, at most 120 characters with its "…".
1865
+ `lines: true` can expose fixed alert text. It never restores legacy transcript content.
1893
1866
  - `notice.subscribe {after?, lines?}` sends the unacknowledged notices filed after the notice `after`
1894
1867
  (all of them without it, at most 500), oldest first, as `notice` events on this connection, then each
1895
1868
  new one as it is filed; it returns `{replayed}`. `notice.list {lines?, limit?}` returns the
1896
- unacknowledged notices, newest first (default 50), and `counts`: `unread`, the lines from others not
1897
- yet marked read over every conversation, and `requests`, the asks, message requests and suggestions held. `notice.ack {ids}` marks
1869
+ unacknowledged notices, newest first (default 50), and `counts.requests`: the asks, message requests and suggestions held. `notice.ack {ids}` marks
1898
1870
  notices seen; unknown ids are ignored. It never answers, releases or uncounts a request: a request stays held, in
1899
1871
  `request.list` and `counts.requests`, until `request.answer` (or its expiry). Notices go with the transcript's retention, and acknowledged ones
1900
1872
  with the read mail's.
@@ -1929,8 +1901,7 @@ delegation chain is verified after these (§6.7). A person refuses another perso
1929
1901
  - Receipts, `task.status` and `task.artifact` answer the recipient's own traffic, so they have a
1930
1902
  budget of their own: four times that.
1931
1903
  - Presence has a budget of its own, also four times that.
1932
- - Chat envelopes (a `message` to the machine with no session, §6.8) have a budget of their own,
1933
- `LIMITS.inboundPerMachinePerMinute`, apart from agent mail.
1904
+ - Person-addressed controls (contact requests, cards and grants) have their own budget of `LIMITS.inboundPerMachinePerMinute`.
1934
1905
  - Over the limit, an envelope is **deferred**: the recipient withholds its `recv.ack` and does not
1935
1906
  mark it seen, so the relay sends it again later (§7 "Delivery") and it is taken when the budget
1936
1907
  has room. A backlog that arrives at once is so taken at the budget's pace, all of it, not
@@ -1939,9 +1910,7 @@ delegation chain is verified after these (§6.7). A person refuses another perso
1939
1910
  can wait.
1940
1911
  - An envelope over the limit that would expire before the budget has room for it, with a minute
1941
1912
  to spare for the relay's next redelivery, can never be taken. It is dropped (acked). A message
1942
- sender gets at most one `rate_limited` receipt a minute. A chat sender gets a `rejected` receipt
1943
- with `rate_limited` for every such chat envelope, up to four times the chat budget a minute, so
1944
- its line is `refused` at once instead of `sent` until it expires.
1913
+ sender gets at most one `rate_limited` receipt a minute. Person-addressed controls get individual refusals, bounded by the response budget.
1945
1914
  - Presence over its budget is dropped: it is never queued, and the next one supersedes it.
1946
1915
  - **Quota.** A recipient may cap the inbox items and bytes it holds from one sender machine
1947
1916
  (`rate_limited` past it).
@@ -1954,6 +1923,21 @@ delegation chain is verified after these (§6.7). A person refuses another perso
1954
1923
  - **`wake` on an inbox item.** This is the sender's hint, AND-ed with the session's tier, which must be
1955
1924
  `push`. Only what consent delivers becomes an inbox item, so a held request never wakes a session.
1956
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
+
1957
1941
  ### 8.4 Delivery stages (sender view)
1958
1942
 
1959
1943
  - The normal sequence is `accepted → relayed → delivered → handed_to_agent → read`. A request the
@@ -2036,8 +2020,8 @@ then lives in plaintext in the sender's and recipient's stores for their retenti
2036
2020
  and in the recipient's model context. The sending daemon therefore refuses such content before it
2037
2021
  stores or seals anything. The relay never sees plaintext, so this check exists only on the sender.
2038
2022
 
2039
- - **Where.** `msg.send` (lines, messages, replies and task requests), `task.update` (status message
2040
- and artifact), `task.cancel` (`reason`) and `person.send` (`text`). The daemon reads every string in
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
2041
2025
  the call's params except the session `key`: values and object keys at any depth, including
2042
2026
  `metadata` and `data` parts, and the base64-decoded bytes of every `raw` field, read as UTF-8.
2043
2027
  - **When.** After the key and task-role checks (`not_found`, `invalid_state` for a wrong role), and
@@ -2076,26 +2060,34 @@ stores or seals anything. The relay never sees plaintext, so this check exists o
2076
2060
 
2077
2061
  ### 8.8 Store schema
2078
2062
 
2079
- The daemon's store (`messaging.db`) and the relay's database each have one baseline, schema 1. Messaging resets
2080
- its stores now and grows them later (owner, 2026-11-08):
2081
-
2082
- - **Until the first release another person uses**, a store change edits the baseline instead of adding a
2083
- migration, and raises the store's generation. The release that carries it resets the stores and says so in its
2084
- notes. Release 0.7.0 is the first such reset: it reads no store of 0.1.0 to 0.6.0.
2085
- - **From that release on**, stores only grow, so that a host can roll a daemon or relay back by moving the
2086
- binary back and keeping the data. Migrations then follow the rules under "When stores grow" below.
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.
2087
2078
 
2088
2079
  **Records.** The daemon's store keeps `generation`, `schema` and `min_reader`, the lowest schema that can read
2089
2080
  it, in its `kv` table. The relay keeps `generation` and `min_reader` in its `meta` table, and `schema` in
2090
- `PRAGMA user_version`. A new store records generation 2, schema 1 and `min_reader` 1. The stores of 0.1.0 to
2091
- 0.6.0 have no generation (generation 1).
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).
2092
2083
 
2093
- **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,
2094
2085
  the tables Litestream adds when it replicates the database (`_litestream_*`) do not count. It opens
2095
2086
  any other store only when the store records its own generation and a `min_reader` at most its own schema. It
2096
- refuses every other store and changes nothing in it, and the error says how to start over: stop the daemon or
2097
- relay, move the home (or the relay database, with its `-wal` and `-shm` files) aside, and set it up again. A
2098
- daemon checks the store before it reads or writes anything else in its home. Nothing deletes a home or a
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
 
2101
2093
  **A refused store** is machine-readable, so that a client never matches the wording. Every refusal has one of two
@@ -2137,10 +2129,13 @@ database by itself.
2137
2129
  - A breaking change needs a new store file with an explicit export and import, never an in-place change.
2138
2130
  `min_reader` is raised only in an emergency, and hosts that pin the daemon are told first.
2139
2131
 
2140
- **The test.** `store-schemas/1.json` in `packages/daemon` and `packages/relay` is the shape of the baseline
2141
- (tables, columns, indexes, constraints, triggers). `test/release/store-schema.test.ts` in each package fails
2142
- when HEAD's store differs from it, so a store change is seen in review. When stores grow, each released schema
2143
- gets its snapshot, and the test holds HEAD to every one of them.
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.
2144
2139
 
2145
2140
  The wire rule (§14) is separate; a rolled-back daemon must still speak to newer peers within `messaging/1`.
2146
2141
 
@@ -2257,9 +2252,9 @@ and names each session an engine may register.
2257
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
2258
2253
  behind it;
2259
2254
  - `cls` is required, and is the method's action class (`ACTION_CLASSES`, `actionClass` in `@messaging/core`):
2260
- `chat` for `person.send`, `conv.markRead` and `notice.ack`; `view` for the reads a host's section makes, the
2261
- read-only human-tier methods and `device.list` (`conv.list`, `conv.read`, `conv.subscribe`, `people.search`,
2262
- `people.get`, `grant.list`, `request.list`, `agents.list`, `agents.read`, `notice.subscribe`, `notice.list`,
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`,
2263
2258
  `device.list`); `local` for every other method, the previews and every other admin-tier method included.
2264
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
2265
2260
  a `local` action would do, takes `local`. A plugin's `jelly` declaration must give each action and read the same
@@ -2273,8 +2268,8 @@ and names each session an engine may register.
2273
2268
  `person.attached`, `daemon.snapshot`, `wake.arm` and `wake.disarm`.
2274
2269
  It is bound to the first connection that presents it, and may be presented again on that connection until
2275
2270
  it expires; on another connection, or after a restart, it is `grant_invalid`. Under a host-scope grant
2276
- `conv.list` returns counts only: each conversation's `conversation`, `kind`, `readUpTo` and `unread`, with
2277
- no channel name, time or seq. A `notice.subscribe` under a host-scope grant ends when the grant does: the
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
2278
2273
  host subscribes again with a fresh grant and `after`.
2279
2274
  - **Refusals.** No grant is `grant_required`; any failure above is `grant_invalid`. The agent tier needs no
2280
2275
  grant on either socket. Both are checked before the params are read. The daemon logs each refusal with
@@ -2500,30 +2495,38 @@ daemon owned by the uid in `MESSAGING_DAEMON_UID` (§8.1).
2500
2495
 
2501
2496
  ### Agent tools
2502
2497
 
2503
- The OpenCode plugin and `messaging-mcp` offer an agent the same eight tools, with the same input and
2504
- output schemas (`AgentTool` in `@messaging/core`, normative like `schema/**`): `contacts`, `send`,
2505
- `inbox`, `read`, `search`, `tasks`, `task_update` and `task_cancel`. Their names are
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
2506
2511
  `messaging_<verb>`; in OpenCode's Code Mode, `tools.messaging.<verb>`. No tool grants, adds
2507
2512
  contacts, changes devices or administers a team or channel.
2508
2513
 
2509
- - **Addresses.** `me` (the session's own person), `me/<agent>`, `<person>`, `<person>/<agent>`,
2510
- `<person>/<agent>@<label>` and `#<channel>` (a channel the person is in, by its local name). An
2511
- agent is a session (§3). Malformed text is `invalid_request` naming the field, and so is a group
2512
- form (`person/*`, `repo:…`, `*`): there are none (§3). A person, agent or channel the daemon does not know
2513
- (`not_found` for a person or channel, `no_recipients` for an agent, from `msg.send` or `conv.read`) is `unknown_address`; an agent
2514
- that runs on none of its person's machines is `unknown_session` (§3, "Resolution"); `ambiguous_address` asks for `@label`. `me`, and one's own person name, is the person's own
2515
- conversation, `dm:<self>` (§6.8): `send` writes a line there (unread for the person), `read` and
2516
- `search` read it, and a guarded peer turn may write there too. An `ask` to `me` (a request card to
2517
- one's own person) is `invalid_request`: it is not in this build.
2518
- - **Conversations.** `person:<person>` is the person's DM room with them (§6.8), seen from the agent;
2519
- `agent:<person>/<agent>` is the session's mail and tasks with that session on any machine
2520
- (`conv.read`, §8.2); `channel:<name>` is a channel room. `read({with})` pages one of them, oldest
2521
- first, with opaque `before`/`after` cursors. Mail and task entries carry previews; outgoing mail
2522
- carries no text, because the daemon keeps no copy of what it sent.
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.
2523
2526
  - **`contacts`** lists `me` (address, person, agent, device label, relay `connected` or `offline`), the
2524
- persons in the directory and in DM rooms, each with their agents (state, `expect` from
2525
- `contacts.list` (§8.2), repo, branch and doing), and the channels `contacts.list` names, with their unread
2526
- counts. `org` is `same` for every teammate. A person's `can` is the daemon's `reach` for them (§8.2
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
2527
2530
  `contacts.list`): `source: "card"` with `card`, when their person signed it, or `source: "default"`, the
2528
2531
  defaults for the org or for a stranger; `can.chat` is false for one's own person and when the client offers no
2529
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
@@ -2533,28 +2536,21 @@ contacts, changes devices or administers a team or channel.
2533
2536
  refused. `can: "ask"` lists those whose `can.ask` is not `no`. A channel's `can` is `default`. `presence` is
2534
2537
  `online`,
2535
2538
  `offline` or (for a channel) `unknown`. No field is invented.
2536
- - **`send`** is one `msg.send`: a chat line to `<person>` or `#<channel>`, or mail or, with `ask`, a
2537
- task to an agent. `replyTo` names a line (the thread), a message from that agent
2538
- (`replyTo` and its `contextId`), or a task the session requested (`taskId`: input for it, to its
2539
- executor only). `onBehalfOf` (with `ask`) passes `msg.send {onBehalfOf: {taskId}}` after
2540
- checking that the session runs that task and it is not finished. `files` (only to an agent) become
2541
- file parts. `key` makes a repeated call send once (`idempotencyKey`);
2542
- without it the client derives one from the tool call. Results carry each `SendResult.expect` and a
2543
- fixed `note`. A result at stage `held` (the recipient's person answers a request card first, §6.9)
2544
- carries `expect: "on_consent"` and a note saying so. `request: {contact: <email>}`, only with `to: "me"`
2545
- (otherwise `invalid_request`), passes `msg.send {request}`: the line goes to the agent's own person with a
2546
- suggestion to send a message request to that exact email (§6.9, "Message requests"); the agent cannot send one,
2547
- and its note says it is not told what the person decides. A send refused `request_required` tells the agent to
2548
- ask its person this way.
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.
2549
2546
  - **`inbox`** lists mail and task requests (`inbox.list`), unread by default, and acks
2550
2547
  `handed_to_agent` for the previews it shows. `consent` says how a request from the sender runs
2551
2548
  here: `self` (the session's own person), `trusted` or `guarded`. It is the inbox item's `consent`, the
2552
2549
  daemon's decision (§6.9); a client decides it only for a task record or an item stored before
2553
2550
  consent, which carry none.
2554
- - **`read({id})`** looks the id up as an inbox item (`inbox.get`, then `item.read`: marked read, read
2555
- receipt), a task (`task.get`), or an envelope the session sent (`item.read`, as `delivery`).
2556
- - **`search`** covers chat lines (`search`) with persons and in channels; agent mail is not
2557
- searched.
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.
2558
2554
  - **`tasks`** lists `task.list` with roles `mine` (requester) and `asked` (executor) and short state
2559
2555
  names (`working`, `input_required`, …); `state` also takes `open` and `done`.
2560
2556
  - **`task_update`** (executor) and **`task_cancel`** (requester) map to `task.update` and
@@ -2593,12 +2589,12 @@ contacts, changes devices or administers a team or channel.
2593
2589
 
2594
2590
  ### Clients
2595
2591
 
2596
- - **OpenCode plugin.** Tools under namespace `messaging`: `contacts`, `send`, `inbox`, `read`,
2597
- `search`, `tasks`, `task_update` and `task_cancel`. Every send asserts `messaging.send` with the
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
2598
2594
  address as its resource (`person/agent`, `#channel`, or `person` and `person/`), and every read of
2599
2595
  a conversation asserts `messaging.read` with the conversation (a `person:` one asks first by
2600
2596
  default). Inbound content is framed as untrusted data. Details are in the plugin's repository, `joe-broadhead/opencode-messaging` (`docs/`).
2601
- - It never calls `conv.markRead`: read state is the person's.
2597
+ - It never changes human read state.
2602
2598
  - With wakes on, it registers each session with `host: {kind: "opencode", location, sessionID,
2603
2599
  pid}`: its location directory, the OpenCode session id, and the server's pid.
2604
2600
  - Its RPC `messaging.opencode/wake {sessionID, envelopeIds}` serves the wake bridge (§8.6). It checks
@@ -2622,16 +2618,10 @@ contacts, changes devices or administers a team or channel.
2622
2618
  item only after the host recorded the tool result with the note intact; until then the item is
2623
2619
  only offered, and an offer that fails or is cut goes back to waiting. No wire change.
2624
2620
  - **MCP server.** `messaging-mcp` (stdio) for other agents. It registers one `pull`-tier session and
2625
- subscribes to its events. Its tools are `messaging_contacts`, `messaging_send`, `messaging_inbox`,
2626
- `messaging_read`, `messaging_search`, `messaging_tasks`, `messaging_task_update` and
2627
- `messaging_task_cancel`, over `daemon.status`, `contacts.list`, `msg.send`, `item.read`,
2628
- `inbox.list/get/ack`, `conv.read`, `task.list/get/update/cancel` and `search`.
2629
- - MCP has no per-conversation approval of its own, so chats are opt-in: the user names channels
2630
- when configuring the server, with `--channel <name>` (repeatable). `send`, `read` and `search`
2631
- reach those channels only, and are refused (`forbidden`) for any other channel before any daemon
2632
- call. Conversations with persons (DM lines) are never available over MCP: a line to a person, or
2633
- `read`/`search` with one, is `forbidden`. It never calls `conv.subscribe`, `conv.markRead`,
2634
- `person.send` or `channel.*`.
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.
2635
2625
  - It has no peer-turn guard: its agent reads every request itself, with its own tools. It reports
2636
2626
  the daemon's `consent` for each item; where the daemon gives none (a task record), `self` for the
2637
2627
  session's own person and `trusted` for everyone else.
@@ -2768,7 +2758,7 @@ contacts, changes devices or administers a team or channel.
2768
2758
  `prompt` when a dialog on every daemon start is acceptable.
2769
2759
  - **Local IPC.** The sockets trust users, not processes (§8.2).
2770
2760
  - The daemon enforces two sockets: `run/agent.sock` serves only `AGENT_METHODS`, so never a line as
2771
- the person (`person.send`); `run/admin.sock` serves everything.
2761
+ the person; `run/admin.sock` serves everything. Agent conversation calls require a registered session.
2772
2762
  - Under one OS user that is no boundary: any process of the user can open `run/admin.sock`, call
2773
2763
  `team.*` and `daemon.stop`, and act for any session whose key it knows: session keys are bearer
2774
2764
  credentials. The tiers keep those methods off agent tool paths, nothing more.
@@ -2813,6 +2803,10 @@ contacts, changes devices or administers a team or channel.
2813
2803
 
2814
2804
  ## 13. Error codes
2815
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
+
2816
2810
  | Code | Meaning |
2817
2811
  |---|---|
2818
2812
  | invalid_request | malformed input or schema violation |
@@ -2825,6 +2819,7 @@ contacts, changes devices or administers a team or channel.
2825
2819
  | refused | recipient policy refuses this sender |
2826
2820
  | declined | the recipient's person declined a held request (§6.9) |
2827
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 |
2828
2823
  | rate_limited | too many envelopes; retry later |
2829
2824
  | loop_guard | agent-to-agent loop guard tripped |
2830
2825
  | too_large | size limit exceeded |
@@ -2838,9 +2833,9 @@ contacts, changes devices or administers a team or channel.
2838
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) |
2839
2834
  | roster_conflict | roster entry does not extend the current head |
2840
2835
  | invite_invalid | invite unknown, expired, revoked, used or mismatched |
2841
- | 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) |
2842
2837
  | binding_invalid | a device binding, a device-log entry or its authority failed verification (§15) |
2843
- | jelly_unavailable | Jelly ID's key set or key history could not be fetched (§15.2); never reported as `binding_invalid` |
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` |
2844
2839
  | log_conflict | a device-log entry does not extend the log's head (§15.3); resync and retry |
2845
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 |
2846
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 |
@@ -3360,11 +3355,9 @@ when a restore does not reach it: keep it on other storage than the database, or
3360
3355
  who accepts the fork on purpose moves the witness aside and starts again.
3361
3356
 
3362
3357
  **Resolution.** From each person's newest verified Jelly token the directory keeps their name, their email when
3363
- Jelly ID proved it, and from that email their **handle** and **org** (`emailHandle`, `emailOrg`):
3358
+ Jelly ID proved it, and from that email their **handle** and **org** (`hostedHandle`, `emailOrg`):
3364
3359
 
3365
- - the handle is the email's local part lower-cased, each run of characters outside `[a-z0-9]` made one `-`, `-`
3366
- trimmed from both ends, cut to 30 characters (trimmed again), and `u-` put in front when it does not start with
3367
- a letter;
3360
+ - the handle uses `hostedHandle`, exactly as §15.8 defines it;
3368
3361
  - the org is `domain:<d>` when the email's domain `d` is in the operator's `orgDomains` (`--org-domains`), and
3369
3362
  none otherwise: such a person is an individual, in no org.
3370
3363
 
@@ -3460,12 +3453,39 @@ and refuses to start if they disagree with the last tree head.
3460
3453
 
3461
3454
  ## 16. Conversations
3462
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
+
3463
3483
  A conversation is one hash-chained log of signed events that the relay sequences and members verify. Membership,
3464
3484
  roles, epochs and settings are a function of the log. Message bodies are sealed under per-device sender keys, so a
3465
3485
  post is one ciphertext whatever the number of members. The schemas are in `schema/conversation.ts`, the rules in
3466
- `conversation/` of core, and the vectors in `docs/spec/vectors/conversation.json`. The relay frames, the daemon's
3467
- storage and the IPC surface that carry them are not specified yet; `docs/design/conversations.md` describes them.
3468
- Until they land, chats are as §6.8 says.
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.
3469
3489
 
3470
3490
  ### 16.1 Identifiers
3471
3491
 
@@ -3500,19 +3520,23 @@ Every event has a header and a body. The header is:
3500
3520
  - `opHead` is the last op the author applied: its `seq` and `h(seq)`, or `{0, h(0)}` before any.
3501
3521
  - `seen` is the head the author had, for gossip (§16.4).
3502
3522
  - `class` is:
3503
- - `op`: a control op in plaintext (`ConvOp`), because the relay routes and checks authority by it;
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);
3504
3525
  - `msg`: a sealed `MsgBody`, with the `epoch` and the ratchet index `idx` of its key (§16.5). `wake` (at most
3505
- `LIMITS.wakeHints` (5)) names sessions of `by.person` the event may wake; it is the only plaintext hint;
3506
- - `keys`: sender-key shares, each sealed to one device (§16.6).
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.
3507
3531
  - `sig` covers the whole event: `Ed25519(by.device, "messaging/1/conv-event\n" || JCS(event without sig))`.
3508
3532
  - An event is at most `LIMITS.convEventBytes` (256 KiB) of JCS (`too_large`). Every object is strict: an unknown
3509
3533
  member is `invalid_request`.
3510
3534
 
3511
- The relay stores and serves **entries**: `{seq, prev, event}`, where `seq` counts from 1 and `prev` is `h(seq − 1)`.
3512
- The relay does not sign entries; members verify the chain (§16.4).
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).
3513
3537
 
3514
3538
  **Ops** (`ConvOp`): `create`, `add`, `accept`, `join`, `leave`, `remove`, `role`, `seat`, `settings`,
3515
- `archive` and `rekey`.
3539
+ `archive`, `rekey`, `policy`, `meta` and `setting`.
3516
3540
 
3517
3541
  - `create` states the kind, the visibility (channels), the history policy, `retainDays` (1 to
3518
3542
  `LIMITS.retainDaysMax` (365)), the org a public channel is open to, the participants as seats `{p, role,
@@ -3523,7 +3547,13 @@ The relay does not sign entries; members verify the chain (§16.4).
3523
3547
  `none`), and accepts its invite. Only an agent's seat carries `device` or `respondTo`.
3524
3548
  - `settings` changes the visibility, history, retention or permits. `archive {on}` archives or restores.
3525
3549
  - `rekey {epoch, cause}` starts the next epoch; `cause` is `removed`, `device`, `added`, `settings` or
3526
- `schedule`.
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).
3527
3557
 
3528
3558
  ### 16.3 Message bodies
3529
3559
 
@@ -3539,17 +3569,25 @@ The plaintext of a `msg` event is one `MsgBody`, at most `LIMITS.convPlaintextBy
3539
3569
  `status` and `artifact` carry the A2A update events (§6.2) and `cancel` and `followUp` the requester's, each
3540
3570
  naming the root.
3541
3571
  - `receipt {target, stage}`: `delivered`, `handed_to_agent` or `read`.
3542
- - `meta {name?, topic?, purpose?}` and `policy {agentContext?, bridges?}`, which sets one or both (§16.8, §16.10).
3543
3572
  - `sync {sync}`: a body of the person's `self` conversation, and only there.
3544
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
+
3545
3577
  A mention is `{p}` (a participant), `{all: "channel"}` or `{all: "here"}`. An attachment names a blob, its own
3546
3578
  AES-256-GCM key and nonce, its SHA-256 and size (at most `LIMITS.blobBytes` (25 MiB)), a media type and a name.
3547
3579
 
3548
3580
  **The self conversation.** A person's `self` conversation has one participant, the person, and history `shared`.
3549
- Its `sync` bodies (`SyncBody`) are `read {cid, upTo, thread?}`, `unread {cid, from}`, `prefs {cid, notify,
3550
- muteUntil?, receipts?}`, `follow`, `save`, `draft`, `joined`, `grant` (a consent grant, §6.9, verified over its
3551
- raw value), `context {rules}` (§16.8), `localRetention {cid, days}` and `keys {cid, shares}` (at most
3552
- `LIMITS.keyShares` (512) chain keys) and `link {link}` (§16.10). The last writer wins by `seq`.
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`.
3553
3591
 
3554
3592
  ### 16.4 The hash chain and gossip
3555
3593
 
@@ -3560,9 +3598,21 @@ raw value), `context {rules}` (§16.8), `localRetention {cid, days}` and `keys {
3560
3598
  fetches up to `seen.seq` first), `consistent` when its `h(seen.seq)` is `seen.hash`, `diverged` otherwise. A
3561
3599
  divergence proves that the relay showed two members different logs; the signed event is the evidence. `seen`
3562
3600
  at or past the event's own `seq` is `invalid_request`.
3563
- - **Retention.** When the relay expires a `msg` or `keys` entry it keeps the entry's `seq`, `prev` and the digest
3564
- `SHA-256(JCS(event))`, so the chain stays verifiable (`ConvLog.applyExpired`). Ops never expire: an op head that
3565
- names an expired entry is `conv_conflict`, so a relay that withholds an op as expired is caught by the next op.
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.
3566
3616
 
3567
3617
  ### 16.5 Sender keys
3568
3618
 
@@ -3585,6 +3635,10 @@ raw value), `context {rules}` (§16.8), `localRetention {cid, days}` and `keys {
3585
3635
  of order it keeps at most `LIMITS.skippedKeys` (1000) derived but unused keys per (epoch, sender device),
3586
3636
  dropping the lowest indexes first; a message further than that past the chain is `too_large`, and one whose key
3587
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.
3588
3642
  - **Epochs.** A `rekey` op starts the next epoch. Each sender then starts a new chain and shares it only with the
3589
3643
  devices of the participant persons at the rekey's `seq`.
3590
3644
  - Authenticity comes from the event signature, not from the key: a member who holds a sender key still cannot
@@ -3593,9 +3647,9 @@ raw value), `context {rules}` (§16.8), `localRetention {cid, days}` and `keys {
3593
3647
 
3594
3648
  ### 16.6 Key shares
3595
3649
 
3596
- A `keys` event's body is `{shares: [{to: {person, device}, epk, nonce, ct}]}`, one entry per recipient device
3597
- (at most `LIMITS.keySharesPerEvent` (500); a larger share is split over several events). Each `ct` seals
3598
- `JCS({shares: [KeyShare]})`, where a `KeyShare` is `{epoch, device, idx, ck}` (at most `LIMITS.keyShares` (512)):
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)):
3599
3653
 
3600
3654
  1. Generate an ephemeral X25519 key pair; `epk` is its raw public key, `nonce` 12 random bytes.
3601
3655
  2. `shared = X25519(eph_priv, recipient.enc)`, the recipient device's key as the realm verified it (§4, §15.7).
@@ -3606,6 +3660,23 @@ A `keys` event's body is `{shares: [{to: {person, device}, epk, nonce, ct}]}`, o
3606
3660
  The AAD binds a share to its event and its recipient: moving it to another event or device makes it fail. A
3607
3661
  receiver keeps the first share for a given (epoch, device, idx); a later one with another key changes nothing.
3608
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
+
3609
3680
  ### 16.7 Authority
3610
3681
 
3611
3682
  `ConvLog.apply` checks each entry, in order, against the state after every earlier entry, and a failed check
@@ -3619,7 +3690,8 @@ changes nothing:
3619
3690
  5. `id` not already in the log (`duplicate`); `seen` before the event (`invalid_request`).
3620
3691
  6. The first entry is `create`, and there is only one (`conv_conflict`).
3621
3692
  7. For an `op`: `opHead` is the current op (`conv_conflict`); `by.agent` is absent: an agent never writes an op
3622
- (`forbidden`). For a `msg` or `keys`: `opHead` names an op of this log, or `{0, h(0)}` (`conv_conflict`).
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).
3623
3695
  8. The rules below. An author who is not a participant is `not_member`, one who lacks the authority `forbidden`, a
3624
3696
  target that is not a message of this log `not_found`, and a target in the wrong state `invalid_state`.
3625
3697
 
@@ -3639,6 +3711,9 @@ participant's.
3639
3711
  | `seat` | The target is an agent participant whose person is the author. It becomes `member`. |
3640
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`). |
3641
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`). |
3642
3717
  | any `msg` | The author's participant is a member; `epoch` is the current epoch and no rekey is due (`stale_epoch`). |
3643
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. |
3644
3719
  | `edit` | Not archived; the target is a line, not deleted, by the same participant (person and agent). |
@@ -3647,10 +3722,8 @@ participant's.
3647
3722
  | `pin` | Not archived; an admin when `pins` is `admins`; the target is not deleted; at most `LIMITS.pins` (100) pinned (`too_large`). |
3648
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`). |
3649
3724
  | `receipt` | The target is a message of the log. |
3650
- | `meta` | Not in a dm; in a channel, an admin. |
3651
- | `policy` | Not by an agent, and not in a `self` conversation. In a channel, an admin; in a dm or group, any participant person, as a vote. |
3652
3725
  | `sync` | Only in a `self` conversation, where no other body goes (`invalid_request`). |
3653
- | `keys` | The author's person is a member participant person and `by.agent` is absent. Every share goes to a participant person (`forbidden`); live, to a current device of that person (`unknown_device`). |
3726
+ | `keys` | Not an entry: §16.6 (`ConvLog.checkShare`). |
3654
3727
 
3655
3728
  **State.** A participant starts as `member`, except an agent whose person is not the author (`invited` until its
3656
3729
  person seats it) and a `guest` (`invited` until they accept). Live, adding a person of another org as anything but a
@@ -3658,9 +3731,10 @@ guest is `forbidden`. A rekey is **due** after a removal or a leave (cause `remo
3658
3731
  and a change of the history policy (`settings`); a channel does not rekey on add. While a rekey is due, every `msg`
3659
3732
  is `stale_epoch`: no message is sealed under a key a removed device holds. A `rekey` clears every due cause.
3660
3733
 
3661
- `ConvLog` keeps the state after every entry: `at(seq)` is the state of a log holding only entries 1 to `seq`. Ops
3662
- and `policy` change it; the rules that read a message body (edit, delete, reactions, pins, tasks) run only when the
3663
- caller passes the opened body, so the relay, which cannot open bodies, applies the header rules alone.
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.
3664
3738
 
3665
3739
  ### 16.8 Agents and history as context
3666
3740
 
@@ -3670,7 +3744,8 @@ policy decides whether members' agents may mine it at all.
3670
3744
  - **Policy** (`AgentContext`): `members` (members' agents may, each under its person's rules), `seats` (only
3671
3745
  agents seated in it, by their seats) or `none`. The default is `members`. In a channel an admin sets it with a
3672
3746
  `policy` body; in a dm or group each participant person votes, and the strictest vote counts (`none` <
3673
- `seats` < `members`). A person's vote goes when they leave.
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.
3674
3749
  - **Rules** (`ContextRule`): `{agent, scope, access, lookbackDays?}`, where `agent` is a session name or `*`,
3675
3750
  `scope` is `{cid}` or `{kind}` with `kind` one of `own`, `dm`, `group`, `channel.private` and
3676
3751
  `channel.public`, and `access` is `allow`, `ask` or `off`. A person holds at most `LIMITS.contextRules` (500),
@@ -3714,7 +3789,7 @@ policy decides whether members' agents may mine it at all.
3714
3789
  | `LIMITS.retainDaysDefault`, `LIMITS.retainDaysMax` | 90 days, 365 days |
3715
3790
  | `LIMITS.skippedKeys`, `LIMITS.chainMessages`, `LIMITS.chainMaxMs` | 1000, 500, 7 days |
3716
3791
  | `LIMITS.rekeyScheduleMs` | 30 days |
3717
- | `LIMITS.keyShares`, `LIMITS.keySharesPerEvent` | 512 chain keys per share, 500 devices per `keys` event |
3792
+ | `LIMITS.keyShares` | 512 chain keys per share |
3718
3793
  | `LIMITS.wakeHints`, `LIMITS.contextRules` | 5, 500 |
3719
3794
 
3720
3795
  ### 16.10 Linked channels (reserved)
@@ -3727,8 +3802,8 @@ agents from a phone. The fields below are part of the schema and are validated,
3727
3802
  signed with the event, so every member sees it. Only a person writes it (`forbidden` on an agent's line, §16.7).
3728
3803
  The person's own devices also check it against the person's links (`checkVia`): a link of that channel, hosted by
3729
3804
  the authoring device, in force. Other members cannot see the links and do not check that.
3730
- - **`link`**, a body of the person's `self` conversation: `{id, channel, account, host, proof, authority, linkedAt,
3731
- revokedAt?}`.
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?}`.
3732
3807
  - `account` is the channel's id for the person's account; it is never in the device log, the directory or
3733
3808
  presence.
3734
3809
  - `host` is the device that runs the bridge.
@@ -3736,9 +3811,71 @@ agents from a phone. The fields below are part of the schema and are validated,
3736
3811
  person matched on both sides.
3737
3812
  - `authority` is a non-empty subset of `chat` and `view` (§8.11), and never more.
3738
3813
  - For each `id` the record written last is the one in force, unless it has `revokedAt` (`currentLinks`).
3739
- - **`bridges`** in `policy`: who may mirror the conversation to a linked channel: `none` (the default), `admins` or
3814
+ - **`bridges`** in the `policy` op: who may mirror the conversation to a linked channel: `none` (the default), `admins` or
3740
3815
  `members`. In a channel an admin sets it. In a dm or group the strictest vote of the participant persons counts,
3741
3816
  and a person who has not voted counts as `none`, so bridging needs every participant person's vote.
3742
3817
  - **Reader ids** (`ReaderId`): a person, or `ext:<channel>:<link id>` for a link's channel provider. They are not on
3743
3818
  the wire; the audience guard's reader sets use them.
3744
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.