@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/runtime.js CHANGED
@@ -1450,7 +1450,53 @@ import { fetch } from "undici";
1450
1450
 
1451
1451
  // src/network/environment-proxy.ts
1452
1452
  import { EnvHttpProxyAgent } from "undici";
1453
+ var configuredHttpProxy = process.env["http_proxy"] ?? process.env["HTTP_PROXY"] ?? "";
1454
+ var configuredHttpsProxy = process.env["https_proxy"] ?? process.env["HTTPS_PROXY"] ?? configuredHttpProxy;
1453
1455
  var environmentProxyDispatcher = new EnvHttpProxyAgent();
1456
+ function bypassesProxy(target) {
1457
+ const noProxy = process.env["no_proxy"] ?? process.env["NO_PROXY"] ?? "";
1458
+ if (noProxy === "*") {
1459
+ return true;
1460
+ }
1461
+ const hostname4 = target.hostname.replace(/^\[|\]$/gu, "").toLowerCase();
1462
+ const defaultPort = target.protocol === "wss:" || target.protocol === "https:" ? 443 : 80;
1463
+ const port = Number.parseInt(target.port, 10) || defaultPort;
1464
+ for (const rawEntry of noProxy.split(/[,\s]/u)) {
1465
+ if (rawEntry.length === 0) {
1466
+ continue;
1467
+ }
1468
+ const parsed = rawEntry.match(/^(.+):(\d+)$/u);
1469
+ const entryHostname = (parsed?.[1] ?? rawEntry).replace(/^\*?\./u, "").toLowerCase();
1470
+ const entryPort = parsed?.[2] === void 0 ? 0 : Number.parseInt(parsed[2], 10);
1471
+ if (entryPort !== 0 && entryPort !== port) {
1472
+ continue;
1473
+ }
1474
+ if (hostname4 === entryHostname || hostname4.endsWith(`.${entryHostname}`)) {
1475
+ return true;
1476
+ }
1477
+ }
1478
+ return false;
1479
+ }
1480
+ function sanitizedProxyOrigin(proxyUrl) {
1481
+ try {
1482
+ return new URL(proxyUrl).origin;
1483
+ } catch {
1484
+ return "configured-invalid-url";
1485
+ }
1486
+ }
1487
+ function describeEnvironmentProxyRoute(target) {
1488
+ if (bypassesProxy(target)) {
1489
+ return { proxyRoute: "direct" };
1490
+ }
1491
+ const proxyUrl = target.protocol === "wss:" || target.protocol === "https:" ? configuredHttpsProxy : configuredHttpProxy;
1492
+ if (proxyUrl.length === 0) {
1493
+ return { proxyRoute: "direct" };
1494
+ }
1495
+ return {
1496
+ proxyOrigin: sanitizedProxyOrigin(proxyUrl),
1497
+ proxyRoute: "proxy"
1498
+ };
1499
+ }
1454
1500
 
1455
1501
  // src/agent-installation/managed-agent-archive.ts
1456
1502
  var ARCHIVE_DOWNLOAD_LIMIT_BYTES = 128 * 1024 * 1024;
@@ -22967,6 +23013,7 @@ var encode4 = (message, schema) => {
22967
23013
  }
22968
23014
  return JSON.stringify(decoded.data);
22969
23015
  };
23016
+ var decodeSessionEventEnvelope = (encoded) => decode5(encoded, eventEnvelopeSchema);
22970
23017
  var encodeSessionEventEnvelope = (message) => encode4(message, eventEnvelopeSchema);
22971
23018
  var decodeSessionEventAcknowledgement = (encoded) => decode5(encoded, eventAcknowledgementSchema);
22972
23019
  var decodeSessionEventRejection = (encoded) => decode5(encoded, eventRejectionSchema);
@@ -28616,9 +28663,11 @@ import { WebSocket } from "undici";
28616
28663
  var CONNECTION_TIMEOUT_MILLISECONDS = 15e3;
28617
28664
  var RelayConnectionError = class extends Error {
28618
28665
  code;
28619
- constructor(code, message, options = {}) {
28666
+ diagnosticDetails;
28667
+ constructor(code, message, diagnosticDetails = {}, options = {}) {
28620
28668
  super(message, options);
28621
28669
  this.code = code;
28670
+ this.diagnosticDetails = diagnosticDetails;
28622
28671
  this.name = "RelayConnectionError";
28623
28672
  }
28624
28673
  };
@@ -28656,16 +28705,34 @@ function connectRelay(url2, signal) {
28656
28705
  };
28657
28706
  const onAbort = () => {
28658
28707
  fail(
28659
- new RelayConnectionError("relay_connection_cancelled", "Relay connection was cancelled", {
28660
- cause: signal.reason
28661
- })
28708
+ new RelayConnectionError(
28709
+ "relay_connection_cancelled",
28710
+ "Relay connection was cancelled",
28711
+ {
28712
+ cancellation: "requested"
28713
+ },
28714
+ { cause: signal.reason }
28715
+ )
28662
28716
  );
28663
28717
  };
28664
- const onClose = () => {
28665
- fail(new RelayConnectionError("relay_connection_closed", "Relay connection closed before it was ready"));
28718
+ const onClose = (event) => {
28719
+ fail(
28720
+ new RelayConnectionError("relay_connection_closed", "Relay connection closed before it was ready", {
28721
+ closeCode: event.code.toString(),
28722
+ closeReason: event.reason
28723
+ })
28724
+ );
28666
28725
  };
28667
- const onError = () => {
28668
- fail(new RelayConnectionError("relay_connection_failed", "Relay connection failed"));
28726
+ const onError = (event) => {
28727
+ const cause = Reflect.get(event, "error");
28728
+ fail(
28729
+ new RelayConnectionError(
28730
+ "relay_connection_failed",
28731
+ "Relay connection failed",
28732
+ {},
28733
+ { cause: cause ?? event }
28734
+ )
28735
+ );
28669
28736
  };
28670
28737
  const onOpen = () => {
28671
28738
  if (finished) {
@@ -28676,7 +28743,11 @@ function connectRelay(url2, signal) {
28676
28743
  resolve6(socket);
28677
28744
  };
28678
28745
  function onTimeout() {
28679
- fail(new RelayConnectionError("relay_connection_timeout", "Relay connection timed out"));
28746
+ fail(
28747
+ new RelayConnectionError("relay_connection_timeout", "Relay connection timed out", {
28748
+ timeoutMilliseconds: CONNECTION_TIMEOUT_MILLISECONDS.toString()
28749
+ })
28750
+ );
28680
28751
  }
28681
28752
  signal.addEventListener("abort", onAbort, { once: true });
28682
28753
  socket.addEventListener("close", onClose, { once: true });
@@ -28688,6 +28759,142 @@ function connectRelay(url2, signal) {
28688
28759
  });
28689
28760
  }
28690
28761
 
28762
+ // src/relay-connection/diagnostics.ts
28763
+ var MAX_DIAGNOSTIC_TEXT_LENGTH = 256;
28764
+ function sanitizeDiagnosticText(value) {
28765
+ const printable = Array.from(value, (character) => {
28766
+ const code = character.charCodeAt(0);
28767
+ return code < 32 || code === 127 ? " " : character;
28768
+ }).join("");
28769
+ const withoutSecrets = printable.replace(/([?&](?:access_token|authorization|credential|key|secret|token)=)[^&\s]*/giu, "$1[redacted]").replace(/(?:https?|wss?):\/\/[^\s)]+/giu, (encodedUrl) => {
28770
+ try {
28771
+ return new URL(encodedUrl).origin;
28772
+ } catch {
28773
+ return "[redacted-url]";
28774
+ }
28775
+ });
28776
+ return withoutSecrets.slice(0, MAX_DIAGNOSTIC_TEXT_LENGTH);
28777
+ }
28778
+ function addFailureDetails(details, failure) {
28779
+ let current = failure;
28780
+ for (let depth = 0; depth < 3 && current instanceof Error; depth += 1) {
28781
+ const prefix = depth === 0 ? "failure" : `cause${depth.toString()}`;
28782
+ details[`${prefix}Name`] = current.name;
28783
+ details[`${prefix}Message`] = sanitizeDiagnosticText(current.message);
28784
+ const code = Reflect.get(current, "code");
28785
+ if (typeof code === "string" || typeof code === "number") {
28786
+ details[`${prefix}Code`] = String(code);
28787
+ }
28788
+ const statusCode = Reflect.get(current, "statusCode");
28789
+ if (typeof statusCode === "string" || typeof statusCode === "number") {
28790
+ details[`${prefix}StatusCode`] = String(statusCode);
28791
+ }
28792
+ const diagnosticDetails = Reflect.get(current, "diagnosticDetails");
28793
+ if (typeof diagnosticDetails === "object" && diagnosticDetails !== null) {
28794
+ for (const [key, value] of Object.entries(diagnosticDetails)) {
28795
+ if (typeof value === "string") {
28796
+ details[key] = sanitizeDiagnosticText(value);
28797
+ }
28798
+ }
28799
+ }
28800
+ current = current.cause;
28801
+ }
28802
+ }
28803
+ var RelayConnectionDiagnostics = class {
28804
+ #audit;
28805
+ #channelId;
28806
+ #lastClosedEpoch = 0;
28807
+ #lastFailedEpoch = 0;
28808
+ #relayOrigin;
28809
+ #route;
28810
+ constructor(audit, channelId, relayUrl) {
28811
+ this.#audit = audit;
28812
+ this.#channelId = channelId;
28813
+ this.#relayOrigin = relayUrl.origin;
28814
+ this.#route = describeEnvironmentProxyRoute(relayUrl);
28815
+ }
28816
+ catalogFailed = (failure) => {
28817
+ const details = { ...this.#route, relayOrigin: this.#relayOrigin };
28818
+ addFailureDetails(details, failure);
28819
+ this.#record("agent_catalog_publish_failed", "catalog", details, true);
28820
+ };
28821
+ connectionClosed = (connectionEpoch, code, reason, source) => {
28822
+ if (this.#lastClosedEpoch === connectionEpoch) {
28823
+ return;
28824
+ }
28825
+ this.#lastClosedEpoch = connectionEpoch;
28826
+ this.#record(
28827
+ "relay_connection_closed",
28828
+ connectionEpoch.toString(),
28829
+ {
28830
+ closeCode: code.toString(),
28831
+ closeReason: sanitizeDiagnosticText(reason),
28832
+ connectionEpoch: connectionEpoch.toString(),
28833
+ relayOrigin: this.#relayOrigin,
28834
+ source,
28835
+ ...this.#route
28836
+ },
28837
+ true
28838
+ );
28839
+ };
28840
+ connectionFailed = (connectionEpoch, failure, source) => {
28841
+ if (this.#lastFailedEpoch === connectionEpoch) {
28842
+ return;
28843
+ }
28844
+ this.#lastFailedEpoch = connectionEpoch;
28845
+ const details = {
28846
+ connectionEpoch: connectionEpoch.toString(),
28847
+ relayOrigin: this.#relayOrigin,
28848
+ source,
28849
+ ...this.#route
28850
+ };
28851
+ addFailureDetails(details, failure);
28852
+ this.#record("relay_connection_failed", connectionEpoch.toString(), details, true);
28853
+ };
28854
+ connectionOpened = (connectionEpoch) => {
28855
+ this.#record(
28856
+ "relay_connection_opened",
28857
+ connectionEpoch.toString(),
28858
+ {
28859
+ connectionEpoch: connectionEpoch.toString(),
28860
+ relayOrigin: this.#relayOrigin,
28861
+ ...this.#route
28862
+ },
28863
+ false
28864
+ );
28865
+ };
28866
+ heartbeatTimedOut = (connectionEpoch, heartbeatId, timeoutMilliseconds) => {
28867
+ this.#record(
28868
+ "relay_heartbeat_timed_out",
28869
+ connectionEpoch.toString(),
28870
+ {
28871
+ connectionEpoch: connectionEpoch.toString(),
28872
+ heartbeatId,
28873
+ relayOrigin: this.#relayOrigin,
28874
+ timeoutMilliseconds: timeoutMilliseconds.toString(),
28875
+ ...this.#route
28876
+ },
28877
+ true
28878
+ );
28879
+ };
28880
+ #record = (event, commandId, details, warning) => {
28881
+ this.#audit.record({
28882
+ commandId,
28883
+ details,
28884
+ event,
28885
+ sessionId: this.#channelId
28886
+ });
28887
+ const encoded = JSON.stringify({ channelId: this.#channelId, event, ...details });
28888
+ if (warning) {
28889
+ process.stderr.write(`Mobius relay diagnostic ${encoded}
28890
+ `);
28891
+ return;
28892
+ }
28893
+ process.stdout.write(`Mobius relay diagnostic ${encoded}
28894
+ `);
28895
+ };
28896
+ };
28897
+
28691
28898
  // src/relay-connection/liveness.ts
28692
28899
  import { randomUUID as randomUUID3 } from "node:crypto";
28693
28900
  var HEARTBEAT_ACK_TIMEOUT_MILLISECONDS = 1e4;
@@ -28764,9 +28971,10 @@ var RelayConnectionLiveness = class {
28764
28971
  if (!(this.#state instanceof AwaitingHeartbeatAckState)) {
28765
28972
  throw new Error("Heartbeat timeout occurred in an invalid state");
28766
28973
  }
28974
+ const heartbeatId = this.#state.heartbeatId;
28767
28975
  this.#state.timeout.cancel();
28768
28976
  this.#state = new FinishedRelayLivenessState();
28769
- this.#transport.unresponsive();
28977
+ this.#transport.unresponsive(heartbeatId, HEARTBEAT_ACK_TIMEOUT_MILLISECONDS);
28770
28978
  };
28771
28979
  #sendHeartbeat = () => {
28772
28980
  if (this.#state instanceof WaitingForHeartbeatState) {
@@ -28815,6 +29023,7 @@ var SessionEventOutbox = class {
28815
29023
  ordinal INTEGER PRIMARY KEY AUTOINCREMENT,
28816
29024
  event_id TEXT NOT NULL UNIQUE,
28817
29025
  session_id TEXT NOT NULL,
29026
+ source_key TEXT NOT NULL,
28818
29027
  envelope_json TEXT NOT NULL
28819
29028
  ) STRICT;
28820
29029
  CREATE INDEX IF NOT EXISTS relay_session_event_outbox_order
@@ -28842,6 +29051,27 @@ var SessionEventOutbox = class {
28842
29051
  )
28843
29052
  ) STRICT;
28844
29053
  `);
29054
+ const outboxColumns = this.#database.prepare("PRAGMA table_info(relay_session_event_outbox)").all();
29055
+ const hasSourceKey = outboxColumns.some(
29056
+ (column) => typeof column === "object" && column !== null && Reflect.get(column, "name") === "source_key"
29057
+ );
29058
+ if (!hasSourceKey) {
29059
+ this.#database.exec(`
29060
+ ALTER TABLE relay_session_event_outbox
29061
+ ADD COLUMN source_key TEXT NOT NULL DEFAULT '';
29062
+ `);
29063
+ }
29064
+ this.#database.exec(`
29065
+ UPDATE relay_session_event_outbox
29066
+ SET source_key = CASE json_extract(envelope_json, '$.source.kind')
29067
+ WHEN 'attempt' THEN
29068
+ 'attempt:' || session_id || ':' || json_extract(envelope_json, '$.source.attemptId')
29069
+ ELSE 'runtime:' || session_id
29070
+ END
29071
+ WHERE source_key = '';
29072
+ CREATE INDEX IF NOT EXISTS relay_session_event_outbox_source_order
29073
+ ON relay_session_event_outbox(source_key, ordinal);
29074
+ `);
28845
29075
  }
28846
29076
  claimExecution = (commandId, sessionId, commandJson) => {
28847
29077
  this.#database.exec("BEGIN IMMEDIATE");
@@ -28899,12 +29129,10 @@ var SessionEventOutbox = class {
28899
29129
  const state = typeof row === "object" && row !== null ? Reflect.get(row, "state") : void 0;
28900
29130
  return state === "succeeded" || state === "failed" || state === "cancelled";
28901
29131
  };
28902
- acknowledge = (acknowledgement) => {
28903
- this.#database.prepare(
28904
- `DELETE FROM relay_session_event_outbox
29132
+ acknowledge = (acknowledgement) => this.#database.prepare(
29133
+ `DELETE FROM relay_session_event_outbox
28905
29134
  WHERE event_id = ? AND session_id = ?`
28906
- ).run(acknowledgement.eventId, acknowledgement.sessionId);
28907
- };
29135
+ ).run(acknowledgement.eventId, acknowledgement.sessionId).changes === 1;
28908
29136
  quarantine = (rejection) => {
28909
29137
  this.#database.exec("BEGIN IMMEDIATE");
28910
29138
  try {
@@ -28971,9 +29199,9 @@ var SessionEventOutbox = class {
28971
29199
  ).run(sourceKey, nextSourceSeq + 1);
28972
29200
  this.#database.prepare(
28973
29201
  `INSERT INTO relay_session_event_outbox (
28974
- event_id, session_id, envelope_json
28975
- ) VALUES (?, ?, ?)`
28976
- ).run(envelope2.eventId, envelope2.sessionId, encoded);
29202
+ event_id, session_id, source_key, envelope_json
29203
+ ) VALUES (?, ?, ?, ?)`
29204
+ ).run(envelope2.eventId, envelope2.sessionId, sourceKey, encoded);
28977
29205
  this.#database.exec("COMMIT");
28978
29206
  return encoded;
28979
29207
  } catch (error61) {
@@ -28986,6 +29214,27 @@ var SessionEventOutbox = class {
28986
29214
  FROM relay_session_event_outbox
28987
29215
  ORDER BY ordinal`
28988
29216
  ).all().map(readPendingEnvelope);
29217
+ pendingBatch = (limit) => {
29218
+ if (!Number.isSafeInteger(limit) || limit < 1) {
29219
+ throw new Error("Session event replay batch limit must be a positive integer");
29220
+ }
29221
+ return this.#database.prepare(
29222
+ `WITH ranked_events AS (
29223
+ SELECT ordinal,
29224
+ ROW_NUMBER() OVER (
29225
+ PARTITION BY source_key
29226
+ ORDER BY ordinal
29227
+ ) AS source_position
29228
+ FROM relay_session_event_outbox
29229
+ )
29230
+ SELECT outbox.envelope_json
29231
+ FROM ranked_events AS ranked
29232
+ INNER JOIN relay_session_event_outbox AS outbox
29233
+ ON outbox.ordinal = ranked.ordinal
29234
+ ORDER BY ranked.source_position, outbox.ordinal
29235
+ LIMIT ?`
29236
+ ).all(limit).map(readPendingEnvelope);
29237
+ };
28989
29238
  };
28990
29239
  function isDaemonExecutionState(value) {
28991
29240
  return value === "cancelled" || value === "dispatching" || value === "failed" || value === "running" || value === "succeeded";
@@ -29002,7 +29251,7 @@ var systemDaemonTimer = new SystemDaemonTimer();
29002
29251
 
29003
29252
  // src/run-daemon.ts
29004
29253
  var RELAY_RECONNECT_DELAY_MILLISECONDS = 500;
29005
- var RELAY_CONNECTION_REFRESH_MILLISECONDS = 20 * 60 * 1e3;
29254
+ var AGENT_ACTIVITY_COALESCING_MILLISECONDS = 100;
29006
29255
  var ACTIVE_TURN_RECONCILIATION_TIMEOUT_MILLISECONDS = 2e4;
29007
29256
  var ACTIVE_TURN_RECONCILIATION_CLOSE_CODE = 4009;
29008
29257
  function botIpcSocketPath() {
@@ -29011,54 +29260,6 @@ function botIpcSocketPath() {
29011
29260
  }
29012
29261
  return join15("/tmp", `mobius-bot-${randomUUID4()}.sock`);
29013
29262
  }
29014
- var RelayConnectionRefreshState = class {
29015
- };
29016
- var ScheduledRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
29017
- };
29018
- var DueRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
29019
- };
29020
- var FinishedRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
29021
- };
29022
- var RelayConnectionRefresh = class {
29023
- #isSafe;
29024
- #refresh;
29025
- #scheduledTask;
29026
- #state = new ScheduledRelayConnectionRefreshState();
29027
- constructor(timer, isSafe, refresh) {
29028
- this.#isSafe = isSafe;
29029
- this.#refresh = refresh;
29030
- this.#scheduledTask = timer.schedule(this.#request, RELAY_CONNECTION_REFRESH_MILLISECONDS);
29031
- }
29032
- afterTurnFinished = () => {
29033
- if (this.#state instanceof DueRelayConnectionRefreshState) {
29034
- this.#request();
29035
- return;
29036
- }
29037
- if (this.#state instanceof ScheduledRelayConnectionRefreshState || this.#state instanceof FinishedRelayConnectionRefreshState) {
29038
- return;
29039
- }
29040
- throw new Error("Unknown relay connection refresh state");
29041
- };
29042
- close = () => {
29043
- this.#scheduledTask.cancel();
29044
- this.#state = new FinishedRelayConnectionRefreshState();
29045
- };
29046
- #request = () => {
29047
- if (this.#state instanceof FinishedRelayConnectionRefreshState) {
29048
- return;
29049
- }
29050
- if (!(this.#state instanceof ScheduledRelayConnectionRefreshState) && !(this.#state instanceof DueRelayConnectionRefreshState)) {
29051
- throw new Error("Unknown relay connection refresh state");
29052
- }
29053
- if (!this.#isSafe()) {
29054
- this.#state = new DueRelayConnectionRefreshState();
29055
- return;
29056
- }
29057
- this.#state = new FinishedRelayConnectionRefreshState();
29058
- this.#scheduledTask.cancel();
29059
- this.#refresh();
29060
- };
29061
- };
29062
29263
  var DaemonAcpObserver = class {
29063
29264
  #record;
29064
29265
  constructor(record2) {
@@ -29323,7 +29524,8 @@ var DaemonSessionEventConnectionState = class {
29323
29524
  };
29324
29525
  var DisconnectedDaemonSessionEventState = class extends DaemonSessionEventConnectionState {
29325
29526
  disconnect = () => this;
29326
- send = () => void 0;
29527
+ matches = () => false;
29528
+ send = () => false;
29327
29529
  };
29328
29530
  var ConnectedDaemonSessionEventState = class extends DaemonSessionEventConnectionState {
29329
29531
  #socket;
@@ -29332,38 +29534,155 @@ var ConnectedDaemonSessionEventState = class extends DaemonSessionEventConnectio
29332
29534
  this.#socket = socket;
29333
29535
  }
29334
29536
  disconnect = (socket) => socket === this.#socket ? new DisconnectedDaemonSessionEventState() : this;
29537
+ matches = (socket) => socket === this.#socket;
29335
29538
  send = (message) => {
29336
- if (this.#socket.readyState === WebSocket2.OPEN) {
29337
- try {
29338
- this.#socket.send(message);
29339
- } catch {
29340
- }
29539
+ if (this.#socket.readyState !== WebSocket2.OPEN) {
29540
+ return false;
29541
+ }
29542
+ try {
29543
+ this.#socket.send(message);
29544
+ return true;
29545
+ } catch {
29546
+ return false;
29341
29547
  }
29342
29548
  };
29343
29549
  };
29550
+ var SESSION_EVENT_REPLAY_WINDOW = 4;
29344
29551
  var DaemonSessionEventPublisher = class {
29345
29552
  #outbox;
29553
+ #inFlightEventIds = /* @__PURE__ */ new Set();
29346
29554
  #connection = new DisconnectedDaemonSessionEventState();
29347
29555
  constructor(outbox) {
29348
29556
  this.#outbox = outbox;
29349
29557
  }
29350
29558
  connect = (socket) => {
29351
- const connection = new ConnectedDaemonSessionEventState(socket);
29352
- this.#connection = connection;
29353
- for (const pending of this.#outbox.pending()) {
29354
- connection.send(pending);
29355
- }
29559
+ this.#connection = new ConnectedDaemonSessionEventState(socket);
29560
+ this.#inFlightEventIds.clear();
29561
+ this.#publishNextBatch();
29356
29562
  };
29357
29563
  disconnect = (socket) => {
29564
+ if (!this.#connection.matches(socket)) {
29565
+ return;
29566
+ }
29358
29567
  this.#connection = this.#connection.disconnect(socket);
29568
+ this.#inFlightEventIds.clear();
29359
29569
  };
29360
29570
  recordAttempt = (sessionId, payload, attemptId) => {
29361
- this.#connection.send(this.#outbox.enqueue(sessionId, payload, attemptId));
29571
+ this.#outbox.enqueue(sessionId, payload, attemptId);
29572
+ this.#publishNextBatch();
29362
29573
  };
29363
29574
  recordRuntime = (sessionId, payload) => {
29364
- this.#connection.send(this.#outbox.enqueue(sessionId, payload));
29575
+ this.#outbox.enqueue(sessionId, payload);
29576
+ this.#publishNextBatch();
29577
+ };
29578
+ acknowledge = (acknowledgement) => {
29579
+ if (!this.#outbox.acknowledge(acknowledgement)) {
29580
+ return;
29581
+ }
29582
+ this.#inFlightEventIds.delete(acknowledgement.eventId);
29583
+ this.#publishNextBatch();
29584
+ };
29585
+ quarantine = (rejection) => {
29586
+ const quarantined = this.#outbox.quarantine(rejection);
29587
+ if (!quarantined) {
29588
+ return false;
29589
+ }
29590
+ this.#inFlightEventIds.delete(rejection.eventId);
29591
+ this.#publishNextBatch();
29592
+ return true;
29593
+ };
29594
+ #publishNextBatch = () => {
29595
+ if (this.#inFlightEventIds.size > 0) {
29596
+ return;
29597
+ }
29598
+ const batch = this.#outbox.pendingBatch(SESSION_EVENT_REPLAY_WINDOW);
29599
+ for (const encoded of batch) {
29600
+ const envelope2 = decodeSessionEventEnvelope(encoded);
29601
+ if (!this.#connection.send(encoded)) {
29602
+ this.#inFlightEventIds.clear();
29603
+ return;
29604
+ }
29605
+ this.#inFlightEventIds.add(envelope2.eventId);
29606
+ }
29607
+ };
29608
+ };
29609
+ var CoalescingDaemonActivityPublisher = class {
29610
+ #pending = /* @__PURE__ */ new Map();
29611
+ #publish;
29612
+ #publishedActivityIds = /* @__PURE__ */ new Set();
29613
+ #scheduled = /* @__PURE__ */ new Map();
29614
+ #timer;
29615
+ constructor(timer, publish) {
29616
+ this.#publish = publish;
29617
+ this.#timer = timer;
29618
+ }
29619
+ flush = () => {
29620
+ const activities = [...this.#pending.values()].sort((left, right) => left.sequence - right.sequence);
29621
+ for (const task of this.#scheduled.values()) {
29622
+ task.cancel();
29623
+ }
29624
+ this.#pending.clear();
29625
+ this.#scheduled.clear();
29626
+ for (const activity of activities) {
29627
+ this.#publish(activity);
29628
+ }
29629
+ };
29630
+ record = (activity) => {
29631
+ if (activity.status !== "pending" && activity.status !== "running") {
29632
+ this.#discardPending(activity.activityId);
29633
+ this.#publishedActivityIds.add(activity.activityId);
29634
+ this.#publish(activity);
29635
+ return Promise.resolve();
29636
+ }
29637
+ if (!this.#publishedActivityIds.has(activity.activityId)) {
29638
+ this.#publishedActivityIds.add(activity.activityId);
29639
+ this.#publish(activity);
29640
+ return Promise.resolve();
29641
+ }
29642
+ this.#pending.set(activity.activityId, activity);
29643
+ if (!this.#scheduled.has(activity.activityId)) {
29644
+ this.#scheduled.set(
29645
+ activity.activityId,
29646
+ this.#timer.schedule(
29647
+ () => this.#flushActivity(activity.activityId),
29648
+ AGENT_ACTIVITY_COALESCING_MILLISECONDS
29649
+ )
29650
+ );
29651
+ }
29652
+ return Promise.resolve();
29653
+ };
29654
+ #discardPending = (activityId) => {
29655
+ this.#scheduled.get(activityId)?.cancel();
29656
+ this.#scheduled.delete(activityId);
29657
+ this.#pending.delete(activityId);
29658
+ };
29659
+ #flushActivity = (activityId) => {
29660
+ const activity = this.#pending.get(activityId);
29661
+ this.#discardPending(activityId);
29662
+ if (activity !== void 0) {
29663
+ this.#publish(activity);
29664
+ }
29365
29665
  };
29366
29666
  };
29667
+ function encodeDaemonActivity(command, activity) {
29668
+ return encodeAgentActivityEvent({
29669
+ activityId: activity.activityId,
29670
+ activityType: activity.activityType,
29671
+ commandId: command.commandId,
29672
+ content: activity.content,
29673
+ detail: activity.detail,
29674
+ input: activity.input,
29675
+ kind: AGENT_ACTIVITY_KIND,
29676
+ locations: activity.locations,
29677
+ name: activity.name,
29678
+ output: activity.output,
29679
+ sequence: activity.sequence,
29680
+ sessionId: command.sessionId,
29681
+ status: activity.status,
29682
+ title: activity.title,
29683
+ toolKind: activity.toolKind
29684
+ });
29685
+ }
29367
29686
  function reportFailure(publisher, command, code, message, attemptId) {
29368
29687
  publisher.recordAttempt(
29369
29688
  command.sessionId,
@@ -29540,7 +29859,10 @@ async function executeSessionConfigure(publisher, socket, command, config2, dire
29540
29859
  reportOperationFailure(socket, command.requestId, command.sessionId, error61);
29541
29860
  }
29542
29861
  }
29543
- async function executePrompt(outbox, publisher, command, config2, directories, audit, sessions, attemptId, signal) {
29862
+ async function executePrompt(outbox, publisher, command, config2, directories, audit, sessions, attemptId, timer, signal) {
29863
+ const activities = new CoalescingDaemonActivityPublisher(timer, (activity) => {
29864
+ publisher.recordAttempt(command.sessionId, encodeDaemonActivity(command, activity), attemptId);
29865
+ });
29544
29866
  try {
29545
29867
  const resolved = await resolveSessionDirectories(
29546
29868
  directories,
@@ -29597,30 +29919,7 @@ async function executePrompt(outbox, publisher, command, config2, directories, a
29597
29919
  cwd: resolved.cwd,
29598
29920
  disposition: command.disposition,
29599
29921
  images: attachments.images,
29600
- onActivity: (activity) => {
29601
- publisher.recordAttempt(
29602
- command.sessionId,
29603
- encodeAgentActivityEvent({
29604
- activityId: activity.activityId,
29605
- activityType: activity.activityType,
29606
- commandId: command.commandId,
29607
- content: activity.content,
29608
- detail: activity.detail,
29609
- input: activity.input,
29610
- kind: AGENT_ACTIVITY_KIND,
29611
- locations: activity.locations,
29612
- name: activity.name,
29613
- output: activity.output,
29614
- sequence: activity.sequence,
29615
- sessionId: command.sessionId,
29616
- status: activity.status,
29617
- title: activity.title,
29618
- toolKind: activity.toolKind
29619
- }),
29620
- attemptId
29621
- );
29622
- return Promise.resolve();
29623
- },
29922
+ onActivity: activities.record,
29624
29923
  onText: (text, messageId) => {
29625
29924
  publisher.recordAttempt(
29626
29925
  command.sessionId,
@@ -29643,6 +29942,7 @@ async function executePrompt(outbox, publisher, command, config2, directories, a
29643
29942
  });
29644
29943
  stopReason = result.stopReason;
29645
29944
  }
29945
+ activities.flush();
29646
29946
  publisher.recordAttempt(
29647
29947
  command.sessionId,
29648
29948
  encodeSessionCompletedEvent({
@@ -29660,6 +29960,7 @@ async function executePrompt(outbox, publisher, command, config2, directories, a
29660
29960
  sessionId: command.sessionId
29661
29961
  });
29662
29962
  } catch (error61) {
29963
+ activities.flush();
29663
29964
  audit.record({
29664
29965
  commandId: command.commandId,
29665
29966
  event: "session_failed",
@@ -29698,7 +29999,10 @@ function botExecutionAgent(agent, contextHandle, socketPath) {
29698
29999
  agent.capabilities
29699
30000
  );
29700
30001
  }
29701
- async function executeBotPrompt(outbox, publisher, command, config2, directories, audit, sessions, grants, contexts, applicator, ipc, attemptId, signal) {
30002
+ async function executeBotPrompt(outbox, publisher, command, config2, directories, audit, sessions, grants, contexts, applicator, ipc, attemptId, timer, signal) {
30003
+ const activities = new CoalescingDaemonActivityPublisher(timer, (activity) => {
30004
+ publisher.recordAttempt(command.sessionId, encodeDaemonActivity(command, activity), attemptId);
30005
+ });
29702
30006
  try {
29703
30007
  const deadline = Date.parse(command.executionDeadlineAt);
29704
30008
  const remaining = deadline - Date.now();
@@ -29782,30 +30086,7 @@ async function executeBotPrompt(outbox, publisher, command, config2, directories
29782
30086
  commandId: command.commandId,
29783
30087
  cwd: resolved.cwd,
29784
30088
  images: attachments.images,
29785
- onActivity: (activity) => {
29786
- publisher.recordAttempt(
29787
- command.sessionId,
29788
- encodeAgentActivityEvent({
29789
- activityId: activity.activityId,
29790
- activityType: activity.activityType,
29791
- commandId: command.commandId,
29792
- content: activity.content,
29793
- detail: activity.detail,
29794
- input: activity.input,
29795
- kind: AGENT_ACTIVITY_KIND,
29796
- locations: activity.locations,
29797
- name: activity.name,
29798
- output: activity.output,
29799
- sequence: activity.sequence,
29800
- sessionId: command.sessionId,
29801
- status: activity.status,
29802
- title: activity.title,
29803
- toolKind: activity.toolKind
29804
- }),
29805
- attemptId
29806
- );
29807
- return Promise.resolve();
29808
- },
30089
+ onActivity: activities.record,
29809
30090
  onText: (text, messageId) => {
29810
30091
  publisher.recordAttempt(
29811
30092
  command.sessionId,
@@ -29825,6 +30106,7 @@ async function executeBotPrompt(outbox, publisher, command, config2, directories
29825
30106
  sessionId: command.sessionId,
29826
30107
  signal: executionSignal
29827
30108
  });
30109
+ activities.flush();
29828
30110
  publisher.recordAttempt(
29829
30111
  command.sessionId,
29830
30112
  encodeSessionCompletedEvent({
@@ -29849,6 +30131,7 @@ async function executeBotPrompt(outbox, publisher, command, config2, directories
29849
30131
  lease.release();
29850
30132
  }
29851
30133
  } catch (error61) {
30134
+ activities.flush();
29852
30135
  const failure = Date.parse(command.executionDeadlineAt) <= Date.now() ? new LocalBotAuthorizationError("turn_expired", "The Bot execution deadline has expired", {
29853
30136
  cause: error61
29854
30137
  }) : error61;
@@ -29888,15 +30171,15 @@ function waitForRelayRetry(signal, timer) {
29888
30171
  signal.addEventListener("abort", finish, { once: true });
29889
30172
  });
29890
30173
  }
29891
- async function publishAgentCatalogUntilAvailable(config2, signal, timer) {
30174
+ async function publishAgentCatalogUntilAvailable(config2, diagnostics, signal, timer) {
29892
30175
  try {
29893
30176
  await publishAgentCatalog(config2, signal);
29894
30177
  return;
29895
- } catch {
30178
+ } catch (error61) {
29896
30179
  if (signal.aborted) {
29897
30180
  return;
29898
30181
  }
29899
- console.warn("Mobius daemon is waiting to publish its Agent catalog");
30182
+ diagnostics.catalogFailed(error61);
29900
30183
  }
29901
30184
  while (!signal.aborted) {
29902
30185
  await waitForRelayRetry(signal, timer);
@@ -29910,11 +30193,12 @@ async function publishAgentCatalogUntilAvailable(config2, signal, timer) {
29910
30193
  }
29911
30194
  }
29912
30195
  }
29913
- async function runRelayConnection(config2, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, onConnected, signal, timer) {
30196
+ async function runRelayConnection(config2, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, connectionEpoch, diagnostics, onConnected, signal, timer) {
29914
30197
  const relayUrl = new URL(config2.relayUrl);
29915
30198
  relayUrl.searchParams.set("liveness", "1");
29916
30199
  const socket = await connectRelay(relayUrl, signal);
29917
30200
  const reconnectingTurns = [...activeTurns.values()];
30201
+ diagnostics.connectionOpened(connectionEpoch);
29918
30202
  onConnected();
29919
30203
  const connectionController = new AbortController();
29920
30204
  const nativeRequests = /* @__PURE__ */ new Map();
@@ -29923,7 +30207,6 @@ async function runRelayConnection(config2, directories, audit, sessions, install
29923
30207
  await new Promise((resolve6, reject) => {
29924
30208
  let finished = false;
29925
30209
  const cleanup = () => {
29926
- connectionRefresh.close();
29927
30210
  connectionLiveness.close();
29928
30211
  signal.removeEventListener("abort", stop);
29929
30212
  socket.removeEventListener("close", handleClose);
@@ -29941,61 +30224,70 @@ async function runRelayConnection(config2, directories, audit, sessions, install
29941
30224
  cleanup();
29942
30225
  resolve6();
29943
30226
  };
29944
- const rejectConnection = () => {
30227
+ const rejectConnection = (failure) => {
29945
30228
  if (finished) {
29946
30229
  return;
29947
30230
  }
29948
30231
  finished = true;
29949
30232
  cleanup();
29950
- reject(new Error("Relay WebSocket failed"));
30233
+ reject(failure);
29951
30234
  };
29952
30235
  const stop = () => {
30236
+ diagnostics.connectionClosed(connectionEpoch, 1e3, "Daemon stopped", "daemon_stop");
29953
30237
  closeRelaySocket(socket, 1e3, "Daemon stopped");
29954
30238
  resolveConnection();
29955
30239
  };
29956
- const refreshConnection = () => {
29957
- closeRelaySocket(socket, 1e3, "Refreshing idle relay connection");
29958
- resolveConnection();
30240
+ const failAndClose = (failure, code, reason, source) => {
30241
+ diagnostics.connectionFailed(connectionEpoch, failure, source);
30242
+ diagnostics.connectionClosed(connectionEpoch, code, reason, source);
30243
+ closeRelaySocket(socket, code, reason);
30244
+ rejectConnection(failure);
29959
30245
  };
29960
30246
  const connectionLiveness = new RelayConnectionLiveness(timer, {
29961
30247
  send: (message) => send(socket, message),
29962
- unresponsive: () => {
30248
+ unresponsive: (heartbeatId, timeoutMilliseconds) => {
30249
+ diagnostics.heartbeatTimedOut(connectionEpoch, heartbeatId, timeoutMilliseconds);
30250
+ diagnostics.connectionClosed(
30251
+ connectionEpoch,
30252
+ 4001,
30253
+ "Relay liveness check timed out",
30254
+ "heartbeat_timeout"
30255
+ );
29963
30256
  closeRelaySocket(socket, 4001, "Relay liveness check timed out");
29964
30257
  resolveConnection();
29965
30258
  }
29966
30259
  });
29967
- const connectionRefresh = new RelayConnectionRefresh(
29968
- timer,
29969
- () => activeTurns.size === 0,
29970
- refreshConnection
29971
- );
29972
30260
  signal.addEventListener("abort", stop, { once: true });
29973
- const handleClose = () => {
30261
+ const handleClose = (event) => {
30262
+ diagnostics.connectionClosed(connectionEpoch, event.code, event.reason, "relay");
29974
30263
  resolveConnection();
29975
30264
  };
29976
- const handleError = () => {
30265
+ const handleError = (event) => {
29977
30266
  if (signal.aborted) {
29978
30267
  resolveConnection();
29979
30268
  return;
29980
30269
  }
29981
- rejectConnection();
30270
+ const cause = Reflect.get(event, "error");
30271
+ const failure = new Error("Relay WebSocket emitted an error", { cause: cause ?? event });
30272
+ diagnostics.connectionFailed(connectionEpoch, failure, "socket_error");
30273
+ diagnostics.connectionClosed(connectionEpoch, 1006, "WebSocket error", "socket_error");
30274
+ rejectConnection(failure);
29982
30275
  };
29983
30276
  const handleMessage = (event) => {
29984
30277
  const encoded = event.data;
29985
30278
  if (typeof encoded !== "string") {
29986
- closeRelaySocket(socket, 4003, "Text message required");
29987
- rejectConnection();
30279
+ failAndClose(new Error("Relay sent a non-text message"), 4003, "Text message required", "protocol");
29988
30280
  return;
29989
30281
  }
29990
30282
  try {
29991
30283
  const kind = decodeMessageKind(encoded);
29992
30284
  if (kind === SESSION_EVENT_ACK_KIND) {
29993
- outbox.acknowledge(decodeSessionEventAcknowledgement(encoded));
30285
+ publisher.acknowledge(decodeSessionEventAcknowledgement(encoded));
29994
30286
  return;
29995
30287
  }
29996
30288
  if (kind === SESSION_EVENT_REJECTED_KIND) {
29997
30289
  const rejection = decodeSessionEventRejection(encoded);
29998
- if (outbox.quarantine(rejection)) {
30290
+ if (publisher.quarantine(rejection)) {
29999
30291
  audit.record({
30000
30292
  commandId: rejection.eventId,
30001
30293
  details: { code: rejection.code },
@@ -30374,6 +30666,7 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30374
30666
  applicator,
30375
30667
  ipc,
30376
30668
  receipt2.attemptId,
30669
+ timer,
30377
30670
  turn2.signal
30378
30671
  ).catch((error61) => {
30379
30672
  console.error("Mobius daemon Bot turn failed", error61);
@@ -30383,7 +30676,6 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30383
30676
  activeAttemptIds.delete(command2.sessionId);
30384
30677
  }
30385
30678
  turn2.finish();
30386
- connectionRefresh.afterTurnFinished();
30387
30679
  });
30388
30680
  return;
30389
30681
  }
@@ -30412,6 +30704,7 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30412
30704
  audit,
30413
30705
  sessions,
30414
30706
  receipt.attemptId,
30707
+ timer,
30415
30708
  turn.signal
30416
30709
  ).catch((error61) => {
30417
30710
  console.error("Mobius daemon turn failed", error61);
@@ -30421,12 +30714,15 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30421
30714
  activeAttemptIds.delete(command.sessionId);
30422
30715
  }
30423
30716
  turn.finish();
30424
- connectionRefresh.afterTurnFinished();
30425
30717
  });
30426
30718
  } catch (error61) {
30427
30719
  console.error("Mobius daemon rejected a message", error61);
30428
- closeRelaySocket(socket, 4008, "Invalid protocol message");
30429
- rejectConnection();
30720
+ failAndClose(
30721
+ new Error("Relay sent an invalid protocol message", { cause: error61 }),
30722
+ 4008,
30723
+ "Invalid protocol message",
30724
+ "protocol"
30725
+ );
30430
30726
  }
30431
30727
  };
30432
30728
  socket.addEventListener("close", handleClose);
@@ -30438,21 +30734,18 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30438
30734
  return;
30439
30735
  }
30440
30736
  publisher.connect(socket);
30441
- void Promise.all(reconnectingTurns.map((turn) => turn.completion)).then(
30442
- connectionRefresh.afterTurnFinished
30443
- );
30444
30737
  },
30445
30738
  (error61) => {
30446
30739
  if (finished) {
30447
30740
  return;
30448
30741
  }
30449
30742
  console.error("Mobius daemon could not reconcile active Agent turns", error61);
30450
- closeRelaySocket(
30451
- socket,
30743
+ failAndClose(
30744
+ new Error("Active Agent turn reconciliation failed", { cause: error61 }),
30452
30745
  ACTIVE_TURN_RECONCILIATION_CLOSE_CODE,
30453
- "Active Agent turn reconciliation failed"
30746
+ "Active Agent turn reconciliation failed",
30747
+ "reconciliation"
30454
30748
  );
30455
- rejectConnection();
30456
30749
  }
30457
30750
  );
30458
30751
  if (signal.aborted) {
@@ -30496,6 +30789,7 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
30496
30789
  const sessions = new AcpSessionManager(new DaemonAcpObserver(recordSessionEvent), sessionStore);
30497
30790
  const directories = new WorkspaceDirectoryService(config2.bindingId, config2.defaultWorkspace);
30498
30791
  const installationObserver = new DaemonInstallationObserver();
30792
+ const diagnostics = new RelayConnectionDiagnostics(audit, config2.channelId, config2.relayUrl);
30499
30793
  const nativeSessions = new NativeSessionService(config2, directories, sessions, audit);
30500
30794
  const installations = new AgentInstallationManager(
30501
30795
  specifications,
@@ -30503,7 +30797,7 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
30503
30797
  installationObserver,
30504
30798
  installationAudit
30505
30799
  );
30506
- let retryAnnounced = false;
30800
+ let connectionEpoch = 0;
30507
30801
  let connectionAnnounced = false;
30508
30802
  const announceConnection = () => {
30509
30803
  if (connectionAnnounced) {
@@ -30519,10 +30813,11 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
30519
30813
  `
30520
30814
  );
30521
30815
  };
30522
- const catalogPublication = publishAgentCatalogUntilAvailable(config2, signal, timer);
30816
+ const catalogPublication = publishAgentCatalogUntilAvailable(config2, diagnostics, signal, timer);
30523
30817
  try {
30524
30818
  await ipc.listen(signal);
30525
30819
  while (!signal.aborted) {
30820
+ connectionEpoch += 1;
30526
30821
  try {
30527
30822
  await runRelayConnection(
30528
30823
  config2,
@@ -30540,19 +30835,17 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
30540
30835
  ipc,
30541
30836
  activeTurns,
30542
30837
  activeAttemptIds,
30838
+ connectionEpoch,
30839
+ diagnostics,
30543
30840
  announceConnection,
30544
30841
  signal,
30545
30842
  timer
30546
30843
  );
30547
- retryAnnounced = false;
30548
- } catch {
30844
+ } catch (error61) {
30549
30845
  if (signal.aborted) {
30550
30846
  return;
30551
30847
  }
30552
- if (!retryAnnounced) {
30553
- console.warn("Mobius daemon is waiting for the relay");
30554
- retryAnnounced = true;
30555
- }
30848
+ diagnostics.connectionFailed(connectionEpoch, error61, "connection");
30556
30849
  }
30557
30850
  await waitForRelayRetry(signal, timer);
30558
30851
  }