@moxt-ai/mobius 0.0.24 → 0.0.26

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 (47) hide show
  1. package/dist/agent-catalog/publish-agent-catalog.d.ts.map +1 -1
  2. package/dist/agent-catalog/publish-agent-catalog.js +12 -7
  3. package/dist/agent-catalog/publish-agent-catalog.js.map +1 -1
  4. package/dist/cli.js +685 -290
  5. package/dist/cli.js.map +4 -4
  6. package/dist/config.d.ts +1 -1
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +3 -3
  9. package/dist/config.js.map +1 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +746 -254
  13. package/dist/index.js.map +4 -4
  14. package/dist/network/environment-proxy.d.ts +1 -0
  15. package/dist/network/environment-proxy.d.ts.map +1 -1
  16. package/dist/network/environment-proxy.js +47 -0
  17. package/dist/network/environment-proxy.js.map +1 -1
  18. package/dist/pairing/pair-local-machine.d.ts +8 -0
  19. package/dist/pairing/pair-local-machine.d.ts.map +1 -1
  20. package/dist/pairing/pair-local-machine.js +64 -37
  21. package/dist/pairing/pair-local-machine.js.map +1 -1
  22. package/dist/prompt-artifact-resolver.d.ts.map +1 -1
  23. package/dist/prompt-artifact-resolver.js +11 -3
  24. package/dist/prompt-artifact-resolver.js.map +1 -1
  25. package/dist/relay-connection/connect.d.ts.map +1 -1
  26. package/dist/relay-connection/connect.js +25 -9
  27. package/dist/relay-connection/connect.js.map +1 -1
  28. package/dist/relay-connection/diagnostics.d.ts +11 -0
  29. package/dist/relay-connection/diagnostics.d.ts.map +1 -0
  30. package/dist/relay-connection/diagnostics.js +122 -0
  31. package/dist/relay-connection/diagnostics.js.map +1 -0
  32. package/dist/relay-connection/liveness.d.ts +1 -1
  33. package/dist/relay-connection/liveness.d.ts.map +1 -1
  34. package/dist/relay-connection/liveness.js +2 -1
  35. package/dist/relay-connection/liveness.js.map +1 -1
  36. package/dist/relay-connection/session-event-outbox.d.ts +2 -1
  37. package/dist/relay-connection/session-event-outbox.d.ts.map +1 -1
  38. package/dist/relay-connection/session-event-outbox.js +48 -8
  39. package/dist/relay-connection/session-event-outbox.js.map +1 -1
  40. package/dist/run-daemon.d.ts.map +1 -1
  41. package/dist/run-daemon.js +193 -149
  42. package/dist/run-daemon.js.map +1 -1
  43. package/dist/runtime.d.ts +23 -0
  44. package/dist/runtime.d.ts.map +1 -0
  45. package/dist/runtime.js +31339 -0
  46. package/dist/runtime.js.map +7 -0
  47. package/package.json +6 -2
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ import { mkdir as mkdir5 } from "node:fs/promises";
11
11
  // package.json
12
12
  var package_default = {
13
13
  name: "@moxt-ai/mobius",
14
- version: "0.0.24",
14
+ version: "0.0.26",
15
15
  description: "Mobius CLI for pairing and running local AI agent services.",
16
16
  type: "module",
17
17
  bin: {
@@ -21,6 +21,10 @@ var package_default = {
21
21
  ".": {
22
22
  types: "./dist/index.d.ts",
23
23
  import: "./dist/index.js"
24
+ },
25
+ "./runtime": {
26
+ types: "./dist/runtime.d.ts",
27
+ import: "./dist/runtime.js"
24
28
  }
25
29
  },
26
30
  files: [
@@ -38,7 +42,7 @@ var package_default = {
38
42
  directory: "packages/cli"
39
43
  },
40
44
  scripts: {
41
- build: "tsc --project tsconfig.build.json && esbuild src/cli.ts src/index.ts --bundle --platform=node --format=esm --sourcemap --outdir=dist --external:@agentclientprotocol/* --external:undici",
45
+ build: "tsc --project tsconfig.build.json && esbuild src/cli.ts src/index.ts src/runtime.ts --bundle --platform=node --format=esm --sourcemap --outdir=dist --external:@agentclientprotocol/* --external:undici",
42
46
  dev: "node --watch src/cli.ts",
43
47
  prepack: "pnpm build",
44
48
  start: "node dist/cli.js",
@@ -1574,7 +1578,53 @@ import { fetch } from "undici";
1574
1578
 
1575
1579
  // src/network/environment-proxy.ts
1576
1580
  import { EnvHttpProxyAgent } from "undici";
1581
+ var configuredHttpProxy = process.env["http_proxy"] ?? process.env["HTTP_PROXY"] ?? "";
1582
+ var configuredHttpsProxy = process.env["https_proxy"] ?? process.env["HTTPS_PROXY"] ?? configuredHttpProxy;
1577
1583
  var environmentProxyDispatcher = new EnvHttpProxyAgent();
1584
+ function bypassesProxy(target) {
1585
+ const noProxy = process.env["no_proxy"] ?? process.env["NO_PROXY"] ?? "";
1586
+ if (noProxy === "*") {
1587
+ return true;
1588
+ }
1589
+ const hostname4 = target.hostname.replace(/^\[|\]$/gu, "").toLowerCase();
1590
+ const defaultPort = target.protocol === "wss:" || target.protocol === "https:" ? 443 : 80;
1591
+ const port = Number.parseInt(target.port, 10) || defaultPort;
1592
+ for (const rawEntry of noProxy.split(/[,\s]/u)) {
1593
+ if (rawEntry.length === 0) {
1594
+ continue;
1595
+ }
1596
+ const parsed = rawEntry.match(/^(.+):(\d+)$/u);
1597
+ const entryHostname = (parsed?.[1] ?? rawEntry).replace(/^\*?\./u, "").toLowerCase();
1598
+ const entryPort = parsed?.[2] === void 0 ? 0 : Number.parseInt(parsed[2], 10);
1599
+ if (entryPort !== 0 && entryPort !== port) {
1600
+ continue;
1601
+ }
1602
+ if (hostname4 === entryHostname || hostname4.endsWith(`.${entryHostname}`)) {
1603
+ return true;
1604
+ }
1605
+ }
1606
+ return false;
1607
+ }
1608
+ function sanitizedProxyOrigin(proxyUrl) {
1609
+ try {
1610
+ return new URL(proxyUrl).origin;
1611
+ } catch {
1612
+ return "configured-invalid-url";
1613
+ }
1614
+ }
1615
+ function describeEnvironmentProxyRoute(target) {
1616
+ if (bypassesProxy(target)) {
1617
+ return { proxyRoute: "direct" };
1618
+ }
1619
+ const proxyUrl = target.protocol === "wss:" || target.protocol === "https:" ? configuredHttpsProxy : configuredHttpProxy;
1620
+ if (proxyUrl.length === 0) {
1621
+ return { proxyRoute: "direct" };
1622
+ }
1623
+ return {
1624
+ proxyOrigin: sanitizedProxyOrigin(proxyUrl),
1625
+ proxyRoute: "proxy"
1626
+ };
1627
+ }
1578
1628
 
1579
1629
  // src/agent-installation/managed-agent-archive.ts
1580
1630
  var ARCHIVE_DOWNLOAD_LIMIT_BYTES = 128 * 1024 * 1024;
@@ -2578,54 +2628,6 @@ async function discoverLocalAgentInventory(options) {
2578
2628
  return new LocalAgentInventory(discoveries);
2579
2629
  }
2580
2630
 
2581
- // src/audit/audit-store.ts
2582
- import { mkdirSync } from "node:fs";
2583
- import { dirname as dirname3 } from "node:path";
2584
- import { DatabaseSync } from "node:sqlite";
2585
- var AuditStore = class {
2586
- database;
2587
- constructor(path2) {
2588
- mkdirSync(dirname3(path2), { recursive: true });
2589
- this.database = new DatabaseSync(path2);
2590
- this.database.exec(`
2591
- CREATE TABLE IF NOT EXISTS audit_events (
2592
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2593
- recorded_at TEXT NOT NULL,
2594
- event TEXT NOT NULL,
2595
- command_id TEXT NOT NULL,
2596
- session_id TEXT NOT NULL,
2597
- details_json TEXT NOT NULL DEFAULT '{}'
2598
- ) STRICT
2599
- `);
2600
- const columns = this.database.prepare("PRAGMA table_info(audit_events)").all();
2601
- if (!columns.some((column) => Reflect.get(column, "name") === "details_json")) {
2602
- this.database.exec("ALTER TABLE audit_events ADD COLUMN details_json TEXT NOT NULL DEFAULT '{}'");
2603
- }
2604
- }
2605
- record(record2) {
2606
- this.database.prepare(
2607
- `
2608
- INSERT INTO audit_events (
2609
- recorded_at,
2610
- event,
2611
- command_id,
2612
- session_id,
2613
- details_json
2614
- ) VALUES (?, ?, ?, ?, ?)
2615
- `
2616
- ).run(
2617
- (/* @__PURE__ */ new Date()).toISOString(),
2618
- record2.event,
2619
- record2.commandId,
2620
- record2.sessionId,
2621
- JSON.stringify(record2.details ?? {})
2622
- );
2623
- }
2624
- close() {
2625
- this.database.close();
2626
- }
2627
- };
2628
-
2629
2631
  // src/bots/bot-message-command.ts
2630
2632
  import { randomUUID } from "node:crypto";
2631
2633
  import { createConnection } from "node:net";
@@ -23249,14 +23251,15 @@ var encode4 = (message, schema) => {
23249
23251
  }
23250
23252
  return JSON.stringify(decoded.data);
23251
23253
  };
23254
+ var decodeSessionEventEnvelope = (encoded) => decode5(encoded, eventEnvelopeSchema);
23252
23255
  var encodeSessionEventEnvelope = (message) => encode4(message, eventEnvelopeSchema);
23253
23256
  var decodeSessionEventAcknowledgement = (encoded) => decode5(encoded, eventAcknowledgementSchema);
23254
23257
  var decodeSessionEventRejection = (encoded) => decode5(encoded, eventRejectionSchema);
23255
23258
 
23256
23259
  // src/config.ts
23257
- function loadDefaultWorkspace() {
23258
- const { MOBIUS_DEFAULT_WORKSPACE: configuredWorkspace } = process.env;
23259
- return configuredWorkspace ?? join13(readStateDirectory(), "workspace");
23260
+ function loadDefaultWorkspace(environment = process.env) {
23261
+ const { MOBIUS_DEFAULT_WORKSPACE: configuredWorkspace } = environment;
23262
+ return configuredWorkspace ?? join13(readStateDirectory(environment), "workspace");
23260
23263
  }
23261
23264
  function createDaemonConfig(connection, agents = [], installableAgents = []) {
23262
23265
  return {
@@ -23283,7 +23286,9 @@ var IDENTIFIER_PATTERN5 = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
23283
23286
  var TOKEN_PATTERN2 = /^[a-zA-Z0-9_-]{32,256}$/;
23284
23287
  var MAX_CONNECTION_FILE_LENGTH = 16 * 1024;
23285
23288
  var CLAIM_RESPONSE_KEYS = ["channelId", "daemonToken", "runtimeId"];
23286
- var SAVED_CONNECTION_KEYS = ["defaultWorkspace", "channelId", "daemonToken", "relayOrigin"];
23289
+ var REDEEM_RESPONSE_KEYS = ["channelId", "runtimeCredential", "runtimeId"];
23290
+ var LEGACY_SAVED_CONNECTION_KEYS = ["defaultWorkspace", "channelId", "daemonToken", "relayOrigin"];
23291
+ var SAVED_CONNECTION_KEYS = ["defaultWorkspace", "channelId", "relayOrigin", "runtimeCredential"];
23287
23292
  var PairingClientError = class extends Error {
23288
23293
  code;
23289
23294
  constructor(code, message, options = {}) {
@@ -23320,7 +23325,7 @@ function readSingleOption(arguments_, option2) {
23320
23325
  function assertKnownArguments(arguments_) {
23321
23326
  for (let index = 0; index < arguments_.length; index += 2) {
23322
23327
  const option2 = arguments_[index];
23323
- if (option2 !== "--url" && option2 !== "--code" && option2 !== "--workspace") {
23328
+ if (option2 !== "--url" && option2 !== "--code" && option2 !== "--pairing" && option2 !== "--workspace") {
23324
23329
  throw new PairingClientError("invalid_arguments", `Unknown pairing option: ${option2 ?? "missing option"}`);
23325
23330
  }
23326
23331
  }
@@ -23360,10 +23365,17 @@ function parsePairCommand(arguments_) {
23360
23365
  assertKnownArguments(arguments_);
23361
23366
  return {
23362
23367
  code: parsePairingCode(readSingleOption(arguments_, "--code")),
23368
+ pairingId: arguments_.includes("--pairing") ? validatePairingId(readSingleOption(arguments_, "--pairing")) : "",
23363
23369
  relayOrigin: parseRelayOrigin(readSingleOption(arguments_, "--url")),
23364
23370
  workspace: arguments_.includes("--workspace") ? readSingleOption(arguments_, "--workspace") : loadDefaultWorkspace()
23365
23371
  };
23366
23372
  }
23373
+ function validatePairingId(value) {
23374
+ if (!IDENTIFIER_PATTERN5.test(value)) {
23375
+ throw new PairingClientError("invalid_pairing_id", "The Runtime pairing ID is invalid");
23376
+ }
23377
+ return value;
23378
+ }
23367
23379
  function parseChannelArgument(arguments_) {
23368
23380
  if (arguments_.length !== 2 || arguments_[0] !== "--channel") {
23369
23381
  throw new PairingClientError("invalid_arguments", "Usage: mobius start --channel <channel-id>");
@@ -23420,17 +23432,17 @@ function readRelayError(value) {
23420
23432
  }
23421
23433
  return new PairingClientError("pairing_failed", "The relay could not pair this machine");
23422
23434
  }
23423
- function decodeClaimResponse(value) {
23424
- if (!isRecord7(value) || !hasExactKeys(value, CLAIM_RESPONSE_KEYS)) {
23435
+ function decodePairingResponse(value, credentialField, expectedKeys) {
23436
+ if (!isRecord7(value) || !hasExactKeys(value, expectedKeys)) {
23425
23437
  throw new PairingClientError("invalid_relay_response", "The relay returned an invalid pairing response");
23426
23438
  }
23427
23439
  const channelId = value["channelId"];
23428
- const daemonToken2 = value["daemonToken"];
23440
+ const runtimeCredential = value[credentialField];
23429
23441
  const runtimeId = value["runtimeId"];
23430
- if (typeof channelId !== "string" || !IDENTIFIER_PATTERN5.test(channelId) || typeof daemonToken2 !== "string" || !TOKEN_PATTERN2.test(daemonToken2) || typeof runtimeId !== "string" || !IDENTIFIER_PATTERN5.test(runtimeId)) {
23442
+ if (typeof channelId !== "string" || !IDENTIFIER_PATTERN5.test(channelId) || typeof runtimeCredential !== "string" || !TOKEN_PATTERN2.test(runtimeCredential) || typeof runtimeId !== "string" || !IDENTIFIER_PATTERN5.test(runtimeId)) {
23431
23443
  throw new PairingClientError("invalid_relay_response", "The relay returned an invalid pairing response");
23432
23444
  }
23433
- return { channelId, daemonToken: daemonToken2, runtimeId };
23445
+ return { channelId, runtimeCredential, runtimeId };
23434
23446
  }
23435
23447
  async function claimPairing(options, defaultWorkspace, localAgents, installableAgents, signal) {
23436
23448
  const agents = localAgents.map((agent) => ({
@@ -23440,7 +23452,7 @@ async function claimPairing(options, defaultWorkspace, localAgents, installableA
23440
23452
  id: agent.id,
23441
23453
  label: agent.label
23442
23454
  }));
23443
- const endpoint = mobiusApiUrl(options.relayOrigin, "/pairings/claim");
23455
+ const endpoint = options.pairingId.length > 0 ? mobiusApiUrl(options.relayOrigin, `/v1/runtime-pairings/${encodeURIComponent(options.pairingId)}/redeem`) : mobiusApiUrl(options.relayOrigin, "/pairings/claim");
23444
23456
  let response;
23445
23457
  try {
23446
23458
  response = await fetch2(endpoint, {
@@ -23463,10 +23475,10 @@ async function claimPairing(options, defaultWorkspace, localAgents, installableA
23463
23475
  if (!response.ok) {
23464
23476
  throw readRelayError(value);
23465
23477
  }
23466
- return decodeClaimResponse(value);
23478
+ return options.pairingId.length > 0 ? decodePairingResponse(value, "runtimeCredential", REDEEM_RESPONSE_KEYS) : decodePairingResponse(value, "daemonToken", CLAIM_RESPONSE_KEYS);
23467
23479
  }
23468
- async function persistConnection(channelId, relayOrigin, daemonToken2, defaultWorkspace) {
23469
- const connectionsDirectory = join14(readStateDirectory(), "connections");
23480
+ async function persistConnection(channelId, relayOrigin, runtimeCredential, defaultWorkspace, environment) {
23481
+ const connectionsDirectory = join14(readStateDirectory(environment), "connections");
23470
23482
  await mkdir3(connectionsDirectory, { mode: 448, recursive: true });
23471
23483
  const connectionPath = join14(connectionsDirectory, `${channelId}.json`);
23472
23484
  const temporaryPath = join14(connectionsDirectory, `.${channelId}.${randomUUID2()}.tmp`);
@@ -23476,8 +23488,8 @@ async function persistConnection(channelId, relayOrigin, daemonToken2, defaultWo
23476
23488
  {
23477
23489
  defaultWorkspace,
23478
23490
  channelId,
23479
- daemonToken: daemonToken2,
23480
- relayOrigin: relayOrigin.toString()
23491
+ relayOrigin: relayOrigin.toString(),
23492
+ runtimeCredential
23481
23493
  },
23482
23494
  null,
23483
23495
  2
@@ -23489,25 +23501,25 @@ async function persistConnection(channelId, relayOrigin, daemonToken2, defaultWo
23489
23501
  return connectionPath;
23490
23502
  }
23491
23503
  function decodeSavedConnection(value, expectedChannelId) {
23492
- if (!isRecord7(value) || !hasExactKeys(value, SAVED_CONNECTION_KEYS)) {
23504
+ if (!isRecord7(value) || !hasExactKeys(value, SAVED_CONNECTION_KEYS) && !hasExactKeys(value, LEGACY_SAVED_CONNECTION_KEYS)) {
23493
23505
  throw new PairingClientError("invalid_connection", "The saved connection is invalid");
23494
23506
  }
23495
23507
  const defaultWorkspace = value["defaultWorkspace"];
23496
23508
  const channelId = value["channelId"];
23497
- const daemonToken2 = value["daemonToken"];
23509
+ const runtimeCredential = value["runtimeCredential"] ?? value["daemonToken"];
23498
23510
  const relayOrigin = value["relayOrigin"];
23499
- if (typeof defaultWorkspace !== "string" || typeof channelId !== "string" || channelId !== expectedChannelId || !IDENTIFIER_PATTERN5.test(channelId) || typeof daemonToken2 !== "string" || !TOKEN_PATTERN2.test(daemonToken2) || typeof relayOrigin !== "string") {
23511
+ if (typeof defaultWorkspace !== "string" || typeof channelId !== "string" || channelId !== expectedChannelId || !IDENTIFIER_PATTERN5.test(channelId) || typeof runtimeCredential !== "string" || !TOKEN_PATTERN2.test(runtimeCredential) || typeof relayOrigin !== "string") {
23500
23512
  throw new PairingClientError("invalid_connection", "The saved connection is invalid");
23501
23513
  }
23502
23514
  return {
23503
23515
  defaultWorkspace,
23504
23516
  channelId,
23505
- daemonToken: daemonToken2,
23506
- relayOrigin: parseRelayOrigin(relayOrigin)
23517
+ relayOrigin: parseRelayOrigin(relayOrigin),
23518
+ runtimeCredential
23507
23519
  };
23508
23520
  }
23509
- async function readSavedConnection(channelId) {
23510
- const connectionPath = join14(readStateDirectory(), "connections", `${channelId}.json`);
23521
+ async function readSavedConnection(channelId, environment) {
23522
+ const connectionPath = join14(readStateDirectory(environment), "connections", `${channelId}.json`);
23511
23523
  let encoded = "";
23512
23524
  const handle = await open4(connectionPath, "r");
23513
23525
  try {
@@ -23529,8 +23541,8 @@ async function readSavedConnection(channelId) {
23529
23541
  return {
23530
23542
  defaultWorkspace: await resolveDefaultWorkspace(connection.defaultWorkspace),
23531
23543
  channelId: connection.channelId,
23532
- daemonToken: connection.daemonToken,
23533
- relayOrigin: connection.relayOrigin
23544
+ relayOrigin: connection.relayOrigin,
23545
+ runtimeCredential: connection.runtimeCredential
23534
23546
  };
23535
23547
  }
23536
23548
  function assertInventoryUsable(inventory) {
@@ -23538,15 +23550,12 @@ function assertInventoryUsable(inventory) {
23538
23550
  throw new PairingClientError("no_supported_agent", "No supported Agent is available for local discovery");
23539
23551
  }
23540
23552
  }
23541
- function buildPairedDaemon(connection, connectionPath, inventory) {
23553
+ function buildPairedDaemon(connection, connectionPath, inventory, environment) {
23542
23554
  assertInventoryUsable(inventory);
23543
- const relayUrl = mobiusApiUrl(
23544
- connection.relayOrigin,
23545
- `/channels/${encodeURIComponent(connection.channelId)}/daemon`
23546
- );
23555
+ const relayUrl = mobiusApiUrl(connection.relayOrigin, "/v1/runtimes/connect");
23547
23556
  relayUrl.protocol = relayUrl.protocol === "https:" ? "wss:" : "ws:";
23548
- relayUrl.searchParams.set("token", connection.daemonToken);
23549
- const auditDatabasePath = join14(readStateDirectory(), "audit", `${connection.channelId}.sqlite`);
23557
+ relayUrl.searchParams.set("token", connection.runtimeCredential);
23558
+ const auditDatabasePath = join14(readStateDirectory(environment), "audit", `${connection.channelId}.sqlite`);
23550
23559
  return {
23551
23560
  channelUrl: new URL(`/channels/${encodeURIComponent(connection.channelId)}`, connection.relayOrigin).toString(),
23552
23561
  config: createDaemonConfig(
@@ -23562,12 +23571,11 @@ function buildPairedDaemon(connection, connectionPath, inventory) {
23562
23571
  connectionPath
23563
23572
  };
23564
23573
  }
23565
- async function pairLocalMachine(arguments_, signal = new AbortController().signal) {
23566
- const options = parsePairCommand(arguments_);
23574
+ async function pairRuntime(options, signal, environment) {
23567
23575
  const defaultWorkspace = await resolveDefaultWorkspace(options.workspace);
23568
23576
  const inventory = await discoverLocalAgentInventory({
23569
23577
  defaultWorkspace,
23570
- environment: process.env,
23578
+ environment,
23571
23579
  signal
23572
23580
  });
23573
23581
  assertInventoryUsable(inventory);
@@ -23576,32 +23584,88 @@ async function pairLocalMachine(arguments_, signal = new AbortController().signa
23576
23584
  const connectionPath = await persistConnection(
23577
23585
  claim2.channelId,
23578
23586
  options.relayOrigin,
23579
- claim2.daemonToken,
23580
- defaultWorkspace
23587
+ claim2.runtimeCredential,
23588
+ defaultWorkspace,
23589
+ environment
23581
23590
  );
23582
23591
  return buildPairedDaemon(
23583
23592
  {
23584
23593
  defaultWorkspace,
23585
23594
  channelId: claim2.channelId,
23586
- daemonToken: claim2.daemonToken,
23587
- relayOrigin: options.relayOrigin
23595
+ relayOrigin: options.relayOrigin,
23596
+ runtimeCredential: claim2.runtimeCredential
23588
23597
  },
23589
23598
  connectionPath,
23590
- inventory
23599
+ inventory,
23600
+ environment
23591
23601
  );
23592
23602
  }
23593
- async function resumeLocalMachine(arguments_, signal = new AbortController().signal) {
23594
- const channelId = parseChannelArgument(arguments_);
23595
- const connectionPath = join14(readStateDirectory(), "connections", `${channelId}.json`);
23596
- const connection = await readSavedConnection(channelId);
23603
+ async function pairLocalMachine(arguments_, signal = new AbortController().signal) {
23604
+ return await pairRuntime(parsePairCommand(arguments_), signal, process.env);
23605
+ }
23606
+ async function resumeMobiusRuntime(channelId, signal, environment = process.env) {
23607
+ const normalizedChannelId = validatePairingId(channelId);
23608
+ const connectionPath = join14(readStateDirectory(environment), "connections", `${normalizedChannelId}.json`);
23609
+ const connection = await readSavedConnection(normalizedChannelId, environment);
23597
23610
  const inventory = await discoverLocalAgentInventory({
23598
23611
  defaultWorkspace: connection.defaultWorkspace,
23599
- environment: process.env,
23612
+ environment,
23600
23613
  signal
23601
23614
  });
23602
- return buildPairedDaemon(connection, connectionPath, inventory);
23615
+ return buildPairedDaemon(connection, connectionPath, inventory, environment);
23616
+ }
23617
+ async function resumeLocalMachine(arguments_, signal = new AbortController().signal) {
23618
+ return await resumeMobiusRuntime(parseChannelArgument(arguments_), signal, process.env);
23603
23619
  }
23604
23620
 
23621
+ // src/audit/audit-store.ts
23622
+ import { mkdirSync } from "node:fs";
23623
+ import { dirname as dirname3 } from "node:path";
23624
+ import { DatabaseSync } from "node:sqlite";
23625
+ var AuditStore = class {
23626
+ database;
23627
+ constructor(path2) {
23628
+ mkdirSync(dirname3(path2), { recursive: true });
23629
+ this.database = new DatabaseSync(path2);
23630
+ this.database.exec(`
23631
+ CREATE TABLE IF NOT EXISTS audit_events (
23632
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
23633
+ recorded_at TEXT NOT NULL,
23634
+ event TEXT NOT NULL,
23635
+ command_id TEXT NOT NULL,
23636
+ session_id TEXT NOT NULL,
23637
+ details_json TEXT NOT NULL DEFAULT '{}'
23638
+ ) STRICT
23639
+ `);
23640
+ const columns = this.database.prepare("PRAGMA table_info(audit_events)").all();
23641
+ if (!columns.some((column) => Reflect.get(column, "name") === "details_json")) {
23642
+ this.database.exec("ALTER TABLE audit_events ADD COLUMN details_json TEXT NOT NULL DEFAULT '{}'");
23643
+ }
23644
+ }
23645
+ record(record2) {
23646
+ this.database.prepare(
23647
+ `
23648
+ INSERT INTO audit_events (
23649
+ recorded_at,
23650
+ event,
23651
+ command_id,
23652
+ session_id,
23653
+ details_json
23654
+ ) VALUES (?, ?, ?, ?, ?)
23655
+ `
23656
+ ).run(
23657
+ (/* @__PURE__ */ new Date()).toISOString(),
23658
+ record2.event,
23659
+ record2.commandId,
23660
+ record2.sessionId,
23661
+ JSON.stringify(record2.details ?? {})
23662
+ );
23663
+ }
23664
+ close() {
23665
+ this.database.close();
23666
+ }
23667
+ };
23668
+
23605
23669
  // src/run-daemon.ts
23606
23670
  import { randomUUID as randomUUID6 } from "node:crypto";
23607
23671
  import { basename as basename4, join as join16 } from "node:path";
@@ -26992,14 +27056,19 @@ var AgentCatalogPublishError = class extends Error {
26992
27056
  this.name = "AgentCatalogPublishError";
26993
27057
  }
26994
27058
  };
26995
- function catalogEndpoint(relayUrl) {
26996
- const endpoint = new URL(relayUrl);
27059
+ function catalogEndpoint(config2) {
27060
+ const endpoint = new URL(config2.relayUrl);
26997
27061
  endpoint.protocol = endpoint.protocol === "wss:" ? "https:" : "http:";
26998
- if (!endpoint.pathname.endsWith("/daemon")) {
26999
- throw new AgentCatalogPublishError("The Relay URL is invalid");
27062
+ if (endpoint.pathname.endsWith("/daemon")) {
27063
+ endpoint.pathname = `${endpoint.pathname.slice(0, -"/daemon".length)}/catalog`;
27064
+ return endpoint;
27000
27065
  }
27001
- endpoint.pathname = `${endpoint.pathname.slice(0, -"/daemon".length)}/catalog`;
27002
- return endpoint;
27066
+ const runtimePath = "/v1/runtimes/connect";
27067
+ if (endpoint.pathname.endsWith(runtimePath)) {
27068
+ endpoint.pathname = `${endpoint.pathname.slice(0, -runtimePath.length)}/channels/${encodeURIComponent(config2.channelId)}/catalog`;
27069
+ return endpoint;
27070
+ }
27071
+ throw new AgentCatalogPublishError("The Relay URL is invalid");
27003
27072
  }
27004
27073
  async function publishAgentCatalog(config2, signal) {
27005
27074
  signal.throwIfAborted();
@@ -27008,7 +27077,7 @@ async function publishAgentCatalog(config2, signal) {
27008
27077
  let response;
27009
27078
  const catalog = config2.catalog.read();
27010
27079
  try {
27011
- response = await fetch3(catalogEndpoint(config2.relayUrl), {
27080
+ response = await fetch3(catalogEndpoint(config2), {
27012
27081
  body: JSON.stringify({
27013
27082
  agents: catalog.agents.map((agent) => ({
27014
27083
  agentPath: agent.agentIdentity.requestedPath,
@@ -29064,10 +29133,17 @@ function parseReference(resource) {
29064
29133
  function downloadUrl(config2, command, reference) {
29065
29134
  const url2 = new URL(config2.relayUrl);
29066
29135
  url2.protocol = url2.protocol === "wss:" ? "https:" : "http:";
29067
- if (!url2.pathname.endsWith("/daemon")) {
29068
- throw new PromptArtifactError("artifact_endpoint_invalid", "The Relay artifact endpoint is invalid");
29136
+ let channelPath = "";
29137
+ if (url2.pathname.endsWith("/daemon")) {
29138
+ channelPath = url2.pathname.slice(0, -"/daemon".length);
29139
+ } else {
29140
+ const runtimePath = "/v1/runtimes/connect";
29141
+ if (!url2.pathname.endsWith(runtimePath)) {
29142
+ throw new PromptArtifactError("artifact_endpoint_invalid", "The Relay artifact endpoint is invalid");
29143
+ }
29144
+ channelPath = `${url2.pathname.slice(0, -runtimePath.length)}/channels/${encodeURIComponent(config2.channelId)}`;
29069
29145
  }
29070
- url2.pathname = `${url2.pathname.slice(0, -"/daemon".length)}/sessions/${encodeURIComponent(command.sessionId)}/commands/${encodeURIComponent(command.commandId)}/artifacts/${encodeURIComponent(reference.artifactId)}/content`;
29146
+ url2.pathname = `${channelPath}/sessions/${encodeURIComponent(command.sessionId)}/commands/${encodeURIComponent(command.commandId)}/artifacts/${encodeURIComponent(reference.artifactId)}/content`;
29071
29147
  url2.searchParams.delete("liveness");
29072
29148
  return url2;
29073
29149
  }
@@ -29217,17 +29293,27 @@ import { WebSocket } from "undici";
29217
29293
  var CONNECTION_TIMEOUT_MILLISECONDS = 15e3;
29218
29294
  var RelayConnectionError = class extends Error {
29219
29295
  code;
29220
- constructor(code, message, options = {}) {
29296
+ diagnosticDetails;
29297
+ constructor(code, message, diagnosticDetails = {}, options = {}) {
29221
29298
  super(message, options);
29222
29299
  this.code = code;
29300
+ this.diagnosticDetails = diagnosticDetails;
29223
29301
  this.name = "RelayConnectionError";
29224
29302
  }
29225
29303
  };
29226
29304
  function connectRelay(url2, signal) {
29227
29305
  signal.throwIfAborted();
29228
29306
  return new Promise((resolve5, reject) => {
29229
- const socket = new WebSocket(url2, {
29230
- dispatcher: environmentProxyDispatcher
29307
+ const endpoint = new URL(url2);
29308
+ const headers = {};
29309
+ if (endpoint.pathname.endsWith("/v1/runtimes/connect")) {
29310
+ const runtimeCredential = endpoint.searchParams.get("token") ?? "";
29311
+ endpoint.searchParams.delete("token");
29312
+ headers["authorization"] = `Bearer ${runtimeCredential}`;
29313
+ }
29314
+ const socket = new WebSocket(endpoint, {
29315
+ dispatcher: environmentProxyDispatcher,
29316
+ headers
29231
29317
  });
29232
29318
  let finished = false;
29233
29319
  const timeout = setTimeout(onTimeout, CONNECTION_TIMEOUT_MILLISECONDS);
@@ -29249,16 +29335,34 @@ function connectRelay(url2, signal) {
29249
29335
  };
29250
29336
  const onAbort = () => {
29251
29337
  fail(
29252
- new RelayConnectionError("relay_connection_cancelled", "Relay connection was cancelled", {
29253
- cause: signal.reason
29254
- })
29338
+ new RelayConnectionError(
29339
+ "relay_connection_cancelled",
29340
+ "Relay connection was cancelled",
29341
+ {
29342
+ cancellation: "requested"
29343
+ },
29344
+ { cause: signal.reason }
29345
+ )
29255
29346
  );
29256
29347
  };
29257
- const onClose = () => {
29258
- fail(new RelayConnectionError("relay_connection_closed", "Relay connection closed before it was ready"));
29348
+ const onClose = (event) => {
29349
+ fail(
29350
+ new RelayConnectionError("relay_connection_closed", "Relay connection closed before it was ready", {
29351
+ closeCode: event.code.toString(),
29352
+ closeReason: event.reason
29353
+ })
29354
+ );
29259
29355
  };
29260
- const onError = () => {
29261
- fail(new RelayConnectionError("relay_connection_failed", "Relay connection failed"));
29356
+ const onError = (event) => {
29357
+ const cause = Reflect.get(event, "error");
29358
+ fail(
29359
+ new RelayConnectionError(
29360
+ "relay_connection_failed",
29361
+ "Relay connection failed",
29362
+ {},
29363
+ { cause: cause ?? event }
29364
+ )
29365
+ );
29262
29366
  };
29263
29367
  const onOpen = () => {
29264
29368
  if (finished) {
@@ -29269,7 +29373,11 @@ function connectRelay(url2, signal) {
29269
29373
  resolve5(socket);
29270
29374
  };
29271
29375
  function onTimeout() {
29272
- fail(new RelayConnectionError("relay_connection_timeout", "Relay connection timed out"));
29376
+ fail(
29377
+ new RelayConnectionError("relay_connection_timeout", "Relay connection timed out", {
29378
+ timeoutMilliseconds: CONNECTION_TIMEOUT_MILLISECONDS.toString()
29379
+ })
29380
+ );
29273
29381
  }
29274
29382
  signal.addEventListener("abort", onAbort, { once: true });
29275
29383
  socket.addEventListener("close", onClose, { once: true });
@@ -29281,6 +29389,142 @@ function connectRelay(url2, signal) {
29281
29389
  });
29282
29390
  }
29283
29391
 
29392
+ // src/relay-connection/diagnostics.ts
29393
+ var MAX_DIAGNOSTIC_TEXT_LENGTH = 256;
29394
+ function sanitizeDiagnosticText(value) {
29395
+ const printable = Array.from(value, (character) => {
29396
+ const code = character.charCodeAt(0);
29397
+ return code < 32 || code === 127 ? " " : character;
29398
+ }).join("");
29399
+ const withoutSecrets = printable.replace(/([?&](?:access_token|authorization|credential|key|secret|token)=)[^&\s]*/giu, "$1[redacted]").replace(/(?:https?|wss?):\/\/[^\s)]+/giu, (encodedUrl) => {
29400
+ try {
29401
+ return new URL(encodedUrl).origin;
29402
+ } catch {
29403
+ return "[redacted-url]";
29404
+ }
29405
+ });
29406
+ return withoutSecrets.slice(0, MAX_DIAGNOSTIC_TEXT_LENGTH);
29407
+ }
29408
+ function addFailureDetails(details, failure) {
29409
+ let current = failure;
29410
+ for (let depth = 0; depth < 3 && current instanceof Error; depth += 1) {
29411
+ const prefix = depth === 0 ? "failure" : `cause${depth.toString()}`;
29412
+ details[`${prefix}Name`] = current.name;
29413
+ details[`${prefix}Message`] = sanitizeDiagnosticText(current.message);
29414
+ const code = Reflect.get(current, "code");
29415
+ if (typeof code === "string" || typeof code === "number") {
29416
+ details[`${prefix}Code`] = String(code);
29417
+ }
29418
+ const statusCode = Reflect.get(current, "statusCode");
29419
+ if (typeof statusCode === "string" || typeof statusCode === "number") {
29420
+ details[`${prefix}StatusCode`] = String(statusCode);
29421
+ }
29422
+ const diagnosticDetails = Reflect.get(current, "diagnosticDetails");
29423
+ if (typeof diagnosticDetails === "object" && diagnosticDetails !== null) {
29424
+ for (const [key, value] of Object.entries(diagnosticDetails)) {
29425
+ if (typeof value === "string") {
29426
+ details[key] = sanitizeDiagnosticText(value);
29427
+ }
29428
+ }
29429
+ }
29430
+ current = current.cause;
29431
+ }
29432
+ }
29433
+ var RelayConnectionDiagnostics = class {
29434
+ #audit;
29435
+ #channelId;
29436
+ #lastClosedEpoch = 0;
29437
+ #lastFailedEpoch = 0;
29438
+ #relayOrigin;
29439
+ #route;
29440
+ constructor(audit, channelId, relayUrl) {
29441
+ this.#audit = audit;
29442
+ this.#channelId = channelId;
29443
+ this.#relayOrigin = relayUrl.origin;
29444
+ this.#route = describeEnvironmentProxyRoute(relayUrl);
29445
+ }
29446
+ catalogFailed = (failure) => {
29447
+ const details = { ...this.#route, relayOrigin: this.#relayOrigin };
29448
+ addFailureDetails(details, failure);
29449
+ this.#record("agent_catalog_publish_failed", "catalog", details, true);
29450
+ };
29451
+ connectionClosed = (connectionEpoch, code, reason, source) => {
29452
+ if (this.#lastClosedEpoch === connectionEpoch) {
29453
+ return;
29454
+ }
29455
+ this.#lastClosedEpoch = connectionEpoch;
29456
+ this.#record(
29457
+ "relay_connection_closed",
29458
+ connectionEpoch.toString(),
29459
+ {
29460
+ closeCode: code.toString(),
29461
+ closeReason: sanitizeDiagnosticText(reason),
29462
+ connectionEpoch: connectionEpoch.toString(),
29463
+ relayOrigin: this.#relayOrigin,
29464
+ source,
29465
+ ...this.#route
29466
+ },
29467
+ true
29468
+ );
29469
+ };
29470
+ connectionFailed = (connectionEpoch, failure, source) => {
29471
+ if (this.#lastFailedEpoch === connectionEpoch) {
29472
+ return;
29473
+ }
29474
+ this.#lastFailedEpoch = connectionEpoch;
29475
+ const details = {
29476
+ connectionEpoch: connectionEpoch.toString(),
29477
+ relayOrigin: this.#relayOrigin,
29478
+ source,
29479
+ ...this.#route
29480
+ };
29481
+ addFailureDetails(details, failure);
29482
+ this.#record("relay_connection_failed", connectionEpoch.toString(), details, true);
29483
+ };
29484
+ connectionOpened = (connectionEpoch) => {
29485
+ this.#record(
29486
+ "relay_connection_opened",
29487
+ connectionEpoch.toString(),
29488
+ {
29489
+ connectionEpoch: connectionEpoch.toString(),
29490
+ relayOrigin: this.#relayOrigin,
29491
+ ...this.#route
29492
+ },
29493
+ false
29494
+ );
29495
+ };
29496
+ heartbeatTimedOut = (connectionEpoch, heartbeatId, timeoutMilliseconds) => {
29497
+ this.#record(
29498
+ "relay_heartbeat_timed_out",
29499
+ connectionEpoch.toString(),
29500
+ {
29501
+ connectionEpoch: connectionEpoch.toString(),
29502
+ heartbeatId,
29503
+ relayOrigin: this.#relayOrigin,
29504
+ timeoutMilliseconds: timeoutMilliseconds.toString(),
29505
+ ...this.#route
29506
+ },
29507
+ true
29508
+ );
29509
+ };
29510
+ #record = (event, commandId, details, warning) => {
29511
+ this.#audit.record({
29512
+ commandId,
29513
+ details,
29514
+ event,
29515
+ sessionId: this.#channelId
29516
+ });
29517
+ const encoded = JSON.stringify({ channelId: this.#channelId, event, ...details });
29518
+ if (warning) {
29519
+ process.stderr.write(`Mobius relay diagnostic ${encoded}
29520
+ `);
29521
+ return;
29522
+ }
29523
+ process.stdout.write(`Mobius relay diagnostic ${encoded}
29524
+ `);
29525
+ };
29526
+ };
29527
+
29284
29528
  // src/relay-connection/liveness.ts
29285
29529
  import { randomUUID as randomUUID5 } from "node:crypto";
29286
29530
  var HEARTBEAT_ACK_TIMEOUT_MILLISECONDS = 1e4;
@@ -29357,9 +29601,10 @@ var RelayConnectionLiveness = class {
29357
29601
  if (!(this.#state instanceof AwaitingHeartbeatAckState)) {
29358
29602
  throw new Error("Heartbeat timeout occurred in an invalid state");
29359
29603
  }
29604
+ const heartbeatId = this.#state.heartbeatId;
29360
29605
  this.#state.timeout.cancel();
29361
29606
  this.#state = new FinishedRelayLivenessState();
29362
- this.#transport.unresponsive();
29607
+ this.#transport.unresponsive(heartbeatId, HEARTBEAT_ACK_TIMEOUT_MILLISECONDS);
29363
29608
  };
29364
29609
  #sendHeartbeat = () => {
29365
29610
  if (this.#state instanceof WaitingForHeartbeatState) {
@@ -29408,6 +29653,7 @@ var SessionEventOutbox = class {
29408
29653
  ordinal INTEGER PRIMARY KEY AUTOINCREMENT,
29409
29654
  event_id TEXT NOT NULL UNIQUE,
29410
29655
  session_id TEXT NOT NULL,
29656
+ source_key TEXT NOT NULL,
29411
29657
  envelope_json TEXT NOT NULL
29412
29658
  ) STRICT;
29413
29659
  CREATE INDEX IF NOT EXISTS relay_session_event_outbox_order
@@ -29435,6 +29681,27 @@ var SessionEventOutbox = class {
29435
29681
  )
29436
29682
  ) STRICT;
29437
29683
  `);
29684
+ const outboxColumns = this.#database.prepare("PRAGMA table_info(relay_session_event_outbox)").all();
29685
+ const hasSourceKey = outboxColumns.some(
29686
+ (column) => typeof column === "object" && column !== null && Reflect.get(column, "name") === "source_key"
29687
+ );
29688
+ if (!hasSourceKey) {
29689
+ this.#database.exec(`
29690
+ ALTER TABLE relay_session_event_outbox
29691
+ ADD COLUMN source_key TEXT NOT NULL DEFAULT '';
29692
+ `);
29693
+ }
29694
+ this.#database.exec(`
29695
+ UPDATE relay_session_event_outbox
29696
+ SET source_key = CASE json_extract(envelope_json, '$.source.kind')
29697
+ WHEN 'attempt' THEN
29698
+ 'attempt:' || session_id || ':' || json_extract(envelope_json, '$.source.attemptId')
29699
+ ELSE 'runtime:' || session_id
29700
+ END
29701
+ WHERE source_key = '';
29702
+ CREATE INDEX IF NOT EXISTS relay_session_event_outbox_source_order
29703
+ ON relay_session_event_outbox(source_key, ordinal);
29704
+ `);
29438
29705
  }
29439
29706
  claimExecution = (commandId, sessionId, commandJson) => {
29440
29707
  this.#database.exec("BEGIN IMMEDIATE");
@@ -29492,12 +29759,10 @@ var SessionEventOutbox = class {
29492
29759
  const state = typeof row === "object" && row !== null ? Reflect.get(row, "state") : void 0;
29493
29760
  return state === "succeeded" || state === "failed" || state === "cancelled";
29494
29761
  };
29495
- acknowledge = (acknowledgement) => {
29496
- this.#database.prepare(
29497
- `DELETE FROM relay_session_event_outbox
29762
+ acknowledge = (acknowledgement) => this.#database.prepare(
29763
+ `DELETE FROM relay_session_event_outbox
29498
29764
  WHERE event_id = ? AND session_id = ?`
29499
- ).run(acknowledgement.eventId, acknowledgement.sessionId);
29500
- };
29765
+ ).run(acknowledgement.eventId, acknowledgement.sessionId).changes === 1;
29501
29766
  quarantine = (rejection) => {
29502
29767
  this.#database.exec("BEGIN IMMEDIATE");
29503
29768
  try {
@@ -29564,9 +29829,9 @@ var SessionEventOutbox = class {
29564
29829
  ).run(sourceKey, nextSourceSeq + 1);
29565
29830
  this.#database.prepare(
29566
29831
  `INSERT INTO relay_session_event_outbox (
29567
- event_id, session_id, envelope_json
29568
- ) VALUES (?, ?, ?)`
29569
- ).run(envelope2.eventId, envelope2.sessionId, encoded);
29832
+ event_id, session_id, source_key, envelope_json
29833
+ ) VALUES (?, ?, ?, ?)`
29834
+ ).run(envelope2.eventId, envelope2.sessionId, sourceKey, encoded);
29570
29835
  this.#database.exec("COMMIT");
29571
29836
  return encoded;
29572
29837
  } catch (error61) {
@@ -29579,6 +29844,27 @@ var SessionEventOutbox = class {
29579
29844
  FROM relay_session_event_outbox
29580
29845
  ORDER BY ordinal`
29581
29846
  ).all().map(readPendingEnvelope);
29847
+ pendingBatch = (limit) => {
29848
+ if (!Number.isSafeInteger(limit) || limit < 1) {
29849
+ throw new Error("Session event replay batch limit must be a positive integer");
29850
+ }
29851
+ return this.#database.prepare(
29852
+ `WITH ranked_events AS (
29853
+ SELECT ordinal,
29854
+ ROW_NUMBER() OVER (
29855
+ PARTITION BY source_key
29856
+ ORDER BY ordinal
29857
+ ) AS source_position
29858
+ FROM relay_session_event_outbox
29859
+ )
29860
+ SELECT outbox.envelope_json
29861
+ FROM ranked_events AS ranked
29862
+ INNER JOIN relay_session_event_outbox AS outbox
29863
+ ON outbox.ordinal = ranked.ordinal
29864
+ ORDER BY ranked.source_position, outbox.ordinal
29865
+ LIMIT ?`
29866
+ ).all(limit).map(readPendingEnvelope);
29867
+ };
29582
29868
  };
29583
29869
  function isDaemonExecutionState(value) {
29584
29870
  return value === "cancelled" || value === "dispatching" || value === "failed" || value === "running" || value === "succeeded";
@@ -29595,7 +29881,7 @@ var systemDaemonTimer = new SystemDaemonTimer();
29595
29881
 
29596
29882
  // src/run-daemon.ts
29597
29883
  var RELAY_RECONNECT_DELAY_MILLISECONDS = 500;
29598
- var RELAY_CONNECTION_REFRESH_MILLISECONDS = 20 * 60 * 1e3;
29884
+ var AGENT_ACTIVITY_COALESCING_MILLISECONDS = 100;
29599
29885
  var ACTIVE_TURN_RECONCILIATION_TIMEOUT_MILLISECONDS = 2e4;
29600
29886
  var ACTIVE_TURN_RECONCILIATION_CLOSE_CODE = 4009;
29601
29887
  function botIpcSocketPath() {
@@ -29604,54 +29890,6 @@ function botIpcSocketPath() {
29604
29890
  }
29605
29891
  return join16("/tmp", `mobius-bot-${randomUUID6()}.sock`);
29606
29892
  }
29607
- var RelayConnectionRefreshState = class {
29608
- };
29609
- var ScheduledRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
29610
- };
29611
- var DueRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
29612
- };
29613
- var FinishedRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
29614
- };
29615
- var RelayConnectionRefresh = class {
29616
- #isSafe;
29617
- #refresh;
29618
- #scheduledTask;
29619
- #state = new ScheduledRelayConnectionRefreshState();
29620
- constructor(timer, isSafe, refresh) {
29621
- this.#isSafe = isSafe;
29622
- this.#refresh = refresh;
29623
- this.#scheduledTask = timer.schedule(this.#request, RELAY_CONNECTION_REFRESH_MILLISECONDS);
29624
- }
29625
- afterTurnFinished = () => {
29626
- if (this.#state instanceof DueRelayConnectionRefreshState) {
29627
- this.#request();
29628
- return;
29629
- }
29630
- if (this.#state instanceof ScheduledRelayConnectionRefreshState || this.#state instanceof FinishedRelayConnectionRefreshState) {
29631
- return;
29632
- }
29633
- throw new Error("Unknown relay connection refresh state");
29634
- };
29635
- close = () => {
29636
- this.#scheduledTask.cancel();
29637
- this.#state = new FinishedRelayConnectionRefreshState();
29638
- };
29639
- #request = () => {
29640
- if (this.#state instanceof FinishedRelayConnectionRefreshState) {
29641
- return;
29642
- }
29643
- if (!(this.#state instanceof ScheduledRelayConnectionRefreshState) && !(this.#state instanceof DueRelayConnectionRefreshState)) {
29644
- throw new Error("Unknown relay connection refresh state");
29645
- }
29646
- if (!this.#isSafe()) {
29647
- this.#state = new DueRelayConnectionRefreshState();
29648
- return;
29649
- }
29650
- this.#state = new FinishedRelayConnectionRefreshState();
29651
- this.#scheduledTask.cancel();
29652
- this.#refresh();
29653
- };
29654
- };
29655
29893
  var DaemonAcpObserver = class {
29656
29894
  #record;
29657
29895
  constructor(record2) {
@@ -29848,10 +30086,17 @@ var ActiveTurnReconciliationError = class extends Error {
29848
30086
  function activeTurnReconciliationEndpoint(config2, turn) {
29849
30087
  const endpoint = new URL(config2.relayUrl);
29850
30088
  endpoint.protocol = endpoint.protocol === "wss:" ? "https:" : "http:";
29851
- if (!endpoint.pathname.endsWith("/daemon")) {
29852
- throw new ActiveTurnReconciliationError("The Relay URL is invalid");
30089
+ let channelPath = "";
30090
+ if (endpoint.pathname.endsWith("/daemon")) {
30091
+ channelPath = endpoint.pathname.slice(0, -"/daemon".length);
30092
+ } else {
30093
+ const runtimePath = "/v1/runtimes/connect";
30094
+ if (!endpoint.pathname.endsWith(runtimePath)) {
30095
+ throw new ActiveTurnReconciliationError("The Relay URL is invalid");
30096
+ }
30097
+ channelPath = `${endpoint.pathname.slice(0, -runtimePath.length)}/channels/${encodeURIComponent(config2.channelId)}`;
29853
30098
  }
29854
- endpoint.pathname = `${endpoint.pathname.slice(0, -"/daemon".length)}/sessions/${encodeURIComponent(turn.command.sessionId)}/attempts/${encodeURIComponent(turn.attemptId)}/reconciliation`;
30099
+ endpoint.pathname = `${channelPath}/sessions/${encodeURIComponent(turn.command.sessionId)}/attempts/${encodeURIComponent(turn.attemptId)}/reconciliation`;
29855
30100
  endpoint.searchParams.delete("liveness");
29856
30101
  return endpoint;
29857
30102
  }
@@ -29909,7 +30154,8 @@ var DaemonSessionEventConnectionState = class {
29909
30154
  };
29910
30155
  var DisconnectedDaemonSessionEventState = class extends DaemonSessionEventConnectionState {
29911
30156
  disconnect = () => this;
29912
- send = () => void 0;
30157
+ matches = () => false;
30158
+ send = () => false;
29913
30159
  };
29914
30160
  var ConnectedDaemonSessionEventState = class extends DaemonSessionEventConnectionState {
29915
30161
  #socket;
@@ -29918,38 +30164,155 @@ var ConnectedDaemonSessionEventState = class extends DaemonSessionEventConnectio
29918
30164
  this.#socket = socket;
29919
30165
  }
29920
30166
  disconnect = (socket) => socket === this.#socket ? new DisconnectedDaemonSessionEventState() : this;
30167
+ matches = (socket) => socket === this.#socket;
29921
30168
  send = (message) => {
29922
- if (this.#socket.readyState === WebSocket2.OPEN) {
29923
- try {
29924
- this.#socket.send(message);
29925
- } catch {
29926
- }
30169
+ if (this.#socket.readyState !== WebSocket2.OPEN) {
30170
+ return false;
30171
+ }
30172
+ try {
30173
+ this.#socket.send(message);
30174
+ return true;
30175
+ } catch {
30176
+ return false;
29927
30177
  }
29928
30178
  };
29929
30179
  };
30180
+ var SESSION_EVENT_REPLAY_WINDOW = 4;
29930
30181
  var DaemonSessionEventPublisher = class {
29931
30182
  #outbox;
30183
+ #inFlightEventIds = /* @__PURE__ */ new Set();
29932
30184
  #connection = new DisconnectedDaemonSessionEventState();
29933
30185
  constructor(outbox) {
29934
30186
  this.#outbox = outbox;
29935
30187
  }
29936
30188
  connect = (socket) => {
29937
- const connection = new ConnectedDaemonSessionEventState(socket);
29938
- this.#connection = connection;
29939
- for (const pending of this.#outbox.pending()) {
29940
- connection.send(pending);
29941
- }
30189
+ this.#connection = new ConnectedDaemonSessionEventState(socket);
30190
+ this.#inFlightEventIds.clear();
30191
+ this.#publishNextBatch();
29942
30192
  };
29943
30193
  disconnect = (socket) => {
30194
+ if (!this.#connection.matches(socket)) {
30195
+ return;
30196
+ }
29944
30197
  this.#connection = this.#connection.disconnect(socket);
30198
+ this.#inFlightEventIds.clear();
29945
30199
  };
29946
30200
  recordAttempt = (sessionId, payload, attemptId) => {
29947
- this.#connection.send(this.#outbox.enqueue(sessionId, payload, attemptId));
30201
+ this.#outbox.enqueue(sessionId, payload, attemptId);
30202
+ this.#publishNextBatch();
29948
30203
  };
29949
30204
  recordRuntime = (sessionId, payload) => {
29950
- this.#connection.send(this.#outbox.enqueue(sessionId, payload));
30205
+ this.#outbox.enqueue(sessionId, payload);
30206
+ this.#publishNextBatch();
30207
+ };
30208
+ acknowledge = (acknowledgement) => {
30209
+ if (!this.#outbox.acknowledge(acknowledgement)) {
30210
+ return;
30211
+ }
30212
+ this.#inFlightEventIds.delete(acknowledgement.eventId);
30213
+ this.#publishNextBatch();
30214
+ };
30215
+ quarantine = (rejection) => {
30216
+ const quarantined = this.#outbox.quarantine(rejection);
30217
+ if (!quarantined) {
30218
+ return false;
30219
+ }
30220
+ this.#inFlightEventIds.delete(rejection.eventId);
30221
+ this.#publishNextBatch();
30222
+ return true;
30223
+ };
30224
+ #publishNextBatch = () => {
30225
+ if (this.#inFlightEventIds.size > 0) {
30226
+ return;
30227
+ }
30228
+ const batch = this.#outbox.pendingBatch(SESSION_EVENT_REPLAY_WINDOW);
30229
+ for (const encoded of batch) {
30230
+ const envelope2 = decodeSessionEventEnvelope(encoded);
30231
+ if (!this.#connection.send(encoded)) {
30232
+ this.#inFlightEventIds.clear();
30233
+ return;
30234
+ }
30235
+ this.#inFlightEventIds.add(envelope2.eventId);
30236
+ }
29951
30237
  };
29952
30238
  };
30239
+ var CoalescingDaemonActivityPublisher = class {
30240
+ #pending = /* @__PURE__ */ new Map();
30241
+ #publish;
30242
+ #publishedActivityIds = /* @__PURE__ */ new Set();
30243
+ #scheduled = /* @__PURE__ */ new Map();
30244
+ #timer;
30245
+ constructor(timer, publish) {
30246
+ this.#publish = publish;
30247
+ this.#timer = timer;
30248
+ }
30249
+ flush = () => {
30250
+ const activities = [...this.#pending.values()].sort((left, right) => left.sequence - right.sequence);
30251
+ for (const task of this.#scheduled.values()) {
30252
+ task.cancel();
30253
+ }
30254
+ this.#pending.clear();
30255
+ this.#scheduled.clear();
30256
+ for (const activity of activities) {
30257
+ this.#publish(activity);
30258
+ }
30259
+ };
30260
+ record = (activity) => {
30261
+ if (activity.status !== "pending" && activity.status !== "running") {
30262
+ this.#discardPending(activity.activityId);
30263
+ this.#publishedActivityIds.add(activity.activityId);
30264
+ this.#publish(activity);
30265
+ return Promise.resolve();
30266
+ }
30267
+ if (!this.#publishedActivityIds.has(activity.activityId)) {
30268
+ this.#publishedActivityIds.add(activity.activityId);
30269
+ this.#publish(activity);
30270
+ return Promise.resolve();
30271
+ }
30272
+ this.#pending.set(activity.activityId, activity);
30273
+ if (!this.#scheduled.has(activity.activityId)) {
30274
+ this.#scheduled.set(
30275
+ activity.activityId,
30276
+ this.#timer.schedule(
30277
+ () => this.#flushActivity(activity.activityId),
30278
+ AGENT_ACTIVITY_COALESCING_MILLISECONDS
30279
+ )
30280
+ );
30281
+ }
30282
+ return Promise.resolve();
30283
+ };
30284
+ #discardPending = (activityId) => {
30285
+ this.#scheduled.get(activityId)?.cancel();
30286
+ this.#scheduled.delete(activityId);
30287
+ this.#pending.delete(activityId);
30288
+ };
30289
+ #flushActivity = (activityId) => {
30290
+ const activity = this.#pending.get(activityId);
30291
+ this.#discardPending(activityId);
30292
+ if (activity !== void 0) {
30293
+ this.#publish(activity);
30294
+ }
30295
+ };
30296
+ };
30297
+ function encodeDaemonActivity(command, activity) {
30298
+ return encodeAgentActivityEvent({
30299
+ activityId: activity.activityId,
30300
+ activityType: activity.activityType,
30301
+ commandId: command.commandId,
30302
+ content: activity.content,
30303
+ detail: activity.detail,
30304
+ input: activity.input,
30305
+ kind: AGENT_ACTIVITY_KIND,
30306
+ locations: activity.locations,
30307
+ name: activity.name,
30308
+ output: activity.output,
30309
+ sequence: activity.sequence,
30310
+ sessionId: command.sessionId,
30311
+ status: activity.status,
30312
+ title: activity.title,
30313
+ toolKind: activity.toolKind
30314
+ });
30315
+ }
29953
30316
  function reportFailure(publisher, command, code, message, attemptId) {
29954
30317
  publisher.recordAttempt(
29955
30318
  command.sessionId,
@@ -30126,7 +30489,10 @@ async function executeSessionConfigure(publisher, socket, command, config2, dire
30126
30489
  reportOperationFailure(socket, command.requestId, command.sessionId, error61);
30127
30490
  }
30128
30491
  }
30129
- async function executePrompt(outbox, publisher, command, config2, directories, audit, sessions, attemptId, signal) {
30492
+ async function executePrompt(outbox, publisher, command, config2, directories, audit, sessions, attemptId, timer, signal) {
30493
+ const activities = new CoalescingDaemonActivityPublisher(timer, (activity) => {
30494
+ publisher.recordAttempt(command.sessionId, encodeDaemonActivity(command, activity), attemptId);
30495
+ });
30130
30496
  try {
30131
30497
  const resolved = await resolveSessionDirectories(
30132
30498
  directories,
@@ -30183,30 +30549,7 @@ async function executePrompt(outbox, publisher, command, config2, directories, a
30183
30549
  cwd: resolved.cwd,
30184
30550
  disposition: command.disposition,
30185
30551
  images: attachments.images,
30186
- onActivity: (activity) => {
30187
- publisher.recordAttempt(
30188
- command.sessionId,
30189
- encodeAgentActivityEvent({
30190
- activityId: activity.activityId,
30191
- activityType: activity.activityType,
30192
- commandId: command.commandId,
30193
- content: activity.content,
30194
- detail: activity.detail,
30195
- input: activity.input,
30196
- kind: AGENT_ACTIVITY_KIND,
30197
- locations: activity.locations,
30198
- name: activity.name,
30199
- output: activity.output,
30200
- sequence: activity.sequence,
30201
- sessionId: command.sessionId,
30202
- status: activity.status,
30203
- title: activity.title,
30204
- toolKind: activity.toolKind
30205
- }),
30206
- attemptId
30207
- );
30208
- return Promise.resolve();
30209
- },
30552
+ onActivity: activities.record,
30210
30553
  onText: (text, messageId) => {
30211
30554
  publisher.recordAttempt(
30212
30555
  command.sessionId,
@@ -30229,6 +30572,7 @@ async function executePrompt(outbox, publisher, command, config2, directories, a
30229
30572
  });
30230
30573
  stopReason = result.stopReason;
30231
30574
  }
30575
+ activities.flush();
30232
30576
  publisher.recordAttempt(
30233
30577
  command.sessionId,
30234
30578
  encodeSessionCompletedEvent({
@@ -30246,6 +30590,7 @@ async function executePrompt(outbox, publisher, command, config2, directories, a
30246
30590
  sessionId: command.sessionId
30247
30591
  });
30248
30592
  } catch (error61) {
30593
+ activities.flush();
30249
30594
  audit.record({
30250
30595
  commandId: command.commandId,
30251
30596
  event: "session_failed",
@@ -30284,7 +30629,10 @@ function botExecutionAgent(agent, contextHandle, socketPath) {
30284
30629
  agent.capabilities
30285
30630
  );
30286
30631
  }
30287
- async function executeBotPrompt(outbox, publisher, command, config2, directories, audit, sessions, grants, contexts, applicator, ipc, attemptId, signal) {
30632
+ async function executeBotPrompt(outbox, publisher, command, config2, directories, audit, sessions, grants, contexts, applicator, ipc, attemptId, timer, signal) {
30633
+ const activities = new CoalescingDaemonActivityPublisher(timer, (activity) => {
30634
+ publisher.recordAttempt(command.sessionId, encodeDaemonActivity(command, activity), attemptId);
30635
+ });
30288
30636
  try {
30289
30637
  const deadline = Date.parse(command.executionDeadlineAt);
30290
30638
  const remaining = deadline - Date.now();
@@ -30368,30 +30716,7 @@ async function executeBotPrompt(outbox, publisher, command, config2, directories
30368
30716
  commandId: command.commandId,
30369
30717
  cwd: resolved.cwd,
30370
30718
  images: attachments.images,
30371
- onActivity: (activity) => {
30372
- publisher.recordAttempt(
30373
- command.sessionId,
30374
- encodeAgentActivityEvent({
30375
- activityId: activity.activityId,
30376
- activityType: activity.activityType,
30377
- commandId: command.commandId,
30378
- content: activity.content,
30379
- detail: activity.detail,
30380
- input: activity.input,
30381
- kind: AGENT_ACTIVITY_KIND,
30382
- locations: activity.locations,
30383
- name: activity.name,
30384
- output: activity.output,
30385
- sequence: activity.sequence,
30386
- sessionId: command.sessionId,
30387
- status: activity.status,
30388
- title: activity.title,
30389
- toolKind: activity.toolKind
30390
- }),
30391
- attemptId
30392
- );
30393
- return Promise.resolve();
30394
- },
30719
+ onActivity: activities.record,
30395
30720
  onText: (text, messageId) => {
30396
30721
  publisher.recordAttempt(
30397
30722
  command.sessionId,
@@ -30411,6 +30736,7 @@ async function executeBotPrompt(outbox, publisher, command, config2, directories
30411
30736
  sessionId: command.sessionId,
30412
30737
  signal: executionSignal
30413
30738
  });
30739
+ activities.flush();
30414
30740
  publisher.recordAttempt(
30415
30741
  command.sessionId,
30416
30742
  encodeSessionCompletedEvent({
@@ -30435,6 +30761,7 @@ async function executeBotPrompt(outbox, publisher, command, config2, directories
30435
30761
  lease.release();
30436
30762
  }
30437
30763
  } catch (error61) {
30764
+ activities.flush();
30438
30765
  const failure = Date.parse(command.executionDeadlineAt) <= Date.now() ? new LocalBotAuthorizationError("turn_expired", "The Bot execution deadline has expired", {
30439
30766
  cause: error61
30440
30767
  }) : error61;
@@ -30474,15 +30801,15 @@ function waitForRelayRetry(signal, timer) {
30474
30801
  signal.addEventListener("abort", finish, { once: true });
30475
30802
  });
30476
30803
  }
30477
- async function publishAgentCatalogUntilAvailable(config2, signal, timer) {
30804
+ async function publishAgentCatalogUntilAvailable(config2, diagnostics, signal, timer) {
30478
30805
  try {
30479
30806
  await publishAgentCatalog(config2, signal);
30480
30807
  return;
30481
- } catch {
30808
+ } catch (error61) {
30482
30809
  if (signal.aborted) {
30483
30810
  return;
30484
30811
  }
30485
- console.warn("Mobius daemon is waiting to publish its Agent catalog");
30812
+ diagnostics.catalogFailed(error61);
30486
30813
  }
30487
30814
  while (!signal.aborted) {
30488
30815
  await waitForRelayRetry(signal, timer);
@@ -30496,11 +30823,12 @@ async function publishAgentCatalogUntilAvailable(config2, signal, timer) {
30496
30823
  }
30497
30824
  }
30498
30825
  }
30499
- async function runRelayConnection(config2, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, onConnected, signal, timer) {
30826
+ async function runRelayConnection(config2, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, connectionEpoch, diagnostics, onConnected, signal, timer) {
30500
30827
  const relayUrl = new URL(config2.relayUrl);
30501
30828
  relayUrl.searchParams.set("liveness", "1");
30502
30829
  const socket = await connectRelay(relayUrl, signal);
30503
30830
  const reconnectingTurns = [...activeTurns.values()];
30831
+ diagnostics.connectionOpened(connectionEpoch);
30504
30832
  onConnected();
30505
30833
  const connectionController = new AbortController();
30506
30834
  const nativeRequests = /* @__PURE__ */ new Map();
@@ -30509,7 +30837,6 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30509
30837
  await new Promise((resolve5, reject) => {
30510
30838
  let finished = false;
30511
30839
  const cleanup = () => {
30512
- connectionRefresh.close();
30513
30840
  connectionLiveness.close();
30514
30841
  signal.removeEventListener("abort", stop);
30515
30842
  socket.removeEventListener("close", handleClose);
@@ -30527,61 +30854,70 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30527
30854
  cleanup();
30528
30855
  resolve5();
30529
30856
  };
30530
- const rejectConnection = () => {
30857
+ const rejectConnection = (failure) => {
30531
30858
  if (finished) {
30532
30859
  return;
30533
30860
  }
30534
30861
  finished = true;
30535
30862
  cleanup();
30536
- reject(new Error("Relay WebSocket failed"));
30863
+ reject(failure);
30537
30864
  };
30538
30865
  const stop = () => {
30866
+ diagnostics.connectionClosed(connectionEpoch, 1e3, "Daemon stopped", "daemon_stop");
30539
30867
  closeRelaySocket(socket, 1e3, "Daemon stopped");
30540
30868
  resolveConnection();
30541
30869
  };
30542
- const refreshConnection = () => {
30543
- closeRelaySocket(socket, 1e3, "Refreshing idle relay connection");
30544
- resolveConnection();
30870
+ const failAndClose = (failure, code, reason, source) => {
30871
+ diagnostics.connectionFailed(connectionEpoch, failure, source);
30872
+ diagnostics.connectionClosed(connectionEpoch, code, reason, source);
30873
+ closeRelaySocket(socket, code, reason);
30874
+ rejectConnection(failure);
30545
30875
  };
30546
30876
  const connectionLiveness = new RelayConnectionLiveness(timer, {
30547
30877
  send: (message) => send(socket, message),
30548
- unresponsive: () => {
30878
+ unresponsive: (heartbeatId, timeoutMilliseconds) => {
30879
+ diagnostics.heartbeatTimedOut(connectionEpoch, heartbeatId, timeoutMilliseconds);
30880
+ diagnostics.connectionClosed(
30881
+ connectionEpoch,
30882
+ 4001,
30883
+ "Relay liveness check timed out",
30884
+ "heartbeat_timeout"
30885
+ );
30549
30886
  closeRelaySocket(socket, 4001, "Relay liveness check timed out");
30550
30887
  resolveConnection();
30551
30888
  }
30552
30889
  });
30553
- const connectionRefresh = new RelayConnectionRefresh(
30554
- timer,
30555
- () => activeTurns.size === 0,
30556
- refreshConnection
30557
- );
30558
30890
  signal.addEventListener("abort", stop, { once: true });
30559
- const handleClose = () => {
30891
+ const handleClose = (event) => {
30892
+ diagnostics.connectionClosed(connectionEpoch, event.code, event.reason, "relay");
30560
30893
  resolveConnection();
30561
30894
  };
30562
- const handleError = () => {
30895
+ const handleError = (event) => {
30563
30896
  if (signal.aborted) {
30564
30897
  resolveConnection();
30565
30898
  return;
30566
30899
  }
30567
- rejectConnection();
30900
+ const cause = Reflect.get(event, "error");
30901
+ const failure = new Error("Relay WebSocket emitted an error", { cause: cause ?? event });
30902
+ diagnostics.connectionFailed(connectionEpoch, failure, "socket_error");
30903
+ diagnostics.connectionClosed(connectionEpoch, 1006, "WebSocket error", "socket_error");
30904
+ rejectConnection(failure);
30568
30905
  };
30569
30906
  const handleMessage = (event) => {
30570
30907
  const encoded = event.data;
30571
30908
  if (typeof encoded !== "string") {
30572
- closeRelaySocket(socket, 4003, "Text message required");
30573
- rejectConnection();
30909
+ failAndClose(new Error("Relay sent a non-text message"), 4003, "Text message required", "protocol");
30574
30910
  return;
30575
30911
  }
30576
30912
  try {
30577
30913
  const kind = decodeMessageKind(encoded);
30578
30914
  if (kind === SESSION_EVENT_ACK_KIND) {
30579
- outbox.acknowledge(decodeSessionEventAcknowledgement(encoded));
30915
+ publisher.acknowledge(decodeSessionEventAcknowledgement(encoded));
30580
30916
  return;
30581
30917
  }
30582
30918
  if (kind === SESSION_EVENT_REJECTED_KIND) {
30583
30919
  const rejection = decodeSessionEventRejection(encoded);
30584
- if (outbox.quarantine(rejection)) {
30920
+ if (publisher.quarantine(rejection)) {
30585
30921
  audit.record({
30586
30922
  commandId: rejection.eventId,
30587
30923
  details: { code: rejection.code },
@@ -30960,6 +31296,7 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30960
31296
  applicator,
30961
31297
  ipc,
30962
31298
  receipt2.attemptId,
31299
+ timer,
30963
31300
  turn2.signal
30964
31301
  ).catch((error61) => {
30965
31302
  console.error("Mobius daemon Bot turn failed", error61);
@@ -30969,7 +31306,6 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30969
31306
  activeAttemptIds.delete(command2.sessionId);
30970
31307
  }
30971
31308
  turn2.finish();
30972
- connectionRefresh.afterTurnFinished();
30973
31309
  });
30974
31310
  return;
30975
31311
  }
@@ -30998,6 +31334,7 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30998
31334
  audit,
30999
31335
  sessions,
31000
31336
  receipt.attemptId,
31337
+ timer,
31001
31338
  turn.signal
31002
31339
  ).catch((error61) => {
31003
31340
  console.error("Mobius daemon turn failed", error61);
@@ -31007,12 +31344,15 @@ async function runRelayConnection(config2, directories, audit, sessions, install
31007
31344
  activeAttemptIds.delete(command.sessionId);
31008
31345
  }
31009
31346
  turn.finish();
31010
- connectionRefresh.afterTurnFinished();
31011
31347
  });
31012
31348
  } catch (error61) {
31013
31349
  console.error("Mobius daemon rejected a message", error61);
31014
- closeRelaySocket(socket, 4008, "Invalid protocol message");
31015
- rejectConnection();
31350
+ failAndClose(
31351
+ new Error("Relay sent an invalid protocol message", { cause: error61 }),
31352
+ 4008,
31353
+ "Invalid protocol message",
31354
+ "protocol"
31355
+ );
31016
31356
  }
31017
31357
  };
31018
31358
  socket.addEventListener("close", handleClose);
@@ -31024,21 +31364,18 @@ async function runRelayConnection(config2, directories, audit, sessions, install
31024
31364
  return;
31025
31365
  }
31026
31366
  publisher.connect(socket);
31027
- void Promise.all(reconnectingTurns.map((turn) => turn.completion)).then(
31028
- connectionRefresh.afterTurnFinished
31029
- );
31030
31367
  },
31031
31368
  (error61) => {
31032
31369
  if (finished) {
31033
31370
  return;
31034
31371
  }
31035
31372
  console.error("Mobius daemon could not reconcile active Agent turns", error61);
31036
- closeRelaySocket(
31037
- socket,
31373
+ failAndClose(
31374
+ new Error("Active Agent turn reconciliation failed", { cause: error61 }),
31038
31375
  ACTIVE_TURN_RECONCILIATION_CLOSE_CODE,
31039
- "Active Agent turn reconciliation failed"
31376
+ "Active Agent turn reconciliation failed",
31377
+ "reconciliation"
31040
31378
  );
31041
- rejectConnection();
31042
31379
  }
31043
31380
  );
31044
31381
  if (signal.aborted) {
@@ -31082,6 +31419,7 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31082
31419
  const sessions = new AcpSessionManager(new DaemonAcpObserver(recordSessionEvent), sessionStore);
31083
31420
  const directories = new WorkspaceDirectoryService(config2.bindingId, config2.defaultWorkspace);
31084
31421
  const installationObserver = new DaemonInstallationObserver();
31422
+ const diagnostics = new RelayConnectionDiagnostics(audit, config2.channelId, config2.relayUrl);
31085
31423
  const nativeSessions = new NativeSessionService(config2, directories, sessions, audit);
31086
31424
  const installations = new AgentInstallationManager(
31087
31425
  specifications,
@@ -31089,7 +31427,7 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31089
31427
  installationObserver,
31090
31428
  installationAudit
31091
31429
  );
31092
- let retryAnnounced = false;
31430
+ let connectionEpoch = 0;
31093
31431
  let connectionAnnounced = false;
31094
31432
  const announceConnection = () => {
31095
31433
  if (connectionAnnounced) {
@@ -31105,10 +31443,11 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31105
31443
  `
31106
31444
  );
31107
31445
  };
31108
- const catalogPublication = publishAgentCatalogUntilAvailable(config2, signal, timer);
31446
+ const catalogPublication = publishAgentCatalogUntilAvailable(config2, diagnostics, signal, timer);
31109
31447
  try {
31110
31448
  await ipc.listen(signal);
31111
31449
  while (!signal.aborted) {
31450
+ connectionEpoch += 1;
31112
31451
  try {
31113
31452
  await runRelayConnection(
31114
31453
  config2,
@@ -31126,19 +31465,17 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31126
31465
  ipc,
31127
31466
  activeTurns,
31128
31467
  activeAttemptIds,
31468
+ connectionEpoch,
31469
+ diagnostics,
31129
31470
  announceConnection,
31130
31471
  signal,
31131
31472
  timer
31132
31473
  );
31133
- retryAnnounced = false;
31134
- } catch {
31474
+ } catch (error61) {
31135
31475
  if (signal.aborted) {
31136
31476
  return;
31137
31477
  }
31138
- if (!retryAnnounced) {
31139
- console.warn("Mobius daemon is waiting for the relay");
31140
- retryAnnounced = true;
31141
- }
31478
+ diagnostics.connectionFailed(connectionEpoch, error61, "connection");
31142
31479
  }
31143
31480
  await waitForRelayRetry(signal, timer);
31144
31481
  }
@@ -31176,6 +31513,69 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31176
31513
  }
31177
31514
  }
31178
31515
 
31516
+ // src/runtime.ts
31517
+ var APP_CREDENTIAL_PATTERN = /^mapp_[a-zA-Z0-9_-]{22}_[a-zA-Z0-9_-]{43}$/;
31518
+ var CONVERSATION_CREDENTIAL_PATTERN = /^mconv_[a-zA-Z0-9_-]{22}_[a-zA-Z0-9_-]{43}$/;
31519
+ var RUNTIME_CREDENTIAL_PATTERN = /^[a-zA-Z0-9_-]{32,256}$/;
31520
+ var MobiusRuntimeConfigurationError = class extends Error {
31521
+ code = "runtime_configuration_invalid";
31522
+ constructor(message, options = {}) {
31523
+ super(message, options);
31524
+ this.name = "MobiusRuntimeConfigurationError";
31525
+ }
31526
+ };
31527
+ var MobiusRuntimeState = class {
31528
+ };
31529
+ var IdleMobiusRuntimeState = class extends MobiusRuntimeState {
31530
+ };
31531
+ var RunningMobiusRuntimeState = class extends MobiusRuntimeState {
31532
+ };
31533
+ var FinishedMobiusRuntimeState = class extends MobiusRuntimeState {
31534
+ };
31535
+ function validateRuntimeRelay(relayUrl) {
31536
+ if (relayUrl.protocol !== "ws:" && relayUrl.protocol !== "wss:") {
31537
+ throw new MobiusRuntimeConfigurationError("The Runtime relay URL must use WebSocket transport");
31538
+ }
31539
+ const credential = relayUrl.searchParams.get("token") ?? "";
31540
+ if (credential.length === 0) {
31541
+ throw new MobiusRuntimeConfigurationError("The Runtime relay credential is required");
31542
+ }
31543
+ if (APP_CREDENTIAL_PATTERN.test(credential)) {
31544
+ throw new MobiusRuntimeConfigurationError("An AppCredential cannot be held by a local Runtime");
31545
+ }
31546
+ if (CONVERSATION_CREDENTIAL_PATTERN.test(credential)) {
31547
+ throw new MobiusRuntimeConfigurationError("A ConversationAccessToken cannot be held by a local Runtime");
31548
+ }
31549
+ if (!RUNTIME_CREDENTIAL_PATTERN.test(credential)) {
31550
+ throw new MobiusRuntimeConfigurationError("The Runtime relay credential is invalid");
31551
+ }
31552
+ }
31553
+ function validateRuntimeConfiguration(config2) {
31554
+ validateRuntimeRelay(config2.relayUrl);
31555
+ }
31556
+ var MobiusRuntimeEngine = class {
31557
+ #config;
31558
+ #state = new IdleMobiusRuntimeState();
31559
+ constructor(config2) {
31560
+ validateRuntimeConfiguration(config2);
31561
+ this.#config = config2;
31562
+ }
31563
+ run = async (signal) => {
31564
+ if (!(this.#state instanceof IdleMobiusRuntimeState)) {
31565
+ throw new MobiusRuntimeConfigurationError("A Mobius Runtime engine can only be started once");
31566
+ }
31567
+ signal.throwIfAborted();
31568
+ this.#state = new RunningMobiusRuntimeState();
31569
+ const audit = new AuditStore(this.#config.auditDatabasePath);
31570
+ try {
31571
+ await runDaemon(this.#config, audit, signal);
31572
+ } finally {
31573
+ audit.close();
31574
+ this.#state = new FinishedMobiusRuntimeState();
31575
+ }
31576
+ };
31577
+ };
31578
+
31179
31579
  // src/cli.ts
31180
31580
  var shutdown = new AbortController();
31181
31581
  process.once("SIGINT", () => shutdown.abort());
@@ -31183,7 +31583,7 @@ process.once("SIGTERM", () => shutdown.abort());
31183
31583
  var USAGE = `Mobius CLI
31184
31584
 
31185
31585
  Usage:
31186
- mobius pair --url <relay-url> --code <pairing-code> [--workspace <directory>]
31586
+ mobius pair --url <relay-url> --pairing <runtime-pairing-id> --code <pairing-code> [--workspace <directory>]
31187
31587
  mobius start --channel <channel-id>
31188
31588
  mobius agents [--json]
31189
31589
  mobius bot message send --target <current|alias> --text <message>
@@ -31256,12 +31656,7 @@ async function main() {
31256
31656
  process.stdout.write(`Default workspace: ${config2.defaultWorkspace}
31257
31657
 
31258
31658
  `);
31259
- const audit = new AuditStore(config2.auditDatabasePath);
31260
- try {
31261
- await runDaemon(config2, audit, shutdown.signal);
31262
- } finally {
31263
- audit.close();
31264
- }
31659
+ await new MobiusRuntimeEngine(config2).run(shutdown.signal);
31265
31660
  }
31266
31661
  try {
31267
31662
  await main();