@joe-broadhead/messaging-bin-win32-x64 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.
@@ -6376,7 +6376,8 @@ var init_common = __esm(() => {
6376
6376
  URI = {
6377
6377
  binding: "urn:messaging:binding:relay:v1",
6378
6378
  provenance: "urn:messaging:ext:provenance:v1",
6379
- presence: "urn:messaging:ext:presence:v1"
6379
+ presence: "urn:messaging:ext:presence:v1",
6380
+ signIn: "urn:messaging:ext:sign-in:v1"
6380
6381
  };
6381
6382
  Base64Url = string2().regex(/^[A-Za-z0-9_-]*$/, "base64url without padding");
6382
6383
  Key32 = string2().regex(/^[A-Za-z0-9_-]{43}$/, "32 bytes, base64url without padding");
@@ -6550,6 +6551,7 @@ var init_errors3 = __esm(() => {
6550
6551
  "jelly_unavailable",
6551
6552
  "log_conflict",
6552
6553
  "conv_conflict",
6554
+ "conflict",
6553
6555
  "stale_epoch",
6554
6556
  "device_limit",
6555
6557
  "admission_required",
@@ -6803,7 +6805,7 @@ var init_directory = __esm(() => {
6803
6805
  });
6804
6806
 
6805
6807
  // packages/core/dist/schema/a2a.js
6806
- var Struct, TaskState, TERMINAL_STATES, Role, Part, Message, TaskStatus, Artifact, Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent;
6808
+ var Struct, TaskState, TERMINAL_STATES, INTERRUPTED_STATES, Role, Part, Message, TaskStatus, Artifact, Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent;
6807
6809
  var init_a2a = __esm(() => {
6808
6810
  init_zod();
6809
6811
  Struct = record(string2(), unknown());
@@ -6823,6 +6825,7 @@ var init_a2a = __esm(() => {
6823
6825
  "TASK_STATE_CANCELED",
6824
6826
  "TASK_STATE_REJECTED"
6825
6827
  ];
6828
+ INTERRUPTED_STATES = ["TASK_STATE_INPUT_REQUIRED", "TASK_STATE_AUTH_REQUIRED"];
6826
6829
  Role = _enum(["ROLE_USER", "ROLE_AGENT"]);
6827
6830
  Part = object({
6828
6831
  text: string2().optional(),
@@ -7173,6 +7176,16 @@ function grantProblem(grant) {
7173
7176
  if ("conv" in grant.subject && (grant.chat !== undefined || grant.trustedUntil !== undefined || grant.discoverable !== undefined)) {
7174
7177
  return "a conversation grant sets only ask and agentMail";
7175
7178
  }
7179
+ if ("relation" in grant.subject) {
7180
+ if (grant.agentMail !== undefined || grant.trustedUntil !== undefined || grant.discoverable !== undefined)
7181
+ return "a relation grant sets only chat or ask";
7182
+ if (grant.subject.relation === "outside" && grant.ask !== undefined)
7183
+ return "the outside relation grant sets only chat";
7184
+ if (grant.subject.relation === "outside" && grant.chat === "muted")
7185
+ return "the outside relation grant sets chat open or blocked";
7186
+ if (grant.subject.relation === "org" && grant.chat !== undefined)
7187
+ return "the org relation grant sets only ask";
7188
+ }
7176
7189
  if (own && (grant.chat !== undefined || grant.agentMail !== undefined || grant.ask !== undefined || grant.trustedUntil !== undefined)) {
7177
7190
  return "a grant about its own person sets only discoverable";
7178
7191
  }
@@ -7195,7 +7208,7 @@ function cardProblem(card) {
7195
7208
  return "trustedUntil needs ask open";
7196
7209
  return;
7197
7210
  }
7198
- var ChatLevel, AgentMailLevel, AskLevel, GrantSubject, ConsentGrant, GRANTS_PER_SYNC = 100, GrantSync, CardCan, ContactCard, CardData, CardGetData;
7211
+ var ChatLevel, AgentMailLevel, AskLevel, GrantRelation, GrantSubject, ConsentGrant, GRANTS_PER_SYNC = 100, GrantSync, CardCan, ContactCard, CardData, CardGetData;
7199
7212
  var init_consent = __esm(() => {
7200
7213
  init_zod();
7201
7214
  init_common();
@@ -7204,7 +7217,8 @@ var init_consent = __esm(() => {
7204
7217
  ChatLevel = _enum(["open", "muted", "blocked"]);
7205
7218
  AgentMailLevel = _enum(["open", "blocked"]);
7206
7219
  AskLevel = _enum(["open", "request", "no"]);
7207
- GrantSubject = union([strictObject({ person: PersonId }), strictObject({ conv: ConvId })]);
7220
+ GrantRelation = _enum(["outside", "org"]);
7221
+ GrantSubject = union([strictObject({ person: PersonId }), strictObject({ conv: ConvId }), strictObject({ relation: GrantRelation })]);
7208
7222
  ConsentGrant = object({
7209
7223
  v: literal(PROTOCOL),
7210
7224
  realm: RealmId,
@@ -7554,13 +7568,14 @@ function hostedRelayAuthPayload(args) {
7554
7568
  function dirReadPayload(args) {
7555
7569
  return { proto: RELAY_PROTOCOL, relay: args.relay, realm: args.realm, device: args.device, nonce: args.nonce };
7556
7570
  }
7557
- var Req3, Size, Handle, Email, OrgId, DirReadProof, DirBindFrame, HostedHelloFrame, DirAppendFrame, DirKeysFrame, DirSthFrame, DirLookupFrame, DirHeadsFrame, DirDeviceFrame, DirResolveFrame, DirSearchFrame, DirWatchFrame, PresenceWatchFrame, DirEraseFrame, WAKE_AGENTS = 64, WAKE_AWAITING = 500, WAKE_TICKET_BYTES = 4096, WAKE_TICKET_MAX_S, WakeUrl, WakeArmFrame, WakeDisarmFrame, DirectoryClientFrame, HostedWelcomeFrame, DirOkFrame, DirSthIsFrame, DirPersonsFrame, DirHeadsAreFrame, ResolvedPerson, DirResolvedFrame, DirChangedFrame, DirUnboundFrame, DirectoryRelayFrame;
7571
+ var Req3, Size, Handle, Email, OrgId, DirReadProof, DirBindFrame, HostedHelloFrame, DirAppendFrame, DirKeysFrame, DirSthFrame, DirLookupFrame, DirHeadsFrame, DirDeviceFrame, DirResolveFrame, DirSearchFrame, DirWatchFrame, PresenceWatchFrame, DirEraseFrame, WAKE_AGENTS = 64, WAKE_AWAITING = 500, WAKE_DMS = 256, WAKE_TICKET_BYTES = 4096, WAKE_TICKET_MAX_S, WakeUrl, WakeArmFrame, WakeDisarmFrame, DirectoryClientFrame, HostedWelcomeFrame, DirOkFrame, DirSthIsFrame, DirPersonsFrame, DirHeadsAreFrame, ResolvedPerson, DirResolvedFrame, DirChangedFrame, DirUnboundFrame, DirectoryRelayFrame;
7558
7572
  var init_directory_frames = __esm(() => {
7559
7573
  init_zod();
7560
7574
  init_common();
7561
7575
  init_directory();
7562
7576
  init_realm();
7563
7577
  init_directory();
7578
+ init_conversation();
7564
7579
  init_jelly();
7565
7580
  Req3 = string2().min(1).max(64);
7566
7581
  Size = number2().int().positive();
@@ -7635,7 +7650,8 @@ var init_directory_frames = __esm(() => {
7635
7650
  url: WakeUrl,
7636
7651
  ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
7637
7652
  agents: array(SessionName).max(WAKE_AGENTS),
7638
- awaiting: array(Ulid).max(WAKE_AWAITING)
7653
+ awaiting: array(Ulid).max(WAKE_AWAITING),
7654
+ dms: array(ConvId).max(WAKE_DMS).optional()
7639
7655
  });
7640
7656
  WakeDisarmFrame = object({ t: literal("wake.disarm"), req: Req3 });
7641
7657
  DirectoryClientFrame = discriminatedUnion("t", [
@@ -7717,7 +7733,7 @@ var init_directory_frames = __esm(() => {
7717
7733
  });
7718
7734
 
7719
7735
  // packages/core/dist/schema/ipc/device.js
7720
- var BindingState, RealmAlarmSummary, DeviceStatus, DeviceChangeView, DeviceListEntry, LinkText, Sas, LinkPreview, KeyFingerprint, LoginUserCode, LoginPurpose, LoginKind, LoginShown, LoginState, DeviceRemoveParams, DeviceApproveParams, IdentityReplaceParams, SignInStep, DirectoryEraseParams, DEVICE_METHODS;
7736
+ var BindingState, RealmAlarmSummary, DeviceStatus, DeviceChangeView, PresenceFeature, DeviceListEntry, LinkText, Sas, LinkPreview, KeyFingerprint, LoginUserCode, LoginPurpose, LoginKind, LoginShown, LoginState, DeviceRemoveParams, DeviceApproveParams, IdentityReplaceParams, SignInStep, DirectoryEraseParams, DEVICE_METHODS;
7721
7737
  var init_device = __esm(() => {
7722
7738
  init_zod();
7723
7739
  init_common();
@@ -7749,6 +7765,7 @@ var init_device = __esm(() => {
7749
7765
  approver: MachineLabel.optional(),
7750
7766
  at: Millis
7751
7767
  });
7768
+ PresenceFeature = string2().regex(/^[a-z][a-z0-9-]{0,31}$/, "a feature name");
7752
7769
  DeviceListEntry = object({
7753
7770
  device: MachineId,
7754
7771
  label: MachineLabel,
@@ -7759,7 +7776,8 @@ var init_device = __esm(() => {
7759
7776
  self: boolean2(),
7760
7777
  away: literal(true).optional(),
7761
7778
  since: Millis.optional(),
7762
- unbound: object({ at: Millis, reason: UnbindReason, by: MachineId }).optional()
7779
+ unbound: object({ at: Millis, reason: UnbindReason, by: MachineId }).optional(),
7780
+ features: array(PresenceFeature).max(32).optional()
7763
7781
  });
7764
7782
  LinkText = string2().min(1).max(2048);
7765
7783
  Sas = string2().regex(/^[0-9]{6}$/, "six digits");
@@ -7866,7 +7884,10 @@ var init_device = __esm(() => {
7866
7884
  });
7867
7885
 
7868
7886
  // packages/core/dist/schema/ipc/shapes.js
7869
- var SessionKey, SendExpect, DirectoryEntry, PeerRef, InboxItem, TaskFollowUp, TaskRecord, SendTarget, SendResult;
7887
+ function taskViewRules(schema) {
7888
+ return schema.refine((r) => r.signIn === undefined || r.task.status.state === "TASK_STATE_AUTH_REQUIRED", "signIn is for an AUTH_REQUIRED task").refine((r) => r.refusal === undefined || r.task.status.state === "TASK_STATE_REJECTED", "refusal is for a REJECTED task").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");
7889
+ }
7890
+ var SessionKey, ActionToken, SendExpect, DirectoryEntry, PeerRef, InboxItem, TaskFollowUp, TASK_SIGN_IN_TOOL_CHARS, TASK_REFUSAL_CHARS, HostLocalId, TaskQuestion, TaskRecordFields, TaskRecord, PersonTask, SendTarget, SendResult;
7870
7891
  var init_shapes = __esm(() => {
7871
7892
  init_zod();
7872
7893
  init_a2a();
@@ -7875,6 +7896,7 @@ var init_shapes = __esm(() => {
7875
7896
  init_delegation();
7876
7897
  init_errors3();
7877
7898
  SessionKey = string2().min(1).max(200);
7899
+ ActionToken = string2().regex(/^[A-Za-z0-9_-]{43}$/, "action token");
7878
7900
  SendExpect = _enum(["next_turn", "wake", "next_prompt", "on_read", "kept", "offline"]);
7879
7901
  DirectoryEntry = object({
7880
7902
  address: SessionAddress,
@@ -7926,7 +7948,11 @@ var init_shapes = __esm(() => {
7926
7948
  stage: DeliveryStage,
7927
7949
  at: Millis
7928
7950
  });
7929
- TaskRecord = object({
7951
+ TASK_SIGN_IN_TOOL_CHARS = LIMITS.previewChars;
7952
+ TASK_REFUSAL_CHARS = LIMITS.requestChars;
7953
+ HostLocalId = string2().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/, "a plain identifier of at most 200 characters");
7954
+ TaskQuestion = strictObject({ sessionId: HostLocalId, approvalId: HostLocalId });
7955
+ TaskRecordFields = object({
7930
7956
  task: Task.extend({ id: Ulid }),
7931
7957
  role: _enum(["requester", "executor"]),
7932
7958
  key: SessionKey.describe("local session that owns this side of the task"),
@@ -7934,8 +7960,20 @@ var init_shapes = __esm(() => {
7934
7960
  createdAt: Millis,
7935
7961
  updatedAt: Millis,
7936
7962
  chain: DelegationSummary.optional(),
7937
- followUps: array(TaskFollowUp).min(1).max(LIMITS.taskFollowUps).optional()
7938
- });
7963
+ followUps: array(TaskFollowUp).min(1).max(LIMITS.taskFollowUps).optional(),
7964
+ agent: SessionName.optional(),
7965
+ person: object({ id: PersonId, name: string2().max(200) }).optional(),
7966
+ org: _enum(["same", "other", "none"]).optional(),
7967
+ signIn: object({ tool: string2().min(1).max(TASK_SIGN_IN_TOOL_CHARS).optional(), by: _enum(["you", "them"]) }).optional(),
7968
+ refusal: object({ reason: string2().min(1).max(TASK_REFUSAL_CHARS) }).optional(),
7969
+ question: TaskQuestion.optional()
7970
+ });
7971
+ TaskRecord = taskViewRules(TaskRecordFields);
7972
+ PersonTask = taskViewRules(TaskRecordFields.omit({ key: true }).extend({
7973
+ key: never().optional(),
7974
+ session: SessionName.describe("this person's agent on this device that holds this side of the task"),
7975
+ cancel: strictObject({ name: literal("cancel"), token: ActionToken }).optional()
7976
+ })).refine((r) => r.cancel === undefined || r.role === "requester", "cancel is for the requester side");
7939
7977
  SendTarget = SessionAddress;
7940
7978
  SendResult = object({
7941
7979
  to: SendTarget,
@@ -7950,13 +7988,14 @@ var init_shapes = __esm(() => {
7950
7988
  });
7951
7989
 
7952
7990
  // packages/core/dist/schema/ipc/consent.js
7953
- var GrantTarget, Levels, GrantView, GrantReview, GrantSetParams, AskRequest, ContactRequestView, AgentInvitationRequest, ContactSuggestion, HeldRequest, RequestAnswer, RequestText, PersonReach, RequestAnswerParams, ContactRequestParams, CONSENT_METHODS;
7991
+ var GrantTarget, Levels, GrantView, GrantReview, GrantSetParams, RequestAnswer, RequestAction, Actions, AskRequest, ContactRequestView, AgentInvitationRequest, ContactSuggestion, HeldRequest, RequestText, PersonReach, RequestAnswerParams, ContactRequestParams, ReachGroupId, ReachValue, ReachOption, ReachGroup, ReachSnapshot, ReachSetParams, REACH_QUESTIONS, CONSENT_METHODS;
7954
7992
  var init_consent2 = __esm(() => {
7955
7993
  init_zod();
7956
7994
  init_common();
7957
7995
  init_conversation();
7958
7996
  init_consent();
7959
7997
  init_directory_frames();
7998
+ init_shapes();
7960
7999
  GrantTarget = union([strictObject({ person: PersonId }), strictObject({ conv: ConvId })]);
7961
8000
  Levels = {
7962
8001
  chat: ChatLevel.optional(),
@@ -7983,6 +8022,9 @@ var init_consent2 = __esm(() => {
7983
8022
  at: Millis
7984
8023
  });
7985
8024
  GrantSetParams = strictObject({ subject: GrantTarget, ...Levels, trust: literal(true).optional() });
8025
+ RequestAnswer = _enum(["once", "always", "accept", "decline", "block"]);
8026
+ RequestAction = strictObject({ name: RequestAnswer, token: ActionToken });
8027
+ Actions = { actions: array(RequestAction).max(4).optional() };
7986
8028
  AskRequest = object({
7987
8029
  id: Ulid,
7988
8030
  kind: _enum(["message", "task"]),
@@ -7990,7 +8032,8 @@ var init_consent2 = __esm(() => {
7990
8032
  to: SessionName,
7991
8033
  preview: string2().max(LIMITS.previewChars),
7992
8034
  at: Millis,
7993
- exp: Millis
8035
+ exp: Millis,
8036
+ ...Actions
7994
8037
  });
7995
8038
  ContactRequestView = object({
7996
8039
  id: Ulid,
@@ -8007,23 +8050,62 @@ var init_consent2 = __esm(() => {
8007
8050
  }),
8008
8051
  text: string2().max(LIMITS.requestChars),
8009
8052
  at: Millis,
8010
- exp: Millis
8053
+ exp: Millis,
8054
+ ...Actions
8011
8055
  });
8012
- AgentInvitationRequest = strictObject({ kind: literal("agent-invitation"), id: Ulid, cid: ConvId, from: ParticipantId, to: ParticipantId, at: Millis });
8056
+ AgentInvitationRequest = strictObject({ kind: literal("agent-invitation"), id: Ulid, cid: ConvId, from: ParticipantId, to: ParticipantId, at: Millis, ...Actions });
8013
8057
  ContactSuggestion = object({
8014
8058
  id: Ulid,
8015
8059
  kind: literal("suggestion"),
8016
8060
  agent: SessionName,
8017
8061
  email: Email,
8018
- at: Millis
8062
+ at: Millis,
8063
+ ...Actions
8019
8064
  });
8020
8065
  HeldRequest = union([AskRequest, ContactRequestView, ContactSuggestion, AgentInvitationRequest]);
8021
- RequestAnswer = _enum(["once", "always", "accept", "decline", "block"]);
8022
8066
  RequestText = string2().min(1).refine((text) => text.length <= LIMITS.requestChars, { message: `at most ${LIMITS.requestChars} UTF-16 code units` });
8023
8067
  PersonReach = object({ person: PersonId, can: CardCan, source: _enum(["card", "default"]), at: Millis.optional(), devicesChangedAt: Millis.optional() });
8024
- RequestAnswerParams = strictObject({ id: Ulid, answer: RequestAnswer, all: boolean2().optional() });
8068
+ RequestAnswerParams = strictObject({ id: Ulid, answer: RequestAnswer, all: boolean2().optional(), token: ActionToken.optional() }).refine((p) => p.token === undefined || p.all !== true, "an action token answers one request: drop all");
8025
8069
  ContactRequestParams = strictObject({ email: Email, text: RequestText, suggestion: Ulid.optional() });
8070
+ ReachGroupId = _enum(["message", "ask"]);
8071
+ ReachValue = _enum(["open", "ask", "never"]);
8072
+ ReachOption = object({ value: ReachValue, label: string2().max(40), description: string2().max(300) });
8073
+ ReachGroup = object({
8074
+ id: ReachGroupId,
8075
+ label: string2().max(80),
8076
+ options: array(ReachOption).length(3),
8077
+ value: ReachValue.nullable(),
8078
+ lapsed: ReachValue.optional()
8079
+ });
8080
+ ReachSnapshot = object({ revision: string2().min(1).max(64), available: boolean2(), groups: array(ReachGroup).length(2) });
8081
+ ReachSetParams = strictObject({
8082
+ revision: string2().min(1).max(64),
8083
+ changes: strictObject({ message: ReachValue.optional(), ask: ReachValue.optional() })
8084
+ });
8085
+ REACH_QUESTIONS = [
8086
+ {
8087
+ id: "message",
8088
+ label: "Who can message you",
8089
+ options: [
8090
+ { value: "open", label: "Open", description: "Anyone outside your org may message you." },
8091
+ { value: "ask", label: "Ask me first", description: "People outside your org send a message request first, and you accept or decline it. People you have accepted may message you." },
8092
+ { value: "never", label: "Never", description: "No one outside your org may message you or send you a request. Your org, and people you have let message you, still can." }
8093
+ ]
8094
+ },
8095
+ {
8096
+ id: "ask",
8097
+ label: "How agents may ask yours",
8098
+ options: [
8099
+ { value: "open", label: "Open", description: "Your teammates' agents may ask your agents without asking you. Their requests run guarded." },
8100
+ { value: "ask", label: "Ask me first", description: "Each request from a teammate's agent to yours waits for your answer." },
8101
+ { value: "never", label: "Never", description: "Your teammates' agents may not ask yours. Agents of people outside your org never may." }
8102
+ ]
8103
+ }
8104
+ ];
8026
8105
  CONSENT_METHODS = {
8106
+ "reach.get": { params: strictObject({}), result: ReachSnapshot },
8107
+ "reach.set": { params: ReachSetParams, result: ReachSnapshot },
8108
+ "reach.set.preview": { params: ReachSetParams, result: object({ sentence: string2().max(500) }) },
8027
8109
  "grant.list": {
8028
8110
  params: strictObject({ subject: GrantTarget.optional() }),
8029
8111
  result: object({ grants: array(GrantView), review: array(GrantReview) })
@@ -8124,6 +8206,7 @@ var init_jelly2 = __esm(() => {
8124
8206
  from: object({ name: string2().max(200), email: string2().max(320).optional(), org: _enum(["same", "other", "none"]) }).optional(),
8125
8207
  line: string2().max(NOTICE_LINE_CHARS).optional(),
8126
8208
  device: NoticeDevice.optional(),
8209
+ key: string2().regex(/^task:[0-9A-HJKMNP-TV-Z]{26}$/, "task:<task id>").optional(),
8127
8210
  at: Millis
8128
8211
  });
8129
8212
  NoticeCounts = object({ requests: number2().int().nonnegative() });
@@ -8155,7 +8238,7 @@ var init_jelly2 = __esm(() => {
8155
8238
  ticket: Jws.refine((ticket) => ticket.length <= WAKE_TICKET_BYTES, { message: `at most ${WAKE_TICKET_BYTES} bytes` }),
8156
8239
  agents: array(SessionName).max(WAKE_AGENTS)
8157
8240
  }),
8158
- result: object({ relay: _enum(["armed", "pending"]), awaiting: number2().int().nonnegative() })
8241
+ result: object({ relay: _enum(["armed", "pending"]), awaiting: number2().int().nonnegative(), dms: number2().int().nonnegative() })
8159
8242
  },
8160
8243
  "wake.disarm": { params: strictObject({}), result: object({ relay: _enum(["disarmed", "pending"]) }) },
8161
8244
  "daemon.snapshot": {
@@ -8192,6 +8275,7 @@ var init_conversation_views = __esm(() => {
8192
8275
  init_common();
8193
8276
  init_conversation();
8194
8277
  init_a2a();
8278
+ init_shapes();
8195
8279
  Count = number2().int().nonnegative().safe();
8196
8280
  Seq2 = number2().int().nonnegative().safe();
8197
8281
  Name = string2().max(200);
@@ -8219,7 +8303,7 @@ var init_conversation_views = __esm(() => {
8219
8303
  mime: string2().max(100),
8220
8304
  bytes: number2().int().positive().max(LIMITS.blobBytes)
8221
8305
  });
8222
- TaskView = object({ id: Ulid, state: TaskState });
8306
+ TaskView = object({ id: Ulid, state: TaskState, cancel: strictObject({ name: literal("cancel"), token: ActionToken }).optional() });
8223
8307
  EventView = object({
8224
8308
  cid: ConvId,
8225
8309
  id: Ulid,
@@ -8299,7 +8383,8 @@ var init_conversation_views = __esm(() => {
8299
8383
  object({ type: literal("read"), pos: Seq2, cid: ConvId, readUpTo: Seq2, unread: Count, mentions: Count }),
8300
8384
  object({ type: literal("typing"), cid: ConvId, p: ParticipantId, thread: Ulid.optional(), until: Millis }),
8301
8385
  object({ type: literal("activity"), body: ActivityBody }),
8302
- object({ type: literal("gap"), pos: Seq2 })
8386
+ object({ type: literal("gap"), pos: Seq2 }),
8387
+ object({ type: literal("task"), pos: Seq2, task: PersonTask })
8303
8388
  ]).refine((entry) => entry.type !== "event.update" || entry.cid === entry.event.cid && entry.id === entry.event.id, {
8304
8389
  message: "event.update names the same conversation and event as its projection"
8305
8390
  });
@@ -8356,7 +8441,7 @@ var init_conversations = __esm(() => {
8356
8441
  at: Millis,
8357
8442
  cid: ConvId.optional(),
8358
8443
  agent: SessionName.optional(),
8359
- state: _enum(["held", "pending-delivery"])
8444
+ state: _enum(["held", "pending-delivery", "resend", "outdated"])
8360
8445
  });
8361
8446
  ConvListParams = strictObject({ key: Key, filter: strictObject({ unread: boolean2().optional(), mentions: boolean2().optional(), kind: ConvKind.optional(), archived: boolean2().optional() }).optional(), ...Page });
8362
8447
  ConvListResult = ConvPage;
@@ -8671,12 +8756,35 @@ var init_ipc = __esm(() => {
8671
8756
  message: strictObject({ parts: array(Part).min(1).max(LIMITS.partsPerMessage) }).optional(),
8672
8757
  artifact: Artifact.optional(),
8673
8758
  append: boolean2().optional(),
8674
- lastChunk: boolean2().optional()
8675
- }),
8759
+ lastChunk: boolean2().optional(),
8760
+ signIn: strictObject({ tool: string2().min(1).max(TASK_SIGN_IN_TOOL_CHARS) }).optional()
8761
+ }).refine((p) => p.signIn === undefined || p.state === "TASK_STATE_AUTH_REQUIRED", "signIn goes with TASK_STATE_AUTH_REQUIRED"),
8676
8762
  result: TaskRecord
8677
8763
  },
8764
+ "task.question": {
8765
+ params: strictObject({ key: SessionKey, taskId: Ulid, question: TaskQuestion.nullable() }),
8766
+ result: object({ ok: literal(true) })
8767
+ },
8768
+ "tasks.list": {
8769
+ params: strictObject({
8770
+ role: _enum(["requester", "executor"]).optional(),
8771
+ state: TaskState.optional(),
8772
+ session: SessionName.optional().describe("only this agent's tasks"),
8773
+ cursor: string2().min(1).max(200).optional().describe("opaque; from a previous nextCursor"),
8774
+ limit: number2().int().min(1).max(100).optional()
8775
+ }),
8776
+ result: object({
8777
+ tasks: array(PersonTask),
8778
+ nextCursor: string2().max(200).optional().describe("absent when there are no more pages"),
8779
+ total: number2().int().nonnegative()
8780
+ })
8781
+ },
8782
+ "tasks.get": {
8783
+ params: strictObject({ taskId: Ulid, role: _enum(["requester", "executor"]).optional() }),
8784
+ result: PersonTask
8785
+ },
8678
8786
  "task.cancel": {
8679
- params: strictObject({ key: SessionKey, taskId: Ulid, reason: string2().max(1000).optional() }),
8787
+ params: strictObject({ key: SessionKey.optional(), taskId: Ulid, reason: string2().max(1000).optional(), token: ActionToken.optional() }).refine((p) => p.key === undefined !== (p.token === undefined), "give exactly one of key or token"),
8680
8788
  result: TaskRecord
8681
8789
  },
8682
8790
  "events.subscribe": { params: strictObject({ keys: array(SessionKey).min(1).max(256) }), result: Ok2 },
@@ -8764,7 +8872,8 @@ var init_ipc = __esm(() => {
8764
8872
  "task.list",
8765
8873
  "task.get",
8766
8874
  "task.update",
8767
- "task.cancel"
8875
+ "task.cancel",
8876
+ "task.question"
8768
8877
  ];
8769
8878
  HUMAN_METHODS = [
8770
8879
  ...conversationTier("human"),
@@ -8773,6 +8882,9 @@ var init_ipc = __esm(() => {
8773
8882
  "grant.list",
8774
8883
  "grant.set",
8775
8884
  "grant.set.preview",
8885
+ "reach.get",
8886
+ "reach.set",
8887
+ "reach.set.preview",
8776
8888
  "request.list",
8777
8889
  "request.answer",
8778
8890
  "request.answer.preview",
@@ -8782,6 +8894,8 @@ var init_ipc = __esm(() => {
8782
8894
  "notice.subscribe",
8783
8895
  "notice.list",
8784
8896
  "notice.ack",
8897
+ "tasks.list",
8898
+ "tasks.get",
8785
8899
  "device.approve.preview",
8786
8900
  "device.remove.preview",
8787
8901
  "identity.replace.preview",
@@ -8880,7 +8994,12 @@ var init_conv_frames = __esm(() => {
8880
8994
  });
8881
8995
  ConvHeadView = strictObject({ cid: ConvId, head: ConvHead, opHead: ConvHead, shares: Count3 });
8882
8996
  ConvHeadsFrame = strictObject({ t: literal("conv.heads"), req: Req4, heads: array(ConvHeadView).max(LIMITS.convHeads), more: boolean2() });
8883
- ConvCapabilitiesFrame = strictObject({ t: literal("conv.capabilities"), req: Req4, outdated: array(PersonId).max(LIMITS.convOpParticipants) });
8997
+ ConvCapabilitiesFrame = strictObject({
8998
+ t: literal("conv.capabilities"),
8999
+ req: Req4,
9000
+ outdated: array(PersonId).max(LIMITS.convOpParticipants),
9001
+ unknown: array(PersonId).min(1).max(LIMITS.convOpParticipants).optional()
9002
+ });
8884
9003
  ConvPushFrame = union([
8885
9004
  strictObject({ t: literal("conv.push"), cid: ConvId, entry: ConvLogEntry }),
8886
9005
  strictObject({ t: literal("conv.push"), cid: ConvId, share: ConvShareEntry })
@@ -9028,6 +9147,11 @@ var init_agent_tools = __esm(() => {
9028
9147
  titleAuthor: Author.optional().describe("With title: who wrote the request; ours when you asked (mine)"),
9029
9148
  status: authored("The latest status message").optional(),
9030
9149
  statusAuthor: Author.optional().describe("With status: who wrote it; ours when you run the task (asked)"),
9150
+ signIn: object({
9151
+ tool: authored("The tool the executor named").optional(),
9152
+ 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")
9153
+ }).optional().describe("auth_required only: the task waits on a sign-in; the tool was written by the executor, as statusAuthor says"),
9154
+ refusal: object({ reason: authored("The executor's reason") }).optional().describe("rejected only: why the executor declined, in its words, as statusAuthor says"),
9031
9155
  createdAt: iso,
9032
9156
  updatedAt: iso,
9033
9157
  consent: ToolConsentSchema.optional(),
@@ -9204,7 +9328,7 @@ var init_b64u = __esm(() => {
9204
9328
  });
9205
9329
 
9206
9330
  // packages/core/dist/version.js
9207
- var VERSION = "0.8.0";
9331
+ var VERSION = "0.9.0";
9208
9332
 
9209
9333
  // packages/core/dist/jcs.js
9210
9334
  function hasLoneSurrogate(text) {
@@ -11061,8 +11185,11 @@ var init_jelly_host = __esm(() => {
11061
11185
  "people.search",
11062
11186
  "people.get",
11063
11187
  "grant.list",
11188
+ "reach.get",
11064
11189
  "request.list",
11065
11190
  "sessions.list",
11191
+ "tasks.list",
11192
+ "tasks.get",
11066
11193
  "notice.subscribe",
11067
11194
  "notice.list",
11068
11195
  "device.list"
@@ -12387,7 +12514,17 @@ function verifyConsentGrant(raw, ctx) {
12387
12514
  }
12388
12515
  }
12389
12516
  function grantSubjectKey(subject) {
12390
- return "person" in subject ? `person:${subject.person}` : `conv:${subject.conv}`;
12517
+ return "person" in subject ? `person:${subject.person}` : ("conv" in subject) ? `conv:${subject.conv}` : `relation:${subject.relation}`;
12518
+ }
12519
+ function reachChoices(outside, org) {
12520
+ const message = outside === undefined ? undefined : outside.chat === "open" ? "open" : outside.chat === "blocked" ? "never" : "ask";
12521
+ const ask = org?.ask === "open" ? "open" : org?.ask === "request" ? "ask" : org?.ask === "no" ? "never" : undefined;
12522
+ return { ...message ? { message } : {}, ...ask ? { ask } : {} };
12523
+ }
12524
+ function reachGrant(group, value) {
12525
+ if (group === "message")
12526
+ return { subject: { relation: "outside" }, levels: value === "open" ? { chat: "open" } : value === "never" ? { chat: "blocked" } : {} };
12527
+ return { subject: { relation: "org" }, levels: { ask: value === "open" ? "open" : value === "never" ? "no" : "request" } };
12391
12528
  }
12392
12529
  function preferGrant(a, b, current) {
12393
12530
  if (a.ts !== b.ts)
@@ -12429,8 +12566,18 @@ function resignGrant(identity, grant, opts) {
12429
12566
  function levelsOf(grant) {
12430
12567
  return defined({ chat: grant.chat, agentMail: grant.agentMail, ask: grant.ask, trustedUntil: grant.trustedUntil, discoverable: grant.discoverable });
12431
12568
  }
12432
- function effectivePolicy(relation, person, channels, now) {
12569
+ function baseLevels(relation, reach) {
12433
12570
  const base = DEFAULTS[relation];
12571
+ if (relation === "org")
12572
+ return reach.ask === undefined ? base : { ...base, ask: reach.ask === "open" ? "open" : reach.ask === "never" ? "no" : "request" };
12573
+ if (reach.message === "never")
12574
+ return { ...base, chat: "blocked" };
12575
+ if (relation === "stranger" && reach.message === "open")
12576
+ return { ...base, chat: "open" };
12577
+ return base;
12578
+ }
12579
+ function effectivePolicy(relation, person, channels, now, reach = {}) {
12580
+ const base = baseLevels(relation, reach);
12434
12581
  const channelAsk = channels.map((grant) => grant.ask).filter((level) => level !== undefined);
12435
12582
  const channelMail = channels.map((grant) => grant.agentMail).filter((level) => level !== undefined);
12436
12583
  const ask = person?.ask ?? (channelAsk.length > 0 ? channelAsk.reduce((a, b) => ASK_RANK[b] > ASK_RANK[a] ? b : a) : undefined);
@@ -12448,15 +12595,19 @@ function decide(args) {
12448
12595
  const { inbound } = args;
12449
12596
  if (args.relation === "self" || args.sender === args.self)
12450
12597
  return { action: "deliver", consent: "self" };
12451
- const policy = effectivePolicy(args.relation, args.grant, args.channels, args.now);
12598
+ const policy = effectivePolicy(args.relation, args.grant, args.channels, args.now, args.reach);
12452
12599
  const trusted = (chain) => policy.trustedUntil !== undefined && chain.every((person) => person === args.sender || person === args.self);
12453
12600
  if (inbound.kind === "answer")
12454
12601
  return { action: "deliver", consent: trusted([args.sender]) ? "trusted" : "guarded" };
12455
- if (policy.chat === "blocked")
12602
+ const blocked = policy.chat === "blocked";
12603
+ if (blocked && policy.granted.chat)
12456
12604
  return { action: "refuse", code: "refused" };
12457
12605
  const stranger = args.relation === "stranger";
12606
+ const defaultCode = (outside) => outside && !blocked ? "request_required" : "refused";
12458
12607
  switch (inbound.kind) {
12459
12608
  case "op":
12609
+ if (blocked)
12610
+ return { action: "refuse", code: "refused" };
12460
12611
  if (stranger)
12461
12612
  return { action: "refuse", code: "request_required" };
12462
12613
  return { action: "deliver", consent: "guarded" };
@@ -12464,13 +12615,17 @@ function decide(args) {
12464
12615
  if (inbound.agent) {
12465
12616
  if (policy.agentMail === "open")
12466
12617
  return { action: "deliver", consent: "guarded" };
12467
- return { action: "refuse", code: stranger && !policy.granted.agentMail ? "request_required" : "refused" };
12618
+ return { action: "refuse", code: defaultCode(stranger && !policy.granted.agentMail) };
12468
12619
  }
12620
+ if (blocked)
12621
+ return { action: "refuse", code: "refused" };
12469
12622
  if (policy.chat === "request_required")
12470
12623
  return { action: "refuse", code: "request_required" };
12471
12624
  return policy.chat === "muted" ? { action: "deliver", consent: "guarded", silent: true } : { action: "deliver", consent: "guarded" };
12472
12625
  case "request":
12473
- if (stranger)
12626
+ if (blocked)
12627
+ return { action: "refuse", code: "refused" };
12628
+ if (policy.chat === "request_required")
12474
12629
  return { action: "hold" };
12475
12630
  return policy.chat === "muted" ? { action: "deliver", consent: "guarded", silent: true } : { action: "deliver", consent: "guarded" };
12476
12631
  case "ask":
@@ -12478,7 +12633,7 @@ function decide(args) {
12478
12633
  return { action: "deliver", consent: trusted(inbound.chain) ? "trusted" : "guarded" };
12479
12634
  if (policy.ask === "request")
12480
12635
  return { action: "hold" };
12481
- return { action: "refuse", code: !policy.granted.ask && (stranger || args.relation === "contact") ? "request_required" : "refused" };
12636
+ return { action: "refuse", code: defaultCode(!policy.granted.ask && (stranger || args.relation === "contact")) };
12482
12637
  }
12483
12638
  }
12484
12639
  function defined(value) {
@@ -12543,8 +12698,8 @@ function verifyContactCard(raw, ctx) {
12543
12698
  throw asMessagingError(error, "card");
12544
12699
  }
12545
12700
  }
12546
- function cardCan(relation, grant, now) {
12547
- const policy = effectivePolicy(relation, grant, [], now);
12701
+ function cardCan(relation, grant, now, reach = {}) {
12702
+ const policy = effectivePolicy(relation, grant, [], now, reach);
12548
12703
  return {
12549
12704
  chat: policy.chat === "open" || policy.chat === "muted",
12550
12705
  agentMail: policy.agentMail === "open",
@@ -12718,6 +12873,9 @@ function canTransition(from, to) {
12718
12873
  function isTerminal(state) {
12719
12874
  return TERMINAL_STATES.includes(state);
12720
12875
  }
12876
+ function isInterrupted(state) {
12877
+ return INTERRUPTED_STATES.includes(state);
12878
+ }
12721
12879
  var NEXT;
12722
12880
  var init_task = __esm(() => {
12723
12881
  init_a2a();
@@ -13960,6 +14118,7 @@ var init_present = __esm(() => {
13960
14118
  init_errors3();
13961
14119
  init_peer_text();
13962
14120
  init_common();
14121
+ init_ipc();
13963
14122
  init_text();
13964
14123
  PART_CHARS = LIMITS.toolTextChars;
13965
14124
  OWN = createFence({ tag: PEER_FENCE_TAG });
@@ -18665,6 +18824,12 @@ class Store {
18665
18824
  const rows = person === undefined ? this.#all("SELECT * FROM contact_requests ORDER BY at, id") : this.#all("SELECT * FROM contact_requests WHERE person = ? ORDER BY at, id", person);
18666
18825
  return rows.map(contactRequestRow);
18667
18826
  }
18827
+ legacyOutgoingRequests(now) {
18828
+ return this.#all(`SELECT t.id AS id, substr(t.room, 4) AS person, t.at AS at FROM transcript t
18829
+ WHERE t.direction = 'out' AND t.room LIKE 'dm:%'
18830
+ AND EXISTS (SELECT 1 FROM chat_sent c WHERE c.line = t.id AND c.stage = 'sent' AND c.held_at IS NOT NULL AND c.exp > ?)
18831
+ ORDER BY t.at, t.id`, now).map((r) => ({ id: String(r.id), person: String(r.person), at: Number(r.at) }));
18832
+ }
18668
18833
  contactRequestCount() {
18669
18834
  return Number(this.#get("SELECT COUNT(*) AS n FROM contact_requests")?.n ?? 0);
18670
18835
  }
@@ -18694,38 +18859,56 @@ class Store {
18694
18859
  return this.#run("DELETE FROM contact_suggestions WHERE at < ?", before);
18695
18860
  }
18696
18861
  putGrant(subject, grant) {
18862
+ const relation = relationOf(subject);
18863
+ if (relation !== undefined) {
18864
+ this.#run("INSERT INTO reach_grants (relation, grant) VALUES (?, ?) ON CONFLICT (relation) DO UPDATE SET grant = excluded.grant", relation, JSON.stringify(grant));
18865
+ return;
18866
+ }
18697
18867
  this.#run("INSERT INTO grants (subject, grant) VALUES (?, ?) ON CONFLICT (subject) DO UPDATE SET grant = excluded.grant", subject, JSON.stringify(grant));
18698
18868
  }
18699
18869
  grant(subject) {
18700
- const row = this.#get("SELECT grant FROM grants WHERE subject = ?", subject);
18870
+ const relation = relationOf(subject);
18871
+ const row = relation !== undefined ? this.#get("SELECT grant FROM reach_grants WHERE relation = ?", relation) : this.#get("SELECT grant FROM grants WHERE subject = ?", subject);
18701
18872
  return row === undefined ? undefined : JSON.parse(String(row.grant));
18702
18873
  }
18703
18874
  grants() {
18704
- return this.#all("SELECT subject, grant FROM grants ORDER BY subject").map((row) => ({ subject: String(row.subject), grant: JSON.parse(String(row.grant)) }));
18875
+ return [
18876
+ ...this.#all("SELECT subject, grant FROM grants ORDER BY subject").map((row) => ({ subject: String(row.subject), grant: JSON.parse(String(row.grant)) })),
18877
+ ...this.#all("SELECT relation, grant FROM reach_grants ORDER BY relation").map((row) => ({ subject: `${RELATION}${String(row.relation)}`, grant: JSON.parse(String(row.grant)) }))
18878
+ ];
18705
18879
  }
18706
18880
  grantCount() {
18707
18881
  return Number(this.#get("SELECT COUNT(*) AS n FROM grants")?.n ?? 0);
18708
18882
  }
18709
18883
  deleteGrant(subject) {
18710
- this.#run("DELETE FROM grants WHERE subject = ?", subject);
18884
+ const relation = relationOf(subject);
18885
+ if (relation !== undefined)
18886
+ this.#run("DELETE FROM reach_grants WHERE relation = ?", relation);
18887
+ else
18888
+ this.#run("DELETE FROM grants WHERE subject = ?", subject);
18711
18889
  }
18712
18890
  putGrantReview(subject, id, levels, at) {
18713
- this.#run("INSERT INTO grant_reviews (subject, id, levels, at) VALUES (?, ?, ?, ?) ON CONFLICT (subject) DO UPDATE SET id = excluded.id, levels = excluded.levels, at = excluded.at", subject, id, JSON.stringify(levels), at);
18891
+ const relation = relationOf(subject);
18892
+ this.#run(relation !== undefined ? "INSERT INTO reach_reviews (relation, id, levels, at) VALUES (?, ?, ?, ?) ON CONFLICT (relation) DO UPDATE SET id = excluded.id, levels = excluded.levels, at = excluded.at" : "INSERT INTO grant_reviews (subject, id, levels, at) VALUES (?, ?, ?, ?) ON CONFLICT (subject) DO UPDATE SET id = excluded.id, levels = excluded.levels, at = excluded.at", relation ?? subject, id, JSON.stringify(levels), at);
18714
18893
  }
18715
18894
  grantReviews() {
18716
- return this.#all("SELECT * FROM grant_reviews ORDER BY at DESC, subject").map((row) => ({
18717
- subject: String(row.subject),
18718
- id: String(row.id),
18719
- levels: JSON.parse(String(row.levels)),
18720
- at: Number(row.at)
18721
- }));
18895
+ const row = (subject, r) => ({ subject, id: String(r.id), levels: JSON.parse(String(r.levels)), at: Number(r.at) });
18896
+ return [
18897
+ ...this.#all("SELECT * FROM grant_reviews ORDER BY at DESC, subject").map((r) => row(String(r.subject), r)),
18898
+ ...this.#all("SELECT * FROM reach_reviews ORDER BY at DESC, relation").map((r) => row(`${RELATION}${String(r.relation)}`, r))
18899
+ ];
18722
18900
  }
18723
18901
  grantReview(subject) {
18724
- const row = this.#get("SELECT id FROM grant_reviews WHERE subject = ?", subject);
18902
+ const relation = relationOf(subject);
18903
+ const row = relation !== undefined ? this.#get("SELECT id FROM reach_reviews WHERE relation = ?", relation) : this.#get("SELECT id FROM grant_reviews WHERE subject = ?", subject);
18725
18904
  return row === undefined ? undefined : { id: String(row.id) };
18726
18905
  }
18727
18906
  deleteGrantReview(subject) {
18728
- this.#run("DELETE FROM grant_reviews WHERE subject = ?", subject);
18907
+ const relation = relationOf(subject);
18908
+ if (relation !== undefined)
18909
+ this.#run("DELETE FROM reach_reviews WHERE relation = ?", relation);
18910
+ else
18911
+ this.#run("DELETE FROM grant_reviews WHERE subject = ?", subject);
18729
18912
  }
18730
18913
  sentUndelivered(now) {
18731
18914
  return this.#all("SELECT * FROM sent WHERE exp <= ? AND stage IN ('accepted', 'relayed')", now).map(sentRow);
@@ -18795,12 +18978,14 @@ class Store {
18795
18978
  }
18796
18979
  }
18797
18980
  listTasks(filter) {
18798
- const where = ["key = ?"];
18799
- const params = [filter.key];
18981
+ const where = ["1 = 1"];
18982
+ const params = [];
18800
18983
  const add = (clause, ...values) => {
18801
18984
  where.push(clause);
18802
18985
  params.push(...values);
18803
18986
  };
18987
+ if (filter.key !== undefined)
18988
+ add("key = ?", filter.key);
18804
18989
  if (filter.role !== undefined)
18805
18990
  add("role = ?", filter.role);
18806
18991
  if (filter.state !== undefined)
@@ -19187,7 +19372,7 @@ CREATE TABLE IF NOT EXISTS contact_requests (
19187
19372
  ) STRICT;
19188
19373
  CREATE INDEX IF NOT EXISTS contact_requests_person ON contact_requests (person);
19189
19374
  CREATE TABLE IF NOT EXISTS contact_suggestions (id TEXT PRIMARY KEY, session TEXT NOT NULL, email TEXT NOT NULL, at INTEGER NOT NULL) STRICT;
19190
- `, MIGRATIONS, STORE_SCHEMA, kvGet = (db, k) => {
19375
+ `, MIGRATIONS, RELATION = "relation:", relationOf = (subject) => subject.startsWith(RELATION) ? subject.slice(RELATION.length) : undefined, STORE_SCHEMA, kvGet = (db, k) => {
19191
19376
  const row = db.prepare("SELECT v FROM kv WHERE k = ?").get(k);
19192
19377
  return row?.v === undefined || row.v === null ? undefined : String(row.v);
19193
19378
  }, kvSet = (db, k, v) => {
@@ -19263,6 +19448,10 @@ CREATE TABLE IF NOT EXISTS conv_evidence (
19263
19448
  at INTEGER NOT NULL,
19264
19449
  UNIQUE (cid, digest, code)
19265
19450
  ) STRICT;
19451
+ `,
19452
+ `
19453
+ CREATE TABLE IF NOT EXISTS reach_grants (relation TEXT PRIMARY KEY, grant TEXT NOT NULL) STRICT;
19454
+ CREATE TABLE IF NOT EXISTS reach_reviews (relation TEXT PRIMARY KEY, id TEXT NOT NULL, levels TEXT NOT NULL, at INTEGER NOT NULL) STRICT;
19266
19455
  `
19267
19456
  ];
19268
19457
  STORE_SCHEMA = 1 + MIGRATIONS.length;
@@ -19854,7 +20043,7 @@ class ConversationEngine {
19854
20043
  const pending = this.options.store.convPending(cid);
19855
20044
  if (rekey.cause === "device" && pending) {
19856
20045
  const first = admitConvEvent(parseWire(pending.event));
19857
- if (first.class === "op" && first.body.op === "rekey" && first.body.cause === "device" && first.body.device === rekey.device)
20046
+ if (first.class === "op" && first.body.op === "rekey" && first.body.cause === "device" && first.body.device === rekey.device && (rekey.until === undefined || first.body.until !== undefined && first.body.until <= rekey.until))
19858
20047
  return first.id;
19859
20048
  return await this.#queueRekey(cid, rekey, true);
19860
20049
  }
@@ -20637,6 +20826,14 @@ class ConversationIpc {
20637
20826
  inboxNotify(key, cid) {
20638
20827
  this.#label(this.#current(), cid, { key, own: this.options.own().person });
20639
20828
  }
20829
+ taskChanged(side) {
20830
+ this.#ring.push({ pos: ++this.#pos, task: { id: side.id, role: side.role } });
20831
+ if (this.#ring.length > REPLAY)
20832
+ this.#floor = this.#ring.shift().pos;
20833
+ this.options.put("conv.ipc.pos", String(this.#pos));
20834
+ for (const wake of [...this.#waiters])
20835
+ wake();
20836
+ }
20640
20837
  bind(engine) {
20641
20838
  if (this.#engine === engine)
20642
20839
  return;
@@ -20732,7 +20929,7 @@ class ConversationIpc {
20732
20929
  if (authority?.scope === "host")
20733
20930
  throw new MessagingError("forbidden", "conversation content needs an action grant");
20734
20931
  const name = method;
20735
- const result = await this.call(name, params, ctx);
20932
+ const result = await this.call(name, params, ctx, authority);
20736
20933
  const parsed = LIVE_CONVERSATION_METHODS[name].result.parse(result);
20737
20934
  if (Buffer.byteLength(JSON.stringify(parsed)) > RESPONSE_BYTES)
20738
20935
  throw new MessagingError("too_large", "conversation response exceeds the byte budget");
@@ -20833,7 +21030,13 @@ class ConversationIpc {
20833
21030
  throw new MessagingError("forbidden", "the session cannot read this conversation");
20834
21031
  if (access.access === "ask")
20835
21032
  return HOLD;
20836
- const all = await projectEvents(engine, cid, scope.participant, this.#name, (p) => !this.options.blocked(participantPerson(p)));
21033
+ const orgs = new Map;
21034
+ const name = async (p) => {
21035
+ const profile = await this.#name(p);
21036
+ orgs.set(p, profile.org);
21037
+ return profile;
21038
+ };
21039
+ const all = await projectEvents(engine, cid, scope.participant, name, (p) => !this.options.blocked(p, orgs.get(p) ?? "none"));
20837
21040
  let visible = all.filter((e) => e.seq >= access.since && e.at >= access.after);
20838
21041
  if (scope.agent && this.#state(engine, cid, scope).state.agentContext === "none") {
20839
21042
  const roots = new Set(visible.filter((e) => e.author.p === scope.participant || e.mentions?.some((m) => ("p" in m) && m.p === scope.participant)).map((e) => e.thread ?? e.id));
@@ -20908,10 +21111,30 @@ class ConversationIpc {
20908
21111
  return { events: [{ type: "gap", pos: this.#pos }], pos: this.#pos };
20909
21112
  const events = [];
20910
21113
  let pos = after;
20911
- for (const item of this.#ring.filter((i) => i.pos > after)) {
21114
+ const range = this.#ring.filter((i) => i.pos > after);
21115
+ for (const [index, item] of range.entries()) {
20912
21116
  pos = item.pos;
20913
- const c = item.change;
20914
21117
  let rendered;
21118
+ if ("task" in item) {
21119
+ const side = item.task;
21120
+ if (scope.key !== undefined || scope.ctx.tier !== "admin" || !this.options.task)
21121
+ continue;
21122
+ if (range.slice(index + 1).some((later) => ("task" in later) && later.task.id === side.id && later.task.role === side.role))
21123
+ continue;
21124
+ const task = await this.options.task(side.id, side.role, scope.ctx, scope.authority);
21125
+ if (!task)
21126
+ continue;
21127
+ rendered = { type: "task", pos, task };
21128
+ if (Buffer.byteLength(JSON.stringify([...events, rendered])) > RESPONSE_BYTES - 4096) {
21129
+ pos = item.pos - 1;
21130
+ break;
21131
+ }
21132
+ events.push(rendered);
21133
+ if (events.length >= (p.limit ?? 50))
21134
+ break;
21135
+ continue;
21136
+ }
21137
+ const c = item.change;
20915
21138
  try {
20916
21139
  if (this.#access(engine, c.cid, scope).access !== "allow")
20917
21140
  continue;
@@ -20981,7 +21204,7 @@ class ConversationIpc {
20981
21204
  result = await read();
20982
21205
  return result;
20983
21206
  }
20984
- async call(method, raw, ctx) {
21207
+ async call(method, raw, ctx, authority) {
20985
21208
  if (ctx.tier === "agent" && CONVERSATION_TIERS[method] !== "agent")
20986
21209
  throw new MessagingError("forbidden", "this method is human-only");
20987
21210
  const p = LIVE_CONVERSATION_METHODS[method].params.parse(raw);
@@ -21003,7 +21226,7 @@ class ConversationIpc {
21003
21226
  const sessions = this.options.sessions().map((s) => ({ ...s, seats: engine?.list().filter((c) => c.state?.participants.some((p) => p.p === `${this.options.own().person}/${s.agent}` && p.state === "member")).map((c) => c.cid) ?? [] }));
21004
21227
  return { sessions };
21005
21228
  }
21006
- const scope = this.#scope("key" in p ? p.key : undefined, ctx);
21229
+ const scope = { ...this.#scope("key" in p ? p.key : undefined, ctx), authority };
21007
21230
  const engine = this.#current();
21008
21231
  const { cursor: _cursor, ...position } = p;
21009
21232
  const query = canonicalize({ method, person: scope.own, session: scope.key ?? null, ...position });
@@ -21212,8 +21435,11 @@ class ConversationIpc {
21212
21435
  await this.options.current?.(p.participants.map((s) => participantPerson(s.p)));
21213
21436
  }
21214
21437
  const { cid, ...fields } = p;
21215
- if (method !== "conv.meta" && engine.pending(cid).length > 0)
21438
+ for (let drains = 0;method !== "conv.meta" && engine.pending(cid).length > 0; drains++) {
21439
+ if (drains === CONTROL_DRAINS)
21440
+ throw new MessagingError("conv_conflict", "the conversation is busy sending; try again");
21216
21441
  await engine.flush(cid);
21442
+ }
21217
21443
  if (method === "conv.meta")
21218
21444
  await engine.queueMeta(cid, p.meta);
21219
21445
  else
@@ -21241,7 +21467,7 @@ class ConversationIpc {
21241
21467
  }
21242
21468
  }
21243
21469
  }
21244
- var DEFERRED_LINES = 50, DEFERRED_MS, HOLD, REPLAY = 1e4, RESPONSE_BYTES;
21470
+ var DEFERRED_LINES = 50, DEFERRED_MS, CONTROL_DRAINS = 3, HOLD, REPLAY = 1e4, RESPONSE_BYTES;
21245
21471
  var init_conv_ipc = __esm(() => {
21246
21472
  init_dist();
21247
21473
  init_conv_project();
@@ -22140,19 +22366,19 @@ function expectation(view, target, wake, policy) {
22140
22366
  const s = view.sessions.find((row) => row.name === target.session);
22141
22367
  if (!s)
22142
22368
  return;
22143
- return reach({ kept: s.unregisteredAt !== undefined, tier: s.tier, state: s.state, wakes: policy.wake(s) }, wake);
22369
+ return reach2({ kept: s.unregisteredAt !== undefined, tier: s.tier, state: s.state, wakes: policy.wake(s) }, wake);
22144
22370
  }
22145
22371
  const s = view.peers.find((row) => row.machine === target.machine)?.presence.sessions.find((info) => info.session === target.session);
22146
22372
  if (!s)
22147
22373
  return;
22148
22374
  if (!view.online.has(target.machine))
22149
22375
  return "offline";
22150
- return reach({ kept: s.kept === true, tier: s.tier, state: s.state, wakes: s.wake === true }, wake);
22376
+ return reach2({ kept: s.kept === true, tier: s.tier, state: s.state, wakes: s.wake === true }, wake);
22151
22377
  }
22152
22378
  function withExpect(expect) {
22153
22379
  return expect === undefined ? {} : { expect };
22154
22380
  }
22155
- function reach(s, wake) {
22381
+ function reach2(s, wake) {
22156
22382
  if (s.kept)
22157
22383
  return wake && s.wakes ? "wake" : "kept";
22158
22384
  if (s.tier === "pull")
@@ -22196,7 +22422,7 @@ function fitPresence(sessions, baseBytes, limit) {
22196
22422
  var FEATURES;
22197
22423
  var init_directory2 = __esm(() => {
22198
22424
  init_dist();
22199
- FEATURES = ["tasks", "conversations"];
22425
+ FEATURES = ["tasks", "conversations", "reach"];
22200
22426
  });
22201
22427
 
22202
22428
  // packages/daemon/dist/delegation.js
@@ -22905,6 +23131,22 @@ class DirectoryIdentity {
22905
23131
  const head = this.#head;
22906
23132
  return head !== undefined && this.#options.now() - head.sth.ts > maxAgeMs + LIMITS.epochHeartbeatMs;
22907
23133
  }
23134
+ async freshen(persons, batch = LIMITS.dirLookupPersons) {
23135
+ const size1 = Math.max(1, Math.min(batch, LIMITS.dirLookupPersons));
23136
+ const now = this.#options.now();
23137
+ const size = this.#head?.sth.size ?? 0;
23138
+ const stale = [...new Set(persons)].filter((person) => {
23139
+ if (!isHosted(person))
23140
+ return false;
23141
+ const held = this.#persons.get(person);
23142
+ return !held || now - held.fetchedAt >= LIMITS.dirFreshMs || held.size < size;
23143
+ });
23144
+ if (stale.length === 0 && !this.#headOlderThan(LIMITS.dirFreshMs))
23145
+ return;
23146
+ await this.#latest();
23147
+ for (let i = 0;i < stale.length; i += size1)
23148
+ await this.#fetch(stale.slice(i, i + size1));
23149
+ }
22908
23150
  async profile(person) {
22909
23151
  if (!isHosted(person))
22910
23152
  throw new MessagingError("not_found", "no such person in this realm");
@@ -24041,8 +24283,10 @@ class JellyAuth {
24041
24283
  this.#deps = deps;
24042
24284
  }
24043
24285
  authorize(call) {
24044
- const sessionCall = call.params !== null && typeof call.params === "object" && "key" in call.params && typeof call.params.key === "string";
24045
- if (call.ctx.tier === "agent" || AGENT.has(call.method) && (!Object.hasOwn(CONVERSATION_METHODS, call.method) || sessionCall))
24286
+ const params = call.params !== null && typeof call.params === "object" ? call.params : undefined;
24287
+ const sessionCall = params !== undefined && typeof params.key === "string";
24288
+ const tokenCall = params !== undefined && Object.hasOwn(params, "token");
24289
+ if (call.ctx.tier === "agent" || AGENT.has(call.method) && !tokenCall && (!Object.hasOwn(CONVERSATION_METHODS, call.method) || sessionCall))
24046
24290
  return;
24047
24291
  const refuse = (error) => {
24048
24292
  this.#deps.log("warn", "jelly.grant.refused", { method: call.method, code: error.code, reason: error.message });
@@ -24075,7 +24319,7 @@ class JellyAuth {
24075
24319
  ...grant.dev !== undefined ? { dev: grant.dev.slice(0, 200) } : {},
24076
24320
  ...grant.mbr !== undefined ? { mbr: grant.mbr.slice(0, 200) } : {}
24077
24321
  });
24078
- return { scope: grant.scope, expiresAt: (grant.exp + JELLY_HOST_SKEW_S) * 1000 };
24322
+ return { scope: grant.scope, expiresAt: (grant.exp + JELLY_HOST_SKEW_S) * 1000, ...grant.dev !== undefined ? { dev: grant.dev } : {}, ...grant.mbr !== undefined ? { mbr: grant.mbr } : {} };
24079
24323
  }
24080
24324
  ticket(p) {
24081
24325
  const refuse = (error) => {
@@ -24409,6 +24653,9 @@ class Notices {
24409
24653
  held(row) {
24410
24654
  this.#file({ id: `r_${row.id}`, category: "request", person: row.person, source: row.id, at: row.at });
24411
24655
  }
24656
+ task(row) {
24657
+ this.#file({ id: `m_task${row.taskId}`, category: "message", person: row.person, source: `${TASK_SOURCE}${row.taskId}`, at: row.at });
24658
+ }
24412
24659
  alert(kind, at) {
24413
24660
  this.#file({ id: `a_${kind.replace(/_/g, "")}${at}`, category: "alert", source: kind, at });
24414
24661
  }
@@ -24421,7 +24668,7 @@ class Notices {
24421
24668
  #file(row) {
24422
24669
  if (!this.#host.store.putNotice(row))
24423
24670
  return;
24424
- this.#host.emit({ type: "notice", notice: { id: row.id, category: row.category, at: row.at } });
24671
+ this.#host.emit({ type: "notice", notice: { id: row.id, category: row.category, ...row.source.startsWith(TASK_SOURCE) ? { key: `task:${row.source.slice(TASK_SOURCE.length)}` } : {}, at: row.at } });
24425
24672
  }
24426
24673
  async subscribe(ctx, p, until) {
24427
24674
  const rows = this.#host.store.noticesAfter(p.after, REPLAY2);
@@ -24477,6 +24724,8 @@ class Notices {
24477
24724
  const own = row.person === this.#host.me();
24478
24725
  notice.from = profile ? { name: profile.name.slice(0, 200), ...profile.email !== undefined ? { email: profile.email.slice(0, 320) } : {}, org: profile.org } : { name: row.person, org: own ? "same" : "none" };
24479
24726
  }
24727
+ if (row.category === "message" && row.source.startsWith(TASK_SOURCE))
24728
+ notice.key = `task:${row.source.slice(TASK_SOURCE.length)}`;
24480
24729
  if (row.category === "alert" && row.source.startsWith("device:")) {
24481
24730
  const device = this.#host.device?.(Number(row.source.slice("device:".length)));
24482
24731
  if (device)
@@ -24502,7 +24751,7 @@ function deviceLine(d) {
24502
24751
  const line = d.change === "added" ? `New device on your account: ${what}, ${vouchWords(d)}.` : d.change === "vouched" ? `${what} on your account is now vouched by Jelly ID: ${vouchWords(d)}.` : d.change === "replaced" ? `'${d.replaces?.label ?? "a device"}' was replaced by ${what}, ${vouchWords(d)}.` : `${what} was removed from your account.`;
24503
24752
  return line.length <= NOTICE_LINE_CHARS ? line : `${line.slice(0, NOTICE_LINE_CHARS - 1)}\u2026`;
24504
24753
  }
24505
- var ALERT_TEXT, REPLAY2 = 500;
24754
+ var ALERT_TEXT, TASK_SOURCE = "task:", REPLAY2 = 500;
24506
24755
  var init_notices = __esm(() => {
24507
24756
  init_dist();
24508
24757
  ALERT_TEXT = {
@@ -24512,6 +24761,68 @@ var init_notices = __esm(() => {
24512
24761
  };
24513
24762
  });
24514
24763
 
24764
+ // packages/daemon/dist/task-view.js
24765
+ function plainCapped(text, max, singleLine) {
24766
+ let budget = max;
24767
+ for (;; ) {
24768
+ const out = cleanPeerText(text, { singleLine, maxChars: budget }).trim();
24769
+ if (out.length <= max)
24770
+ return out.length > 0 ? out : undefined;
24771
+ budget = Math.max(0, budget - (out.length - max));
24772
+ }
24773
+ }
24774
+ function executorWords(record) {
24775
+ const message = record.task.status.message;
24776
+ return message !== undefined && (record.role === "executor" || message.metadata?.[URI.provenance] !== undefined);
24777
+ }
24778
+ function signInTool(record) {
24779
+ if (!executorWords(record))
24780
+ return;
24781
+ const value = record.task.status.message?.metadata?.[URI.signIn];
24782
+ if (typeof value !== "object" || value === null || Array.isArray(value))
24783
+ return;
24784
+ const tool = value.tool;
24785
+ return typeof tool === "string" ? plainCapped(tool, TASK_SIGN_IN_TOOL_CHARS, true) : undefined;
24786
+ }
24787
+ function refusalReason2(record) {
24788
+ if (!executorWords(record))
24789
+ return;
24790
+ const text = (record.task.status.message?.parts ?? []).flatMap((part) => typeof part.text === "string" ? [part.text] : []).join(`
24791
+ `);
24792
+ return plainCapped(text, TASK_REFUSAL_CHARS, false);
24793
+ }
24794
+ function withTaskView(record, me, named) {
24795
+ const view = { ...record };
24796
+ delete view.agent;
24797
+ delete view.person;
24798
+ delete view.org;
24799
+ delete view.signIn;
24800
+ delete view.refusal;
24801
+ delete view.question;
24802
+ if (record.peer.agent !== undefined)
24803
+ view.agent = record.peer.agent;
24804
+ if (named !== undefined) {
24805
+ view.person = { id: record.peer.person, name: named.name.slice(0, 200) };
24806
+ view.org = named.org;
24807
+ }
24808
+ const state = record.task.status.state;
24809
+ if (state === "TASK_STATE_AUTH_REQUIRED") {
24810
+ const mine = record.role === "executor" || me !== undefined && record.peer.person === me;
24811
+ const tool = signInTool(record);
24812
+ view.signIn = { ...tool !== undefined ? { tool } : {}, by: mine ? "you" : "them" };
24813
+ }
24814
+ if (state === "TASK_STATE_REJECTED") {
24815
+ const reason = refusalReason2(record);
24816
+ if (reason !== undefined)
24817
+ view.refusal = { reason };
24818
+ }
24819
+ return view;
24820
+ }
24821
+ var SIGN_IN_TEXT = "Sign-in required.";
24822
+ var init_task_view = __esm(() => {
24823
+ init_dist();
24824
+ });
24825
+
24515
24826
  // packages/daemon/dist/realm/counterpart.js
24516
24827
  function successors(realm, recorded) {
24517
24828
  const chain = [recorded];
@@ -25408,6 +25719,104 @@ var init_wake_bridge = __esm(() => {
25408
25719
  };
25409
25720
  });
25410
25721
 
25722
+ // packages/daemon/dist/action-tokens.js
25723
+ import { randomBytes as randomBytes14 } from "crypto";
25724
+ function actionView(ctx, authority, sessionKey) {
25725
+ if (ctx.tier !== "admin" || sessionKey !== undefined || authority?.scope === "host")
25726
+ return;
25727
+ return { connection: ctx.id, ...authority?.dev !== undefined ? { dev: authority.dev } : {}, ...authority?.mbr !== undefined ? { mbr: authority.mbr } : {} };
25728
+ }
25729
+ function taskCancelAction(tokens, task, view) {
25730
+ if (view === undefined || task.role !== "requester" || task.ended)
25731
+ return;
25732
+ return { name: "cancel", token: tokens.offer(view, "cancel", `task:${task.id}`) };
25733
+ }
25734
+
25735
+ class ActionTokens {
25736
+ now;
25737
+ limits;
25738
+ #held = new Map;
25739
+ #offers = new Map;
25740
+ #perView = new Map;
25741
+ constructor(now, limits = { perView: ACTION_TOKENS_PER_VIEW, total: ACTION_TOKENS_TOTAL, ttlMs: ACTION_TOKEN_TTL_MS }) {
25742
+ this.now = now;
25743
+ this.limits = limits;
25744
+ }
25745
+ get size() {
25746
+ return this.#held.size;
25747
+ }
25748
+ offer(view, name, target) {
25749
+ const now = this.now();
25750
+ const key = viewKey(view);
25751
+ const offered = this.#offers.get(offerKey(key, name, target));
25752
+ if (offered !== undefined) {
25753
+ const held = this.#held.get(offered);
25754
+ if (held !== undefined && held.exp - now > this.limits.ttlMs / 2)
25755
+ return offered;
25756
+ }
25757
+ this.sweep();
25758
+ while ((this.#perView.get(key) ?? 0) >= this.limits.perView && this.#dropOldest((h) => h.view === key))
25759
+ ;
25760
+ while (this.#held.size >= this.limits.total && this.#dropOldest(() => true))
25761
+ ;
25762
+ const token = randomBytes14(32).toString("base64url");
25763
+ this.#held.set(token, { name, target, view: key, connection: view.connection, exp: now + this.limits.ttlMs });
25764
+ this.#offers.set(offerKey(key, name, target), token);
25765
+ this.#perView.set(key, (this.#perView.get(key) ?? 0) + 1);
25766
+ return token;
25767
+ }
25768
+ redeem(token, want) {
25769
+ const held = this.#held.get(token);
25770
+ if (held !== undefined)
25771
+ this.#delete(token, held);
25772
+ this.#match(held, want);
25773
+ }
25774
+ check(token, want) {
25775
+ this.#match(this.#held.get(token), want);
25776
+ }
25777
+ disconnected(connection) {
25778
+ for (const [token, held] of [...this.#held])
25779
+ if (held.connection === connection)
25780
+ this.#delete(token, held);
25781
+ }
25782
+ sweep() {
25783
+ const now = this.now();
25784
+ for (const [token, held] of [...this.#held])
25785
+ if (held.exp <= now)
25786
+ this.#delete(token, held);
25787
+ }
25788
+ #match(held, want) {
25789
+ const ok = held !== undefined && held.exp > this.now() && held.name === want.name && held.target === want.target && held.view === viewKey(want.view);
25790
+ if (!ok)
25791
+ throw new MessagingError("grant_invalid", "the action token is not one this daemon offered for this action, item and view, or it was spent or expired; read the view again");
25792
+ }
25793
+ #dropOldest(which) {
25794
+ for (const [token, held] of this.#held) {
25795
+ if (!which(held))
25796
+ continue;
25797
+ this.#delete(token, held);
25798
+ return true;
25799
+ }
25800
+ return false;
25801
+ }
25802
+ #delete(token, held) {
25803
+ this.#held.delete(token);
25804
+ const key = offerKey(held.view, held.name, held.target);
25805
+ if (this.#offers.get(key) === token)
25806
+ this.#offers.delete(key);
25807
+ const count = (this.#perView.get(held.view) ?? 1) - 1;
25808
+ if (count > 0)
25809
+ this.#perView.set(held.view, count);
25810
+ else
25811
+ this.#perView.delete(held.view);
25812
+ }
25813
+ }
25814
+ var ACTION_TOKEN_TTL_MS, ACTION_TOKENS_PER_VIEW = 256, ACTION_TOKENS_TOTAL = 4096, viewKey = (view) => JSON.stringify([view.connection, view.dev ?? null, view.mbr ?? null]), offerKey = (view, name, target) => JSON.stringify([view, name, target]);
25815
+ var init_action_tokens = __esm(() => {
25816
+ init_dist();
25817
+ ACTION_TOKEN_TTL_MS = 10 * 60000;
25818
+ });
25819
+
25411
25820
  // packages/daemon/dist/consent/grants.js
25412
25821
  class Grants {
25413
25822
  #host;
@@ -25447,14 +25856,44 @@ class Grants {
25447
25856
  const members = this.#host.participants(cid, event?.seq);
25448
25857
  return members.includes(person) && members.includes(me.person);
25449
25858
  })),
25859
+ reach: this.reach(),
25450
25860
  inbound,
25451
25861
  now: this.#host.now()
25452
25862
  });
25453
25863
  }
25864
+ hides(participant, org) {
25865
+ const slash = participant.indexOf("/");
25866
+ const person = slash === -1 ? participant : participant.slice(0, slash);
25867
+ const { me } = this.#host.joined();
25868
+ if (person === me.person)
25869
+ return false;
25870
+ const grant = this.personGrant(person);
25871
+ if (grant?.chat === "blocked")
25872
+ return true;
25873
+ if (slash !== -1 || org !== "other")
25874
+ return false;
25875
+ const relation = this.hasGrant(person) ? "contact" : "stranger";
25876
+ return effectivePolicy(relation, grant, [], this.#host.now(), this.reach()).chat === "blocked";
25877
+ }
25454
25878
  personGrant(person) {
25455
25879
  const grant = this.#current(grantSubjectKey({ person }));
25456
25880
  return grant && levelsOf(grant);
25457
25881
  }
25882
+ relationGrant(relation) {
25883
+ const grant = this.#current(grantSubjectKey({ relation }));
25884
+ return grant && levelsOf(grant);
25885
+ }
25886
+ reach() {
25887
+ return reachChoices(this.relationGrant("outside"), this.relationGrant("org"));
25888
+ }
25889
+ relationRecord(relation) {
25890
+ const key = grantSubjectKey({ relation });
25891
+ return { grant: this.#store.grant(key) ?? null, review: this.#store.grantReviews().find(({ subject }) => subject === key) ?? null };
25892
+ }
25893
+ relationReview(relation) {
25894
+ const row = this.#store.grantReviews().find(({ subject }) => subject === grantSubjectKey({ relation }));
25895
+ return row?.levels;
25896
+ }
25458
25897
  conversationGrants(conversations) {
25459
25898
  return conversations.flatMap((conv) => {
25460
25899
  const grant = this.#current(grantSubjectKey({ conv }));
@@ -25476,8 +25915,8 @@ class Grants {
25476
25915
  }
25477
25916
  list(p) {
25478
25917
  const only = p.subject === undefined ? undefined : this.#subjectKey(p.subject, false);
25479
- const grants = this.#all().filter(({ subject }) => only === undefined || subject === only).map(({ grant }) => this.#view(grant)).sort((a, b) => b.ts - a.ts || (a.id < b.id ? 1 : -1));
25480
- const review = this.#store.grantReviews().filter(({ subject }) => only === undefined || subject === only).map(({ subject, levels, at }) => ({ subject: this.#subjectView(subject), ...levels, at }));
25918
+ const grants = this.#all().filter(({ subject }) => !isRelationKey(subject)).filter(({ subject }) => only === undefined || subject === only).map(({ grant }) => this.#view(grant)).sort((a, b) => b.ts - a.ts || (a.id < b.id ? 1 : -1));
25919
+ const review = this.#store.grantReviews().filter(({ subject }) => !isRelationKey(subject)).filter(({ subject }) => only === undefined || subject === only).map(({ subject, levels, at }) => ({ subject: this.#subjectView(subject), ...levels, at }));
25481
25920
  return { grants, review };
25482
25921
  }
25483
25922
  set(p, opts = {}) {
@@ -25505,6 +25944,22 @@ class Grants {
25505
25944
  delete levels.trustedUntil;
25506
25945
  return this.set({ subject: { person }, ...levels }, opts);
25507
25946
  }
25947
+ setRelations(changes) {
25948
+ if (changes.length === 0)
25949
+ return;
25950
+ const { identity, realm, me } = this.#host.joined();
25951
+ const now = this.#host.now();
25952
+ const grants = changes.map(({ subject, levels }) => signConsentGrant(identity, { realm: realm.realm, person: me.person, subject, levels, now }));
25953
+ this.#host.tx(() => {
25954
+ for (const grant of grants) {
25955
+ const key = grantSubjectKey(grant.subject);
25956
+ this.#store.putGrant(key, grant);
25957
+ this.#store.deleteGrantReview(key);
25958
+ }
25959
+ this.#sync("grant", grants);
25960
+ });
25961
+ this.#host.log("grant.set", { subject: "relation", grants: grants.length });
25962
+ }
25508
25963
  check(p) {
25509
25964
  const { identity, realm, me } = this.#host.joined();
25510
25965
  const now = this.#host.now();
@@ -25531,7 +25986,7 @@ class Grants {
25531
25986
  }
25532
25987
  const key = grantSubjectKey(grant.subject);
25533
25988
  const held = this.#held(key);
25534
- if (!held && this.#store.grantCount() >= LIMITS.grantsPerPerson) {
25989
+ if (!held && !isRelationKey(key) && this.#store.grantCount() >= LIMITS.grantsPerPerson) {
25535
25990
  this.#host.log("grant.sync.skipped", { id: env.id, code: "rate_limited" });
25536
25991
  continue;
25537
25992
  }
@@ -25665,10 +26120,17 @@ class Grants {
25665
26120
  return grantSubjectKey(target);
25666
26121
  }
25667
26122
  #subjectOf(key) {
25668
- return key.startsWith("person:") ? { person: key.slice("person:".length) } : { conv: key.slice("conv:".length) };
26123
+ if (key.startsWith("person:"))
26124
+ return { person: key.slice("person:".length) };
26125
+ if (isRelationKey(key))
26126
+ return { relation: key.slice("relation:".length) };
26127
+ return { conv: key.slice("conv:".length) };
25669
26128
  }
25670
26129
  #subjectView(key) {
25671
- return this.#subjectOf(key);
26130
+ const subject = this.#subjectOf(key);
26131
+ if ("relation" in subject)
26132
+ throw new MessagingError("internal", "a relation grant has no grant view");
26133
+ return subject;
25672
26134
  }
25673
26135
  #view(grant) {
25674
26136
  return {
@@ -25681,6 +26143,9 @@ class Grants {
25681
26143
  };
25682
26144
  }
25683
26145
  }
26146
+ function isRelationKey(key) {
26147
+ return key.startsWith("relation:");
26148
+ }
25684
26149
  function levelsFrom(p, now) {
25685
26150
  const trustedUntil = p.trustedUntil ?? (p.trust ? now + LIMITS.trustDefaultMs : undefined);
25686
26151
  return levelsOf({ chat: p.chat, agentMail: p.agentMail, ask: p.ask, trustedUntil, discoverable: p.discoverable });
@@ -25816,7 +26281,7 @@ class Cards {
25816
26281
  seq,
25817
26282
  now,
25818
26283
  ...grant !== undefined && typeof held?.id === "string" ? { grantId: held.id } : {},
25819
- can: cardCan(relation, grant, now)
26284
+ can: cardCan(relation, grant, now, this.#host.reach?.() ?? {})
25820
26285
  });
25821
26286
  for (const device of realm.known(person).devices) {
25822
26287
  this.#host.route(this.#host.seal(device.machine, cardMessage2({ card }, "card")));
@@ -25943,7 +26408,10 @@ class ConversationRequests {
25943
26408
  return this.#ids().map((id) => this.get(id)).sort((a, b) => a.at - b.at || a.id.localeCompare(b.id));
25944
26409
  }
25945
26410
  waitingOnThem() {
25946
- return this.list().filter((r) => r.direction === "outgoing" && r.state === "submitted" && r.request.descriptor.expiresAt > this.host.now()).map((r) => ({ id: r.id, kind: "contact", to: r.person, name: r.person, at: r.at, cid: r.request.descriptor.cid }));
26411
+ const now = this.host.now(), rows = this.list().filter((r) => r.direction === "outgoing");
26412
+ const submitted = rows.filter((r) => r.state === "submitted" && r.request.descriptor.expiresAt > now).map((r) => ({ id: r.id, kind: "contact", to: r.person, name: r.person, at: r.at, cid: r.request.descriptor.cid }));
26413
+ const resend = rows.filter((r) => r.state === "resend" && r.at + LIMITS.ttlMs > now && !rows.some((n) => n.person === r.person && n.request && n.at >= r.at)).map((r) => ({ id: r.id, kind: "contact", to: r.person, name: r.person, at: r.at, cid: dmConvId(this.host.person, r.person), resend: true }));
26414
+ return [...submitted, ...resend];
25947
26415
  }
25948
26416
  cutoverLegacy(outgoing = []) {
25949
26417
  this.host.store.tx(() => {
@@ -26015,6 +26483,8 @@ class ConversationRequests {
26015
26483
  throw new MessagingError("expired", "request has expired");
26016
26484
  if (this.host.chat(d.from) === "blocked")
26017
26485
  throw new MessagingError("declined", "request declined");
26486
+ if (this.host.refuses?.(d.from))
26487
+ throw new MessagingError("refused", "this person accepts no message requests from outside their org");
26018
26488
  return this.host.store.tx(() => {
26019
26489
  const old = this.get(d.id);
26020
26490
  if (old) {
@@ -26180,6 +26650,7 @@ class Requests {
26180
26650
  resolve: (email) => this.#resolve(email),
26181
26651
  current: (person) => h.current?.([person]) ?? Promise.resolve(),
26182
26652
  chat: (person) => h.grants.personGrant(person)?.chat,
26653
+ refuses: (person) => h.grants.decide(person, { kind: "request" }).action === "refuse",
26183
26654
  grantChat: (person, chat) => {
26184
26655
  h.grants.setPerson(person, { chat }, { card: false });
26185
26656
  },
@@ -26246,7 +26717,7 @@ class Requests {
26246
26717
  }, proof, body);
26247
26718
  }
26248
26719
  });
26249
- this.#lifecycle.cutoverLegacy();
26720
+ this.#lifecycle.cutoverLegacy(h.store.legacyOutgoingRequests(h.now()));
26250
26721
  return this.#lifecycle;
26251
26722
  }
26252
26723
  #mailbox() {
@@ -26327,12 +26798,17 @@ class Requests {
26327
26798
  return true;
26328
26799
  if (payload.stage === "held")
26329
26800
  this.host.store.setKv(`conv.request.held.${sent.request}`, "true");
26801
+ if (payload.stage === "delivered" || payload.stage === "read") {
26802
+ if (!this.host.store.getKv(`conv.request.outdated.${sent.request}`))
26803
+ this.host.log("request.outdated", { id: sent.request });
26804
+ this.host.store.setKv(`conv.request.outdated.${sent.request}`, "true");
26805
+ }
26330
26806
  return true;
26331
26807
  }
26332
26808
  async waitingOnThem() {
26333
- return this.#current().waitingOnThem().map((row) => ({
26809
+ return this.#current().waitingOnThem().map(({ resend, ...row }) => ({
26334
26810
  ...row,
26335
- state: this.host.store.getKv(`conv.request.held.${row.id}`) === "true" ? "held" : "pending-delivery"
26811
+ state: resend ? "resend" : this.host.store.getKv(`conv.request.outdated.${row.id}`) === "true" ? "outdated" : this.host.store.getKv(`conv.request.held.${row.id}`) === "true" ? "held" : "pending-delivery"
26336
26812
  }));
26337
26813
  }
26338
26814
  get(id) {
@@ -26449,6 +26925,91 @@ var init_requests2 = __esm(() => {
26449
26925
  init_request_importer();
26450
26926
  });
26451
26927
 
26928
+ // packages/daemon/dist/consent/reach.js
26929
+ function revision(grants, choices) {
26930
+ const records = Object.fromEntries(GROUPS2.map((group) => [group, grants.relationRecord(RELATION2[group])]));
26931
+ return sha256(canonicalize({ records, choices })).slice(0, 32);
26932
+ }
26933
+ function reachSnapshot(grants) {
26934
+ const choices = grants.reach();
26935
+ const lapsed = reachChoices(grants.relationReview("outside"), grants.relationReview("org"));
26936
+ return {
26937
+ revision: revision(grants, choices),
26938
+ available: true,
26939
+ groups: REACH_QUESTIONS.map((question) => ({
26940
+ id: question.id,
26941
+ label: question.label,
26942
+ options: question.options.map((option) => ({ ...option })),
26943
+ value: choices[question.id] ?? null,
26944
+ ...lapsed[question.id] !== undefined ? { lapsed: lapsed[question.id] } : {}
26945
+ }))
26946
+ };
26947
+ }
26948
+ function changesOf(grants, p) {
26949
+ if (p.revision !== revision(grants, grants.reach())) {
26950
+ throw new MessagingError("conflict", "your reach choices changed since they were read; read them again and choose again");
26951
+ }
26952
+ return GROUPS2.flatMap((group) => p.changes[group] === undefined ? [] : [[group, p.changes[group]]]);
26953
+ }
26954
+ function reachSet(grants, p) {
26955
+ const changes = changesOf(grants, p);
26956
+ grants.setRelations(changes.map(([group, value]) => {
26957
+ const { levels } = reachGrant(group, value);
26958
+ return { subject: { relation: RELATION2[group] }, levels };
26959
+ }));
26960
+ return reachSnapshot(grants);
26961
+ }
26962
+ function reachPreview(grants, p) {
26963
+ const changes = changesOf(grants, p);
26964
+ if (changes.length === 0)
26965
+ return "Nothing changes.";
26966
+ return changes.map(([group, value]) => SENTENCES[group][value]).join(" ");
26967
+ }
26968
+ var RELATION2, GROUPS2, SENTENCES;
26969
+ var init_reach = __esm(() => {
26970
+ init_dist();
26971
+ RELATION2 = { message: "outside", ask: "org" };
26972
+ GROUPS2 = ["message", "ask"];
26973
+ SENTENCES = {
26974
+ message: {
26975
+ open: "Anyone outside your org may message you.",
26976
+ ask: "People outside your org send you a message request first.",
26977
+ never: "No one outside your org may message you or send you a request, except people you have let message you."
26978
+ },
26979
+ ask: {
26980
+ open: "Your teammates' agents may ask yours without asking you; their requests run guarded.",
26981
+ ask: "You answer each request from a teammate's agent to yours first.",
26982
+ never: "Your teammates' agents may not ask yours."
26983
+ }
26984
+ };
26985
+ });
26986
+
26987
+ // packages/daemon/dist/conv-caps.js
26988
+ async function ensureCurrent(link, own, persons, handle) {
26989
+ const others = [...new Set(persons)].filter((p) => p !== own);
26990
+ if (!others.length)
26991
+ return;
26992
+ let outdated;
26993
+ try {
26994
+ outdated = (await link?.convCaps?.(others))?.outdated ?? [];
26995
+ } catch {
26996
+ return;
26997
+ }
26998
+ if (!outdated.length)
26999
+ return;
27000
+ const handles = await Promise.all(outdated.map(async (p) => {
27001
+ try {
27002
+ return await handle(p);
27003
+ } catch {
27004
+ return p;
27005
+ }
27006
+ }));
27007
+ throw new MessagingError("peer_outdated", `${handles.join(", ")} ${handles.length === 1 ? "needs" : "need"} to update Messaging`);
27008
+ }
27009
+ var init_conv_caps = __esm(() => {
27010
+ init_dist();
27011
+ });
27012
+
26452
27013
  // packages/daemon/dist/consent/sentence.js
26453
27014
  function grantSentence(p, me, now) {
26454
27015
  const levels = levelsFrom(p, now);
@@ -26487,6 +27048,41 @@ var init_sentence = __esm(() => {
26487
27048
  init_grants();
26488
27049
  });
26489
27050
 
27051
+ // packages/daemon/dist/wake-dms.js
27052
+ function teammateDms(conversations, self) {
27053
+ const member = (state, person) => state.participants.some((p) => p.person === person && p.state === "member");
27054
+ const seated = (state, person) => state.participants.some((p) => p.p === person && p.state === "member");
27055
+ const channels = conversations.flatMap((c) => c.state?.kind === "channel" && seated(c.state, self) ? [c.state] : []);
27056
+ const shares = new Map;
27057
+ const shared = (person) => {
27058
+ let found = shares.get(person);
27059
+ if (found === undefined)
27060
+ shares.set(person, found = channels.some((state) => seated(state, person)));
27061
+ return found;
27062
+ };
27063
+ const found = [];
27064
+ for (const { cid, state } of conversations) {
27065
+ if (state?.kind !== "dm" || !member(state, self))
27066
+ continue;
27067
+ const others = [...new Set(state.participants.filter((p) => p.person !== self).map((p) => p.person))];
27068
+ if (others.length !== 1)
27069
+ continue;
27070
+ const person = others[0];
27071
+ if (!member(state, person) || !shared(person))
27072
+ continue;
27073
+ found.push({ cid, person });
27074
+ }
27075
+ return found;
27076
+ }
27077
+ function wakeArmable(grants, person, org) {
27078
+ const chat = grants.personGrant(person)?.chat;
27079
+ if (chat === "blocked" || chat === "muted")
27080
+ return false;
27081
+ if (grants.hides(person, org))
27082
+ return false;
27083
+ return true;
27084
+ }
27085
+
26490
27086
  // packages/daemon/dist/daemon.js
26491
27087
  async function startDaemon(options) {
26492
27088
  const daemon = await Daemon.start(options);
@@ -26498,6 +27094,7 @@ async function startDaemon(options) {
26498
27094
  status: () => daemon.status(),
26499
27095
  conversations: () => daemon.conversations(),
26500
27096
  conversationsReady: () => daemon.conversationsReady(),
27097
+ actionTokens: () => daemon.actionTokens(),
26501
27098
  sweep: () => daemon.sweep(),
26502
27099
  close: () => daemon.close()
26503
27100
  };
@@ -26562,11 +27159,14 @@ class Daemon {
26562
27159
  #cards;
26563
27160
  #requests;
26564
27161
  #notices;
27162
+ #named = new Map;
27163
+ #questions = new Map;
26565
27164
  #jellyHost;
26566
27165
  #attached = false;
26567
27166
  #wakeArmed = false;
26568
27167
  #snapshots;
26569
27168
  #jellyAuth;
27169
+ #actions;
26570
27170
  #state = "starting";
26571
27171
  #sync;
26572
27172
  #pending = [];
@@ -26602,6 +27202,7 @@ class Daemon {
26602
27202
  this.#jellyHttp = options.jellyHttp;
26603
27203
  this.#loginWait = options.loginWait;
26604
27204
  this.#jellyHost = options.jellyHost;
27205
+ this.#actions = new ActionTokens(() => this.#now());
26605
27206
  this.#config = readConfig(paths.config);
26606
27207
  this.#configStamp = configStamp(paths.config);
26607
27208
  this.closed = new Promise((resolve) => this.#settleClosed = resolve);
@@ -26619,9 +27220,10 @@ class Daemon {
26619
27220
  put: (key, value) => store.setKv(key, value),
26620
27221
  fresh: (people) => this.#withFresh(people, () => {}),
26621
27222
  refuseSecrets: (value) => this.#refuseSecrets(value, "conv.send", "conversation line"),
26622
- blocked: (person) => this.#grants.personGrant(person)?.chat === "blocked",
27223
+ blocked: (participant, org) => this.#grants.hides(participant, org),
26623
27224
  current: (persons) => this.#ensureCurrent(persons),
26624
- outgoingRequests: () => this.#requests.waitingOnThem()
27225
+ outgoingRequests: () => this.#requests.waitingOnThem(),
27226
+ task: (id, role, ctx, authority) => this.#personTaskEntry(id, role, ctx, authority)
26625
27227
  });
26626
27228
  this.#conversationInbox = new ConversationInbox({
26627
27229
  engine: () => this.#conversationEngine(),
@@ -26706,6 +27308,7 @@ class Daemon {
26706
27308
  joined: () => this.#joined(),
26707
27309
  relation: (person) => this.#grants.relation(person),
26708
27310
  personGrant: (person) => this.#grants.personGrant(person),
27311
+ reach: () => this.#grants.reach(),
26709
27312
  prefetch: (person) => this.#grants.prefetch(person),
26710
27313
  fresh: async (persons, fn) => {
26711
27314
  await this.#withFresh(persons, () => this.#tx(fn), { first: false });
@@ -27051,7 +27654,7 @@ class Daemon {
27051
27654
  },
27052
27655
  "device.list": () => {
27053
27656
  const result = this.#directory?.list() ?? this.#rosterDevices();
27054
- return { devices: result.devices.map((d) => d.self && this.#away() ? { ...d, away: true } : d) };
27657
+ return { devices: result.devices.map((d) => this.#withFeatures(d.self && this.#away() ? { ...d, away: true } : d)) };
27055
27658
  },
27056
27659
  "device.remove": (p) => this.#homeDirectory("device.remove").remove(p),
27057
27660
  "device.link": (p) => this.#homeDirectory("device.link").link(p),
@@ -27095,10 +27698,10 @@ class Daemon {
27095
27698
  "inbox.ack": (p, ctx) => this.#ack(this.#bound(p, ctx), ctx),
27096
27699
  "task.create": (p, ctx) => this.#sendTask({ ...this.#bound(p, ctx), kind: "task" }),
27097
27700
  "task.continue": (p, ctx) => this.#sendTask({ ...this.#bound(p, ctx), kind: "message" }),
27098
- "task.list": (p, ctx) => this.#listTasks(this.#bound(p, ctx)),
27099
- "task.get": (p, ctx) => this.#getTask(this.#bound(p, ctx)),
27100
- "task.update": (p, ctx) => this.#updateTask(this.#bound(p, ctx)),
27101
- "task.cancel": (p, ctx) => this.#cancelTask(this.#bound(p, ctx)),
27701
+ "task.list": (p, ctx) => this.#listTasks(this.#bound(p, ctx), ctx.tier === "admin"),
27702
+ "task.get": (p, ctx) => this.#getTask(this.#bound(p, ctx), ctx.tier === "admin"),
27703
+ "task.update": (p, ctx) => this.#updateTask(this.#bound(p, ctx), ctx.tier === "admin"),
27704
+ "task.cancel": (p, ctx, authority) => p.token === undefined ? this.#cancelTask(this.#bound({ ...p, key: p.key }, ctx), ctx.tier === "admin") : this.#cancelByToken(p, p.token, this.#tokenView("task.cancel", ctx, authority)),
27102
27705
  "events.subscribe": (p, ctx) => {
27103
27706
  for (const key of p.keys)
27104
27707
  this.#bound({ key }, ctx);
@@ -27128,9 +27731,24 @@ class Daemon {
27128
27731
  this.#grants.check(p);
27129
27732
  return { sentence: grantSentence(p, this.#joined().me.person, this.#now()) };
27130
27733
  },
27131
- "request.list": async () => ({ requests: [...this.#holds.list().requests, ...await this.#requests.list(), ...this.#conversationInvitations.list()] }),
27132
- "request.answer": (p) => this.#conversationInvitations.holds(p.id) ? this.#conversationInvitations.answer(p.id, p.answer, p.all) : this.#requests.holds(p.id) ? this.#requests.answer(p) : this.#holds.answer(p),
27133
- "request.answer.preview": (p) => this.#answerPreview(p),
27734
+ "reach.get": () => reachSnapshot(this.#grants),
27735
+ "reach.set": (p) => reachSet(this.#grants, p),
27736
+ "reach.set.preview": (p) => ({ sentence: reachPreview(this.#grants, p) }),
27737
+ "request.list": async (_p, ctx, authority) => {
27738
+ const requests = [...this.#holds.list().requests, ...await this.#requests.list(), ...this.#conversationInvitations.list()];
27739
+ const view = actionView(ctx, authority, undefined);
27740
+ return { requests: view === undefined ? requests : requests.map((request) => this.#withActions(request, view)) };
27741
+ },
27742
+ "request.answer": (p, ctx, authority) => {
27743
+ this.#requestToken("request.answer", p, ctx, authority, "redeem");
27744
+ const { token: _token, ...answer } = p;
27745
+ return this.#conversationInvitations.holds(answer.id) ? this.#conversationInvitations.answer(answer.id, answer.answer, answer.all) : this.#requests.holds(answer.id) ? this.#requests.answer(answer) : this.#holds.answer(answer);
27746
+ },
27747
+ "request.answer.preview": (p, ctx, authority) => {
27748
+ this.#requestToken("request.answer.preview", p, ctx, authority, "check");
27749
+ const { token: _token, ...answer } = p;
27750
+ return this.#answerPreview(answer);
27751
+ },
27134
27752
  "contact.request": (p) => this.#requests.request(p),
27135
27753
  "contact.request.preview": (p) => this.#requests.preview(p),
27136
27754
  "card.get": async (p) => {
@@ -27147,6 +27765,9 @@ class Daemon {
27147
27765
  "notice.subscribe": (p, ctx, authority) => this.#notices.subscribe(ctx, p, authority?.scope === "host" ? authority.expiresAt : undefined),
27148
27766
  "notice.list": (p) => this.#notices.list(p),
27149
27767
  "notice.ack": (p) => this.#notices.ack(p),
27768
+ "task.question": (p, ctx) => this.#setQuestion(this.#bound(p, ctx)),
27769
+ "tasks.list": (p, ctx, authority) => this.#personTasks(p, actionView(ctx, authority, undefined)),
27770
+ "tasks.get": (p, ctx, authority) => this.#personTaskGet(p, actionView(ctx, authority, undefined)),
27150
27771
  "device.remove.preview": (p) => this.#removePreview(p),
27151
27772
  ...this.#conversationIpc.handlers()
27152
27773
  };
@@ -27214,8 +27835,9 @@ class Daemon {
27214
27835
  this.#publish(event);
27215
27836
  }
27216
27837
  #emitTask(row, origin, update) {
27217
- const record = this.#withFollowUps(boundedRecord(row.record, TASK_BYTES));
27838
+ const record = this.#taskView(this.#withFollowUps(boundedRecord(row.record, TASK_BYTES)));
27218
27839
  this.#emit({ type: "task", key: row.record.key, record, origin, ...update ? { update: structuredClone(update) } : {} });
27840
+ this.#conversationIpc.taskChanged({ id: row.record.task.id, role: row.record.role });
27219
27841
  }
27220
27842
  #overBudget(before, after) {
27221
27843
  const size = jsonBytes(after);
@@ -27420,6 +28042,7 @@ class Daemon {
27420
28042
  #disconnected(ctx) {
27421
28043
  if (this.#closed)
27422
28044
  return;
28045
+ this.#actions.disconnected(ctx.id);
27423
28046
  this.#notices.disconnected(ctx.id);
27424
28047
  let changed = false;
27425
28048
  for (const [key, lapsed] of [...this.#lapsed])
@@ -28335,6 +28958,8 @@ ${row.receipt ? "receipt" : "envelope"}`;
28335
28958
  };
28336
28959
  this.#store.putTask(row);
28337
28960
  this.#emitTask(row, "peer");
28961
+ if (sender.person !== this.#joined().me.person)
28962
+ this.#notices.task({ taskId, person: sender.person, at: now });
28338
28963
  } else if (executor) {
28339
28964
  executor.record.task.history = [...executor.record.task.history ?? [], message];
28340
28965
  executor.record.updatedAt = now;
@@ -28407,6 +29032,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
28407
29032
  if (isTerminal(row.record.task.status.state))
28408
29033
  return this.#dropped("invalid_state", env.id);
28409
29034
  const status = { state: "TASK_STATE_CANCELED", timestamp: iso3(now) };
29035
+ this.#questions.delete(row.record.task.id);
28410
29036
  row.record.task.status = status;
28411
29037
  row.record.updatedAt = now;
28412
29038
  this.#store.putTask(row);
@@ -28488,11 +29114,11 @@ ${row.receipt ? "receipt" : "envelope"}`;
28488
29114
  }
28489
29115
  throw new MessagingError("not_found", "no such task for this session");
28490
29116
  }
28491
- #getTask(p) {
29117
+ async#getTask(p, owner) {
28492
29118
  for (const role of ["requester", "executor"]) {
28493
29119
  const row = this.#store.task(p.taskId, role);
28494
29120
  if (row && row.record.key === p.key)
28495
- return this.#withFollowUps(row.record);
29121
+ return this.#withQuestion(await this.#viewNamed(this.#withFollowUps(row.record)), owner);
28496
29122
  }
28497
29123
  throw new MessagingError("not_found", "no such task for this session");
28498
29124
  }
@@ -28511,7 +29137,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
28511
29137
  }));
28512
29138
  return { ...record, followUps };
28513
29139
  }
28514
- #listTasks(p) {
29140
+ async#listTasks(p, owner) {
28515
29141
  const limit = p.limit ?? 50;
28516
29142
  const { rows, total, more } = this.#store.listTasks({
28517
29143
  key: p.key,
@@ -28535,13 +29161,14 @@ ${row.receipt ? "receipt" : "envelope"}`;
28535
29161
  }
28536
29162
  const last = tasks.at(-1);
28537
29163
  const truncated = tasks.length < rows.length;
29164
+ await Promise.all([...new Set(tasks.map((t) => t.peer.person))].map((person) => this.#nameOf(person)));
28538
29165
  return {
28539
- tasks,
29166
+ tasks: tasks.map((t) => this.#withQuestion(this.#taskView(t), owner)),
28540
29167
  total,
28541
29168
  ...(more || truncated) && last ? { nextCursor: encodeCursor({ updatedAt: last.updatedAt, taskId: last.task.id, role: last.role }) } : {}
28542
29169
  };
28543
29170
  }
28544
- async#updateTask(p) {
29171
+ async#updateTask(p, owner) {
28545
29172
  this.#joined();
28546
29173
  const row = this.#ownTask(p.key, p.taskId, "executor");
28547
29174
  this.#refuseSecrets(p, "task.update", "task update");
@@ -28555,7 +29182,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
28555
29182
  const now = this.#now();
28556
29183
  const status = statusChange ? {
28557
29184
  state: p.state,
28558
- ...p.message !== undefined ? { message: agentMessage(p.message.parts, task.id, task.contextId, now) } : {},
29185
+ ...p.signIn !== undefined ? { message: signInMessage(agentMessage(p.message?.parts ?? [{ text: SIGN_IN_TEXT }], task.id, task.contextId, now), p.signIn.tool) } : p.message !== undefined ? { message: agentMessage(p.message.parts, task.id, task.contextId, now) } : {},
28559
29186
  timestamp: iso3(now)
28560
29187
  } : undefined;
28561
29188
  const after = structuredClone(row.record);
@@ -28581,6 +29208,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
28581
29208
  }
28582
29209
  if (status) {
28583
29210
  const env = this.#seal("task.status", requester, session, { taskId: task.id, contextId, status }, { replyTo: request });
29211
+ this.#questions.delete(task.id);
28584
29212
  task.status = status;
28585
29213
  row.record.updatedAt = now;
28586
29214
  this.#emitTask(row, "local", { status });
@@ -28590,12 +29218,63 @@ ${row.receipt ? "receipt" : "envelope"}`;
28590
29218
  this.#store.putTask(row);
28591
29219
  });
28592
29220
  await this.#settle(ids);
28593
- return boundedRecord(this.#store.task(p.taskId, "executor")?.record ?? row.record, TASK_BYTES);
29221
+ return this.#withQuestion(await this.#viewNamed(boundedRecord(this.#store.task(p.taskId, "executor")?.record ?? row.record, TASK_BYTES)), owner);
28594
29222
  }
28595
- async#cancelTask(p) {
29223
+ async#cancelTask(p, owner) {
28596
29224
  this.#joined();
28597
- const row = this.#ownTask(p.key, p.taskId, "requester");
28598
- this.#refuseSecrets(p, "task.cancel", "task cancel");
29225
+ return this.#cancelRow(this.#ownTask(p.key, p.taskId, "requester"), p, owner);
29226
+ }
29227
+ #tokenView(method, ctx, authority) {
29228
+ const view = actionView(ctx, authority, undefined);
29229
+ if (view === undefined) {
29230
+ this.#log("warn", "action.token.refused", { method, reason: "not the human tier" });
29231
+ throw new MessagingError("forbidden", `${method} takes an action token only on the human tier`);
29232
+ }
29233
+ return view;
29234
+ }
29235
+ #requestToken(method, p, ctx, authority, use) {
29236
+ if (p.token === undefined) {
29237
+ if (this.#jellyHost === undefined)
29238
+ return;
29239
+ this.#log("warn", "action.token.refused", { method, reason: "missing" });
29240
+ throw new MessagingError("grant_required", `${method} under a Jelly host needs the request's action token from request.list`);
29241
+ }
29242
+ this.#useToken(method, p.token, { name: p.answer, target: `request:${p.id}`, view: this.#tokenView(method, ctx, authority) }, use);
29243
+ }
29244
+ #useToken(method, token, want, use) {
29245
+ try {
29246
+ if (use === "redeem")
29247
+ this.#actions.redeem(token, want);
29248
+ else
29249
+ this.#actions.check(token, want);
29250
+ } catch (error) {
29251
+ this.#log("warn", "action.token.refused", { method, reason: "no matching live token" });
29252
+ throw error;
29253
+ }
29254
+ }
29255
+ actionTokens() {
29256
+ return this.#actions.size;
29257
+ }
29258
+ #withActions(request, view) {
29259
+ if (request.kind === "contact" && request.state !== "pending")
29260
+ return request;
29261
+ const names = request.kind === "suggestion" ? ["decline"] : request.kind === "contact" ? ["accept", "decline", "block"] : request.kind === "agent-invitation" ? ["accept", "decline"] : ["once", "always", "decline", "block"];
29262
+ return { ...request, actions: names.map((name) => ({ name, token: this.#actions.offer(view, name, `request:${request.id}`) })) };
29263
+ }
29264
+ taskCancel(row, role, view) {
29265
+ return taskCancelAction(this.#actions, { id: row.record.task.id, role, ended: isTerminal(row.record.task.status.state) }, view);
29266
+ }
29267
+ async#cancelByToken(p, token, view) {
29268
+ this.#joined();
29269
+ this.#useToken("task.cancel", token, { name: "cancel", target: `task:${p.taskId}`, view }, "redeem");
29270
+ const row = this.#store.task(p.taskId, "requester");
29271
+ if (row === undefined)
29272
+ throw new MessagingError("not_found", "no task this person's agent asked has that id");
29273
+ return this.#cancelRow(row, p, true);
29274
+ }
29275
+ async#cancelRow(row, p, owner) {
29276
+ const { token: _token, ...scanned } = p;
29277
+ this.#refuseSecrets(scanned, "task.cancel", "task cancel");
28599
29278
  if (isTerminal(row.record.task.status.state))
28600
29279
  throw new MessagingError("invalid_state", "the task is in a terminal state");
28601
29280
  const env = this.#tx(() => {
@@ -28604,11 +29283,112 @@ ${row.receipt ? "receipt" : "envelope"}`;
28604
29283
  return env;
28605
29284
  });
28606
29285
  await this.#settle([env.id]);
28607
- return boundedRecord(this.#store.task(p.taskId, "requester")?.record ?? row.record, TASK_BYTES);
29286
+ return this.#withQuestion(await this.#viewNamed(boundedRecord(this.#store.task(p.taskId, "requester")?.record ?? row.record, TASK_BYTES)), owner);
29287
+ }
29288
+ async#nameOf(person) {
29289
+ const realm = this.#realm ?? this.#directory?.realm;
29290
+ if (!realm)
29291
+ return this.#named.get(person);
29292
+ let named;
29293
+ try {
29294
+ const profile = await realm.profile(person);
29295
+ named = { name: profile.name, org: profile.org };
29296
+ } catch (error) {
29297
+ if (!(error instanceof MessagingError) || error.code !== "not_found")
29298
+ return this.#named.get(person);
29299
+ named = { name: person, org: "none" };
29300
+ }
29301
+ if (this.#named.size >= NAMED_PERSONS && !this.#named.has(person))
29302
+ this.#named.clear();
29303
+ this.#named.set(person, named);
29304
+ return named;
29305
+ }
29306
+ #taskView(record) {
29307
+ const named = this.#named.get(record.peer.person);
29308
+ if (named === undefined)
29309
+ this.#nameOf(record.peer.person).catch(() => {
29310
+ return;
29311
+ });
29312
+ return withTaskView(record, this.#me(), named);
29313
+ }
29314
+ #setQuestion(p) {
29315
+ const row = this.#ownTask(p.key, p.taskId, "executor");
29316
+ if (p.question === null) {
29317
+ if (this.#questions.delete(p.taskId))
29318
+ this.#conversationIpc.taskChanged({ id: p.taskId, role: "executor" });
29319
+ return { ok: true };
29320
+ }
29321
+ if (!isInterrupted(row.record.task.status.state))
29322
+ throw new MessagingError("invalid_state", "only a task in auth-required or input-required waits on a question");
29323
+ const host = this.#store.sessionHost(p.key);
29324
+ if (host !== undefined && host.sessionID !== p.question.sessionId)
29325
+ throw new MessagingError("forbidden", "a question names the session's own host session");
29326
+ this.#questions.set(p.taskId, { sessionId: p.question.sessionId, approvalId: p.question.approvalId });
29327
+ this.#conversationIpc.taskChanged({ id: p.taskId, role: "executor" });
29328
+ return { ok: true };
29329
+ }
29330
+ async#personTask(row, view) {
29331
+ const record = this.#withQuestion(await this.#viewNamed(this.#withFollowUps(boundedRecord(row.record, TASK_BYTES))), true);
29332
+ const { key: _key, ...rest } = record;
29333
+ const cancel = this.taskCancel(row, row.record.role, view);
29334
+ return { ...rest, session: this.#sessionName(row), ...cancel ? { cancel } : {} };
29335
+ }
29336
+ async#personTasks(p, view) {
29337
+ const limit = p.limit ?? 50;
29338
+ const key = p.session === undefined ? undefined : this.#store.sessionByName(p.session)?.key;
29339
+ if (p.session !== undefined && key === undefined)
29340
+ return { tasks: [], total: 0 };
29341
+ const { rows, total, more } = this.#store.listTasks({
29342
+ ...key !== undefined ? { key } : {},
29343
+ role: p.role,
29344
+ state: p.state,
29345
+ after: p.cursor === undefined ? undefined : decodeCursor(p.cursor),
29346
+ limit
29347
+ });
29348
+ const tasks = [];
29349
+ let bytes = 0;
29350
+ for (const row of rows) {
29351
+ const task = await this.#personTask(row, view);
29352
+ const size = jsonBytes(task);
29353
+ if (tasks.length > 0 && bytes + size > LIST_BYTES)
29354
+ break;
29355
+ tasks.push(task);
29356
+ bytes += size;
29357
+ }
29358
+ const last = tasks.at(-1);
29359
+ return {
29360
+ tasks,
29361
+ total,
29362
+ ...(more || tasks.length < rows.length) && last ? { nextCursor: encodeCursor({ updatedAt: last.updatedAt, taskId: last.task.id, role: last.role }) } : {}
29363
+ };
29364
+ }
29365
+ async#personTaskGet(p, view) {
29366
+ const rows = (p.role !== undefined ? [p.role] : ["requester", "executor"]).flatMap((role) => this.#store.task(p.taskId, role) ?? []);
29367
+ if (rows.length === 0)
29368
+ throw new MessagingError("not_found", "no task of this person's agents has that id");
29369
+ if (rows.length > 1)
29370
+ throw new MessagingError("invalid_request", "this device holds both sides of that task; supply role");
29371
+ return await this.#personTask(rows[0], view);
29372
+ }
29373
+ async#personTaskEntry(id, role, ctx, authority) {
29374
+ const row = this.#store.task(id, role);
29375
+ return row ? await this.#personTask(row, actionView(ctx, authority, undefined)) : undefined;
29376
+ }
29377
+ #withQuestion(view, owner) {
29378
+ const question = this.#questions.get(view.task.id);
29379
+ if (!owner || question === undefined || view.role !== "executor" || !isInterrupted(view.task.status.state))
29380
+ return view;
29381
+ return { ...view, question: { ...question } };
29382
+ }
29383
+ async#viewNamed(record) {
29384
+ return withTaskView(record, this.#me(), await this.#nameOf(record.peer.person).catch(() => {
29385
+ return;
29386
+ }));
28608
29387
  }
28609
29388
  sweep() {
28610
29389
  if (this.#closed)
28611
29390
  return;
29391
+ this.#actions.sweep();
28612
29392
  this.#directory?.sweep();
28613
29393
  const now = this.#now();
28614
29394
  this.#expireTickets(now);
@@ -28663,6 +29443,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
28663
29443
  message: agentMessage([{ text: INPUT_TIMEOUT_TEXT }], row.record.task.id, row.record.task.contextId, now),
28664
29444
  timestamp: iso3(now)
28665
29445
  };
29446
+ this.#questions.delete(row.record.task.id);
28666
29447
  row.record.task.status = status;
28667
29448
  row.record.updatedAt = now;
28668
29449
  this.#store.putTask(row);
@@ -28670,26 +29451,7 @@ ${row.receipt ? "receipt" : "envelope"}`;
28670
29451
  this.#sendStatus(row);
28671
29452
  }
28672
29453
  async#ensureCurrent(persons) {
28673
- const own = this.#joined().me.person;
28674
- const others = [...new Set(persons)].filter((p) => p !== own);
28675
- if (!others.length)
28676
- return;
28677
- let outdated;
28678
- try {
28679
- outdated = (await this.#transport?.conversations?.convCaps?.(others))?.outdated ?? [];
28680
- } catch {
28681
- return;
28682
- }
28683
- if (!outdated.length)
28684
- return;
28685
- const handles = await Promise.all(outdated.map(async (p) => {
28686
- try {
28687
- return (await this.#joined().realm.profile(p)).handle;
28688
- } catch {
28689
- return p;
28690
- }
28691
- }));
28692
- throw new MessagingError("peer_outdated", `${handles.join(", ")} ${handles.length === 1 ? "needs" : "need"} to update Messaging`);
29454
+ await ensureCurrent(this.#transport?.conversations, this.#joined().me.person, persons, async (p) => (await this.#joined().realm.profile(p)).handle);
28693
29455
  }
28694
29456
  conversations() {
28695
29457
  return this.#conversationEngine();
@@ -29059,6 +29821,13 @@ ${row.receipt ? "receipt" : "envelope"}`;
29059
29821
  const devices = realm.known(me.person).devices.map((d) => ({ device: d.machine, label: d.label, vouched: "admin", current: true, self: d.machine === identity.machine }));
29060
29822
  return { devices };
29061
29823
  }
29824
+ #withFeatures(d) {
29825
+ const raw = d.self ? [...FEATURES] : this.#store.directoryRow(d.device)?.presence.features;
29826
+ if (raw === undefined)
29827
+ return d;
29828
+ const features = [...new Set(raw.filter((f) => PresenceFeature.safeParse(f).success))].slice(0, 32);
29829
+ return { ...d, features };
29830
+ }
29062
29831
  #away() {
29063
29832
  return this.#jellyHost !== undefined && !this.#attached;
29064
29833
  }
@@ -29094,10 +29863,37 @@ ${row.receipt ? "receipt" : "envelope"}`;
29094
29863
  throw new MessagingError("invalid_request", "the wake URL is not on the realm's Jelly ID");
29095
29864
  const agents = [...new Set(p.agents)];
29096
29865
  const awaiting = this.#store.awaitedRequests(agents, TERMINAL_STATES, WAKE_AWAITING);
29097
- const sent = await home.wake({ url: p.url, ticket: p.ticket, agents, awaiting });
29866
+ const dms = await this.#wakeDms();
29867
+ const sent = await home.wake({ url: p.url, ticket: p.ticket, agents, awaiting, dms });
29098
29868
  this.#wakeArmed = true;
29099
- this.#log("info", "wake.arm", { relay: sent === "sent" ? "armed" : "pending", agents: agents.length, awaiting: awaiting.length });
29100
- return { relay: sent === "sent" ? "armed" : "pending", awaiting: awaiting.length };
29869
+ this.#log("info", "wake.arm", { relay: sent === "sent" ? "armed" : "pending", agents: agents.length, awaiting: awaiting.length, dms: dms.length });
29870
+ return { relay: sent === "sent" ? "armed" : "pending", awaiting: awaiting.length, dms: dms.length };
29871
+ }
29872
+ async#wakeDms() {
29873
+ const engine = this.#conversationEngine();
29874
+ if (!engine)
29875
+ return [];
29876
+ const { me, realm } = this.#joined();
29877
+ const orgs = new Map;
29878
+ const orgOf = (person) => {
29879
+ let org = orgs.get(person);
29880
+ if (!org)
29881
+ orgs.set(person, org = realm.profile(person).then((profile) => profile.org, () => "none"));
29882
+ return org;
29883
+ };
29884
+ const candidates = teammateDms(engine.list(), me.person);
29885
+ await realm.freshen?.(candidates.map((c) => c.person)).catch(() => {});
29886
+ const dms = [];
29887
+ for (const { cid, person } of candidates) {
29888
+ if (dms.length >= WAKE_DMS)
29889
+ break;
29890
+ if (this.#shouldArm(person, await orgOf(person)))
29891
+ dms.push(cid);
29892
+ }
29893
+ return dms;
29894
+ }
29895
+ #shouldArm(person, org) {
29896
+ return wakeArmable(this.#grants, person, org);
29101
29897
  }
29102
29898
  async#wakeDisarm() {
29103
29899
  const home = this.#directory;
@@ -29782,6 +30578,9 @@ ${row.receipt ? "receipt" : "envelope"}`;
29782
30578
  function jsonBytes(value) {
29783
30579
  return Buffer.byteLength(JSON.stringify(value) ?? "");
29784
30580
  }
30581
+ function signInMessage(message, tool) {
30582
+ return { ...message, metadata: { ...message.metadata, [URI.signIn]: { tool } }, extensions: [...message.extensions ?? [], URI.signIn] };
30583
+ }
29785
30584
  function boundedRecord(record, limit) {
29786
30585
  if (jsonBytes(record) <= limit)
29787
30586
  return structuredClone(record);
@@ -29879,7 +30678,7 @@ function signInToken(token) {
29879
30678
  return false;
29880
30679
  }
29881
30680
  }
29882
- var DAY3 = 86400000, KEYCHAIN_LEFTOVERS_KEY = "keychain.stale", DEFAULT_TUNING, KEPT_MS, INPUT_TIMEOUT_MS, TASK_BYTES = 1048576, PREDECESSOR_KEY = "identity.predecessor", REPLACE_LABEL_KEY = "identity.replace.label", REPLACED_KEY = "identity.replaced", ERASURES_PER_DAY = 10, DEVICE_NOTICES_KEY = "notice.devices.seq", TRUST_NOTICE_MS, DEVICES_CHANGED_MS, MACHINE_KEY = "identity.machine", DEFAULT_INVITE_TTL_MS, LIST_BYTES = 1572864;
30681
+ var DAY3 = 86400000, KEYCHAIN_LEFTOVERS_KEY = "keychain.stale", DEFAULT_TUNING, KEPT_MS, INPUT_TIMEOUT_MS, TASK_BYTES = 1048576, PREDECESSOR_KEY = "identity.predecessor", REPLACE_LABEL_KEY = "identity.replace.label", REPLACED_KEY = "identity.replaced", ERASURES_PER_DAY = 10, DEVICE_NOTICES_KEY = "notice.devices.seq", TRUST_NOTICE_MS, DEVICES_CHANGED_MS, MACHINE_KEY = "identity.machine", DEFAULT_INVITE_TTL_MS, LIST_BYTES = 1572864, NAMED_PERSONS = 1000;
29883
30682
  var init_daemon = __esm(() => {
29884
30683
  init_dist();
29885
30684
  init_dist2();
@@ -29908,6 +30707,7 @@ var init_daemon = __esm(() => {
29908
30707
  init_answer_preview();
29909
30708
  init_snapshot();
29910
30709
  init_notices();
30710
+ init_task_view();
29911
30711
  init_resolve();
29912
30712
  init_secrets();
29913
30713
  init_roster_sync();
@@ -29916,10 +30716,13 @@ var init_daemon = __esm(() => {
29916
30716
  init_tasks2();
29917
30717
  init_version();
29918
30718
  init_wake_bridge();
30719
+ init_action_tokens();
29919
30720
  init_grants();
29920
30721
  init_holds();
29921
30722
  init_cards2();
29922
30723
  init_requests2();
30724
+ init_reach();
30725
+ init_conv_caps();
29923
30726
  init_sentence();
29924
30727
  DEFAULT_TUNING = {
29925
30728
  presenceMinMs: 2000,
@@ -30035,6 +30838,15 @@ CREATE TABLE IF NOT EXISTS conv_request_decisions (
30035
30838
  decision TEXT NOT NULL,
30036
30839
  PRIMARY KEY (realm, id)
30037
30840
  );
30841
+ `,
30842
+ `
30843
+ CREATE TABLE IF NOT EXISTS dir_wake_dms (
30844
+ device TEXT NOT NULL,
30845
+ cid TEXT NOT NULL,
30846
+ exp INTEGER NOT NULL,
30847
+ PRIMARY KEY (device, cid)
30848
+ );
30849
+ CREATE INDEX IF NOT EXISTS dir_wake_dms_exp ON dir_wake_dms (exp);
30038
30850
  `
30039
30851
  ];
30040
30852
  RELAY_SCHEMA = 1 + MIGRATIONS2.length;
@@ -30350,7 +31162,7 @@ class RelayClient {
30350
31162
  return this.#dir("dir.ok", { t: "presence.watch", persons });
30351
31163
  }
30352
31164
  wakeArm(arm) {
30353
- return this.#dir("dir.ok", { t: "wake.arm", url: arm.url, ticket: arm.ticket, agents: arm.agents, awaiting: arm.awaiting });
31165
+ return this.#dir("dir.ok", { t: "wake.arm", url: arm.url, ticket: arm.ticket, agents: arm.agents, awaiting: arm.awaiting, ...arm.dms ? { dms: arm.dms } : {} });
30354
31166
  }
30355
31167
  wakeDisarm() {
30356
31168
  return this.#dir("dir.ok", { t: "wake.disarm" });
@@ -31308,6 +32120,7 @@ var HINTS = {
31308
32120
  login_denied: "the sign-in was denied at Jelly ID; run messaging login again if it was you, or check the account.",
31309
32121
  log_conflict: "retry: the device log changed at the same time.",
31310
32122
  conv_conflict: "retry: the conversation changed at the same time.",
32123
+ conflict: "your settings changed at the same time: run the command again to see them, then choose again.",
31311
32124
  stale_epoch: "retry: the conversation's keys changed at the same time.",
31312
32125
  device_limit: "remove one of this person's devices first.",
31313
32126
  admission_required: "bind this device from Jelly, or approve it from one of your Jelly devices.",
@@ -31951,6 +32764,72 @@ var grant2 = {
31951
32764
  function optional2(key, value) {
31952
32765
  return value === undefined ? {} : { [key]: value };
31953
32766
  }
32767
+ function choiceLabel(group, value) {
32768
+ return group.options.find((o) => o.value === value)?.label ?? value;
32769
+ }
32770
+ function printReach(ctx, snapshot) {
32771
+ ctx.out.lines(table(["QUESTION", "CHOICE", "MEANS"], snapshot.groups.map((g) => {
32772
+ const option = g.options.find((o) => o.value === (g.value ?? "ask"));
32773
+ return [g.label, g.value === null ? `${choiceLabel(g, "ask")} (the default)` : choiceLabel(g, g.value), option?.description ?? ""];
32774
+ }), ctx.out.style));
32775
+ for (const g of snapshot.groups) {
32776
+ if (g.lapsed !== undefined)
32777
+ ctx.out.line(ctx.out.style.yellow(`${g.label}: ${choiceLabel(g, g.lapsed)} lapsed with the device that chose it; choose again.`));
32778
+ }
32779
+ }
32780
+ var reach = {
32781
+ name: "reach",
32782
+ summary: "Show or set who can message you and how agents may ask yours: open, ask (me first) or never",
32783
+ usage: "[--message open|ask|never] [--ask open|ask|never] [--dry-run]",
32784
+ details: [
32785
+ "--message (Who can message you) is for people outside your org: open lets anyone message you; ask (the default)",
32786
+ "has them send a message request first; never refuses them and their requests. Your org can always message you.",
32787
+ "--ask (How agents may ask yours) is for your teammates' agents: open lets them ask your agents without asking you",
32788
+ "(guarded); ask (the default) holds each request for you (messaging requests); never refuses them. Agents of people",
32789
+ "outside your org never may. A grant you set for one person (messaging grant) wins over either choice.",
32790
+ "With no option it shows your choices. --dry-run prints what the change would do and changes nothing.",
32791
+ "A device of yours on an older Messaging release does not enforce these choices: update every device."
32792
+ ].join(`
32793
+ `),
32794
+ options: {
32795
+ message: { type: "string", value: "open|ask|never", description: "Who can message you (people outside your org)" },
32796
+ ask: { type: "string", value: "open|ask|never", description: "How your teammates' agents may ask yours" },
32797
+ "dry-run": { type: "boolean", description: "Print what it would do and change nothing" }
32798
+ },
32799
+ args: { min: 0, max: 0 },
32800
+ async run(ctx, _positionals, values) {
32801
+ const changes = {
32802
+ ...optional2("message", levelOption(values, "message", ReachValue, "open, ask or never")),
32803
+ ...optional2("ask", levelOption(values, "ask", ReachValue, "open, ask or never"))
32804
+ };
32805
+ const none = Object.keys(changes).length === 0;
32806
+ if (none && values["dry-run"] === true)
32807
+ throw new UsageError("--dry-run goes with --message or --ask");
32808
+ const client = await ctx.client();
32809
+ const shown = await client.call("reach.get", {});
32810
+ if (none) {
32811
+ if (ctx.json)
32812
+ return ctx.out.json(shown);
32813
+ printReach(ctx, shown);
32814
+ ctx.out.note("Change with: messaging reach --message open|ask|never --ask open|ask|never");
32815
+ return;
32816
+ }
32817
+ const params = { revision: shown.revision, changes };
32818
+ const { sentence } = await client.call("reach.set.preview", params);
32819
+ if (values["dry-run"] === true) {
32820
+ if (ctx.json)
32821
+ ctx.out.json({ sentence });
32822
+ else
32823
+ ctx.out.line(sentence);
32824
+ return;
32825
+ }
32826
+ const result = await client.call("reach.set", params);
32827
+ if (ctx.json)
32828
+ return ctx.out.json({ sentence, ...result });
32829
+ ctx.out.line(sentence);
32830
+ printReach(ctx, result);
32831
+ }
32832
+ };
31954
32833
  var requests2 = {
31955
32834
  name: "requests",
31956
32835
  summary: "List the requests waiting for you, or answer one: once, always, accept, decline or block",
@@ -31986,7 +32865,7 @@ var requests2 = {
31986
32865
  throw new UsageError("--all goes with `requests answer`");
31987
32866
  const { requests: held } = await client.call("request.list", {});
31988
32867
  if (ctx.json) {
31989
- ctx.out.json({ requests: held });
32868
+ ctx.out.json({ requests: held.map(({ actions: _actions, ...request }) => request) });
31990
32869
  return;
31991
32870
  }
31992
32871
  if (held.length === 0) {
@@ -32072,7 +32951,7 @@ var card = {
32072
32951
  ctx.out.line(sent ? "Asked. Their card comes when one of their devices answers." : "You asked them in the last day; their card comes when they answer.");
32073
32952
  }
32074
32953
  };
32075
- var CONSENT_COMMANDS = [grants, grant2, requests2, contact, card];
32954
+ var CONSENT_COMMANDS = [grants, grant2, reach, requests2, contact, card];
32076
32955
 
32077
32956
  // packages/daemon/dist/cli/commands/daemon.js
32078
32957
  init_dist();
@@ -32941,6 +33820,8 @@ function conversationEvent(event) {
32941
33820
  return { type: event.type };
32942
33821
  case "gap":
32943
33822
  return { type: event.type, pos: event.pos };
33823
+ case "task":
33824
+ return { type: event.type, task: event.task.task.id, role: event.task.role, state: event.task.task.status.state };
32944
33825
  }
32945
33826
  }
32946
33827
  async function syncConversations(client, ctx, position) {
@@ -34031,7 +34912,7 @@ var SECTIONS = [
34031
34912
  ["Daemon", ["daemon start", "daemon stop", "status"]],
34032
34913
  ["Team", ["team prepare", "team create", "team card", "team invite", "team join", "team pair", "team add", "team claim", "team pair-cancel", "team members", "team remove", "team role", "team revoke-invite"]],
34033
34914
  ["Identity", ["identity show", "identity migrate", "identity rotate-enc", "identity replace", "erase"]],
34034
- ["People", ["login", "people", "devices", "grants", "grant", "requests", "contact", "card", "notices"]],
34915
+ ["People", ["login", "people", "devices", "grants", "grant", "reach", "requests", "contact", "card", "notices"]],
34035
34916
  ["Messages", ["who", "send", "inbox", "read"]],
34036
34917
  ["Chats", ["chat list", "chat read", "chat post", "chat search"]],
34037
34918
  ["Tasks", ["tasks", "task", "cancel"]],