@canonmsg/agent-sdk 13.1.0 → 13.2.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.
@@ -11,6 +11,7 @@ import {
11
11
  } from "./chunk-DTAWEQCM.js";
12
12
  import {
13
13
  BoundedFileReadError,
14
+ CanonApiError,
14
15
  CanonClient,
15
16
  MAX_CANON_MEDIA_BYTES,
16
17
  RUNTIME_NEW_SESSION_ACTION,
@@ -18,7 +19,6 @@ import {
18
19
  RUNTIME_STOP_AND_DROP_ACTION,
19
20
  acquireLock,
20
21
  conversationMediaCacheDir,
21
- createCompletedOutputJournal,
22
22
  createRuntimeHeartbeat,
23
23
  createRuntimeStatePublisher,
24
24
  inferUploadMimeType,
@@ -31,7 +31,7 @@ import {
31
31
  resolveCanonRuntimeConnection,
32
32
  splitTextWithinBytes,
33
33
  verifyCanonRuntimeConnection
34
- } from "./chunk-ZPBOGOAD.js";
34
+ } from "./chunk-5YKGDUJX.js";
35
35
  import {
36
36
  NodeMlsDevCustodyError,
37
37
  createNodeMlsDevCustody,
@@ -1585,7 +1585,7 @@ ${name}`, `Errands: ${name} in the inbox is not a command file; it is left alone
1585
1585
  }
1586
1586
  const path = join2(inbox, name);
1587
1587
  try {
1588
- if (handler.taken(id) || await handler.take(id, readCommand(path, id))) unlinkSync(path);
1588
+ if (await handler.taken(id) || await handler.take(id, readCommand(path, id))) unlinkSync(path);
1589
1589
  } catch (error) {
1590
1590
  if (error.code === "ENOENT") continue;
1591
1591
  const reason = error instanceof Error ? error.message : String(error);
@@ -2413,10 +2413,10 @@ function relayRunnerHome(home, installationId) {
2413
2413
  __name(relayRunnerHome, "relayRunnerHome");
2414
2414
 
2415
2415
  // src/runner/runner.ts
2416
- import { createHash as createHash8 } from "node:crypto";
2417
- import { readFileSync as readFileSync6, rmSync as rmSync2 } from "node:fs";
2416
+ import { createHash as createHash9, randomUUID as randomUUID2 } from "node:crypto";
2417
+ import { readFileSync as readFileSync7, rmSync as rmSync2 } from "node:fs";
2418
2418
  import { realpath as realpath4, stat as stat2 } from "node:fs/promises";
2419
- import { basename as basename4, isAbsolute as isAbsolute3, join as join7, sep } from "node:path";
2419
+ import { basename as basename4, isAbsolute as isAbsolute3, join as join8, sep } from "node:path";
2420
2420
 
2421
2421
  // ../agent-tools/src/communication-tool.ts
2422
2422
  var CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS = 10;
@@ -2638,6 +2638,54 @@ async function executeCanonCommunicateTool(client, args) {
2638
2638
  }
2639
2639
  __name(executeCanonCommunicateTool, "executeCanonCommunicateTool");
2640
2640
 
2641
+ // src/runner/finals.ts
2642
+ import { createHash as createHash7 } from "node:crypto";
2643
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5, rmdirSync, unlinkSync as unlinkSync2 } from "node:fs";
2644
+ import { join as join6 } from "node:path";
2645
+ var sha2562 = /* @__PURE__ */ __name((value) => createHash7("sha256").update(value).digest("hex"), "sha256");
2646
+ async function importFinals(directory, scope, input) {
2647
+ const folder = join6(directory, sha2562(JSON.stringify(scope)));
2648
+ let names;
2649
+ try {
2650
+ names = readdirSync3(folder);
2651
+ } catch {
2652
+ return;
2653
+ }
2654
+ for (const name of names.sort()) {
2655
+ const match = /^([a-f0-9]{64})(\.\d+\.[a-f0-9-]+\.tmp|\.json)$/.exec(name);
2656
+ if (!match) continue;
2657
+ const path = join6(folder, name);
2658
+ const torn = match[2] !== ".json";
2659
+ let record;
2660
+ try {
2661
+ record = JSON.parse(readFileSync5(path, "utf8"));
2662
+ } catch {
2663
+ record = void 0;
2664
+ }
2665
+ if (!input.valid(record) || sha2562(record.turnId) !== match[1]) {
2666
+ if (torn) {
2667
+ try {
2668
+ unlinkSync2(path);
2669
+ } catch (error) {
2670
+ input.warn(`torn output write ${name}`)(error);
2671
+ }
2672
+ } else input.warn(`saved output ${name}`)(new Error("it is not a valid output; it stays for operator reconciliation"));
2673
+ continue;
2674
+ }
2675
+ try {
2676
+ await input.save(record);
2677
+ unlinkSync2(path);
2678
+ } catch (error) {
2679
+ input.warn(`saved output ${name} stays where it was`)(error);
2680
+ }
2681
+ }
2682
+ try {
2683
+ rmdirSync(folder);
2684
+ } catch {
2685
+ }
2686
+ }
2687
+ __name(importFinals, "importFinals");
2688
+
2641
2689
  // src/runner/live.ts
2642
2690
  import { randomBytes as randomBytes3 } from "node:crypto";
2643
2691
  var KEEPALIVE_MS = 3500;
@@ -2681,10 +2729,10 @@ function createLivePublisher(options) {
2681
2729
  __name(createLivePublisher, "createLivePublisher");
2682
2730
 
2683
2731
  // src/runner/mls-ask.ts
2684
- import { createHash as createHash7 } from "node:crypto";
2685
- import { readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync } from "node:fs";
2686
- import { join as join6 } from "node:path";
2687
- var sha2562 = /* @__PURE__ */ __name((value) => createHash7("sha256").update(value).digest("hex"), "sha256");
2732
+ import { createHash as createHash8 } from "node:crypto";
2733
+ import { readdirSync as readdirSync4, readFileSync as readFileSync6, rmSync } from "node:fs";
2734
+ import { join as join7 } from "node:path";
2735
+ var sha2563 = /* @__PURE__ */ __name((value) => createHash8("sha256").update(value).digest("hex"), "sha256");
2688
2736
  var pause = /* @__PURE__ */ __name((ms) => new Promise((resolve4) => {
2689
2737
  setTimeout(resolve4, ms).unref?.();
2690
2738
  }), "pause");
@@ -2738,10 +2786,10 @@ function createMlsAsks(options, approvalScope2) {
2738
2786
  });
2739
2787
  }
2740
2788
  });
2741
- const file = /* @__PURE__ */ __name((conversationId, requestId) => join6(options.directory, `${sha2562(JSON.stringify([conversationId, requestId])).slice(0, 32)}.json`), "file");
2789
+ const file = /* @__PURE__ */ __name((conversationId, requestId) => join7(options.directory, `${sha2563(JSON.stringify([conversationId, requestId])).slice(0, 32)}.json`), "file");
2742
2790
  const save = /* @__PURE__ */ __name((journal) => writeDurable(file(journal.conversationId, journal.requestId), JSON.stringify(journal)), "save");
2743
2791
  const forget = /* @__PURE__ */ __name((journal) => rmSync(file(journal.conversationId, journal.requestId), { force: true }), "forget");
2744
- const operationOf = /* @__PURE__ */ __name((conversationId, messageId) => `runner-mls-ask:${sha2562(JSON.stringify([conversationId, messageId])).slice(0, 48)}`, "operationOf");
2792
+ const operationOf = /* @__PURE__ */ __name((conversationId, messageId) => `runner-mls-ask:${sha2563(JSON.stringify([conversationId, messageId])).slice(0, 48)}`, "operationOf");
2745
2793
  const withdrawalOf = /* @__PURE__ */ __name((journal) => operationOf(journal.conversationId, `${journal.requestId}:withdraw`), "withdrawalOf");
2746
2794
  const audienceChanged = /* @__PURE__ */ __name((error) => codeOf(error) === "MLS_DEV_AUDIENCE_CHANGED", "audienceChanged");
2747
2795
  const sameHead = /* @__PURE__ */ __name((a, b) => JSON.stringify(a.head) === JSON.stringify(b.head), "sameHead");
@@ -3075,18 +3123,18 @@ function createMlsAsks(options, approvalScope2) {
3075
3123
  async recover() {
3076
3124
  let names;
3077
3125
  try {
3078
- names = readdirSync3(options.directory).filter((name) => name.endsWith(".json"));
3126
+ names = readdirSync4(options.directory).filter((name) => name.endsWith(".json"));
3079
3127
  } catch {
3080
3128
  return;
3081
3129
  }
3082
3130
  for (const name of names) {
3083
3131
  let journal;
3084
3132
  try {
3085
- journal = JSON.parse(readFileSync5(join6(options.directory, name), "utf8"));
3133
+ journal = JSON.parse(readFileSync6(join7(options.directory, name), "utf8"));
3086
3134
  if (journal.version !== 1 || typeof journal.conversationId !== "string" || typeof journal.requestId !== "string" || !Array.isArray(journal.sends) || journal.sends.some((sent) => typeof sent?.operationId !== "string" || typeof sent.messageId !== "string")) throw new Error();
3087
3135
  for (const sent of journal.sends) sent.audience = snapshotMlsAudience(sent.audience);
3088
3136
  } catch {
3089
- rmSync(join6(options.directory, name), { force: true });
3137
+ rmSync(join7(options.directory, name), { force: true });
3090
3138
  continue;
3091
3139
  }
3092
3140
  try {
@@ -3182,6 +3230,38 @@ __name(startToolServer, "startToolServer");
3182
3230
 
3183
3231
  // src/runner/runner.ts
3184
3232
  var STOPS = ["stop", "drop", "new_session", "membership", "audience"];
3233
+ var toCompletion = /* @__PURE__ */ __name((record) => ({
3234
+ version: 1,
3235
+ id: record.turnId,
3236
+ conversationId: record.conversationId,
3237
+ createdAt: record.createdAt,
3238
+ inputIds: record.inputs,
3239
+ fence: record.mls ?? null,
3240
+ output: {
3241
+ text: record.text,
3242
+ options: record.options,
3243
+ notice: record.notice,
3244
+ ...record.items ? { items: record.items } : {},
3245
+ ...record.parts !== void 0 ? { parts: record.parts } : {},
3246
+ ...record.errand ? { errand: record.errand } : {},
3247
+ ...record.proactive ? { proactive: true } : {},
3248
+ ...record.sent ? { sent: record.sent } : {}
3249
+ }
3250
+ }), "toCompletion");
3251
+ var fromCompletion = /* @__PURE__ */ __name(({ id, conversationId, createdAt, inputIds, fence, output }) => ({
3252
+ turnId: id,
3253
+ conversationId,
3254
+ createdAt,
3255
+ inputs: inputIds,
3256
+ ...output,
3257
+ ...fence ? { mls: fence } : {}
3258
+ }), "fromCompletion");
3259
+ var isOutputFile = /* @__PURE__ */ __name((record) => {
3260
+ const value = record;
3261
+ return !!value && value.version === 1 && typeof value.turnId === "string" && !!value.turnId && typeof value.conversationId === "string" && !!value.conversationId && typeof value.createdAt === "string" && typeof value.text === "string" && typeof value.options?.messageId === "string" && Array.isArray(value.inputs) && value.inputs.every((id) => typeof id === "string" && !!id) && typeof value.notice === "boolean" && (value.mls === void 0 || !!value.mls && typeof value.mls === "object") && (value.parts === void 0 || Number.isSafeInteger(value.parts)) && (value.errand === void 0 || typeof value.errand === "string") && (value.proactive === void 0 || value.proactive === true) && (value.sent === void 0 || typeof value.sent?.fingerprint === "string" && ["delivered", "refused"].includes(value.sent.outcome));
3262
+ }, "isOutputFile");
3263
+ var textFingerprint = /* @__PURE__ */ __name((text3) => createHash9("sha256").update(JSON.stringify(["canon.runner.proactive-text.v1", text3])).digest("hex"), "textFingerprint");
3264
+ var PLAINTEXT_START = ["opening-text-required", "encrypted-room-required"];
3185
3265
  var MAX_INPUTS_PER_TURN = 20;
3186
3266
  var OUTBOUND_UNANSWERED = ["queued", "sending", "deferred", "pending"];
3187
3267
  var INBOUND_UNSETTLED = ["observed", "deferred", "offered"];
@@ -3246,7 +3326,7 @@ function answerText(answer) {
3246
3326
  return answer.text?.trim() ? answer.text : "They answered with an empty message.";
3247
3327
  }
3248
3328
  __name(answerText, "answerText");
3249
- var sha2563 = /* @__PURE__ */ __name((value) => createHash8("sha256").update(value).digest("hex"), "sha256");
3329
+ var sha2564 = /* @__PURE__ */ __name((value) => createHash9("sha256").update(value).digest("hex"), "sha256");
3250
3330
  var toolText = /* @__PURE__ */ __name((text3, isError = false) => ({ content: [{ type: "text", text: text3 }], ...isError ? { isError } : {} }), "toolText");
3251
3331
  var turnEnded2 = /* @__PURE__ */ __name(() => new DOMException("This turn has ended", "AbortError"), "turnEnded");
3252
3332
  function within(promise, ms) {
@@ -3347,7 +3427,7 @@ function approvalScope(request) {
3347
3427
  if (request.scopeKey !== void 0 && /^[A-Za-z0-9_:-]{1,64}$/.test(request.scopeKey)) return request.scopeKey;
3348
3428
  const key = request.scopeKey ?? request.title;
3349
3429
  const slug = key.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40) || "approval";
3350
- return `${slug}-${sha2563(JSON.stringify([request.scopeKey === void 0 ? "title" : "scope", key])).slice(0, 16)}`;
3430
+ return `${slug}-${sha2564(JSON.stringify([request.scopeKey === void 0 ? "title" : "scope", key])).slice(0, 16)}`;
3351
3431
  }
3352
3432
  __name(approvalScope, "approvalScope");
3353
3433
  var MAX_GROUP_PARTS = 100;
@@ -3457,7 +3537,6 @@ async function createRunner(options) {
3457
3537
  let endpoint;
3458
3538
  let inbound;
3459
3539
  let heartbeat;
3460
- let journal;
3461
3540
  let agentId = "";
3462
3541
  let scope = "";
3463
3542
  let stopping;
@@ -3481,6 +3560,7 @@ async function createRunner(options) {
3481
3560
  const ruleNotes = /* @__PURE__ */ new Set();
3482
3561
  const guards = /* @__PURE__ */ new Map();
3483
3562
  const relaySends = /* @__PURE__ */ new Map();
3563
+ const proactive = /* @__PURE__ */ new Map();
3484
3564
  let errands;
3485
3565
  let errandInbox2;
3486
3566
  let errandScope = "";
@@ -3584,7 +3664,7 @@ async function createRunner(options) {
3584
3664
  const dispose = /* @__PURE__ */ __name((inputs, kind, reason) => Promise.allSettled(inputs.map((input) => input.handle[kind](reason))).then(() => void 0), "dispose");
3585
3665
  const settle = /* @__PURE__ */ __name(async (ids, reason) => (await Promise.allSettled(ids.map((id) => endpoint.setInboundState(id, "settled", reason)))).every(({ status }) => status === "fulfilled"), "settle");
3586
3666
  const queued = /* @__PURE__ */ __name((turn) => turn.inputs.filter((input) => input.handle.phase === "queued"), "queued");
3587
- const turnId = /* @__PURE__ */ __name((roomId, inputs) => `turn-${sha2563(JSON.stringify([roomId, ...inputs.map((input) => [input.id, input.generation])])).slice(0, 32)}`, "turnId");
3667
+ const turnId = /* @__PURE__ */ __name((roomId, inputs) => `turn-${sha2564(JSON.stringify([roomId, ...inputs.map((input) => [input.id, input.generation])])).slice(0, 32)}`, "turnId");
3588
3668
  const livePublisher = /* @__PURE__ */ __name((current, turn) => createLivePublisher({
3589
3669
  client,
3590
3670
  conversationId: current.id,
@@ -3593,7 +3673,7 @@ async function createRunner(options) {
3593
3673
  current.lane = current.lane.then(publish2).catch(() => void 0);
3594
3674
  }, "enqueue")
3595
3675
  }), "livePublisher");
3596
- const stateFile = /* @__PURE__ */ __name((session) => join7(home, "runner", options.environmentId, agentId, "state", driver.name, `${sha2563(JSON.stringify([scope, session]))}.json`), "stateFile");
3676
+ const stateFile = /* @__PURE__ */ __name((session) => join8(home, "runner", options.environmentId, agentId, "state", driver.name, `${sha2564(JSON.stringify([scope, session]))}.json`), "stateFile");
3597
3677
  const resetSession = /* @__PURE__ */ __name(async (session) => {
3598
3678
  rmSync2(stateFile(session), { force: true });
3599
3679
  mlsAsks?.endSession(session);
@@ -4061,7 +4141,7 @@ ${decision.reasonCode}`;
4061
4141
  return check;
4062
4142
  }
4063
4143
  __name(reaudience, "reaudience");
4064
- const inboundMedia = /* @__PURE__ */ __name((conversationId) => ({ agentId: "inbound", conversationId, rootDir: join7(home, "runner", options.environmentId, agentId) }), "inboundMedia");
4144
+ const inboundMedia = /* @__PURE__ */ __name((conversationId) => ({ agentId: "inbound", conversationId, rootDir: join8(home, "runner", options.environmentId, agentId) }), "inboundMedia");
4065
4145
  async function materialize(current, turn) {
4066
4146
  if (driver.capabilities.remote) return;
4067
4147
  for (const { payload: { message: message2 } } of turn.inputs) {
@@ -4112,7 +4192,7 @@ ${decision.reasonCode}`;
4112
4192
  ask_question: /* @__PURE__ */ __name(async (args) => {
4113
4193
  const question = questionOf(args);
4114
4194
  if (typeof question === "string") return { result: toolText(question, true) };
4115
- const key = sha2563(JSON.stringify(question));
4195
+ const key = sha2564(JSON.stringify(question));
4116
4196
  const waiting = questions.get(key);
4117
4197
  const entry = waiting?.waiting ? waiting : { id: `canon:ask_question:${asked += 1}`, waiting: true };
4118
4198
  questions.set(key, entry);
@@ -4195,7 +4275,7 @@ ${decision.reasonCode}`;
4195
4275
  }
4196
4276
  }
4197
4277
  if (errand && "contact" in errand.target && (command.action === "start_conversation" || command.action === "start_direct") && command.principalId === errand.target.contact) {
4198
- const result = await client.communicate({ ...command, action: "start_conversation", selection: { mode: "new" } });
4278
+ const result = await startConversation({ ...command, action: "start_conversation", selection: { mode: "new" } });
4199
4279
  if (result.status === "messaged") reach(turn, result.conversationId);
4200
4280
  return toolText(`communicate: ${result.status}
4201
4281
  ${JSON.stringify(result, null, 2)}`);
@@ -4211,20 +4291,18 @@ ${JSON.stringify(result, null, 2)}`);
4211
4291
  turn.said.set(messageId, sending = (async () => {
4212
4292
  const mls2 = audience ?? (await currentAudience(roomId)).audience;
4213
4293
  const record = {
4214
- version: 1,
4215
4294
  turnId: messageId,
4216
4295
  conversationId: roomId,
4217
4296
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4218
- audience: null,
4219
4297
  inputs: [],
4220
4298
  text: text3,
4221
4299
  options: { messageId },
4222
4300
  notice: false,
4223
4301
  mls: mls2
4224
4302
  };
4225
- saveOutput(record);
4303
+ await saveOutput(record);
4226
4304
  const sent2 = await publish(record, turn.retire.signal);
4227
- if (sent2 !== "pending") journal.remove(messageId);
4305
+ if (sent2 !== "pending") await endpoint.finishCompletion(messageId);
4228
4306
  else if (turn.retire.signal.aborted) await stopOutput(record);
4229
4307
  return sent2;
4230
4308
  })().catch((error) => {
@@ -4308,11 +4386,9 @@ ${JSON.stringify(result, null, 2)}`);
4308
4386
  const kind = failed ? "failed" : "final";
4309
4387
  const replyAuthority = await replyGrant(current.id, turn.inputs.at(-1).payload.message.id, stopped);
4310
4388
  const record = {
4311
- version: 1,
4312
4389
  turnId: turn.id,
4313
4390
  conversationId: current.id,
4314
4391
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4315
- audience: null,
4316
4392
  inputs: turn.inputs.map((input) => input.id),
4317
4393
  text: text3,
4318
4394
  ...items.length ? { items } : {},
@@ -4320,7 +4396,7 @@ ${JSON.stringify(result, null, 2)}`);
4320
4396
  notice: failed,
4321
4397
  mls: audience
4322
4398
  };
4323
- saveOutput(record);
4399
+ await saveOutput(record);
4324
4400
  sent = await publish(record, turn.retire.signal);
4325
4401
  if (sent === "pending" && !stopped()) return turn.live.end(turn.reason && STOPS.includes(turn.reason) ? "stopped" : "ended");
4326
4402
  if (sent === "pending") await stopOutput(record);
@@ -4334,7 +4410,7 @@ ${JSON.stringify(result, null, 2)}`);
4334
4410
  if (current.session === turn.session) current.session = void 0;
4335
4411
  }
4336
4412
  const reason = stopped() ? turn.reason : silent ? "silent" : answered2 ? "answered" : "failed";
4337
- if (await settle(turn.inputs.map((input) => input.id), reason) && sent !== void 0) journal.remove(turn.id);
4413
+ await endpoint.finishCompletion(turn.id, { inputIds: turn.inputs.map((input) => input.id), reason }).catch(warn("reply kept for restart recovery"));
4338
4414
  }
4339
4415
  __name(deliver, "deliver");
4340
4416
  const forOwner2 = /* @__PURE__ */ __name((turn) => turn.inputs.every(({ payload: { message: message2 } }) => message2.isOwner === true || message2.senderType === "ai_agent" && turn.trusted.has(message2.id)), "forOwner");
@@ -4364,11 +4440,16 @@ ${JSON.stringify(result, null, 2)}`);
4364
4440
  }
4365
4441
  __name(send, "send");
4366
4442
  const partsOf = /* @__PURE__ */ __name((text3, options2) => encryptedParts(text3, options2.replyAuthority ? MAX_REPLY_PARTS : MAX_GROUP_PARTS), "partsOf");
4367
- function saveOutput(record) {
4443
+ async function saveOutput(record) {
4368
4444
  record.parts = partsOf(record.text, record.options).length;
4369
- journal.save(record);
4445
+ await endpoint.saveCompletion(toCompletion(record));
4370
4446
  }
4371
4447
  __name(saveOutput, "saveOutput");
4448
+ const outputs = /* @__PURE__ */ __name(async () => (await endpoint.listCompletions()).map(fromCompletion).filter((record) => !record.sent), "outputs");
4449
+ const savedOutput = /* @__PURE__ */ __name(async (turnId2) => {
4450
+ const found = await endpoint.getCompletion(turnId2);
4451
+ return found && fromCompletion(found);
4452
+ }, "savedOutput");
4372
4453
  function encryptedMessages(mls2, text3, options2, record) {
4373
4454
  const all = record && record.parts === void 0 ? [fitEncrypted(text3)] : partsOf(text3, options2);
4374
4455
  return all.slice(0, record?.parts).map((part, index) => ({
@@ -4384,11 +4465,9 @@ ${JSON.stringify(result, null, 2)}`);
4384
4465
  const status = (await endpoint.getOperation(encryptedOperation(record.mls, messageId)))?.status;
4385
4466
  if (status && status !== "succeeded" && status !== "rejected") queued2 = index + 1;
4386
4467
  }
4387
- if (queued2) {
4388
- const { items: _items, errand: _errand, ...rest } = record;
4389
- journal.save({ ...rest, turnId: `${record.turnId}:stopped`, inputs: [], parts: queued2 });
4390
- }
4391
- journal.remove(record.turnId);
4468
+ const { items: _items, errand: _errand, ...rest } = record;
4469
+ if (queued2) await endpoint.replaceCompletion(record.turnId, toCompletion({ ...rest, turnId: `${record.turnId}:stopped`, inputs: [], parts: queued2 }));
4470
+ else await endpoint.finishCompletion(record.turnId);
4392
4471
  }
4393
4472
  __name(stopOutput, "stopOutput");
4394
4473
  async function sendEncrypted(audience, messageId, text3, signal, replyAuthority) {
@@ -4484,7 +4563,7 @@ ${JSON.stringify(result, null, 2)}`);
4484
4563
  return { items, refused: refused2 };
4485
4564
  }
4486
4565
  __name(admitItems, "admitItems");
4487
- const encryptedOperation = /* @__PURE__ */ __name((audience, messageId) => `runner-mls:${sha2563(JSON.stringify([audience.conversationId, messageId])).slice(0, 48)}`, "encryptedOperation");
4566
+ const encryptedOperation = /* @__PURE__ */ __name((audience, messageId) => `runner-mls:${sha2564(JSON.stringify([audience.conversationId, messageId])).slice(0, 48)}`, "encryptedOperation");
4488
4567
  function forgetGuard(audience, messageId, sent) {
4489
4568
  if (sent === "pending") return;
4490
4569
  const operationId = encryptedOperation(audience, messageId);
@@ -4622,7 +4701,7 @@ ${JSON.stringify(result, null, 2)}`);
4622
4701
  const file = stateFile(session);
4623
4702
  let data = {};
4624
4703
  try {
4625
- data = JSON.parse(readFileSync6(file, "utf8"));
4704
+ data = JSON.parse(readFileSync7(file, "utf8"));
4626
4705
  } catch {
4627
4706
  }
4628
4707
  const save = /* @__PURE__ */ __name((next) => {
@@ -4693,9 +4772,8 @@ ${JSON.stringify(result, null, 2)}`);
4693
4772
  __name(scheduleControl, "scheduleControl");
4694
4773
  async function recover() {
4695
4774
  await mlsAsks?.recover().catch(warn("pending encrypted cards"));
4696
- const records = journal.list();
4697
- const answered2 = new Set(records.flatMap((record) => record.inputs));
4698
- const orphans = (await endpoint.listInbound({ states: ["uncertain"] })).filter(({ event }) => event.kind === "message.created" && event.conversationId && !answered2.has(event.id));
4775
+ const records = await outputs();
4776
+ const orphans = (await endpoint.listInbound({ states: ["uncertain"], unanswered: true })).filter(({ event }) => event.kind === "message.created" && event.conversationId);
4699
4777
  for (const roomId of /* @__PURE__ */ new Set([...records.map((record) => record.conversationId), ...orphans.map(({ event }) => event.conversationId)])) {
4700
4778
  const target = room(roomId);
4701
4779
  const work = (async () => {
@@ -4725,14 +4803,14 @@ ${JSON.stringify(result, null, 2)}`);
4725
4803
  async function redeliver(record) {
4726
4804
  if (!record.mls) {
4727
4805
  if (record.errand) return finishErrandOutput(record, "refused", "its conversation is not encrypted");
4728
- if (await settle(record.inputs, "plaintext-output-dropped")) journal.remove(record.turnId);
4729
- return;
4806
+ return endpoint.finishCompletion(record.turnId, { inputIds: record.inputs, reason: "plaintext-output-dropped" });
4730
4807
  }
4731
4808
  if (!mlsOn) return refuse(record.conversationId, "no-device");
4732
4809
  const sent = await publish(record);
4733
4810
  if (sent === "pending") return;
4734
4811
  if (record.errand) return finishErrandOutput(record, sent);
4735
- if (await settle(record.inputs, "recovered")) journal.remove(record.turnId);
4812
+ if (record.proactive) return finishProactive(record, sent);
4813
+ await endpoint.finishCompletion(record.turnId, { inputIds: record.inputs, reason: "recovered" });
4736
4814
  }
4737
4815
  __name(redeliver, "redeliver");
4738
4816
  function pendingNotes(roomId) {
@@ -4824,25 +4902,12 @@ ${JSON.stringify(result, null, 2)}`);
4824
4902
  log(`Errand ${id} (tell) kept for the next turn in ${conversationId}.`);
4825
4903
  return true;
4826
4904
  }
4827
- const turnId2 = `turn-${sha2563(JSON.stringify(["canon.runner.errand", id])).slice(0, 32)}`;
4828
- const record = {
4829
- version: 1,
4830
- turnId: turnId2,
4831
- conversationId,
4832
- createdAt,
4833
- audience: null,
4834
- inputs: [],
4835
- text: parsed.text,
4836
- options: { messageId: keyed().text("final", conversationId, turnId2) },
4837
- notice: false,
4838
- errand: id,
4839
- mls: target.mls
4840
- };
4841
- saveOutput(record);
4905
+ const turnId2 = `turn-${sha2564(JSON.stringify(["canon.runner.errand", id])).slice(0, 32)}`;
4906
+ const record = await outsideTurn({ conversationId, turnId: turnId2, text: parsed.text, mls: target.mls, createdAt, errand: id });
4842
4907
  errands.put({ ...base, status: "sending", updatedAt });
4843
- const delivery = publish(record).then((sent) => {
4844
- if (sent !== "pending") finishErrandOutput(record, sent);
4845
- }, warn(`errand ${id}`));
4908
+ const delivery = publish(record).then(async (sent) => {
4909
+ if (sent !== "pending") await finishErrandOutput(record, sent);
4910
+ }).catch(warn(`errand ${id}`));
4846
4911
  posting.add(delivery);
4847
4912
  touch();
4848
4913
  void delivery.finally(() => {
@@ -4863,10 +4928,12 @@ ${JSON.stringify(result, null, 2)}`);
4863
4928
  const mode = await endpoint.getConversationContentMode(roomId).catch(missing);
4864
4929
  if (mode === "plaintext") return "it is not encrypted, and the agent takes part only in encrypted conversations";
4865
4930
  if (mode !== "mls-dev-text" || !mlsOn) return "it is an encrypted conversation this runner cannot take part in";
4866
- return kind === "post" ? { mls: (await currentAudience(roomId)).audience } : {};
4931
+ if (kind !== "post") return {};
4932
+ const { audience, ownerId } = await currentAudience(roomId);
4933
+ return { mls: audience, ownerRoom: conversation.memberIds.length === 2 && conversation.memberIds.includes(ownerId) };
4867
4934
  }
4868
4935
  __name(errandRoom, "errandRoom");
4869
- function finishErrandOutput(record, sent, reason = "Canon did not accept it") {
4936
+ async function finishErrandOutput(record, sent, reason = "Canon did not accept it") {
4870
4937
  const id = record.errand;
4871
4938
  const known = latest(id) ?? { version: 1, id, kind: "post", conversationId: record.conversationId, createdAt: record.createdAt };
4872
4939
  const ok = sent === "delivered";
@@ -4877,9 +4944,108 @@ ${JSON.stringify(result, null, 2)}`);
4877
4944
  errands?.put({ ...known, status: ok ? "sent" : "refused", ...ok ? {} : { reason }, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
4878
4945
  log(`Errand ${id} (post) ${ok ? "sent" : `refused: ${reason}`} in ${record.conversationId}.`);
4879
4946
  }
4880
- journal.remove(record.turnId);
4947
+ await endpoint.finishCompletion(record.turnId);
4881
4948
  }
4882
4949
  __name(finishErrandOutput, "finishErrandOutput");
4950
+ async function outsideTurn(input) {
4951
+ const { conversationId, turnId: turnId2, text: text3, mls: mls2, errand } = input;
4952
+ const record = {
4953
+ turnId: turnId2,
4954
+ conversationId,
4955
+ createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4956
+ inputs: [],
4957
+ text: text3,
4958
+ options: { messageId: keyed().text("final", conversationId, turnId2) },
4959
+ notice: false,
4960
+ ...errand ? { errand } : {},
4961
+ ...input.proactive ? { proactive: true } : {},
4962
+ mls: mls2
4963
+ };
4964
+ await saveOutput(record);
4965
+ return record;
4966
+ }
4967
+ __name(outsideTurn, "outsideTurn");
4968
+ const refusal = /* @__PURE__ */ __name((status, code, message2) => new CanonApiError(status, JSON.stringify({ code, message: message2 })), "refusal");
4969
+ function sendOutside(command) {
4970
+ const { conversationId, text: text3 } = command;
4971
+ if (command.privateContext !== void 0 || command.sourceLabel !== void 0) {
4972
+ return Promise.reject(refusal(400, "ENCRYPTED_TEXT_ONLY", "Encrypted text carries no private context or source label"));
4973
+ }
4974
+ const turnId2 = `turn-${sha2564(JSON.stringify(["canon.runner.communicate", conversationId, command.messageId ?? randomUUID2()])).slice(0, 32)}`;
4975
+ const conflict = /* @__PURE__ */ __name(() => refusal(409, "MESSAGE_ID_CONFLICT", "This messageId was already sent with other text"), "conflict");
4976
+ const known = proactive.get(turnId2);
4977
+ if (known) return known.text === text3 ? known.result : Promise.reject(conflict());
4978
+ const result = (async () => {
4979
+ let record = await savedOutput(turnId2);
4980
+ const done = record ? void 0 : await savedOutput(`${turnId2}:sent`);
4981
+ if (record ? record.text !== text3 : done && done.sent.fingerprint !== textFingerprint(text3)) throw conflict();
4982
+ if (done) return sendResult(done.sent.outcome, conversationId, done.options.messageId);
4983
+ if (!record) {
4984
+ const room2 = await errandRoom(conversationId, "post");
4985
+ if (typeof room2 === "string") throw refusal(403, "CONVERSATION_UNAVAILABLE", `This runner cannot send to ${conversationId}: ${room2}`);
4986
+ if (outboundClosed && !room2.ownerRoom) {
4987
+ throw refusal(403, "OUTBOUND_NOT_ALLOWED", "This agent's outbound policy is closed: it answers messages, and writes unprompted only to its owner");
4988
+ }
4989
+ record = await outsideTurn({ conversationId, turnId: turnId2, text: text3, mls: room2.mls, proactive: command.messageId !== void 0 });
4990
+ }
4991
+ const sent = await publish(record);
4992
+ if (sent !== "pending") await finishProactive(record, sent);
4993
+ return sendResult(sent, conversationId, record.options.messageId);
4994
+ })().finally(() => proactive.delete(turnId2));
4995
+ proactive.set(turnId2, { text: text3, result });
4996
+ return result;
4997
+ }
4998
+ __name(sendOutside, "sendOutside");
4999
+ async function finishProactive(record, outcome) {
5000
+ const id = `${record.turnId}:sent`;
5001
+ if (!record.proactive || await savedOutput(id)) return endpoint.finishCompletion(record.turnId);
5002
+ await endpoint.replaceCompletion(record.turnId, toCompletion({
5003
+ turnId: id,
5004
+ conversationId: record.conversationId,
5005
+ createdAt: record.createdAt,
5006
+ inputs: [],
5007
+ text: "",
5008
+ options: { messageId: record.options.messageId },
5009
+ notice: false,
5010
+ sent: { fingerprint: textFingerprint(record.text), outcome }
5011
+ }));
5012
+ }
5013
+ __name(finishProactive, "finishProactive");
5014
+ function sendResult(sent, conversationId, messageId) {
5015
+ if (sent === "delivered") return { status: "messaged", conversationId, messageId };
5016
+ if (sent === "refused") throw refusal(409, "MESSAGE_REFUSED", "Canon did not accept the message; the conversation's members may have changed");
5017
+ throw refusal(202, "OPERATION_PENDING", "The message is saved and will be sent when Canon is reachable; call again with the same messageId to learn how it went");
5018
+ }
5019
+ __name(sendResult, "sendResult");
5020
+ async function startConversation(command) {
5021
+ if (!mlsOn) throw refusal(403, "MLS_DEVICE_REQUIRED", "This runner has no MLS device, so it can start no conversation");
5022
+ const input = {
5023
+ targetUserId: command.principalId,
5024
+ text: command.text,
5025
+ messageId: command.messageId ?? randomUUID2(),
5026
+ selection: command.selection ?? (command.action === "start_conversation" ? { mode: "new" } : { mode: "latest_or_new" })
5027
+ };
5028
+ const result = await endpoint.startWithheldOpener(input).catch(async (error) => {
5029
+ const { code, operationId } = error;
5030
+ if (code !== "OPERATION_PENDING" || typeof operationId !== "string") throw error;
5031
+ const receipt = await endpoint.waitForOperation(operationId);
5032
+ if (receipt.status !== "succeeded" || !receipt.result) throw error;
5033
+ return receipt.result;
5034
+ });
5035
+ if ("reason" in result) {
5036
+ if (PLAINTEXT_START.includes(result.reason)) throw refusal(403, "ENCRYPTED_ROOM_REQUIRED", "Canon would make that conversation plaintext, so nothing was sent");
5037
+ return { status: result.status, reason: result.reason };
5038
+ }
5039
+ if ("requestId" in result) return { status: result.status, requestId: result.requestId };
5040
+ return {
5041
+ status: "messaged",
5042
+ conversationId: result.conversationId,
5043
+ messageId: result.messageId ?? input.messageId,
5044
+ ...result.created !== void 0 ? { created: result.created } : {},
5045
+ ...result.reused !== void 0 ? { reused: result.reused } : {}
5046
+ };
5047
+ }
5048
+ __name(startConversation, "startConversation");
4883
5049
  const latest = /* @__PURE__ */ __name((id) => errandsById.get(id) ?? errands?.get(id), "latest");
4884
5050
  const openErrand = /* @__PURE__ */ __name((record) => record.kind === "errand" && !!record.conversationId && !["pending", "cancelled", "expired", "refused", "rejected"].includes(record.status), "openErrand");
4885
5051
  const aliveErrand = /* @__PURE__ */ __name((record) => record.kind === "errand" && (["pending", "running"].includes(record.status) || openErrand(record)), "aliveErrand");
@@ -4898,7 +5064,7 @@ ${JSON.stringify(result, null, 2)}`);
4898
5064
  pump();
4899
5065
  }
4900
5066
  __name(enqueueErrand, "enqueueErrand");
4901
- const errandTurnId = /* @__PURE__ */ __name((id) => `turn-${sha2563(JSON.stringify(["canon.runner.errand-turn", id])).slice(0, 32)}`, "errandTurnId");
5067
+ const errandTurnId = /* @__PURE__ */ __name((id) => `turn-${sha2564(JSON.stringify(["canon.runner.errand-turn", id])).slice(0, 32)}`, "errandTurnId");
4902
5068
  const errandFacts = /* @__PURE__ */ __name((record) => Object.freeze({
4903
5069
  id: record.id,
4904
5070
  why: record.why,
@@ -5080,11 +5246,9 @@ ${JSON.stringify(result, null, 2)}`);
5080
5246
  }
5081
5247
  const kind = failed ? "failed" : "final";
5082
5248
  const out = {
5083
- version: 1,
5084
5249
  turnId: turn.id,
5085
5250
  conversationId: origin.room,
5086
5251
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5087
- audience: null,
5088
5252
  inputs: [],
5089
5253
  text: text3,
5090
5254
  notice: failed,
@@ -5093,7 +5257,7 @@ ${JSON.stringify(result, null, 2)}`);
5093
5257
  options: { messageId: keyed().text(kind, origin.room, turn.id) }
5094
5258
  };
5095
5259
  try {
5096
- saveOutput(out);
5260
+ await saveOutput(out);
5097
5261
  } catch (error) {
5098
5262
  warn(`errand ${record.id}: its report could not be journaled; it stays in the record`)(error);
5099
5263
  saveErrand({ ...closing, report: text3, reported: "refused", reason: "its report: it could not be saved for sending" });
@@ -5105,7 +5269,7 @@ ${JSON.stringify(result, null, 2)}`);
5105
5269
  await stopOutput(out);
5106
5270
  const { reported: _reported, ...withheld2 } = latest(record.id) ?? closing;
5107
5271
  saveErrand({ ...withheld2, status: "cancelled", reason: STOPPED_ERRAND[turn.reason] ?? "stopped" });
5108
- } else if (sent !== "pending") finishErrandOutput(out, sent);
5272
+ } else if (sent !== "pending") await finishErrandOutput(out, sent);
5109
5273
  turn.live.end(sent === "delivered" ? "answered" : stopped() ? "stopped" : "ended");
5110
5274
  }
5111
5275
  __name(deliverReport, "deliverReport");
@@ -5179,7 +5343,7 @@ ${JSON.stringify(result, null, 2)}`);
5179
5343
  if (await endpoint.getConversationContentMode(conversationId).catch(() => "unavailable") !== "mls-dev-text") {
5180
5344
  throw new ConnectorRelayError(403, "CONNECTOR_RELAY_NOT_ENCRYPTED", "The relay serves only encrypted conversations its agent is in");
5181
5345
  }
5182
- const journaled = journal.list().find((candidate) => candidate.turnId === id);
5346
+ const journaled = await savedOutput(id);
5183
5347
  if (journaled) return { text: journaled.text, record: journaled };
5184
5348
  const { messages } = await readAgain(() => endpoint.query(
5185
5349
  "get_messages",
@@ -5190,22 +5354,20 @@ ${JSON.stringify(result, null, 2)}`);
5190
5354
  const { audience } = await relayRoom(conversationId);
5191
5355
  if (Date.now() >= deadline) throw relayBusy();
5192
5356
  const record = {
5193
- version: 1,
5194
5357
  turnId: id,
5195
5358
  conversationId,
5196
5359
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5197
- audience: null,
5198
5360
  inputs: [],
5199
5361
  text: text3,
5200
5362
  options: { messageId: id },
5201
5363
  notice: false,
5202
5364
  mls: audience
5203
5365
  };
5204
- saveOutput(record);
5366
+ await saveOutput(record);
5205
5367
  return { text: text3, record };
5206
5368
  })();
5207
- const sent = ready.then(({ record, settled }) => settled ?? publish(record).then((result) => {
5208
- if (result !== "pending") journal.remove(id);
5369
+ const sent = ready.then(({ record, settled }) => settled ?? publish(record).then(async (result) => {
5370
+ if (result !== "pending") await endpoint.finishCompletion(id);
5209
5371
  return result;
5210
5372
  }));
5211
5373
  const entry = { ready, sent };
@@ -5230,7 +5392,7 @@ ${JSON.stringify(result, null, 2)}`);
5230
5392
  client = new CanonClient(options.apiKey, connection.apiBaseUrl, {
5231
5393
  environmentId: connection.environmentId,
5232
5394
  streamUrl: connection.streamUrl,
5233
- stateDirectory: join7(home, "endpoint"),
5395
+ stateDirectory: join8(home, "endpoint"),
5234
5396
  ...options.client ? { client: options.client } : {},
5235
5397
  // Below Canon's client floor nothing can work, wherever the refusal surfaces: a query, an operation or the stream.
5236
5398
  ...options.onFatal ? { onClientUpdateRequired: /* @__PURE__ */ __name((error) => options.onFatal(error), "onClientUpdateRequired") } : {},
@@ -5278,29 +5440,30 @@ ${JSON.stringify(result, null, 2)}`);
5278
5440
  }
5279
5441
  scope = JSON.stringify([identity.environmentId, agentId, identity.ownershipVersion ?? 0, identity.installationId]);
5280
5442
  if (mlsOn) {
5281
- mlsIds = createMlsIds(privateKey(join7(home, "runner", options.environmentId, agentId, "mls-ids", `${sha2563(scope).slice(0, 32)}.key`), MLS_ID_KEY_BYTES));
5443
+ mlsIds = createMlsIds(privateKey(join8(home, "runner", options.environmentId, agentId, "mls-ids", `${sha2564(scope).slice(0, 32)}.key`), MLS_ID_KEY_BYTES));
5282
5444
  mlsAsks = createMlsAsks({
5283
5445
  endpoint,
5284
5446
  agentId,
5285
- directory: join7(home, "runner", options.environmentId, agentId, "asks", sha2563(scope).slice(0, 32)),
5447
+ directory: join8(home, "runner", options.environmentId, agentId, "asks", sha2564(scope).slice(0, 32)),
5286
5448
  assertAudience,
5287
5449
  sessionForDestructive: options.sessionApprovalForDestructive === true,
5288
5450
  log
5289
5451
  }, approvalScope);
5290
5452
  }
5291
- journal = createCompletedOutputJournal(join7(home, "runner", options.environmentId, agentId, "finals"), scope, {
5292
- label: "agent runner",
5293
- validate: /* @__PURE__ */ __name((record) => typeof record.text === "string" && typeof record.options?.messageId === "string" && Array.isArray(record.inputs) && typeof record.notice === "boolean" && (record.mls === void 0 || !!record.mls && typeof record.mls === "object") && (record.parts === void 0 || Number.isSafeInteger(record.parts)) && (record.errand === void 0 || typeof record.errand === "string"), "validate")
5294
- });
5453
+ await importFinals(
5454
+ join8(home, "runner", options.environmentId, agentId, "finals"),
5455
+ scope,
5456
+ { valid: isOutputFile, save: /* @__PURE__ */ __name((record) => endpoint.saveCompletion(toCompletion(record)), "save"), warn }
5457
+ );
5295
5458
  if (!options.relay && !driver.capabilities.remote) {
5296
5459
  errands = errandJournal(errandsDirectory(home, options.environmentId, agentId));
5297
- errandScope = sha2563(scope).slice(0, 32);
5460
+ errandScope = sha2564(scope).slice(0, 32);
5298
5461
  const pending = errands.list().filter((record) => record.kind === "tell" && record.status === "pending").sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? "") || a.id.localeCompare(b.id));
5299
5462
  for (const note of pending) {
5300
5463
  if (note.scope === errandScope) notes.set(note.conversationId, [...notes.get(note.conversationId) ?? [], note]);
5301
5464
  else settleNote(note.conversationId, note, "expired", void 0, "it was left under another owner or installation");
5302
5465
  }
5303
- const reports = new Map(journal.list().flatMap((output) => output.errand ? [[output.errand, output]] : []));
5466
+ const reports = new Map((await outputs()).flatMap((output) => output.errand ? [[output.errand, output]] : []));
5304
5467
  for (const record of errands.list().filter(aliveErrand)) {
5305
5468
  const report = reports.get(record.id);
5306
5469
  if (record.scope !== errandScope) saveErrand({ ...record, status: "expired", reason: "it was left under another owner or installation" });
@@ -5340,11 +5503,11 @@ ${JSON.stringify(result, null, 2)}`);
5340
5503
  onError: warn("event stream")
5341
5504
  }));
5342
5505
  if (errands) {
5343
- const journaled = /* @__PURE__ */ __name((id) => journal.list().some((record) => record.errand === id), "journaled");
5506
+ const journaled = /* @__PURE__ */ __name(async (id) => (await outputs()).some((record) => record.errand === id), "journaled");
5344
5507
  try {
5345
5508
  errandInbox2 = watchErrandInbox(
5346
5509
  errandsDirectory(home, options.environmentId, agentId),
5347
- { taken: /* @__PURE__ */ __name((id) => errands.has(id) || journaled(id), "taken"), take: takeErrand },
5510
+ { taken: /* @__PURE__ */ __name(async (id) => errands.has(id) || journaled(id), "taken"), take: takeErrand },
5348
5511
  { log }
5349
5512
  );
5350
5513
  } catch (error) {
@@ -5391,7 +5554,8 @@ ${JSON.stringify(result, null, 2)}`);
5391
5554
  },
5392
5555
  async communicate(input) {
5393
5556
  if (!client) throw new Error("Start the runner before sending");
5394
- return activeCall(() => client.communicate(parseCommunicateToolInput(input)));
5557
+ const command = parseCommunicateToolInput(input);
5558
+ return activeCall(() => command.action === "message_existing" ? sendOutside(command) : command.action === "start_conversation" || command.action === "start_direct" ? startConversation(command) : client.communicate(command));
5395
5559
  },
5396
5560
  async relayRead(conversationId, page = {}) {
5397
5561
  touch();
@@ -5418,7 +5582,7 @@ ${JSON.stringify(result, null, 2)}`);
5418
5582
  throw new ConnectorRelayError(400, "CONNECTOR_RELAY_INVALID", "A send needs a messageId and 1 byte to 32 KiB of text");
5419
5583
  }
5420
5584
  const deadline = options2.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
5421
- const id = `relay-send-${sha2563(JSON.stringify([conversationId, messageId])).slice(0, 32)}`;
5585
+ const id = `relay-send-${sha2564(JSON.stringify([conversationId, messageId])).slice(0, 32)}`;
5422
5586
  let entry = relaySends.get(id);
5423
5587
  if (!entry) relaySends.set(id, entry = prepareRelaySend(conversationId, id, text3, deadline));
5424
5588
  const prepared = await beforeDeadline(activeCall(() => entry.ready), deadline);
@@ -5435,7 +5599,7 @@ ${JSON.stringify(result, null, 2)}`);
5435
5599
  if (!options.relay || !client) throw new ConnectorRelayError(403, "CONNECTOR_RELAY_UNAVAILABLE", "This runner is not a relay");
5436
5600
  if (!mlsOn) throw new ConnectorRelayError(503, "CONNECTOR_RELAY_NO_DEVICE", "The relay has no MLS device");
5437
5601
  const deadline = startOptions.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
5438
- const identity = { messageId, historyMessageId: `relay-start-${sha2563(messageId).slice(0, 32)}` };
5602
+ const identity = { messageId, historyMessageId: `relay-start-${sha2564(messageId).slice(0, 32)}` };
5439
5603
  const answer = await within(
5440
5604
  activeCall(() => endpoint.startWithheldOpener({
5441
5605
  targetUserId,
@@ -5460,7 +5624,7 @@ ${JSON.stringify(result, null, 2)}`);
5460
5624
  }
5461
5625
  const result = answer.result;
5462
5626
  if ("reason" in result) {
5463
- if (result.reason === "opening-text-required") {
5627
+ if (PLAINTEXT_START.includes(result.reason)) {
5464
5628
  throw new ConnectorRelayError(403, "CONNECTOR_RELAY_NOT_ENCRYPTED", "The relay serves only encrypted conversations its agent is in");
5465
5629
  }
5466
5630
  return { ...identity, status: result.status, reason: result.reason };