@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.
- package/README.md +11 -1
- package/dist/chunks/{chunk-ZPBOGOAD.js → chunk-5YKGDUJX.js} +271 -359
- package/dist/chunks/{chunk-XKUG2BQR.js → chunk-6FD6KFQN.js} +528 -167
- package/dist/cli.js +50 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -2
- package/dist/internal/core/client.d.ts +1 -215
- package/dist/internal/core/index.d.ts +0 -1
- package/dist/internal/endpoint/completions.d.ts +45 -0
- package/dist/internal/endpoint/engine.d.ts +15 -0
- package/dist/internal/endpoint/index.d.ts +1 -0
- package/dist/internal/endpoint/transient-publisher.d.ts +3 -2
- package/dist/media.js +1 -1
- package/dist/runner/errands.d.ts +2 -2
- package/dist/runner/runner.d.ts +34 -1
- package/dist/runner/wake.d.ts +16 -0
- package/package.json +1 -1
- package/dist/internal/core/completed-output-journal.d.ts +0 -34
|
@@ -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-
|
|
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((
|
|
265
|
+
call: /* @__PURE__ */ __name((method, params) => new Promise((resolve4, reject) => {
|
|
266
266
|
next += 1;
|
|
267
|
-
pending.set(next, { resolve: /* @__PURE__ */ __name((value) =>
|
|
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((
|
|
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 }) =>
|
|
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((
|
|
405
|
-
exit =
|
|
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((
|
|
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
|
|
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((
|
|
535
|
+
ended = new Promise((resolve5) => {
|
|
536
536
|
setTimeout(() => {
|
|
537
537
|
signalAgent(child, "SIGKILL");
|
|
538
|
-
|
|
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((
|
|
793
|
-
exit =
|
|
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((
|
|
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
|
-
|
|
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((
|
|
1116
|
-
exit =
|
|
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((
|
|
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
|
|
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((
|
|
1347
|
-
timer = setTimeout(
|
|
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((
|
|
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")
|
|
1396
|
+
else if (result.outcome === "reply") resolve4({ reply: result.text });
|
|
1397
1397
|
else if (result.outcome === "failed") reject(new Error(FAILED));
|
|
1398
|
-
else
|
|
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((
|
|
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((
|
|
2165
|
-
prepareProtectedKeyPackage: /* @__PURE__ */ __name((context, ...rest) => device(context, false).then((
|
|
2166
|
-
prepareProtectedGroup: /* @__PURE__ */ __name((identity, ...rest) => device(identity, false).then((
|
|
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((
|
|
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
|
|
2417
|
-
import { readFileSync as
|
|
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
|
|
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
|
|
2685
|
-
import { readdirSync as
|
|
2686
|
-
import { join as
|
|
2687
|
-
var
|
|
2688
|
-
var pause = /* @__PURE__ */ __name((ms) => new Promise((
|
|
2689
|
-
setTimeout(
|
|
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((
|
|
2692
|
-
if (signal.aborted) return
|
|
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
|
-
|
|
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) =>
|
|
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:${
|
|
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((
|
|
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
|
-
|
|
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((
|
|
3008
|
-
const stop = /* @__PURE__ */ __name(() =>
|
|
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(
|
|
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((
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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((
|
|
3209
|
+
await new Promise((resolve4, reject) => {
|
|
3162
3210
|
http.once("error", reject);
|
|
3163
|
-
http.listen(0, "127.0.0.1",
|
|
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((
|
|
3222
|
+
close: /* @__PURE__ */ __name(() => new Promise((resolve4) => {
|
|
3175
3223
|
turns.clear();
|
|
3176
3224
|
http.closeAllConnections();
|
|
3177
|
-
http.close(() =>
|
|
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
|
|
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((
|
|
3252
|
-
timer = setTimeout(
|
|
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((
|
|
3258
|
-
setTimeout(
|
|
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}-${
|
|
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((
|
|
3488
|
-
stop = /* @__PURE__ */ __name(() =>
|
|
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-${
|
|
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) =>
|
|
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(
|
|
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:
|
|
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 =
|
|
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
|
|
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")
|
|
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((
|
|
4174
|
-
if (signal.aborted)
|
|
4175
|
-
else signal.addEventListener("abort", () =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
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:${
|
|
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
|
|
4418
|
-
const live = /* @__PURE__ */ __name(() =>
|
|
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,
|
|
4619
|
+
state: driverState(turn.session, open5),
|
|
4441
4620
|
tools: tools2,
|
|
4442
|
-
confirmPickup: /* @__PURE__ */ __name(() => confirmPickup(roomId, turn,
|
|
4621
|
+
confirmPickup: /* @__PURE__ */ __name(() => confirmPickup(roomId, turn, open5), "confirmPickup")
|
|
4443
4622
|
};
|
|
4444
4623
|
}
|
|
4445
4624
|
__name(buildTurn, "buildTurn");
|
|
4446
|
-
async function confirmPickup(roomId, turn,
|
|
4447
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
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")
|
|
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 =
|
|
4592
|
-
const
|
|
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
|
-
|
|
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 (
|
|
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-${
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-${
|
|
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
|
|
4825
|
-
const stale =
|
|
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
|
|
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 =
|
|
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")
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
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 =
|
|
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(
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
})
|
|
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-${
|
|
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-${
|
|
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
|
|
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
|
};
|