@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
@@ -16,6 +16,8 @@ import { Message, TaskArtifactUpdateEvent, TaskStatusUpdateEvent, Part } from ".
16
16
  import { Base64Url, ChannelName, EncKey, Hash, Key32, LIMITS, MachineId, Millis, PersonId, PROTOCOL, SessionName, Ulid } from "./common.js";
17
17
  import { DelegationChain, DelegationGrant } from "./delegation.js";
18
18
  import { RealmId } from "./realm.js";
19
+ import { ActivationProof } from "./contact-request.js";
20
+ import { GuestInvitation } from "./guest-invitation.js";
19
21
  // ---- identifiers (spec §16.1) ----
20
22
  /** A person, or one agent of a person (`person/agent`). Every agent belongs to one person. */
21
23
  export const ParticipantId = z
@@ -44,6 +46,17 @@ const Sig = z.string().regex(/^[A-Za-z0-9_-]{86}$/, "Ed25519 signature, base64ur
44
46
  /** An AES-GCM nonce: 12 bytes. */
45
47
  const Nonce = z.string().regex(/^[A-Za-z0-9_-]{16}$/, "12-byte nonce, base64url");
46
48
  const isAgent = (p) => p.includes("/");
49
+ // ---- linked channels (spec §16.10; reserved: validated, no daemon behaviour yet) ----
50
+ /** An outside channel a person may link as an alias of themselves. Telegram first, then Slack. */
51
+ export const LinkChannel = z.enum(["telegram", "slack"]);
52
+ /** What a linked channel may do: Jelly's `chat` and `view` action classes (spec §8.11), never more. */
53
+ export const LinkAuthority = z.enum(["chat", "view"]);
54
+ /** Who may mirror a conversation to a linked channel: nobody (the default), its owners and admins, or any member. */
55
+ export const BridgePolicy = z.enum(["none", "admins", "members"]);
56
+ /** The kinds of a `self` conversation's `setting` op: the person's settings, which never expire (spec §16.3). */
57
+ export const SettingKind = z.enum(["prefs", "grant", "context", "localRetention", "link"]);
58
+ /** A sealed op content: base64url(nonce ‖ AES-256-GCM ciphertext ‖ tag), at most a share's size. */
59
+ const SealedState = Base64Url.min(38).max(Math.ceil(((LIMITS.convPlaintextBytes + 28) * 4) / 3));
47
60
  // ---- ops (spec §16.2, "Ops") ----
48
61
  /** A participant record as an op states it. `device` and `respondTo` are for agent participants only. */
49
62
  export const Seat = z
@@ -65,9 +78,13 @@ export const ConvOp = z.discriminatedUnion("op", [
65
78
  pins: Permit.optional(),
66
79
  /** JCS(`ConvMeta`) sealed under the creator's epoch-0 message key 0 (spec §16.5). */
67
80
  sealedMeta: Base64Url.max(2048).optional(),
81
+ /** External human DM: no content or shares until the permanent activation proof. */
82
+ admission: z.literal("request").optional(),
68
83
  }),
69
84
  z.strictObject({ op: z.literal("add"), participants: z.array(Seat).min(1).max(LIMITS.convOpParticipants) }),
70
- z.strictObject({ op: z.literal("accept") }),
85
+ z.strictObject({ op: z.literal("accept"), invitation: GuestInvitation.optional() }),
86
+ z.strictObject({ op: z.literal("activate"), proof: ActivationProof }),
87
+ z.strictObject({ op: z.literal("request.complete"), request: Ulid, seq: z.number().int().min(2), digest: Hash }),
71
88
  z.strictObject({ op: z.literal("join") }),
72
89
  z.strictObject({ op: z.literal("leave") }),
73
90
  z.strictObject({ op: z.literal("remove"), participants: z.array(ParticipantId).min(1).max(LIMITS.convOpParticipants) }),
@@ -83,7 +100,19 @@ export const ConvOp = z.discriminatedUnion("op", [
83
100
  pins: Permit.optional(),
84
101
  }),
85
102
  z.strictObject({ op: z.literal("archive"), on: z.boolean() }),
86
- z.strictObject({ op: z.literal("rekey"), epoch: z.number().int().min(1), cause: RekeyCause }),
103
+ z.strictObject({ op: z.literal("rekey"), epoch: z.number().int().min(1), cause: RekeyCause, device: MachineId.optional(), until: Seq.optional() })
104
+ .refine((op) => op.cause === "device" ? op.device !== undefined : op.device === undefined && op.until === undefined, { message: "only a device rekey names a device and optional cutoff" }),
105
+ /**
106
+ * The agent-context policy and the linked-channel `bridges` policy (spec §16.8, §16.10), in plaintext: an op, so it
107
+ * never expires, and a joiner who holds no earlier keys still knows it. In a dm or a group, the author's vote.
108
+ */
109
+ z
110
+ .strictObject({ op: z.literal("policy"), agentContext: AgentContext.optional(), bridges: BridgePolicy.optional() })
111
+ .refine((p) => p.agentContext !== undefined || p.bridges !== undefined, { message: "a policy sets agentContext, bridges or both" }),
112
+ /** A group's or channel's name, topic and purpose: JCS(`ConvMeta`) sealed under the author's key (`epoch`, `idx`). */
113
+ z.strictObject({ op: z.literal("meta"), epoch: Seq, idx: Seq, sealed: SealedState }),
114
+ /** A setting of the person's `self` conversation: JCS(`SettingBody` of `kind`) sealed under (`epoch`, `idx`). */
115
+ z.strictObject({ op: z.literal("setting"), kind: SettingKind, epoch: Seq, idx: Seq, sealed: SealedState }),
87
116
  ]);
88
117
  /** The plaintext of `create.sealedMeta` and of a `meta` body's fields. */
89
118
  export const ConvMeta = z.strictObject({
@@ -110,13 +139,6 @@ export const Attachment = z.strictObject({
110
139
  name: z.string().max(255),
111
140
  thumb: Base64Url.max(16_384).optional(),
112
141
  });
113
- // ---- linked channels (spec §16.10; reserved: validated, no daemon behaviour yet) ----
114
- /** An outside channel a person may link as an alias of themselves. Telegram first, then Slack. */
115
- export const LinkChannel = z.enum(["telegram", "slack"]);
116
- /** What a linked channel may do: Jelly's `chat` and `view` action classes (spec §8.11), never more. */
117
- export const LinkAuthority = z.enum(["chat", "view"]);
118
- /** Who may mirror a conversation to a linked channel: nobody (the default), its owners and admins, or any member. */
119
- export const BridgePolicy = z.enum(["none", "admins", "members"]);
120
142
  /** A line's provenance when its person wrote it from a linked channel. Signed and sealed with the event. */
121
143
  export const Via = z.strictObject({ channel: LinkChannel });
122
144
  /**
@@ -179,10 +201,24 @@ export const ContextRules = z
179
201
  export const KeyShare = z.strictObject({ epoch: Seq, device: MachineId, idx: Seq, ck: Key32 });
180
202
  /** The plaintext of one recipient's sealed share in a `keys` event. */
181
203
  export const KeySharePayload = z.strictObject({ shares: z.array(KeyShare).min(1).max(LIMITS.keyShares) });
182
- /** The `self` conversation's bodies (spec §16.3, "The self conversation"). The last writer wins by `seq`. */
204
+ /**
205
+ * The `self` conversation's passing state (spec §16.3, "The self conversation"), in `sync` message bodies: it expires
206
+ * with the conversation's retention. The last writer wins by `seq`.
207
+ */
183
208
  export const SyncBody = z.discriminatedUnion("s", [
184
209
  z.strictObject({ s: z.literal("read"), cid: ConvId, upTo: Seq, thread: Ulid.optional() }),
185
210
  z.strictObject({ s: z.literal("unread"), cid: ConvId, from: z.number().int().min(1) }),
211
+ z.strictObject({ s: z.literal("follow"), cid: ConvId, thread: Ulid, on: z.boolean() }),
212
+ z.strictObject({ s: z.literal("save"), cid: ConvId, target: Ulid, on: z.boolean() }),
213
+ z.strictObject({ s: z.literal("draft"), cid: ConvId, thread: Ulid.optional(), text: z.string().max(8000) }),
214
+ z.strictObject({ s: z.literal("joined"), cid: ConvId, on: z.boolean() }),
215
+ z.strictObject({ s: z.literal("keys"), cid: ConvId, shares: z.array(KeyShare).min(1).max(LIMITS.keyShares) }),
216
+ ]);
217
+ /**
218
+ * The person's settings, each sealed in a `setting` op of their `self` conversation (spec §16.3): ops never expire,
219
+ * so a setting outlives the retention. `s` is the op's `kind`. The last writer wins by `seq`.
220
+ */
221
+ export const SettingBody = z.discriminatedUnion("s", [
186
222
  z.strictObject({
187
223
  s: z.literal("prefs"),
188
224
  cid: ConvId,
@@ -191,15 +227,10 @@ export const SyncBody = z.discriminatedUnion("s", [
191
227
  /** dms only; absent is off. */
192
228
  receipts: z.boolean().optional(),
193
229
  }),
194
- z.strictObject({ s: z.literal("follow"), cid: ConvId, thread: Ulid, on: z.boolean() }),
195
- z.strictObject({ s: z.literal("save"), cid: ConvId, target: Ulid, on: z.boolean() }),
196
- z.strictObject({ s: z.literal("draft"), cid: ConvId, thread: Ulid.optional(), text: z.string().max(8000) }),
197
- z.strictObject({ s: z.literal("joined"), cid: ConvId, on: z.boolean() }),
198
230
  /** A consent grant (spec §6.9), verified with `verifyConsentGrant` over this raw value. */
199
231
  z.strictObject({ s: z.literal("grant"), grant: z.record(z.string(), z.unknown()) }),
200
232
  z.strictObject({ s: z.literal("context"), rules: ContextRules }),
201
233
  z.strictObject({ s: z.literal("localRetention"), cid: ConvId, days: RetainDays }),
202
- z.strictObject({ s: z.literal("keys"), cid: ConvId, shares: z.array(KeyShare).min(1).max(LIMITS.keyShares) }),
203
234
  z.strictObject({ s: z.literal("link"), link: LinkRecord }),
204
235
  ]);
205
236
  /** The plaintext of a `msg` event. */
@@ -228,15 +259,6 @@ export const MsgBody = z.discriminatedUnion("t", [
228
259
  z.strictObject({ t: z.literal("pin"), target: Ulid, on: z.boolean() }),
229
260
  z.strictObject({ t: z.literal("task"), task: TaskEventBody }),
230
261
  z.strictObject({ t: z.literal("receipt"), target: Ulid, stage: ReceiptStage }),
231
- z.strictObject({
232
- t: z.literal("meta"),
233
- name: ChannelName.optional(),
234
- topic: z.string().max(250).optional(),
235
- purpose: z.string().max(250).optional(),
236
- }),
237
- z
238
- .strictObject({ t: z.literal("policy"), agentContext: AgentContext.optional(), bridges: BridgePolicy.optional() })
239
- .refine((p) => p.agentContext !== undefined || p.bridges !== undefined, { message: "a policy sets agentContext, bridges or both" }),
240
262
  z.strictObject({ t: z.literal("sync"), sync: SyncBody }),
241
263
  ]);
242
264
  // ---- events and the log (spec §16.2) ----
@@ -266,6 +288,8 @@ export const ConvMsgEvent = z.strictObject({
266
288
  idx: Seq,
267
289
  /** Own-person wake hint only: sessions of `by.person` that this event may wake. */
268
290
  wake: z.array(SessionName).min(1).max(LIMITS.wakeHints).optional(),
291
+ /** The accepted request reservation. Only a sealed request.import can use it. */
292
+ request: Ulid.optional(),
269
293
  body: SealedBody,
270
294
  sig: Sig,
271
295
  });
@@ -276,17 +300,27 @@ export const SealedShare = z.strictObject({
276
300
  nonce: Nonce,
277
301
  ct: Base64Url.max(Math.ceil(((LIMITS.keyShares * 160 + 16) * 4) / 3)),
278
302
  });
279
- export const KeysBody = z.strictObject({
280
- shares: z
281
- .array(SealedShare)
282
- .min(1)
283
- .max(LIMITS.keySharesPerEvent)
284
- .refine((shares) => new Set(shares.map((s) => s.to.device)).size === shares.length, { message: "one share per device" }),
285
- });
286
- /** Sender-key and history shares, sealed pairwise; the relay serves each share only to its recipient. */
303
+ /** A `keys` event's body: one share, to one recipient device. */
304
+ export const KeysBody = SealedShare;
305
+ /**
306
+ * A sender-key or history share to one recipient device (spec §16.6). It is signed like every event but is not an
307
+ * entry of the log: the relay keeps it for its recipient alone and serves it to no one else (`ConvShareEntry`).
308
+ */
287
309
  export const ConvKeysEvent = z.strictObject({ ...header, class: z.literal("keys"), body: KeysBody, sig: Sig });
288
- export const ConvEvent = z.discriminatedUnion("class", [ConvOpEvent, ConvMsgEvent, ConvKeysEvent]);
310
+ /** The events of the log: ops and messages. */
311
+ export const ConvEvent = z.discriminatedUnion("class", [ConvOpEvent, ConvMsgEvent]);
312
+ /** Every signed conversation event: the log's, and key shares. */
313
+ export const ConvAnyEvent = z.discriminatedUnion("class", [ConvOpEvent, ConvMsgEvent, ConvKeysEvent]);
289
314
  /** An event as the relay stores and serves it. The relay does not sign it; members verify the chain. */
290
315
  export const ConvLogEntry = z.strictObject({ seq: z.number().int().min(1), prev: Hash, event: ConvEvent });
316
+ /**
317
+ * A `msg` entry whose body the relay pruned after the retention (spec §16.4): its seq, its prev and the digest
318
+ * SHA-256(JCS(event)), so the chain stays verifiable (`ConvLog.applyExpired`). An op is never served this way.
319
+ */
320
+ export const ConvExpiredEntry = z.strictObject({ seq: z.number().int().min(2), prev: Hash, digest: Hash });
321
+ /** What `conv.fetch` serves for one seq: the entry, or its digest once expired. */
322
+ export const ConvServedEntry = z.union([ConvLogEntry, ConvExpiredEntry]);
323
+ /** A key share as the relay serves it to its recipient: `n` orders the recipient's shares of one conversation. */
324
+ export const ConvShareEntry = z.strictObject({ n: z.number().int().min(1), event: ConvKeysEvent });
291
325
  /** A log position: a seq and h(seq). */
292
326
  export const ConvHead = Position;
@@ -5,11 +5,12 @@
5
5
  */
6
6
  import { z } from "../../vendor/zod/index.js";
7
7
  import { Message, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from "./a2a.js";
8
- import { AgentKind, Base64Url, ChannelName, DeliveryTier, MachineId, MachineLabel, Millis, PersonId, PROTOCOL, SessionAddress, SessionName, SessionState, Ulid, LIMITS, } from "./common.js";
8
+ import { AgentKind, Base64Url, DeliveryTier, MachineId, MachineLabel, Millis, PersonId, PROTOCOL, SessionAddress, SessionName, SessionState, Ulid, LIMITS, } from "./common.js";
9
9
  import { CardData, CardGetData, GrantSync } from "./consent.js";
10
10
  import { DelegationChain } from "./delegation.js";
11
11
  import { ErrorCode } from "./errors.js";
12
12
  import { Anchor, RealmId } from "./realm.js";
13
+ import { PortableRequest } from "./contact-request.js";
13
14
  export const EnvelopeType = z.enum(["message", "task.status", "task.artifact", "task.cancel", "receipt", "presence"]);
14
15
  export const SEAL_ALG = "x25519-hkdf-sha256-a256gcm";
15
16
  /**
@@ -62,22 +63,6 @@ export const Envelope = z.object({
62
63
  sig: Base64Url,
63
64
  });
64
65
  // ---- Decrypted body payloads, by envelope type ----
65
- /** A channel's persons: roster person names, or hosted `p_…` ids in a directory realm (spec §6.8). */
66
- const ChannelPersons = z.array(PersonId).min(1).max(LIMITS.channelPersons);
67
- /**
68
- * A channel change or post (spec §6.8), on a `kind: "message"` envelope sealed to a device with no recipient agent.
69
- * `create`: the full channel record with the person who created it, to every member device and to each later
70
- * invitee. `invite`: persons added, to the existing members. `leave`: the sender left. `remove`: persons removed by
71
- * the creator or an admin, to every member device. `post`: a line in the channel. `name` is always the channel's own
72
- * name. A DM line has no `conv` (spec §5, "Meaning").
73
- */
74
- export const ChannelConv = z.discriminatedUnion("op", [
75
- z.object({ kind: z.literal("channel"), op: z.literal("create"), id: Ulid, name: ChannelName, persons: ChannelPersons, creator: PersonId }),
76
- z.object({ kind: z.literal("channel"), op: z.literal("invite"), id: Ulid, name: ChannelName, persons: ChannelPersons }),
77
- z.object({ kind: z.literal("channel"), op: z.literal("leave"), id: Ulid, name: ChannelName }),
78
- z.object({ kind: z.literal("channel"), op: z.literal("remove"), id: Ulid, name: ChannelName, persons: ChannelPersons }),
79
- z.object({ kind: z.literal("channel"), op: z.literal("post"), id: Ulid, name: ChannelName }),
80
- ]);
81
66
  /**
82
67
  * Sync between two devices of one person (spec §6.9, "Sync"): `grant` carries grants the person set or re-signed,
83
68
  * `snapshot` every current grant, for a device of the person that is new to the sender. The message carries exactly
@@ -88,14 +73,14 @@ export const SelfConv = z.object({ kind: z.literal("self"), what: z.enum(["grant
88
73
  * A message request (spec §6.9, "Message requests"), on an envelope of class `request` from a person to a person:
89
74
  * its message carries exactly one text part of at most `LIMITS.requestChars`.
90
75
  */
91
- export const RequestConv = z.object({ kind: z.literal("request") });
76
+ export const RequestConv = z.object({ kind: z.literal("request"), proof: PortableRequest.optional() });
92
77
  /**
93
78
  * A contact card between two persons (spec §6.9, "Contact cards"): `card` carries one data part `{card}`, the signed
94
79
  * card; `get` asks for the receiver's card and carries one data part `{get: true}`.
95
80
  */
96
81
  export const CardConv = z.object({ kind: z.literal("card"), what: z.enum(["card", "get"]) });
97
82
  /** The conversation a `message` belongs to when its ends alone do not say (spec §5, "Meaning"). */
98
- export const Conv = z.discriminatedUnion("kind", [ChannelConv, SelfConv, RequestConv, CardConv]);
83
+ export const Conv = z.discriminatedUnion("kind", [SelfConv, RequestConv, CardConv]);
99
84
  /**
100
85
  * type "message". `kind: "task"` asks the recipient to run `message` as a new A2A task;
101
86
  * the sender assigns `message.taskId` (a ULID) and `message.contextId`.
@@ -109,11 +94,7 @@ export const MessagePayload = z
109
94
  wake: z.boolean().optional(),
110
95
  /** Required on `kind: "task"`, absent otherwise: the signed chain from the root requester to this hop (spec §6.7). Receivers run `verifyChain`. */
111
96
  delegation: DelegationChain.optional(),
112
- /**
113
- * The conversation, when the ends do not say it (spec §5, "Meaning"): a channel change or post (§6.8).
114
- * `message.messageId` is a ULID shared by every copy; `message.contextId`, when set, is the ULID `messageId` of
115
- * the thread's first line.
116
- */
97
+ /** Device-addressed consent sync, portable contact request or contact card. */
117
98
  conv: Conv.optional(),
118
99
  })
119
100
  .refine((p) => (p.kind === "task" ? p.message.taskId !== undefined : true), { message: "a task message needs message.taskId" })
@@ -123,6 +104,9 @@ export const MessagePayload = z
123
104
  })
124
105
  .refine((p) => p.conv?.kind !== "request" || requestMessage(p.message), {
125
106
  message: `a message request carries one text part of 1 to ${LIMITS.requestChars} characters, and nothing else`,
107
+ })
108
+ .refine((p) => p.conv?.kind !== "request" || p.conv.proof === undefined || (p.message.messageId === p.conv.proof.descriptor.id && p.message.parts[0]?.text === p.conv.proof.opening.text), {
109
+ message: "a portable request's message id and text match its signed opening",
126
110
  })
127
111
  .refine((p) => p.conv?.kind !== "card" || cardMessage(p.message, p.conv.what), {
128
112
  message: "a card message carries one data part, the card or the request for one, and nothing else",
@@ -11,6 +11,7 @@ export const ErrorCode = z.enum([
11
11
  "refused", // recipient policy refuses this sender
12
12
  "declined", // the recipient's person declined a held request (spec §6.9)
13
13
  "request_required", // the recipient's person has not accepted this sender: a message request comes first (spec §6.9)
14
+ "peer_outdated", // the other person's devices run a Messaging from before conversations: they need to update first
14
15
  "rate_limited", // too many envelopes; retry later
15
16
  "loop_guard", // agent-to-agent loop guard tripped
16
17
  "too_large", // size limit exceeded
@@ -24,9 +25,9 @@ export const ErrorCode = z.enum([
24
25
  "guarded_turn", // a guarded peer turn tried to send outside its allowance (client-side; never on the wire)
25
26
  "roster_conflict", // roster entry does not extend the current head; resync and retry
26
27
  "invite_invalid", // unknown, expired, revoked, used or mismatched invite
27
- "no_identity", // daemon has no identity/team yet
28
+ "no_identity", // daemon has no usable team identity or bound directory device with published keys
28
29
  "binding_invalid", // a device binding, log entry or its authority failed verification (hosted realms)
29
- "jelly_unavailable", // Jelly ID's key set or key history could not be fetched; never reported as invalid
30
+ "jelly_unavailable", // Jelly ID unreachable or an unusable response for keys or sign-in; never reported as invalid
30
31
  "log_conflict", // a device-log entry does not extend the log's head; resync and retry
31
32
  "conv_conflict", // a conversation event does not extend the log, or its op head is not the current one; re-read and retry
32
33
  "stale_epoch", // a conversation message is sealed under an epoch that is not the current one, or a rekey is due; re-seal and retry
@@ -50,7 +51,7 @@ export const ErrorCode = z.enum([
50
51
  /**
51
52
  * The exit status of a messagingd or messaging-relay that refuses its store and changes nothing (spec §8.8): `EX_CONFIG`
52
53
  * of sysexits(3), for either reason. messagingd also prints one stderr line,
53
- * `messagingd: store_refused: <reason>: <why and how to start over>`. A client matches this status, the code and the
54
+ * `messagingd: store_refused: <reason>: <why and the remedy>`. A client matches this status, the code and the
54
55
  * reason, never the wording.
55
56
  */
56
57
  export const STORE_REFUSED_EXIT = 78;
@@ -0,0 +1,13 @@
1
+ /** A bounded, sealed preview. Its signature is portable; the guest accepts exactly this op head and exposure. */
2
+ import { z } from "../../vendor/zod/index.js";
3
+ import { Hash, LIMITS, MachineId, Millis, PersonId, PROTOCOL, Ulid } from "./common.js";
4
+ import { RealmId } from "./realm.js";
5
+ export const GuestInvitation = z.strictObject({
6
+ v: z.literal(PROTOCOL), realm: RealmId, cid: z.string().regex(/^c_[0-7][0-9A-HJKMNP-TV-Z]{25}$/),
7
+ id: Ulid, to: PersonId, inviter: PersonId, by: MachineId,
8
+ head: z.strictObject({ seq: z.number().int().min(1), hash: Hash }), issuedAt: Millis, expiresAt: Millis,
9
+ kind: z.enum(["channel", "group"]), history: z.enum(["shared", "none"]),
10
+ agentContext: z.enum(["members", "seats", "none"]), bridges: z.enum(["members", "admins", "none"]),
11
+ audience: z.array(z.strictObject({ p: z.string().min(1).max(100), person: PersonId, role: z.enum(["owner", "admin", "member", "guest"]), state: z.enum(["member", "invited"]) })).min(1).max(LIMITS.convParticipants),
12
+ sig: z.string().regex(/^[A-Za-z0-9_-]{86}$/),
13
+ });
@@ -14,3 +14,8 @@ export * from "./ipc.js";
14
14
  export * from "./ipc/device.js";
15
15
  export * from "./ipc/consent.js";
16
16
  export * from "./conversation.js";
17
+ export * from "./conv-frames.js";
18
+ export * from "./contact-request.js";
19
+ export * from "./guest-invitation.js";
20
+ export * from "./ipc/conversation-views.js";
21
+ export * from "./ipc/conversations.js";
@@ -3,11 +3,12 @@
3
3
  * socket refuses them, and agents never set or read a person's grants. Spread into `IPC_METHODS`.
4
4
  */
5
5
  import { z } from "../../../vendor/zod/index.js";
6
- import { ChannelName, LIMITS, MachineId, MachineLabel, Millis, PersonId, RoomRef, SessionAddress, SessionName, Ulid } from "../common.js";
6
+ import { LIMITS, MachineId, MachineLabel, Millis, PersonId, SessionAddress, SessionName, Ulid } from "../common.js";
7
+ import { ConvId, ParticipantId } from "../conversation.js";
7
8
  import { AgentMailLevel, AskLevel, CardCan, ChatLevel } from "../consent.js";
8
9
  import { Email } from "../directory-frames.js";
9
- /** A grant's subject as a person names it: a person, or a channel by its name here. */
10
- export const GrantTarget = z.union([z.strictObject({ person: PersonId }), z.strictObject({ channel: ChannelName })]);
10
+ /** A grant's subject: a person, or a verified conversation id. */
11
+ export const GrantTarget = z.union([z.strictObject({ person: PersonId }), z.strictObject({ conv: ConvId })]);
11
12
  const Levels = {
12
13
  chat: ChatLevel.optional(),
13
14
  agentMail: AgentMailLevel.optional(),
@@ -17,7 +18,7 @@ const Levels = {
17
18
  };
18
19
  /** A grant as this device holds it. A channel subject carries the channel's id and, while this person is in it, its name here. */
19
20
  export const GrantView = z.object({
20
- subject: z.union([z.object({ person: PersonId }), z.object({ channel: Ulid, name: ChannelName.optional() })]),
21
+ subject: GrantTarget,
21
22
  ...Levels,
22
23
  id: Ulid,
23
24
  ts: Millis,
@@ -61,13 +62,15 @@ export const AskRequest = z.object({
61
62
  * write to them. `name`, `email` and `org` are the realm's verified profile of the sender, never the request's text.
62
63
  */
63
64
  export const ContactRequestView = z.object({
64
- /** The envelope id. */
65
+ /** The portable request id; old unsigned requests explicitly require a resend. */
65
66
  id: Ulid,
66
67
  kind: z.literal("contact"),
68
+ cid: ConvId.optional(),
69
+ state: z.enum(["pending", "answering", "accepted", "resend"]),
67
70
  from: z.object({
68
71
  person: PersonId,
69
72
  device: MachineId,
70
- label: MachineLabel,
73
+ label: MachineLabel.optional(),
71
74
  name: z.string().max(200),
72
75
  email: z.string().max(320).optional(),
73
76
  org: z.enum(["same", "other", "none"]),
@@ -77,6 +80,7 @@ export const ContactRequestView = z.object({
77
80
  at: Millis,
78
81
  exp: Millis,
79
82
  });
83
+ export const AgentInvitationRequest = z.strictObject({ kind: z.literal("agent-invitation"), id: Ulid, cid: ConvId, from: ParticipantId, to: ParticipantId, at: Millis });
80
84
  /**
81
85
  * A contact one of this person's agents asks them to make (spec §6.9, "Message requests"): `send {to: "me", request:
82
86
  * {contact}}`. Only the person sends a message request, with `contact.request`; answering this one `decline`
@@ -91,7 +95,7 @@ export const ContactSuggestion = z.object({
91
95
  at: Millis,
92
96
  });
93
97
  /** What `request.list` lists: asks to the person's agents, message requests, and contacts their agents suggest. */
94
- export const HeldRequest = z.union([AskRequest, ContactRequestView, ContactSuggestion]);
98
+ export const HeldRequest = z.union([AskRequest, ContactRequestView, ContactSuggestion, AgentInvitationRequest]);
95
99
  /**
96
100
  * For an ask: `once` delivers it, guarded; `always` sets the sender's grant to `ask: "open"` (keeping its other
97
101
  * levels), then delivers it, guarded. For a message request: `accept` sets the sender's grant to `chat: "open"`
@@ -152,7 +156,7 @@ export const CONSENT_METHODS = {
152
156
  "request.answer.preview": {
153
157
  params: RequestAnswerParams,
154
158
  result: z.object({
155
- kind: z.enum(["message", "task", "contact", "suggestion"]),
159
+ kind: z.enum(["message", "task", "contact", "suggestion", "agent-invitation"]),
156
160
  from: z.object({ name: z.string().max(200), email: z.string().max(320).optional(), org: z.enum(["same", "other", "none"]) }),
157
161
  agent: SessionName.optional(),
158
162
  to: SessionName.optional(),
@@ -169,7 +173,7 @@ export const CONSENT_METHODS = {
169
173
  */
170
174
  "contact.request": {
171
175
  params: ContactRequestParams,
172
- result: z.object({ id: Ulid, conversation: RoomRef, person: PersonId }),
176
+ result: z.object({ id: Ulid, cid: ConvId, person: PersonId }),
173
177
  },
174
178
  /** Whether `contact.request` would find a person for `email`; counts against the directory's lookup limits. Sends nothing. */
175
179
  "contact.request.preview": { params: ContactRequestParams.partial({ text: true }), result: z.object({ found: z.boolean() }) },
@@ -0,0 +1,80 @@
1
+ /** Rendered conversation projections for the future conversation IPC cut-over.
2
+ * Peer names, text and file names are untrusted; clients must fence or sanitize them.
3
+ * These schemas do not register a method on either socket.
4
+ */
5
+ import { z } from "../../../vendor/zod/index.js";
6
+ import { LIMITS, Millis, SessionName, Ulid } from "../common.js";
7
+ import { AgentContext, ConvId, ConvKind, ConvRole, HistoryPolicy, Mention, ParticipantId, RespondTo, Visibility } from "../conversation.js";
8
+ import { TaskState } from "../a2a.js";
9
+ const Count = z.number().int().nonnegative().safe();
10
+ const Seq = z.number().int().nonnegative().safe();
11
+ const Name = z.string().max(200);
12
+ export const ParticipantView = z.object({
13
+ p: ParticipantId,
14
+ kind: z.enum(["person", "agent"]),
15
+ role: ConvRole,
16
+ state: z.enum(["member", "invited"]),
17
+ name: Name,
18
+ handle: z.string().max(64).optional(),
19
+ org: z.enum(["same", "other", "none"]),
20
+ presence: z.enum(["online", "away", "offline", "unknown"]),
21
+ agentState: z.enum(["idle", "busy", "kept"]).optional(),
22
+ respondTo: RespondTo.optional(),
23
+ });
24
+ /** Names travel with a notice so rendering it does not need one lookup per participant. */
25
+ export const NoticeView = z.object({
26
+ op: z.enum(["create", "add", "accept", "join", "leave", "remove", "role", "seat", "settings", "archive", "meta", "policy"]),
27
+ by: ParticipantView.pick({ p: true, kind: true, name: true }),
28
+ participants: z.array(ParticipantView.pick({ p: true, kind: true, name: true })).max(LIMITS.convOpParticipants).optional(),
29
+ fields: z.array(z.enum(["name", "topic", "purpose", "visibility", "history", "retainDays", "post", "invite", "pins", "agentContext", "bridges"])).max(12).optional(),
30
+ });
31
+ /** A file projection deliberately excludes the key and nonce in the encrypted wire attachment. */
32
+ export const FileView = z.object({
33
+ blob: z.string().regex(/^b_[A-Za-z0-9_-]{43}$/),
34
+ name: z.string().max(255), mime: z.string().max(100), bytes: z.number().int().positive().max(LIMITS.blobBytes),
35
+ });
36
+ export const TaskView = z.object({ id: Ulid, state: TaskState });
37
+ export const EventView = z.object({
38
+ cid: ConvId, id: Ulid, seq: z.number().int().positive().safe(), at: Millis,
39
+ author: ParticipantView.pick({ p: true, kind: true, name: true }),
40
+ kind: z.enum(["line", "task", "notice"]),
41
+ text: z.string().max(LIMITS.lineChars).optional(), format: z.enum(["plain", "md"]).optional(),
42
+ thread: Ulid.optional(), broadcast: z.boolean().optional(),
43
+ mentions: z.array(Mention).max(LIMITS.mentions).optional(), mentionsMe: z.boolean(),
44
+ files: z.array(FileView).max(LIMITS.files).optional(), edited: Millis.optional(), deleted: z.boolean().optional(),
45
+ reactions: z.array(z.object({ key: z.string().min(1).max(64), count: Count, mine: z.boolean() })).max(LIMITS.reactionsPerEvent).optional(),
46
+ pinned: z.boolean().optional(),
47
+ replies: z.object({ count: Count, lastAt: Millis, participants: z.array(ParticipantId).max(5), unread: Count.optional(), following: z.boolean().optional() }).optional(),
48
+ task: TaskView.optional(), notice: NoticeView.optional(),
49
+ delivery: z.enum(["pending", "sent", "delivered", "read", "failed"]).optional(),
50
+ clientId: z.string().min(1).max(200).optional(),
51
+ }).refine((event) => !event.deleted || (event.text === undefined && event.files === undefined), {
52
+ message: "a deleted event contains no text or files",
53
+ });
54
+ export const ConvView = z.object({
55
+ cid: ConvId, kind: ConvKind, visibility: Visibility.optional(), history: HistoryPolicy,
56
+ retainDays: z.number().int().min(1).max(LIMITS.retainDaysMax), agentContext: AgentContext,
57
+ name: z.string().max(80).optional(), topic: z.string().max(250).optional(), purpose: z.string().max(250).optional(),
58
+ archived: z.boolean(), role: ConvRole, participants: Count, dmWith: ParticipantView.optional(), last: EventView.optional(),
59
+ headSeq: Seq, readUpTo: Seq.optional(), unread: Count.optional(), mentions: Count.optional(), threadsUnread: Count.optional(),
60
+ notify: z.enum(["all", "mentions", "none"]).optional(), mutedUntil: Millis.optional(), draft: z.string().max(8000).optional(),
61
+ });
62
+ export const ActivityBody = z.object({
63
+ session: SessionName, turn: Ulid, seq: Seq,
64
+ source: z.object({ item: Ulid.optional(), cid: ConvId.optional(), task: Ulid.optional() }).optional(),
65
+ kind: z.enum(["turn_started", "step", "turn_ended", "error", "cancelled"]), summary: z.string().max(200).optional(),
66
+ });
67
+ export const ConvStreamEvent = z.discriminatedUnion("type", [
68
+ z.object({ type: z.literal("event"), pos: Seq, event: EventView }),
69
+ z.object({ type: z.literal("event.update"), pos: Seq, cid: ConvId, id: Ulid,
70
+ change: z.enum(["edit", "delete", "react", "pin", "replies", "delivery", "task"]), event: EventView }),
71
+ z.object({ type: z.literal("conv"), pos: Seq, conv: ConvView }),
72
+ z.object({ type: z.literal("member"), pos: Seq, cid: ConvId, participant: ParticipantView,
73
+ change: z.enum(["added", "invited", "removed", "role", "seat", "presence"]) }),
74
+ z.object({ type: z.literal("read"), pos: Seq, cid: ConvId, readUpTo: Seq, unread: Count, mentions: Count }),
75
+ z.object({ type: z.literal("typing"), cid: ConvId, p: ParticipantId, thread: Ulid.optional(), until: Millis }),
76
+ z.object({ type: z.literal("activity"), body: ActivityBody }),
77
+ z.object({ type: z.literal("gap"), pos: Seq }),
78
+ ]).refine((entry) => entry.type !== "event.update" || (entry.cid === entry.event.cid && entry.id === entry.event.id), {
79
+ message: "event.update names the same conversation and event as its projection",
80
+ });
@@ -0,0 +1,128 @@
1
+ /** Conversation contracts, including later lanes. Only LIVE_CONVERSATION_METHODS registers IPC. */
2
+ import { z } from "../../../vendor/zod/index.js";
3
+ import { LIMITS, MachineId, Millis, PersonId, SessionName, Ulid } from "../common.js";
4
+ import { AgentContext, Attachment, ContextAccess, ContextRules, ConvId, ConvKind, ConvMeta, ConvRole, HistoryPolicy, Mention, ParticipantId, Permit, RespondTo, Seat, Visibility } from "../conversation.js";
5
+ import { PersonSummary } from "./surfaces.js";
6
+ import { SessionKey } from "./shapes.js";
7
+ import { ActivityBody, ConvStreamEvent, ConvView, EventView, ParticipantView } from "./conversation-views.js";
8
+ const Empty = z.strictObject({});
9
+ const Ok = z.object({ ok: z.literal(true) });
10
+ const Count = z.number().int().nonnegative().safe();
11
+ const Seq = z.number().int().nonnegative().safe();
12
+ const Limit = z.number().int().min(1).max(50).optional();
13
+ const Cursor = z.string().min(1).max(512).optional();
14
+ const Key = SessionKey.optional(); // required by the daemon on an agent connection
15
+ const Cid = { cid: ConvId };
16
+ const Page = { limit: Limit, cursor: Cursor };
17
+ const Id = { cid: ConvId, id: Ulid };
18
+ const Text = z.string().min(1).max(LIMITS.lineChars);
19
+ const CommonErrors = ["invalid_request", "forbidden", "not_found", "too_large", "internal", "no_identity", "unknown_session", "grant_required", "grant_invalid", "directory_unavailable", "directory_inconsistent"];
20
+ const WriteErrors = [...CommonErrors, "invalid_state", "conv_conflict", "stale_epoch", "relay_unavailable", "rate_limited", "duplicate", "secret_refused", "refused", "declined", "request_required", "loop_guard", "unknown_device", "binding_invalid"];
21
+ const ReadErrors = [...CommonErrors, "relay_unavailable"];
22
+ const Write = (params) => ({ params, result: Ok, errors: WriteErrors });
23
+ const Read = (params, result) => ({ params, result, errors: ReadErrors });
24
+ const EventPage = z.object({ events: z.array(EventView).max(50), more: z.boolean(), cursor: Cursor });
25
+ const ConvPage = z.object({ conversations: z.array(ConvView).max(50), cursor: Cursor });
26
+ const PageParams = z.strictObject({ ...Cid, ...Page });
27
+ const Hold = z.object({ stage: z.literal("held"), code: z.enum(["context", "audience"]), expect: z.literal("on_consent") });
28
+ const LineFields = { text: Text, format: z.enum(["plain", "md"]).optional(), thread: Ulid.optional(), broadcast: z.boolean().optional(),
29
+ mentions: z.array(Mention).max(LIMITS.mentions).optional(), files: z.array(Attachment).max(LIMITS.files).optional() };
30
+ const OptionalSettings = { visibility: Visibility.optional(), history: HistoryPolicy.optional(), retainDays: z.number().int().min(1).max(LIMITS.retainDaysMax).optional(),
31
+ post: Permit.optional(), invite: Permit.optional(), pins: Permit.optional() };
32
+ /** Explicit consent evidence and unconfirmed delivery are different states. */
33
+ export const WaitingOnThemView = z.object({
34
+ id: Ulid, kind: z.enum(["message", "task", "contact"]), to: PersonId,
35
+ name: z.string().max(200), at: Millis, cid: ConvId.optional(), agent: SessionName.optional(),
36
+ state: z.enum(["held", "pending-delivery"]),
37
+ });
38
+ export const ConvListParams = z.strictObject({ key: Key, filter: z.strictObject({ unread: z.boolean().optional(), mentions: z.boolean().optional(), kind: ConvKind.optional(), archived: z.boolean().optional() }).optional(), ...Page });
39
+ export const ConvListResult = ConvPage;
40
+ export const ConvReadParams = z.strictObject({ key: Key, ...Cid, thread: Ulid.optional(), before: Seq.optional(), after: Seq.optional(), around: Ulid.optional(), limit: Limit, cursor: Cursor })
41
+ .refine((p) => [p.before, p.after, p.around, p.cursor].filter((v) => v !== undefined).length <= 1, "choose one page position");
42
+ export const ConvReadResult = z.union([EventPage, Hold]);
43
+ export const ConvSendParams = z.strictObject({ key: Key, cid: ConvId.optional(), to: ParticipantId.optional(), ...LineFields, answer: Ulid.optional(), clientId: z.string().min(1).max(200) })
44
+ .refine((p) => (p.cid === undefined) !== (p.to === undefined), "choose exactly one of cid or to")
45
+ .refine((p) => p.broadcast !== true || p.thread !== undefined, "broadcast needs a thread");
46
+ export const ConvSendResult = z.union([z.object({ cid: ConvId, id: Ulid, clientId: z.string().min(1).max(200), stage: z.literal("pending") }), Hold]);
47
+ export const ConvSyncParams = z.strictObject({ key: Key, after: Seq.optional(), waitMs: z.number().int().min(0).max(25_000).optional(), limit: Limit });
48
+ export const ConvSyncResult = z.object({ events: z.array(ConvStreamEvent).max(50), pos: Seq });
49
+ export const ConvCreateParams = z.strictObject({ kind: z.enum(["dm", "group", "channel"]), participants: z.array(Seat).min(1).max(LIMITS.convParticipants), visibility: Visibility.optional(), history: HistoryPolicy, retainDays: z.number().int().min(1).max(LIMITS.retainDaysMax), meta: ConvMeta.optional() })
50
+ .refine((p) => p.kind !== "dm" || p.participants.length === 2, "a dm has exactly two participants")
51
+ .refine((p) => p.kind !== "group" || (p.participants.length >= 3 && p.participants.length <= LIMITS.groupParticipants), "a group has 3 to 50 participants")
52
+ .refine((p) => p.kind === "channel" || p.visibility === undefined, "only a channel has visibility");
53
+ export const ConvSeatParams = z.strictObject({ ...Cid, p: ParticipantId, device: MachineId.optional(), respondTo: RespondTo.optional() })
54
+ .refine((p) => p.p.includes("/"), "a seat names an agent");
55
+ export const PeopleGetParams = z.strictObject({ people: z.array(PersonId).min(1).max(100).refine((p) => new Set(p).size === p.length, "each person once") });
56
+ export const PeopleGetResult = z.object({ people: z.array(PersonSummary).max(100), missing: z.array(PersonId).max(100) });
57
+ /** Socket tiers are declared alongside this table below. */
58
+ export const CONVERSATION_METHODS = {
59
+ "conv.list": Read(ConvListParams, ConvListResult),
60
+ "conv.get": Read(z.strictObject({ key: Key, ...Cid }), ConvView),
61
+ "conv.members": Read(PageParams, z.object({ participants: z.array(ParticipantView).max(50), cursor: Cursor })),
62
+ "conv.read": Read(ConvReadParams, ConvReadResult),
63
+ "conv.threads": Read(z.strictObject({ ...Cid, filter: z.enum(["all", "unread", "following"]).optional(), ...Page }), EventPage),
64
+ "conv.send": { params: ConvSendParams, result: ConvSendResult, errors: WriteErrors },
65
+ "conv.react": Write(z.strictObject({ key: Key, ...Id, reaction: z.string().min(1).max(64), on: z.boolean() })),
66
+ "conv.edit": Write(z.strictObject({ key: Key, ...Id, text: Text, mentions: z.array(Mention).max(LIMITS.mentions).optional() })),
67
+ "conv.delete": Write(z.strictObject({ key: Key, ...Id })),
68
+ "conv.pin": Write(z.strictObject({ ...Id, on: z.boolean() })),
69
+ "conv.create": { params: ConvCreateParams, result: z.object({ cid: ConvId }), errors: WriteErrors },
70
+ "conv.add": Write(z.strictObject({ ...Cid, participants: z.array(Seat).min(1).max(LIMITS.convOpParticipants) })),
71
+ "conv.remove": Write(z.strictObject({ ...Cid, participants: z.array(ParticipantId).min(1).max(LIMITS.convOpParticipants) })),
72
+ "conv.role": Write(z.strictObject({ ...Cid, p: ParticipantId, role: ConvRole })),
73
+ "conv.seat": Write(ConvSeatParams),
74
+ "conv.settings": Write(z.strictObject({ ...Cid, ...OptionalSettings }).refine((p) => Object.values(p).some((v, i) => i > 0 && v !== undefined), "set at least one setting")),
75
+ "conv.meta": Write(z.strictObject({ ...Cid, meta: ConvMeta })),
76
+ "conv.archive": Write(z.strictObject({ ...Cid, on: z.boolean() })),
77
+ "conv.leave": Write(z.strictObject(Cid)),
78
+ "conv.join": Write(z.strictObject(Cid)),
79
+ "conv.accept": Write(z.strictObject(Cid)),
80
+ "conv.catalogue": Read(z.strictObject({ query: z.string().max(200).optional(), limit: Limit }), z.object({ channels: z.array(z.object({ cid: ConvId, name: z.string().max(80), topic: z.string().max(250).optional(), purpose: z.string().max(250).optional(), participants: Count })).max(50) })),
81
+ "conv.publish": Write(z.strictObject({ ...Cid, on: z.boolean() })),
82
+ "conv.markRead": Write(z.strictObject({ ...Cid, upTo: Seq, thread: Ulid.optional() })),
83
+ "conv.markUnread": Write(z.strictObject({ ...Cid, from: z.number().int().positive().safe() })),
84
+ "conv.prefs": Write(z.strictObject({ ...Cid, notify: z.enum(["all", "mentions", "none"]), muteUntil: Millis.optional(), receipts: z.boolean().optional(), typing: z.boolean().optional() })),
85
+ "conv.follow": Write(z.strictObject({ ...Cid, thread: Ulid, on: z.boolean() })),
86
+ "conv.save": Write(z.strictObject({ ...Id, on: z.boolean() })),
87
+ "conv.saved": Read(PageParams, EventPage),
88
+ "conv.draft": Write(z.strictObject({ ...Cid, thread: Ulid.optional(), text: z.string().max(8000) })),
89
+ "conv.schedule": { params: z.strictObject({ ...Cid, ...LineFields, at: Millis, clientId: z.string().min(1).max(200) }), result: z.object({ id: Ulid }), errors: WriteErrors },
90
+ "conv.scheduled": Read(z.strictObject(Page), z.object({ items: z.array(z.object({ id: Ulid, cid: ConvId, at: Millis, text: Text })).max(50), cursor: Cursor })),
91
+ "conv.unschedule": Write(z.strictObject({ id: Ulid })),
92
+ "conv.typing": Write(z.strictObject({ ...Cid, thread: Ulid.optional() })),
93
+ "conv.search": Read(z.strictObject({ key: Key, query: z.string().min(1).max(200), cid: ConvId.optional(), from: ParticipantId.optional(), after: Millis.optional(), before: Millis.optional(), thread: Ulid.optional(), has: z.enum(["file", "link"]).optional(), ...Page }), z.union([EventPage, Hold])),
94
+ "conv.context": Read(z.strictObject({ key: Key, ...Cid, event: Ulid, before: z.number().int().min(0).max(20).optional(), after: z.number().int().min(0).max(20).optional() }), z.union([z.object({ events: z.array(EventView).max(41), root: EventView.optional() }), Hold])),
95
+ "conv.access": Read(z.strictObject({ key: SessionKey, cid: ConvId.optional(), ...Page }), z.object({ conversations: z.array(z.object({ conv: ConvView, reason: z.enum(["seat", "allow", "ask"]), lookbackDays: z.number().int().positive().max(LIMITS.retainDaysMax), indexedFrom: Millis.optional(), complete: z.boolean() })).max(50), cursor: Cursor })),
96
+ "context.get": Read(Empty, z.object({ rules: ContextRules, defaults: ContextRules, effective: z.array(z.object({ agent: SessionName, cid: ConvId, access: ContextAccess })).max(500) })),
97
+ "context.set": Write(z.strictObject({ rules: ContextRules })),
98
+ "context.set.preview": Read(z.strictObject({ rules: ContextRules }), z.object({ changes: z.array(z.object({ agent: SessionName, cid: ConvId, from: ContextAccess, to: ContextAccess })).max(500) })),
99
+ "conv.policy": Write(z.strictObject({ ...Cid, agentContext: AgentContext })),
100
+ "conv.localRetention": Write(z.strictObject({ ...Cid, days: z.number().int().min(1).max(LIMITS.retainDaysMax) })),
101
+ "conv.backfill": { params: z.strictObject(Cid), result: z.object({ indexedFrom: Millis.optional(), complete: z.boolean() }), errors: WriteErrors },
102
+ "file.get": Read(z.strictObject({ ...Cid, blob: z.string().regex(/^b_[A-Za-z0-9_-]{43}$/), offset: Count.optional(), limit: z.number().int().min(1).max(512 * 1024).optional() }), z.object({ data: z.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(699_052), offset: Count, more: z.boolean(), mime: z.string().max(100), name: z.string().max(255) })),
103
+ "session.activity": Write(z.strictObject({ key: SessionKey, body: ActivityBody })),
104
+ "turn.cancel": Write(z.strictObject({ device: MachineId, session: SessionName, turn: Ulid.optional() })),
105
+ "conv.sync": Read(ConvSyncParams, ConvSyncResult),
106
+ "people.get": Read(PeopleGetParams, PeopleGetResult),
107
+ "sessions.list": Read(Empty, z.object({ sessions: z.array(z.object({ agent: SessionName, state: z.enum(["idle", "busy", "kept"]), seats: z.array(ConvId) })) })),
108
+ "request.waiting": Read(z.strictObject(Page), z.object({ items: z.array(WaitingOnThemView).max(50), cursor: Cursor })),
109
+ };
110
+ /** Staged contracts have no live handler or socket tier until their owning lane ships. */
111
+ export const STAGED_CONVERSATION_METHODS = [
112
+ "conv.catalogue", "conv.publish", "conv.markRead", "conv.markUnread", "conv.prefs", "conv.follow",
113
+ "conv.save", "conv.saved", "conv.draft", "conv.schedule", "conv.scheduled", "conv.unschedule", "conv.typing",
114
+ "context.get", "context.set", "context.set.preview", "conv.localRetention", "file.get", "session.activity", "turn.cancel",
115
+ ];
116
+ export const LIVE_CONVERSATION_METHODS = Object.fromEntries(Object.entries(CONVERSATION_METHODS)
117
+ .filter(([name]) => !STAGED_CONVERSATION_METHODS.includes(name)));
118
+ /** agent means both sockets, human means admin socket only. session.activity is runtime-only, not a model tool. */
119
+ export const CONVERSATION_TIERS = {
120
+ "conv.list": "agent", "conv.get": "human", "conv.members": "human", "conv.read": "agent", "conv.threads": "human", "conv.send": "agent",
121
+ "conv.react": "agent", "conv.edit": "agent", "conv.delete": "agent", "conv.pin": "human", "conv.create": "human", "conv.add": "human",
122
+ "conv.remove": "human", "conv.role": "human", "conv.seat": "human", "conv.settings": "human", "conv.meta": "human", "conv.archive": "human",
123
+ "conv.leave": "human", "conv.join": "human", "conv.accept": "human", "conv.catalogue": "human", "conv.publish": "human", "conv.markRead": "human",
124
+ "conv.markUnread": "human", "conv.prefs": "human", "conv.follow": "human", "conv.save": "human", "conv.saved": "human", "conv.draft": "human",
125
+ "conv.schedule": "human", "conv.scheduled": "human", "conv.unschedule": "human", "conv.typing": "human", "conv.search": "agent", "conv.context": "agent",
126
+ "conv.access": "agent", "context.get": "human", "context.set": "human", "context.set.preview": "human", "conv.policy": "human", "conv.localRetention": "human",
127
+ "conv.backfill": "human", "file.get": "human", "session.activity": "agent", "turn.cancel": "human", "conv.sync": "agent", "people.get": "human", "request.waiting": "human", "sessions.list": "human",
128
+ };
@@ -67,6 +67,8 @@ export const DeviceListEntry = z.object({
67
67
  via: z.enum(["host", "browser"]).optional(),
68
68
  current: z.boolean(),
69
69
  self: z.boolean(),
70
+ /** This local Jelly host has no attached person window. Unknown remote presence is omitted. */
71
+ away: z.literal(true).optional(),
70
72
  since: Millis.optional(),
71
73
  /** For a removed device of a directory realm: when, why and by which device. */
72
74
  unbound: z.object({ at: Millis, reason: UnbindReason, by: MachineId }).optional(),