@joe-broadhead/messaging 0.8.0 → 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 (50) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/dist/bin/messaging-mcp.js +202 -31
  3. package/dist/bin/messaging-relay.js +291 -43
  4. package/dist/bin/messaging.js +290 -32
  5. package/dist/bin/messagingd.js +1158 -138
  6. package/dist/lib/modules/core/agent/present.js +5 -0
  7. package/dist/lib/modules/core/agent-tools.js +12 -0
  8. package/dist/lib/modules/core/consent.js +54 -14
  9. package/dist/lib/modules/core/contact-card.js +3 -3
  10. package/dist/lib/modules/core/index.js +1 -1
  11. package/dist/lib/modules/core/jelly-host.js +3 -0
  12. package/dist/lib/modules/core/schema/common.js +2 -0
  13. package/dist/lib/modules/core/schema/consent.js +20 -3
  14. package/dist/lib/modules/core/schema/conv-frames.js +10 -3
  15. package/dist/lib/modules/core/schema/directory-frames.js +8 -3
  16. package/dist/lib/modules/core/schema/errors.js +3 -2
  17. package/dist/lib/modules/core/schema/ipc/consent.js +85 -10
  18. package/dist/lib/modules/core/schema/ipc/conversation-views.js +8 -1
  19. package/dist/lib/modules/core/schema/ipc/conversations.js +5 -1
  20. package/dist/lib/modules/core/schema/ipc/device.js +7 -0
  21. package/dist/lib/modules/core/schema/ipc/jelly.js +14 -7
  22. package/dist/lib/modules/core/schema/ipc/shapes.js +60 -1
  23. package/dist/lib/modules/core/schema/ipc.js +51 -3
  24. package/dist/lib/modules/core/version.js +1 -1
  25. package/dist/spec/messaging-v1.md +321 -49
  26. package/dist/types/core/agent/present.d.ts +1 -1
  27. package/dist/types/core/agent-tools.d.ts +32 -0
  28. package/dist/types/core/consent.d.ts +30 -7
  29. package/dist/types/core/contact-card.d.ts +3 -3
  30. package/dist/types/core/conversation-agent-tools.d.ts +21 -0
  31. package/dist/types/core/index.d.ts +1 -1
  32. package/dist/types/core/schema/common.d.ts +2 -0
  33. package/dist/types/core/schema/consent.d.ts +23 -2
  34. package/dist/types/core/schema/conv-frames.d.ts +6 -2
  35. package/dist/types/core/schema/directory-frames.d.ts +8 -3
  36. package/dist/types/core/schema/envelope.d.ts +2 -0
  37. package/dist/types/core/schema/errors.d.ts +2 -0
  38. package/dist/types/core/schema/ipc/consent.d.ts +377 -15
  39. package/dist/types/core/schema/ipc/conversation-views.d.ts +176 -0
  40. package/dist/types/core/schema/ipc/conversations.d.ts +368 -0
  41. package/dist/types/core/schema/ipc/device.d.ts +6 -0
  42. package/dist/types/core/schema/ipc/jelly.d.ts +14 -6
  43. package/dist/types/core/schema/ipc/shapes.d.ts +198 -0
  44. package/dist/types/core/schema/ipc/surfaces.d.ts +1 -0
  45. package/dist/types/core/schema/ipc/tasks.d.ts +2 -0
  46. package/dist/types/core/schema/ipc.d.ts +826 -3
  47. package/dist/types/core/schema/relay.d.ts +4 -0
  48. package/dist/types/core/version.d.ts +1 -1
  49. package/dist/types/ipc/server.d.ts +6 -0
  50. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,156 @@ Versions follow [Semantic Versioning](https://semver.org); see
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.0] - 2026-11-11
10
+
11
+ **What Jelly asked for in opencode-messaging #17, #18, #19 and #24.** The person sees their agents' tasks as a view
12
+ (who does the task, for whom, a sign-in it waits on, why it was refused) and stops one with a source-issued action.
13
+ Inbound tasks become notices. A resting Jelly host wakes for a teammate's dm. The person answers two questions about
14
+ who may reach them. The 0.8.0 follow-ups for mixed versions, the `conv.caps` presence oracle and the control-op drain
15
+ are here too.
16
+
17
+ **Upgrade the relay, then the daemons, in any order after it.** 0.9.0 and 0.8.0 daemons interoperate (below). Stores
18
+ only grow: the daemon store goes to schema 5 and the relay database to schema 4, and a rollback keeps the data.
19
+
20
+ ### Compatibility
21
+
22
+ - **0.9.0 and 0.8.0 daemons interoperate** (spec §8.3). Nothing on the wire between daemons was removed or tightened.
23
+ - Conversations, message requests and tasks work in both directions. Both releases advertise the
24
+ `conversations` feature that `conv.caps` checks, so neither answers the other with `peer_outdated`.
25
+ - A 0.9.0 executor's sign-in names its tool in the task status as A2A extension data
26
+ (`metadata["urn:messaging:ext:sign-in:v1"]`, spec §6.2, "Sign-in"). A 0.8.0 requester still reads a valid status
27
+ and its text.
28
+ - A 0.9.0 device advertises the new presence feature `reach` (spec §6.5). A 0.8.0 device does not, and **does not
29
+ enforce the reach choices** (spec §6.9, "Older daemons do not enforce the choices"). A relation grant synced to a
30
+ 0.8.0 device of the same person is skipped there (`grant.sync.skipped`), and it keeps working.
31
+ - **dm wakes need the relay at 0.9.0** (relay schema 4). A 0.8.0 relay parses `wake.arm` without `dms` and still arms
32
+ the host's agents and awaited tasks, so a 0.9.0 host behind it arms no dm and wakes as before. A 0.8.0 daemon never
33
+ sends `dms`, and a 0.9.0 relay arms it as before.
34
+ - **A 0.8.0 daemon behind a 0.9.0 relay** gets the new optional `unknown` in a `conv.caps` answer only over the new
35
+ budget (below). It cannot parse that answer and goes ahead without one, as 0.8.0 already did when `conv.caps` was
36
+ refused.
37
+ - **Rollback keeps the data.** Daemon schema 5 and relay schema 4 are additive, with generation 2 and `min_reader` 1,
38
+ so 0.8.0 opens both. A 0.8.0 daemon ignores the reach tables and does not enforce the choices; a 0.8.0 relay
39
+ ignores `dir_wake_dms`, and the rows it leaves name a ticket that no longer counts.
40
+
41
+ ### Added
42
+
43
+ - **The task view (#17)** (spec §8.2, "The task view"). Every `TaskRecord` the daemon projects carries fields it
44
+ computes and never stores or sends:
45
+ - `agent`, the counterpart session; `person: {id, name}`, verified by the realm, never parsed from an agent's name;
46
+ `org` (`same`, `other` or `none`);
47
+ - `signIn: {tool?, by}` in `AUTH_REQUIRED`, where `by` (`you` or `them`) is computed by the viewer's own daemon and
48
+ never read from the peer. An executor names the tool with `task.update {signIn: {tool}}`;
49
+ - `refusal: {reason}` in `REJECTED`.
50
+ - `tool` and `reason` come only from the executor itself, through `cleanPeerText`, capped at
51
+ `TASK_SIGN_IN_TOOL_CHARS` and `TASK_REFUSAL_CHARS`. Agent tools show them fenced when the peer wrote them.
52
+ - **Task notices (#18)** (spec §8.2, "Notices"). A task another person's agent asked of one of this person's agents
53
+ files a `message` notice with `key: "task:<task id>"` when this device accepts it. It copies no text and is never a
54
+ `request`. An ask held for consent stays a `request` until its person answers. A task between the person's own
55
+ agents files none.
56
+ - **dm wakes (#19)** (spec §7 and §8.11, "Wake tickets").
57
+ - `wake.arm` takes `dms`, at most `WAKE_DMS` (256). The relay presents the ticket for a line in an armed dm by its
58
+ other person while the host is offline, within the existing budget (1 a minute, 6 an hour, 48 a day).
59
+ - The daemon arms the verified dms whose other person shares a channel with this person as a member, and returns
60
+ their count (`dms`). It never arms a person this person blocked or muted, or one their reach choices hide.
61
+ - It reads the candidates' directory views in batches (`LIMITS.dirLookupPersons` to a lookup) before it decides.
62
+ - The list is what held at the arm: a block while the host sleeps applies at the next arm, so at most one wake.
63
+ - Neither side logs a dm's id. The relay can't tell a line from other sealed events in an armed dm, so a reaction or
64
+ edit there may also wake the host, within the budget.
65
+ - **Consent choices (#24)** (spec §6.9, "Reach choices").
66
+ - `reach.get`, `reach.set {revision, changes}` and `reach.set.preview`, on the human tier only. "Who can message
67
+ you" is Open, Ask me first (the default) or Never for people outside the org. "How agents may ask yours" is Open,
68
+ Ask me first or Never for the org's agents.
69
+ - Each choice is the person's own signed grant on a relation subject (`{relation: "outside" | "org"}`). A grant for
70
+ one person or conversation wins over it.
71
+ - A stale `revision` is the new error `conflict`, and nothing changes.
72
+ - Under Never, an outsider whose person grant sets no `chat` is refused, and their lines are hidden in existing
73
+ conversations at read time. Choosing again shows them. The org, the person's own lines and outsiders' agents are
74
+ never hidden. A person the realm no longer knows stays visible.
75
+ - CLI: `messaging reach`.
76
+ - **Action tokens** (spec §8.2, "Action tokens"; design §12.5).
77
+ - On the human tier the daemon puts `{name, token}` on what it renders: `actions` on every `request.list` item, one
78
+ per answer its kind takes, and `cancel` on a task this person's agent asked that has not ended. The executor side
79
+ never gets `cancel`.
80
+ - A token binds the action, the item and the view: the admin connection, and under a Jelly host the grant's `dev`
81
+ and `mbr`. It is spent at its first presentation, matched or not, and lives 10 minutes. At most 256 are held per
82
+ view and 4,096 in all. They are in memory only, so a restart invalidates every one.
83
+ - A token is never logged or printed, and the agent tier never receives one or redeems one.
84
+ - `request.answer` and `request.answer.preview` take `token`, never with `all`. Under a Jelly host `request.answer`
85
+ requires it. `task.cancel` takes exactly one of `key` (the requester session) and `token` (the person, human
86
+ tier). A preview checks a token without spending it. Every failure is `grant_invalid`.
87
+ - **The person's tasks, and Stop** (spec §8.2, "The person's tasks").
88
+ - `tasks.list` and `tasks.get`, on the admin socket with no session key, Jelly class `view`, read every side of a
89
+ task this device's sessions hold, as `PersonTask`. That is the task view as the owner sees it, the agent's
90
+ `session` name and never its key, and `cancel`, which stops the task through `task.cancel {taskId, token}`.
91
+ - A person-level `conv.sync` carries `task` entries, rendered for each reader. Task changes share the replay
92
+ buffer with conversation changes.
93
+ - Only this device's tasks are listed.
94
+ - **`task.question`** (spec §8.2, "The task view").
95
+ - On the agent tier, from the executing session only. It ties a task in `AUTH_REQUIRED` or `INPUT_REQUIRED` to the
96
+ host's own pending approval or question, `{sessionId, approvalId}`.
97
+ - It is held in memory only and untied by any later status. It is shown only on the person's own reads, never on
98
+ the agent tier, in an event, or to the peer.
99
+ - When the session registered a host session, `sessionId` must be that one (`forbidden`).
100
+ - **The `reach` presence feature** (spec §6.5): a device that enforces the reach choices says so.
101
+ - **`device.list` features** (spec §8.2, "Devices"). Each of the person's own devices lists the presence features last
102
+ verified from it. They are absent when none was seen, which means unknown, not none.
103
+ - **`request.waiting` states `resend` and `outdated`** (spec §8.2, §8.3 "Mixed versions").
104
+ - `resend`: a message request sent from 0.7.1 that its recipient held and nobody answered is shown, not re-signed
105
+ or resent. A request still in flight at the upgrade can't be told from a line and is not shown.
106
+ - `outdated`: a request whose recipient device answered with a `delivered` receipt, which only an older client sends
107
+ on accepting. It must be sent again once they update.
108
+
109
+ ### Fixed
110
+
111
+ - **Presence for people who only talk in conversations** (spec §15.10). The presence audience, and the relation
112
+ `conv.caps` uses, counts accepted members of one conversation the relay sequences: an external dm only once
113
+ activated, never an invited participant. A cross-org contact no longer drops out of presence 30 days after their
114
+ last envelope.
115
+ - **A control op no longer fails with a spurious `conv_conflict`** when maintenance queues a share while it drains. It
116
+ drains until nothing is pending, up to 3 drains. A conversation that refills on every drain is `conv_conflict`, "the
117
+ conversation is busy sending; try again", which can be retried as is (spec §13).
118
+
119
+ ### Security
120
+
121
+ - **The engine always sends the strictest cut asked for a removed device** (spec §16.4, "Device cuts"). This was
122
+ an ordering bug in the daemon's conversation engine, present since 0.8.0, that no 0.8.0 IPC call could reach.
123
+ - Asked to rekey a device with an earlier `until` while a weaker rekey of that device was already queued or in
124
+ flight, the engine returned the queued one and dropped the earlier cut.
125
+ - No IPC method passes `until`, the engine isn't part of the package's exports, and the daemon's automatic rekeys
126
+ after a removal never ask for a stricter cut than the one they queue.
127
+ - Fixed and guarded by tests:
128
+ - a queued rekey stands in only when its cut is at least as strict;
129
+ - two cuts of one device combine as a union in the verifier;
130
+ - a cut covers the device's held entries.
131
+ - **`conv.caps` is no longer a free presence oracle** (spec §7). Answers about unrelated persons count against
132
+ `LIMITS.requestsPerDay` (20) distinct persons per asking person per UTC day, shared by that person's devices.
133
+ Related persons, and persons already asked about that day, are always answered. Past the budget the answer names
134
+ the rest in `unknown`, and `peer_outdated` still applies to anyone named `outdated`.
135
+ - **A token answers only what it was offered for.** A forged, spent, expired, dropped or mismatched token, or one
136
+ from another view or an earlier start, does nothing. Under a Jelly host a call that carries a token needs an action
137
+ grant, even for an agent-tier method.
138
+ - **Action tokens and the question association never leave the person's own clients**, and neither is ever logged.
139
+
140
+ ### Stores
141
+
142
+ **Stores only grow** (spec §8.8). Generation stays 2 and `min_reader` stays 1.
143
+
144
+ - **Daemon store, schema 5** (`packages/daemon/store-schemas/5.json`) adds `reach_grants` and `reach_reviews`: the
145
+ reach choices, apart from `grants`. A daemon before schema 5 would delete a `grants` row it cannot parse, so it
146
+ ignores these tables instead.
147
+ - **Relay database, schema 4** (`packages/relay/store-schemas/4.json`) adds `dir_wake_dms` and an index on `exp`. A
148
+ row counts only with its ticket's `exp`, and goes with it.
149
+ - Action tokens and the question association add no table: they are held in memory only.
150
+
151
+ ### Tests
152
+
153
+ - Each new check has a test that goes red when the check is removed: the token bindings, single use, expiry,
154
+ restart, tier and log; the executor never gets `cancel`; Never in existing conversations; the arm's block, mute and
155
+ reach rules; the expiry purge of armed dms; the question on one device holding both sides of a task.
156
+ - The mixed-version e2e with message requests runs in both realms against `@joe-broadhead/messaging@0.7.1` when
157
+ `MESSAGING_E2E_OLD` is set. The gates skip it.
158
+
9
159
  ## [0.8.0] - 2026-11-10
10
160
 
11
161
  **Conversations replace chat and mail.** A dm, a group, a channel and a mail between two agents are now all one
@@ -6746,7 +6746,8 @@ const DOMAIN = {
6746
6746
  const URI = {
6747
6747
  binding: "urn:messaging:binding:relay:v1",
6748
6748
  provenance: "urn:messaging:ext:provenance:v1",
6749
- presence: "urn:messaging:ext:presence:v1"
6749
+ presence: "urn:messaging:ext:presence:v1",
6750
+ signIn: "urn:messaging:ext:sign-in:v1"
6750
6751
  };
6751
6752
  const Base64Url = string().regex(/^[A-Za-z0-9_-]*$/, "base64url without padding");
6752
6753
  const Key32 = string().regex(/^[A-Za-z0-9_-]{43}$/, "32 bytes, base64url without padding");
@@ -6917,6 +6918,7 @@ const ErrorCode = _enum([
6917
6918
  "jelly_unavailable",
6918
6919
  "log_conflict",
6919
6920
  "conv_conflict",
6921
+ "conflict",
6920
6922
  "stale_epoch",
6921
6923
  "device_limit",
6922
6924
  "admission_required",
@@ -7866,7 +7868,12 @@ const AskLevel = _enum([
7866
7868
  "request",
7867
7869
  "no"
7868
7870
  ]);
7869
- const GrantSubject = union$1([strictObject({ person: PersonId }), strictObject({ conv: ConvId })]);
7871
+ const GrantRelation = _enum(["outside", "org"]);
7872
+ const GrantSubject = union$1([
7873
+ strictObject({ person: PersonId }),
7874
+ strictObject({ conv: ConvId }),
7875
+ strictObject({ relation: GrantRelation })
7876
+ ]);
7870
7877
  object$1({
7871
7878
  v: literal(PROTOCOL),
7872
7879
  realm: RealmId,
@@ -7893,6 +7900,12 @@ const GrantSync = strictObject({ grants: array(record(string(), unknown())).min(
7893
7900
  function grantProblem(grant) {
7894
7901
  const own = "person" in grant.subject && grant.subject.person === grant.person;
7895
7902
  if ("conv" in grant.subject && (grant.chat !== void 0 || grant.trustedUntil !== void 0 || grant.discoverable !== void 0)) return "a conversation grant sets only ask and agentMail";
7903
+ if ("relation" in grant.subject) {
7904
+ if (grant.agentMail !== void 0 || grant.trustedUntil !== void 0 || grant.discoverable !== void 0) return "a relation grant sets only chat or ask";
7905
+ if (grant.subject.relation === "outside" && grant.ask !== void 0) return "the outside relation grant sets only chat";
7906
+ if (grant.subject.relation === "outside" && grant.chat === "muted") return "the outside relation grant sets chat open or blocked";
7907
+ if (grant.subject.relation === "org" && grant.chat !== void 0) return "the org relation grant sets only ask";
7908
+ }
7896
7909
  if (own && (grant.chat !== void 0 || grant.agentMail !== void 0 || grant.ask !== void 0 || grant.trustedUntil !== void 0)) return "a grant about its own person sets only discoverable";
7897
7910
  if (!own && grant.discoverable !== void 0) return "discoverable is set only on a grant about its own person";
7898
7911
  if (grant.trustedUntil !== void 0) {
@@ -8356,7 +8369,8 @@ discriminatedUnion("t", [
8356
8369
  url: WakeUrl,
8357
8370
  ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
8358
8371
  agents: array(SessionName).max(64),
8359
- awaiting: array(Ulid).max(500)
8372
+ awaiting: array(Ulid).max(500),
8373
+ dms: array(ConvId).max(256).optional()
8360
8374
  }),
8361
8375
  object$1({
8362
8376
  t: literal("wake.disarm"),
@@ -8471,6 +8485,7 @@ const DeviceChangeView = object$1({
8471
8485
  approver: MachineLabel.optional(),
8472
8486
  at: Millis
8473
8487
  });
8488
+ const PresenceFeature = string().regex(/^[a-z][a-z0-9-]{0,31}$/, "a feature name");
8474
8489
  const DeviceListEntry = object$1({
8475
8490
  device: MachineId,
8476
8491
  label: MachineLabel,
@@ -8489,7 +8504,8 @@ const DeviceListEntry = object$1({
8489
8504
  at: Millis,
8490
8505
  reason: UnbindReason,
8491
8506
  by: MachineId
8492
- }).optional()
8507
+ }).optional(),
8508
+ features: array(PresenceFeature).max(32).optional()
8493
8509
  });
8494
8510
  const LinkText = string().min(1).max(2048);
8495
8511
  const Sas = string().regex(/^[0-9]{6}$/, "six digits");
@@ -8696,6 +8712,7 @@ const DEVICE_METHODS = {
8696
8712
  };
8697
8713
  // packages/core/dist/schema/ipc/shapes.js
8698
8714
  const SessionKey = string().min(1).max(200);
8715
+ const ActionToken = string().regex(/^[A-Za-z0-9_-]{43}$/, "action token");
8699
8716
  const SendExpect = _enum([
8700
8717
  "next_turn",
8701
8718
  "wake",
@@ -8766,7 +8783,14 @@ const TaskFollowUp = object$1({
8766
8783
  stage: DeliveryStage,
8767
8784
  at: Millis
8768
8785
  });
8769
- const TaskRecord = object$1({
8786
+ const TASK_SIGN_IN_TOOL_CHARS = LIMITS.previewChars;
8787
+ const TASK_REFUSAL_CHARS = LIMITS.requestChars;
8788
+ const HostLocalId = string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/, "a plain identifier of at most 200 characters");
8789
+ const TaskQuestion = strictObject({
8790
+ sessionId: HostLocalId,
8791
+ approvalId: HostLocalId
8792
+ });
8793
+ const TaskRecordFields = object$1({
8770
8794
  task: Task$1.extend({ id: Ulid }),
8771
8795
  role: _enum(["requester", "executor"]),
8772
8796
  key: SessionKey.describe("local session that owns this side of the task"),
@@ -8774,8 +8798,36 @@ const TaskRecord = object$1({
8774
8798
  createdAt: Millis,
8775
8799
  updatedAt: Millis,
8776
8800
  chain: DelegationSummary.optional(),
8777
- followUps: array(TaskFollowUp).min(1).max(LIMITS.taskFollowUps).optional()
8778
- });
8801
+ followUps: array(TaskFollowUp).min(1).max(LIMITS.taskFollowUps).optional(),
8802
+ agent: SessionName.optional(),
8803
+ person: object$1({
8804
+ id: PersonId,
8805
+ name: string().max(200)
8806
+ }).optional(),
8807
+ org: _enum([
8808
+ "same",
8809
+ "other",
8810
+ "none"
8811
+ ]).optional(),
8812
+ signIn: object$1({
8813
+ tool: string().min(1).max(TASK_SIGN_IN_TOOL_CHARS).optional(),
8814
+ by: _enum(["you", "them"])
8815
+ }).optional(),
8816
+ refusal: object$1({ reason: string().min(1).max(TASK_REFUSAL_CHARS) }).optional(),
8817
+ question: TaskQuestion.optional()
8818
+ });
8819
+ function taskViewRules(schema) {
8820
+ return schema.refine((r) => r.signIn === void 0 || r.task.status.state === "TASK_STATE_AUTH_REQUIRED", "signIn is for an AUTH_REQUIRED task").refine((r) => r.refusal === void 0 || r.task.status.state === "TASK_STATE_REJECTED", "refusal is for a REJECTED task").refine((r) => r.question === void 0 || 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");
8821
+ }
8822
+ const TaskRecord = taskViewRules(TaskRecordFields);
8823
+ const PersonTask = taskViewRules(TaskRecordFields.omit({ key: true }).extend({
8824
+ key: never().optional(),
8825
+ session: SessionName.describe("this person's agent on this device that holds this side of the task"),
8826
+ cancel: strictObject({
8827
+ name: literal("cancel"),
8828
+ token: ActionToken
8829
+ }).optional()
8830
+ })).refine((r) => r.cancel === void 0 || r.role === "requester", "cancel is for the requester side");
8779
8831
  const SendTarget = SessionAddress;
8780
8832
  const SendResult = object$1({
8781
8833
  to: SendTarget,
@@ -8818,6 +8870,17 @@ const GrantSetParams = strictObject({
8818
8870
  ...Levels,
8819
8871
  trust: literal(true).optional()
8820
8872
  });
8873
+ const RequestAnswer = _enum([
8874
+ "once",
8875
+ "always",
8876
+ "accept",
8877
+ "decline",
8878
+ "block"
8879
+ ]);
8880
+ const Actions = { actions: array(strictObject({
8881
+ name: RequestAnswer,
8882
+ token: ActionToken
8883
+ })).max(4).optional() };
8821
8884
  const AskRequest = object$1({
8822
8885
  id: Ulid,
8823
8886
  kind: _enum(["message", "task"]),
@@ -8831,7 +8894,8 @@ const AskRequest = object$1({
8831
8894
  to: SessionName,
8832
8895
  preview: string().max(LIMITS.previewChars),
8833
8896
  at: Millis,
8834
- exp: Millis
8897
+ exp: Millis,
8898
+ ...Actions
8835
8899
  });
8836
8900
  const ContactRequestView = object$1({
8837
8901
  id: Ulid,
@@ -8857,7 +8921,8 @@ const ContactRequestView = object$1({
8857
8921
  }),
8858
8922
  text: string().max(LIMITS.requestChars),
8859
8923
  at: Millis,
8860
- exp: Millis
8924
+ exp: Millis,
8925
+ ...Actions
8861
8926
  });
8862
8927
  const AgentInvitationRequest = strictObject({
8863
8928
  kind: literal("agent-invitation"),
@@ -8865,7 +8930,8 @@ const AgentInvitationRequest = strictObject({
8865
8930
  cid: ConvId,
8866
8931
  from: ParticipantId,
8867
8932
  to: ParticipantId,
8868
- at: Millis
8933
+ at: Millis,
8934
+ ...Actions
8869
8935
  });
8870
8936
  const HeldRequest = union$1([
8871
8937
  AskRequest,
@@ -8875,17 +8941,11 @@ const HeldRequest = union$1([
8875
8941
  kind: literal("suggestion"),
8876
8942
  agent: SessionName,
8877
8943
  email: Email,
8878
- at: Millis
8944
+ at: Millis,
8945
+ ...Actions
8879
8946
  }),
8880
8947
  AgentInvitationRequest
8881
8948
  ]);
8882
- const RequestAnswer = _enum([
8883
- "once",
8884
- "always",
8885
- "accept",
8886
- "decline",
8887
- "block"
8888
- ]);
8889
8949
  const RequestText = string().min(1).refine((text) => text.length <= LIMITS.requestChars, { message: `at most ${LIMITS.requestChars} UTF-16 code units` });
8890
8950
  const PersonReach = object$1({
8891
8951
  person: PersonId,
@@ -8897,14 +8957,57 @@ const PersonReach = object$1({
8897
8957
  const RequestAnswerParams = strictObject({
8898
8958
  id: Ulid,
8899
8959
  answer: RequestAnswer,
8900
- all: boolean().optional()
8901
- });
8960
+ all: boolean().optional(),
8961
+ token: ActionToken.optional()
8962
+ }).refine((p) => p.token === void 0 || p.all !== true, "an action token answers one request: drop all");
8902
8963
  const ContactRequestParams = strictObject({
8903
8964
  email: Email,
8904
8965
  text: RequestText,
8905
8966
  suggestion: Ulid.optional()
8906
8967
  });
8968
+ const ReachGroupId = _enum(["message", "ask"]);
8969
+ const ReachValue = _enum([
8970
+ "open",
8971
+ "ask",
8972
+ "never"
8973
+ ]);
8974
+ const ReachOption = object$1({
8975
+ value: ReachValue,
8976
+ label: string().max(40),
8977
+ description: string().max(300)
8978
+ });
8979
+ const ReachGroup = object$1({
8980
+ id: ReachGroupId,
8981
+ label: string().max(80),
8982
+ options: array(ReachOption).length(3),
8983
+ value: ReachValue.nullable(),
8984
+ lapsed: ReachValue.optional()
8985
+ });
8986
+ const ReachSnapshot = object$1({
8987
+ revision: string().min(1).max(64),
8988
+ available: boolean(),
8989
+ groups: array(ReachGroup).length(2)
8990
+ });
8991
+ const ReachSetParams = strictObject({
8992
+ revision: string().min(1).max(64),
8993
+ changes: strictObject({
8994
+ message: ReachValue.optional(),
8995
+ ask: ReachValue.optional()
8996
+ })
8997
+ });
8907
8998
  const CONSENT_METHODS = {
8999
+ "reach.get": {
9000
+ params: strictObject({}),
9001
+ result: ReachSnapshot
9002
+ },
9003
+ "reach.set": {
9004
+ params: ReachSetParams,
9005
+ result: ReachSnapshot
9006
+ },
9007
+ "reach.set.preview": {
9008
+ params: ReachSetParams,
9009
+ result: object$1({ sentence: string().max(500) })
9010
+ },
8908
9011
  "grant.list": {
8909
9012
  params: strictObject({ subject: GrantTarget.optional() }),
8910
9013
  result: object$1({
@@ -9068,6 +9171,7 @@ const Notice = object$1({
9068
9171
  }).optional(),
9069
9172
  line: string().max(120).optional(),
9070
9173
  device: NoticeDevice.optional(),
9174
+ key: string().regex(/^task:[0-9A-HJKMNP-TV-Z]{26}$/, "task:<task id>").optional(),
9071
9175
  at: Millis
9072
9176
  });
9073
9177
  const NoticeCounts = object$1({ requests: number().int().nonnegative() });
@@ -9138,7 +9242,8 @@ const JELLY_METHODS = {
9138
9242
  }),
9139
9243
  result: object$1({
9140
9244
  relay: _enum(["armed", "pending"]),
9141
- awaiting: number().int().nonnegative()
9245
+ awaiting: number().int().nonnegative(),
9246
+ dms: number().int().nonnegative()
9142
9247
  })
9143
9248
  },
9144
9249
  "wake.disarm": {
@@ -9251,7 +9356,11 @@ const FileView = object$1({
9251
9356
  });
9252
9357
  const TaskView = object$1({
9253
9358
  id: Ulid,
9254
- state: TaskState
9359
+ state: TaskState,
9360
+ cancel: strictObject({
9361
+ name: literal("cancel"),
9362
+ token: ActionToken
9363
+ }).optional()
9255
9364
  });
9256
9365
  const EventView = object$1({
9257
9366
  cid: ConvId,
@@ -9410,6 +9519,11 @@ const ConvStreamEvent = discriminatedUnion("type", [
9410
9519
  object$1({
9411
9520
  type: literal("gap"),
9412
9521
  pos: Seq$1
9522
+ }),
9523
+ object$1({
9524
+ type: literal("task"),
9525
+ pos: Seq$1,
9526
+ task: PersonTask
9413
9527
  })
9414
9528
  ]).refine((entry) => entry.type !== "event.update" || entry.cid === entry.event.cid && entry.id === entry.event.id, { message: "event.update names the same conversation and event as its projection" });
9415
9529
  // packages/core/dist/schema/ipc/conversations.js
@@ -9516,7 +9630,12 @@ const WaitingOnThemView = object$1({
9516
9630
  at: Millis,
9517
9631
  cid: ConvId.optional(),
9518
9632
  agent: SessionName.optional(),
9519
- state: _enum(["held", "pending-delivery"])
9633
+ state: _enum([
9634
+ "held",
9635
+ "pending-delivery",
9636
+ "resend",
9637
+ "outdated"
9638
+ ])
9520
9639
  });
9521
9640
  const ConvListParams = strictObject({
9522
9641
  key: Key,
@@ -10131,16 +10250,47 @@ const IPC_METHODS = {
10131
10250
  message: strictObject({ parts: array(Part$1).min(1).max(LIMITS.partsPerMessage) }).optional(),
10132
10251
  artifact: Artifact.optional(),
10133
10252
  append: boolean().optional(),
10134
- lastChunk: boolean().optional()
10135
- }),
10253
+ lastChunk: boolean().optional(),
10254
+ signIn: strictObject({ tool: string().min(1).max(TASK_SIGN_IN_TOOL_CHARS) }).optional()
10255
+ }).refine((p) => p.signIn === void 0 || p.state === "TASK_STATE_AUTH_REQUIRED", "signIn goes with TASK_STATE_AUTH_REQUIRED"),
10136
10256
  result: TaskRecord
10137
10257
  },
10138
- "task.cancel": {
10258
+ "task.question": {
10139
10259
  params: strictObject({
10140
10260
  key: SessionKey,
10141
10261
  taskId: Ulid,
10142
- reason: string().max(1e3).optional()
10262
+ question: TaskQuestion.nullable()
10143
10263
  }),
10264
+ result: object$1({ ok: literal(true) })
10265
+ },
10266
+ "tasks.list": {
10267
+ params: strictObject({
10268
+ role: _enum(["requester", "executor"]).optional(),
10269
+ state: TaskState.optional(),
10270
+ session: SessionName.optional().describe("only this agent's tasks"),
10271
+ cursor: string().min(1).max(200).optional().describe("opaque; from a previous nextCursor"),
10272
+ limit: number().int().min(1).max(100).optional()
10273
+ }),
10274
+ result: object$1({
10275
+ tasks: array(PersonTask),
10276
+ nextCursor: string().max(200).optional().describe("absent when there are no more pages"),
10277
+ total: number().int().nonnegative()
10278
+ })
10279
+ },
10280
+ "tasks.get": {
10281
+ params: strictObject({
10282
+ taskId: Ulid,
10283
+ role: _enum(["requester", "executor"]).optional()
10284
+ }),
10285
+ result: PersonTask
10286
+ },
10287
+ "task.cancel": {
10288
+ params: strictObject({
10289
+ key: SessionKey.optional(),
10290
+ taskId: Ulid,
10291
+ reason: string().max(1e3).optional(),
10292
+ token: ActionToken.optional()
10293
+ }).refine((p) => p.key === void 0 !== (p.token === void 0), "give exactly one of key or token"),
10144
10294
  result: TaskRecord
10145
10295
  },
10146
10296
  "events.subscribe": {
@@ -10293,7 +10443,8 @@ const AGENT_METHODS = [
10293
10443
  "task.list",
10294
10444
  "task.get",
10295
10445
  "task.update",
10296
- "task.cancel"
10446
+ "task.cancel",
10447
+ "task.question"
10297
10448
  ];
10298
10449
  const HUMAN_METHODS = [
10299
10450
  ...conversationTier("human"),
@@ -10302,6 +10453,9 @@ const HUMAN_METHODS = [
10302
10453
  "grant.list",
10303
10454
  "grant.set",
10304
10455
  "grant.set.preview",
10456
+ "reach.get",
10457
+ "reach.set",
10458
+ "reach.set.preview",
10305
10459
  "request.list",
10306
10460
  "request.answer",
10307
10461
  "request.answer.preview",
@@ -10311,6 +10465,8 @@ const HUMAN_METHODS = [
10311
10465
  "notice.subscribe",
10312
10466
  "notice.list",
10313
10467
  "notice.ack",
10468
+ "tasks.list",
10469
+ "tasks.get",
10314
10470
  "device.approve.preview",
10315
10471
  "device.remove.preview",
10316
10472
  "identity.replace.preview",
@@ -10461,7 +10617,8 @@ strictObject({
10461
10617
  strictObject({
10462
10618
  t: literal("conv.capabilities"),
10463
10619
  req: Req,
10464
- outdated: array(PersonId).max(LIMITS.convOpParticipants)
10620
+ outdated: array(PersonId).max(LIMITS.convOpParticipants),
10621
+ unknown: array(PersonId).min(1).max(LIMITS.convOpParticipants).optional()
10465
10622
  });
10466
10623
  union$1([strictObject({
10467
10624
  t: literal("conv.push"),
@@ -10575,6 +10732,7 @@ const TOOL_REMEDIES = {
10575
10732
  login_denied: "tell your person: the sign-in at Jelly ID was denied.",
10576
10733
  log_conflict: "try again: their devices changed at the same time.",
10577
10734
  conv_conflict: "try again: the conversation changed at the same time.",
10735
+ conflict: "tell your person: their settings changed at the same time; they choose again.",
10578
10736
  stale_epoch: "try again: the conversation's keys changed at the same time.",
10579
10737
  device_limit: "ask your person to remove one of their devices first.",
10580
10738
  admission_required: "ask your person to open Messaging in Jelly once: this device is not bound to their Jelly account yet.",
@@ -10752,6 +10910,11 @@ const Task = object$1({
10752
10910
  titleAuthor: Author$1.optional().describe("With title: who wrote the request; ours when you asked (mine)"),
10753
10911
  status: authored("The latest status message").optional(),
10754
10912
  statusAuthor: Author$1.optional().describe("With status: who wrote it; ours when you run the task (asked)"),
10913
+ signIn: object$1({
10914
+ tool: authored("The tool the executor named").optional(),
10915
+ by: _enum(["you", "them"]).describe("Whose sign-in it is: you (your person, for an agent of theirs) or them (the other person); your Messaging daemon decides, never the peer")
10916
+ }).optional().describe("auth_required only: the task waits on a sign-in; the tool was written by the executor, as statusAuthor says"),
10917
+ refusal: object$1({ reason: authored("The executor's reason") }).optional().describe("rejected only: why the executor declined, in its words, as statusAuthor says"),
10755
10918
  createdAt: iso,
10756
10919
  updatedAt: iso,
10757
10920
  consent: ToolConsentSchema.optional(),
@@ -11312,8 +11475,11 @@ const VIEW_METHODS = [
11312
11475
  "people.search",
11313
11476
  "people.get",
11314
11477
  "grant.list",
11478
+ "reach.get",
11315
11479
  "request.list",
11316
11480
  "sessions.list",
11481
+ "tasks.list",
11482
+ "tasks.get",
11317
11483
  "notice.subscribe",
11318
11484
  "notice.list",
11319
11485
  "device.list"
@@ -12623,6 +12789,11 @@ function presentTask(framer, record, consent) {
12623
12789
  status: shown$1(framer, statusAuthor, peerAttributes(record))(status, PART_CHARS),
12624
12790
  statusAuthor
12625
12791
  } : {},
12792
+ ...record.signIn !== void 0 ? { signIn: {
12793
+ ...record.signIn.tool !== void 0 ? { tool: shown$1(framer, statusAuthor, peerAttributes(record))(record.signIn.tool, TASK_SIGN_IN_TOOL_CHARS) } : {},
12794
+ by: record.signIn.by
12795
+ } } : {},
12796
+ ...record.refusal !== void 0 ? { refusal: { reason: shown$1(framer, statusAuthor, peerAttributes(record))(record.refusal.reason, TASK_REFUSAL_CHARS) } } : {},
12626
12797
  updatedAt: isoOf(record.updatedAt),
12627
12798
  ...consent !== void 0 && record.role === "executor" ? { consent } : {},
12628
12799
  ...record.chain !== void 0 ? { chain: presentChain(framer, record.chain) } : {}
@@ -13445,7 +13616,7 @@ var MessagingClient = class MessagingClient {
13445
13616
  timer = setTimeout(() => reject(new DaemonMismatchError(`the process on the Messaging daemon socket did not say which version it is within ${wait} ms: it is a messagingd from before this version, or it is stuck. ${REMEDY}`)), wait);
13446
13617
  })]);
13447
13618
  if (pointer !== void 0 && hello.pid !== pointer.pid) throw new MessagingError("forbidden", "the process on the Messaging daemon's pipe is not the daemon that published it");
13448
- const expected = options.version ?? "0.8.0";
13619
+ const expected = options.version ?? "0.9.0";
13449
13620
  if (hello.protocol !== 1 || hello.version !== expected) throw new DaemonMismatchError(`the Messaging daemon for this home is version ${shown(hello.version)} (IPC protocol ${hello.protocol}), and this client is version ${shown(expected)} (IPC protocol 1). ${REMEDY}`, hello);
13450
13621
  client.daemonHello = hello;
13451
13622
  return client;
@@ -27335,7 +27506,7 @@ function registerTools(server, deps) {
27335
27506
  }
27336
27507
  }
27337
27508
  // packages/mcp/dist/version.js
27338
- const VERSION = "0.8.0";
27509
+ const VERSION = "0.9.0";
27339
27510
  // packages/mcp/dist/server.js
27340
27511
  function createMessagingMcpServer(options) {
27341
27512
  if (!options.client && !options.connect) throw new TypeError("createMessagingMcpServer needs `client` or `connect`");