@canonmsg/agent-sdk 13.2.1 → 13.3.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.
@@ -5,8 +5,9 @@ import {
5
5
  createHttpEndpointTransport,
6
6
  createNodeEndpointStore,
7
7
  validateEndpointContentBoundary
8
- } from "./chunk-BPSFHEAA.js";
8
+ } from "./chunk-HFT75BNB.js";
9
9
  import {
10
+ AGENT_HOME_ELSEWHERE,
10
11
  CANON_ENDPOINT_PROTOCOL,
11
12
  CANON_VERB_WIRE_SCHEMA_VERSION,
12
13
  ENDPOINT_ACTION_PATTERN,
@@ -15,6 +16,7 @@ import {
15
16
  MLS_DEV_DISCLOSED_PROCESSING_FIELDS,
16
17
  MLS_DEV_GROUP_RECEIPT_CLOCK_SKEW,
17
18
  MLS_DEV_INTERACTION_LIMITS,
19
+ MLS_DEV_KEY_PACKAGE_CLAIM_LEASE_MS,
18
20
  MLS_DEV_REPLY_QUOTE_MAX_CHARACTERS,
19
21
  MLS_DEV_TEXT_PROTOCOL_V3,
20
22
  MLS_DEV_UNSUPPORTED_CONTENT_ACTIONS,
@@ -22,6 +24,7 @@ import {
22
24
  RUNTIME_CARD_FIELD_TYPES,
23
25
  RUNTIME_CARD_LINE_ITEM_COLUMN_TYPES,
24
26
  __name,
27
+ agentHomeElsewhereMessage,
25
28
  assertMlsDevInteractionPayloadBinding,
26
29
  canonicalEndpointJson,
27
30
  createMlsDevCredentialSigningInput,
@@ -36,6 +39,7 @@ import {
36
39
  getCanonEnvironmentContract,
37
40
  isCanonMediaOrigin,
38
41
  isCanonUploadUrl,
42
+ isConnectorRelayInstallation,
39
43
  isMlsDevInteractionAnswerAllowed,
40
44
  isMlsDevTextProtocol,
41
45
  isMlsEnvironmentId,
@@ -104,7 +108,7 @@ import {
104
108
  readMembershipChangeMetadata,
105
109
  resolveAllowedUploadMimeType,
106
110
  sanitizeDownloadFileName
107
- } from "./chunk-VIS5WXNH.js";
111
+ } from "./chunk-CUPSYLBV.js";
108
112
 
109
113
  // src/media.ts
110
114
  import { createWriteStream as createWriteStream2 } from "node:fs";
@@ -1315,19 +1319,22 @@ async function recordMlsInteractionProjection(tx, event, timestamp) {
1315
1319
  await tx.put(key2, { event: copyJson(event), state: "settled", reason: "mls_interaction", createdAt: timestamp, updatedAt: timestamp });
1316
1320
  }
1317
1321
  __name(recordMlsInteractionProjection, "recordMlsInteractionProjection");
1322
+ var WORK_FLOOR_KEY = "recovery/work-floor";
1323
+ var beforeWorkFloor = /* @__PURE__ */ __name((floor, acceptedAt) => !!floor && Date.parse(acceptedAt) < Date.parse(floor.cutoverAt), "beforeWorkFloor");
1318
1324
  async function recordVerifiedMlsInboundEvent(tx, event, feedOrdinal, timestamp, own = false) {
1319
1325
  if (!event.conversationId || !Number.isSafeInteger(feedOrdinal) || feedOrdinal < 1) throw new Error("Invalid verified MLS input");
1320
1326
  const key2 = `inbound/${event.id}`, existing = await tx.get(key2);
1321
1327
  if (existing && existing.reason !== "mls_plaintext_withheld") return false;
1328
+ const history = !own && beforeWorkFloor(await tx.get(WORK_FLOOR_KEY), timestamp);
1322
1329
  const record6 = {
1323
1330
  event: copyJson(event),
1324
- state: own ? "settled" : "observed",
1325
- ...own ? { reason: "own_mls_message" } : { queueKey: `inbound-ready/${encodeURIComponent(event.conversationId)}/mls/${String(feedOrdinal).padStart(16, "0")}/${encodeURIComponent(event.id)}` },
1331
+ state: own || history ? "settled" : "observed",
1332
+ ...own ? { reason: "own_mls_message" } : history ? { reason: "before_take_over" } : { queueKey: `inbound-ready/${encodeURIComponent(event.conversationId)}/mls/${String(feedOrdinal).padStart(16, "0")}/${encodeURIComponent(event.id)}` },
1326
1333
  createdAt: timestamp,
1327
1334
  updatedAt: timestamp
1328
1335
  };
1329
1336
  await tx.put(key2, record6);
1330
- if (!own) await enqueueInbound(tx, record6);
1337
+ if (!own && !history) await enqueueInbound(tx, record6);
1331
1338
  return true;
1332
1339
  }
1333
1340
  __name(recordVerifiedMlsInboundEvent, "recordVerifiedMlsInboundEvent");
@@ -2261,7 +2268,7 @@ function createEndpointMediaPipeline(options) {
2261
2268
  const progress = /* @__PURE__ */ __name((phase) => resumeOptions?.onProgress?.({ phase, totalBytes, uploadedBytes: offsets.reduce((sum, offset) => sum + offset, 0) }), "progress");
2262
2269
  const publicationOperationId = /* @__PURE__ */ __name(() => `message:${flow.conversationId}:${flow.messageId}${flow.publicationGeneration ? `:g${flow.publicationGeneration}` : ""}`, "publicationOperationId");
2263
2270
  const publish = /* @__PURE__ */ __name(async (reconciling = false) => {
2264
- if (!reconciling && flow.requiresPublicationApproval && !await publicationGuards.get(id2)?.check()) {
2271
+ if (!reconciling && flow.requiresPublicationApproval && !await publicationGuards.get(id2)?.check({ automaticResend: false })) {
2265
2272
  throw new EndpointError("Attachment publication requires a current audience approval", { code: "MEDIA_PUBLICATION_APPROVAL_REQUIRED", retryable: true });
2266
2273
  }
2267
2274
  flow.status = "sending";
@@ -4969,6 +4976,12 @@ var appIntentKey = /* @__PURE__ */ __name((operationId) => `mls-dev-group/applic
4969
4976
  var appOperationKey = /* @__PURE__ */ __name((deviceId, operationId) => `mls-dev-group/application-operation/${encodeURIComponent(deviceId)}/${encodeURIComponent(operationId)}`, "appOperationKey");
4970
4977
  var acceptedReceiptKey = /* @__PURE__ */ __name((operationId) => `mls-dev-group/accepted-receipt/${encodeURIComponent(operationId)}`, "acceptedReceiptKey");
4971
4978
  var appMessageKey = /* @__PURE__ */ __name((cid, messageId) => `mls-dev-group/application-id/${encodeURIComponent(cid)}/${encodeURIComponent(messageId)}`, "appMessageKey");
4979
+ var BEHIND_CODES = ["MLS_DEV_GROUP_HEAD_CHANGED", "MLS_DEV_GROUP_ADMISSION_CHANGED"];
4980
+ var MLS_DEV_RESEND_MAX = 3;
4981
+ var readersOf = /* @__PURE__ */ __name((roster) => [...new Set(roster.flatMap(({ device: { principal, installationId } }) => [
4982
+ `${principal.type}:${principal.id}`,
4983
+ ...isConnectorRelayInstallation(installationId) ? [`relay:${installationId}`] : []
4984
+ ]))].sort(), "readersOf");
4972
4985
  var same6 = /* @__PURE__ */ __name((a, b) => canonicalEndpointJson(a) === canonicalEndpointJson(b), "same");
4973
4986
  async function roomRemoval(tx, conversationId) {
4974
4987
  const room = await tx.get(`conversation/${conversationId}`);
@@ -4996,6 +5009,10 @@ function fail6(code = "MLS_DEV_GROUP_STATE_INVALID", retryable = false) {
4996
5009
  throw new EndpointError(code, { code, retryable });
4997
5010
  }
4998
5011
  __name(fail6, "fail");
5012
+ function admissionOf(operation) {
5013
+ return operation.request?.body.value?.preconditions?.admissionId;
5014
+ }
5015
+ __name(admissionOf, "admissionOf");
4999
5016
  function id(value) {
5000
5017
  if (typeof value !== "string" || !ENDPOINT_ID_PATTERN.test(value) || value === "." || value === "..") fail6("INVALID_MLS_DEV_GROUP_INPUT");
5001
5018
  return value;
@@ -5739,9 +5756,10 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
5739
5756
  const room = await tx.get(`conversation/${conversationId}`);
5740
5757
  const group = envelope((await readPreparationSnapshot(tx, scope4(conversationId))).protocolState, init.binding, conversationId);
5741
5758
  const missed = await tx.get(missedEarlierKey(conversationId)) === true ? { missedEarlier: true } : {};
5759
+ const outside = !!room && !!room.mls && (!group || group.removed);
5760
+ const noted = { ...missed, ...outside ? { outside: true } : {} };
5742
5761
  if (room?.mlsAdmissionHold !== true) {
5743
- const joining = !!room && !!room.mls && (!group || group.removed);
5744
- return Object.freeze({ held: joining, waitingPrincipalIds: Object.freeze(joining ? [init.identity.principalId] : []), ...missed });
5762
+ return Object.freeze({ held: outside, waitingPrincipalIds: Object.freeze(outside ? [init.identity.principalId] : []), ...noted });
5745
5763
  }
5746
5764
  const accounts = mlsRoomAccounts(room, init.binding.environmentId);
5747
5765
  const admitted = new Set((group && !group.removed ? group.roster : []).map((entry) => mlsAccountOf(accounts, entry.device.principal)));
@@ -5754,7 +5772,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
5754
5772
  joinedAtMs: Number(room.mlsMemberJoinedAtMs),
5755
5773
  nowMs: context.clock()
5756
5774
  });
5757
- return Object.freeze({ held: waiting.length > 0, waitingPrincipalIds: Object.freeze(waiting), ...missed });
5775
+ return Object.freeze({ held: waiting.length > 0, waitingPrincipalIds: Object.freeze(waiting), ...noted });
5758
5776
  });
5759
5777
  }
5760
5778
  __name(admissionHold, "admissionHold");
@@ -5762,7 +5780,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
5762
5780
  if ((await admissionHold(conversationId)).held) fail6("MLS_DEV_GROUP_ADMISSION_PENDING", true);
5763
5781
  }
5764
5782
  __name(assertNotHeld, "assertNotHeld");
5765
- async function sendText(input, operationId, guardId) {
5783
+ async function sendText(input, operationId, guardId, resend) {
5766
5784
  const cid = input.conversationId, init = await initialized();
5767
5785
  const authoredHash = await bridge.hash(canonicalEndpointJson(guardId ? { ...input, guardId } : input));
5768
5786
  async function existing(tx) {
@@ -5774,10 +5792,11 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
5774
5792
  __name(existing, "existing");
5775
5793
  const previous = await store.transaction(existing);
5776
5794
  if (previous) return previous;
5777
- if (!await caughtUp(cid)) await refresh(cid);
5795
+ if (resend || !await caughtUp(cid)) await refresh(cid);
5778
5796
  await assertNotHeld(cid);
5779
5797
  const messageId = input.messageId ?? id(context.ids());
5780
5798
  let captured, replyTo;
5799
+ let replaced, written;
5781
5800
  const operation = await prepareAndCommitContent({
5782
5801
  store,
5783
5802
  scope: scope4(cid),
@@ -5785,7 +5804,9 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
5785
5804
  existing: /* @__PURE__ */ __name(async (tx, state) => {
5786
5805
  const previous2 = await existing(tx);
5787
5806
  if (previous2) return previous2;
5788
- if (await tx.get(appMessageKey(cid, messageId)) !== void 0 || await tx.get(mlsDevTextMessageKey(cid, messageId)) !== void 0) fail6("MLS_DEV_APPLICATION_ID_CONFLICT");
5807
+ const mapped = await tx.get(appMessageKey(cid, messageId)), local = await tx.get(mlsDevTextMessageKey(cid, messageId));
5808
+ if (resend ? mapped !== resend.previous.operationId || local?.message.operationId !== resend.previous.operationId || local.feedOrdinal !== null || local.deleted : mapped !== void 0 || local !== void 0) fail6("MLS_DEV_APPLICATION_ID_CONFLICT");
5809
+ written = local?.createdAtMs;
5789
5810
  const current = await capture(tx, state, init, cid), group = current.envelope;
5790
5811
  if (!isMlsDevTextProtocol(current.protocol)) fail6("MLS_DEV_TEXT_PROTOCOL_UNSUPPORTED");
5791
5812
  if ((input.replyTo || input.mentions || input.item) && current.protocol !== MLS_DEV_TEXT_PROTOCOL_V3) fail6("MLS_DEV_TEXT_ONLY");
@@ -5794,7 +5815,8 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
5794
5815
  if (!Array.isArray(members) || input.mentions.some((mention) => !members.includes(mention))) fail6("MLS_DEV_MENTION_NOT_MEMBER");
5795
5816
  }
5796
5817
  if (input.item && !canonItemReference(input.item, cid, init.binding.environmentId)) fail6("MLS_DEV_ITEM_INVALID");
5797
- if (input.replyTo) {
5818
+ if (resend?.replyTo) replyTo = resend.replyTo;
5819
+ else if (input.replyTo) {
5798
5820
  const quote = mlsDevReplyQuote(await tx.get(mlsDevTextMessageKey(cid, input.replyTo)), MLS_DEV_REPLY_QUOTE_MAX_CHARACTERS);
5799
5821
  replyTo = { messageId: input.replyTo, quote: quote ?? fail6("MLS_DEV_REPLY_UNAVAILABLE") };
5800
5822
  }
@@ -5802,6 +5824,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
5802
5824
  await assertRoomOpen(tx, cid, group.roster);
5803
5825
  if (group.pending) fail6("MLS_DEV_GROUP_PENDING_CONTROL", true);
5804
5826
  if (!same6(group.head, current.authority?.marker?.head)) fail6("MLS_DEV_GROUP_HEAD_CHANGED", true);
5827
+ if (resend?.readers && (readersOf(group.roster).some((reader) => !resend.readers.includes(reader)) || current.authority?.admissionId !== admissionOf(resend.previous))) fail6("MLS_DEV_RESEND_AUDIENCE_CHANGED");
5805
5828
  captured = current;
5806
5829
  return void 0;
5807
5830
  }, "existing"),
@@ -5853,8 +5876,24 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
5853
5876
  commit: /* @__PURE__ */ __name(async (tx, prepared, state) => {
5854
5877
  const current = await capture(tx, state, init, cid);
5855
5878
  if (current.fingerprint !== prepared.fingerprint) fail6("MLS_DEV_GROUP_DEPENDENCY_CHANGED", true);
5856
- const createdAtMs = context.clock(), timestamp = new Date(createdAtMs).toISOString();
5857
- await tx.put(appIntentKey(operationId), { version: 1, authoredHash, envelope: prepared.wire, envelopeHash: prepared.envelopeHash });
5879
+ const timestamp = new Date(context.clock()).toISOString();
5880
+ let queueOrder;
5881
+ if (resend) {
5882
+ const before = await tx.get(`operation/${resend.previous.operationId}`);
5883
+ const settled = before && resend.receipt ? await bridge.settle(tx, before, resend.receipt, false) : before;
5884
+ if (settled?.status !== "rejected" || settled.resentAs) fail6("MLS_DEV_RESEND_UNAVAILABLE");
5885
+ if (resend.receipt) queueOrder = settled.queueOrder;
5886
+ replaced = { ...settled, resentAs: operationId, updatedAt: timestamp };
5887
+ await tx.put(`operation/${settled.operationId}`, replaced);
5888
+ }
5889
+ await tx.put(appIntentKey(operationId), {
5890
+ version: 1,
5891
+ authoredHash,
5892
+ envelope: prepared.wire,
5893
+ envelopeHash: prepared.envelopeHash,
5894
+ readers: resend?.readers ?? readersOf(current.envelope.roster),
5895
+ resends: resend?.resends ?? 0
5896
+ });
5858
5897
  await tx.put(appMessageKey(cid, messageId), operationId);
5859
5898
  await putMlsDevTextMessage(tx, {
5860
5899
  version: 1,
@@ -5862,7 +5901,7 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
5862
5901
  senderId: init.binding.principal.id,
5863
5902
  senderType: principal.type,
5864
5903
  status: "queued",
5865
- createdAtMs,
5904
+ createdAtMs: written ?? context.clock(),
5866
5905
  feedOrdinal: null
5867
5906
  });
5868
5907
  const media = prepared.text.kind === "item" ? { mediaUploadIds: [prepared.text.item.uploadId] } : {};
@@ -5875,7 +5914,9 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
5875
5914
  messageId,
5876
5915
  fingerprint: prepared.envelopeHash,
5877
5916
  ordering: "conversation",
5917
+ ...queueOrder === void 0 ? {} : { queueOrder },
5878
5918
  ...guardId ? { guardId } : {},
5919
+ ...resend?.receipt ? { automaticResend: true } : {},
5879
5920
  sensitive: false,
5880
5921
  status: "queued",
5881
5922
  attempts: 0,
@@ -5893,10 +5934,45 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
5893
5934
  });
5894
5935
  }, "commit")
5895
5936
  });
5937
+ if (replaced) await bridge.afterReceipt(replaced);
5896
5938
  bridge.submitted(operation);
5897
5939
  return operation;
5898
5940
  }
5899
5941
  __name(sendText, "sendText");
5942
+ async function resendText(operation, receipt) {
5943
+ const cid = operation.conversationId, messageId = operation.messageId;
5944
+ if (operation.action !== "mls_application" || !cid || !messageId) return void 0;
5945
+ if (receipt && (receipt.status !== "rejected" || !BEHIND_CODES.includes(receipt.error?.code ?? ""))) return void 0;
5946
+ const { intent: intent2, local } = await store.transaction(async (tx) => ({
5947
+ intent: await tx.get(appIntentKey(operation.operationId)),
5948
+ local: await tx.get(mlsDevTextMessageKey(cid, messageId))
5949
+ }));
5950
+ const message = local?.message;
5951
+ if (!intent2 || !local || local.deleted || local.feedOrdinal !== null || message?.operationId !== operation.operationId || message.kind !== "text" && message.kind !== "item") return void 0;
5952
+ if (receipt && (!intent2.readers || (intent2.resends ?? 0) >= MLS_DEV_RESEND_MAX)) return void 0;
5953
+ const { replyTo, mentions, ...item } = message.kind === "item" ? message.item : { replyTo: message.replyTo, mentions: message.mentions };
5954
+ const replyAuthority = operation.request?.body.value.replyAuthority;
5955
+ const input = {
5956
+ conversationId: cid,
5957
+ messageId,
5958
+ ...message.kind === "item" ? { item } : { text: message.text },
5959
+ ...replyTo ? { replyTo: replyTo.messageId } : {},
5960
+ ...mentions?.length ? { mentions: [...mentions] } : {},
5961
+ ...replyAuthority ? { replyAuthority } : {}
5962
+ };
5963
+ const successor = id(await bridge.hash(canonicalEndpointJson(["canon.mls.dev-resend.v1", operation.operationId])));
5964
+ try {
5965
+ return await sendText(input, successor, operation.guardId, {
5966
+ previous: operation,
5967
+ ...replyTo ? { replyTo } : {},
5968
+ ...receipt ? { receipt, readers: intent2.readers, resends: (intent2.resends ?? 0) + 1 } : { resends: 0 }
5969
+ });
5970
+ } catch (error) {
5971
+ if (!receipt || !(error instanceof EndpointError) || error.retryable || ["ENDPOINT_CLOSED", "PREPARATION_INSTALLATION_FENCED"].includes(error.code)) throw error;
5972
+ return void 0;
5973
+ }
5974
+ }
5975
+ __name(resendText, "resendText");
5900
5976
  async function holdText(input, operationId) {
5901
5977
  const cid = input.conversationId;
5902
5978
  if (input.item || input.replyAuthority || typeof input.text !== "string") return sendText(input, operationId);
@@ -6743,6 +6819,11 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
6743
6819
  const input = textInput(value), selectedId = id(operationId), selectedGuard = guardId === void 0 ? void 0 : id(guardId);
6744
6820
  return run(() => serializeInboundPreparation(store, input.conversationId, () => sendText(input, selectedId, selectedGuard)));
6745
6821
  },
6822
+ /** A person's Retry of a text or item Canon refused: encrypted again for the room as it is now (`resendText`). */
6823
+ resendText(operation) {
6824
+ const cid = id(operation.conversationId);
6825
+ return run(() => serializeInboundPreparation(store, cid, () => resendText(operation)));
6826
+ },
6746
6827
  /** `sendText` for a person's composer: in a held room the text is kept, not refused (undefined), until `sendHeldTexts`. */
6747
6828
  holdText(value, operationId) {
6748
6829
  const input = textInput(value), selectedId = id(operationId);
@@ -6790,9 +6871,9 @@ function createMlsDevGroups(options, context, keyPackages, bridge) {
6790
6871
  return receipt;
6791
6872
  });
6792
6873
  },
6793
- async settleReceipt(operation, receipt) {
6874
+ async settleReceipt(operation, receipt, options2) {
6794
6875
  if (!["mls_group_control", "mls_application"].includes(operation.action)) return false;
6795
- if (operation.action === "mls_application" && receipt.status !== "succeeded") return false;
6876
+ if (operation.action === "mls_application" && receipt.status !== "succeeded") return !options2?.reconcileOnly && receipt.status === "rejected" && BEHIND_CODES.includes(receipt.error?.code ?? "") && !!operation.conversationId && await run(() => serializeInboundPreparation(store, operation.conversationId, () => resendText(operation, receipt))) !== void 0;
6796
6877
  if (receipt.status === "succeeded" && isMlsDevTextProtocol(await bridge.resolveProtocol?.(operation.conversationId ?? fail6()))) {
6797
6878
  const cid = operation.conversationId;
6798
6879
  const accepted2 = operation.action === "mls_application" ? parseMlsDevApplicationReceipt(receipt.result) : parseMlsDevGroupAcceptedControl(receipt.result);
@@ -6878,6 +6959,8 @@ function terminal(status) {
6878
6959
  return status === "succeeded" || status === "rejected";
6879
6960
  }
6880
6961
  __name(terminal, "terminal");
6962
+ var TAKE_OVER_OPERATION = "initialize-endpoint:take-over:";
6963
+ var TAKE_OVER_DISCARDED = "A take-over is not sent again by itself; start the runner with --take-over again";
6881
6964
  function safeError(error) {
6882
6965
  return error instanceof EndpointError ? error : new EndpointError("Canon delivery is deferred until the connection recovers");
6883
6966
  }
@@ -6969,6 +7052,19 @@ function createEndpoint(options) {
6969
7052
  return identity;
6970
7053
  });
6971
7054
  void initialization.catch(() => void 0);
7055
+ const opened = initialization.then(() => store.transaction(async (tx) => {
7056
+ for (let after; ; ) {
7057
+ const page = await tx.scan("outbox/", { ...after ? { after } : {}, limit: 100 });
7058
+ for (const entry of page) {
7059
+ if (!entry.value.operationId.startsWith(TAKE_OVER_OPERATION)) continue;
7060
+ const operation = await tx.get(key2(entry.value.operationId));
7061
+ if (operation && !terminal(operation.status)) await discardOperation(tx, operation, TAKE_OVER_DISCARDED);
7062
+ }
7063
+ if (page.length < 100) return;
7064
+ after = page.at(-1).key;
7065
+ }
7066
+ }));
7067
+ void opened.catch(() => void 0);
6972
7068
  let mlsPrincipal;
6973
7069
  function readMlsPrincipalId() {
6974
7070
  mlsPrincipal ??= (async () => {
@@ -7180,6 +7276,7 @@ function createEndpoint(options) {
7180
7276
  const admissionTimers = /* @__PURE__ */ new Map();
7181
7277
  const ADMISSION_IDLE_MS = 10 * 6e4, CLAIM_JOURNAL_KEEP_MS = 60 * 6e4;
7182
7278
  const admissionUnfinished = /* @__PURE__ */ new Set();
7279
+ const announcementLooks = /* @__PURE__ */ new Map();
7183
7280
  const claimAdds = /* @__PURE__ */ new Map(), CLAIM_ADDS_MAX = 3;
7184
7281
  const admissionRetry = { initial: options.mlsAdmissionRetryMs?.initial ?? 3e4, maximum: options.mlsAdmissionRetryMs?.maximum ?? 12e4 };
7185
7282
  function scheduleAdmission(conversationId, delayMs, discover = false) {
@@ -7206,13 +7303,24 @@ function createEndpoint(options) {
7206
7303
  const inRoster = new Set(group.roster.map((entry) => mlsAccountOf(accounts, entry.device.principal)));
7207
7304
  const invitee = local?.mlsInviteeId;
7208
7305
  const missing = [...local?.memberIds ?? [], ...typeof invitee === "string" ? [invitee] : []].some((memberId) => !inRoster.has(memberId)) || admissionUnfinished.has(conversationId);
7209
- if (!missing) admissionDelays.delete(conversationId);
7210
- const delay = admissionDelays.get(conversationId) ?? admissionRetry.initial;
7211
7306
  const announced = local?.mlsDeviceAnnouncedAtMs;
7212
- const due = announced !== announcementsSeen.get(conversationId) ? -Infinity : (admissionRuns.get(conversationId) ?? -Infinity) + (missing ? delay : ADMISSION_IDLE_MS);
7213
- if (clock() < due) return missing ? scheduleAdmission(conversationId, due - clock()) : void 0;
7307
+ const fresh = announced !== announcementsSeen.get(conversationId);
7308
+ if ((announcementLooks.get(conversationId)?.roster ?? Infinity) < group.roster.length) announcementLooks.delete(conversationId);
7309
+ const look = announcementLooks.get(conversationId), retrying = missing || !!look;
7310
+ if (!retrying) admissionDelays.delete(conversationId);
7311
+ const delay = admissionDelays.get(conversationId) ?? admissionRetry.initial;
7312
+ let due = fresh ? -Infinity : (admissionRuns.get(conversationId) ?? -Infinity) + (retrying ? delay : ADMISSION_IDLE_MS);
7313
+ if (look && !missing) due = Math.min(due, look.untilMs);
7314
+ if (clock() < due) return retrying ? scheduleAdmission(conversationId, due - clock()) : void 0;
7214
7315
  admitting.add(conversationId);
7316
+ const watched = announcementsSeen.has(conversationId);
7215
7317
  announcementsSeen.set(conversationId, announced);
7318
+ if (fresh && typeof announced === "number" && (watched || clock() < announced + MLS_DEV_KEY_PACKAGE_CLAIM_LEASE_MS)) {
7319
+ announcementLooks.set(conversationId, {
7320
+ untilMs: clock() + MLS_DEV_KEY_PACKAGE_CLAIM_LEASE_MS + admissionRetry.initial,
7321
+ roster: group.roster.length
7322
+ });
7323
+ }
7216
7324
  try {
7217
7325
  admissionRuns.set(conversationId, clock());
7218
7326
  await mlsDevKeyPackages.pruneClaims(clock() - CLAIM_JOURNAL_KEEP_MS);
@@ -7270,13 +7378,16 @@ function createEndpoint(options) {
7270
7378
  admissionRuns.delete(conversationId);
7271
7379
  admissionDelays.delete(conversationId);
7272
7380
  admissionUnfinished.delete(conversationId);
7381
+ announcementLooks.delete(conversationId);
7273
7382
  return scheduleAdmission(conversationId, admissionRetry.initial);
7274
7383
  }
7275
7384
  if (unfinished) admissionUnfinished.add(conversationId);
7276
7385
  else admissionUnfinished.delete(conversationId);
7277
- if (missing || unfinished) {
7386
+ if ((announcementLooks.get(conversationId)?.untilMs ?? -Infinity) <= clock()) announcementLooks.delete(conversationId);
7387
+ const looking = !missing && !unfinished ? announcementLooks.get(conversationId) : void 0;
7388
+ if (missing || unfinished || looking) {
7278
7389
  admissionDelays.set(conversationId, Math.min(delay * 2, admissionRetry.maximum));
7279
- scheduleAdmission(conversationId, delay);
7390
+ scheduleAdmission(conversationId, looking ? Math.min(delay, Math.max(0, looking.untilMs - clock())) : delay);
7280
7391
  }
7281
7392
  } finally {
7282
7393
  admitting.delete(conversationId);
@@ -7773,6 +7884,24 @@ function createEndpoint(options) {
7773
7884
  else await enqueueInbound(tx, next);
7774
7885
  }
7775
7886
  __name(writeInboundState, "writeInboundState");
7887
+ async function discardOperation(tx, operation, message) {
7888
+ const status = operation.attempted ? "uncertain" : "rejected";
7889
+ const next = { ...operation, status, updatedAt: now(), receipt: {
7890
+ ...deferredReceipt(operation),
7891
+ status,
7892
+ updatedAt: now(),
7893
+ error: { code: "OPERATION_DISCARDED", message, retryable: false }
7894
+ } };
7895
+ delete next.request;
7896
+ delete next.preparedContent;
7897
+ delete next.lease;
7898
+ delete next.nextAttemptAt;
7899
+ await tx.put(key2(operation.operationId), next);
7900
+ await settleMlsDevTextOperation(tx, next);
7901
+ await removeOperationOutbox(tx, operation);
7902
+ if (!operation.attempted) await settleOperationOrder(tx, operation);
7903
+ }
7904
+ __name(discardOperation, "discardOperation");
7776
7905
  function deferredReceipt(operation, error) {
7777
7906
  return operation.receipt ?? {
7778
7907
  protocol: CANON_ENDPOINT_PROTOCOL,
@@ -7894,12 +8023,18 @@ function createEndpoint(options) {
7894
8023
  if (mlsDevGroups && typeof made === "string" && ENDPOINT_ID_PATTERN.test(made)) void refreshMlsRoom(made, true).catch(() => void 0);
7895
8024
  }
7896
8025
  __name(afterReceipt, "afterReceipt");
7897
- async function persistReceipt(operation, receipt) {
7898
- if (await mlsDevGroups?.settleReceipt(operation, receipt)) return;
8026
+ async function persistReceipt(operation, receipt, reconcileOnly = false) {
8027
+ if (await mlsDevGroups?.settleReceipt(operation, receipt, { reconcileOnly })) return;
7899
8028
  const updated = await store.transaction((tx) => commitReceipt(tx, operation, receipt));
7900
8029
  await afterReceipt(updated);
7901
8030
  }
7902
8031
  __name(persistReceipt, "persistReceipt");
8032
+ async function answerOf(operationId, receipt, reconcileOnly = false) {
8033
+ if (receipt.action !== "mls_application" || receipt.status !== "rejected") return receipt;
8034
+ const resentAs = (await store.transaction((tx) => tx.get(key2(operationId))))?.resentAs;
8035
+ return resentAs ? endpoint.retry(resentAs, { reconcileOnly }) : receipt;
8036
+ }
8037
+ __name(answerOf, "answerOf");
7903
8038
  async function dispatch(operationId, reconcileOnly = false) {
7904
8039
  assertOpen();
7905
8040
  await initialization;
@@ -7939,12 +8074,17 @@ function createEndpoint(options) {
7939
8074
  await tx.put(key2(operationId), next);
7940
8075
  return { operation: next, acquired: true };
7941
8076
  });
8077
+ if (operation.resentAs) return endpoint.retry(operation.resentAs, { reconcileOnly });
7942
8078
  if (terminal(operation.status) && operation.receipt) {
7943
8079
  if ((operation.receipt.resultReference || operation.resultEvicted) && operation.status === "succeeded") {
7944
8080
  const hydrated = await transport.getOperation(operationId, operation.installationId, controller.signal);
7945
8081
  if (!hydrated) throw new EndpointError("The private operation result is no longer available", { status: 410, code: "RESULT_EXPIRED", retryable: false, operationId });
7946
8082
  return hydrated;
7947
8083
  }
8084
+ if (!reconcileOnly && operation.action === "mls_application" && operation.status === "rejected") {
8085
+ await mlsDevGroups?.settleReceipt(operation, operation.receipt);
8086
+ return answerOf(operationId, operation.receipt);
8087
+ }
7948
8088
  return operation.receipt;
7949
8089
  }
7950
8090
  if (!acquired) {
@@ -7986,8 +8126,8 @@ function createEndpoint(options) {
7986
8126
  });
7987
8127
  return receipt2;
7988
8128
  }
7989
- await persistReceipt(operation, receipt2);
7990
- return receipt2;
8129
+ await persistReceipt(operation, receipt2, reconcileOnly);
8130
+ return answerOf(operationId, receipt2, reconcileOnly);
7991
8131
  }
7992
8132
  if (operation.attempted) {
7993
8133
  resolvingReceipt = true;
@@ -7995,7 +8135,7 @@ function createEndpoint(options) {
7995
8135
  resolvingReceipt = false;
7996
8136
  if (receipt2 && (receipt2.status !== "pending" || !operation.request && !sensitiveRequests.has(operationId))) {
7997
8137
  await persistReceipt(operation, receipt2);
7998
- return receipt2;
8138
+ return answerOf(operationId, receipt2, reconcileOnly);
7999
8139
  }
8000
8140
  }
8001
8141
  if (clock() - Date.parse(operation.createdAt) > retryHorizonMs) {
@@ -8020,7 +8160,7 @@ function createEndpoint(options) {
8020
8160
  checkingDeliveryGuard = true;
8021
8161
  const guard = deliveryGuards.get(operation.guardId);
8022
8162
  if (!guard) throw new EndpointError("This publication requires its runtime to recheck the current audience", { code: "DELIVERY_GUARD_REQUIRED", retryable: true });
8023
- if (!await guard()) throw new EndpointError("Publication is withheld by the runtime policy", { code: "DELIVERY_GUARD_WITHHELD", retryable: true });
8163
+ if (!await guard({ automaticResend: operation.automaticResend === true })) throw new EndpointError("Publication is withheld by the runtime policy", { code: "DELIVERY_GUARD_WITHHELD", retryable: true });
8024
8164
  assertOpen();
8025
8165
  checkingDeliveryGuard = false;
8026
8166
  }
@@ -8055,7 +8195,7 @@ function createEndpoint(options) {
8055
8195
  });
8056
8196
  const receipt = await transport.execute(request, controller.signal);
8057
8197
  await persistReceipt(operation, receipt);
8058
- return receipt;
8198
+ return answerOf(operationId, receipt, reconcileOnly);
8059
8199
  } catch (error) {
8060
8200
  const failure = safeError(error);
8061
8201
  if (reconcileOnly && failure.retryable) {
@@ -8320,9 +8460,20 @@ function createEndpoint(options) {
8320
8460
  running.set(operationId, work);
8321
8461
  return work;
8322
8462
  },
8323
- async drain(drainOptions = {}) {
8463
+ async resend(operationId) {
8324
8464
  assertOpen();
8325
8465
  await initialization;
8466
+ let operation = await endpoint.getOperation(operationId);
8467
+ while (operation?.resentAs) operation = await endpoint.getOperation(operation.resentAs);
8468
+ if (!operation) throw new EndpointError("Unknown endpoint operation", { status: 404, code: "OPERATION_NOT_FOUND", retryable: false, operationId });
8469
+ if (operation.status !== "rejected") return operation;
8470
+ const resent = operation.action === "mls_application" && mlsDevGroups ? await mlsDevGroups.resendText(operation) : void 0;
8471
+ if (!resent) throw new EndpointError("This message can no longer be sent again", { code: "OPERATION_NOT_RESENDABLE", retryable: false, operationId });
8472
+ return resent;
8473
+ },
8474
+ async drain(drainOptions = {}) {
8475
+ assertOpen();
8476
+ await opened;
8326
8477
  await compactExpiredPayloads(store, clock());
8327
8478
  await sendDeferredOpeners(void 0, true).catch(() => void 0);
8328
8479
  let dispatched = 0;
@@ -8371,7 +8522,8 @@ function createEndpoint(options) {
8371
8522
  await initialization;
8372
8523
  return store.transaction((tx) => tx.get(key2(operationId)));
8373
8524
  },
8374
- waitForOperation(operationId, waitOptions = {}) {
8525
+ waitForOperation(waited, waitOptions = {}) {
8526
+ let operationId = waited;
8375
8527
  return new Promise((resolve3, reject) => {
8376
8528
  let finished = false;
8377
8529
  let checking = false;
@@ -8397,7 +8549,11 @@ function createEndpoint(options) {
8397
8549
  if (finished || checking) return;
8398
8550
  checking = true;
8399
8551
  try {
8400
- const operation = await endpoint.getOperation(operationId);
8552
+ let operation = await endpoint.getOperation(operationId);
8553
+ while (operation?.resentAs) {
8554
+ operationId = operation.resentAs;
8555
+ operation = await endpoint.getOperation(operationId);
8556
+ }
8401
8557
  if (!operation) return finish(void 0, new EndpointError("Unknown endpoint operation", { status: 404, code: "OPERATION_NOT_FOUND", retryable: false, operationId }));
8402
8558
  if (operation.status === "uncertain") return finish(void 0, new EndpointError("Delivery outcome requires reconciliation", { code: "OPERATION_UNCERTAIN", retryable: false, operationId }));
8403
8559
  if (terminal(operation.status) && operation.receipt) {
@@ -8660,6 +8816,57 @@ function createEndpoint(options) {
8660
8816
  startRecovery: recovery.start,
8661
8817
  configureRecoveryBaseline: recovery.configureBaseline,
8662
8818
  getRecoveryBaseline: recovery.getBaseline,
8819
+ async startWorkAt(cutoverAt) {
8820
+ assertOpen();
8821
+ await initialization;
8822
+ if (typeof cutoverAt !== "string" || !Number.isFinite(Date.parse(cutoverAt))) {
8823
+ throw new EndpointError("Invalid work floor", { code: "INVALID_RECOVERY_BASELINE", retryable: false });
8824
+ }
8825
+ const sweep = await store.transaction(async (tx) => {
8826
+ const floor = await tx.get(WORK_FLOOR_KEY);
8827
+ const at = floor ? Date.parse(floor.cutoverAt) : -Infinity, next = Date.parse(cutoverAt);
8828
+ if (at > next || at === next && floor?.swept) return false;
8829
+ if (at < next) await tx.put(WORK_FLOOR_KEY, { cutoverAt });
8830
+ return true;
8831
+ });
8832
+ if (!sweep) return;
8833
+ for (let after; ; ) {
8834
+ const page = await store.transaction(async (tx) => {
8835
+ const floor = await tx.get(WORK_FLOOR_KEY);
8836
+ const answered = await completions.answered(tx);
8837
+ const entries2 = await tx.scan("inbound/", { ...after ? { after } : {}, limit: 100 });
8838
+ for (const { key: recordKey, value } of entries2) {
8839
+ if (value.state === "settled" || value.externalClaim || answered.has(value.event.id) || !beforeWorkFloor(floor, value.createdAt)) continue;
8840
+ const next = { ...value, state: "settled", reason: "before_take_over", updatedAt: now() };
8841
+ delete next.lease;
8842
+ delete next.handoffGeneration;
8843
+ delete next.handoffInstallationGeneration;
8844
+ await removeInboundReady(tx, value);
8845
+ await tx.put(recordKey, next);
8846
+ }
8847
+ return entries2;
8848
+ });
8849
+ if (page.length < 100) break;
8850
+ after = page.at(-1).key;
8851
+ }
8852
+ await store.transaction(async (tx) => {
8853
+ const floor = await tx.get(WORK_FLOOR_KEY);
8854
+ if (floor && Date.parse(floor.cutoverAt) === Date.parse(cutoverAt)) await tx.put(WORK_FLOOR_KEY, { cutoverAt: floor.cutoverAt, swept: true });
8855
+ });
8856
+ },
8857
+ async takeOverHome() {
8858
+ assertOpen();
8859
+ await opened;
8860
+ const operationId = `${TAKE_OVER_OPERATION}${ids()}`;
8861
+ const { cutoverAt } = await endpoint.executeResult({ action: "initialize_endpoint", body: {} }, { operationId }).catch(async (error) => {
8862
+ await store.transaction(async (tx) => {
8863
+ const current = await tx.get(key2(operationId));
8864
+ if (current && !terminal(current.status)) await discardOperation(tx, current, TAKE_OVER_DISCARDED);
8865
+ });
8866
+ throw error;
8867
+ });
8868
+ await endpoint.startWorkAt(cutoverAt);
8869
+ },
8663
8870
  refreshRecovery: recovery.refresh,
8664
8871
  planRecoveryMigration(baseline, conversationIds) {
8665
8872
  assertOpen();
@@ -8768,23 +8975,11 @@ function createEndpoint(options) {
8768
8975
  const batch = await store.transaction(async (tx) => {
8769
8976
  const entries2 = await tx.scan("operation/", { after, limit: 100 });
8770
8977
  for (const entry of entries2) {
8771
- if (!terminal(entry.value.status)) {
8772
- const status = entry.value.attempted ? "uncertain" : "rejected";
8773
- const next = { ...entry.value, status, updatedAt: now(), receipt: {
8774
- ...deferredReceipt(entry.value),
8775
- status,
8776
- updatedAt: now(),
8777
- error: { code: "OPERATION_DISCARDED", message: "Local delivery was cancelled", retryable: false }
8778
- } };
8779
- delete next.request;
8780
- delete next.preparedContent;
8781
- delete next.lease;
8782
- delete next.nextAttemptAt;
8783
- await tx.put(entry.key, next);
8784
- await settleMlsDevTextOperation(tx, next);
8978
+ if (!terminal(entry.value.status)) await discardOperation(tx, entry.value, "Local delivery was cancelled");
8979
+ else {
8980
+ await removeOperationOutbox(tx, entry.value);
8981
+ await settleOperationOrder(tx, entry.value);
8785
8982
  }
8786
- await removeOperationOutbox(tx, entry.value);
8787
- if (!entry.value.attempted || terminal(entry.value.status)) await settleOperationOrder(tx, entry.value);
8788
8983
  }
8789
8984
  return entries2;
8790
8985
  });
@@ -8894,7 +9089,7 @@ async function acquireAgentEndpoint(input) {
8894
9089
  }
8895
9090
  const requestedMls = input.options.mlsDev;
8896
9091
  const mls = requestedMls ? await (async () => {
8897
- const crypto2 = await import("./mls-crypto-ZBEC26B5.js");
9092
+ const crypto2 = await import("./mls-crypto-AW7TY744.js");
8898
9093
  const configuration = crypto2.parseMlsDevClientConfiguration(requestedMls.configuration);
8899
9094
  if (configuration.environmentId !== environmentId || baseUrl !== CANON_RUNTIME_ENDPOINT_DEFAULTS[configuration.environmentId]?.apiBaseUrl || configuration.principalIds && !configuration.principalIds.includes(input.principalId) || !Number.isSafeInteger(input.ownershipVersion) || input.ownershipVersion < 0) throw new Error("MLS_DEV_AGENT_CONFIGURATION_BINDING_MISMATCH");
8900
9095
  const custody = requestedMls.custody;
@@ -9079,6 +9274,17 @@ function isDiscoverAgentsResult(value) {
9079
9274
  return isRecord(value) && Array.isArray(value.agents) && value.agents.every(isAgentDirectoryEntry) && (value.nextCursor === null || typeof value.nextCursor === "string");
9080
9275
  }
9081
9276
  __name(isDiscoverAgentsResult, "isDiscoverAgentsResult");
9277
+ function homeClaim(input) {
9278
+ const first = input.baseline ? void 0 : "initialize-endpoint";
9279
+ if (isConnectorRelayInstallation(input.installationId)) return first;
9280
+ if (input.home?.kind === "connector") return input.runner ? "refuse" : void 0;
9281
+ const holder = input.home?.installationId;
9282
+ if (holder === void 0 || holder === input.installationId) return first;
9283
+ if (!input.runner) return void 0;
9284
+ if (!input.baseline) return first;
9285
+ return input.takeOver ? "take-over" : "refuse";
9286
+ }
9287
+ __name(homeClaim, "homeClaim");
9082
9288
  var CanonClient = class {
9083
9289
  static {
9084
9290
  __name(this, "CanonClient");
@@ -9110,12 +9316,29 @@ var CanonClient = class {
9110
9316
  options: this.endpointOptions
9111
9317
  });
9112
9318
  try {
9113
- if (context.endpointBootstrapEligible && this.endpointOptions.autoDrain !== false && !await lease.endpoint.getRecoveryBaseline()) {
9114
- const baseline = await lease.endpoint.executeResult(
9115
- { action: "initialize_endpoint", body: {} },
9116
- { operationId: "initialize-endpoint" }
9117
- );
9118
- await lease.endpoint.configureRecoveryBaseline(baseline);
9319
+ if (context.endpointBootstrapEligible && this.endpointOptions.autoDrain !== false) {
9320
+ const baseline = await lease.endpoint.getRecoveryBaseline();
9321
+ const { installationId } = await lease.endpoint.ready();
9322
+ const home = context.home;
9323
+ const claim = homeClaim({
9324
+ installationId,
9325
+ home,
9326
+ baseline: !!baseline,
9327
+ runner: !!this.endpointOptions.claimHome,
9328
+ takeOver: !!this.endpointOptions.takeOver
9329
+ });
9330
+ if (claim === "refuse") {
9331
+ throw new EndpointError(agentHomeElsewhereMessage(context.displayName, home), { status: 409, code: AGENT_HOME_ELSEWHERE, retryable: false });
9332
+ }
9333
+ if (claim === "take-over") await lease.endpoint.takeOverHome();
9334
+ else if (claim) {
9335
+ await lease.endpoint.configureRecoveryBaseline(await lease.endpoint.executeResult(
9336
+ { action: "initialize_endpoint", body: {} },
9337
+ { operationId: claim }
9338
+ ));
9339
+ } else if (home?.kind === "runner" && home.installationId === installationId && home.since) {
9340
+ await lease.endpoint.startWorkAt(new Date(home.since).toISOString());
9341
+ }
9119
9342
  }
9120
9343
  return lease;
9121
9344
  } catch (error) {
@@ -10280,8 +10503,16 @@ async function ensureAgentProfile(options) {
10280
10503
  await verifyCanonRuntimeConnection(connection);
10281
10504
  return { status: "approved", apiKey: existing.apiKey, agentId: existing.agentId, agentName: existing.agentName };
10282
10505
  }
10506
+ const requestedAgentId = input.requestedAgentId ?? (options.reconnect && !pending ? existing?.agentId : void 0);
10507
+ if (options.reconnect && !pending && !requestedAgentId) {
10508
+ throw new Error(`Canon profile "${profile}" has no saved agent to reconnect: give input.requestedAgentId, or register without reconnect`);
10509
+ }
10510
+ if (options.reconnect && !pending && existing && requestedAgentId !== existing.agentId) {
10511
+ throw new Error("Saved Canon profile belongs to another agent identity");
10512
+ }
10283
10513
  return resumeRegistration({
10284
10514
  ...options,
10515
+ input: { ...input, ...requestedAgentId ? { requestedAgentId } : {} },
10285
10516
  store: {
10286
10517
  load: /* @__PURE__ */ __name(() => loadPendingRegistrations()[profile], "load"),
10287
10518
  save: /* @__PURE__ */ __name((session) => {
@@ -11534,14 +11765,15 @@ export {
11534
11765
  loadProfiles,
11535
11766
  isProfileLocked,
11536
11767
  acquireLock,
11768
+ loadPendingRegistrations,
11537
11769
  clearPendingRegistration,
11538
11770
  ensureAgentProfile,
11539
11771
  initRTDBAuth,
11540
11772
  renderCanonHostInboundContent,
11541
11773
  createRuntimeStatePublisher,
11542
11774
  isPendingCanonOperation,
11775
+ utf8ByteLength,
11543
11776
  splitTextWithinBytes,
11544
- readCliPackageVersion,
11545
11777
  runCli,
11546
11778
  BoundedFileReadError,
11547
11779
  readBoundedRegularFileHandle,