@joe-broadhead/messaging 0.7.1 → 0.9.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 (94) hide show
  1. package/CHANGELOG.md +364 -2
  2. package/dist/bin/messaging-mcp.js +5498 -4338
  3. package/dist/bin/messaging-relay.js +8626 -5540
  4. package/dist/bin/messaging.js +4492 -3158
  5. package/dist/bin/messagingd.js +19672 -12412
  6. package/dist/lib/modules/core/agent/operations.js +19 -327
  7. package/dist/lib/modules/core/agent/present.js +20 -55
  8. package/dist/lib/modules/core/agent-tools.js +35 -139
  9. package/dist/lib/modules/core/consent.js +54 -14
  10. package/dist/lib/modules/core/contact-card.js +3 -3
  11. package/dist/lib/modules/core/conversation/event.js +29 -4
  12. package/dist/lib/modules/core/conversation/guest-invitation.js +42 -0
  13. package/dist/lib/modules/core/conversation/log.js +287 -65
  14. package/dist/lib/modules/core/conversation/request-log.js +158 -0
  15. package/dist/lib/modules/core/conversation/request-seal.js +41 -0
  16. package/dist/lib/modules/core/conversation/requests.js +87 -0
  17. package/dist/lib/modules/core/conversation/seal.js +90 -37
  18. package/dist/lib/modules/core/conversation-agent-tools.js +69 -0
  19. package/dist/lib/modules/core/federation/directory-proof.js +83 -0
  20. package/dist/lib/modules/core/federation/proof-state.js +167 -0
  21. package/dist/lib/modules/core/federation/roster-proof.js +74 -0
  22. package/dist/lib/modules/core/index.js +9 -4
  23. package/dist/lib/modules/core/jelly-host.js +8 -5
  24. package/dist/lib/modules/core/schema/common.js +33 -5
  25. package/dist/lib/modules/core/schema/consent.js +23 -5
  26. package/dist/lib/modules/core/schema/contact-request.js +22 -0
  27. package/dist/lib/modules/core/schema/conv-frames.js +101 -0
  28. package/dist/lib/modules/core/schema/conversation.js +67 -33
  29. package/dist/lib/modules/core/schema/directory-frames.js +8 -3
  30. package/dist/lib/modules/core/schema/envelope.js +8 -24
  31. package/dist/lib/modules/core/schema/errors.js +7 -5
  32. package/dist/lib/modules/core/schema/guest-invitation.js +13 -0
  33. package/dist/lib/modules/core/schema/index.js +5 -0
  34. package/dist/lib/modules/core/schema/ipc/consent.js +97 -18
  35. package/dist/lib/modules/core/schema/ipc/conversation-views.js +87 -0
  36. package/dist/lib/modules/core/schema/ipc/conversations.js +132 -0
  37. package/dist/lib/modules/core/schema/ipc/device.js +9 -0
  38. package/dist/lib/modules/core/schema/ipc/jelly.js +17 -45
  39. package/dist/lib/modules/core/schema/ipc/shapes.js +74 -73
  40. package/dist/lib/modules/core/schema/ipc/surfaces.js +12 -195
  41. package/dist/lib/modules/core/schema/ipc/tasks.js +31 -0
  42. package/dist/lib/modules/core/schema/ipc.js +68 -46
  43. package/dist/lib/modules/core/testing.js +1 -1
  44. package/dist/lib/modules/core/version.js +1 -1
  45. package/dist/lib/modules/ipc/client.js +10 -2
  46. package/dist/lib/modules/ipc/server.js +14 -5
  47. package/dist/lib/modules/ipc/testing.js +13 -23
  48. package/dist/spec/messaging-v1.md +854 -445
  49. package/dist/third-party-notices.md +1 -1
  50. package/dist/third-party.json +1 -1
  51. package/dist/types/core/agent/operations.d.ts +5 -52
  52. package/dist/types/core/agent/present.d.ts +6 -21
  53. package/dist/types/core/agent-tools.d.ts +295 -442
  54. package/dist/types/core/consent.d.ts +30 -7
  55. package/dist/types/core/contact-card.d.ts +3 -3
  56. package/dist/types/core/conversation/event.d.ts +6 -6
  57. package/dist/types/core/conversation/guest-invitation.d.ts +8 -0
  58. package/dist/types/core/conversation/log.d.ts +28 -13
  59. package/dist/types/core/conversation/request-log.d.ts +24 -0
  60. package/dist/types/core/conversation/request-seal.d.ts +6 -0
  61. package/dist/types/core/conversation/requests.d.ts +14 -0
  62. package/dist/types/core/conversation/seal.d.ts +32 -9
  63. package/dist/types/core/conversation-agent-tools.d.ts +939 -0
  64. package/dist/types/core/federation/directory-proof.d.ts +8 -0
  65. package/dist/types/core/federation/proof-state.d.ts +114 -0
  66. package/dist/types/core/federation/roster-proof.d.ts +7 -0
  67. package/dist/types/core/index.d.ts +11 -6
  68. package/dist/types/core/jelly-host.d.ts +1 -1
  69. package/dist/types/core/schema/common.d.ts +33 -6
  70. package/dist/types/core/schema/consent.d.ts +29 -8
  71. package/dist/types/core/schema/contact-request.d.ts +264 -0
  72. package/dist/types/core/schema/conv-frames.d.ts +1204 -0
  73. package/dist/types/core/schema/conversation.d.ts +1410 -264
  74. package/dist/types/core/schema/directory-frames.d.ts +8 -3
  75. package/dist/types/core/schema/envelope.d.ts +85 -130
  76. package/dist/types/core/schema/errors.d.ts +5 -1
  77. package/dist/types/core/schema/guest-invitation.d.ts +51 -0
  78. package/dist/types/core/schema/index.d.ts +5 -0
  79. package/dist/types/core/schema/ipc/consent.d.ts +464 -62
  80. package/dist/types/core/schema/ipc/conversation-views.d.ts +1199 -0
  81. package/dist/types/core/schema/ipc/conversations.d.ts +4355 -0
  82. package/dist/types/core/schema/ipc/device.d.ts +10 -0
  83. package/dist/types/core/schema/ipc/jelly.d.ts +16 -111
  84. package/dist/types/core/schema/ipc/shapes.d.ts +211 -95
  85. package/dist/types/core/schema/ipc/surfaces.d.ts +26 -427
  86. package/dist/types/core/schema/ipc/tasks.d.ts +210 -0
  87. package/dist/types/core/schema/ipc.d.ts +3859 -890
  88. package/dist/types/core/schema/relay.d.ts +12 -4
  89. package/dist/types/core/testing.d.ts +1 -1
  90. package/dist/types/core/version.d.ts +1 -1
  91. package/dist/types/ipc/client.d.ts +4 -1
  92. package/dist/types/ipc/server.d.ts +16 -1
  93. package/dist/types/ipc/testing.d.ts +21 -8
  94. package/package.json +1 -1
@@ -0,0 +1,87 @@
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
+ import { ActionToken, PersonTask } from "./shapes.js";
10
+ const Count = z.number().int().nonnegative().safe();
11
+ const Seq = z.number().int().nonnegative().safe();
12
+ const Name = z.string().max(200);
13
+ export const ParticipantView = z.object({
14
+ p: ParticipantId,
15
+ kind: z.enum(["person", "agent"]),
16
+ role: ConvRole,
17
+ state: z.enum(["member", "invited"]),
18
+ name: Name,
19
+ handle: z.string().max(64).optional(),
20
+ org: z.enum(["same", "other", "none"]),
21
+ presence: z.enum(["online", "away", "offline", "unknown"]),
22
+ agentState: z.enum(["idle", "busy", "kept"]).optional(),
23
+ respondTo: RespondTo.optional(),
24
+ });
25
+ /** Names travel with a notice so rendering it does not need one lookup per participant. */
26
+ export const NoticeView = z.object({
27
+ op: z.enum(["create", "add", "accept", "join", "leave", "remove", "role", "seat", "settings", "archive", "meta", "policy"]),
28
+ by: ParticipantView.pick({ p: true, kind: true, name: true }),
29
+ participants: z.array(ParticipantView.pick({ p: true, kind: true, name: true })).max(LIMITS.convOpParticipants).optional(),
30
+ fields: z.array(z.enum(["name", "topic", "purpose", "visibility", "history", "retainDays", "post", "invite", "pins", "agentContext", "bridges"])).max(12).optional(),
31
+ });
32
+ /** A file projection deliberately excludes the key and nonce in the encrypted wire attachment. */
33
+ export const FileView = z.object({
34
+ blob: z.string().regex(/^b_[A-Za-z0-9_-]{43}$/),
35
+ name: z.string().max(255), mime: z.string().max(100), bytes: z.number().int().positive().max(LIMITS.blobBytes),
36
+ });
37
+ /**
38
+ * `cancel`: the source-issued action that stops the task (spec §8.2, "Action tokens"), redeemed with
39
+ * `task.cancel {taskId, token}`. Only on a task this person's agent asked that has not ended, and only on the human tier.
40
+ */
41
+ export const TaskView = z.object({ id: Ulid, state: TaskState, cancel: z.strictObject({ name: z.literal("cancel"), token: ActionToken }).optional() });
42
+ export const EventView = z.object({
43
+ cid: ConvId, id: Ulid, seq: z.number().int().positive().safe(), at: Millis,
44
+ author: ParticipantView.pick({ p: true, kind: true, name: true }),
45
+ kind: z.enum(["line", "task", "notice"]),
46
+ text: z.string().max(LIMITS.lineChars).optional(), format: z.enum(["plain", "md"]).optional(),
47
+ thread: Ulid.optional(), broadcast: z.boolean().optional(),
48
+ mentions: z.array(Mention).max(LIMITS.mentions).optional(), mentionsMe: z.boolean(),
49
+ files: z.array(FileView).max(LIMITS.files).optional(), edited: Millis.optional(), deleted: z.boolean().optional(),
50
+ reactions: z.array(z.object({ key: z.string().min(1).max(64), count: Count, mine: z.boolean() })).max(LIMITS.reactionsPerEvent).optional(),
51
+ pinned: z.boolean().optional(),
52
+ replies: z.object({ count: Count, lastAt: Millis, participants: z.array(ParticipantId).max(5), unread: Count.optional(), following: z.boolean().optional() }).optional(),
53
+ task: TaskView.optional(), notice: NoticeView.optional(),
54
+ delivery: z.enum(["pending", "sent", "delivered", "read", "failed"]).optional(),
55
+ clientId: z.string().min(1).max(200).optional(),
56
+ }).refine((event) => !event.deleted || (event.text === undefined && event.files === undefined), {
57
+ message: "a deleted event contains no text or files",
58
+ });
59
+ export const ConvView = z.object({
60
+ cid: ConvId, kind: ConvKind, visibility: Visibility.optional(), history: HistoryPolicy,
61
+ retainDays: z.number().int().min(1).max(LIMITS.retainDaysMax), agentContext: AgentContext,
62
+ name: z.string().max(80).optional(), topic: z.string().max(250).optional(), purpose: z.string().max(250).optional(),
63
+ archived: z.boolean(), role: ConvRole, participants: Count, dmWith: ParticipantView.optional(), last: EventView.optional(),
64
+ headSeq: Seq, readUpTo: Seq.optional(), unread: Count.optional(), mentions: Count.optional(), threadsUnread: Count.optional(),
65
+ notify: z.enum(["all", "mentions", "none"]).optional(), mutedUntil: Millis.optional(), draft: z.string().max(8000).optional(),
66
+ });
67
+ export const ActivityBody = z.object({
68
+ session: SessionName, turn: Ulid, seq: Seq,
69
+ source: z.object({ item: Ulid.optional(), cid: ConvId.optional(), task: Ulid.optional() }).optional(),
70
+ kind: z.enum(["turn_started", "step", "turn_ended", "error", "cancelled"]), summary: z.string().max(200).optional(),
71
+ });
72
+ export const ConvStreamEvent = z.discriminatedUnion("type", [
73
+ z.object({ type: z.literal("event"), pos: Seq, event: EventView }),
74
+ z.object({ type: z.literal("event.update"), pos: Seq, cid: ConvId, id: Ulid,
75
+ change: z.enum(["edit", "delete", "react", "pin", "replies", "delivery", "task"]), event: EventView }),
76
+ z.object({ type: z.literal("conv"), pos: Seq, conv: ConvView }),
77
+ z.object({ type: z.literal("member"), pos: Seq, cid: ConvId, participant: ParticipantView,
78
+ change: z.enum(["added", "invited", "removed", "role", "seat", "presence"]) }),
79
+ z.object({ type: z.literal("read"), pos: Seq, cid: ConvId, readUpTo: Seq, unread: Count, mentions: Count }),
80
+ z.object({ type: z.literal("typing"), cid: ConvId, p: ParticipantId, thread: Ulid.optional(), until: Millis }),
81
+ z.object({ type: z.literal("activity"), body: ActivityBody }),
82
+ z.object({ type: z.literal("gap"), pos: Seq }),
83
+ /** Human tier, person-level `conv.sync` only (no session key): a task of this person's agents changed (spec §12.4). */
84
+ z.object({ type: z.literal("task"), pos: Seq, task: PersonTask }),
85
+ ]).refine((entry) => entry.type !== "event.update" || (entry.cid === entry.event.cid && entry.id === entry.event.id), {
86
+ message: "event.update names the same conversation and event as its projection",
87
+ });
@@ -0,0 +1,132 @@
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
+ /**
37
+ * `resend`: a request sent before the cut-over and held unanswered; it must be sent again. `outdated`: the recipient
38
+ * accepted it on a client before conversations, which cannot open the dm; they update, then it is sent again (§8.3).
39
+ */
40
+ state: z.enum(["held", "pending-delivery", "resend", "outdated"]),
41
+ });
42
+ 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 });
43
+ export const ConvListResult = ConvPage;
44
+ export const ConvReadParams = z.strictObject({ key: Key, ...Cid, thread: Ulid.optional(), before: Seq.optional(), after: Seq.optional(), around: Ulid.optional(), limit: Limit, cursor: Cursor })
45
+ .refine((p) => [p.before, p.after, p.around, p.cursor].filter((v) => v !== undefined).length <= 1, "choose one page position");
46
+ export const ConvReadResult = z.union([EventPage, Hold]);
47
+ export const ConvSendParams = z.strictObject({ key: Key, cid: ConvId.optional(), to: ParticipantId.optional(), ...LineFields, answer: Ulid.optional(), clientId: z.string().min(1).max(200) })
48
+ .refine((p) => (p.cid === undefined) !== (p.to === undefined), "choose exactly one of cid or to")
49
+ .refine((p) => p.broadcast !== true || p.thread !== undefined, "broadcast needs a thread");
50
+ export const ConvSendResult = z.union([z.object({ cid: ConvId, id: Ulid, clientId: z.string().min(1).max(200), stage: z.literal("pending") }), Hold]);
51
+ export const ConvSyncParams = z.strictObject({ key: Key, after: Seq.optional(), waitMs: z.number().int().min(0).max(25_000).optional(), limit: Limit });
52
+ export const ConvSyncResult = z.object({ events: z.array(ConvStreamEvent).max(50), pos: Seq });
53
+ 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() })
54
+ .refine((p) => p.kind !== "dm" || p.participants.length === 2, "a dm has exactly two participants")
55
+ .refine((p) => p.kind !== "group" || (p.participants.length >= 3 && p.participants.length <= LIMITS.groupParticipants), "a group has 3 to 50 participants")
56
+ .refine((p) => p.kind === "channel" || p.visibility === undefined, "only a channel has visibility");
57
+ export const ConvSeatParams = z.strictObject({ ...Cid, p: ParticipantId, device: MachineId.optional(), respondTo: RespondTo.optional() })
58
+ .refine((p) => p.p.includes("/"), "a seat names an agent");
59
+ export const PeopleGetParams = z.strictObject({ people: z.array(PersonId).min(1).max(100).refine((p) => new Set(p).size === p.length, "each person once") });
60
+ export const PeopleGetResult = z.object({ people: z.array(PersonSummary).max(100), missing: z.array(PersonId).max(100) });
61
+ /** Socket tiers are declared alongside this table below. */
62
+ export const CONVERSATION_METHODS = {
63
+ "conv.list": Read(ConvListParams, ConvListResult),
64
+ "conv.get": Read(z.strictObject({ key: Key, ...Cid }), ConvView),
65
+ "conv.members": Read(PageParams, z.object({ participants: z.array(ParticipantView).max(50), cursor: Cursor })),
66
+ "conv.read": Read(ConvReadParams, ConvReadResult),
67
+ "conv.threads": Read(z.strictObject({ ...Cid, filter: z.enum(["all", "unread", "following"]).optional(), ...Page }), EventPage),
68
+ "conv.send": { params: ConvSendParams, result: ConvSendResult, errors: WriteErrors },
69
+ "conv.react": Write(z.strictObject({ key: Key, ...Id, reaction: z.string().min(1).max(64), on: z.boolean() })),
70
+ "conv.edit": Write(z.strictObject({ key: Key, ...Id, text: Text, mentions: z.array(Mention).max(LIMITS.mentions).optional() })),
71
+ "conv.delete": Write(z.strictObject({ key: Key, ...Id })),
72
+ "conv.pin": Write(z.strictObject({ ...Id, on: z.boolean() })),
73
+ "conv.create": { params: ConvCreateParams, result: z.object({ cid: ConvId }), errors: WriteErrors },
74
+ "conv.add": Write(z.strictObject({ ...Cid, participants: z.array(Seat).min(1).max(LIMITS.convOpParticipants) })),
75
+ "conv.remove": Write(z.strictObject({ ...Cid, participants: z.array(ParticipantId).min(1).max(LIMITS.convOpParticipants) })),
76
+ "conv.role": Write(z.strictObject({ ...Cid, p: ParticipantId, role: ConvRole })),
77
+ "conv.seat": Write(ConvSeatParams),
78
+ "conv.settings": Write(z.strictObject({ ...Cid, ...OptionalSettings }).refine((p) => Object.values(p).some((v, i) => i > 0 && v !== undefined), "set at least one setting")),
79
+ "conv.meta": Write(z.strictObject({ ...Cid, meta: ConvMeta })),
80
+ "conv.archive": Write(z.strictObject({ ...Cid, on: z.boolean() })),
81
+ "conv.leave": Write(z.strictObject(Cid)),
82
+ "conv.join": Write(z.strictObject(Cid)),
83
+ "conv.accept": Write(z.strictObject(Cid)),
84
+ "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) })),
85
+ "conv.publish": Write(z.strictObject({ ...Cid, on: z.boolean() })),
86
+ "conv.markRead": Write(z.strictObject({ ...Cid, upTo: Seq, thread: Ulid.optional() })),
87
+ "conv.markUnread": Write(z.strictObject({ ...Cid, from: z.number().int().positive().safe() })),
88
+ "conv.prefs": Write(z.strictObject({ ...Cid, notify: z.enum(["all", "mentions", "none"]), muteUntil: Millis.optional(), receipts: z.boolean().optional(), typing: z.boolean().optional() })),
89
+ "conv.follow": Write(z.strictObject({ ...Cid, thread: Ulid, on: z.boolean() })),
90
+ "conv.save": Write(z.strictObject({ ...Id, on: z.boolean() })),
91
+ "conv.saved": Read(PageParams, EventPage),
92
+ "conv.draft": Write(z.strictObject({ ...Cid, thread: Ulid.optional(), text: z.string().max(8000) })),
93
+ "conv.schedule": { params: z.strictObject({ ...Cid, ...LineFields, at: Millis, clientId: z.string().min(1).max(200) }), result: z.object({ id: Ulid }), errors: WriteErrors },
94
+ "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 })),
95
+ "conv.unschedule": Write(z.strictObject({ id: Ulid })),
96
+ "conv.typing": Write(z.strictObject({ ...Cid, thread: Ulid.optional() })),
97
+ "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])),
98
+ "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])),
99
+ "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 })),
100
+ "context.get": Read(Empty, z.object({ rules: ContextRules, defaults: ContextRules, effective: z.array(z.object({ agent: SessionName, cid: ConvId, access: ContextAccess })).max(500) })),
101
+ "context.set": Write(z.strictObject({ rules: ContextRules })),
102
+ "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) })),
103
+ "conv.policy": Write(z.strictObject({ ...Cid, agentContext: AgentContext })),
104
+ "conv.localRetention": Write(z.strictObject({ ...Cid, days: z.number().int().min(1).max(LIMITS.retainDaysMax) })),
105
+ "conv.backfill": { params: z.strictObject(Cid), result: z.object({ indexedFrom: Millis.optional(), complete: z.boolean() }), errors: WriteErrors },
106
+ "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) })),
107
+ "session.activity": Write(z.strictObject({ key: SessionKey, body: ActivityBody })),
108
+ "turn.cancel": Write(z.strictObject({ device: MachineId, session: SessionName, turn: Ulid.optional() })),
109
+ "conv.sync": Read(ConvSyncParams, ConvSyncResult),
110
+ "people.get": Read(PeopleGetParams, PeopleGetResult),
111
+ "sessions.list": Read(Empty, z.object({ sessions: z.array(z.object({ agent: SessionName, state: z.enum(["idle", "busy", "kept"]), seats: z.array(ConvId) })) })),
112
+ "request.waiting": Read(z.strictObject(Page), z.object({ items: z.array(WaitingOnThemView).max(50), cursor: Cursor })),
113
+ };
114
+ /** Staged contracts have no live handler or socket tier until their owning lane ships. */
115
+ export const STAGED_CONVERSATION_METHODS = [
116
+ "conv.catalogue", "conv.publish", "conv.markRead", "conv.markUnread", "conv.prefs", "conv.follow",
117
+ "conv.save", "conv.saved", "conv.draft", "conv.schedule", "conv.scheduled", "conv.unschedule", "conv.typing",
118
+ "context.get", "context.set", "context.set.preview", "conv.localRetention", "file.get", "session.activity", "turn.cancel",
119
+ ];
120
+ export const LIVE_CONVERSATION_METHODS = Object.fromEntries(Object.entries(CONVERSATION_METHODS)
121
+ .filter(([name]) => !STAGED_CONVERSATION_METHODS.includes(name)));
122
+ /** agent means both sockets, human means admin socket only. session.activity is runtime-only, not a model tool. */
123
+ export const CONVERSATION_TIERS = {
124
+ "conv.list": "agent", "conv.get": "human", "conv.members": "human", "conv.read": "agent", "conv.threads": "human", "conv.send": "agent",
125
+ "conv.react": "agent", "conv.edit": "agent", "conv.delete": "agent", "conv.pin": "human", "conv.create": "human", "conv.add": "human",
126
+ "conv.remove": "human", "conv.role": "human", "conv.seat": "human", "conv.settings": "human", "conv.meta": "human", "conv.archive": "human",
127
+ "conv.leave": "human", "conv.join": "human", "conv.accept": "human", "conv.catalogue": "human", "conv.publish": "human", "conv.markRead": "human",
128
+ "conv.markUnread": "human", "conv.prefs": "human", "conv.follow": "human", "conv.save": "human", "conv.saved": "human", "conv.draft": "human",
129
+ "conv.schedule": "human", "conv.scheduled": "human", "conv.unschedule": "human", "conv.typing": "human", "conv.search": "agent", "conv.context": "agent",
130
+ "conv.access": "agent", "context.get": "human", "context.set": "human", "context.set.preview": "human", "conv.policy": "human", "conv.localRetention": "human",
131
+ "conv.backfill": "human", "file.get": "human", "session.activity": "agent", "turn.cancel": "human", "conv.sync": "agent", "people.get": "human", "request.waiting": "human", "sessions.list": "human",
132
+ };
@@ -52,6 +52,8 @@ export const DeviceChangeView = z.object({
52
52
  approver: MachineLabel.optional(),
53
53
  at: Millis,
54
54
  });
55
+ /** A presence feature name, such as `tasks`, `conversations` or `reach`. */
56
+ export const PresenceFeature = z.string().regex(/^[a-z][a-z0-9-]{0,31}$/, "a feature name");
55
57
  /** One device of this device's person. */
56
58
  export const DeviceListEntry = z.object({
57
59
  device: MachineId,
@@ -67,9 +69,16 @@ export const DeviceListEntry = z.object({
67
69
  via: z.enum(["host", "browser"]).optional(),
68
70
  current: z.boolean(),
69
71
  self: z.boolean(),
72
+ /** This local Jelly host has no attached person window. Unknown remote presence is omitted. */
73
+ away: z.literal(true).optional(),
70
74
  since: Millis.optional(),
71
75
  /** For a removed device of a directory realm: when, why and by which device. */
72
76
  unbound: z.object({ at: Millis, reason: UnbindReason, by: MachineId }).optional(),
77
+ /**
78
+ * The protocol features the device advertises (spec §8.2, "Devices"): this device's own, or the last verified presence
79
+ * of another device of the person. Absent when no presence of it has been seen, which is unknown, not none.
80
+ */
81
+ features: z.array(PresenceFeature).max(32).optional(),
73
82
  });
74
83
  /** A link request's text (`messaging-link.…`, spec §15.3): what a new device shows and its person's device reads. */
75
84
  const LinkText = z.string().min(1).max(2048);
@@ -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,11 +13,10 @@ 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
- * A notice's id, stable per source: `m_<line id>` (a line), `r_<request id>` (a held request), `a_<id>` (an alert).
21
- * A replay repeats the same ids, so a host rings once.
17
+ * A notice's id, stable per source: `m_<line id>` (a line), `m_task<task id>` (a task another person's agent asked of
18
+ * one of this person's agents), `r_<request id>` (a held request), `a_<id>` (an alert). A replay repeats the same ids, so
19
+ * a host rings once.
22
20
  */
23
21
  export const NoticeId = z.string().regex(/^[mra]_[0-9A-Za-z]{1,64}$/, "notice id");
24
22
  /** Most characters of a notice's `line`. */
@@ -37,23 +35,26 @@ export const NoticeDevice = DeviceChangeView.extend({
37
35
  remove: z.enum(["token", "sign-in", "team"]).optional(),
38
36
  });
39
37
  /**
40
- * One notice (design §13.11). `message`: a line in a DM or a channel from another person or their agent, or an
41
- * agent's line to its own person; `request`: a request held for the person; `alert`: an alarm of this device, a
42
- * change of the person's other devices (with `device`), or trust the person gave that ends within 7 days (with `from`).
38
+ * One notice (design §13.11). `message`: a line in a DM or a channel from another person or their agent, an
39
+ * agent's line to its own person, or a task another person's agent asked of one of this person's agents (with `key`
40
+ * `task:<task id>`, and `from` that person), never a request: it waits on the agent, not the person; `request`: a request
41
+ * held for the person (an ask held for consent stays one); `alert`: an alarm of this device, a change of the person's
42
+ * other devices (with `device`), or trust the person gave that ends within 7 days (with `from`).
43
43
  * `line` is the first line of a message, through `cleanPeerText`, only when the caller asked for lines; alerts carry
44
44
  * fixed text. `from.name` and `from.email` are untrusted text in a hosted realm.
45
45
  */
46
46
  export const Notice = z.object({
47
47
  id: NoticeId,
48
48
  category: z.enum(["message", "request", "alert"]),
49
- conversation: ConversationRef.optional(),
50
49
  from: z.object({ name: z.string().max(200), email: z.string().max(320).optional(), org: z.enum(["same", "other", "none"]) }).optional(),
51
50
  line: z.string().max(NOTICE_LINE_CHARS).optional(),
52
51
  device: NoticeDevice.optional(),
52
+ /** Where a host opens it in its section: `task:<task id>` for a task notice; absent for the others. */
53
+ key: z.string().regex(/^task:[0-9A-HJKMNP-TV-Z]{26}$/, "task:<task id>").optional(),
53
54
  at: Millis,
54
55
  });
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() });
56
+ /** What the person's badges count: requests held for them. Conversation read state remains staged. */
57
+ export const NoticeCounts = z.object({ requests: z.number().int().nonnegative() });
57
58
  /** `daemon.health` (spec §8.2): the daemon's state, with no secrets and no counts. */
58
59
  export const DaemonHealth = z.object({
59
60
  state: z.enum(["starting", "running", "stopping"]),
@@ -66,43 +67,12 @@ export const DaemonHealth = z.object({
66
67
  version: z.string().max(64),
67
68
  storeSchema: z.number().int().positive(),
68
69
  });
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
70
  export const JELLY_METHODS = {
81
71
  /**
82
72
  * Agent tier. A cheap status for a host that polls: the daemon's state, the relay, the directory, an alarm, and
83
73
  * whether it is busy. No counts: they are the person's (`notice.list`).
84
74
  */
85
75
  "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
76
  /**
107
77
  * Human tier; host scope in `deployment: "jelly"`. Replays the unacknowledged notices after `after` (all of them
108
78
  * without it), oldest first, as `notice` events on this connection, then sends each new one. With `lines`, a
@@ -137,7 +107,9 @@ export const JELLY_METHODS = {
137
107
  * whose own person's mail may wake the host, and the tasks those sessions await (their unfinished requests). The relay
138
108
  * presents the ticket once, for such traffic only, while this device is offline (spec §7, "Wake tickets"). `relay`:
139
109
  * `armed` once the relay holds it, `pending` while it is not connected (it is sent at the next connection);
140
- * `awaiting`: how many task requests the relay may wake for. A directory realm only.
110
+ * `awaiting`: how many task requests the relay may wake for; `dms`: how many dms the relay may wake for, those whose
111
+ * other person shares a channel with this person and is not blocked or muted (spec §8.11, "Wake tickets"). A
112
+ * directory realm only.
141
113
  */
142
114
  "wake.arm": {
143
115
  params: z.strictObject({
@@ -145,7 +117,7 @@ export const JELLY_METHODS = {
145
117
  ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
146
118
  agents: z.array(SessionName).max(WAKE_AGENTS),
147
119
  }),
148
- result: z.object({ relay: z.enum(["armed", "pending"]), awaiting: z.number().int().nonnegative() }),
120
+ result: z.object({ relay: z.enum(["armed", "pending"]), awaiting: z.number().int().nonnegative(), dms: z.number().int().nonnegative() }),
149
121
  },
150
122
  /** Admin tier; host scope in `deployment: "jelly"`. The relay forgets this device's wake ticket (`pending` until connected). */
151
123
  "wake.disarm": { params: z.strictObject({}), result: z.object({ relay: z.enum(["disarmed", "pending"]) }) },
@@ -5,12 +5,19 @@
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";
12
13
  /** Opaque, client-chosen session key, stable for the life of a client session, e.g. "opencode:<sessionID>". */
13
14
  export const SessionKey = z.string().min(1).max(200);
15
+ /**
16
+ * A source-issued action token (spec §8.2, "Action tokens"): 32 random bytes, base64url. The daemon mints one per
17
+ * action, item and human-tier view, keeps it in memory only, and spends it at its first presentation. Opaque to
18
+ * clients; never logged.
19
+ */
20
+ export const ActionToken = z.string().regex(/^[A-Za-z0-9_-]{43}$/, "action token");
14
21
  /**
15
22
  * When the recipient will likely see a message (spec §8.2): a hint from its machine's last presence
16
23
  * at send time, never a promise.
@@ -41,42 +48,10 @@ export const DirectoryEntry = z.object({
41
48
  self: z.boolean().describe("true for sessions registered on this machine"),
42
49
  /**
43
50
  * 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.
51
+ * same `SendExpect` a task send result carries (spec §8.2). Absent when no presence lists it.
45
52
  */
46
53
  expect: SendExpect.optional(),
47
54
  });
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
55
  /** The other end of an item or a task: a person's device and, when an agent wrote it, the agent. */
81
56
  export const PeerRef = z.object({
82
57
  address: SessionAddress,
@@ -86,9 +61,12 @@ export const PeerRef = z.object({
86
61
  label: MachineLabel,
87
62
  });
88
63
  export const InboxItem = z.object({
89
- id: Ulid.describe("envelope id"),
64
+ id: Ulid.describe("task envelope id, or conversation event id"),
90
65
  key: SessionKey,
91
- from: PeerRef,
66
+ from: PeerRef.extend({ address: z.union([SessionAddress, ParticipantId]), device: MachineId.optional(), label: MachineLabel.optional() }),
67
+ /** A projected line is addressed by this pair. Task envelopes have neither field. */
68
+ cid: ConvId.optional(),
69
+ eventId: Ulid.optional(),
92
70
  kind: z.enum(["message", "task"]),
93
71
  messageId: z.string().min(1).max(128),
94
72
  taskId: Ulid.optional(),
@@ -108,10 +86,11 @@ export const InboxItem = z.object({
108
86
  * Absent only on items stored before consent.
109
87
  */
110
88
  consent: z.enum(["self", "guarded", "trusted"]).optional(),
111
- });
89
+ }).refine((item) => item.cid !== undefined
90
+ ? item.eventId === item.id && item.kind === "message" && item.taskId === undefined
91
+ : item.eventId === undefined && PeerRef.safeParse(item.from).success, "a projected line has a matching cid/eventId; an envelope has device provenance");
112
92
  /**
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).
93
+ * A task continuation and its stage from this daemon's own delivery records (spec §6.3).
115
94
  */
116
95
  export const TaskFollowUp = z.object({
117
96
  envelopeId: Ulid,
@@ -121,7 +100,18 @@ export const TaskFollowUp = z.object({
121
100
  /** When it reached `stage`. */
122
101
  at: Millis,
123
102
  });
124
- export const TaskRecord = z.object({
103
+ /** Most characters of `TaskRecord.signIn.tool`: the executor's words, cleaned and capped like a preview. */
104
+ export const TASK_SIGN_IN_TOOL_CHARS = LIMITS.previewChars;
105
+ /** Most characters of `TaskRecord.refusal.reason`: the executor's words, cleaned and capped like a message request. */
106
+ export const TASK_REFUSAL_CHARS = LIMITS.requestChars;
107
+ /** An id a host's engine chose for its session or its pending approval or question: an opaque, plain identifier. */
108
+ export const HostLocalId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/, "a plain identifier of at most 200 characters");
109
+ /**
110
+ * The executor host's own pending approval or question that an `AUTH_REQUIRED` or `INPUT_REQUIRED` task waits on
111
+ * (spec §8.2, "The task view"): set by the person's own client with `task.question`, held in this daemon's memory only.
112
+ */
113
+ export const TaskQuestion = z.strictObject({ sessionId: HostLocalId, approvalId: HostLocalId });
114
+ const TaskRecordFields = z.object({
125
115
  task: Task.extend({ id: Ulid }),
126
116
  role: z.enum(["requester", "executor"]),
127
117
  key: SessionKey.describe("local session that owns this side of the task"),
@@ -136,12 +126,53 @@ export const TaskRecord = z.object({
136
126
  * Absent when there are none.
137
127
  */
138
128
  followUps: z.array(TaskFollowUp).min(1).max(LIMITS.taskFollowUps).optional(),
129
+ // ---- the task view (spec §8.2, "The task view"): computed by this daemon as it projects the record; never stored or sent ----
130
+ /** The counterpart agent: the session the verified envelopes name (`peer.agent`). */
131
+ agent: SessionName.optional(),
132
+ /**
133
+ * The counterpart agent's person: `peer.person` and the display name the realm verified for it (the person id when
134
+ * the realm cannot name them). Never parsed from an agent's name. Absent while this daemon has not named them yet.
135
+ */
136
+ person: z.object({ id: PersonId, name: z.string().max(200) }).optional(),
137
+ /** That person's relation to this device's person, as `ParticipantView.org` computes it; absent with `person`. */
138
+ org: z.enum(["same", "other", "none"]).optional(),
139
+ /**
140
+ * `AUTH_REQUIRED` only. `by`: `you` when the executor is an agent of this device's person, else `them`, computed here,
141
+ * never read from the peer. `tool`: the tool the executor named (`metadata[URI.signIn]` of its status message), as
142
+ * plain text cleaned and capped at `TASK_SIGN_IN_TOOL_CHARS`; absent when it named none.
143
+ */
144
+ signIn: z.object({ tool: z.string().min(1).max(TASK_SIGN_IN_TOOL_CHARS).optional(), by: z.enum(["you", "them"]) }).optional(),
145
+ /** `REJECTED` only, when the executor gave words: its status message's text, cleaned and capped at `TASK_REFUSAL_CHARS`. */
146
+ refusal: z.object({ reason: z.string().min(1).max(TASK_REFUSAL_CHARS) }).optional(),
147
+ /**
148
+ * The executor's side, on the admin socket only (human tier), in `AUTH_REQUIRED` or `INPUT_REQUIRED`: the host's own
149
+ * approval or question the task waits on (`task.question`). Never on the agent tier, never sent to the peer.
150
+ */
151
+ question: TaskQuestion.optional(),
139
152
  });
153
+ /** The task view's fields go with the states they belong to (spec §8.2, "The task view"). */
154
+ function taskViewRules(schema) {
155
+ return schema
156
+ .refine((r) => r.signIn === undefined || r.task.status.state === "TASK_STATE_AUTH_REQUIRED", "signIn is for an AUTH_REQUIRED task")
157
+ .refine((r) => r.refusal === undefined || r.task.status.state === "TASK_STATE_REJECTED", "refusal is for a REJECTED task")
158
+ .refine((r) => r.question === undefined || (r.role === "executor" && (r.task.status.state === "TASK_STATE_AUTH_REQUIRED" || r.task.status.state === "TASK_STATE_INPUT_REQUIRED")), "question is for the executor's side of an AUTH_REQUIRED or INPUT_REQUIRED task");
159
+ }
160
+ export const TaskRecord = taskViewRules(TaskRecordFields);
161
+ /**
162
+ * One side of a task one of this person's agents on this device is in, as the person's own clients see it (spec §8.2,
163
+ * "The person's tasks"): the task view without the session key (a bearer credential), with `session` the agent's name
164
+ * and, on the requester side of a task that has not ended, J2's `cancel` action minted for this view.
165
+ */
166
+ export const PersonTask = taskViewRules(TaskRecordFields.omit({ key: true }).extend({
167
+ /** Never present: a session key is a bearer credential for its session. */
168
+ key: z.never().optional(),
169
+ session: SessionName.describe("this person's agent on this device that holds this side of the task"),
170
+ cancel: z.strictObject({ name: z.literal("cancel"), token: ActionToken }).optional(),
171
+ })).refine((r) => r.cancel === undefined || r.role === "requester", "cancel is for the requester side");
140
172
  /**
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).
173
+ * Where a retained task envelope went: an agent session `person/agent@label`.
143
174
  */
144
- export const SendTarget = z.union([SessionAddress, PersonId, z.string().regex(/^#[a-z][a-z0-9-]{0,31}$/, "#<channel>")]);
175
+ export const SendTarget = SessionAddress;
145
176
  export const SendResult = z.object({
146
177
  to: SendTarget,
147
178
  envelopeId: Ulid,
@@ -153,33 +184,3 @@ export const SendResult = z.object({
153
184
  /** Absent when the stage is `rejected` or `expired`, or when no presence lists the session. */
154
185
  expect: SendExpect.optional(),
155
186
  });
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
- });