@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
@@ -2,8 +2,7 @@
2
2
  * The IPC methods a Jelly host needs (spec §8.2, §8.11; design §13.6, §13.10-§13.12), served in every deployment:
3
3
  *
4
4
  * - Agent tier: `daemon.health`, a cheap status with no secrets and no counts.
5
- * - Human tier: `agents.list` and `agents.read` (the person's view of their own agents' traffic on this device),
6
- * `notice.subscribe`, `notice.list` and `notice.ack` (notices in fixed categories), and the read-only preview
5
+ * - Human tier: `notice.subscribe`, `notice.list` and `notice.ack` (notices in fixed categories), and the read-only preview
7
6
  * `device.remove.preview`.
8
7
  *
9
8
  * Spread into `IPC_METHODS`; `HOST_SCOPE_METHODS` (in `ipc.ts`) is what a host-scope action grant allows.
@@ -14,8 +13,6 @@ import { WAKE_AGENTS, WAKE_TICKET_BYTES, WakeUrl } from "../directory-frames.js"
14
13
  import { Jws } from "../jelly.js";
15
14
  import { DeviceKind } from "../directory.js";
16
15
  import { DeviceChangeView, DeviceRemoveParams } from "./device.js";
17
- import { ConvCursor, ConvEntry } from "./shapes.js";
18
- import { ConversationRef } from "./surfaces.js";
19
16
  /**
20
17
  * A notice's id, stable per source: `m_<line id>` (a line), `r_<request id>` (a held request), `a_<id>` (an alert).
21
18
  * A replay repeats the same ids, so a host rings once.
@@ -46,14 +43,13 @@ export const NoticeDevice = DeviceChangeView.extend({
46
43
  export const Notice = z.object({
47
44
  id: NoticeId,
48
45
  category: z.enum(["message", "request", "alert"]),
49
- conversation: ConversationRef.optional(),
50
46
  from: z.object({ name: z.string().max(200), email: z.string().max(320).optional(), org: z.enum(["same", "other", "none"]) }).optional(),
51
47
  line: z.string().max(NOTICE_LINE_CHARS).optional(),
52
48
  device: NoticeDevice.optional(),
53
49
  at: Millis,
54
50
  });
55
- /** What the person's badges count: unread lines in their conversations, and requests held for them. */
56
- export const NoticeCounts = z.object({ unread: z.number().int().nonnegative(), requests: z.number().int().nonnegative() });
51
+ /** What the person's badges count: requests held for them. Conversation read state remains staged. */
52
+ export const NoticeCounts = z.object({ requests: z.number().int().nonnegative() });
57
53
  /** `daemon.health` (spec §8.2): the daemon's state, with no secrets and no counts. */
58
54
  export const DaemonHealth = z.object({
59
55
  state: z.enum(["starting", "running", "stopping"]),
@@ -66,43 +62,12 @@ export const DaemonHealth = z.object({
66
62
  version: z.string().max(64),
67
63
  storeSchema: z.number().int().positive(),
68
64
  });
69
- /** One local agent (a registered or kept session) and its traffic on this device. */
70
- export const AgentSummary = z.object({
71
- agent: SessionName,
72
- /** Other agents it has mail or tasks with. */
73
- conversations: z.number().int().nonnegative(),
74
- /** Mail it received and has not read. */
75
- unread: z.number().int().nonnegative(),
76
- lastAt: Millis.optional(),
77
- state: z.enum(["idle", "busy", "kept"]),
78
- });
79
- const AgentConversation = z.object({ conversation: ConversationRef, unread: z.number().int().nonnegative(), lastAt: Millis });
80
65
  export const JELLY_METHODS = {
81
66
  /**
82
67
  * Agent tier. A cheap status for a host that polls: the daemon's state, the relay, the directory, an alarm, and
83
68
  * whether it is busy. No counts: they are the person's (`notice.list`).
84
69
  */
85
70
  "daemon.health": { params: z.strictObject({}), result: DaemonHealth },
86
- /** Human tier. This person's local agents on this device, most recent first. Read-only. */
87
- "agents.list": { params: z.strictObject({}), result: z.object({ agents: z.array(AgentSummary) }) },
88
- /**
89
- * Human tier. One local agent's traffic: without `conversation`, the other agents it has mail or tasks with, most
90
- * recent first; with `conversation` (`agent:<person>/<agent>`), a page of its entries, as `conv.read` gives them to
91
- * the agent, latest first unless `before` names an entry. Changes no stage and sends no receipt. Peer text is
92
- * untrusted: a client cleans it for display.
93
- */
94
- "agents.read": {
95
- params: z.strictObject({
96
- agent: SessionName,
97
- conversation: ConversationRef.refine((c) => c.startsWith("agent:"), { message: "a conversation with an agent: agent:<person>/<agent>" }).optional(),
98
- before: ConvCursor.optional(),
99
- limit: z.number().int().min(1).max(100).optional(),
100
- }),
101
- result: z.discriminatedUnion("kind", [
102
- z.object({ kind: z.literal("conversations"), conversations: z.array(AgentConversation) }),
103
- z.object({ kind: z.literal("entries"), conversation: ConversationRef, entries: z.array(ConvEntry), more: z.boolean() }),
104
- ]),
105
- },
106
71
  /**
107
72
  * Human tier; host scope in `deployment: "jelly"`. Replays the unacknowledged notices after `after` (all of them
108
73
  * without it), oldest first, as `notice` events on this connection, then sends each new one. With `lines`, a
@@ -5,7 +5,8 @@
5
5
  * imports `ipc.ts` (which spreads their tables).
6
6
  */
7
7
  import { z } from "../../../vendor/zod/index.js";
8
- import { Task, TaskState } from "../a2a.js";
8
+ import { Task } from "../a2a.js";
9
+ import { ConvId, ParticipantId } from "../conversation.js";
9
10
  import { AgentKind, DeliveryStage, DeliveryTier, MachineId, MachineLabel, Millis, PersonId, SessionAddress, SessionName, SessionState, Ulid, LIMITS, } from "../common.js";
10
11
  import { DelegationSummary } from "../delegation.js";
11
12
  import { ErrorCode } from "../errors.js";
@@ -41,42 +42,10 @@ export const DirectoryEntry = z.object({
41
42
  self: z.boolean().describe("true for sessions registered on this machine"),
42
43
  /**
43
44
  * When a message sent to this session now, with the wake hint set, would likely be seen: the
44
- * same `SendExpect` a `msg.send` result carries (spec §8.2). Absent when no presence lists it.
45
+ * same `SendExpect` a task send result carries (spec §8.2). Absent when no presence lists it.
45
46
  */
46
47
  expect: SendExpect.optional(),
47
48
  });
48
- /** Chat text, at most `LIMITS.chatChars` UTF-16 code units (`max` would count code points). */
49
- export const ChatText = z.string().refine((text) => text.length <= LIMITS.chatChars, { message: `at most ${LIMITS.chatChars} UTF-16 code units` });
50
- /**
51
- * A position in this machine's chat order (spec §6.8): every filed line and every chat change takes
52
- * the next one. Local: another machine numbers the same line differently. 0 is before the first.
53
- */
54
- export const ChatSeq = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
55
- /** One transcript line (spec §6.8). `id` is the line's `messageId`, the same on every machine. */
56
- export const ChatLine = z.object({
57
- id: Ulid,
58
- /** This machine's filing order of the line: the cursor for `conv.read` paging and `conv.markRead`. */
59
- seq: ChatSeq,
60
- at: Millis,
61
- person: PersonId,
62
- /** The sending agent, when an agent posted it; absent on the person's own line. */
63
- agent: SessionName.optional(),
64
- direction: z.enum(["in", "out"]),
65
- body: ChatText,
66
- /** The `id` of the thread's first line; absent on a first line. */
67
- thread: Ulid.optional(),
68
- /**
69
- * On a line this machine sent to other persons (spec §6.8): `sent` until every person has a
70
- * delivered copy, `delivered` then, `refused` once some person's every copy was rejected or expired.
71
- * `held` while every person still to deliver it holds it for their person to answer: a message request
72
- * waiting to be accepted (spec §6.9).
73
- */
74
- state: z.enum(["sent", "held", "delivered", "refused"]).optional(),
75
- /** With `refused`: the persons whose every copy was rejected or expired. */
76
- refused: z.array(PersonId).max(LIMITS.channelPersons).optional(),
77
- /** A line this daemon wrote about a channel change or a refusal of one; its text is the daemon's, never a peer's. */
78
- notice: z.literal(true).optional(),
79
- });
80
49
  /** The other end of an item or a task: a person's device and, when an agent wrote it, the agent. */
81
50
  export const PeerRef = z.object({
82
51
  address: SessionAddress,
@@ -86,9 +55,12 @@ export const PeerRef = z.object({
86
55
  label: MachineLabel,
87
56
  });
88
57
  export const InboxItem = z.object({
89
- id: Ulid.describe("envelope id"),
58
+ id: Ulid.describe("task envelope id, or conversation event id"),
90
59
  key: SessionKey,
91
- from: PeerRef,
60
+ from: PeerRef.extend({ address: z.union([SessionAddress, ParticipantId]), device: MachineId.optional(), label: MachineLabel.optional() }),
61
+ /** A projected line is addressed by this pair. Task envelopes have neither field. */
62
+ cid: ConvId.optional(),
63
+ eventId: Ulid.optional(),
92
64
  kind: z.enum(["message", "task"]),
93
65
  messageId: z.string().min(1).max(128),
94
66
  taskId: Ulid.optional(),
@@ -108,10 +80,11 @@ export const InboxItem = z.object({
108
80
  * Absent only on items stored before consent.
109
81
  */
110
82
  consent: z.enum(["self", "guarded", "trusted"]).optional(),
111
- });
83
+ }).refine((item) => item.cid !== undefined
84
+ ? item.eventId === item.id && item.kind === "message" && item.taskId === undefined
85
+ : item.eventId === undefined && PeerRef.safeParse(item.from).success, "a projected line has a matching cid/eventId; an envelope has device provenance");
112
86
  /**
113
- * A message this side sent to continue a task it requested (`msg.send` with `kind: "message"`
114
- * and the `taskId`), and how far it got, from this daemon's own delivery records (spec §6.3).
87
+ * A task continuation and its stage from this daemon's own delivery records (spec §6.3).
115
88
  */
116
89
  export const TaskFollowUp = z.object({
117
90
  envelopeId: Ulid,
@@ -138,10 +111,9 @@ export const TaskRecord = z.object({
138
111
  followUps: z.array(TaskFollowUp).min(1).max(LIMITS.taskFollowUps).optional(),
139
112
  });
140
113
  /**
141
- * Where a send went (`msg.send`, spec §8.2): an agent session `person/agent@label`, a person (a line
142
- * from an agent to them) or `#<channel>` (a post).
114
+ * Where a retained task envelope went: an agent session `person/agent@label`.
143
115
  */
144
- export const SendTarget = z.union([SessionAddress, PersonId, z.string().regex(/^#[a-z][a-z0-9-]{0,31}$/, "#<channel>")]);
116
+ export const SendTarget = SessionAddress;
145
117
  export const SendResult = z.object({
146
118
  to: SendTarget,
147
119
  envelopeId: Ulid,
@@ -153,33 +125,3 @@ export const SendResult = z.object({
153
125
  /** Absent when the stage is `rejected` or `expired`, or when no presence lists the session. */
154
126
  expect: SendExpect.optional(),
155
127
  });
156
- /** A place in a conversation page (`conv.read`): an entry's time and id. */
157
- export const ConvCursor = z.strictObject({ at: Millis, id: Ulid });
158
- /**
159
- * One entry of a conversation between a local session and one other session (spec §8.2
160
- * `conv.read`): mail either way, or a task either way. `text` is untrusted peer text for incoming
161
- * mail and for a task the other side asked for; clients frame it as data.
162
- */
163
- export const ConvEntry = z.object({
164
- /** The envelope id (mail) or the task id (task). */
165
- id: Ulid,
166
- kind: z.enum(["mail", "task"]),
167
- /** `in`: from the other session (mail it sent, a task it asked this session to run); `out`: the reverse. */
168
- direction: z.enum(["in", "out"]),
169
- /** The other session, as this machine knows it. */
170
- peer: SessionAddress,
171
- /** Mail: received (in) or sent (out). Task: created. */
172
- at: Millis,
173
- /**
174
- * Incoming mail: its preview. Task: the start of its request. Absent on outgoing mail: the
175
- * daemon keeps no copy of what it sent.
176
- */
177
- text: z.string().max(LIMITS.previewChars).optional(),
178
- /** Mail that continues a task. */
179
- taskId: Ulid.optional(),
180
- /** Mail: its stage (incoming: this side's inbox stage; outgoing: how far it got). */
181
- stage: DeliveryStage.optional(),
182
- /** Task: this session's role and the task's state now. */
183
- role: z.enum(["requester", "executor"]).optional(),
184
- state: TaskState.optional(),
185
- });
@@ -1,223 +1,40 @@
1
- /**
2
- * The local API's surface methods (spec §8.2; design §7.5): what agents reach through their tools
3
- * and what people reach through the CLI and the TUI, by conversation.
4
- *
5
- * - Agent tier (`run/agent.sock`): `contacts.list`, `msg.send`, `item.read`, `conv.read`, `search`.
6
- * - Human tier (`run/admin.sock`): `conv.list`, `conv.read`, `conv.markRead`, `conv.subscribe`,
7
- * `person.send`, `people.search`, `people.get`.
8
- *
9
- * A conversation is named by a `ConversationRef`: `dm:<person>` (this person's DM with another),
10
- * `channel:<name>` (a channel this person is in, by its local name) or `agent:<person>/<agent>`
11
- * (a local session's mail and tasks with that agent, on any of the person's devices).
12
- */
1
+ /** Discovery and task-envelope reads. Conversation methods live in conversations.ts. */
13
2
  import { z } from "../../../vendor/zod/index.js";
14
- import { Message, Part } from "../a2a.js";
15
- import { Address, DeliveryStage, MachineId, MachineLabel, Millis, PersonId, RoomRef, Ulid, LIMITS } from "../common.js";
16
- import { DelegationScope } from "../delegation.js";
17
- import { Email } from "../directory-frames.js";
3
+ import { Message } from "../a2a.js";
4
+ import { DeliveryStage, Millis, PersonId, Ulid } from "../common.js";
18
5
  import { ErrorCode } from "../errors.js";
19
6
  import { PersonReach } from "./consent.js";
20
- import { DeviceChangeView } from "./device.js";
21
- import { ChatLine, ChatSeq, ChatText, ConvCursor, ConvEntry, DirectoryEntry, InboxItem, SendResult, SendTarget, SessionKey } from "./shapes.js";
22
- /**
23
- * Max UTF-8 bytes of `msg.send` metadata as JSON. For wire JSON that is its JCS length: JCS only
24
- * reorders members.
25
- */
26
- const METADATA_BYTES = 16_384;
27
- /** A conversation: `dm:<person>`, `channel:<name>` or `agent:<person>/<agent>`. */
28
- export const ConversationRef = z
29
- .string()
30
- .regex(/^(?:dm:(?:[a-z][a-z0-9-]{0,31}|p_[a-z2-7]{26})|channel:[a-z][a-z0-9-]{0,31}|agent:(?:[a-z][a-z0-9-]{0,31}|p_[a-z2-7]{26})\/[a-z0-9][a-z0-9._-]{0,63})$/, "dm:<person>, channel:<name> or agent:<person>/<agent>");
31
- /** A place in a conversation: a line's `seq` in a DM or a channel, an entry's `{at, id}` with an agent. */
32
- export const ConvPlace = z.union([ChatSeq, ConvCursor]);
33
- /** One conversation this person has (DMs with lines, channels it is in) and this device's read state. */
34
- export const ConvSummary = z.object({
35
- conversation: RoomRef,
36
- kind: z.enum(["dm", "channel"]),
37
- lastAt: Millis.optional(),
38
- /** A channel held under a local name (another channel here already had its name) carries its own name. */
39
- name: z.string().max(32).optional(),
40
- /** The newest line's seq; absent in a channel with no lines. */
41
- lastSeq: ChatSeq.optional(),
42
- /** 0 when nothing is marked read. */
43
- readUpTo: ChatSeq,
44
- /** Lines from others after `readUpTo`. */
45
- unread: z.number().int().nonnegative(),
7
+ import { ConvId, ConvKind, ParticipantId } from "../conversation.js";
8
+ import { DirectoryEntry, InboxItem, SendTarget, SessionKey } from "./shapes.js";
9
+ /** Verified discovery metadata, without transcript or read-state placeholders. */
10
+ export const ContactConversation = z.strictObject({
11
+ cid: ConvId, kind: ConvKind, name: z.string().max(200).optional(), dmWith: ParticipantId.optional(),
46
12
  });
47
- /** A person of the realm, as verified (a roster realm: a team person). */
48
13
  export const PersonSummary = z.object({
49
14
  person: PersonId,
50
- /** The display name; a roster realm has only the person name. Untrusted text in a hosted realm. */
51
15
  name: z.string().max(200),
52
- /** What a person types to address them. */
53
16
  handle: z.string().max(64),
54
- /** `same`: in this device's person's org (a roster realm: the team). */
55
17
  org: z.enum(["same", "other", "none"]),
56
- /** The weakest vouch among their current devices. */
57
18
  vouched: z.enum(["jelly", "device", "service", "admin"]),
58
19
  });
59
20
  export const SURFACE_METHODS = {
60
- /**
61
- * Agent tier. Who this session can reach: every known agent session (`entries`, as `directory.list`
62
- * did in 0.1), and this person's conversations with their read state (`conversations`, as
63
- * `conv.list` returns them).
64
- */
65
21
  "contacts.list": {
66
22
  params: z.strictObject({ person: PersonId.optional(), includeOffline: z.boolean().optional() }),
67
- /**
68
- * `reach`: for each other person among `entries` and the DMs, and each person whose card this device holds, what they may
69
- * be sent, from their card or the defaults of the relation (spec §6.9, "Contact cards").
70
- */
71
- result: z.object({ entries: z.array(DirectoryEntry), conversations: z.array(ConvSummary), reach: z.array(PersonReach) }),
23
+ result: z.object({ entries: z.array(DirectoryEntry), conversations: z.array(ContactConversation), reach: z.array(PersonReach) }),
72
24
  },
73
- /**
74
- * Agent tier. One send from the session `key` (spec §8.2): mail or a task to an agent
75
- * (`person/agent[@label]`), a line to a person (`<person>`), or a post to a channel (`#<channel>`).
76
- * A line or a post is attributed to the session; it takes one text part of at most
77
- * `LIMITS.chatChars`, `kind: "message"`, and `replyTo` as the thread (the thread's first line),
78
- * and none of the A2A fields. `idempotencyKey` makes any send idempotent.
79
- */
80
- "msg.send": {
81
- params: z.strictObject({
82
- key: SessionKey,
83
- to: z.union([Address, z.string().regex(/^#[a-z][a-z0-9-]{0,31}$/, "#<channel>")]),
84
- kind: z.enum(["message", "task"]),
85
- parts: z.array(Part).min(1).max(LIMITS.partsPerMessage),
86
- contextId: z.string().max(128).optional(),
87
- taskId: Ulid.optional().describe("for kind=message: the task this message continues (INPUT_REQUIRED replies)"),
88
- replyTo: Ulid.optional(),
89
- idempotencyKey: z.string().min(1).max(200).optional(),
90
- /** A2A passthrough. The daemon generates messageId when omitted and always overwrites metadata[URI.provenance]. */
91
- messageId: z.string().min(1).max(128).optional(),
92
- metadata: z
93
- .record(z.string(), z.unknown())
94
- .refine((m) => new TextEncoder().encode(JSON.stringify(m)).length <= METADATA_BYTES, { message: `metadata is limited to ${METADATA_BYTES} bytes of JSON` })
95
- .optional(),
96
- extensions: z.array(z.string().min(1).max(256)).max(16).optional(),
97
- referenceTaskIds: z.array(Ulid).max(16).optional(),
98
- wake: z.boolean().optional().describe("hint that the recipient may wake an idle session; default true"),
99
- /**
100
- * Only on a line to the session's own person (`<own person>`, spec §6.9 "Message requests"): the agent asks its
101
- * person to send a message request to this exact email. The daemon files it for the person to answer; only the
102
- * person sends the request.
103
- */
104
- request: z.strictObject({ contact: Email }).optional(),
105
- /**
106
- * kind=task only (spec §6.7): delegate on behalf of a task this session executes. The daemon extends that
107
- * task's chain to each recipient, narrowed to `scope` and `maxDepth` when given. Without it, a task starts a new chain.
108
- */
109
- onBehalfOf: z
110
- .strictObject({
111
- taskId: Ulid.describe("a task this session executes"),
112
- scope: z.array(DelegationScope).max(LIMITS.delegationScopes).optional().describe("narrow the scope; never wider than the task's"),
113
- maxDepth: z.number().int().min(1).max(LIMITS.delegationDepth).optional().describe("narrow the depth; never above the task's"),
114
- })
115
- .optional(),
116
- }),
117
- result: z.object({ results: z.array(SendResult) }),
118
- },
119
- /**
120
- * Agent tier. One item of the session `key` by id: something it received (`received`: returns the
121
- * full message, marks it read and sends a read receipt, as `inbox.read` did in 0.1), or something
122
- * it sent to an agent (`sent`: how far it got, as `message.status` did). `not_found` otherwise.
123
- */
25
+ /** A received item, or the delivery records of a sent task envelope. */
124
26
  "item.read": {
125
- params: z.strictObject({ key: SessionKey, id: Ulid }),
27
+ params: z.strictObject({ key: SessionKey, id: Ulid, cid: ConvId.optional() }),
126
28
  result: z.discriminatedUnion("kind", [
127
29
  z.object({ kind: z.literal("received"), item: InboxItem, message: Message }),
128
30
  z.object({
129
- kind: z.literal("sent"),
130
- envelopeId: Ulid,
131
- to: SendTarget,
132
- stage: DeliveryStage,
31
+ kind: z.literal("sent"), envelopeId: Ulid, to: SendTarget, stage: DeliveryStage,
133
32
  history: z.array(z.object({ stage: DeliveryStage, at: Millis, code: ErrorCode.optional(), note: z.string().optional() })),
134
33
  }),
135
34
  ]),
136
35
  },
137
- /**
138
- * Agent and human tiers. A page of one conversation, oldest first. With no cursor, the latest
139
- * `limit` (default 30 with an agent, 50 in a DM or a channel); with `before`, the latest `limit`
140
- * before that place; with `after`, the earliest `limit` after it. `more`: entries remain past the
141
- * page in the direction read. Changes no stage and sends no receipt.
142
- * - `dm:` and `channel:` give `lines`; `thread` narrows to a thread's first line and its replies;
143
- * the cursor is a line's `seq`.
144
- * - `agent:<person>/<agent>` needs the session `key` and gives `entries`: mail either way and tasks
145
- * either way between that session and the agent; the cursor is an entry's `{at, id}`.
146
- */
147
- "conv.read": {
148
- params: z
149
- .strictObject({
150
- key: SessionKey.optional(),
151
- conversation: ConversationRef,
152
- thread: Ulid.optional(),
153
- before: ConvPlace.optional(),
154
- after: ConvPlace.optional(),
155
- limit: z.number().int().min(1).max(100).optional(),
156
- })
157
- .refine((p) => p.before === undefined || p.after === undefined, { message: "before and after do not combine" }),
158
- result: z.discriminatedUnion("kind", [
159
- z.object({ kind: z.literal("lines"), lines: z.array(ChatLine), more: z.boolean() }),
160
- z.object({ kind: z.literal("agent"), entries: z.array(ConvEntry), more: z.boolean() }),
161
- ]),
162
- },
163
- /** Agent tier. Full-text search over the DM and channel lines `conv.read` can read, newest first. */
164
- search: {
165
- params: z.strictObject({ query: z.string().min(1).max(200), conversation: RoomRef.optional(), limit: z.number().int().min(1).max(50).optional() }),
166
- result: z.object({ hits: z.array(ChatLine.extend({ conversation: RoomRef })) }),
167
- },
168
- /**
169
- * Human tier. The channels this person is in and the DMs with lines, newest first, at most `limit`
170
- * (default 100), each with this device's read state.
171
- */
172
- "conv.list": {
173
- params: z.strictObject({ limit: z.number().int().min(1).max(200).optional() }),
174
- result: z.object({ conversations: z.array(ConvSummary) }),
175
- },
176
- /**
177
- * Human tier. Marks a DM or a channel read on this device up to `upTo` (a line's `seq`): the cursor
178
- * only moves forward, and never past the newest seq here. Returns the cursor and what is still unread.
179
- */
180
- "conv.markRead": {
181
- params: z.strictObject({ conversation: RoomRef, upTo: ChatSeq }),
182
- result: z.object({ readUpTo: ChatSeq, unread: z.number().int().nonnegative() }),
183
- },
184
- /**
185
- * Human tier. `conv` events go only to connections that call this (spec §6.8). With `since` (the
186
- * `seq` of the last event the client handled), the daemon first replays one event per line changed
187
- * after it, oldest first, or sends none and answers `gap: true` when it cannot replay them all; the
188
- * client then reloads with `conv.list` and `conv.read`. `seq` is the newest seq at the call.
189
- */
190
- "conv.subscribe": { params: z.strictObject({ since: ChatSeq.optional() }), result: z.object({ seq: ChatSeq, gap: z.boolean() }) },
191
- /**
192
- * Human tier. A line from the person (no agent) to a DM or a channel this person is in. `thread` is
193
- * the `id` of the thread's first line. `clientId` makes the call idempotent: a repeat with the same
194
- * `clientId` and the same conversation, thread and text returns the first line's result and sends nothing.
195
- */
196
- "person.send": {
197
- params: z.strictObject({
198
- conversation: RoomRef,
199
- text: ChatText.min(1),
200
- thread: Ulid.optional(),
201
- clientId: z.string().min(1).max(200).optional(),
202
- }),
203
- result: z.object({ id: Ulid, conversation: RoomRef }),
204
- },
205
- /** Human tier. The persons of the realm whose handle starts with `query` (all with none), at most `limit` (default 50). */
206
36
  "people.search": {
207
37
  params: z.strictObject({ query: z.string().max(100).optional(), limit: z.number().int().min(1).max(200).optional() }),
208
38
  result: z.object({ people: z.array(PersonSummary) }),
209
39
  },
210
- /** Human tier. One person and their current devices, as verified. `not_found` for a person this realm does not know. */
211
- "people.get": {
212
- params: z.strictObject({ person: PersonId }),
213
- /** A device's `away`: its latest presence, still fresh, says no window of the person is visible on it (spec §6.5). */
214
- result: PersonSummary.extend({
215
- devices: z.array(z.object({ device: MachineId, label: MachineLabel, self: z.boolean(), away: z.literal(true).optional() })),
216
- /**
217
- * Their devices added, removed or replaced in the last 7 days, newest first, at most 10 (design §2.8: "Bob's
218
- * devices changed: added 'bob-vps' (vouched by Jelly ID)"). Absent for this device's own person and when none.
219
- */
220
- deviceChanges: z.array(DeviceChangeView).max(10).optional(),
221
- }),
222
- },
223
40
  };
@@ -0,0 +1,31 @@
1
+ /** Retained task envelopes, independent of conversation lines. */
2
+ import { z } from "../../../vendor/zod/index.js";
3
+ import { Part } from "../a2a.js";
4
+ import { Address, LIMITS, Ulid } from "../common.js";
5
+ import { DelegationScope } from "../delegation.js";
6
+ import { SendResult, SessionKey } from "./shapes.js";
7
+ const fields = {
8
+ key: SessionKey,
9
+ to: Address,
10
+ parts: z.array(Part).min(1).max(LIMITS.partsPerMessage),
11
+ contextId: z.string().max(128).optional(),
12
+ replyTo: Ulid.optional(),
13
+ idempotencyKey: z.string().min(1).max(200).optional(),
14
+ messageId: z.string().min(1).max(128).optional(),
15
+ metadata: z.record(z.string(), z.unknown()).refine((m) => new TextEncoder().encode(JSON.stringify(m)).length <= 16_384, { message: "metadata is limited to 16384 bytes of JSON" }).optional(),
16
+ extensions: z.array(z.string().min(1).max(256)).max(16).optional(),
17
+ referenceTaskIds: z.array(Ulid).max(16).optional(),
18
+ wake: z.boolean().optional(),
19
+ };
20
+ const result = z.object({ results: z.array(SendResult) });
21
+ export const TASK_SEND_METHODS = {
22
+ "task.create": {
23
+ params: z.strictObject({ ...fields, onBehalfOf: z.strictObject({
24
+ taskId: Ulid,
25
+ scope: z.array(DelegationScope).max(LIMITS.delegationScopes).optional(),
26
+ maxDepth: z.number().int().min(1).max(LIMITS.delegationDepth).optional(),
27
+ }).optional() }),
28
+ result,
29
+ },
30
+ "task.continue": { params: z.strictObject({ ...fields, taskId: Ulid }), result },
31
+ };
@@ -15,21 +15,25 @@
15
15
  *
16
16
  * Errors: standard JSON-RPC codes for transport/schema problems (-32700, -32600, -32601, -32602,
17
17
  * -32603); every Messaging error uses code -32000 with `data: { code: ErrorCode }`.
18
- * The daemon pushes notifications `{"jsonrpc":"2.0","method":"event","params":Event}`: `conv` events
19
- * to connections that called `conv.subscribe`, every other event to connections that called
20
- * `events.subscribe` for its key (or to all of them, for events without a key).
18
+ * The daemon pushes notifications `{"jsonrpc":"2.0","method":"event","params":Event}` to connections
19
+ * subscribed to the event's key. Notice subscriptions are human-only. Conversations use `conv.sync`.
21
20
  */
22
21
  import { z } from "../../vendor/zod/index.js";
23
22
  import { DEVICE_METHODS, DeviceStatus, LoginPurpose } from "./ipc/device.js";
24
23
  import { Artifact, Message, Part, TaskState, TaskStatus } from "./a2a.js";
25
- import { AgentKind, ChannelName, DeliveryStage, DeliveryTier, EncKey, Hash, MachineId, MachineLabel, MemberRole, Millis, PersonId, PersonName, RelayUrl, RoomRef, SessionAddress, SessionName, SessionState, TeamId, TeamName, Ulid, LIMITS, } from "./common.js";
24
+ import { AgentKind, DeliveryStage, DeliveryTier, EncKey, Hash, MachineId, MachineLabel, MemberRole, Millis, PersonId, PersonName, RelayUrl, SessionAddress, SessionName, SessionState, TeamId, TeamName, Ulid, LIMITS, } from "./common.js";
26
25
  import { ErrorCode } from "./errors.js";
27
- import { ChatSeq, InboxItem, SessionKey, TaskRecord } from "./ipc/shapes.js";
26
+ import { InboxItem, SessionKey, TaskRecord } from "./ipc/shapes.js";
28
27
  import { SURFACE_METHODS } from "./ipc/surfaces.js";
29
28
  import { Member } from "./roster.js";
30
29
  import { CONSENT_METHODS } from "./ipc/consent.js";
31
30
  import { JELLY_METHODS, Notice } from "./ipc/jelly.js";
32
31
  import { Jws } from "./jelly.js";
32
+ import { LIVE_CONVERSATION_METHODS, CONVERSATION_TIERS } from "./ipc/conversations.js";
33
+ import { TASK_SEND_METHODS } from "./ipc/tasks.js";
34
+ import { ConvId } from "./conversation.js";
35
+ const conversationTier = (tier) => Object.keys(LIVE_CONVERSATION_METHODS)
36
+ .filter((m) => CONVERSATION_TIERS[m] === tier);
33
37
  export * from "./ipc/shapes.js";
34
38
  export * from "./ipc/surfaces.js";
35
39
  export * from "./ipc/jelly.js";
@@ -120,6 +124,7 @@ export const InviteSummary = z.object({
120
124
  /** Method table: name → params/result schemas. The daemon validates params; clients validate results. */
121
125
  export const IPC_METHODS = {
122
126
  ...DEVICE_METHODS,
127
+ ...TASK_SEND_METHODS,
123
128
  "daemon.status": { params: Empty, result: DaemonStatus },
124
129
  /** Human CLI only: replies, then shuts the daemon down cleanly. */
125
130
  "daemon.stop": { params: Empty, result: Ok },
@@ -166,11 +171,11 @@ export const IPC_METHODS = {
166
171
  * and ack after (`inbox.ack`), so a crash in between leaves the item unread.
167
172
  */
168
173
  "inbox.get": {
169
- params: z.strictObject({ key: SessionKey, id: Ulid, message: z.boolean().optional() }),
174
+ params: z.strictObject({ key: SessionKey, id: Ulid, cid: ConvId.optional(), message: z.boolean().optional() }),
170
175
  result: z.object({ item: InboxItem, message: Message.optional() }),
171
176
  },
172
177
  "inbox.ack": {
173
- params: z.strictObject({ key: SessionKey, id: Ulid, stage: z.enum(["handed_to_agent", "read"]) }),
178
+ params: z.strictObject({ key: SessionKey, id: Ulid, cid: ConvId.optional(), stage: z.enum(["handed_to_agent", "read"]) }),
174
179
  result: Ok,
175
180
  },
176
181
  "task.list": {
@@ -291,24 +296,10 @@ export const IPC_METHODS = {
291
296
  * while they are pending. Private keys stay in the daemon.
292
297
  */
293
298
  "team.prepare": { params: Empty, result: z.object({ machine: MachineId, enc: EncKey }) },
294
- /** Creates a channel with this person and `persons` (at most `LIMITS.channelPersons` together). */
295
- "channel.create": {
296
- params: z.strictObject({ name: ChannelName, persons: z.array(PersonId).max(LIMITS.channelPersons) }),
297
- result: z.object({ id: Ulid, name: ChannelName }),
298
- },
299
- "channel.invite": {
300
- params: z.strictObject({ name: ChannelName, persons: z.array(PersonId).min(1).max(LIMITS.channelPersons) }),
301
- result: Ok,
302
- },
303
- "channel.leave": { params: z.strictObject({ name: ChannelName }), result: Ok },
304
- /** Removes other persons from a channel; only its creator or a team admin may (spec §6.8). */
305
- "channel.remove": {
306
- params: z.strictObject({ name: ChannelName, persons: z.array(PersonId).min(1).max(LIMITS.channelPersons) }),
307
- result: Ok,
308
- },
309
299
  ...CONSENT_METHODS,
310
300
  ...JELLY_METHODS,
311
301
  ...SURFACE_METHODS,
302
+ ...LIVE_CONVERSATION_METHODS,
312
303
  };
313
304
  /**
314
305
  * The agent tier (design §7.5): the methods an agent-facing client may call, and exactly what the
@@ -325,13 +316,13 @@ export const AGENT_METHODS = [
325
316
  "session.unregister",
326
317
  "events.subscribe",
327
318
  "contacts.list",
328
- "msg.send",
329
319
  "inbox.list",
330
320
  "inbox.get",
331
321
  "inbox.ack",
332
322
  "item.read",
333
- "conv.read",
334
- "search",
323
+ ...conversationTier("agent"),
324
+ "task.create",
325
+ "task.continue",
335
326
  "task.list",
336
327
  "task.get",
337
328
  "task.update",
@@ -343,13 +334,9 @@ export const AGENT_METHODS = [
343
334
  * `conv.read` is in both tiers.
344
335
  */
345
336
  export const HUMAN_METHODS = [
346
- "conv.list",
347
- "conv.read",
348
- "conv.markRead",
349
- "conv.subscribe",
350
- "person.send",
337
+ ...conversationTier("human"),
338
+ ...conversationTier("agent"),
351
339
  "people.search",
352
- "people.get",
353
340
  "grant.list",
354
341
  "grant.set",
355
342
  "grant.set.preview",
@@ -359,8 +346,6 @@ export const HUMAN_METHODS = [
359
346
  "contact.request",
360
347
  "contact.request.preview",
361
348
  "card.get",
362
- "agents.list",
363
- "agents.read",
364
349
  "notice.subscribe",
365
350
  "notice.list",
366
351
  "notice.ack",
@@ -368,10 +353,6 @@ export const HUMAN_METHODS = [
368
353
  "device.remove.preview",
369
354
  "identity.replace.preview",
370
355
  "directory.erase.preview",
371
- "channel.create",
372
- "channel.invite",
373
- "channel.leave",
374
- "channel.remove",
375
356
  ];
376
357
  /**
377
358
  * The admin tier (design §7.5): this device and the daemon, and in a roster realm the team, on
@@ -434,13 +415,6 @@ export const IpcEvent = z.discriminatedUnion("type", [
434
415
  /** What `contacts.list` returns changed: a presence, a session, the roster. */
435
416
  z.object({ type: z.literal("contacts") }),
436
417
  z.object({ type: z.literal("relay"), connected: z.boolean() }),
437
- /**
438
- * A line was stored in a DM or a channel, a channel this person is in changed, or the delivery state
439
- * of an outgoing line changed. Only for connections that called `conv.subscribe`. The text stays in
440
- * the transcript. `seq` rises with every event; a client resumes from the last one it handled
441
- * (`conv.subscribe {since}`).
442
- */
443
- z.object({ type: z.literal("conv"), conversation: RoomRef, id: Ulid, person: PersonId, seq: ChatSeq }),
444
418
  /**
445
419
  * A device's sign-in through the browser changed state (spec §15.9): its state and, when it failed, the code. Never
446
420
  * the code to type, the link or a token; `device.login.status` gives the rest on the admin socket.
@@ -7,7 +7,7 @@ export { encodeTicket } from "./roster.js";
7
7
  export { grantHash } from "./delegation.js";
8
8
  export { signJellyToken } from "./directory/jelly-issuer.js";
9
9
  export { jellyHostDouble } from "./jelly-host-double.js";
10
- export { sealConvMetaWith, sealConvMsgWith, sealKeySharesWith } from "./conversation/seal.js";
10
+ export { sealConvMetaWith, sealConvMsgWith, sealConvStateWith, sealKeyShareWith } from "./conversation/seal.js";
11
11
  /**
12
12
  * Narrows a result whose `kind` tells its shape (`item.read`, `conv.read`) to the expected kind, and
13
13
  * throws with both kinds named otherwise: `client.call("item.read", p).then(asKind("received"))`.
@@ -3,4 +3,4 @@
3
3
  * its IPC hello and a client compares it with its own (spec §8.2). A test keeps it equal to the
4
4
  * root package.json.
5
5
  */
6
- export const VERSION = "0.7.1";
6
+ export const VERSION = "0.8.0";