@moxt-ai/mobius 0.0.25 → 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.
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.25",
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: {
@@ -1578,7 +1578,53 @@ import { fetch } from "undici";
1578
1578
 
1579
1579
  // src/network/environment-proxy.ts
1580
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;
1581
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
+ }
1582
1628
 
1583
1629
  // src/agent-installation/managed-agent-archive.ts
1584
1630
  var ARCHIVE_DOWNLOAD_LIMIT_BYTES = 128 * 1024 * 1024;
@@ -23205,6 +23251,7 @@ var encode4 = (message, schema) => {
23205
23251
  }
23206
23252
  return JSON.stringify(decoded.data);
23207
23253
  };
23254
+ var decodeSessionEventEnvelope = (encoded) => decode5(encoded, eventEnvelopeSchema);
23208
23255
  var encodeSessionEventEnvelope = (message) => encode4(message, eventEnvelopeSchema);
23209
23256
  var decodeSessionEventAcknowledgement = (encoded) => decode5(encoded, eventAcknowledgementSchema);
23210
23257
  var decodeSessionEventRejection = (encoded) => decode5(encoded, eventRejectionSchema);
@@ -29246,9 +29293,11 @@ import { WebSocket } from "undici";
29246
29293
  var CONNECTION_TIMEOUT_MILLISECONDS = 15e3;
29247
29294
  var RelayConnectionError = class extends Error {
29248
29295
  code;
29249
- constructor(code, message, options = {}) {
29296
+ diagnosticDetails;
29297
+ constructor(code, message, diagnosticDetails = {}, options = {}) {
29250
29298
  super(message, options);
29251
29299
  this.code = code;
29300
+ this.diagnosticDetails = diagnosticDetails;
29252
29301
  this.name = "RelayConnectionError";
29253
29302
  }
29254
29303
  };
@@ -29286,16 +29335,34 @@ function connectRelay(url2, signal) {
29286
29335
  };
29287
29336
  const onAbort = () => {
29288
29337
  fail(
29289
- new RelayConnectionError("relay_connection_cancelled", "Relay connection was cancelled", {
29290
- cause: signal.reason
29291
- })
29338
+ new RelayConnectionError(
29339
+ "relay_connection_cancelled",
29340
+ "Relay connection was cancelled",
29341
+ {
29342
+ cancellation: "requested"
29343
+ },
29344
+ { cause: signal.reason }
29345
+ )
29292
29346
  );
29293
29347
  };
29294
- const onClose = () => {
29295
- 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
+ );
29296
29355
  };
29297
- const onError = () => {
29298
- 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
+ );
29299
29366
  };
29300
29367
  const onOpen = () => {
29301
29368
  if (finished) {
@@ -29306,7 +29373,11 @@ function connectRelay(url2, signal) {
29306
29373
  resolve5(socket);
29307
29374
  };
29308
29375
  function onTimeout() {
29309
- 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
+ );
29310
29381
  }
29311
29382
  signal.addEventListener("abort", onAbort, { once: true });
29312
29383
  socket.addEventListener("close", onClose, { once: true });
@@ -29318,6 +29389,142 @@ function connectRelay(url2, signal) {
29318
29389
  });
29319
29390
  }
29320
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
+
29321
29528
  // src/relay-connection/liveness.ts
29322
29529
  import { randomUUID as randomUUID5 } from "node:crypto";
29323
29530
  var HEARTBEAT_ACK_TIMEOUT_MILLISECONDS = 1e4;
@@ -29394,9 +29601,10 @@ var RelayConnectionLiveness = class {
29394
29601
  if (!(this.#state instanceof AwaitingHeartbeatAckState)) {
29395
29602
  throw new Error("Heartbeat timeout occurred in an invalid state");
29396
29603
  }
29604
+ const heartbeatId = this.#state.heartbeatId;
29397
29605
  this.#state.timeout.cancel();
29398
29606
  this.#state = new FinishedRelayLivenessState();
29399
- this.#transport.unresponsive();
29607
+ this.#transport.unresponsive(heartbeatId, HEARTBEAT_ACK_TIMEOUT_MILLISECONDS);
29400
29608
  };
29401
29609
  #sendHeartbeat = () => {
29402
29610
  if (this.#state instanceof WaitingForHeartbeatState) {
@@ -29445,6 +29653,7 @@ var SessionEventOutbox = class {
29445
29653
  ordinal INTEGER PRIMARY KEY AUTOINCREMENT,
29446
29654
  event_id TEXT NOT NULL UNIQUE,
29447
29655
  session_id TEXT NOT NULL,
29656
+ source_key TEXT NOT NULL,
29448
29657
  envelope_json TEXT NOT NULL
29449
29658
  ) STRICT;
29450
29659
  CREATE INDEX IF NOT EXISTS relay_session_event_outbox_order
@@ -29472,6 +29681,27 @@ var SessionEventOutbox = class {
29472
29681
  )
29473
29682
  ) STRICT;
29474
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
+ `);
29475
29705
  }
29476
29706
  claimExecution = (commandId, sessionId, commandJson) => {
29477
29707
  this.#database.exec("BEGIN IMMEDIATE");
@@ -29529,12 +29759,10 @@ var SessionEventOutbox = class {
29529
29759
  const state = typeof row === "object" && row !== null ? Reflect.get(row, "state") : void 0;
29530
29760
  return state === "succeeded" || state === "failed" || state === "cancelled";
29531
29761
  };
29532
- acknowledge = (acknowledgement) => {
29533
- this.#database.prepare(
29534
- `DELETE FROM relay_session_event_outbox
29762
+ acknowledge = (acknowledgement) => this.#database.prepare(
29763
+ `DELETE FROM relay_session_event_outbox
29535
29764
  WHERE event_id = ? AND session_id = ?`
29536
- ).run(acknowledgement.eventId, acknowledgement.sessionId);
29537
- };
29765
+ ).run(acknowledgement.eventId, acknowledgement.sessionId).changes === 1;
29538
29766
  quarantine = (rejection) => {
29539
29767
  this.#database.exec("BEGIN IMMEDIATE");
29540
29768
  try {
@@ -29601,9 +29829,9 @@ var SessionEventOutbox = class {
29601
29829
  ).run(sourceKey, nextSourceSeq + 1);
29602
29830
  this.#database.prepare(
29603
29831
  `INSERT INTO relay_session_event_outbox (
29604
- event_id, session_id, envelope_json
29605
- ) VALUES (?, ?, ?)`
29606
- ).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);
29607
29835
  this.#database.exec("COMMIT");
29608
29836
  return encoded;
29609
29837
  } catch (error61) {
@@ -29616,6 +29844,27 @@ var SessionEventOutbox = class {
29616
29844
  FROM relay_session_event_outbox
29617
29845
  ORDER BY ordinal`
29618
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
+ };
29619
29868
  };
29620
29869
  function isDaemonExecutionState(value) {
29621
29870
  return value === "cancelled" || value === "dispatching" || value === "failed" || value === "running" || value === "succeeded";
@@ -29632,7 +29881,7 @@ var systemDaemonTimer = new SystemDaemonTimer();
29632
29881
 
29633
29882
  // src/run-daemon.ts
29634
29883
  var RELAY_RECONNECT_DELAY_MILLISECONDS = 500;
29635
- var RELAY_CONNECTION_REFRESH_MILLISECONDS = 20 * 60 * 1e3;
29884
+ var AGENT_ACTIVITY_COALESCING_MILLISECONDS = 100;
29636
29885
  var ACTIVE_TURN_RECONCILIATION_TIMEOUT_MILLISECONDS = 2e4;
29637
29886
  var ACTIVE_TURN_RECONCILIATION_CLOSE_CODE = 4009;
29638
29887
  function botIpcSocketPath() {
@@ -29641,54 +29890,6 @@ function botIpcSocketPath() {
29641
29890
  }
29642
29891
  return join16("/tmp", `mobius-bot-${randomUUID6()}.sock`);
29643
29892
  }
29644
- var RelayConnectionRefreshState = class {
29645
- };
29646
- var ScheduledRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
29647
- };
29648
- var DueRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
29649
- };
29650
- var FinishedRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
29651
- };
29652
- var RelayConnectionRefresh = class {
29653
- #isSafe;
29654
- #refresh;
29655
- #scheduledTask;
29656
- #state = new ScheduledRelayConnectionRefreshState();
29657
- constructor(timer, isSafe, refresh) {
29658
- this.#isSafe = isSafe;
29659
- this.#refresh = refresh;
29660
- this.#scheduledTask = timer.schedule(this.#request, RELAY_CONNECTION_REFRESH_MILLISECONDS);
29661
- }
29662
- afterTurnFinished = () => {
29663
- if (this.#state instanceof DueRelayConnectionRefreshState) {
29664
- this.#request();
29665
- return;
29666
- }
29667
- if (this.#state instanceof ScheduledRelayConnectionRefreshState || this.#state instanceof FinishedRelayConnectionRefreshState) {
29668
- return;
29669
- }
29670
- throw new Error("Unknown relay connection refresh state");
29671
- };
29672
- close = () => {
29673
- this.#scheduledTask.cancel();
29674
- this.#state = new FinishedRelayConnectionRefreshState();
29675
- };
29676
- #request = () => {
29677
- if (this.#state instanceof FinishedRelayConnectionRefreshState) {
29678
- return;
29679
- }
29680
- if (!(this.#state instanceof ScheduledRelayConnectionRefreshState) && !(this.#state instanceof DueRelayConnectionRefreshState)) {
29681
- throw new Error("Unknown relay connection refresh state");
29682
- }
29683
- if (!this.#isSafe()) {
29684
- this.#state = new DueRelayConnectionRefreshState();
29685
- return;
29686
- }
29687
- this.#state = new FinishedRelayConnectionRefreshState();
29688
- this.#scheduledTask.cancel();
29689
- this.#refresh();
29690
- };
29691
- };
29692
29893
  var DaemonAcpObserver = class {
29693
29894
  #record;
29694
29895
  constructor(record2) {
@@ -29953,7 +30154,8 @@ var DaemonSessionEventConnectionState = class {
29953
30154
  };
29954
30155
  var DisconnectedDaemonSessionEventState = class extends DaemonSessionEventConnectionState {
29955
30156
  disconnect = () => this;
29956
- send = () => void 0;
30157
+ matches = () => false;
30158
+ send = () => false;
29957
30159
  };
29958
30160
  var ConnectedDaemonSessionEventState = class extends DaemonSessionEventConnectionState {
29959
30161
  #socket;
@@ -29962,38 +30164,155 @@ var ConnectedDaemonSessionEventState = class extends DaemonSessionEventConnectio
29962
30164
  this.#socket = socket;
29963
30165
  }
29964
30166
  disconnect = (socket) => socket === this.#socket ? new DisconnectedDaemonSessionEventState() : this;
30167
+ matches = (socket) => socket === this.#socket;
29965
30168
  send = (message) => {
29966
- if (this.#socket.readyState === WebSocket2.OPEN) {
29967
- try {
29968
- this.#socket.send(message);
29969
- } catch {
29970
- }
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;
29971
30177
  }
29972
30178
  };
29973
30179
  };
30180
+ var SESSION_EVENT_REPLAY_WINDOW = 4;
29974
30181
  var DaemonSessionEventPublisher = class {
29975
30182
  #outbox;
30183
+ #inFlightEventIds = /* @__PURE__ */ new Set();
29976
30184
  #connection = new DisconnectedDaemonSessionEventState();
29977
30185
  constructor(outbox) {
29978
30186
  this.#outbox = outbox;
29979
30187
  }
29980
30188
  connect = (socket) => {
29981
- const connection = new ConnectedDaemonSessionEventState(socket);
29982
- this.#connection = connection;
29983
- for (const pending of this.#outbox.pending()) {
29984
- connection.send(pending);
29985
- }
30189
+ this.#connection = new ConnectedDaemonSessionEventState(socket);
30190
+ this.#inFlightEventIds.clear();
30191
+ this.#publishNextBatch();
29986
30192
  };
29987
30193
  disconnect = (socket) => {
30194
+ if (!this.#connection.matches(socket)) {
30195
+ return;
30196
+ }
29988
30197
  this.#connection = this.#connection.disconnect(socket);
30198
+ this.#inFlightEventIds.clear();
29989
30199
  };
29990
30200
  recordAttempt = (sessionId, payload, attemptId) => {
29991
- this.#connection.send(this.#outbox.enqueue(sessionId, payload, attemptId));
30201
+ this.#outbox.enqueue(sessionId, payload, attemptId);
30202
+ this.#publishNextBatch();
29992
30203
  };
29993
30204
  recordRuntime = (sessionId, payload) => {
29994
- 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
+ }
30237
+ };
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
+ }
29995
30295
  };
29996
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
+ }
29997
30316
  function reportFailure(publisher, command, code, message, attemptId) {
29998
30317
  publisher.recordAttempt(
29999
30318
  command.sessionId,
@@ -30170,7 +30489,10 @@ async function executeSessionConfigure(publisher, socket, command, config2, dire
30170
30489
  reportOperationFailure(socket, command.requestId, command.sessionId, error61);
30171
30490
  }
30172
30491
  }
30173
- 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
+ });
30174
30496
  try {
30175
30497
  const resolved = await resolveSessionDirectories(
30176
30498
  directories,
@@ -30227,30 +30549,7 @@ async function executePrompt(outbox, publisher, command, config2, directories, a
30227
30549
  cwd: resolved.cwd,
30228
30550
  disposition: command.disposition,
30229
30551
  images: attachments.images,
30230
- onActivity: (activity) => {
30231
- publisher.recordAttempt(
30232
- command.sessionId,
30233
- encodeAgentActivityEvent({
30234
- activityId: activity.activityId,
30235
- activityType: activity.activityType,
30236
- commandId: command.commandId,
30237
- content: activity.content,
30238
- detail: activity.detail,
30239
- input: activity.input,
30240
- kind: AGENT_ACTIVITY_KIND,
30241
- locations: activity.locations,
30242
- name: activity.name,
30243
- output: activity.output,
30244
- sequence: activity.sequence,
30245
- sessionId: command.sessionId,
30246
- status: activity.status,
30247
- title: activity.title,
30248
- toolKind: activity.toolKind
30249
- }),
30250
- attemptId
30251
- );
30252
- return Promise.resolve();
30253
- },
30552
+ onActivity: activities.record,
30254
30553
  onText: (text, messageId) => {
30255
30554
  publisher.recordAttempt(
30256
30555
  command.sessionId,
@@ -30273,6 +30572,7 @@ async function executePrompt(outbox, publisher, command, config2, directories, a
30273
30572
  });
30274
30573
  stopReason = result.stopReason;
30275
30574
  }
30575
+ activities.flush();
30276
30576
  publisher.recordAttempt(
30277
30577
  command.sessionId,
30278
30578
  encodeSessionCompletedEvent({
@@ -30290,6 +30590,7 @@ async function executePrompt(outbox, publisher, command, config2, directories, a
30290
30590
  sessionId: command.sessionId
30291
30591
  });
30292
30592
  } catch (error61) {
30593
+ activities.flush();
30293
30594
  audit.record({
30294
30595
  commandId: command.commandId,
30295
30596
  event: "session_failed",
@@ -30328,7 +30629,10 @@ function botExecutionAgent(agent, contextHandle, socketPath) {
30328
30629
  agent.capabilities
30329
30630
  );
30330
30631
  }
30331
- 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
+ });
30332
30636
  try {
30333
30637
  const deadline = Date.parse(command.executionDeadlineAt);
30334
30638
  const remaining = deadline - Date.now();
@@ -30412,30 +30716,7 @@ async function executeBotPrompt(outbox, publisher, command, config2, directories
30412
30716
  commandId: command.commandId,
30413
30717
  cwd: resolved.cwd,
30414
30718
  images: attachments.images,
30415
- onActivity: (activity) => {
30416
- publisher.recordAttempt(
30417
- command.sessionId,
30418
- encodeAgentActivityEvent({
30419
- activityId: activity.activityId,
30420
- activityType: activity.activityType,
30421
- commandId: command.commandId,
30422
- content: activity.content,
30423
- detail: activity.detail,
30424
- input: activity.input,
30425
- kind: AGENT_ACTIVITY_KIND,
30426
- locations: activity.locations,
30427
- name: activity.name,
30428
- output: activity.output,
30429
- sequence: activity.sequence,
30430
- sessionId: command.sessionId,
30431
- status: activity.status,
30432
- title: activity.title,
30433
- toolKind: activity.toolKind
30434
- }),
30435
- attemptId
30436
- );
30437
- return Promise.resolve();
30438
- },
30719
+ onActivity: activities.record,
30439
30720
  onText: (text, messageId) => {
30440
30721
  publisher.recordAttempt(
30441
30722
  command.sessionId,
@@ -30455,6 +30736,7 @@ async function executeBotPrompt(outbox, publisher, command, config2, directories
30455
30736
  sessionId: command.sessionId,
30456
30737
  signal: executionSignal
30457
30738
  });
30739
+ activities.flush();
30458
30740
  publisher.recordAttempt(
30459
30741
  command.sessionId,
30460
30742
  encodeSessionCompletedEvent({
@@ -30479,6 +30761,7 @@ async function executeBotPrompt(outbox, publisher, command, config2, directories
30479
30761
  lease.release();
30480
30762
  }
30481
30763
  } catch (error61) {
30764
+ activities.flush();
30482
30765
  const failure = Date.parse(command.executionDeadlineAt) <= Date.now() ? new LocalBotAuthorizationError("turn_expired", "The Bot execution deadline has expired", {
30483
30766
  cause: error61
30484
30767
  }) : error61;
@@ -30518,15 +30801,15 @@ function waitForRelayRetry(signal, timer) {
30518
30801
  signal.addEventListener("abort", finish, { once: true });
30519
30802
  });
30520
30803
  }
30521
- async function publishAgentCatalogUntilAvailable(config2, signal, timer) {
30804
+ async function publishAgentCatalogUntilAvailable(config2, diagnostics, signal, timer) {
30522
30805
  try {
30523
30806
  await publishAgentCatalog(config2, signal);
30524
30807
  return;
30525
- } catch {
30808
+ } catch (error61) {
30526
30809
  if (signal.aborted) {
30527
30810
  return;
30528
30811
  }
30529
- console.warn("Mobius daemon is waiting to publish its Agent catalog");
30812
+ diagnostics.catalogFailed(error61);
30530
30813
  }
30531
30814
  while (!signal.aborted) {
30532
30815
  await waitForRelayRetry(signal, timer);
@@ -30540,11 +30823,12 @@ async function publishAgentCatalogUntilAvailable(config2, signal, timer) {
30540
30823
  }
30541
30824
  }
30542
30825
  }
30543
- 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) {
30544
30827
  const relayUrl = new URL(config2.relayUrl);
30545
30828
  relayUrl.searchParams.set("liveness", "1");
30546
30829
  const socket = await connectRelay(relayUrl, signal);
30547
30830
  const reconnectingTurns = [...activeTurns.values()];
30831
+ diagnostics.connectionOpened(connectionEpoch);
30548
30832
  onConnected();
30549
30833
  const connectionController = new AbortController();
30550
30834
  const nativeRequests = /* @__PURE__ */ new Map();
@@ -30553,7 +30837,6 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30553
30837
  await new Promise((resolve5, reject) => {
30554
30838
  let finished = false;
30555
30839
  const cleanup = () => {
30556
- connectionRefresh.close();
30557
30840
  connectionLiveness.close();
30558
30841
  signal.removeEventListener("abort", stop);
30559
30842
  socket.removeEventListener("close", handleClose);
@@ -30571,61 +30854,70 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30571
30854
  cleanup();
30572
30855
  resolve5();
30573
30856
  };
30574
- const rejectConnection = () => {
30857
+ const rejectConnection = (failure) => {
30575
30858
  if (finished) {
30576
30859
  return;
30577
30860
  }
30578
30861
  finished = true;
30579
30862
  cleanup();
30580
- reject(new Error("Relay WebSocket failed"));
30863
+ reject(failure);
30581
30864
  };
30582
30865
  const stop = () => {
30866
+ diagnostics.connectionClosed(connectionEpoch, 1e3, "Daemon stopped", "daemon_stop");
30583
30867
  closeRelaySocket(socket, 1e3, "Daemon stopped");
30584
30868
  resolveConnection();
30585
30869
  };
30586
- const refreshConnection = () => {
30587
- closeRelaySocket(socket, 1e3, "Refreshing idle relay connection");
30588
- 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);
30589
30875
  };
30590
30876
  const connectionLiveness = new RelayConnectionLiveness(timer, {
30591
30877
  send: (message) => send(socket, message),
30592
- 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
+ );
30593
30886
  closeRelaySocket(socket, 4001, "Relay liveness check timed out");
30594
30887
  resolveConnection();
30595
30888
  }
30596
30889
  });
30597
- const connectionRefresh = new RelayConnectionRefresh(
30598
- timer,
30599
- () => activeTurns.size === 0,
30600
- refreshConnection
30601
- );
30602
30890
  signal.addEventListener("abort", stop, { once: true });
30603
- const handleClose = () => {
30891
+ const handleClose = (event) => {
30892
+ diagnostics.connectionClosed(connectionEpoch, event.code, event.reason, "relay");
30604
30893
  resolveConnection();
30605
30894
  };
30606
- const handleError = () => {
30895
+ const handleError = (event) => {
30607
30896
  if (signal.aborted) {
30608
30897
  resolveConnection();
30609
30898
  return;
30610
30899
  }
30611
- 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);
30612
30905
  };
30613
30906
  const handleMessage = (event) => {
30614
30907
  const encoded = event.data;
30615
30908
  if (typeof encoded !== "string") {
30616
- closeRelaySocket(socket, 4003, "Text message required");
30617
- rejectConnection();
30909
+ failAndClose(new Error("Relay sent a non-text message"), 4003, "Text message required", "protocol");
30618
30910
  return;
30619
30911
  }
30620
30912
  try {
30621
30913
  const kind = decodeMessageKind(encoded);
30622
30914
  if (kind === SESSION_EVENT_ACK_KIND) {
30623
- outbox.acknowledge(decodeSessionEventAcknowledgement(encoded));
30915
+ publisher.acknowledge(decodeSessionEventAcknowledgement(encoded));
30624
30916
  return;
30625
30917
  }
30626
30918
  if (kind === SESSION_EVENT_REJECTED_KIND) {
30627
30919
  const rejection = decodeSessionEventRejection(encoded);
30628
- if (outbox.quarantine(rejection)) {
30920
+ if (publisher.quarantine(rejection)) {
30629
30921
  audit.record({
30630
30922
  commandId: rejection.eventId,
30631
30923
  details: { code: rejection.code },
@@ -31004,6 +31296,7 @@ async function runRelayConnection(config2, directories, audit, sessions, install
31004
31296
  applicator,
31005
31297
  ipc,
31006
31298
  receipt2.attemptId,
31299
+ timer,
31007
31300
  turn2.signal
31008
31301
  ).catch((error61) => {
31009
31302
  console.error("Mobius daemon Bot turn failed", error61);
@@ -31013,7 +31306,6 @@ async function runRelayConnection(config2, directories, audit, sessions, install
31013
31306
  activeAttemptIds.delete(command2.sessionId);
31014
31307
  }
31015
31308
  turn2.finish();
31016
- connectionRefresh.afterTurnFinished();
31017
31309
  });
31018
31310
  return;
31019
31311
  }
@@ -31042,6 +31334,7 @@ async function runRelayConnection(config2, directories, audit, sessions, install
31042
31334
  audit,
31043
31335
  sessions,
31044
31336
  receipt.attemptId,
31337
+ timer,
31045
31338
  turn.signal
31046
31339
  ).catch((error61) => {
31047
31340
  console.error("Mobius daemon turn failed", error61);
@@ -31051,12 +31344,15 @@ async function runRelayConnection(config2, directories, audit, sessions, install
31051
31344
  activeAttemptIds.delete(command.sessionId);
31052
31345
  }
31053
31346
  turn.finish();
31054
- connectionRefresh.afterTurnFinished();
31055
31347
  });
31056
31348
  } catch (error61) {
31057
31349
  console.error("Mobius daemon rejected a message", error61);
31058
- closeRelaySocket(socket, 4008, "Invalid protocol message");
31059
- rejectConnection();
31350
+ failAndClose(
31351
+ new Error("Relay sent an invalid protocol message", { cause: error61 }),
31352
+ 4008,
31353
+ "Invalid protocol message",
31354
+ "protocol"
31355
+ );
31060
31356
  }
31061
31357
  };
31062
31358
  socket.addEventListener("close", handleClose);
@@ -31068,21 +31364,18 @@ async function runRelayConnection(config2, directories, audit, sessions, install
31068
31364
  return;
31069
31365
  }
31070
31366
  publisher.connect(socket);
31071
- void Promise.all(reconnectingTurns.map((turn) => turn.completion)).then(
31072
- connectionRefresh.afterTurnFinished
31073
- );
31074
31367
  },
31075
31368
  (error61) => {
31076
31369
  if (finished) {
31077
31370
  return;
31078
31371
  }
31079
31372
  console.error("Mobius daemon could not reconcile active Agent turns", error61);
31080
- closeRelaySocket(
31081
- socket,
31373
+ failAndClose(
31374
+ new Error("Active Agent turn reconciliation failed", { cause: error61 }),
31082
31375
  ACTIVE_TURN_RECONCILIATION_CLOSE_CODE,
31083
- "Active Agent turn reconciliation failed"
31376
+ "Active Agent turn reconciliation failed",
31377
+ "reconciliation"
31084
31378
  );
31085
- rejectConnection();
31086
31379
  }
31087
31380
  );
31088
31381
  if (signal.aborted) {
@@ -31126,6 +31419,7 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31126
31419
  const sessions = new AcpSessionManager(new DaemonAcpObserver(recordSessionEvent), sessionStore);
31127
31420
  const directories = new WorkspaceDirectoryService(config2.bindingId, config2.defaultWorkspace);
31128
31421
  const installationObserver = new DaemonInstallationObserver();
31422
+ const diagnostics = new RelayConnectionDiagnostics(audit, config2.channelId, config2.relayUrl);
31129
31423
  const nativeSessions = new NativeSessionService(config2, directories, sessions, audit);
31130
31424
  const installations = new AgentInstallationManager(
31131
31425
  specifications,
@@ -31133,7 +31427,7 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31133
31427
  installationObserver,
31134
31428
  installationAudit
31135
31429
  );
31136
- let retryAnnounced = false;
31430
+ let connectionEpoch = 0;
31137
31431
  let connectionAnnounced = false;
31138
31432
  const announceConnection = () => {
31139
31433
  if (connectionAnnounced) {
@@ -31149,10 +31443,11 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31149
31443
  `
31150
31444
  );
31151
31445
  };
31152
- const catalogPublication = publishAgentCatalogUntilAvailable(config2, signal, timer);
31446
+ const catalogPublication = publishAgentCatalogUntilAvailable(config2, diagnostics, signal, timer);
31153
31447
  try {
31154
31448
  await ipc.listen(signal);
31155
31449
  while (!signal.aborted) {
31450
+ connectionEpoch += 1;
31156
31451
  try {
31157
31452
  await runRelayConnection(
31158
31453
  config2,
@@ -31170,19 +31465,17 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31170
31465
  ipc,
31171
31466
  activeTurns,
31172
31467
  activeAttemptIds,
31468
+ connectionEpoch,
31469
+ diagnostics,
31173
31470
  announceConnection,
31174
31471
  signal,
31175
31472
  timer
31176
31473
  );
31177
- retryAnnounced = false;
31178
- } catch {
31474
+ } catch (error61) {
31179
31475
  if (signal.aborted) {
31180
31476
  return;
31181
31477
  }
31182
- if (!retryAnnounced) {
31183
- console.warn("Mobius daemon is waiting for the relay");
31184
- retryAnnounced = true;
31185
- }
31478
+ diagnostics.connectionFailed(connectionEpoch, error61, "connection");
31186
31479
  }
31187
31480
  await waitForRelayRetry(signal, timer);
31188
31481
  }