@canonmsg/agent-sdk 12.6.1 → 13.0.2

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.
Files changed (30) hide show
  1. package/README.md +23 -21
  2. package/dist/chunks/{chunk-S7BV74DN.js → chunk-5WPAB5MB.js} +293 -8
  3. package/dist/chunks/{chunk-DAJ7L7TN.js → chunk-DNHJGAEL.js} +378 -663
  4. package/dist/chunks/{chunk-HH7KR22P.js → chunk-DTAWEQCM.js} +2 -2
  5. package/dist/chunks/{chunk-5J4YPLNW.js → chunk-TYZT346O.js} +266 -1217
  6. package/dist/chunks/{chunk-QK37HAHC.js → chunk-YHDDOADK.js} +1 -1
  7. package/dist/chunks/{chunk-FVYKLH6I.js → chunk-YJUPKU47.js} +20 -5
  8. package/dist/chunks/{mls-crypto-4DZT2UFH.js → mls-crypto-NZVMSUFU.js} +2 -2
  9. package/dist/cli.js +32 -36
  10. package/dist/drivers/subprocess.d.ts +4 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +6 -6
  13. package/dist/internal/backend-contracts/mlsDevGroup.d.ts +17 -0
  14. package/dist/internal/core/completed-output-journal.d.ts +0 -5
  15. package/dist/internal/core/index.d.ts +3 -9
  16. package/dist/internal/core/retry.d.ts +2 -89
  17. package/dist/internal/core/silent-turn.d.ts +4 -33
  18. package/dist/internal/endpoint/engine.d.ts +3 -0
  19. package/dist/internal/endpoint/mls-dev-groups.d.ts +12 -0
  20. package/dist/internal/endpoint/mls-dev-text.d.ts +29 -0
  21. package/dist/internal/endpoint/transport.d.ts +1 -1
  22. package/dist/media.js +4 -4
  23. package/dist/mls-wasm-host.js +3 -3
  24. package/dist/runner/mls.d.ts +4 -7
  25. package/dist/runner/runner.d.ts +9 -12
  26. package/dist/runner/types.d.ts +31 -0
  27. package/package.json +1 -1
  28. package/dist/internal/core/approval-manager.d.ts +0 -43
  29. package/dist/internal/core/runtime-request-manager.d.ts +0 -283
  30. package/dist/internal/core/turn-verbosity.d.ts +0 -79
@@ -8,9 +8,8 @@ import {
8
8
  wasmMlsCustody,
9
9
  windowsAclProblems,
10
10
  writeDurable
11
- } from "./chunk-HH7KR22P.js";
11
+ } from "./chunk-DTAWEQCM.js";
12
12
  import {
13
- ApprovalManager,
14
13
  BoundedFileReadError,
15
14
  CanonClient,
16
15
  MAX_CANON_MEDIA_BYTES,
@@ -18,8 +17,6 @@ import {
18
17
  RUNTIME_STOP_ACTION,
19
18
  RUNTIME_STOP_AND_DROP_ACTION,
20
19
  acquireLock,
21
- assertCompletedOutputAudience,
22
- buildChunkedMessagePartMetadata,
23
20
  conversationMediaCacheDir,
24
21
  createCompletedOutputJournal,
25
22
  createRuntimeHeartbeat,
@@ -27,26 +24,22 @@ import {
27
24
  inferUploadMimeType,
28
25
  initRTDBAuth,
29
26
  isPendingCanonOperation,
30
- isRetryableCanonDeliveryError,
31
27
  materializeAttachment,
32
28
  normalizeRuntimeCommandDescriptors,
33
29
  readBoundedRegularFileHandle,
34
30
  renderCanonHostInboundContent,
35
- reportNoReplyOutcome,
36
31
  resolveCanonRuntimeConnection,
37
- resolveTurnVerbosity,
38
- sendMessageWithRetryChunked,
39
- splitTextByUtf8Bytes,
32
+ splitTextWithinBytes,
40
33
  verifyCanonRuntimeConnection
41
- } from "./chunk-5J4YPLNW.js";
34
+ } from "./chunk-TYZT346O.js";
42
35
  import {
43
36
  NodeMlsDevCustodyError,
44
37
  createNodeMlsDevCustody,
45
38
  createPortableNodeMlsDevCustody
46
- } from "./chunk-FVYKLH6I.js";
39
+ } from "./chunk-YJUPKU47.js";
47
40
  import {
48
41
  parseMlsDevClientConfiguration
49
- } from "./chunk-QK37HAHC.js";
42
+ } from "./chunk-YHDDOADK.js";
50
43
  import {
51
44
  CONNECTOR_RELAY_ANSWER_WITHIN_MS,
52
45
  CONNECTOR_RELAY_INSTALLATION_PATTERN,
@@ -60,9 +53,11 @@ import {
60
53
  MLS_DEV_TEXT_MAX_BYTES,
61
54
  MLS_DEV_TEXT_PROTOCOL_V3,
62
55
  NO_REPLY_ACK_NOTE,
63
- VERB_LIMITS,
56
+ PARTICIPATION_HISTORY_FETCH_LIMIT,
64
57
  __name,
58
+ buildParticipationHistorySnapshot,
65
59
  connectorRelayResponseBytes,
60
+ evaluateParticipationPolicy,
66
61
  isConnectorRelayCredential,
67
62
  isConnectorRelayInstallation,
68
63
  isMlsDevTextProtocol,
@@ -73,8 +68,9 @@ import {
73
68
  parseMlsDevCredentialToken,
74
69
  parseMlsDevGroupHead,
75
70
  parseMlsDevGroupRoster,
76
- resolveAllowedUploadMimeType
77
- } from "./chunk-S7BV74DN.js";
71
+ resolveAllowedUploadMimeType,
72
+ resolveConversationPolicyScope
73
+ } from "./chunk-5WPAB5MB.js";
78
74
 
79
75
  // src/runner/types.ts
80
76
  var DriverSessionError = class extends Error {
@@ -225,6 +221,7 @@ async function approve(turn, request) {
225
221
  return answer.scope === "session" ? "session" : "once";
226
222
  }
227
223
  __name(approve, "approve");
224
+ var PIPE_GRACE_MS = 1e3;
228
225
  function stdioRpc(child, handlers, { name, envelope }) {
229
226
  let next = 0;
230
227
  const pending = /* @__PURE__ */ new Map();
@@ -258,6 +255,12 @@ function stdioRpc(child, handlers, { name, envelope }) {
258
255
  } else handlers.notify(message2.method, params);
259
256
  });
260
257
  child.stderr?.resume();
258
+ child.once("exit", () => {
259
+ setTimeout(() => {
260
+ child.stdout?.destroy();
261
+ child.stderr?.destroy();
262
+ }, PIPE_GRACE_MS).unref();
263
+ });
261
264
  return {
262
265
  call: /* @__PURE__ */ __name((method, params) => new Promise((resolve3, reject) => {
263
266
  next += 1;
@@ -283,7 +286,6 @@ var AUTH_REQUIRED = -32e3;
283
286
  var SYSTEM = "You are an agent in Canon, a messaging app where people and AI agents talk. Each prompt holds the new messages in one conversation. Your final message in a turn is posted there as your reply, so put your whole answer in it. Canon's tools are on the MCP server whose name starts with canon_. To ask the people in the conversation a question, call its ask_question tool and wait for the answer. When you have nothing to add, call its no_reply tool instead of answering.";
284
287
  var GUEST = "Only your owner can let you use tools in this conversation, and this turn holds a message from someone else: do not read files, run commands or use any tool but those of the MCP server whose name starts with canon_. Answer from the conversation and what you already know, and say so when something needs your owner.";
285
288
  var OWNER_ONLY = "This agent only takes messages from its owner.";
286
- var PIPE_GRACE_MS = 1e3;
287
289
  var KILL_AFTER_MS = 2e3;
288
290
  var MACHINE_KINDS = /* @__PURE__ */ new Set(["read", "edit", "delete", "move", "search", "execute", "fetch"]);
289
291
  var BRIDGE = `
@@ -537,10 +539,6 @@ ${text(item.newText)}`);
537
539
  }, KILL_AFTER_MS);
538
540
  });
539
541
  }
540
- setTimeout(() => {
541
- child.stdout?.destroy();
542
- child.stderr?.destroy();
543
- }, PIPE_GRACE_MS).unref();
544
542
  });
545
543
  child.once("error", (error) => gone(new Error(`Could not run ${command}: ${error.message}`)));
546
544
  child.once("close", () => gone(new Error(`${agent} exited before finishing the turn`)));
@@ -1701,7 +1699,7 @@ function createMlsIds(key) {
1701
1699
  }
1702
1700
  __name(createMlsIds, "createMlsIds");
1703
1701
  function requireMlsSupport(input) {
1704
- if (!isMlsEnvironmentId(input.environmentId) || input.agent.outboundPolicy === "closed") fail();
1702
+ if (!isMlsEnvironmentId(input.environmentId)) fail();
1705
1703
  }
1706
1704
  __name(requireMlsSupport, "requireMlsSupport");
1707
1705
  function mentionsAgent(text3, names) {
@@ -2421,153 +2419,7 @@ import { realpath as realpath4, stat as stat2 } from "node:fs/promises";
2421
2419
  import { basename as basename4, isAbsolute as isAbsolute3, join as join7, sep } from "node:path";
2422
2420
 
2423
2421
  // ../agent-tools/src/communication-tool.ts
2424
- var CANON_COMMUNICATE_TOOL_NAME = "communicate";
2425
2422
  var CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS = 10;
2426
- var NON_EMPTY_ID = { type: "string", minLength: 1, maxLength: 160 };
2427
- var MESSAGE_TEXT = { type: "string", minLength: 1, maxLength: 4096 };
2428
- var OPTIONAL_CAPTION = { type: "string", maxLength: 4096 };
2429
- var DIRECT_SELECTION_SCHEMA = {
2430
- oneOf: [
2431
- {
2432
- type: "object",
2433
- required: ["mode"],
2434
- additionalProperties: false,
2435
- properties: { mode: { const: "latest_or_new" } }
2436
- },
2437
- {
2438
- type: "object",
2439
- required: ["mode"],
2440
- additionalProperties: false,
2441
- properties: { mode: { const: "new" } }
2442
- },
2443
- {
2444
- type: "object",
2445
- required: ["mode", "conversationId"],
2446
- additionalProperties: false,
2447
- properties: {
2448
- mode: { const: "specific" },
2449
- conversationId: NON_EMPTY_ID
2450
- }
2451
- }
2452
- ]
2453
- };
2454
- function canonCommunicateToolDefinition(name = CANON_COMMUNICATE_TOOL_NAME) {
2455
- return {
2456
- name,
2457
- description: "Find agents, list current conversations with their participants, message an explicit conversation, start_conversation with a person or agent (new by default), create_conversation with an optional title, manage_participants, leave, forward a message or share a contact. Check the current participants before selecting an existing conversation. Canon enforces admission and operator permissions.",
2458
- inputSchema: {
2459
- type: "object",
2460
- oneOf: [
2461
- { type: "object", required: ["action"], additionalProperties: false, properties: { action: { const: "list_conversations" }, limit: { type: "integer", minimum: 1, maximum: 100 } } },
2462
- { type: "object", required: ["action", "conversationId"], additionalProperties: false, properties: { action: { const: "leave_conversation" }, conversationId: NON_EMPTY_ID } },
2463
- {
2464
- type: "object",
2465
- required: ["action"],
2466
- additionalProperties: false,
2467
- properties: {
2468
- action: { const: "discover_agents" },
2469
- query: {
2470
- type: "string",
2471
- maxLength: MAX_DISCOVER_AGENTS_QUERY_LENGTH,
2472
- description: "Optional name or description prefix. Empty lists the directory."
2473
- },
2474
- limit: {
2475
- type: "integer",
2476
- minimum: 1,
2477
- maximum: CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS,
2478
- default: CANON_COMMUNICATE_DISCOVERY_MAX_RESULTS
2479
- },
2480
- cursor: {
2481
- type: "string",
2482
- minLength: 1,
2483
- description: "Opaque cursor from a prior discovery action with the same query."
2484
- }
2485
- }
2486
- },
2487
- {
2488
- type: "object",
2489
- required: ["action", "conversationId", "text"],
2490
- additionalProperties: false,
2491
- properties: {
2492
- action: { const: "message_existing" },
2493
- conversationId: NON_EMPTY_ID,
2494
- text: MESSAGE_TEXT,
2495
- messageId: NON_EMPTY_ID
2496
- }
2497
- },
2498
- {
2499
- type: "object",
2500
- required: ["action", "principalId", "text"],
2501
- additionalProperties: false,
2502
- properties: {
2503
- action: { enum: ["start_conversation", "start_direct"] },
2504
- principalId: NON_EMPTY_ID,
2505
- text: MESSAGE_TEXT,
2506
- selection: DIRECT_SELECTION_SCHEMA,
2507
- messageId: NON_EMPTY_ID
2508
- }
2509
- },
2510
- {
2511
- type: "object",
2512
- required: ["action", "memberIds"],
2513
- additionalProperties: false,
2514
- properties: {
2515
- action: { enum: ["create_conversation", "create_group"] },
2516
- name: { type: "string", minLength: 1, maxLength: VERB_LIMITS.groupNameChars },
2517
- memberIds: {
2518
- type: "array",
2519
- minItems: 1,
2520
- maxItems: VERB_LIMITS.groupMembers - 1,
2521
- uniqueItems: true,
2522
- items: NON_EMPTY_ID
2523
- }
2524
- }
2525
- },
2526
- {
2527
- type: "object",
2528
- required: [
2529
- "action",
2530
- "sourceConversationId",
2531
- "targetConversationId",
2532
- "messageId"
2533
- ],
2534
- additionalProperties: false,
2535
- properties: {
2536
- action: { const: "forward_message" },
2537
- sourceConversationId: NON_EMPTY_ID,
2538
- targetConversationId: NON_EMPTY_ID,
2539
- messageId: NON_EMPTY_ID,
2540
- text: OPTIONAL_CAPTION
2541
- }
2542
- },
2543
- {
2544
- type: "object",
2545
- required: ["action", "conversationId", "contactUserId"],
2546
- additionalProperties: false,
2547
- properties: {
2548
- action: { const: "share_contact" },
2549
- conversationId: NON_EMPTY_ID,
2550
- contactUserId: NON_EMPTY_ID,
2551
- text: OPTIONAL_CAPTION,
2552
- messageId: NON_EMPTY_ID
2553
- }
2554
- },
2555
- {
2556
- type: "object",
2557
- required: ["action", "conversationId", "userId", "operation"],
2558
- additionalProperties: false,
2559
- properties: {
2560
- action: { enum: ["manage_participants", "manage_group_members"] },
2561
- conversationId: NON_EMPTY_ID,
2562
- userId: NON_EMPTY_ID,
2563
- operation: { enum: ["add", "remove"] }
2564
- }
2565
- }
2566
- ]
2567
- }
2568
- };
2569
- }
2570
- __name(canonCommunicateToolDefinition, "canonCommunicateToolDefinition");
2571
2423
  function isRecord3(value) {
2572
2424
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
2573
2425
  }
@@ -2788,124 +2640,41 @@ __name(executeCanonCommunicateTool, "executeCanonCommunicateTool");
2788
2640
 
2789
2641
  // src/runner/live.ts
2790
2642
  import { randomBytes as randomBytes3 } from "node:crypto";
2791
- var SNAPSHOT_GAP_MS = 250;
2792
2643
  var KEEPALIVE_MS = 3500;
2793
2644
  var PROGRESS_REFRESH_MS = 6e4;
2794
- var MAX_ROWS = 30;
2795
2645
  function createLivePublisher(options) {
2796
2646
  const { client, conversationId, enqueue } = options;
2797
- const presence = options.mode === "presence";
2798
2647
  const presenceId = `turn-${randomBytes3(16).toString("hex")}`;
2799
- const publicTurnId = /* @__PURE__ */ __name(() => presence && options.turnId() !== null ? presenceId : options.turnId(), "publicTurnId");
2648
+ const publicTurnId = /* @__PURE__ */ __name(() => options.turnId() !== null ? presenceId : null, "publicTurnId");
2800
2649
  const openedAt = Date.now();
2801
- const rows = /* @__PURE__ */ new Map();
2802
- let mode = options.mode;
2803
- let text3 = "";
2804
2650
  let waiting = false;
2805
2651
  let ended = false;
2806
- let sequence = 0;
2807
- let state;
2808
2652
  let stateAt = 0;
2809
- let snapshotAt = 0;
2810
- let timer;
2811
2653
  let keepalive;
2812
- const current = /* @__PURE__ */ __name(() => waiting ? "waiting_input" : presence ? "thinking" : [...rows.values()].some((row) => row.status === "running") ? "tool" : text3 ? "streaming" : "thinking", "current");
2813
- const typing = /* @__PURE__ */ __name((on) => enqueue(() => client.setTyping(conversationId, on, on ? "thinking" : void 0)), "typing");
2814
- const node = /* @__PURE__ */ __name((blank = false) => enqueue(async () => {
2815
- if (ended && !blank) return;
2816
- const turnId = options.turnId();
2817
- await client.setStreaming({
2818
- conversationId,
2819
- turnId,
2820
- messageId: turnId ?? "",
2821
- text: blank ? "" : text3,
2822
- status: blank ? "thinking" : current(),
2823
- blocks: blank ? [] : [...rows.values()]
2824
- });
2825
- }), "node");
2826
2654
  const publishState = /* @__PURE__ */ __name(() => {
2827
- const next = state = current();
2655
+ const state = waiting ? "waiting_input" : "thinking";
2828
2656
  stateAt = Date.now();
2829
- enqueue(() => client.setRuntimeTurn({
2830
- conversationId,
2831
- turnId: publicTurnId(),
2832
- state: next,
2833
- openedAt,
2834
- turnUpdatedAt: Date.now(),
2835
- ...presence ? {} : options.context()
2836
- }));
2657
+ enqueue(() => client.setRuntimeTurn({ conversationId, turnId: publicTurnId(), state, openedAt, turnUpdatedAt: Date.now() }));
2837
2658
  }, "publishState");
2838
- const progress = /* @__PURE__ */ __name(() => {
2839
- if (current() !== state || Date.now() - stateAt > PROGRESS_REFRESH_MS) publishState();
2840
- if (mode !== "full" || timer) return;
2841
- timer = setTimeout(() => {
2842
- timer = void 0;
2843
- snapshotAt = Date.now();
2844
- node();
2845
- }, Math.max(0, snapshotAt + SNAPSHOT_GAP_MS - Date.now()));
2846
- }, "progress");
2847
2659
  return {
2848
2660
  open() {
2849
2661
  if (ended) return;
2850
- if (!presence) typing(true);
2851
2662
  publishState();
2852
- if (mode === "full") node();
2853
2663
  keepalive = setInterval(() => {
2854
- if (ended) return;
2855
- if (presence) {
2856
- if (Date.now() - stateAt > PROGRESS_REFRESH_MS) publishState();
2857
- return;
2858
- }
2859
- if (waiting) return;
2860
- typing(true);
2861
- if (mode === "full" && !text3 && !rows.size) node();
2664
+ if (!ended && Date.now() - stateAt > PROGRESS_REFRESH_MS) publishState();
2862
2665
  }, KEEPALIVE_MS);
2863
2666
  keepalive.unref?.();
2864
2667
  },
2865
- setMode(next) {
2866
- if (ended || next === mode || presence || next === "presence") return;
2867
- mode = next;
2868
- if (mode === "full") node();
2869
- },
2870
- text(snapshot) {
2871
- if (ended || presence || snapshot === text3) return;
2872
- text3 = snapshot;
2873
- progress();
2874
- },
2875
- activity(event) {
2876
- if (ended || presence) return;
2877
- rows.set(event.id, {
2878
- id: event.id,
2879
- turnId: options.turnId() ?? "",
2880
- kind: event.kind,
2881
- title: event.title,
2882
- status: event.state === "done" ? "completed" : event.state,
2883
- sequence: rows.get(event.id)?.sequence ?? ++sequence
2884
- });
2885
- for (const id of rows.keys()) if (rows.size > MAX_ROWS) rows.delete(id);
2886
- progress();
2887
- },
2888
2668
  waiting(on) {
2889
2669
  if (ended || waiting === on) return;
2890
2670
  waiting = on;
2891
- if (!presence) typing(!on);
2892
2671
  publishState();
2893
- if (mode === "full") node();
2894
- },
2895
- refresh() {
2896
- if (!ended && state && !presence) publishState();
2897
2672
  },
2898
2673
  end(how) {
2899
2674
  if (ended) return;
2900
2675
  ended = true;
2901
- clearTimeout(timer);
2902
2676
  clearInterval(keepalive);
2903
- if (!presence) typing(false);
2904
- if (mode === "full") {
2905
- if (how !== "answered") node(true);
2906
- enqueue(() => client.clearStreaming(conversationId));
2907
- }
2908
- enqueue(() => client.setRuntimeTurn(how === "stopped" ? { conversationId, turnId: publicTurnId(), state: "interrupted", ...presence ? {} : options.context() } : { conversationId, turnId: null, state: "idle", ...presence ? {} : { queueDepth: 0 }, openedAt: null, turnUpdatedAt: null }));
2677
+ enqueue(() => client.setRuntimeTurn(how === "stopped" ? { conversationId, turnId: publicTurnId(), state: "interrupted" } : { conversationId, turnId: null, state: "idle", openedAt: null, turnUpdatedAt: null }));
2909
2678
  }
2910
2679
  };
2911
2680
  }
@@ -3419,7 +3188,6 @@ var CLAIM_PATIENCE_MS = 2e4;
3419
3188
  var HISTORY_WINDOW = 20;
3420
3189
  var STATE_LIMIT_BYTES = 64 * 1024;
3421
3190
  var FAILURE_NOTICE = "Sorry, something went wrong and I could not finish this reply.";
3422
- var STOPS_SHORT_NOTICE = "Sorry, my reply could not be delivered in full.";
3423
3191
  var RESTART_NOTICE = "Sorry, I was restarted before I could finish replying to this. Please send it again if you still need an answer.";
3424
3192
  var CLIENT_TYPES = { code: "generic", claude: "claude-code", codex: "codex", acp: "generic", connector: "generic" };
3425
3193
  var objectSchema = /* @__PURE__ */ __name((properties, required = []) => ({ type: "object", properties, required, additionalProperties: false }), "objectSchema");
@@ -3489,16 +3257,60 @@ __name(within, "within");
3489
3257
  var pause2 = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => {
3490
3258
  setTimeout(resolve3, ms).unref?.();
3491
3259
  }), "pause");
3492
- var audienceOf = /* @__PURE__ */ __name((room) => room?.memberIds ? { memberIds: [...room.memberIds], ...typeof room.membershipRevision === "number" ? { membershipRevision: room.membershipRevision } : {} } : null, "audienceOf");
3260
+ function agentRun(turns) {
3261
+ const history = turns.map(({ senderId, senderType }) => senderType === "ai_agent" ? { senderId, senderType, metadata: { turnSemantics: "turn_complete" } } : { senderId, senderType: "human" });
3262
+ return {
3263
+ consecutiveAgentTurns: buildParticipationHistorySnapshot(history.slice(1)).consecutiveAgentTurns,
3264
+ currentAgentStreakStartedByHuman: buildParticipationHistorySnapshot(history).currentAgentStreakStartedByHuman
3265
+ };
3266
+ }
3267
+ __name(agentRun, "agentRun");
3493
3268
  var inert = /* @__PURE__ */ __name((value) => value.replace(/\s+/g, " ").trim(), "inert");
3494
3269
  var OWNER_MARK = " [your owner]";
3495
3270
  var TRUSTED_MARK = " [trusted teammate agent]";
3496
3271
  var speaker = /* @__PURE__ */ __name((name) => inert(name).replace(/\[/g, "(").replace(/\]/g, ")"), "speaker");
3497
- var defused = /* @__PURE__ */ __name((text3) => text3.replace(/\[(\s*(?:trusted\s+teammate\s+agent|your\s+owner)\s*)\]/gi, "($1)").replace(/\[(\s*(?:end\s+of\s+)?owner['\u2019]s\s+(?:private\s+note|errand)\s*)\]/gi, "($1)"), "defused");
3272
+ var defused = /* @__PURE__ */ __name((text3) => text3.replace(/\[(\s*(?:trusted\s+teammate\s+agent|your\s+owner)\s*)\]/gi, "($1)").replace(/\[(\s*(?:end\s+of\s+)?owner['\u2019]s\s+(?:private\s+note|errand)\s*)\]/gi, "($1)").replace(/\[(\s*(?:end\s+of\s+)?conversation\s+facts\s*)\]/gi, "($1)"), "defused");
3498
3273
  var NOTE_OPEN = "[owner's private note] Your owner left this for you on the machine you run on. It is not a message in this conversation: no one in it sees it, and it was not posted.";
3499
3274
  var NOTE_CLOSE = "[end of owner's private note]";
3500
3275
  var ERRAND_OPEN = "[owner's errand]";
3501
3276
  var ERRAND_CLOSE = "[end of owner's errand]";
3277
+ var FACTS_OPEN = "[conversation facts] Your runner added this from Canon's records. It is not a message, and no one in the conversation sees it.";
3278
+ var FACTS_CLOSE = "[end of conversation facts]";
3279
+ var MAX_FACT_MEMBERS = 50;
3280
+ var MAX_FACT_NAME = 200;
3281
+ var MAX_FACT_TOPIC = 500;
3282
+ var STYLE_RULES = {
3283
+ natural: void 0,
3284
+ collaborative: "Work openly with the people and agents here.",
3285
+ "mention-first": "Speak when you are clearly addressed; otherwise end your turn with no_reply.",
3286
+ "approval-gated": "Be cautious: join in only on a strong signal; otherwise end your turn with no_reply.",
3287
+ "handoff-only": "Act when someone explicitly hands you a task; otherwise end your turn with no_reply.",
3288
+ observer: "Stay quiet unless a reply is necessary, and end your turn with no_reply."
3289
+ };
3290
+ var factText = /* @__PURE__ */ __name((value, limit = MAX_FACT_NAME) => speaker(value).slice(0, limit).trimEnd(), "factText");
3291
+ function factsPrompt({ name, topic, owner, members, rules }, kind) {
3292
+ const shown = members.slice(0, MAX_FACT_MEMBERS).map((member) => member.isYou ? "you" : factText(member.name ?? member.id) + (member.isOwner ? OWNER_MARK : member.type === "ai_agent" ? " [agent]" : ""));
3293
+ const lines = !rules ? [] : [
3294
+ STYLE_RULES[rules.style],
3295
+ kind === "group" && rules.requireMentionForGroupReplies ? "Only messages that mention you reach you." : void 0,
3296
+ rules.allowAgentToAgent ? void 0 : "Messages from other agents do not reach you.",
3297
+ rules.allowLongRunningCollaboration ? void 0 : "After a person writes, another agent gets one exchange with you.",
3298
+ typeof rules.maxConsecutiveAgentTurns === "number" ? `Agents get at most ${rules.maxConsecutiveAgentTurns} turns in a row before a person writes.` : void 0
3299
+ ].filter((line) => line !== void 0);
3300
+ return [
3301
+ FACTS_OPEN,
3302
+ `Your owner: ${factText(owner.name ?? owner.id)}`,
3303
+ ...name ? [`Conversation: ${factText(name)}`] : [],
3304
+ ...topic ? [`Topic: ${factText(topic, MAX_FACT_TOPIC)}`] : [],
3305
+ `Members: ${shown.join(", ")}${members.length > shown.length ? `, and ${members.length - shown.length} more` : ""}`,
3306
+ ...lines.length ? [
3307
+ "This conversation's rules, which your owner or any of its admins can set. They say how you take part here, not what you may do:",
3308
+ ...lines.map((line) => `- ${line}`)
3309
+ ] : [],
3310
+ FACTS_CLOSE
3311
+ ];
3312
+ }
3313
+ __name(factsPrompt, "factsPrompt");
3502
3314
  var errandCommunicate = /* @__PURE__ */ __name((contact) => ({
3503
3315
  name: "communicate",
3504
3316
  description: "List Canon conversations or agents, or send text to your errand's recipient or to the conversation it came from" + (contact ? ". Start the conversation with the recipient first, with start_conversation." : "."),
@@ -3527,21 +3339,6 @@ var inputMessage = /* @__PURE__ */ __name((message2, trusted = false) => Object.
3527
3339
  ...message2.replyTo ? { replyTo: message2.replyTo } : {},
3528
3340
  createdAt: message2.createdAt ?? ""
3529
3341
  }), "inputMessage");
3530
- function outputOptions(turnId, trigger, kind, actedForOwner = false, messageId = `${turnId}:${kind}`) {
3531
- return {
3532
- messageId,
3533
- ...trigger.replyAuthority ? { replyAuthority: trigger.replyAuthority } : {},
3534
- ...trigger.activeSelfContextId ? { selfContextId: trigger.activeSelfContextId } : {},
3535
- metadata: {
3536
- turnId,
3537
- turnSemantics: "turn_complete",
3538
- ...kind === "final" ? {} : { replyBehavior: "suppress_auto_reply" },
3539
- ...kind === "final" && actedForOwner ? { [ACTED_FOR_OWNER]: true } : {}
3540
- }
3541
- };
3542
- }
3543
- __name(outputOptions, "outputOptions");
3544
- var ACTED_FOR_OWNER = "actedForOwner";
3545
3342
  var MAX_TEAM = 20;
3546
3343
  function approvalScope(request) {
3547
3344
  if (request.scopeKey !== void 0 && /^[A-Za-z0-9_:-]{1,64}$/.test(request.scopeKey)) return request.scopeKey;
@@ -3552,19 +3349,6 @@ function approvalScope(request) {
3552
3349
  __name(approvalScope, "approvalScope");
3553
3350
  var MAX_GROUP_PARTS = 100;
3554
3351
  var CUT_SHORT_NOTE = "(This reply was too long to send in full and was cut short.)";
3555
- var EXPIRED_UPLOAD = ["MEDIA_UPLOAD_TEMPORARY_EXPIRED", "MEDIA_UPLOAD_EXPIRED"];
3556
- function fitGroup(text3, files) {
3557
- const room = Math.max(1, MAX_GROUP_PARTS - files);
3558
- const count = /* @__PURE__ */ __name((value) => value ? splitTextByUtf8Bytes(value).length : 0, "count");
3559
- let fitted = text3;
3560
- for (let end = Math.floor(text3.length * Math.min(1, room / Math.max(1, count(text3)))); count(fitted) > room; end = Math.floor(end * 0.95)) {
3561
- fitted = `${text3.slice(0, end).replace(/[\uD800-\uDBFF]$/, "").trimEnd()}
3562
-
3563
- ${CUT_SHORT_NOTE}`;
3564
- }
3565
- return fitted;
3566
- }
3567
- __name(fitGroup, "fitGroup");
3568
3352
  function fitEncrypted(text3) {
3569
3353
  if (Buffer.byteLength(text3) <= MLS_DEV_TEXT_MAX_BYTES) return text3;
3570
3354
  const room = MLS_DEV_TEXT_MAX_BYTES - Buffer.byteLength(`
@@ -3582,6 +3366,27 @@ ${CUT_SHORT_NOTE}`);
3582
3366
  ${CUT_SHORT_NOTE}`;
3583
3367
  }
3584
3368
  __name(fitEncrypted, "fitEncrypted");
3369
+ function encryptedParts(text3, limit) {
3370
+ if (Buffer.byteLength(text3) <= MLS_DEV_TEXT_MAX_BYTES) return [text3];
3371
+ const parts = splitTextWithinBytes(text3, MLS_DEV_TEXT_MAX_BYTES).filter((part) => part.trim());
3372
+ if (parts.length <= limit) return parts;
3373
+ const room = MLS_DEV_TEXT_MAX_BYTES - Buffer.byteLength(`
3374
+
3375
+ ${CUT_SHORT_NOTE}`);
3376
+ let bytes = 0;
3377
+ let end = 0;
3378
+ for (const char of parts[limit - 1]) {
3379
+ bytes += Buffer.byteLength(char);
3380
+ if (bytes > room) break;
3381
+ end += char.length;
3382
+ }
3383
+ return [...parts.slice(0, limit - 1), `${parts[limit - 1].slice(0, end).trimEnd()}
3384
+
3385
+ ${CUT_SHORT_NOTE}`];
3386
+ }
3387
+ __name(encryptedParts, "encryptedParts");
3388
+ var MAX_REPLY_PARTS = 32;
3389
+ var REPLY_GRANT_RETRIES = 6;
3585
3390
  var ENCRYPTED_FILES = "Files cannot be sent in encrypted conversations yet";
3586
3391
  var ERRAND_RETRY_MS = 5e3;
3587
3392
  var STOPPED_ERRAND = {
@@ -3593,14 +3398,10 @@ var STOPPED_ERRAND = {
3593
3398
  };
3594
3399
  var SUBMITTED = { phase: "submitted", settle: /* @__PURE__ */ __name(async () => void 0, "settle"), release: /* @__PURE__ */ __name(async () => void 0, "release"), cancel: /* @__PURE__ */ __name(async () => void 0, "cancel") };
3595
3400
  var SILENT = { open() {
3596
- }, setMode() {
3597
- }, text() {
3598
- }, activity() {
3599
3401
  }, waiting() {
3600
- }, refresh() {
3601
3402
  }, end() {
3602
3403
  } };
3603
- var UNAVAILABLE_ENCRYPTED = ["react", "send_file", "ask_question"];
3404
+ var UNAVAILABLE_ENCRYPTED = ["send_file", "ask_question"];
3604
3405
  var carriesCards = /* @__PURE__ */ __name((audience) => mlsDevProtocolCarriesCards(audience?.protocol), "carriesCards");
3605
3406
  var carriesItems = /* @__PURE__ */ __name((audience) => audience?.protocol === MLS_DEV_TEXT_PROTOCOL_V3, "carriesItems");
3606
3407
  function runtimeDescriptor(driver) {
@@ -3641,14 +3442,6 @@ async function createRunner(options) {
3641
3442
  const mentionAliases = names(options.mentionAliases, "mentionAliases").map((name) => name.replace(/^@+/, "")).filter(Boolean);
3642
3443
  const descriptor = runtimeDescriptor(driver);
3643
3444
  const offersQuestions = !driver.capabilities.nativeQuestions && ["choose", "text"].every((kind) => driver.capabilities.ask.includes(kind));
3644
- const capabilities = {
3645
- supportsInterrupt: true,
3646
- supportsInputInterrupt: false,
3647
- supportsQueue: true,
3648
- supportsInterleave: false,
3649
- supportsRequiresAction: driver.capabilities.ask.length > 0,
3650
- supportsNonFinalPermanentMessages: false
3651
- };
3652
3445
  const rooms = /* @__PURE__ */ new Map();
3653
3446
  const running = /* @__PURE__ */ new Set();
3654
3447
  const detach = [];
@@ -3663,7 +3456,6 @@ async function createRunner(options) {
3663
3456
  let tools;
3664
3457
  let lock;
3665
3458
  let owner;
3666
- const approvals = /* @__PURE__ */ new Map();
3667
3459
  let roots = [];
3668
3460
  let mlsOn = false;
3669
3461
  let mlsAsks;
@@ -3672,12 +3464,14 @@ async function createRunner(options) {
3672
3464
  if (!mlsIds) throw new Error("MLS_AUTHORITY_REQUIRED");
3673
3465
  return mlsIds;
3674
3466
  }, "keyed");
3467
+ let outboundClosed = false;
3675
3468
  let ownershipVersion;
3676
3469
  let displayName;
3470
+ let ownerOf;
3677
3471
  const refused = /* @__PURE__ */ new Set();
3678
3472
  const deferredNotes = /* @__PURE__ */ new Set();
3473
+ const ruleNotes = /* @__PURE__ */ new Set();
3679
3474
  const guards = /* @__PURE__ */ new Map();
3680
- const relayed = /* @__PURE__ */ new Set();
3681
3475
  const relaySends = /* @__PURE__ */ new Map();
3682
3476
  let errands;
3683
3477
  let errandInbox2;
@@ -3706,17 +3500,10 @@ async function createRunner(options) {
3706
3500
  const settle = /* @__PURE__ */ __name(async (ids, reason) => (await Promise.allSettled(ids.map((id) => endpoint.setInboundState(id, "settled", reason)))).every(({ status }) => status === "fulfilled"), "settle");
3707
3501
  const queued = /* @__PURE__ */ __name((turn) => turn.inputs.filter((input) => input.handle.phase === "queued"), "queued");
3708
3502
  const turnId = /* @__PURE__ */ __name((roomId, inputs) => `turn-${sha2563(JSON.stringify([roomId, ...inputs.map((input) => [input.id, input.generation])])).slice(0, 32)}`, "turnId");
3709
- const livePublisher = /* @__PURE__ */ __name((current, turn, mode) => createLivePublisher({
3503
+ const livePublisher = /* @__PURE__ */ __name((current, turn) => createLivePublisher({
3710
3504
  client,
3711
3505
  conversationId: current.id,
3712
- mode,
3713
3506
  turnId: /* @__PURE__ */ __name(() => turn?.id ?? null, "turnId"),
3714
- context: /* @__PURE__ */ __name(() => ({
3715
- queueDepth: current.queue.length,
3716
- currentSpeakerId: agentId,
3717
- capabilities,
3718
- activeMessageIds: turn?.inputs.map(({ payload }) => payload.message.id) ?? []
3719
- }), "context"),
3720
3507
  enqueue: /* @__PURE__ */ __name((publish2) => {
3721
3508
  current.lane = current.lane.then(publish2).catch(() => void 0);
3722
3509
  }, "enqueue")
@@ -3731,15 +3518,18 @@ async function createRunner(options) {
3731
3518
  function refuse(roomId, why) {
3732
3519
  if (refused.has(roomId)) return;
3733
3520
  refused.add(roomId);
3734
- log(`Refusing encrypted room ${roomId}: ${why === "no-device" ? "this runner has no MLS device (run it with --mls-dev-config)" : why === "remote" ? `the ${driver.name} driver runs off this host, which an encrypted room allows only in relay mode` : "its encryption protocol is not supported"}. Its messages are not delivered to this agent.`);
3521
+ log(why === "plaintext" ? `Refusing room ${roomId}: it is not encrypted, and this runner takes part only in encrypted rooms. Its messages are not delivered to this agent.` : `Refusing encrypted room ${roomId}: ${why === "no-device" ? "this runner has no MLS device (run it with --mls-dev-config)" : why === "remote" ? `the ${driver.name} driver runs off this host, which an encrypted room allows only in relay mode` : "its encryption protocol is not supported"}. Its messages are not delivered to this agent.`);
3735
3522
  }
3736
3523
  __name(refuse, "refuse");
3737
3524
  async function authorize() {
3738
3525
  await endpoint.mlsDevDevice.ensureReady();
3739
3526
  const me = await client.getAgentMe();
3740
- requireMlsSupport({ environmentId: options.environmentId, agent: me });
3527
+ requireMlsSupport({ environmentId: options.environmentId });
3741
3528
  if (me.agentId !== agentId || me.ownershipVersion !== ownershipVersion) throw new Error("MLS_AUTHORITY_REQUIRED");
3529
+ outboundClosed = me.outboundPolicy === "closed";
3530
+ if (outboundClosed && options.relay) throw new Error("MLS_AUTHORITY_REQUIRED");
3742
3531
  displayName = me.displayName;
3532
+ ownerOf = { id: me.ownerId, ...me.ownerName ? { name: me.ownerName } : {} };
3743
3533
  return me.ownerId;
3744
3534
  }
3745
3535
  __name(authorize, "authorize");
@@ -3790,55 +3580,53 @@ ${error instanceof Error ? error.message : String(error)}`;
3790
3580
  if (!input) return;
3791
3581
  if (stopping) return void await handle.release("runner-stopping");
3792
3582
  const target = room(input.payload.conversationId);
3793
- if (input.mls) target.mls = true;
3794
3583
  target.queue.push(input);
3795
- target.turn?.live.refresh();
3796
3584
  requeue(target);
3797
3585
  pump();
3798
3586
  }
3799
3587
  __name(offer, "offer");
3800
3588
  async function admitInput(event, handle) {
3801
3589
  const payload = event.data;
3590
+ const { conversationId } = payload;
3802
3591
  const generation = (await endpoint.getInbound(event.id))?.handoffGeneration ?? "";
3803
- const mode = await endpoint.getConversationContentMode(payload.conversationId).catch((error) => {
3804
- if (error.code === "MLS_DEV_ROOM_MODE_UNAVAILABLE") return "plaintext";
3592
+ let mode;
3593
+ try {
3594
+ mode = await endpoint.getConversationContentMode(conversationId);
3595
+ } catch (error) {
3596
+ if (error.code !== "MLS_DEV_ROOM_MODE_UNAVAILABLE") throw error;
3597
+ if (!(await client.getConversation(conversationId))?.memberIds?.includes(agentId)) return void await handle.cancel("membership-lost");
3805
3598
  throw error;
3806
- });
3807
- if (mode === "plaintext") {
3808
- if (options.relay) {
3809
- if (!relayed.has(payload.conversationId)) log(`Relay mode: leaving plaintext room ${payload.conversationId} to the connector.`);
3810
- relayed.add(payload.conversationId);
3811
- return void await handle.cancel("relay-plaintext-room");
3812
- }
3813
- if (payload.turnDispatch && payload.turnDispatch.kind !== "run_turn") return void await handle.cancel("observe-only");
3814
- return { id: event.id, payload, handle, generation };
3815
3599
  }
3816
3600
  if (mode !== "mls-dev-text" || !mlsOn || driver.capabilities.remote && !options.relay) {
3817
- refuse(payload.conversationId, !mlsOn ? "no-device" : mode !== "mls-dev-text" ? "protocol" : "remote");
3818
- return void await handle.cancel("encrypted-room-refused");
3601
+ refuse(conversationId, mode === "plaintext" ? "plaintext" : !mlsOn ? "no-device" : mode !== "mls-dev-text" ? "protocol" : "remote");
3602
+ return void await handle.cancel(mode === "plaintext" ? "plaintext-room-refused" : "encrypted-room-refused");
3819
3603
  }
3820
- const { conversationId } = payload;
3821
3604
  const groups = endpoint.mlsDevGroups;
3822
3605
  await authorize();
3823
3606
  const binding = await groups.readVerifiedMessageBinding({ conversationId, messageId: payload.message.id });
3824
3607
  if (!binding) throw new Error("MLS_INPUT_NOT_VERIFIED");
3825
3608
  const { messages } = await endpoint.query("get_messages", { conversationId, messageIds: [binding.messageId] });
3826
3609
  const [message2] = messages;
3827
- if (messages.length !== 1 || message2.id !== binding.messageId || message2.senderId !== binding.senderId || message2.senderType !== binding.senderPrincipal.type || typeof message2.text !== "string" && !(message2.text === null && (message2.attachments?.length ?? 0) === 1)) throw new Error("MLS_INPUT_NOT_VERIFIED");
3610
+ if (messages.length !== 1 || message2.id !== binding.messageId || message2.senderId !== binding.senderId || message2.senderType !== binding.senderPrincipal.type || !Number.isSafeInteger(message2.mlsFeedOrdinal) || typeof message2.text !== "string" && !(message2.text === null && (message2.attachments?.length ?? 0) === 1)) throw new Error("MLS_INPUT_NOT_VERIFIED");
3828
3611
  const { audience, ownerId } = await currentAudience(conversationId);
3829
3612
  if (binding.groupId !== audience.groupId) throw new Error("MLS_INPUT_NOT_VERIFIED");
3830
3613
  const obsolete = binding.head.sequence < audience.head.sequence;
3831
3614
  if (!obsolete && JSON.stringify(binding.head) !== JSON.stringify(audience.head)) throw new Error("MLS_INPUT_AHEAD_OF_AUDIENCE");
3832
3615
  if (obsolete) return void await handle.settle("mls-obsolete-audience");
3833
3616
  const fromAgent = binding.senderPrincipal.type !== "human";
3834
- if (fromAgent) {
3835
- const wake = agentWake(conversationId, event.id, message2.senderId, message2.text ?? "", message2.mentions ?? []);
3836
- if (wake !== "admit") return void await handle.settle(wake);
3837
- } else {
3617
+ const mentioned = (message2.mentions ?? []).includes(agentId) || mentionsAgent(message2.text ?? "", [displayName, agentId, ...mentionAliases]);
3618
+ if (fromAgent && (message2.senderId === agentId || !mentioned)) return void await handle.settle("mls-observe-only-sender");
3619
+ if (!fromAgent) {
3838
3620
  const target = room(conversationId);
3839
3621
  target.agentInputs = void 0;
3840
3622
  target.capped = false;
3841
3623
  }
3624
+ const rule = await withheld(conversationId, message2, fromAgent, mentioned, ownerId);
3625
+ if (rule) return void await handle.settle(rule);
3626
+ if (fromAgent) {
3627
+ const wake = agentWake(conversationId, event.id);
3628
+ if (wake !== "admit") return void await handle.settle(wake);
3629
+ }
3842
3630
  const verified = {
3843
3631
  id: message2.id,
3844
3632
  senderId: message2.senderId,
@@ -3855,8 +3643,48 @@ ${error instanceof Error ? error.message : String(error)}`;
3855
3643
  return { id: event.id, handle, generation, mls: audience, payload: { conversationId, message: verified } };
3856
3644
  }
3857
3645
  __name(admitInput, "admitInput");
3858
- function agentWake(roomId, inputId, senderId, text3, mentions) {
3859
- if (senderId === agentId || !mentions.includes(agentId) && !mentionsAgent(text3, [displayName, agentId, ...mentionAliases])) return "mls-observe-only-sender";
3646
+ async function withheld(roomId, message2, fromAgent, mentioned, ownerId) {
3647
+ const current = await endpoint.query(
3648
+ "conversation",
3649
+ { conversationId: roomId }
3650
+ );
3651
+ if (current?.blockedMemberIds?.includes(message2.senderId)) return "mls-blocked-sender";
3652
+ if (options.relay || !current?.behavior) return void 0;
3653
+ const { maxConsecutiveAgentTurns, allowLongRunningCollaboration } = current.behavior.participation;
3654
+ const decision = evaluateParticipationPolicy(current.behavior, {
3655
+ conversationType: resolveConversationPolicyScope(current.memberIds),
3656
+ senderType: fromAgent ? "ai_agent" : "human",
3657
+ isOwner: !fromAgent && message2.senderId === ownerId,
3658
+ mentionedAgent: mentioned,
3659
+ ...fromAgent ? agentRun(await turnsBehind(roomId, message2, Math.max(maxConsecutiveAgentTurns ?? 0, allowLongRunningCollaboration ? 0 : 2))) : {}
3660
+ });
3661
+ if (decision.allow) return void 0;
3662
+ const note = `${roomId}
3663
+ ${decision.reasonCode}`;
3664
+ if (!ruleNotes.has(note)) log(`The rules of ${roomId} keep messages from a turn: ${decision.reasonCode}.`);
3665
+ ruleNotes.add(note);
3666
+ return `mls-${decision.reasonCode}`;
3667
+ }
3668
+ __name(withheld, "withheld");
3669
+ async function turnsBehind(roomId, message2, limit) {
3670
+ const turns = [message2];
3671
+ let before = String(message2.mlsFeedOrdinal);
3672
+ while (before && turns.length <= limit) {
3673
+ const page = await endpoint.query(
3674
+ "list_messages",
3675
+ { conversationId: roomId, limit: PARTICIPATION_HISTORY_FETCH_LIMIT, before }
3676
+ );
3677
+ for (const entry of page.messages) {
3678
+ if (entry.contentType === "interaction" || entry.senderId === turns.at(-1).senderId) continue;
3679
+ turns.push(entry);
3680
+ if (entry.senderType !== "ai_agent" || turns.length > limit) return turns;
3681
+ }
3682
+ before = page.nextBefore;
3683
+ }
3684
+ return turns;
3685
+ }
3686
+ __name(turnsBehind, "turnsBehind");
3687
+ function agentWake(roomId, inputId) {
3860
3688
  const target = room(roomId);
3861
3689
  const streak = target.agentInputs ??= /* @__PURE__ */ new Set();
3862
3690
  if (!streak.has(inputId) && streak.size >= agentTurnCap) {
@@ -3896,12 +3724,10 @@ ${error instanceof Error ? error.message : String(error)}`;
3896
3724
  continue;
3897
3725
  }
3898
3726
  if (!candidate.queue.length) continue;
3899
- const encrypted = !!candidate.queue[0].mls;
3900
- const other = candidate.queue.findIndex((input) => !!input.mls !== encrypted);
3901
- const inputs = candidate.queue.splice(0, Math.min(MAX_INPUTS_PER_TURN, other < 0 ? MAX_INPUTS_PER_TURN : other));
3727
+ const inputs = candidate.queue.splice(0, MAX_INPUTS_PER_TURN);
3902
3728
  const turn = newTurn(inputs, turnId(candidate.id, inputs), candidate.id);
3903
3729
  candidate.turn = turn;
3904
- (turn.live = driver.capabilities.remote ? SILENT : livePublisher(candidate, turn, encrypted ? "presence" : "status")).open();
3730
+ (turn.live = driver.capabilities.remote ? SILENT : livePublisher(candidate, turn)).open();
3905
3731
  const done = execute(candidate, turn).catch((error) => {
3906
3732
  warn("turn failed")(error);
3907
3733
  return dispose(queued(turn), "release", "turn-failed");
@@ -3918,6 +3744,32 @@ ${error instanceof Error ? error.message : String(error)}`;
3918
3744
  while (localErrands.length && !stopping && running.size < maxTurns) startErrand(void 0, localErrands.shift());
3919
3745
  }
3920
3746
  __name(pump, "pump");
3747
+ async function roomFacts(conversation) {
3748
+ const { members } = await endpoint.query("get_members", { conversationId: conversation.id });
3749
+ const names2 = new Map(members.map(({ userId, displayName: name }) => [userId, name]));
3750
+ const owner2 = Object.freeze(ownerOf);
3751
+ const participation = conversation.behavior?.participation;
3752
+ return Object.freeze({
3753
+ ...conversation.name ? { name: conversation.name } : {},
3754
+ ...conversation.topic ? { topic: conversation.topic } : {},
3755
+ owner: owner2,
3756
+ members: Object.freeze(conversation.memberIds.map((id) => Object.freeze({
3757
+ id,
3758
+ ...names2.get(id) ? { name: names2.get(id) } : {},
3759
+ type: conversation.participantTypes?.[id] === "ai_agent" || id === agentId ? "ai_agent" : "human",
3760
+ isOwner: id === owner2.id,
3761
+ isYou: id === agentId
3762
+ }))),
3763
+ ...participation ? { rules: Object.freeze({
3764
+ style: participation.style,
3765
+ requireMentionForGroupReplies: participation.requireMentionForGroupReplies,
3766
+ allowAgentToAgent: participation.allowAgentToAgent,
3767
+ allowLongRunningCollaboration: participation.allowLongRunningCollaboration,
3768
+ maxConsecutiveAgentTurns: participation.maxConsecutiveAgentTurns ?? null
3769
+ }) } : {}
3770
+ });
3771
+ }
3772
+ __name(roomFacts, "roomFacts");
3921
3773
  async function execute(current, turn) {
3922
3774
  let conversation;
3923
3775
  try {
@@ -3928,12 +3780,18 @@ ${error instanceof Error ? error.message : String(error)}`;
3928
3780
  }
3929
3781
  if (!conversation?.memberIds?.includes(agentId)) return loseMembership(current);
3930
3782
  const kind = conversation.memberIds.length > 2 ? "group" : "direct";
3931
- if (turn.inputs[0].mls && !await beforeTurn(current, turn)) return;
3783
+ if (!await beforeTurn(current, turn)) return;
3784
+ if (!options.relay) {
3785
+ try {
3786
+ turn.facts = await roomFacts(conversation);
3787
+ } catch (error) {
3788
+ warn("room read failed; input returns to the inbox")(error);
3789
+ return dispose(queued(turn), "release", "room-unavailable");
3790
+ }
3791
+ }
3932
3792
  turn.trusted = await confirmTrusted(turn, conversation.memberIds);
3933
3793
  turn.notes = pendingNotes(current.id);
3934
- turn.contexts = await Promise.all(openErrands(current.id).map(async (record) => ({ record, encryptedOrigin: !!record.origin && await endpoint.getConversationContentMode(record.origin).then((mode2) => mode2 === "mls-dev-text", () => false) })));
3935
- const mode = turn.mls ? "presence" : driver.capabilities.streaming && resolveTurnVerbosity({ conversationType: kind }) === "verbose" ? "full" : "status";
3936
- turn.live.setMode(mode);
3794
+ turn.contexts = openErrands(current.id);
3937
3795
  const history = driver.capabilities.sessionMemory === "none" ? await recentHistory(current, turn) : [];
3938
3796
  await materialize(current, turn);
3939
3797
  const bound = bindTools(current, turn);
@@ -3944,7 +3802,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3944
3802
  if (live.length && !turn.controller.signal.aborted) {
3945
3803
  turn.inputs = live;
3946
3804
  turn.id = turnId(current.id, live);
3947
- turn.pickUp = () => pickUp(current, turn, mode);
3805
+ turn.pickUp = () => pickUp(current, turn);
3948
3806
  try {
3949
3807
  work = Promise.resolve(driver.runTurn(buildTurn(current.id, turn, kind, history, bound.tools)));
3950
3808
  } catch (error) {
@@ -3990,7 +3848,7 @@ ${error instanceof Error ? error.message : String(error)}`;
3990
3848
  turn.ended.abort();
3991
3849
  bound.release();
3992
3850
  clearTimeout(turn.timer);
3993
- await deliver(current, turn, outcome, audienceOf(conversation));
3851
+ await deliver(current, turn, outcome);
3994
3852
  } finally {
3995
3853
  turn.ended.abort();
3996
3854
  bound.release();
@@ -4001,15 +3859,10 @@ ${error instanceof Error ? error.message : String(error)}`;
4001
3859
  function started(current, turn) {
4002
3860
  turn.submitted = true;
4003
3861
  progress(turn);
4004
- if (!turn.mls) void client.markAsRead(current.id).catch(warn("mark read"));
4005
- for (const { payload } of turn.mls ? [] : turn.inputs) {
4006
- if (payload.message.metadata?.inboundDisposition === "queued") {
4007
- void client.updateMessageDisposition(current.id, payload.message.id, "accepted_now").catch(warn("accepted_now"));
4008
- }
4009
- }
3862
+ if (!options.relay) void client.markAsRead(current.id).catch(warn("mark read"));
4010
3863
  }
4011
3864
  __name(started, "started");
4012
- async function pickUp(current, turn, mode) {
3865
+ async function pickUp(current, turn) {
4013
3866
  const live = queued(turn);
4014
3867
  if (live.length !== turn.inputs.length) {
4015
3868
  abort(turn, "requeue");
@@ -4019,7 +3872,7 @@ ${error instanceof Error ? error.message : String(error)}`;
4019
3872
  if (result.status === "submitted") {
4020
3873
  turn.pickUp = void 0;
4021
3874
  started(current, turn);
4022
- if (!turn.controller.signal.aborted) (turn.live = livePublisher(current, turn, mode)).open();
3875
+ if (!turn.controller.signal.aborted) (turn.live = livePublisher(current, turn)).open();
4023
3876
  }
4024
3877
  return result;
4025
3878
  });
@@ -4033,20 +3886,19 @@ ${error instanceof Error ? error.message : String(error)}`;
4033
3886
  __name(pickUp, "pickUp");
4034
3887
  function requeue(current) {
4035
3888
  const turn = current.turn, next = current.queue[0];
4036
- if (!turn?.pickUp || turn.pickingUp || turn.controller.signal.aborted || !next) return;
4037
- if (!!next.mls !== !!turn.inputs[0].mls || turn.inputs.length >= MAX_INPUTS_PER_TURN) return;
3889
+ if (!turn?.pickUp || turn.pickingUp || turn.controller.signal.aborted || !next || turn.inputs.length >= MAX_INPUTS_PER_TURN) return;
4038
3890
  abort(turn, "requeue");
4039
3891
  }
4040
3892
  __name(requeue, "requeue");
4041
3893
  async function confirmTrusted(turn, memberIds) {
4042
- if (!trustedAgents.size || driver.capabilities.remote || options.relay) return /* @__PURE__ */ new Set();
4043
- const candidates = turn.inputs.map(({ payload }) => payload.message).filter((message2) => message2.senderType === "ai_agent" && message2.senderId !== agentId && trustedAgents.has(message2.senderId) && (turn.mls ? true : message2.metadata?.[ACTED_FOR_OWNER] === true && message2.metadata.turnSemantics === "turn_complete" && message2.forwarded !== true));
3894
+ const audience = turn.mls;
3895
+ if (!trustedAgents.size || driver.capabilities.remote || options.relay || !audience) return /* @__PURE__ */ new Set();
3896
+ const candidates = turn.inputs.map(({ payload }) => payload.message).filter((message2) => message2.senderType === "ai_agent" && message2.senderId !== agentId && trustedAgents.has(message2.senderId));
4044
3897
  if (!candidates.length) return /* @__PURE__ */ new Set();
4045
- const roster = turn.mls?.roster ?? [];
4046
- if (roster.some((entry) => isConnectorRelayInstallation(entry.device.installationId))) return /* @__PURE__ */ new Set();
4047
- const rosterIds = turn.mls ? await Promise.all(roster.map(async (entry) => await endpoint.mlsAccountOf(turn.mls.conversationId, entry.device.principal) ?? "")) : [];
3898
+ if (audience.roster.some((entry) => isConnectorRelayInstallation(entry.device.installationId))) return /* @__PURE__ */ new Set();
3899
+ const rosterIds = await Promise.all(audience.roster.map(async (entry) => await endpoint.mlsAccountOf(audience.conversationId, entry.device.principal) ?? ""));
4048
3900
  if (rosterIds.includes("")) return /* @__PURE__ */ new Set();
4049
- const member = /* @__PURE__ */ __name((id) => memberIds.includes(id) && (!turn.mls || rosterIds.includes(id)), "member");
3901
+ const member = /* @__PURE__ */ __name((id) => memberIds.includes(id) && rosterIds.includes(id), "member");
4050
3902
  if (candidates.some((message2) => !member(message2.senderId))) return /* @__PURE__ */ new Set();
4051
3903
  try {
4052
3904
  const { ownerId } = await client.getAgentMe();
@@ -4174,35 +4026,30 @@ ${error instanceof Error ? error.message : String(error)}`;
4174
4026
  };
4175
4027
  const cards = carriesCards(turn.mls), items = carriesItems(turn.mls);
4176
4028
  const errand = turn.errand;
4177
- const sendsFiles = roots.length > 0 && actedForOwner(turn);
4029
+ const sendsFiles = roots.length > 0 && forOwner2(turn);
4178
4030
  const binding = {
4179
- // §6: an encrypted turn gets no_reply and a communicate that only lists and sends encrypted text to its own room,
4180
- // and in a room created for cards, ask_question. §12.6: an errand turn reaches its recipient and its origin, and
4181
- // asks only in an origin its report may go to.
4031
+ // §6: a turn gets no_reply, a communicate that only lists and sends encrypted text to its own room, and react, which
4032
+ // a relay's credential cannot do; in a room created for cards, ask_question, and in one that carries items, send_file.
4033
+ // §12.6: an errand turn reaches its recipient and its origin, and asks only in an origin its report goes to.
4182
4034
  definitions: errand ? [
4183
4035
  TOOLS.no_reply,
4184
4036
  errandCommunicate("contact" in errand.target),
4185
- ...errand.reports && (!turn.mls || cards) && offersQuestions ? [TOOLS.ask_question] : []
4186
- ] : turn.mls ? [
4037
+ ...errand.reports && cards && offersQuestions ? [TOOLS.ask_question] : []
4038
+ ] : [
4187
4039
  TOOLS.no_reply,
4188
4040
  ENCRYPTED_COMMUNICATE,
4041
+ ...options.relay ? [] : [TOOLS.react],
4189
4042
  ...items && sendsFiles ? [TOOLS.send_file] : [],
4190
4043
  ...cards && offersQuestions ? [TOOLS.ask_question] : []
4191
- ] : [
4192
- TOOLS.no_reply,
4193
- canonCommunicateToolDefinition(),
4194
- TOOLS.react,
4195
- ...sendsFiles ? [TOOLS.send_file] : [],
4196
- ...offersQuestions ? [TOOLS.ask_question] : []
4197
4044
  ],
4198
4045
  async call(name, args) {
4199
4046
  if (!turn.open) throw turnEnded2();
4200
4047
  if (name === "no_reply" && turn.silenced) return { result: toolText(NO_REPLY_ACK_NOTE) };
4201
4048
  if (turn.controller.signal.aborted || turn.silenced) throw turnEnded2();
4202
- if (turn.mls && !errand && UNAVAILABLE_ENCRYPTED.includes(name) && !(name === "ask_question" && cards) && !(name === "send_file" && items)) {
4049
+ if (!errand && UNAVAILABLE_ENCRYPTED.includes(name) && !(name === "ask_question" && cards) && !(name === "send_file" && items)) {
4203
4050
  return { result: toolText(`${name} is not available in encrypted conversations yet.`, true) };
4204
4051
  }
4205
- if (name === "send_file" && !errand && roots.length && !actedForOwner(turn)) {
4052
+ if (name === "send_file" && !errand && roots.length && !forOwner2(turn)) {
4206
4053
  return { result: toolText("send_file sends files only in a turn answering your owner.", true) };
4207
4054
  }
4208
4055
  const handler = binding.definitions.some((definition) => definition.name === name) ? handlers[name] : void 0;
@@ -4223,11 +4070,9 @@ ${error instanceof Error ? error.message : String(error)}`;
4223
4070
  }, "call") } };
4224
4071
  }
4225
4072
  __name(bindTools, "bindTools");
4226
- const sealed = /* @__PURE__ */ __name((turn) => turn.errand ? "room" in turn.errand.target && !!turn.errand.target.mls : !!turn.mls, "sealed");
4227
4073
  async function communicate(current, turn, args) {
4228
4074
  const errand = turn.errand;
4229
- const origins = errand ? errand.origin ? [errand.origin.room] : [] : turn.contexts.flatMap(({ record }) => record.origin ? [record.origin] : []);
4230
- if (!errand && !sealed(turn) && !origins.length) return executeCanonCommunicateTool(client, args);
4075
+ const origins = errand ? errand.origin ? [errand.origin.room] : [] : turn.contexts.flatMap((record) => record.origin ? [record.origin] : []);
4231
4076
  let command;
4232
4077
  try {
4233
4078
  command = parseCommunicateToolInput(args);
@@ -4239,19 +4084,14 @@ ${error instanceof Error ? error.message : String(error)}`;
4239
4084
  }
4240
4085
  if (command.action === "message_existing" && command.text.trim()) {
4241
4086
  const to = command.conversationId;
4242
- if (turn.mls && !errand && to === current.id) return sealedSend(turn, to, command.text, turn.mls);
4087
+ if (!errand && to === current.id) return sealedSend(turn, to, command.text, turn.mls);
4243
4088
  if (errand && "room" in errand.target && to === errand.target.room) {
4244
4089
  reach(turn, to);
4245
- return errand.target.mls ? sealedSend(turn, to, command.text) : executeCanonCommunicateTool(client, args);
4090
+ return sealedSend(turn, to, command.text);
4246
4091
  }
4247
4092
  if (origins.includes(to)) {
4248
- const mode = await endpoint.getConversationContentMode(to);
4249
- if (mode === "mls-dev-text" && mlsOn) return sealedSend(turn, to, command.text);
4250
- if (mode !== "plaintext") return toolText(`${to} is an encrypted conversation this runner cannot write to.`, true);
4251
- if (sealed(turn)) {
4252
- return toolText(errand ? `This errand's recipient is in an encrypted conversation, so nothing of it may go to ${to}, which is not encrypted. Your final answer stays on your owner's machine as your report.` : `Nothing from this encrypted conversation may go to ${to}: it is not encrypted, and Canon must never receive this conversation's content in plaintext. Your owner sees on their machine that the errand was answered.`, true);
4253
- }
4254
- return executeCanonCommunicateTool(client, args);
4093
+ if (await endpoint.getConversationContentMode(to) === "mls-dev-text" && mlsOn) return sealedSend(turn, to, command.text);
4094
+ return toolText(`${to} is not an encrypted conversation this runner can write to.`, true);
4255
4095
  }
4256
4096
  }
4257
4097
  if (errand && "contact" in errand.target && (command.action === "start_conversation" || command.action === "start_direct") && command.principalId === errand.target.contact) {
@@ -4261,8 +4101,7 @@ ${error instanceof Error ? error.message : String(error)}`;
4261
4101
  ${JSON.stringify(result, null, 2)}`);
4262
4102
  }
4263
4103
  if (errand) return toolText("On an errand, communicate can only list conversations or agents, or send text to the errand's recipient or its origin.", true);
4264
- if (sealed(turn)) return toolText("In an encrypted conversation, communicate can only list conversations or agents, or send text to this conversation.", true);
4265
- return executeCanonCommunicateTool(client, args);
4104
+ return toolText("In an encrypted conversation, communicate can only list conversations or agents, or send text to this conversation.", true);
4266
4105
  }
4267
4106
  __name(communicate, "communicate");
4268
4107
  async function sealedSend(turn, roomId, text3, audience) {
@@ -4283,9 +4122,10 @@ ${JSON.stringify(result, null, 2)}`);
4283
4122
  notice: false,
4284
4123
  mls: mls2
4285
4124
  };
4286
- journal.save(record);
4287
- const sent2 = await publish(record);
4125
+ saveOutput(record);
4126
+ const sent2 = await publish(record, turn.retire.signal);
4288
4127
  if (sent2 !== "pending") journal.remove(messageId);
4128
+ else if (turn.retire.signal.aborted) await stopOutput(record);
4289
4129
  return sent2;
4290
4130
  })().catch((error) => {
4291
4131
  turn.said.delete(messageId);
@@ -4309,60 +4149,6 @@ ${JSON.stringify(result, null, 2)}`);
4309
4149
  }
4310
4150
  }
4311
4151
  __name(admitFile, "admitFile");
4312
- async function attach(conversationId, turn, candidates) {
4313
- const attachments = [];
4314
- const flows = [];
4315
- const refused2 = [];
4316
- const seen = /* @__PURE__ */ new Set();
4317
- for (const candidate of candidates) {
4318
- const file = await admitFile(candidate);
4319
- if (typeof file === "string") {
4320
- refused2.push(typeof candidate === "string" ? basename4(candidate) : "a file");
4321
- continue;
4322
- }
4323
- if (seen.has(file.path)) continue;
4324
- seen.add(file.path);
4325
- if (attachments.length >= MAX_GROUP_PARTS - 1) {
4326
- refused2.push(basename4(file.path));
4327
- continue;
4328
- }
4329
- try {
4330
- const media = await client.getFileMediaPipeline(options.fetch);
4331
- const attachmentId = `${turn.id}-${seen.size}`;
4332
- const [prepared] = await media.prepare({ conversationId, attachmentId, files: [{
4333
- source: file.path,
4334
- descriptor: { mimeType: inferUploadMimeType(file.path), sizeBytes: file.size, fileName: basename4(file.path) }
4335
- }] }, { signal: turn.retire.signal });
4336
- attachments.push(prepared);
4337
- flows.push(`${conversationId}:upload:${attachmentId}`);
4338
- } catch (error) {
4339
- if (turn.retire.signal.aborted) break;
4340
- warn("file upload")(error);
4341
- refused2.push(basename4(file.path));
4342
- }
4343
- }
4344
- return { attachments, flows, refused: refused2 };
4345
- }
4346
- __name(attach, "attach");
4347
- const releaseUploads = /* @__PURE__ */ __name((attachments = []) => void (attachments.length && client.getFileMediaPipeline(options.fetch).then((media) => Promise.all(attachments.map((attachment) => attachment.uploadId && media.releasePreparedUpload(attachment.uploadId)))).catch(warn("release upload"))), "releaseUploads");
4348
- async function renew(flows, signal) {
4349
- const media = await client.getFileMediaPipeline(options.fetch);
4350
- const renewed = [];
4351
- for (const id of flows) {
4352
- let prepared;
4353
- const off = media.subscribe((flow) => {
4354
- if (flow.id === id && flow.status === "prepared") prepared = flow.files[0]?.attachment;
4355
- });
4356
- try {
4357
- await media.resume(id, { signal });
4358
- } finally {
4359
- off();
4360
- }
4361
- renewed.push(prepared);
4362
- }
4363
- return renewed;
4364
- }
4365
- __name(renew, "renew");
4366
4152
  async function recentHistory(current, turn) {
4367
4153
  const first = turn.inputs[0].payload.message;
4368
4154
  try {
@@ -4373,8 +4159,8 @@ ${JSON.stringify(result, null, 2)}`);
4373
4159
  const ownerId = driver.capabilities.ownerHistoryOnly ? (await client.getAgentMe()).ownerId : void 0;
4374
4160
  const allowed = /* @__PURE__ */ __name((message2) => ownerId === void 0 || message2.senderId === agentId || message2.senderId === ownerId, "allowed");
4375
4161
  const window = recent.filter((message2, index) => before(message2, index) && !message2.deleted && !skip.has(message2.id) && allowed(message2)).slice(0, HISTORY_WINDOW);
4376
- const sameAudience = turn.mls ? await Promise.all(window.map((message2) => endpoint.mlsDevGroups.readVerifiedMessageBinding({ conversationId: current.id, messageId: message2.id }).then((binding) => !!binding && JSON.stringify(binding.head) === JSON.stringify(turn.mls.head), () => false))) : void 0;
4377
- return window.filter((_, index) => !sameAudience || sameAudience[index]).reverse();
4162
+ const sameAudience = await Promise.all(window.map((message2) => endpoint.mlsDevGroups.readVerifiedMessageBinding({ conversationId: current.id, messageId: message2.id }).then((binding) => !!binding && JSON.stringify(binding.head) === JSON.stringify(turn.mls.head), () => false)));
4163
+ return window.filter((_, index) => sameAudience[index]).reverse();
4378
4164
  } catch (error) {
4379
4165
  warn("history read failed; the turn runs without it")(error);
4380
4166
  return [];
@@ -4394,8 +4180,9 @@ ${JSON.stringify(result, null, 2)}`);
4394
4180
  return void 0;
4395
4181
  }
4396
4182
  __name(outcomeOf, "outcomeOf");
4397
- async function deliver(current, turn, outcome, audience) {
4183
+ async function deliver(current, turn, outcome) {
4398
4184
  if (turn.reason === "shutdown") return turn.live.end("stopped");
4185
+ const audience = turn.mls;
4399
4186
  const result = outcome && "result" in outcome ? outcome.result : void 0;
4400
4187
  const stopped = /* @__PURE__ */ __name(() => turn.retire.signal.aborted, "stopped");
4401
4188
  const silent = !stopped() && (turn.silenced !== void 0 || !turn.reason && result?.reply === null);
@@ -4403,141 +4190,108 @@ ${JSON.stringify(result, null, 2)}`);
4403
4190
  const failed = !stopped() && !silent && !answered2;
4404
4191
  if (failed && outcome && "error" in outcome) warn("the runtime failed the turn")(outcome.error);
4405
4192
  let text3 = answered2 ? result.reply : failed ? FAILURE_NOTICE : null;
4406
- let attachments = [];
4407
- let flows = [];
4408
4193
  let items = [];
4409
4194
  const resultFiles = answered2 && Array.isArray(result.files) ? result.files : [];
4410
- if (answered2 && turn.mls && carriesItems(turn.mls)) {
4195
+ if (answered2 && carriesItems(audience)) {
4411
4196
  const admitted = await admitItems(current.id, turn, [...turn.outbox, ...resultFiles]);
4412
4197
  items = admitted.items;
4413
4198
  if (admitted.refused.length) text3 = `${text3}
4414
4199
 
4415
4200
  (Could not attach ${admitted.refused.join(", ")}.)`;
4416
- } else if (answered2 && turn.mls && resultFiles.length) {
4201
+ } else if (answered2 && resultFiles.length) {
4417
4202
  text3 = `${text3}
4418
4203
 
4419
4204
  (${ENCRYPTED_FILES}: ${resultFiles.map((file) => typeof file === "string" ? basename4(file) : "a file").join(", ")}.)`;
4420
- } else if (answered2 && !turn.mls) {
4421
- const files = await attach(current.id, turn, [...turn.outbox, ...resultFiles]);
4422
- ({ attachments, flows } = files);
4423
- if (files.refused.length) text3 = `${text3}
4424
-
4425
- (Could not attach ${files.refused.join(", ")}.)`;
4426
4205
  }
4427
4206
  let sent;
4428
4207
  if (text3 !== null && !stopped()) {
4429
4208
  const kind = failed ? "failed" : "final";
4209
+ const replyAuthority = await replyGrant(current.id, turn.inputs.at(-1).payload.message.id, stopped);
4430
4210
  const record = {
4431
4211
  version: 1,
4432
4212
  turnId: turn.id,
4433
4213
  conversationId: current.id,
4434
4214
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4435
- audience,
4215
+ audience: null,
4436
4216
  inputs: turn.inputs.map((input) => input.id),
4437
4217
  text: text3,
4438
- ...attachments.length ? { attachments, flows } : {},
4439
4218
  ...items.length ? { items } : {},
4440
- options: outputOptions(
4441
- turn.id,
4442
- turn.inputs.at(-1).payload,
4443
- kind,
4444
- !turn.mls && actedForOwner(turn),
4445
- turn.mls ? keyed().text(kind, current.id, turn.id) : void 0
4446
- ),
4219
+ options: { messageId: keyed().text(kind, current.id, turn.id), ...replyAuthority ? { replyAuthority } : {} },
4447
4220
  notice: failed,
4448
- ...turn.mls ? { mls: turn.mls } : {}
4221
+ mls: audience
4449
4222
  };
4450
- journal.save(record);
4223
+ saveOutput(record);
4451
4224
  sent = await publish(record, turn.retire.signal);
4452
- attachments = record.attachments ?? [];
4453
- if (sent === "pending" && !stopped()) return turn.live.end(turn.reason && (!turn.mls || STOPS.includes(turn.reason)) ? "stopped" : "ended");
4225
+ if (sent === "pending" && !stopped()) return turn.live.end(turn.reason && STOPS.includes(turn.reason) ? "stopped" : "ended");
4226
+ if (sent === "pending") await stopOutput(record);
4454
4227
  }
4455
4228
  turn.live.end(sent === "delivered" ? "answered" : stopped() ? "stopped" : "ended");
4456
- releaseUploads(attachments);
4457
4229
  if (failed && outcome && "error" in outcome && outcome.error instanceof DriverSessionError) {
4458
4230
  await resetSession(turn.session).catch(warn("session reset"));
4459
4231
  }
4460
- if (turn.mls && (turn.reason === "audience" || turn.reason === "membership")) {
4232
+ if (turn.reason === "audience" || turn.reason === "membership") {
4461
4233
  await resetSession(turn.session).catch(warn("session reset"));
4462
4234
  if (current.session === turn.session) current.session = void 0;
4463
4235
  }
4464
- if (silent && !turn.mls) reportNoReplyOutcome(client, { conversationId: current.id, messageId: turn.inputs.at(-1).payload.message.id, ...turn.silenced });
4465
4236
  const reason = stopped() ? turn.reason : silent ? "silent" : answered2 ? "answered" : "failed";
4466
4237
  if (await settle(turn.inputs.map((input) => input.id), reason) && sent !== void 0) journal.remove(turn.id);
4467
4238
  }
4468
4239
  __name(deliver, "deliver");
4469
- const actedForOwner = /* @__PURE__ */ __name((turn) => turn.inputs.every(({ payload: { message: message2 } }) => message2.isOwner === true || message2.senderType === "ai_agent" && turn.trusted.has(message2.id)), "actedForOwner");
4470
- async function sendWithFiles(record, text3, options2, signal) {
4471
- const root = options2.messageId;
4472
- const register = /* @__PURE__ */ __name((attachments, replaces) => {
4473
- const bodies = [
4474
- ...attachments.map((attachment) => ({ text: "", contentType: attachment.kind, attachments: [attachment] })),
4475
- ...(text3 ? splitTextByUtf8Bytes(text3) : []).map((part) => ({ text: part }))
4476
- ];
4477
- const parts = bodies.map(({ text: partText, ...body }, index) => ({ text: partText, options: {
4478
- ...options2,
4479
- ...body,
4480
- messageId: `${root}-part-${index + 1}`,
4481
- metadata: buildChunkedMessagePartMetadata({ groupId: root, metadata: options2.metadata, index: index + 1, count: bodies.length })
4482
- } }));
4483
- return client.prepareMessageGroup(record.conversationId, root, parts, { signal, ...replaces ? { operationId: sha2563(JSON.stringify([root, ...attachments.map((attachment) => attachment.uploadId)])), replacesOperationId: replaces } : {} }).then((group2) => ({ ...group2, count: parts.length }));
4484
- }, "register");
4485
- const original = await register(record.attachments);
4486
- let group = original;
4487
- const cancel = /* @__PURE__ */ __name(() => {
4488
- void client.cancelMessageGroup(group.id).catch(() => void 0);
4489
- }, "cancel");
4490
- signal?.addEventListener("abort", cancel, { once: true });
4491
- try {
4492
- for (; ; ) {
4493
- try {
4494
- for (let index = 0; index < group.count; index += 1) await group.sendPart(index, { signal });
4495
- return;
4496
- } catch (error) {
4497
- if (!record.flows || !EXPIRED_UPLOAD.includes(error.code ?? "")) throw error;
4498
- const renewed = await renew(record.flows, signal).catch((failure) => {
4499
- throw failure.retryable === false ? error : failure;
4500
- });
4501
- if (renewed.some((attachment) => !attachment) || renewed.every((attachment, index) => attachment.uploadId === record.attachments[index].uploadId)) throw error;
4502
- record.attachments = renewed;
4503
- group = await register(record.attachments, original.id);
4504
- }
4505
- }
4506
- } finally {
4507
- signal?.removeEventListener("abort", cancel);
4508
- }
4509
- }
4510
- __name(sendWithFiles, "sendWithFiles");
4240
+ const forOwner2 = /* @__PURE__ */ __name((turn) => turn.inputs.every(({ payload: { message: message2 } }) => message2.isOwner === true || message2.senderType === "ai_agent" && turn.trusted.has(message2.id)), "forOwner");
4511
4241
  async function publish(record, retire) {
4512
- const sent = await send(record.conversationId, record.text, record.options, retire, record);
4513
- if (sent !== "refused" || record.notice || record.mls || record.errand) return sent;
4514
- const notice = {
4515
- ...record.options,
4516
- messageId: `${record.options.messageId}:short`,
4517
- metadata: { ...record.options.metadata, replyBehavior: "suppress_auto_reply" }
4518
- };
4519
- return await send(record.conversationId, STOPS_SHORT_NOTICE, notice, retire) === "pending" ? "pending" : "refused";
4242
+ return send(record.text, record.options, record.mls, retire, record);
4520
4243
  }
4521
4244
  __name(publish, "publish");
4522
- async function send(conversationId, text3, options2, retire, record, mls2 = record?.mls) {
4245
+ async function send(text3, options2, mls2, retire, record) {
4246
+ const grant = options2.replyAuthority;
4247
+ const parts = encryptedMessages(mls2, text3, options2, record);
4523
4248
  const sent = await (async () => {
4524
4249
  for (let attempt = 0; ; attempt += 1) {
4525
- const files = record?.attachments?.length ?? 0;
4526
- const result = await untilHalt((mls2 ? sendEncryptedItems(mls2, record?.items ?? [], retire).then(() => sendEncrypted(mls2, options2.messageId, text3, retire)) : files ? sendWithFiles(record, fitGroup(text3, files), options2, retire) : sendMessageWithRetryChunked(client, conversationId, fitGroup(text3, 0), options2, { signal: retire })).then(() => "delivered", (error) => error));
4250
+ const result = await untilHalt(sendEncryptedItems(mls2, record?.items ?? [], retire).then(async () => {
4251
+ for (const part of parts) await sendEncrypted(mls2, part.messageId, part.text, retire, grant && { ...grant, turnId: options2.messageId, complete: part.last });
4252
+ }).then(() => "delivered", (error) => error));
4527
4253
  if (result === "delivered" || result === "pending") return result;
4528
- if (retire?.aborted || isPendingCanonOperation(result)) return "pending";
4529
- if (mls2 ? result.retryable === false : !isRetryableCanonDeliveryError(result)) {
4254
+ if (retire?.aborted || isPendingCanonOperation(result) && parts.length < 2) return "pending";
4255
+ if (result.retryable === false) {
4530
4256
  warn("Canon refused a reply")(result);
4531
4257
  return "refused";
4532
4258
  }
4533
4259
  if (await untilHalt(pause2(Math.min(3e4, 1e3 * 2 ** attempt))) === "pending") return "pending";
4534
4260
  }
4535
4261
  })();
4536
- if (mls2) forgetGuard(mls2, options2.messageId, sent);
4262
+ for (const part of parts) forgetGuard(mls2, part.messageId, sent);
4537
4263
  return sent;
4538
4264
  }
4539
4265
  __name(send, "send");
4540
- async function sendEncrypted(audience, messageId, text3, signal) {
4266
+ const partsOf = /* @__PURE__ */ __name((text3, options2) => encryptedParts(text3, options2.replyAuthority ? MAX_REPLY_PARTS : MAX_GROUP_PARTS), "partsOf");
4267
+ function saveOutput(record) {
4268
+ record.parts = partsOf(record.text, record.options).length;
4269
+ journal.save(record);
4270
+ }
4271
+ __name(saveOutput, "saveOutput");
4272
+ function encryptedMessages(mls2, text3, options2, record) {
4273
+ const all = record && record.parts === void 0 ? [fitEncrypted(text3)] : partsOf(text3, options2);
4274
+ return all.slice(0, record?.parts).map((part, index) => ({
4275
+ text: part,
4276
+ last: index === all.length - 1,
4277
+ messageId: index ? keyed().text("part", mls2.conversationId, options2.messageId, String(index)) : options2.messageId
4278
+ }));
4279
+ }
4280
+ __name(encryptedMessages, "encryptedMessages");
4281
+ async function stopOutput(record) {
4282
+ let queued2 = 0;
4283
+ for (const [index, { messageId }] of encryptedMessages(record.mls, record.text, record.options, record).entries()) {
4284
+ const status = (await endpoint.getOperation(encryptedOperation(record.mls, messageId)))?.status;
4285
+ if (status && status !== "succeeded" && status !== "rejected") queued2 = index + 1;
4286
+ }
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);
4292
+ }
4293
+ __name(stopOutput, "stopOutput");
4294
+ async function sendEncrypted(audience, messageId, text3, signal, replyAuthority) {
4541
4295
  const operationId = encryptedOperation(audience, messageId);
4542
4296
  if (!guards.has(operationId)) {
4543
4297
  guards.set(operationId, endpoint.registerDeliveryGuard(operationId, () => assertAudience(audience).then(() => true, (error) => {
@@ -4548,11 +4302,38 @@ ${JSON.stringify(result, null, 2)}`);
4548
4302
  if (!await endpoint.getOperation(operationId)) await assertAudience(audience);
4549
4303
  signal?.throwIfAborted();
4550
4304
  await endpoint.executeResult(
4551
- { action: "send_message", body: { conversationId: audience.conversationId, messageId, text: fitEncrypted(text3), contentType: "text" } },
4305
+ { action: "send_message", body: {
4306
+ conversationId: audience.conversationId,
4307
+ messageId,
4308
+ text: text3,
4309
+ contentType: "text",
4310
+ ...replyAuthority ? { replyAuthority } : {}
4311
+ } },
4552
4312
  { operationId, guardId: operationId }
4553
4313
  );
4554
4314
  }
4555
4315
  __name(sendEncrypted, "sendEncrypted");
4316
+ async function replyGrant(roomId, source, stopped) {
4317
+ for (let attempt = 0; !options.relay; attempt += 1) {
4318
+ try {
4319
+ await authorize();
4320
+ if (!outboundClosed) return void 0;
4321
+ const { changes } = await endpoint.query(
4322
+ "admit_messages",
4323
+ { conversationId: roomId, messageIds: [source] }
4324
+ );
4325
+ return changes[0]?.data?.replyAuthority;
4326
+ } catch (error) {
4327
+ if (attempt >= REPLY_GRANT_RETRIES || stopped() || halt.signal.aborted) {
4328
+ warn("no reply authority")(error);
4329
+ return void 0;
4330
+ }
4331
+ await untilHalt(pause2(1e3 * 2 ** attempt));
4332
+ }
4333
+ }
4334
+ return void 0;
4335
+ }
4336
+ __name(replyGrant, "replyGrant");
4556
4337
  async function sendEncryptedItems(audience, items, signal) {
4557
4338
  if (!items.length) return;
4558
4339
  const media = await client.getFileMediaPipeline(options.fetch);
@@ -4628,8 +4409,9 @@ ${JSON.stringify(result, null, 2)}`);
4628
4409
  messages: Object.freeze(messages),
4629
4410
  history: Object.freeze(history.map((message2) => inputMessage(message2))),
4630
4411
  ...turn.notes.length ? { ownerNotes: Object.freeze(turn.notes.map((note) => note.text)) } : {},
4631
- ...turn.contexts.length ? { errands: Object.freeze(turn.contexts.map(({ record }) => errandFacts(record))) } : {},
4632
- ...turn.errand ? { errand: errandFacts(turn.errand.record) } : {}
4412
+ ...turn.contexts.length ? { errands: Object.freeze(turn.contexts.map((record) => errandFacts(record))) } : {},
4413
+ ...turn.errand ? { errand: errandFacts(turn.errand.record) } : {},
4414
+ ...turn.facts ? { roomFacts: turn.facts } : {}
4633
4415
  });
4634
4416
  const line = /* @__PURE__ */ __name((message2, mark = "") => promptLine(message2, message2.senderId === agentId ? "You" : message2.senderName ?? message2.senderId, turn.media.get(message2.id), mark), "line");
4635
4417
  const open4 = /* @__PURE__ */ __name(() => turn.open && !turn.controller.signal.aborted, "open");
@@ -4638,25 +4420,21 @@ ${JSON.stringify(result, null, 2)}`);
4638
4420
  session: turn.session,
4639
4421
  // §12.6: an errand turn's prompt is its errand; the owner's message is only its input's record.
4640
4422
  prompt: turn.errand ? errandPrompt(turn.errand) : [
4423
+ ...turn.facts ? [...factsPrompt(turn.facts, kind), ""] : [],
4641
4424
  ...turn.notes.flatMap((note) => [NOTE_OPEN, defused(note.text), NOTE_CLOSE, ""]),
4642
- ...turn.contexts.flatMap((context) => [...contextPrompt(context, !!turn.mls), ""]),
4643
- ...history.length ? ["Earlier in this conversation (context, not new requests):", ...history.map((message2) => line(message2)), "", "New messages:"] : turn.notes.length || turn.contexts.length ? ["New messages:"] : [],
4425
+ ...turn.contexts.flatMap((record) => [...contextPrompt(record), ""]),
4426
+ ...history.length ? ["Earlier in this conversation (context, not new requests):", ...history.map((message2) => line(message2)), "", "New messages:"] : turn.facts || turn.notes.length || turn.contexts.length ? ["New messages:"] : [],
4644
4427
  ...turn.inputs.map(({ payload: { message: message2 } }) => line(message2, message2.isOwner === true ? OWNER_MARK : trustedSender(message2) ? TRUSTED_MARK : ""))
4645
4428
  ].join("\n"),
4646
4429
  input,
4647
4430
  files: turn.inputs.flatMap(({ payload: { message: message2 } }) => turn.media.get(message2.id)?.files.map((file) => file.path) ?? []),
4648
4431
  signal: turn.controller.signal,
4649
- onText: /* @__PURE__ */ __name((snapshot) => {
4650
- if (live()) {
4651
- progress(turn);
4652
- turn.live.text(snapshot);
4653
- }
4432
+ // Text and activity are progress (RUN-12), and nothing of them is published (§6).
4433
+ onText: /* @__PURE__ */ __name(() => {
4434
+ if (live()) progress(turn);
4654
4435
  }, "onText"),
4655
- onActivity: /* @__PURE__ */ __name((event) => {
4656
- if (live()) {
4657
- progress(turn);
4658
- turn.live.activity(event);
4659
- }
4436
+ onActivity: /* @__PURE__ */ __name(() => {
4437
+ if (live()) progress(turn);
4660
4438
  }, "onActivity"),
4661
4439
  ask: /* @__PURE__ */ __name((request) => live() ? ask(roomId, turn, input.sender, request) : Promise.reject(turnEnded2()), "ask"),
4662
4440
  state: driverState(turn.session, open4),
@@ -4675,7 +4453,7 @@ ${JSON.stringify(result, null, 2)}`);
4675
4453
  loseMembership(room(roomId));
4676
4454
  throw new Error("The agent is no longer a member of this conversation");
4677
4455
  }
4678
- if (turn.mls && await readAgain(() => reaudience(roomId, turn), Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS) === "changed") {
4456
+ if (await readAgain(() => reaudience(roomId, turn), Date.now() + CONNECTOR_RELAY_ANSWER_WITHIN_MS) === "changed") {
4679
4457
  if (!turn.controller.signal.aborted) {
4680
4458
  void dispose(queued(turn), "settle", "mls-obsolete-audience");
4681
4459
  abort(turn, "audience");
@@ -4698,68 +4476,32 @@ ${JSON.stringify(result, null, 2)}`);
4698
4476
  if (turn.errand && !turn.errand.reports) return Promise.resolve({ status: "cancelled" });
4699
4477
  const signal = AbortSignal.any([turn.controller.signal, turn.ended.signal]);
4700
4478
  turn.asking += 1;
4701
- if (!turn.mls) turn.live.waiting(true);
4702
4479
  const answer = (async () => {
4703
4480
  try {
4704
- if (turn.mls && (!carriesCards(turn.mls) || !mlsAsks)) throw new Error("approvals and questions need an encrypted conversation created for them");
4481
+ if (!turn.mls || !carriesCards(turn.mls) || !mlsAsks) throw new Error("approvals and questions need an encrypted conversation created for them");
4705
4482
  const ownerId = await (owner ??= client.getAgentMe().then((me) => me.ownerId, (error) => {
4706
4483
  owner = void 0;
4707
4484
  throw error;
4708
4485
  }));
4709
4486
  const secret = request.kind === "text" && request.secret === true;
4710
4487
  const responder = secret || sender.type !== "human" ? ownerId : sender.id;
4711
- const requestId = turn.mls ? keyed().request(roomId, turn.id, request.id) : `ask-${sha2563(JSON.stringify([turn.id, request.id])).slice(0, 32)}`;
4712
- const expiresAt = Date.now() + askExpiryMs;
4713
- if (turn.mls) {
4714
- return await mlsAsks.ask({
4715
- conversationId: roomId,
4716
- session: turn.session,
4717
- requestId,
4718
- responder,
4719
- ownerId,
4720
- request,
4721
- expiresAtMs: expiresAt,
4722
- audience: /* @__PURE__ */ __name(() => turn.mls, "audience"),
4723
- signal,
4724
- recheck: /* @__PURE__ */ __name(() => checkAudience(room(roomId)), "recheck"),
4725
- pending: /* @__PURE__ */ __name(() => turn.live.waiting(true), "pending"),
4726
- // Stop, and a new member (whose card is void for every device), withdraw a card, and so does the turn's end;
4727
- // shutdown leaves it to restart recovery, and an agent no longer in the room can send nothing.
4728
- ending: /* @__PURE__ */ __name(() => turn.reason === "shutdown" ? "keep" : turn.reason === "membership" ? "drop" : turn.reason && STOPS.includes(turn.reason) ? "stopped" : "turn_ended", "ending")
4729
- });
4730
- }
4731
- let manager = approvals.get(roomId);
4732
- if (!manager) approvals.set(roomId, manager = new ApprovalManager(client, agentId, ownerId));
4733
- const options2 = { requestId, expiresAt, signal };
4734
- if (request.kind === "approve") {
4735
- let created = false;
4736
- const result2 = await manager.requestApproval(roomId, approvalScope(request), {}, {
4737
- toolSummary: request.title,
4738
- responseUserId: responder,
4739
- turnId: turn.id,
4740
- ...request.detail ? { details: [{ label: "Details", value: request.detail.slice(0, 500) }] } : {},
4741
- ...request.risk ? { riskLevel: request.risk, risk: request.risk } : {},
4742
- ignoreSessionRules: request.allowSession !== true,
4743
- allowSessionRule: request.allowSession === true
4744
- }, { ...options2, onCreated: /* @__PURE__ */ __name(() => {
4745
- created = true;
4746
- }, "onCreated") });
4747
- if (created && !result2.respondedBy) return { status: "expired" };
4748
- return { status: "answered", by: result2.respondedBy ?? ownerId, allow: result2.decision === "allow", scope: !created || result2.sessionRule ? "session" : "once" };
4749
- }
4750
- const choices = request.kind === "choose" ? request.options.map(({ id, label }) => ({ label, value: id })) : void 0;
4751
- const result = await manager.request("input", roomId, {
4752
- kind: secret ? "secret" : "clarify",
4753
- prompt: request.question,
4754
- responseUserId: responder,
4755
- turnId: turn.id,
4756
- ...secret ? { sensitive: true } : {},
4757
- ...request.kind === "choose" && request.multiple ? { questions: [{ id: "choice", question: request.question, choices, multiSelect: true }] } : choices ? { choices } : {}
4758
- }, options2);
4759
- if (result.status !== "submitted") return { status: result.status === "timeout" ? "expired" : "cancelled" };
4760
- if (request.kind !== "choose") return { status: "answered", by: responder, text: result.value };
4761
- const picked = result.answers ? Object.values(result.answers).flatMap((entry) => entry.answers) : [result.value];
4762
- return { status: "answered", by: responder, choices: picked.filter((id) => request.options.some((option) => option.id === id)) };
4488
+ const requestId = keyed().request(roomId, turn.id, request.id);
4489
+ return await mlsAsks.ask({
4490
+ conversationId: roomId,
4491
+ session: turn.session,
4492
+ requestId,
4493
+ responder,
4494
+ ownerId,
4495
+ request,
4496
+ expiresAtMs: Date.now() + askExpiryMs,
4497
+ audience: /* @__PURE__ */ __name(() => turn.mls, "audience"),
4498
+ signal,
4499
+ recheck: /* @__PURE__ */ __name(() => checkAudience(room(roomId)), "recheck"),
4500
+ pending: /* @__PURE__ */ __name(() => turn.live.waiting(true), "pending"),
4501
+ // Stop, and a new member (whose card is void for every device), withdraw a card, and so does the turn's end;
4502
+ // shutdown leaves it to restart recovery, and an agent no longer in the room can send nothing.
4503
+ ending: /* @__PURE__ */ __name(() => turn.reason === "shutdown" ? "keep" : turn.reason === "membership" ? "drop" : turn.reason && STOPS.includes(turn.reason) ? "stopped" : "turn_ended", "ending")
4504
+ });
4763
4505
  } catch (error) {
4764
4506
  if (signal.aborted) throw turnEnded2();
4765
4507
  warn("ask could not be put to anyone")(error);
@@ -4812,20 +4554,13 @@ ${JSON.stringify(result, null, 2)}`);
4812
4554
  if (consumed.status !== "signal") return;
4813
4555
  const signal = consumed.signal;
4814
4556
  const turn = current.turn;
4815
- if (signal === "stop_and_drop" || signal === "new_session") {
4816
- const dropped = current.queue.splice(0);
4817
- for (const { payload } of current.mls ? [] : dropped) {
4818
- void client.updateMessageDisposition(current.id, payload.message.id, "rejected").catch(warn("rejected disposition"));
4819
- }
4820
- void dispose(dropped, "cancel", "stop-and-clear");
4821
- }
4557
+ if (signal === "stop_and_drop" || signal === "new_session") void dispose(current.queue.splice(0), "cancel", "stop-and-clear");
4822
4558
  if (turn) {
4823
4559
  void dispose(turn.inputs, "cancel", "stopped-before-submit");
4824
4560
  abort(turn, signal === "interrupt" ? "stop" : signal === "stop_and_drop" ? "drop" : "new_session");
4825
4561
  }
4826
- (turn?.live ?? livePublisher(current, void 0, current.mls ? "presence" : "full")).end("stopped");
4562
+ (turn?.live ?? livePublisher(current, void 0)).end("stopped");
4827
4563
  if (signal === "new_session") {
4828
- approvals.delete(current.id);
4829
4564
  const sessions = /* @__PURE__ */ new Set([
4830
4565
  turn?.session ?? current.session ?? current.id,
4831
4566
  current.session ?? (turn?.mls ? mlsSession(current.id, mlsAudienceEpoch(turn.mls)) : current.id)
@@ -4860,25 +4595,19 @@ ${JSON.stringify(result, null, 2)}`);
4860
4595
  const target = room(roomId);
4861
4596
  const work = (async () => {
4862
4597
  for (const record of records.filter((candidate) => candidate.conversationId === roomId)) {
4863
- if (record.mls) target.mls = true;
4864
4598
  await redeliver(record).catch(warn("journaled reply kept for reconciliation"));
4865
4599
  }
4866
4600
  const inputs = orphans.filter(({ event }) => event.conversationId === roomId).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.event.id.localeCompare(b.event.id));
4867
4601
  if (inputs.length) {
4868
4602
  const id = turnId(roomId, inputs.map(({ event, handoffGeneration }) => ({ id: event.id, generation: handoffGeneration ?? "" })));
4869
- const trigger = inputs.at(-1).event.data;
4870
4603
  const mode = await endpoint.getConversationContentMode(roomId);
4871
- if (mode !== "plaintext") target.mls = true;
4872
- if (mode !== "plaintext" && (!mlsOn || mode !== "mls-dev-text")) refuse(roomId, mlsOn ? "protocol" : "no-device");
4604
+ if (mode !== "mls-dev-text" || !mlsOn) refuse(roomId, mode === "plaintext" ? "plaintext" : mlsOn ? "protocol" : "no-device");
4873
4605
  const mls2 = mode === "mls-dev-text" && mlsOn ? (await currentAudience(roomId)).audience : void 0;
4874
4606
  const legacy = `${id}:restart`;
4875
4607
  const messageId = mls2 && !await endpoint.getOperation(encryptedOperation(mls2, legacy)) ? keyed().text("restart", roomId, id) : legacy;
4876
- if (mode !== "plaintext" && !mls2) await settle(inputs.map(({ event }) => event.id), "interrupted-by-restart");
4877
- else if (await send(roomId, RESTART_NOTICE, outputOptions(id, trigger, "restart", false, messageId), void 0, void 0, mls2) !== "pending") {
4878
- await settle(inputs.map(({ event }) => event.id), "interrupted-by-restart");
4879
- }
4608
+ if (!mls2 || await send(RESTART_NOTICE, { messageId }, mls2) !== "pending") await settle(inputs.map(({ event }) => event.id), "interrupted-by-restart");
4880
4609
  }
4881
- livePublisher(target, void 0, target.mls ? "presence" : "full").end("ended");
4610
+ livePublisher(target, void 0).end("ended");
4882
4611
  })().catch(warn("restart recovery"));
4883
4612
  target.recovering = work.finally(() => {
4884
4613
  target.recovering = void 0;
@@ -4888,24 +4617,16 @@ ${JSON.stringify(result, null, 2)}`);
4888
4617
  }
4889
4618
  __name(recover, "recover");
4890
4619
  async function redeliver(record) {
4891
- if (record.mls) {
4892
- if (!mlsOn) return refuse(record.conversationId, "no-device");
4893
- } else if (!await client.getMessageOperation(record.conversationId, record.options.messageId)) {
4894
- const members = audienceOf(await client.getConversation(record.conversationId));
4895
- try {
4896
- assertCompletedOutputAudience(record, members, "agent runner");
4897
- } catch (error) {
4898
- if (!record.errand) throw error;
4899
- return finishErrandOutput(record, "refused", "the conversation's members changed before it was sent");
4900
- }
4620
+ if (!record.mls) {
4621
+ 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;
4901
4624
  }
4625
+ if (!mlsOn) return refuse(record.conversationId, "no-device");
4902
4626
  const sent = await publish(record);
4903
4627
  if (sent === "pending") return;
4904
4628
  if (record.errand) return finishErrandOutput(record, sent);
4905
- if (await settle(record.inputs, "recovered")) {
4906
- journal.remove(record.turnId);
4907
- releaseUploads(record.attachments);
4908
- }
4629
+ if (await settle(record.inputs, "recovered")) journal.remove(record.turnId);
4909
4630
  }
4910
4631
  __name(redeliver, "redeliver");
4911
4632
  function pendingNotes(roomId) {
@@ -5003,15 +4724,15 @@ ${JSON.stringify(result, null, 2)}`);
5003
4724
  turnId: turnId2,
5004
4725
  conversationId,
5005
4726
  createdAt,
5006
- audience: target.audience,
4727
+ audience: null,
5007
4728
  inputs: [],
5008
4729
  text: parsed.text,
5009
- options: target.mls ? { messageId: keyed().text("final", conversationId, turnId2) } : { messageId: `${turnId2}:final`, metadata: { turnId: turnId2, turnSemantics: "turn_complete" } },
4730
+ options: { messageId: keyed().text("final", conversationId, turnId2) },
5010
4731
  notice: false,
5011
4732
  errand: id,
5012
- ...target.mls ? { mls: target.mls } : {}
4733
+ mls: target.mls
5013
4734
  };
5014
- journal.save(record);
4735
+ saveOutput(record);
5015
4736
  errands.put({ ...base, status: "sending", updatedAt });
5016
4737
  const delivery = publish(record).then((sent) => {
5017
4738
  if (sent !== "pending") finishErrandOutput(record, sent);
@@ -5030,9 +4751,9 @@ ${JSON.stringify(result, null, 2)}`);
5030
4751
  if (!conversation) return "Canon does not show the agent that conversation";
5031
4752
  if (!conversation.memberIds?.includes(agentId)) return "the agent is not a member of that conversation";
5032
4753
  const mode = await endpoint.getConversationContentMode(roomId).catch(missing);
5033
- if (mode === "plaintext") return { audience: audienceOf(conversation) };
4754
+ if (mode === "plaintext") return "it is not encrypted, and the agent takes part only in encrypted conversations";
5034
4755
  if (mode !== "mls-dev-text" || !mlsOn) return "it is an encrypted conversation this runner cannot take part in";
5035
- return { audience: null, ...kind === "post" ? { mls: (await currentAudience(roomId)).audience } : {} };
4756
+ return kind === "post" ? { mls: (await currentAudience(roomId)).audience } : {};
5036
4757
  }
5037
4758
  __name(errandRoom, "errandRoom");
5038
4759
  function finishErrandOutput(record, sent, reason = "Canon did not accept it") {
@@ -5075,8 +4796,8 @@ ${JSON.stringify(result, null, 2)}`);
5075
4796
  ...record.origin ? { origin: record.origin } : {}
5076
4797
  }), "errandFacts");
5077
4798
  function errandPrompt({ record, target, origin, reports }) {
5078
- const recipient = "contact" in target ? `${target.contact}. Start a new conversation with them: communicate start_conversation.` : `the ${target.mls ? "encrypted " : ""}conversation ${target.room}. Send to it with communicate message_existing.`;
5079
- const report = reports ? `your final answer is your report to your owner, posted in the conversation ${origin.room}.` : origin ? `your final answer is your report to your owner, and it stays on their machine: the conversation ${origin.room} is not encrypted, and nothing of an errand to an encrypted conversation may go there.` : "your final answer is your report to your owner, and it stays on their machine.";
4799
+ const recipient = "contact" in target ? `${target.contact}. Start a new conversation with them: communicate start_conversation.` : `the encrypted conversation ${target.room}. Send to it with communicate message_existing.`;
4800
+ const report = reports ? `your final answer is your report to your owner, posted in the conversation ${origin.room}.` : "your final answer is your report to your owner, and it stays on their machine.";
5080
4801
  return [
5081
4802
  `${ERRAND_OPEN} Your owner sends you on an errand, from the machine you run on. It is not a message in any conversation, and no one else sees it.`,
5082
4803
  `Why: ${defused(record.why)}`,
@@ -5088,8 +4809,8 @@ ${JSON.stringify(result, null, 2)}`);
5088
4809
  ].join("\n");
5089
4810
  }
5090
4811
  __name(errandPrompt, "errandPrompt");
5091
- function contextPrompt({ record, encryptedOrigin }, encryptedRoom) {
5092
- const report = !record.origin ? "Your owner follows this errand on their machine; it has no conversation to report to." : encryptedOrigin || !encryptedRoom ? `You may report to your owner in the ${encryptedOrigin ? "encrypted " : ""}conversation ${record.origin} with communicate.` : `Nothing from this encrypted conversation may go to ${record.origin}, which is not encrypted; your owner sees on their machine that the errand was answered.`;
4812
+ function contextPrompt(record) {
4813
+ const report = !record.origin ? "Your owner follows this errand on their machine; it has no conversation to report to." : `You may report to your owner in the encrypted conversation ${record.origin} with communicate.`;
5093
4814
  return [
5094
4815
  `${ERRAND_OPEN} Your owner sent you into this conversation on an errand, from the machine you run on. No one here sees this note.`,
5095
4816
  `Why: ${defused(record.why)}`,
@@ -5107,7 +4828,7 @@ ${JSON.stringify(result, null, 2)}`);
5107
4828
  }
5108
4829
  __name(openErrands, "openErrands");
5109
4830
  function replied(turn) {
5110
- for (const { record } of turn.contexts) {
4831
+ for (const record of turn.contexts) {
5111
4832
  const known = latest(record.id);
5112
4833
  if (!known || !openErrand(known)) continue;
5113
4834
  try {
@@ -5132,7 +4853,7 @@ ${JSON.stringify(result, null, 2)}`);
5132
4853
  const turn = newTurn([], errandTurnId(record.id), `errand:${record.id}`);
5133
4854
  holder.turn = turn;
5134
4855
  errandTurns.set(record.id, turn);
5135
- (turn.live = origin ? livePublisher(origin, turn, "presence") : SILENT).open();
4856
+ (turn.live = origin ? livePublisher(origin, turn) : SILENT).open();
5136
4857
  const done = executeErrand(holder, turn, record).catch(warn(`errand ${record.id}`)).finally(() => {
5137
4858
  turn.live.end("ended");
5138
4859
  if (holder.turn === turn) holder.turn = void 0;
@@ -5167,7 +4888,6 @@ ${JSON.stringify(result, null, 2)}`);
5167
4888
  if (typeof setup === "string") return void saveErrand({ ...known, status: "refused", reason: setup });
5168
4889
  turn.errand = setup;
5169
4890
  turn.mls = setup.origin?.mls;
5170
- if (turn.mls) current.mls = true;
5171
4891
  const message2 = {
5172
4892
  id: `${turn.id}-errand`,
5173
4893
  senderId: setup.owner,
@@ -5210,16 +4930,15 @@ ${JSON.stringify(result, null, 2)}`);
5210
4930
  if (record.target === "room") {
5211
4931
  const found = await errandRoom(record.to, "post");
5212
4932
  if (typeof found === "string") return `its recipient: ${found}`;
5213
- target = { room: record.to, ...found.mls ? { mls: found.mls } : {} };
4933
+ target = { room: record.to };
5214
4934
  }
5215
4935
  let origin;
5216
4936
  if (record.origin) {
5217
4937
  const found = await errandRoom(record.origin, "post");
5218
4938
  if (typeof found === "string") return `its origin: ${found}`;
5219
- origin = { room: record.origin, audience: found.audience, ...found.mls ? { mls: found.mls } : {} };
4939
+ origin = { room: record.origin, mls: found.mls };
5220
4940
  }
5221
- const reports = !!origin && (!!origin.mls || !("room" in target && target.mls));
5222
- return { record, owner: owner2, target, ...origin ? { origin } : {}, reports };
4941
+ return { record, owner: owner2, target, ...origin ? { origin } : {}, reports: !!origin };
5223
4942
  }
5224
4943
  __name(errandSetup, "errandSetup");
5225
4944
  async function deliverReport(turn, outcome) {
@@ -5252,19 +4971,16 @@ ${JSON.stringify(result, null, 2)}`);
5252
4971
  turnId: turn.id,
5253
4972
  conversationId: origin.room,
5254
4973
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5255
- audience: origin.audience,
4974
+ audience: null,
5256
4975
  inputs: [],
5257
4976
  text: text3,
5258
4977
  notice: failed,
5259
4978
  errand: record.id,
5260
- ...origin.mls ? { mls: origin.mls } : {},
5261
- options: origin.mls ? { messageId: keyed().text(kind, origin.room, turn.id) } : {
5262
- messageId: `${turn.id}:${kind}`,
5263
- metadata: { turnId: turn.id, turnSemantics: "turn_complete", ...failed ? { replyBehavior: "suppress_auto_reply" } : {} }
5264
- }
4979
+ mls: origin.mls,
4980
+ options: { messageId: keyed().text(kind, origin.room, turn.id) }
5265
4981
  };
5266
4982
  try {
5267
- journal.save(out);
4983
+ saveOutput(out);
5268
4984
  } catch (error) {
5269
4985
  warn(`errand ${record.id}: its report could not be journaled; it stays in the record`)(error);
5270
4986
  saveErrand({ ...closing, report: text3, reported: "refused", reason: "its report: it could not be saved for sending" });
@@ -5273,9 +4989,9 @@ ${JSON.stringify(result, null, 2)}`);
5273
4989
  saveErrand({ ...closing, reported: "sending" });
5274
4990
  const sent = await publish(out, turn.retire.signal);
5275
4991
  if (sent === "pending" && stopped()) {
5276
- journal.remove(out.turnId);
5277
- const { reported: _reported, ...withheld } = latest(record.id) ?? closing;
5278
- saveErrand({ ...withheld, status: "cancelled", reason: STOPPED_ERRAND[turn.reason] ?? "stopped" });
4992
+ await stopOutput(out);
4993
+ const { reported: _reported, ...withheld2 } = latest(record.id) ?? closing;
4994
+ saveErrand({ ...withheld2, status: "cancelled", reason: STOPPED_ERRAND[turn.reason] ?? "stopped" });
5279
4995
  } else if (sent !== "pending") finishErrandOutput(out, sent);
5280
4996
  turn.live.end(sent === "delivered" ? "answered" : stopped() ? "stopped" : "ended");
5281
4997
  }
@@ -5309,7 +5025,6 @@ ${JSON.stringify(result, null, 2)}`);
5309
5025
  } else if (event.kind === "conversation.updated") {
5310
5026
  void onRoomChanged(data).catch(warn("membership update"));
5311
5027
  } else if (event.kind === "runtime.control" && data.kind === "signal" && typeof data.conversationId === "string") {
5312
- if (options.relay && !rooms.get(data.conversationId)?.mls) return;
5313
5028
  scheduleControl(room(data.conversationId));
5314
5029
  }
5315
5030
  }
@@ -5373,7 +5088,7 @@ ${JSON.stringify(result, null, 2)}`);
5373
5088
  notice: false,
5374
5089
  mls: audience
5375
5090
  };
5376
- journal.save(record);
5091
+ saveOutput(record);
5377
5092
  return { text: text3, record };
5378
5093
  })();
5379
5094
  const sent = ready.then(({ record, settled }) => settled ?? publish(record).then((result) => {
@@ -5462,7 +5177,7 @@ ${JSON.stringify(result, null, 2)}`);
5462
5177
  }
5463
5178
  journal = createCompletedOutputJournal(join7(home, "runner", options.environmentId, agentId, "finals"), scope, {
5464
5179
  label: "agent runner",
5465
- validate: /* @__PURE__ */ __name((record) => typeof record.text === "string" && typeof record.options?.messageId === "string" && Array.isArray(record.inputs) && typeof record.notice === "boolean" && (record.attachments === void 0 || Array.isArray(record.attachments)) && (record.flows === void 0 || Array.isArray(record.flows) && record.flows.length === record.attachments?.length) && (record.mls === void 0 || !!record.mls && typeof record.mls === "object") && (record.errand === void 0 || typeof record.errand === "string"), "validate")
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")
5466
5181
  });
5467
5182
  if (!options.relay && !driver.capabilities.remote) {
5468
5183
  errands = errandJournal(errandsDirectory(home, options.environmentId, agentId));
@@ -5480,7 +5195,7 @@ ${JSON.stringify(result, null, 2)}`);
5480
5195
  else if (report) saveErrand({ ...record, status: ended(record, report.notice), reported: "sending" });
5481
5196
  else {
5482
5197
  saveErrand({ ...record, status: "interrupted", reason: "the runner stopped during its turn, which never runs again" });
5483
- if (record.origin) livePublisher(room(record.origin), void 0, "presence").end("stopped");
5198
+ if (record.origin) livePublisher(room(record.origin), void 0).end("stopped");
5484
5199
  }
5485
5200
  }
5486
5201
  }