@canonmsg/agent-sdk 13.0.3 → 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,
@@ -262,9 +262,9 @@ function stdioRpc(child, handlers, { name, envelope }) {
262
262
  }, PIPE_GRACE_MS).unref();
263
263
  });
264
264
  return {
265
- call: /* @__PURE__ */ __name((method, params) => new Promise((resolve3, reject) => {
265
+ call: /* @__PURE__ */ __name((method, params) => new Promise((resolve4, reject) => {
266
266
  next += 1;
267
- pending.set(next, { resolve: /* @__PURE__ */ __name((value) => resolve3(isRecord(value) ? value : {}), "resolve"), reject });
267
+ pending.set(next, { resolve: /* @__PURE__ */ __name((value) => resolve4(isRecord(value) ? value : {}), "resolve"), reject });
268
268
  write({ id: next, method, params });
269
269
  }), "call"),
270
270
  notify: /* @__PURE__ */ __name((method, params) => write({ method, ...params ? { params } : {} }), "notify"),
@@ -341,11 +341,11 @@ async function checkAcp({ command, args = [], cwd, timeoutMs = 3e4 }) {
341
341
  const link = stdioRpc(child, { notify: /* @__PURE__ */ __name(() => void 0, "notify"), request: /* @__PURE__ */ __name((method) => refuseMethod(method), "request") }, { name: basename(command), envelope: { jsonrpc: "2.0" } });
342
342
  let timer;
343
343
  try {
344
- return await new Promise((resolve3, reject) => {
344
+ return await new Promise((resolve4, reject) => {
345
345
  timer = setTimeout(() => reject(new Error(`${command} did not answer the Agent Client Protocol's initialize within ${timeoutMs / 1e3} s`)), timeoutMs);
346
346
  child.once("error", (error) => reject(new Error(`Could not run ${command}: ${error.message}`)));
347
347
  child.once("close", () => reject(new Error(`${command} exited before answering the Agent Client Protocol's initialize`)));
348
- initialize(link, command).then(({ capabilities, info }) => resolve3({
348
+ initialize(link, command).then(({ capabilities, info }) => resolve4({
349
349
  name: text(info.title) || text(info.name) || basename(command),
350
350
  version: text(info.version),
351
351
  sessionMemory: restoreMethod(capabilities) ? "native" : "none"
@@ -401,8 +401,8 @@ function acpDriver(options) {
401
401
  if (guest && options.guestMode === void 0 && !options.unguardedGuests) return { reply: OWNER_ONLY };
402
402
  const child = startAgent(command, args, cwd);
403
403
  let exit = /* @__PURE__ */ __name(() => void 0, "exit");
404
- const exited = new Promise((resolve3) => {
405
- exit = resolve3;
404
+ const exited = new Promise((resolve4) => {
405
+ exit = resolve4;
406
406
  });
407
407
  let killing;
408
408
  let stopping = false;
@@ -413,7 +413,7 @@ function acpDriver(options) {
413
413
  }, "close"), exited };
414
414
  running.set(turn.session, entry);
415
415
  const server = `${CANON}_${randomBytes(4).toString("hex")}`;
416
- return new Promise((resolve3, reject) => {
416
+ return new Promise((resolve4, reject) => {
417
417
  const messages = [];
418
418
  const calls = /* @__PURE__ */ new Map();
419
419
  let sessionId = "";
@@ -439,7 +439,7 @@ function acpDriver(options) {
439
439
  finish();
440
440
  if (turn.signal.aborted) reject(turn.signal.reason);
441
441
  else if (outcome instanceof Error) reject(outcome);
442
- else resolve3(outcome);
442
+ else resolve4(outcome);
443
443
  }, "settle");
444
444
  const stopGuest = /* @__PURE__ */ __name((what) => {
445
445
  link.notify("session/cancel", { sessionId });
@@ -532,10 +532,10 @@ ${text(item.newText)}`);
532
532
  child.once("exit", () => {
533
533
  if (stopping || guest || turn.signal.aborted) {
534
534
  signalAgent(child, "SIGTERM");
535
- ended = new Promise((resolve4) => {
535
+ ended = new Promise((resolve5) => {
536
536
  setTimeout(() => {
537
537
  signalAgent(child, "SIGKILL");
538
- resolve4();
538
+ resolve5();
539
539
  }, KILL_AFTER_MS);
540
540
  });
541
541
  }
@@ -789,8 +789,8 @@ function claudeDriver(options) {
789
789
  const resume = turn.state.get(SESSION2);
790
790
  const native = query({ prompt: turn.prompt, options: queryOptions(turn, resume) });
791
791
  let exit = /* @__PURE__ */ __name(() => void 0, "exit");
792
- const entry = { close: /* @__PURE__ */ __name(() => native.close(), "close"), exited: new Promise((resolve3) => {
793
- exit = resolve3;
792
+ const entry = { close: /* @__PURE__ */ __name(() => native.close(), "close"), exited: new Promise((resolve4) => {
793
+ exit = resolve4;
794
794
  }) };
795
795
  running.set(turn.session, entry);
796
796
  let closing;
@@ -803,7 +803,7 @@ function claudeDriver(options) {
803
803
  }, "interrupt");
804
804
  turn.signal.addEventListener("abort", interrupt, { once: true });
805
805
  const reader = liveReader(turn);
806
- return new Promise((resolve3, reject) => void (async () => {
806
+ return new Promise((resolve4, reject) => void (async () => {
807
807
  let session = resume;
808
808
  let started = false;
809
809
  let finished = false;
@@ -820,7 +820,7 @@ function claudeDriver(options) {
820
820
  closeLater();
821
821
  if (message2.subtype === "success" && !message2.is_error) {
822
822
  const reply = message2.result.trim() ? message2.result : reader.text();
823
- resolve3({ reply: reply.trim() ? reply : null });
823
+ resolve4({ reply: reply.trim() ? reply : null });
824
824
  } else {
825
825
  const detail = message2.subtype === "success" ? message2.result : message2.errors.join("; ");
826
826
  reject(failed(new Error(`Claude Code ended the turn with ${message2.subtype}${detail ? `: ${oneLine2(detail, 300)}` : ""}`)));
@@ -1112,14 +1112,14 @@ function codexDriver(options) {
1112
1112
  const cli = installedCli(codexPath);
1113
1113
  const child = spawn3(cli.file, [...cli.args, "app-server", "--listen", "stdio://"], { cwd, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
1114
1114
  let exit = /* @__PURE__ */ __name(() => void 0, "exit");
1115
- const exited = new Promise((resolve3) => {
1116
- exit = resolve3;
1115
+ const exited = new Promise((resolve4) => {
1116
+ exit = resolve4;
1117
1117
  });
1118
1118
  const entry = { close: /* @__PURE__ */ __name(() => {
1119
1119
  stopCli(child);
1120
1120
  }, "close"), exited };
1121
1121
  running.set(turn.session, entry);
1122
- return new Promise((resolve3, reject) => {
1122
+ return new Promise((resolve4, reject) => {
1123
1123
  const messages = /* @__PURE__ */ new Map();
1124
1124
  const items = /* @__PURE__ */ new Map();
1125
1125
  const completed = /* @__PURE__ */ new Map();
@@ -1145,7 +1145,7 @@ function codexDriver(options) {
1145
1145
  finish();
1146
1146
  if (turn.signal.aborted) reject(turn.signal.reason);
1147
1147
  else if (outcome instanceof Error) reject(outcome);
1148
- else resolve3(outcome);
1148
+ else resolve4(outcome);
1149
1149
  }, "settle");
1150
1150
  const conclude = /* @__PURE__ */ __name(() => {
1151
1151
  const ended = completed.get(nativeTurn);
@@ -1343,8 +1343,8 @@ function connectorDriver() {
1343
1343
  entry.confirmation = confirmation;
1344
1344
  const deadline = options.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
1345
1345
  let timer;
1346
- const late = new Promise((resolve3) => {
1347
- timer = setTimeout(resolve3, Math.max(0, deadline - Date.now()), "late");
1346
+ const late = new Promise((resolve4) => {
1347
+ timer = setTimeout(resolve4, Math.max(0, deadline - Date.now()), "late");
1348
1348
  });
1349
1349
  const confirmed = await Promise.race([confirmation, late]).finally(() => clearTimeout(timer));
1350
1350
  const state = entry.state;
@@ -1383,7 +1383,7 @@ function connectorDriver() {
1383
1383
  ])).digest("hex").slice(0, 40)}`;
1384
1384
  const input = fitInput(inputId, turn);
1385
1385
  if (!input) return Promise.reject(new Error(TOO_LARGE));
1386
- return new Promise((resolve3, reject) => {
1386
+ return new Promise((resolve4, reject) => {
1387
1387
  const entry = {
1388
1388
  ref: { inputId, conversationId: turn.input.conversationId, messageId: messages.at(-1).id, createdAt: messages.at(-1).createdAt },
1389
1389
  session: turn.session,
@@ -1393,9 +1393,9 @@ function connectorDriver() {
1393
1393
  finish: /* @__PURE__ */ __name((result) => {
1394
1394
  turn.signal.removeEventListener("abort", onAbort);
1395
1395
  if (result instanceof Error) reject(result);
1396
- else if (result.outcome === "reply") resolve3({ reply: result.text });
1396
+ else if (result.outcome === "reply") resolve4({ reply: result.text });
1397
1397
  else if (result.outcome === "failed") reject(new Error(FAILED));
1398
- else resolve3({ reply: null });
1398
+ else resolve4({ reply: null });
1399
1399
  }, "finish")
1400
1400
  };
1401
1401
  const onAbort = /* @__PURE__ */ __name(() => withdraw(entry, turn.signal.reason), "onAbort");
@@ -1531,7 +1531,7 @@ var answered = /* @__PURE__ */ __name((kind, record) => record.status !== "sendi
1531
1531
  async function awaitErrand(directory, command, ms) {
1532
1532
  const journal = errandJournal(directory);
1533
1533
  let record;
1534
- for (const deadline = Date.now() + ms; Date.now() < deadline; await new Promise((resolve3) => setTimeout(resolve3, 200))) {
1534
+ for (const deadline = Date.now() + ms; Date.now() < deadline; await new Promise((resolve4) => setTimeout(resolve4, 200))) {
1535
1535
  record = journal.get(command.id);
1536
1536
  if (record && answered(command.kind, record)) break;
1537
1537
  }
@@ -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);
@@ -2161,9 +2161,9 @@ function managedMlsDevice(options) {
2161
2161
  }
2162
2162
  __name(device, "device");
2163
2163
  const custody2 = {
2164
- prepareProtected: /* @__PURE__ */ __name((context, ...rest) => device(context, false).then((open4) => open4.prepareProtected(context, ...rest)), "prepareProtected"),
2165
- prepareProtectedKeyPackage: /* @__PURE__ */ __name((context, ...rest) => device(context, false).then((open4) => open4.prepareProtectedKeyPackage(context, ...rest)), "prepareProtectedKeyPackage"),
2166
- prepareProtectedGroup: /* @__PURE__ */ __name((identity, ...rest) => device(identity, false).then((open4) => open4.prepareProtectedGroup(identity, ...rest)), "prepareProtectedGroup")
2164
+ prepareProtected: /* @__PURE__ */ __name((context, ...rest) => device(context, false).then((open5) => open5.prepareProtected(context, ...rest)), "prepareProtected"),
2165
+ prepareProtectedKeyPackage: /* @__PURE__ */ __name((context, ...rest) => device(context, false).then((open5) => open5.prepareProtectedKeyPackage(context, ...rest)), "prepareProtectedKeyPackage"),
2166
+ prepareProtectedGroup: /* @__PURE__ */ __name((identity, ...rest) => device(identity, false).then((open5) => open5.prepareProtectedGroup(identity, ...rest)), "prepareProtectedGroup")
2167
2167
  };
2168
2168
  return {
2169
2169
  configuration,
@@ -2181,7 +2181,7 @@ function managedMlsDevice(options) {
2181
2181
  return false;
2182
2182
  },
2183
2183
  async close() {
2184
- await Promise.allSettled([...devices.values()].map((open4) => open4.then((custody3) => custody3.close())));
2184
+ await Promise.allSettled([...devices.values()].map((open5) => open5.then((custody3) => custody3.close())));
2185
2185
  }
2186
2186
  };
2187
2187
  }
@@ -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,19 +2729,19 @@ 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");
2688
- var pause = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => {
2689
- setTimeout(resolve3, ms).unref?.();
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");
2736
+ var pause = /* @__PURE__ */ __name((ms) => new Promise((resolve4) => {
2737
+ setTimeout(resolve4, ms).unref?.();
2690
2738
  }), "pause");
2691
- var pauseUnless = /* @__PURE__ */ __name((ms, signal) => new Promise((resolve3) => {
2692
- if (signal.aborted) return resolve3();
2739
+ var pauseUnless = /* @__PURE__ */ __name((ms, signal) => new Promise((resolve4) => {
2740
+ if (signal.aborted) return resolve4();
2693
2741
  const done = /* @__PURE__ */ __name(() => {
2694
2742
  clearTimeout(timer);
2695
2743
  signal.removeEventListener("abort", done);
2696
- resolve3();
2744
+ resolve4();
2697
2745
  }, "done");
2698
2746
  const timer = setTimeout(done, ms);
2699
2747
  timer.unref?.();
@@ -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");
@@ -2986,14 +3034,14 @@ function createMlsAsks(options, approvalScope2) {
2986
3034
  }
2987
3035
  }
2988
3036
  if (signal.aborted) throw ended();
2989
- await new Promise((resolve3) => {
3037
+ await new Promise((resolve4) => {
2990
3038
  const timer = setTimeout(done, Math.max(0, Math.min(pollMs, expiresAtMs - Date.now())));
2991
3039
  timer.unref?.();
2992
3040
  function done() {
2993
3041
  clearTimeout(timer);
2994
3042
  signal.removeEventListener("abort", done);
2995
3043
  card.wake = () => void 0;
2996
- resolve3();
3044
+ resolve4();
2997
3045
  }
2998
3046
  __name(done, "done");
2999
3047
  card.wake = done;
@@ -3004,12 +3052,12 @@ function createMlsAsks(options, approvalScope2) {
3004
3052
  __name(wait, "wait");
3005
3053
  function bounded(card, work) {
3006
3054
  const { expiresAtMs, signal } = card.input;
3007
- return new Promise((resolve3, reject) => {
3008
- const stop = /* @__PURE__ */ __name(() => resolve3(void 0), "stop");
3055
+ return new Promise((resolve4, reject) => {
3056
+ const stop = /* @__PURE__ */ __name(() => resolve4(void 0), "stop");
3009
3057
  const timer = setTimeout(stop, Math.max(0, expiresAtMs - Date.now()));
3010
3058
  timer.unref?.();
3011
3059
  signal.addEventListener("abort", stop, { once: true });
3012
- work.then(resolve3, reject).finally(() => {
3060
+ work.then(resolve4, reject).finally(() => {
3013
3061
  clearTimeout(timer);
3014
3062
  signal.removeEventListener("abort", stop);
3015
3063
  });
@@ -3058,11 +3106,11 @@ function createMlsAsks(options, approvalScope2) {
3058
3106
  throw error;
3059
3107
  });
3060
3108
  })();
3061
- return new Promise((resolve3, reject) => {
3109
+ return new Promise((resolve4, reject) => {
3062
3110
  const stop = /* @__PURE__ */ __name(() => reject(turnEnded()), "stop");
3063
3111
  if (signal.aborted) stop();
3064
3112
  else signal.addEventListener("abort", stop, { once: true });
3065
- work.then(resolve3, reject).finally(() => signal.removeEventListener("abort", stop));
3113
+ work.then(resolve4, reject).finally(() => signal.removeEventListener("abort", stop));
3066
3114
  });
3067
3115
  },
3068
3116
  wake,
@@ -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 {
@@ -3158,9 +3206,9 @@ async function startToolServer() {
3158
3206
  if (!response.headersSent) response.writeHead(500).end();
3159
3207
  });
3160
3208
  });
3161
- await new Promise((resolve3, reject) => {
3209
+ await new Promise((resolve4, reject) => {
3162
3210
  http.once("error", reject);
3163
- http.listen(0, "127.0.0.1", resolve3);
3211
+ http.listen(0, "127.0.0.1", resolve4);
3164
3212
  });
3165
3213
  const { port } = http.address();
3166
3214
  return {
@@ -3171,10 +3219,10 @@ async function startToolServer() {
3171
3219
  turns.delete(token);
3172
3220
  }, "release") };
3173
3221
  },
3174
- close: /* @__PURE__ */ __name(() => new Promise((resolve3) => {
3222
+ close: /* @__PURE__ */ __name(() => new Promise((resolve4) => {
3175
3223
  turns.clear();
3176
3224
  http.closeAllConnections();
3177
- http.close(() => resolve3());
3225
+ http.close(() => resolve4());
3178
3226
  }), "close")
3179
3227
  };
3180
3228
  }
@@ -3182,7 +3230,42 @@ __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;
3266
+ var OUTBOUND_UNANSWERED = ["queued", "sending", "deferred", "pending"];
3267
+ var INBOUND_UNSETTLED = ["observed", "deferred", "offered"];
3268
+ var ACTIVITY_COUNT_LIMIT = 1e3;
3186
3269
  var CLAIM_RETRY_MS = 250;
3187
3270
  var CLAIM_PATIENCE_MS = 2e4;
3188
3271
  var HISTORY_WINDOW = 20;
@@ -3243,19 +3326,19 @@ function answerText(answer) {
3243
3326
  return answer.text?.trim() ? answer.text : "They answered with an empty message.";
3244
3327
  }
3245
3328
  __name(answerText, "answerText");
3246
- 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");
3247
3330
  var toolText = /* @__PURE__ */ __name((text3, isError = false) => ({ content: [{ type: "text", text: text3 }], ...isError ? { isError } : {} }), "toolText");
3248
3331
  var turnEnded2 = /* @__PURE__ */ __name(() => new DOMException("This turn has ended", "AbortError"), "turnEnded");
3249
3332
  function within(promise, ms) {
3250
3333
  let timer;
3251
- const expiry = new Promise((resolve3) => {
3252
- timer = setTimeout(resolve3, ms, void 0);
3334
+ const expiry = new Promise((resolve4) => {
3335
+ timer = setTimeout(resolve4, ms, void 0);
3253
3336
  });
3254
3337
  return Promise.race([promise, expiry]).finally(() => clearTimeout(timer));
3255
3338
  }
3256
3339
  __name(within, "within");
3257
- var pause2 = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => {
3258
- setTimeout(resolve3, ms).unref?.();
3340
+ var pause2 = /* @__PURE__ */ __name((ms) => new Promise((resolve4) => {
3341
+ setTimeout(resolve4, ms).unref?.();
3259
3342
  }), "pause");
3260
3343
  function agentRun(turns) {
3261
3344
  const history = turns.map(({ senderId, senderType }) => senderType === "ai_agent" ? { senderId, senderType, metadata: { turnSemantics: "turn_complete" } } : { senderId, senderType: "human" });
@@ -3344,7 +3427,7 @@ function approvalScope(request) {
3344
3427
  if (request.scopeKey !== void 0 && /^[A-Za-z0-9_:-]{1,64}$/.test(request.scopeKey)) return request.scopeKey;
3345
3428
  const key = request.scopeKey ?? request.title;
3346
3429
  const slug = key.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 40) || "approval";
3347
- 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)}`;
3348
3431
  }
3349
3432
  __name(approvalScope, "approvalScope");
3350
3433
  var MAX_GROUP_PARTS = 100;
@@ -3432,6 +3515,11 @@ async function createRunner(options) {
3432
3515
  const maxTurns = driver.capabilities.maxConcurrentTurns ?? 8;
3433
3516
  const agentTurnCap = options.agentTurnCap ?? 6;
3434
3517
  if (!Number.isSafeInteger(agentTurnCap) || agentTurnCap < 0 || agentTurnCap > 1e3) throw new Error("agentTurnCap must be a whole number from 0 to 1000");
3518
+ const { idleTimeoutMs, onIdle } = options;
3519
+ if (idleTimeoutMs === void 0 !== (onIdle === void 0)) throw new Error("idleTimeoutMs and onIdle go together");
3520
+ if (idleTimeoutMs !== void 0 && (!Number.isSafeInteger(idleTimeoutMs) || idleTimeoutMs < 1e3)) {
3521
+ throw new Error("idleTimeoutMs must be a whole number of milliseconds, at least 1000");
3522
+ }
3435
3523
  const names = /* @__PURE__ */ __name((list, what) => {
3436
3524
  if (list !== void 0 && (!Array.isArray(list) || list.some((name) => typeof name !== "string" || !name.trim() || name.length > 200))) {
3437
3525
  throw new Error(`${what} must be a list of non-empty names`);
@@ -3449,7 +3537,6 @@ async function createRunner(options) {
3449
3537
  let endpoint;
3450
3538
  let inbound;
3451
3539
  let heartbeat;
3452
- let journal;
3453
3540
  let agentId = "";
3454
3541
  let scope = "";
3455
3542
  let stopping;
@@ -3473,6 +3560,7 @@ async function createRunner(options) {
3473
3560
  const ruleNotes = /* @__PURE__ */ new Set();
3474
3561
  const guards = /* @__PURE__ */ new Map();
3475
3562
  const relaySends = /* @__PURE__ */ new Map();
3563
+ const proactive = /* @__PURE__ */ new Map();
3476
3564
  let errands;
3477
3565
  let errandInbox2;
3478
3566
  let errandScope = "";
@@ -3484,13 +3572,90 @@ async function createRunner(options) {
3484
3572
  const halt = new AbortController();
3485
3573
  const untilHalt = /* @__PURE__ */ __name((work) => {
3486
3574
  let stop = /* @__PURE__ */ __name(() => void 0, "stop");
3487
- const halted = new Promise((resolve3) => {
3488
- stop = /* @__PURE__ */ __name(() => resolve3("pending"), "stop");
3575
+ const halted = new Promise((resolve4) => {
3576
+ stop = /* @__PURE__ */ __name(() => resolve4("pending"), "stop");
3489
3577
  if (halt.signal.aborted) stop();
3490
3578
  else halt.signal.addEventListener("abort", stop, { once: true });
3491
3579
  });
3492
3580
  return Promise.race([work, halted]).finally(() => halt.signal.removeEventListener("abort", stop));
3493
3581
  }, "untilHalt");
3582
+ let lastActiveAt = Date.now();
3583
+ let idleCalled = false;
3584
+ let offering = 0;
3585
+ let calls = 0;
3586
+ const touch = /* @__PURE__ */ __name(() => {
3587
+ lastActiveAt = Date.now();
3588
+ idleCalled = false;
3589
+ }, "touch");
3590
+ async function activeCall(work) {
3591
+ calls += 1;
3592
+ touch();
3593
+ try {
3594
+ return await work();
3595
+ } finally {
3596
+ calls -= 1;
3597
+ touch();
3598
+ }
3599
+ }
3600
+ __name(activeCall, "activeCall");
3601
+ function ownActivity() {
3602
+ let turns = running.size;
3603
+ let queued2 = offering;
3604
+ for (const current of rooms.values()) {
3605
+ queued2 += current.queue.length;
3606
+ if (current.recovering) turns += 1;
3607
+ if (current.resetting) turns += 1;
3608
+ }
3609
+ let errandsWaiting = 0;
3610
+ for (const record of errandsById.values()) if (record.status === "pending") errandsWaiting += 1;
3611
+ return { turns, queued: queued2, errands: errandsWaiting, sending: posting.size + calls };
3612
+ }
3613
+ __name(ownActivity, "ownActivity");
3614
+ async function activity() {
3615
+ if (stopping) throw new Error("The runner is stopping");
3616
+ if (!inbound) throw new Error("Start the runner first");
3617
+ const [operations, records] = await Promise.all([
3618
+ endpoint.listOperations({ statuses: [...OUTBOUND_UNANSWERED], limit: ACTIVITY_COUNT_LIMIT }),
3619
+ endpoint.listInbound({ states: [...INBOUND_UNSETTLED], limit: ACTIVITY_COUNT_LIMIT })
3620
+ ]);
3621
+ const own = ownActivity();
3622
+ const unsettled = records.filter(({ event, deleted, externalClaim }) => event.kind === "message.created" && !deleted && !externalClaim).length;
3623
+ const inputs = Math.max(own.queued, unsettled);
3624
+ const outbound = own.sending + operations.length;
3625
+ const idle = own.turns === 0 && inputs === 0 && own.errands === 0 && outbound === 0;
3626
+ if (!idle) touch();
3627
+ return { idle, lastActiveAt, turns: own.turns, inputs, errands: own.errands, outbound };
3628
+ }
3629
+ __name(activity, "activity");
3630
+ function watchIdle() {
3631
+ if (idleTimeoutMs === void 0) return;
3632
+ let checking = false;
3633
+ const check = /* @__PURE__ */ __name(async () => {
3634
+ if (checking || stopping || idleCalled) return;
3635
+ const own = ownActivity();
3636
+ if (own.turns || own.queued || own.errands || own.sending) return touch();
3637
+ checking = true;
3638
+ try {
3639
+ const now = await activity();
3640
+ if (now.idle && !stopping && !idleCalled && Date.now() - lastActiveAt >= idleTimeoutMs) {
3641
+ idleCalled = true;
3642
+ try {
3643
+ onIdle(now);
3644
+ } catch (error) {
3645
+ warn("onIdle")(error);
3646
+ }
3647
+ }
3648
+ } catch (error) {
3649
+ if (!stopping) warn("idle check")(error);
3650
+ } finally {
3651
+ checking = false;
3652
+ }
3653
+ }, "check");
3654
+ const timer = setInterval(() => void check(), Math.min(3e4, Math.max(1e3, Math.floor(idleTimeoutMs / 4))));
3655
+ timer.unref?.();
3656
+ detach.push(() => clearInterval(timer));
3657
+ }
3658
+ __name(watchIdle, "watchIdle");
3494
3659
  const room = /* @__PURE__ */ __name((id) => {
3495
3660
  let found = rooms.get(id);
3496
3661
  if (!found) rooms.set(id, found = { id, queue: [], errands: [], controls: Promise.resolve(), lane: Promise.resolve() });
@@ -3499,7 +3664,7 @@ async function createRunner(options) {
3499
3664
  const dispose = /* @__PURE__ */ __name((inputs, kind, reason) => Promise.allSettled(inputs.map((input) => input.handle[kind](reason))).then(() => void 0), "dispose");
3500
3665
  const settle = /* @__PURE__ */ __name(async (ids, reason) => (await Promise.allSettled(ids.map((id) => endpoint.setInboundState(id, "settled", reason)))).every(({ status }) => status === "fulfilled"), "settle");
3501
3666
  const queued = /* @__PURE__ */ __name((turn) => turn.inputs.filter((input) => input.handle.phase === "queued"), "queued");
3502
- 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");
3503
3668
  const livePublisher = /* @__PURE__ */ __name((current, turn) => createLivePublisher({
3504
3669
  client,
3505
3670
  conversationId: current.id,
@@ -3508,7 +3673,7 @@ async function createRunner(options) {
3508
3673
  current.lane = current.lane.then(publish2).catch(() => void 0);
3509
3674
  }, "enqueue")
3510
3675
  }), "livePublisher");
3511
- 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");
3512
3677
  const resetSession = /* @__PURE__ */ __name(async (session) => {
3513
3678
  rmSync2(stateFile(session), { force: true });
3514
3679
  mlsAsks?.endSession(session);
@@ -3569,9 +3734,21 @@ ${error instanceof Error ? error.message : String(error)}`;
3569
3734
  }
3570
3735
  __name(progress, "progress");
3571
3736
  async function offer(event) {
3737
+ offering += 1;
3738
+ touch();
3739
+ try {
3740
+ await take(event);
3741
+ } finally {
3742
+ offering -= 1;
3743
+ touch();
3744
+ }
3745
+ }
3746
+ __name(offer, "offer");
3747
+ async function take(event) {
3572
3748
  const handle = await endpoint.acquireInbound(event.id);
3573
3749
  if (!handle) return;
3574
3750
  const roomId = event.data.conversationId ?? "";
3751
+ const clears = rooms.get(roomId)?.clears;
3575
3752
  const input = await admitInput(event, handle).catch(async (error) => {
3576
3753
  deferredNote(roomId)(error);
3577
3754
  await handle.release("not-admitted-yet");
@@ -3580,11 +3757,12 @@ ${error instanceof Error ? error.message : String(error)}`;
3580
3757
  if (!input) return;
3581
3758
  if (stopping) return void await handle.release("runner-stopping");
3582
3759
  const target = room(input.payload.conversationId);
3760
+ if (target.clears !== clears) return void await dispose([input], "cancel", "stop-and-clear");
3583
3761
  target.queue.push(input);
3584
3762
  requeue(target);
3585
3763
  pump();
3586
3764
  }
3587
- __name(offer, "offer");
3765
+ __name(take, "take");
3588
3766
  async function admitInput(event, handle) {
3589
3767
  const payload = event.data;
3590
3768
  const { conversationId } = payload;
@@ -3735,11 +3913,13 @@ ${decision.reasonCode}`;
3735
3913
  turn.live.end("ended");
3736
3914
  if (candidate.turn === turn) candidate.turn = void 0;
3737
3915
  running.delete(done);
3916
+ touch();
3738
3917
  rooms.delete(candidate.id);
3739
3918
  rooms.set(candidate.id, candidate);
3740
3919
  pump();
3741
3920
  });
3742
3921
  running.add(done);
3922
+ touch();
3743
3923
  }
3744
3924
  while (localErrands.length && !stopping && running.size < maxTurns) startErrand(void 0, localErrands.shift());
3745
3925
  }
@@ -3961,7 +4141,7 @@ ${decision.reasonCode}`;
3961
4141
  return check;
3962
4142
  }
3963
4143
  __name(reaudience, "reaudience");
3964
- 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");
3965
4145
  async function materialize(current, turn) {
3966
4146
  if (driver.capabilities.remote) return;
3967
4147
  for (const { payload: { message: message2 } } of turn.inputs) {
@@ -4012,7 +4192,7 @@ ${decision.reasonCode}`;
4012
4192
  ask_question: /* @__PURE__ */ __name(async (args) => {
4013
4193
  const question = questionOf(args);
4014
4194
  if (typeof question === "string") return { result: toolText(question, true) };
4015
- const key = sha2563(JSON.stringify(question));
4195
+ const key = sha2564(JSON.stringify(question));
4016
4196
  const waiting = questions.get(key);
4017
4197
  const entry = waiting?.waiting ? waiting : { id: `canon:ask_question:${asked += 1}`, waiting: true };
4018
4198
  questions.set(key, entry);
@@ -4095,7 +4275,7 @@ ${decision.reasonCode}`;
4095
4275
  }
4096
4276
  }
4097
4277
  if (errand && "contact" in errand.target && (command.action === "start_conversation" || command.action === "start_direct") && command.principalId === errand.target.contact) {
4098
- const result = await client.communicate({ ...command, action: "start_conversation", selection: { mode: "new" } });
4278
+ const result = await startConversation({ ...command, action: "start_conversation", selection: { mode: "new" } });
4099
4279
  if (result.status === "messaged") reach(turn, result.conversationId);
4100
4280
  return toolText(`communicate: ${result.status}
4101
4281
  ${JSON.stringify(result, null, 2)}`);
@@ -4111,20 +4291,18 @@ ${JSON.stringify(result, null, 2)}`);
4111
4291
  turn.said.set(messageId, sending = (async () => {
4112
4292
  const mls2 = audience ?? (await currentAudience(roomId)).audience;
4113
4293
  const record = {
4114
- version: 1,
4115
4294
  turnId: messageId,
4116
4295
  conversationId: roomId,
4117
4296
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4118
- audience: null,
4119
4297
  inputs: [],
4120
4298
  text: text3,
4121
4299
  options: { messageId },
4122
4300
  notice: false,
4123
4301
  mls: mls2
4124
4302
  };
4125
- saveOutput(record);
4303
+ await saveOutput(record);
4126
4304
  const sent2 = await publish(record, turn.retire.signal);
4127
- if (sent2 !== "pending") journal.remove(messageId);
4305
+ if (sent2 !== "pending") await endpoint.finishCompletion(messageId);
4128
4306
  else if (turn.retire.signal.aborted) await stopOutput(record);
4129
4307
  return sent2;
4130
4308
  })().catch((error) => {
@@ -4170,9 +4348,9 @@ ${JSON.stringify(result, null, 2)}`);
4170
4348
  async function outcomeOf(turn, work) {
4171
4349
  const signal = turn.controller.signal;
4172
4350
  const settled = work.then((result) => ({ result }), (error) => ({ error }));
4173
- const aborted = new Promise((resolve3) => {
4174
- if (signal.aborted) resolve3(void 0);
4175
- else signal.addEventListener("abort", () => resolve3(void 0), { once: true });
4351
+ const aborted = new Promise((resolve4) => {
4352
+ if (signal.aborted) resolve4(void 0);
4353
+ else signal.addEventListener("abort", () => resolve4(void 0), { once: true });
4176
4354
  });
4177
4355
  const first = await Promise.race([settled, aborted]);
4178
4356
  if (first && !signal.aborted) return first;
@@ -4208,11 +4386,9 @@ ${JSON.stringify(result, null, 2)}`);
4208
4386
  const kind = failed ? "failed" : "final";
4209
4387
  const replyAuthority = await replyGrant(current.id, turn.inputs.at(-1).payload.message.id, stopped);
4210
4388
  const record = {
4211
- version: 1,
4212
4389
  turnId: turn.id,
4213
4390
  conversationId: current.id,
4214
4391
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4215
- audience: null,
4216
4392
  inputs: turn.inputs.map((input) => input.id),
4217
4393
  text: text3,
4218
4394
  ...items.length ? { items } : {},
@@ -4220,7 +4396,7 @@ ${JSON.stringify(result, null, 2)}`);
4220
4396
  notice: failed,
4221
4397
  mls: audience
4222
4398
  };
4223
- saveOutput(record);
4399
+ await saveOutput(record);
4224
4400
  sent = await publish(record, turn.retire.signal);
4225
4401
  if (sent === "pending" && !stopped()) return turn.live.end(turn.reason && STOPS.includes(turn.reason) ? "stopped" : "ended");
4226
4402
  if (sent === "pending") await stopOutput(record);
@@ -4234,7 +4410,7 @@ ${JSON.stringify(result, null, 2)}`);
4234
4410
  if (current.session === turn.session) current.session = void 0;
4235
4411
  }
4236
4412
  const reason = stopped() ? turn.reason : silent ? "silent" : answered2 ? "answered" : "failed";
4237
- 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"));
4238
4414
  }
4239
4415
  __name(deliver, "deliver");
4240
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");
@@ -4264,11 +4440,16 @@ ${JSON.stringify(result, null, 2)}`);
4264
4440
  }
4265
4441
  __name(send, "send");
4266
4442
  const partsOf = /* @__PURE__ */ __name((text3, options2) => encryptedParts(text3, options2.replyAuthority ? MAX_REPLY_PARTS : MAX_GROUP_PARTS), "partsOf");
4267
- function saveOutput(record) {
4443
+ async function saveOutput(record) {
4268
4444
  record.parts = partsOf(record.text, record.options).length;
4269
- journal.save(record);
4445
+ await endpoint.saveCompletion(toCompletion(record));
4270
4446
  }
4271
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");
4272
4453
  function encryptedMessages(mls2, text3, options2, record) {
4273
4454
  const all = record && record.parts === void 0 ? [fitEncrypted(text3)] : partsOf(text3, options2);
4274
4455
  return all.slice(0, record?.parts).map((part, index) => ({
@@ -4284,11 +4465,9 @@ ${JSON.stringify(result, null, 2)}`);
4284
4465
  const status = (await endpoint.getOperation(encryptedOperation(record.mls, messageId)))?.status;
4285
4466
  if (status && status !== "succeeded" && status !== "rejected") queued2 = index + 1;
4286
4467
  }
4287
- if (queued2) {
4288
- const { items: _items, errand: _errand, ...rest } = record;
4289
- journal.save({ ...rest, turnId: `${record.turnId}:stopped`, inputs: [], parts: queued2 });
4290
- }
4291
- 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);
4292
4471
  }
4293
4472
  __name(stopOutput, "stopOutput");
4294
4473
  async function sendEncrypted(audience, messageId, text3, signal, replyAuthority) {
@@ -4384,7 +4563,7 @@ ${JSON.stringify(result, null, 2)}`);
4384
4563
  return { items, refused: refused2 };
4385
4564
  }
4386
4565
  __name(admitItems, "admitItems");
4387
- 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");
4388
4567
  function forgetGuard(audience, messageId, sent) {
4389
4568
  if (sent === "pending") return;
4390
4569
  const operationId = encryptedOperation(audience, messageId);
@@ -4414,8 +4593,8 @@ ${JSON.stringify(result, null, 2)}`);
4414
4593
  ...turn.facts ? { roomFacts: turn.facts } : {}
4415
4594
  });
4416
4595
  const line = /* @__PURE__ */ __name((message2, mark = "") => promptLine(message2, message2.senderId === agentId ? "You" : message2.senderName ?? message2.senderId, turn.media.get(message2.id), mark), "line");
4417
- const open4 = /* @__PURE__ */ __name(() => turn.open && !turn.controller.signal.aborted, "open");
4418
- const live = /* @__PURE__ */ __name(() => open4() && !turn.silenced, "live");
4596
+ const open5 = /* @__PURE__ */ __name(() => turn.open && !turn.controller.signal.aborted, "open");
4597
+ const live = /* @__PURE__ */ __name(() => open5() && !turn.silenced, "live");
4419
4598
  return {
4420
4599
  session: turn.session,
4421
4600
  // §12.6: an errand turn's prompt is its errand; the owner's message is only its input's record.
@@ -4437,18 +4616,18 @@ ${JSON.stringify(result, null, 2)}`);
4437
4616
  if (live()) progress(turn);
4438
4617
  }, "onActivity"),
4439
4618
  ask: /* @__PURE__ */ __name((request) => live() ? ask(roomId, turn, input.sender, request) : Promise.reject(turnEnded2()), "ask"),
4440
- state: driverState(turn.session, open4),
4619
+ state: driverState(turn.session, open5),
4441
4620
  tools: tools2,
4442
- confirmPickup: /* @__PURE__ */ __name(() => confirmPickup(roomId, turn, open4), "confirmPickup")
4621
+ confirmPickup: /* @__PURE__ */ __name(() => confirmPickup(roomId, turn, open5), "confirmPickup")
4443
4622
  };
4444
4623
  }
4445
4624
  __name(buildTurn, "buildTurn");
4446
- async function confirmPickup(roomId, turn, open4) {
4447
- if (!open4()) throw turnEnded2();
4625
+ async function confirmPickup(roomId, turn, open5) {
4626
+ if (!open5()) throw turnEnded2();
4448
4627
  turn.pickingUp = true;
4449
4628
  try {
4450
4629
  const current = await client.getConversation(roomId);
4451
- if (!open4()) throw turnEnded2();
4630
+ if (!open5()) throw turnEnded2();
4452
4631
  if (!current?.memberIds?.includes(agentId)) {
4453
4632
  loseMembership(room(roomId));
4454
4633
  throw new Error("The agent is no longer a member of this conversation");
@@ -4460,7 +4639,7 @@ ${JSON.stringify(result, null, 2)}`);
4460
4639
  }
4461
4640
  throw new Error("Who can read this encrypted conversation changed");
4462
4641
  }
4463
- if (!open4()) throw turnEnded2();
4642
+ if (!open5()) throw turnEnded2();
4464
4643
  if (turn.pickUp) await turn.pickUp();
4465
4644
  progress(turn);
4466
4645
  } finally {
@@ -4522,7 +4701,7 @@ ${JSON.stringify(result, null, 2)}`);
4522
4701
  const file = stateFile(session);
4523
4702
  let data = {};
4524
4703
  try {
4525
- data = JSON.parse(readFileSync6(file, "utf8"));
4704
+ data = JSON.parse(readFileSync7(file, "utf8"));
4526
4705
  } catch {
4527
4706
  }
4528
4707
  const save = /* @__PURE__ */ __name((next) => {
@@ -4552,9 +4731,13 @@ ${JSON.stringify(result, null, 2)}`);
4552
4731
  async function control(current) {
4553
4732
  const consumed = await client.consumeRuntimeSignal(current.id);
4554
4733
  if (consumed.status !== "signal") return;
4734
+ touch();
4555
4735
  const signal = consumed.signal;
4556
4736
  const turn = current.turn;
4557
- if (signal === "stop_and_drop" || signal === "new_session") void dispose(current.queue.splice(0), "cancel", "stop-and-clear");
4737
+ if (signal === "stop_and_drop" || signal === "new_session") {
4738
+ current.clears = (current.clears ?? 0) + 1;
4739
+ void dispose(current.queue.splice(0), "cancel", "stop-and-clear");
4740
+ }
4558
4741
  if (turn) {
4559
4742
  void dispose(turn.inputs, "cancel", "stopped-before-submit");
4560
4743
  abort(turn, signal === "interrupt" ? "stop" : signal === "stop_and_drop" ? "drop" : "new_session");
@@ -4568,6 +4751,7 @@ ${JSON.stringify(result, null, 2)}`);
4568
4751
  const reset = Promise.all([...sessions].map((session) => resetSession(session).catch(warn("session reset")))).then(() => void 0);
4569
4752
  await (current.resetting = reset.finally(() => {
4570
4753
  current.resetting = void 0;
4754
+ touch();
4571
4755
  pump();
4572
4756
  }));
4573
4757
  }
@@ -4588,9 +4772,8 @@ ${JSON.stringify(result, null, 2)}`);
4588
4772
  __name(scheduleControl, "scheduleControl");
4589
4773
  async function recover() {
4590
4774
  await mlsAsks?.recover().catch(warn("pending encrypted cards"));
4591
- const records = journal.list();
4592
- const answered2 = new Set(records.flatMap((record) => record.inputs));
4593
- 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);
4594
4777
  for (const roomId of /* @__PURE__ */ new Set([...records.map((record) => record.conversationId), ...orphans.map(({ event }) => event.conversationId)])) {
4595
4778
  const target = room(roomId);
4596
4779
  const work = (async () => {
@@ -4611,6 +4794,7 @@ ${JSON.stringify(result, null, 2)}`);
4611
4794
  })().catch(warn("restart recovery"));
4612
4795
  target.recovering = work.finally(() => {
4613
4796
  target.recovering = void 0;
4797
+ touch();
4614
4798
  pump();
4615
4799
  });
4616
4800
  }
@@ -4619,14 +4803,14 @@ ${JSON.stringify(result, null, 2)}`);
4619
4803
  async function redeliver(record) {
4620
4804
  if (!record.mls) {
4621
4805
  if (record.errand) return finishErrandOutput(record, "refused", "its conversation is not encrypted");
4622
- if (await settle(record.inputs, "plaintext-output-dropped")) journal.remove(record.turnId);
4623
- return;
4806
+ return endpoint.finishCompletion(record.turnId, { inputIds: record.inputs, reason: "plaintext-output-dropped" });
4624
4807
  }
4625
4808
  if (!mlsOn) return refuse(record.conversationId, "no-device");
4626
4809
  const sent = await publish(record);
4627
4810
  if (sent === "pending") return;
4628
4811
  if (record.errand) return finishErrandOutput(record, sent);
4629
- 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" });
4630
4814
  }
4631
4815
  __name(redeliver, "redeliver");
4632
4816
  function pendingNotes(roomId) {
@@ -4718,27 +4902,18 @@ ${JSON.stringify(result, null, 2)}`);
4718
4902
  log(`Errand ${id} (tell) kept for the next turn in ${conversationId}.`);
4719
4903
  return true;
4720
4904
  }
4721
- const turnId2 = `turn-${sha2563(JSON.stringify(["canon.runner.errand", id])).slice(0, 32)}`;
4722
- const record = {
4723
- version: 1,
4724
- turnId: turnId2,
4725
- conversationId,
4726
- createdAt,
4727
- audience: null,
4728
- inputs: [],
4729
- text: parsed.text,
4730
- options: { messageId: keyed().text("final", conversationId, turnId2) },
4731
- notice: false,
4732
- errand: id,
4733
- mls: target.mls
4734
- };
4735
- 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 });
4736
4907
  errands.put({ ...base, status: "sending", updatedAt });
4737
- const delivery = publish(record).then((sent) => {
4738
- if (sent !== "pending") finishErrandOutput(record, sent);
4739
- }, warn(`errand ${id}`));
4908
+ const delivery = publish(record).then(async (sent) => {
4909
+ if (sent !== "pending") await finishErrandOutput(record, sent);
4910
+ }).catch(warn(`errand ${id}`));
4740
4911
  posting.add(delivery);
4741
- void delivery.finally(() => posting.delete(delivery));
4912
+ touch();
4913
+ void delivery.finally(() => {
4914
+ posting.delete(delivery);
4915
+ touch();
4916
+ });
4742
4917
  return true;
4743
4918
  }
4744
4919
  __name(takeErrand, "takeErrand");
@@ -4753,10 +4928,12 @@ ${JSON.stringify(result, null, 2)}`);
4753
4928
  const mode = await endpoint.getConversationContentMode(roomId).catch(missing);
4754
4929
  if (mode === "plaintext") return "it is not encrypted, and the agent takes part only in encrypted conversations";
4755
4930
  if (mode !== "mls-dev-text" || !mlsOn) return "it is an encrypted conversation this runner cannot take part in";
4756
- 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) };
4757
4934
  }
4758
4935
  __name(errandRoom, "errandRoom");
4759
- function finishErrandOutput(record, sent, reason = "Canon did not accept it") {
4936
+ async function finishErrandOutput(record, sent, reason = "Canon did not accept it") {
4760
4937
  const id = record.errand;
4761
4938
  const known = latest(id) ?? { version: 1, id, kind: "post", conversationId: record.conversationId, createdAt: record.createdAt };
4762
4939
  const ok = sent === "delivered";
@@ -4767,9 +4944,108 @@ ${JSON.stringify(result, null, 2)}`);
4767
4944
  errands?.put({ ...known, status: ok ? "sent" : "refused", ...ok ? {} : { reason }, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
4768
4945
  log(`Errand ${id} (post) ${ok ? "sent" : `refused: ${reason}`} in ${record.conversationId}.`);
4769
4946
  }
4770
- journal.remove(record.turnId);
4947
+ await endpoint.finishCompletion(record.turnId);
4771
4948
  }
4772
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");
4773
5049
  const latest = /* @__PURE__ */ __name((id) => errandsById.get(id) ?? errands?.get(id), "latest");
4774
5050
  const openErrand = /* @__PURE__ */ __name((record) => record.kind === "errand" && !!record.conversationId && !["pending", "cancelled", "expired", "refused", "rejected"].includes(record.status), "openErrand");
4775
5051
  const aliveErrand = /* @__PURE__ */ __name((record) => record.kind === "errand" && (["pending", "running"].includes(record.status) || openErrand(record)), "aliveErrand");
@@ -4784,10 +5060,11 @@ ${JSON.stringify(result, null, 2)}`);
4784
5060
  __name(saveErrand, "saveErrand");
4785
5061
  function enqueueErrand(record) {
4786
5062
  (record.origin ? room(record.origin).errands : localErrands).push(record);
5063
+ touch();
4787
5064
  pump();
4788
5065
  }
4789
5066
  __name(enqueueErrand, "enqueueErrand");
4790
- 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");
4791
5068
  const errandFacts = /* @__PURE__ */ __name((record) => Object.freeze({
4792
5069
  id: record.id,
4793
5070
  why: record.why,
@@ -4821,10 +5098,10 @@ ${JSON.stringify(result, null, 2)}`);
4821
5098
  }
4822
5099
  __name(contextPrompt, "contextPrompt");
4823
5100
  function openErrands(roomId) {
4824
- const open4 = [...errandsById.values()].filter((record) => record.conversationId === roomId && openErrand(record));
4825
- const stale = open4.filter((record) => Date.now() - Date.parse(record.repliedAt ?? record.reachedAt ?? record.updatedAt) > ERRAND_OPEN_MS);
5101
+ const open5 = [...errandsById.values()].filter((record) => record.conversationId === roomId && openErrand(record));
5102
+ const stale = open5.filter((record) => Date.now() - Date.parse(record.repliedAt ?? record.reachedAt ?? record.updatedAt) > ERRAND_OPEN_MS);
4826
5103
  for (const record of stale) saveErrand({ ...record, status: "expired", reason: "no reply within a week" });
4827
- return open4.filter((record) => !stale.includes(record));
5104
+ return open5.filter((record) => !stale.includes(record));
4828
5105
  }
4829
5106
  __name(openErrands, "openErrands");
4830
5107
  function replied(turn) {
@@ -4859,6 +5136,7 @@ ${JSON.stringify(result, null, 2)}`);
4859
5136
  if (holder.turn === turn) holder.turn = void 0;
4860
5137
  errandTurns.delete(record.id);
4861
5138
  running.delete(done);
5139
+ touch();
4862
5140
  if (origin) {
4863
5141
  rooms.delete(origin.id);
4864
5142
  rooms.set(origin.id, origin);
@@ -4866,6 +5144,7 @@ ${JSON.stringify(result, null, 2)}`);
4866
5144
  pump();
4867
5145
  });
4868
5146
  running.add(done);
5147
+ touch();
4869
5148
  }
4870
5149
  __name(startErrand, "startErrand");
4871
5150
  async function executeErrand(current, turn, record) {
@@ -4967,11 +5246,9 @@ ${JSON.stringify(result, null, 2)}`);
4967
5246
  }
4968
5247
  const kind = failed ? "failed" : "final";
4969
5248
  const out = {
4970
- version: 1,
4971
5249
  turnId: turn.id,
4972
5250
  conversationId: origin.room,
4973
5251
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4974
- audience: null,
4975
5252
  inputs: [],
4976
5253
  text: text3,
4977
5254
  notice: failed,
@@ -4980,7 +5257,7 @@ ${JSON.stringify(result, null, 2)}`);
4980
5257
  options: { messageId: keyed().text(kind, origin.room, turn.id) }
4981
5258
  };
4982
5259
  try {
4983
- saveOutput(out);
5260
+ await saveOutput(out);
4984
5261
  } catch (error) {
4985
5262
  warn(`errand ${record.id}: its report could not be journaled; it stays in the record`)(error);
4986
5263
  saveErrand({ ...closing, report: text3, reported: "refused", reason: "its report: it could not be saved for sending" });
@@ -4992,7 +5269,7 @@ ${JSON.stringify(result, null, 2)}`);
4992
5269
  await stopOutput(out);
4993
5270
  const { reported: _reported, ...withheld2 } = latest(record.id) ?? closing;
4994
5271
  saveErrand({ ...withheld2, status: "cancelled", reason: STOPPED_ERRAND[turn.reason] ?? "stopped" });
4995
- } else if (sent !== "pending") finishErrandOutput(out, sent);
5272
+ } else if (sent !== "pending") await finishErrandOutput(out, sent);
4996
5273
  turn.live.end(sent === "delivered" ? "answered" : stopped() ? "stopped" : "ended");
4997
5274
  }
4998
5275
  __name(deliverReport, "deliverReport");
@@ -5066,7 +5343,7 @@ ${JSON.stringify(result, null, 2)}`);
5066
5343
  if (await endpoint.getConversationContentMode(conversationId).catch(() => "unavailable") !== "mls-dev-text") {
5067
5344
  throw new ConnectorRelayError(403, "CONNECTOR_RELAY_NOT_ENCRYPTED", "The relay serves only encrypted conversations its agent is in");
5068
5345
  }
5069
- const journaled = journal.list().find((candidate) => candidate.turnId === id);
5346
+ const journaled = await savedOutput(id);
5070
5347
  if (journaled) return { text: journaled.text, record: journaled };
5071
5348
  const { messages } = await readAgain(() => endpoint.query(
5072
5349
  "get_messages",
@@ -5077,22 +5354,20 @@ ${JSON.stringify(result, null, 2)}`);
5077
5354
  const { audience } = await relayRoom(conversationId);
5078
5355
  if (Date.now() >= deadline) throw relayBusy();
5079
5356
  const record = {
5080
- version: 1,
5081
5357
  turnId: id,
5082
5358
  conversationId,
5083
5359
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5084
- audience: null,
5085
5360
  inputs: [],
5086
5361
  text: text3,
5087
5362
  options: { messageId: id },
5088
5363
  notice: false,
5089
5364
  mls: audience
5090
5365
  };
5091
- saveOutput(record);
5366
+ await saveOutput(record);
5092
5367
  return { text: text3, record };
5093
5368
  })();
5094
- const sent = ready.then(({ record, settled }) => settled ?? publish(record).then((result) => {
5095
- 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);
5096
5371
  return result;
5097
5372
  }));
5098
5373
  const entry = { ready, sent };
@@ -5117,7 +5392,7 @@ ${JSON.stringify(result, null, 2)}`);
5117
5392
  client = new CanonClient(options.apiKey, connection.apiBaseUrl, {
5118
5393
  environmentId: connection.environmentId,
5119
5394
  streamUrl: connection.streamUrl,
5120
- stateDirectory: join7(home, "endpoint"),
5395
+ stateDirectory: join8(home, "endpoint"),
5121
5396
  ...options.client ? { client: options.client } : {},
5122
5397
  // Below Canon's client floor nothing can work, wherever the refusal surfaces: a query, an operation or the stream.
5123
5398
  ...options.onFatal ? { onClientUpdateRequired: /* @__PURE__ */ __name((error) => options.onFatal(error), "onClientUpdateRequired") } : {},
@@ -5165,29 +5440,30 @@ ${JSON.stringify(result, null, 2)}`);
5165
5440
  }
5166
5441
  scope = JSON.stringify([identity.environmentId, agentId, identity.ownershipVersion ?? 0, identity.installationId]);
5167
5442
  if (mlsOn) {
5168
- 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));
5169
5444
  mlsAsks = createMlsAsks({
5170
5445
  endpoint,
5171
5446
  agentId,
5172
- 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)),
5173
5448
  assertAudience,
5174
5449
  sessionForDestructive: options.sessionApprovalForDestructive === true,
5175
5450
  log
5176
5451
  }, approvalScope);
5177
5452
  }
5178
- journal = createCompletedOutputJournal(join7(home, "runner", options.environmentId, agentId, "finals"), scope, {
5179
- label: "agent runner",
5180
- 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")
5181
- });
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
+ );
5182
5458
  if (!options.relay && !driver.capabilities.remote) {
5183
5459
  errands = errandJournal(errandsDirectory(home, options.environmentId, agentId));
5184
- errandScope = sha2563(scope).slice(0, 32);
5460
+ errandScope = sha2564(scope).slice(0, 32);
5185
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));
5186
5462
  for (const note of pending) {
5187
5463
  if (note.scope === errandScope) notes.set(note.conversationId, [...notes.get(note.conversationId) ?? [], note]);
5188
5464
  else settleNote(note.conversationId, note, "expired", void 0, "it was left under another owner or installation");
5189
5465
  }
5190
- 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]] : []));
5191
5467
  for (const record of errands.list().filter(aliveErrand)) {
5192
5468
  const report = reports.get(record.id);
5193
5469
  if (record.scope !== errandScope) saveErrand({ ...record, status: "expired", reason: "it was left under another owner or installation" });
@@ -5227,11 +5503,11 @@ ${JSON.stringify(result, null, 2)}`);
5227
5503
  onError: warn("event stream")
5228
5504
  }));
5229
5505
  if (errands) {
5230
- 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");
5231
5507
  try {
5232
5508
  errandInbox2 = watchErrandInbox(
5233
5509
  errandsDirectory(home, options.environmentId, agentId),
5234
- { 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 },
5235
5511
  { log }
5236
5512
  );
5237
5513
  } catch (error) {
@@ -5247,7 +5523,10 @@ ${JSON.stringify(result, null, 2)}`);
5247
5523
  }
5248
5524
  warn("recovery")(error);
5249
5525
  }, "onError") }));
5526
+ touch();
5527
+ watchIdle();
5250
5528
  },
5529
+ activity,
5251
5530
  stop() {
5252
5531
  stopping ??= (async () => {
5253
5532
  for (const off of detach.splice(0).reverse()) off();
@@ -5275,13 +5554,15 @@ ${JSON.stringify(result, null, 2)}`);
5275
5554
  },
5276
5555
  async communicate(input) {
5277
5556
  if (!client) throw new Error("Start the runner before sending");
5278
- return 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));
5279
5559
  },
5280
5560
  async relayRead(conversationId, page = {}) {
5561
+ touch();
5281
5562
  const limit = page.limit ?? 20;
5282
5563
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new ConnectorRelayError(400, "CONNECTOR_RELAY_INVALID", "limit must be 1 to 100");
5283
5564
  const deadline = page.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
5284
- return beforeDeadline((async () => {
5565
+ return beforeDeadline(activeCall(async () => {
5285
5566
  const { ownerId } = await relayRoom(conversationId);
5286
5567
  const result = await readAgain(() => endpoint.query("list_messages", { conversationId, limit, ...page.before ? { before: page.before } : {} }), deadline);
5287
5568
  const accepted = result.messages.filter((message2) => !message2.deleted && typeof message2.text === "string" && message2.status === "sent" && Number.isSafeInteger(message2.mlsFeedOrdinal) && message2.mlsFeedOrdinal > 0);
@@ -5293,37 +5574,39 @@ ${JSON.stringify(result, null, 2)}`);
5293
5574
  nextBefore = String(accepted.at(-1).mlsFeedOrdinal);
5294
5575
  }
5295
5576
  return { messages, nextBefore };
5296
- })(), deadline);
5577
+ }), deadline);
5297
5578
  },
5298
5579
  async relaySend(conversationId, messageId, text3, options2 = {}) {
5580
+ touch();
5299
5581
  if (typeof messageId !== "string" || !ENDPOINT_ID_PATTERN.test(messageId) || messageId.length > 120 || typeof text3 !== "string" || !text3.trim() || Buffer.byteLength(text3) > MLS_DEV_TEXT_MAX_BYTES) {
5300
5582
  throw new ConnectorRelayError(400, "CONNECTOR_RELAY_INVALID", "A send needs a messageId and 1 byte to 32 KiB of text");
5301
5583
  }
5302
5584
  const deadline = options2.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
5303
- 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)}`;
5304
5586
  let entry = relaySends.get(id);
5305
5587
  if (!entry) relaySends.set(id, entry = prepareRelaySend(conversationId, id, text3, deadline));
5306
- const prepared = await beforeDeadline(entry.ready, deadline);
5588
+ const prepared = await beforeDeadline(activeCall(() => entry.ready), deadline);
5307
5589
  if (prepared.text !== text3) throw new ConnectorRelayError(409, "CONNECTOR_RELAY_MESSAGE_CONFLICT", "This messageId was already sent with other text");
5308
- const sent = await within(entry.sent, Math.max(0, deadline - Date.now()));
5590
+ const sent = await within(activeCall(() => entry.sent), Math.max(0, deadline - Date.now()));
5309
5591
  if (sent === "refused") throw new ConnectorRelayError(409, "CONNECTOR_RELAY_SEND_REFUSED", "Canon did not accept the message; the conversation's members may have changed");
5310
5592
  return { conversationId, messageId, historyMessageId: id, status: sent === "delivered" ? "sent" : "pending" };
5311
5593
  },
5312
5594
  async relayStart(targetUserId, messageId, text3, selection, startOptions = {}) {
5595
+ touch();
5313
5596
  if (typeof targetUserId !== "string" || !targetUserId || targetUserId.length > 200 || targetUserId.includes("/") || typeof messageId !== "string" || !ENDPOINT_ID_PATTERN.test(messageId) || messageId.length > 120 || typeof text3 !== "string" || !text3.trim() || Buffer.byteLength(text3) > MLS_DEV_TEXT_MAX_BYTES) {
5314
5597
  throw new ConnectorRelayError(400, "CONNECTOR_RELAY_INVALID", "A start needs a person or agent, a messageId and 1 byte to 32 KiB of text");
5315
5598
  }
5316
5599
  if (!options.relay || !client) throw new ConnectorRelayError(403, "CONNECTOR_RELAY_UNAVAILABLE", "This runner is not a relay");
5317
5600
  if (!mlsOn) throw new ConnectorRelayError(503, "CONNECTOR_RELAY_NO_DEVICE", "The relay has no MLS device");
5318
5601
  const deadline = startOptions.deadline ?? Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS;
5319
- const identity = { messageId, historyMessageId: `relay-start-${sha2563(messageId).slice(0, 32)}` };
5602
+ const identity = { messageId, historyMessageId: `relay-start-${sha2564(messageId).slice(0, 32)}` };
5320
5603
  const answer = await within(
5321
- endpoint.startWithheldOpener({
5604
+ activeCall(() => endpoint.startWithheldOpener({
5322
5605
  targetUserId,
5323
5606
  text: text3,
5324
5607
  messageId: identity.historyMessageId,
5325
5608
  ...selection ? { selection } : {}
5326
- }).then((result2) => ({ result: result2 }), (error) => ({ error })),
5609
+ })).then((result2) => ({ result: result2 }), (error) => ({ error })),
5327
5610
  Math.max(0, deadline - Date.now())
5328
5611
  );
5329
5612
  if (!answer || "error" in answer && answer.error?.retryable === true) {
@@ -5341,7 +5624,7 @@ ${JSON.stringify(result, null, 2)}`);
5341
5624
  }
5342
5625
  const result = answer.result;
5343
5626
  if ("reason" in result) {
5344
- if (result.reason === "opening-text-required") {
5627
+ if (PLAINTEXT_START.includes(result.reason)) {
5345
5628
  throw new ConnectorRelayError(403, "CONNECTOR_RELAY_NOT_ENCRYPTED", "The relay serves only encrypted conversations its agent is in");
5346
5629
  }
5347
5630
  return { ...identity, status: result.status, reason: result.reason };
@@ -5373,6 +5656,81 @@ function relayMessage(message2, ownerId) {
5373
5656
  }
5374
5657
  __name(relayMessage, "relayMessage");
5375
5658
 
5659
+ // src/runner/wake.ts
5660
+ import { constants as constants5 } from "node:fs";
5661
+ import { open as open4, realpath as realpath5 } from "node:fs/promises";
5662
+ import { isAbsolute as isAbsolute4, resolve as resolve3 } from "node:path";
5663
+ var MAX_WAKE_CONFIGURATION_BYTES = 16 * 1024;
5664
+ var MAX_WAKE_ROOMS = 32;
5665
+ var ROOM_ID = /^[A-Za-z0-9_-]{1,200}$/;
5666
+ function isWakeSecret(value) {
5667
+ if (typeof value !== "string" || !/^whsec_[A-Za-z0-9+/]+={0,2}$/.test(value)) return false;
5668
+ const key = Buffer.from(value.slice(6), "base64");
5669
+ return key.length >= 32 && key.length <= 64 && key.toString("base64") === value.slice(6);
5670
+ }
5671
+ __name(isWakeSecret, "isWakeSecret");
5672
+ function parseWakeConfiguration(value) {
5673
+ const invalid2 = /* @__PURE__ */ __name((what) => {
5674
+ throw new Error(`Invalid wake configuration: ${what}`);
5675
+ }, "invalid");
5676
+ if (!value || typeof value !== "object" || Array.isArray(value)) return invalid2("expected a JSON object");
5677
+ const v = value;
5678
+ const keys = ["version", "callbackUrl", "secret", "conversationIds"];
5679
+ if (Object.keys(v).sort().join() !== [...keys].sort().join()) invalid2(`expected ${keys.join(", ")}`);
5680
+ if (v.version !== 1) invalid2("version");
5681
+ let url;
5682
+ try {
5683
+ url = typeof v.callbackUrl === "string" && v.callbackUrl.length <= 2048 ? new URL(v.callbackUrl) : void 0;
5684
+ } catch {
5685
+ }
5686
+ if (!url || url.protocol !== "https:" || url.username || url.password || url.hash) invalid2("callbackUrl: a public https URL, with no credentials or fragment");
5687
+ if (!isWakeSecret(v.secret)) invalid2("secret: whsec_ and the padded base64 of 32 to 64 random bytes");
5688
+ const rooms = v.conversationIds;
5689
+ if (!Array.isArray(rooms) || rooms.length < 1 || rooms.length > MAX_WAKE_ROOMS || rooms.some((id) => typeof id !== "string" || !ROOM_ID.test(id))) {
5690
+ invalid2(`conversationIds: 1 to ${MAX_WAKE_ROOMS} conversation ids`);
5691
+ }
5692
+ return { callbackUrl: url.toString(), secret: v.secret, conversationIds: [...new Set(rooms)].sort() };
5693
+ }
5694
+ __name(parseWakeConfiguration, "parseWakeConfiguration");
5695
+ async function loadWakeConfiguration(path) {
5696
+ const fail2 = /* @__PURE__ */ __name(() => {
5697
+ throw new Error(`The wake configuration must be a private file (0600, owned by this user, not a link): ${path}`);
5698
+ }, "fail");
5699
+ if (typeof path !== "string" || !isAbsolute4(path) || path.includes("\0") || resolve3(path) !== path || await realpath5(path).catch(() => "") !== path) fail2();
5700
+ const windows = process.platform === "win32";
5701
+ if (windows) assertPrivateWindowsFile(path);
5702
+ const file = await open4(path, constants5.O_RDONLY | (constants5.O_NOFOLLOW ?? 0) | (constants5.O_NONBLOCK ?? 0)).catch(() => fail2());
5703
+ let text3;
5704
+ try {
5705
+ const info = await file.stat();
5706
+ if (!info.isFile() || info.nlink !== 1 || info.size < 1 || info.size > MAX_WAKE_CONFIGURATION_BYTES) fail2();
5707
+ if (!windows && (info.uid !== process.getuid?.() || (info.mode & 63) !== 0)) fail2();
5708
+ const bytes = await readBoundedRegularFileHandle(file, MAX_WAKE_CONFIGURATION_BYTES).catch(() => fail2());
5709
+ try {
5710
+ text3 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
5711
+ } catch {
5712
+ return fail2();
5713
+ }
5714
+ } finally {
5715
+ await file.close();
5716
+ }
5717
+ let value;
5718
+ try {
5719
+ value = JSON.parse(text3);
5720
+ } catch {
5721
+ throw new Error("Invalid wake configuration: not JSON");
5722
+ }
5723
+ return parseWakeConfiguration(value);
5724
+ }
5725
+ __name(loadWakeConfiguration, "loadWakeConfiguration");
5726
+ function parseIdleTimeout(value) {
5727
+ const match = /^(\d{1,7})([smh]?)$/.exec(value.trim());
5728
+ const ms = match ? Number(match[1]) * { "": 1e3, s: 1e3, m: 6e4, h: 36e5 }[match[2]] : NaN;
5729
+ if (!Number.isSafeInteger(ms) || ms < 1e3) throw new Error("--idle-timeout takes a duration of at least 1 second: 90, 90s, 10m or 1h.");
5730
+ return ms;
5731
+ }
5732
+ __name(parseIdleTimeout, "parseIdleTimeout");
5733
+
5376
5734
  export {
5377
5735
  DriverSessionError,
5378
5736
  ConnectorRelayError,
@@ -5404,5 +5762,8 @@ export {
5404
5762
  writeRelayCredential,
5405
5763
  relayRunnerHome,
5406
5764
  runtimeDescriptor,
5407
- createRunner
5765
+ createRunner,
5766
+ parseWakeConfiguration,
5767
+ loadWakeConfiguration,
5768
+ parseIdleTimeout
5408
5769
  };