@joe-broadhead/messaging 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +259 -2
  2. package/dist/bin/messaging-mcp.js +5484 -3858
  3. package/dist/bin/messaging-relay.js +4728 -1136
  4. package/dist/bin/messaging.js +4616 -2807
  5. package/dist/bin/messagingd.js +16451 -9444
  6. package/dist/lib/modules/core/agent/operations.js +19 -327
  7. package/dist/lib/modules/core/agent/present.js +15 -55
  8. package/dist/lib/modules/core/agent-tools.js +46 -143
  9. package/dist/lib/modules/core/consent.js +1 -1
  10. package/dist/lib/modules/core/conversation/context.js +80 -0
  11. package/dist/lib/modules/core/conversation/event.js +125 -0
  12. package/dist/lib/modules/core/conversation/guest-invitation.js +42 -0
  13. package/dist/lib/modules/core/conversation/ids.js +38 -0
  14. package/dist/lib/modules/core/conversation/link.js +35 -0
  15. package/dist/lib/modules/core/conversation/log.js +988 -0
  16. package/dist/lib/modules/core/conversation/ratchet.js +243 -0
  17. package/dist/lib/modules/core/conversation/request-log.js +158 -0
  18. package/dist/lib/modules/core/conversation/request-seal.js +41 -0
  19. package/dist/lib/modules/core/conversation/requests.js +87 -0
  20. package/dist/lib/modules/core/conversation/seal.js +251 -0
  21. package/dist/lib/modules/core/conversation-agent-tools.js +69 -0
  22. package/dist/lib/modules/core/federation/directory-proof.js +83 -0
  23. package/dist/lib/modules/core/federation/proof-state.js +167 -0
  24. package/dist/lib/modules/core/federation/roster-proof.js +74 -0
  25. package/dist/lib/modules/core/index.js +16 -2
  26. package/dist/lib/modules/core/jelly-host.js +5 -5
  27. package/dist/lib/modules/core/schema/common.js +85 -3
  28. package/dist/lib/modules/core/schema/consent.js +5 -4
  29. package/dist/lib/modules/core/schema/contact-request.js +22 -0
  30. package/dist/lib/modules/core/schema/conv-frames.js +94 -0
  31. package/dist/lib/modules/core/schema/conversation.js +326 -0
  32. package/dist/lib/modules/core/schema/envelope.js +8 -24
  33. package/dist/lib/modules/core/schema/errors.js +42 -2
  34. package/dist/lib/modules/core/schema/guest-invitation.js +13 -0
  35. package/dist/lib/modules/core/schema/index.js +6 -0
  36. package/dist/lib/modules/core/schema/ipc/consent.js +13 -9
  37. package/dist/lib/modules/core/schema/ipc/conversation-views.js +80 -0
  38. package/dist/lib/modules/core/schema/ipc/conversations.js +128 -0
  39. package/dist/lib/modules/core/schema/ipc/device.js +2 -0
  40. package/dist/lib/modules/core/schema/ipc/jelly.js +3 -38
  41. package/dist/lib/modules/core/schema/ipc/shapes.js +14 -72
  42. package/dist/lib/modules/core/schema/ipc/surfaces.js +12 -195
  43. package/dist/lib/modules/core/schema/ipc/tasks.js +31 -0
  44. package/dist/lib/modules/core/schema/ipc.js +18 -44
  45. package/dist/lib/modules/core/testing.js +1 -0
  46. package/dist/lib/modules/core/version.js +1 -1
  47. package/dist/lib/modules/ipc/client.js +10 -2
  48. package/dist/lib/modules/ipc/server.js +14 -5
  49. package/dist/lib/modules/ipc/testing.js +13 -23
  50. package/dist/spec/messaging-v1.md +835 -380
  51. package/dist/third-party-notices.md +1 -1
  52. package/dist/third-party.json +1 -1
  53. package/dist/types/core/agent/operations.d.ts +5 -52
  54. package/dist/types/core/agent/present.d.ts +6 -21
  55. package/dist/types/core/agent-tools.d.ts +277 -438
  56. package/dist/types/core/conversation/context.d.ts +56 -0
  57. package/dist/types/core/conversation/event.d.ts +36 -0
  58. package/dist/types/core/conversation/guest-invitation.d.ts +8 -0
  59. package/dist/types/core/conversation/ids.d.ts +18 -0
  60. package/dist/types/core/conversation/link.d.ts +16 -0
  61. package/dist/types/core/conversation/log.d.ts +157 -0
  62. package/dist/types/core/conversation/ratchet.d.ts +95 -0
  63. package/dist/types/core/conversation/request-log.d.ts +24 -0
  64. package/dist/types/core/conversation/request-seal.d.ts +6 -0
  65. package/dist/types/core/conversation/requests.d.ts +14 -0
  66. package/dist/types/core/conversation/seal.d.ts +70 -0
  67. package/dist/types/core/conversation-agent-tools.d.ts +918 -0
  68. package/dist/types/core/federation/directory-proof.d.ts +8 -0
  69. package/dist/types/core/federation/proof-state.d.ts +114 -0
  70. package/dist/types/core/federation/roster-proof.d.ts +7 -0
  71. package/dist/types/core/index.d.ts +15 -3
  72. package/dist/types/core/jelly-host.d.ts +1 -1
  73. package/dist/types/core/schema/common.d.ts +85 -4
  74. package/dist/types/core/schema/consent.d.ts +7 -7
  75. package/dist/types/core/schema/contact-request.d.ts +264 -0
  76. package/dist/types/core/schema/conv-frames.d.ts +1200 -0
  77. package/dist/types/core/schema/conversation.d.ts +2803 -0
  78. package/dist/types/core/schema/envelope.d.ts +89 -130
  79. package/dist/types/core/schema/errors.d.ts +40 -0
  80. package/dist/types/core/schema/guest-invitation.d.ts +51 -0
  81. package/dist/types/core/schema/index.d.ts +6 -0
  82. package/dist/types/core/schema/ipc/consent.d.ts +89 -49
  83. package/dist/types/core/schema/ipc/conversation-views.d.ts +1023 -0
  84. package/dist/types/core/schema/ipc/conversations.d.ts +3987 -0
  85. package/dist/types/core/schema/ipc/device.d.ts +10 -0
  86. package/dist/types/core/schema/ipc/jelly.d.ts +2 -105
  87. package/dist/types/core/schema/ipc/shapes.d.ts +16 -95
  88. package/dist/types/core/schema/ipc/surfaces.d.ts +28 -424
  89. package/dist/types/core/schema/ipc/tasks.d.ts +208 -0
  90. package/dist/types/core/schema/ipc.d.ts +2684 -523
  91. package/dist/types/core/schema/relay.d.ts +20 -4
  92. package/dist/types/core/testing.d.ts +1 -0
  93. package/dist/types/core/version.d.ts +1 -1
  94. package/dist/types/ipc/client.d.ts +4 -1
  95. package/dist/types/ipc/server.d.ts +10 -1
  96. package/dist/types/ipc/testing.d.ts +21 -8
  97. package/package.json +1 -1
@@ -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(),
@@ -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
- });