@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/index.js CHANGED
@@ -20497,6 +20497,7 @@ var encode4 = (message, schema) => {
20497
20497
  }
20498
20498
  return JSON.stringify(decoded.data);
20499
20499
  };
20500
+ var decodeSessionEventEnvelope = (encoded) => decode5(encoded, eventEnvelopeSchema);
20500
20501
  var encodeSessionEventEnvelope = (message) => encode4(message, eventEnvelopeSchema);
20501
20502
  var decodeSessionEventAcknowledgement = (encoded) => decode5(encoded, eventAcknowledgementSchema);
20502
20503
  var decodeSessionEventRejection = (encoded) => decode5(encoded, eventRejectionSchema);
@@ -26286,7 +26287,53 @@ import { fetch as fetch2 } from "undici";
26286
26287
 
26287
26288
  // src/network/environment-proxy.ts
26288
26289
  import { EnvHttpProxyAgent } from "undici";
26290
+ var configuredHttpProxy = process.env["http_proxy"] ?? process.env["HTTP_PROXY"] ?? "";
26291
+ var configuredHttpsProxy = process.env["https_proxy"] ?? process.env["HTTPS_PROXY"] ?? configuredHttpProxy;
26289
26292
  var environmentProxyDispatcher = new EnvHttpProxyAgent();
26293
+ function bypassesProxy(target) {
26294
+ const noProxy = process.env["no_proxy"] ?? process.env["NO_PROXY"] ?? "";
26295
+ if (noProxy === "*") {
26296
+ return true;
26297
+ }
26298
+ const hostname4 = target.hostname.replace(/^\[|\]$/gu, "").toLowerCase();
26299
+ const defaultPort = target.protocol === "wss:" || target.protocol === "https:" ? 443 : 80;
26300
+ const port = Number.parseInt(target.port, 10) || defaultPort;
26301
+ for (const rawEntry of noProxy.split(/[,\s]/u)) {
26302
+ if (rawEntry.length === 0) {
26303
+ continue;
26304
+ }
26305
+ const parsed = rawEntry.match(/^(.+):(\d+)$/u);
26306
+ const entryHostname = (parsed?.[1] ?? rawEntry).replace(/^\*?\./u, "").toLowerCase();
26307
+ const entryPort = parsed?.[2] === void 0 ? 0 : Number.parseInt(parsed[2], 10);
26308
+ if (entryPort !== 0 && entryPort !== port) {
26309
+ continue;
26310
+ }
26311
+ if (hostname4 === entryHostname || hostname4.endsWith(`.${entryHostname}`)) {
26312
+ return true;
26313
+ }
26314
+ }
26315
+ return false;
26316
+ }
26317
+ function sanitizedProxyOrigin(proxyUrl) {
26318
+ try {
26319
+ return new URL(proxyUrl).origin;
26320
+ } catch {
26321
+ return "configured-invalid-url";
26322
+ }
26323
+ }
26324
+ function describeEnvironmentProxyRoute(target) {
26325
+ if (bypassesProxy(target)) {
26326
+ return { proxyRoute: "direct" };
26327
+ }
26328
+ const proxyUrl = target.protocol === "wss:" || target.protocol === "https:" ? configuredHttpsProxy : configuredHttpProxy;
26329
+ if (proxyUrl.length === 0) {
26330
+ return { proxyRoute: "direct" };
26331
+ }
26332
+ return {
26333
+ proxyOrigin: sanitizedProxyOrigin(proxyUrl),
26334
+ proxyRoute: "proxy"
26335
+ };
26336
+ }
26290
26337
 
26291
26338
  // src/agent-installation/managed-agent-archive.ts
26292
26339
  var ARCHIVE_DOWNLOAD_LIMIT_BYTES = 128 * 1024 * 1024;
@@ -29181,9 +29228,11 @@ import { WebSocket } from "undici";
29181
29228
  var CONNECTION_TIMEOUT_MILLISECONDS = 15e3;
29182
29229
  var RelayConnectionError = class extends Error {
29183
29230
  code;
29184
- constructor(code, message, options = {}) {
29231
+ diagnosticDetails;
29232
+ constructor(code, message, diagnosticDetails = {}, options = {}) {
29185
29233
  super(message, options);
29186
29234
  this.code = code;
29235
+ this.diagnosticDetails = diagnosticDetails;
29187
29236
  this.name = "RelayConnectionError";
29188
29237
  }
29189
29238
  };
@@ -29221,16 +29270,34 @@ function connectRelay(url2, signal) {
29221
29270
  };
29222
29271
  const onAbort = () => {
29223
29272
  fail(
29224
- new RelayConnectionError("relay_connection_cancelled", "Relay connection was cancelled", {
29225
- cause: signal.reason
29226
- })
29273
+ new RelayConnectionError(
29274
+ "relay_connection_cancelled",
29275
+ "Relay connection was cancelled",
29276
+ {
29277
+ cancellation: "requested"
29278
+ },
29279
+ { cause: signal.reason }
29280
+ )
29227
29281
  );
29228
29282
  };
29229
- const onClose = () => {
29230
- fail(new RelayConnectionError("relay_connection_closed", "Relay connection closed before it was ready"));
29283
+ const onClose = (event) => {
29284
+ fail(
29285
+ new RelayConnectionError("relay_connection_closed", "Relay connection closed before it was ready", {
29286
+ closeCode: event.code.toString(),
29287
+ closeReason: event.reason
29288
+ })
29289
+ );
29231
29290
  };
29232
- const onError = () => {
29233
- fail(new RelayConnectionError("relay_connection_failed", "Relay connection failed"));
29291
+ const onError = (event) => {
29292
+ const cause = Reflect.get(event, "error");
29293
+ fail(
29294
+ new RelayConnectionError(
29295
+ "relay_connection_failed",
29296
+ "Relay connection failed",
29297
+ {},
29298
+ { cause: cause ?? event }
29299
+ )
29300
+ );
29234
29301
  };
29235
29302
  const onOpen = () => {
29236
29303
  if (finished) {
@@ -29241,7 +29308,11 @@ function connectRelay(url2, signal) {
29241
29308
  resolve6(socket);
29242
29309
  };
29243
29310
  function onTimeout() {
29244
- fail(new RelayConnectionError("relay_connection_timeout", "Relay connection timed out"));
29311
+ fail(
29312
+ new RelayConnectionError("relay_connection_timeout", "Relay connection timed out", {
29313
+ timeoutMilliseconds: CONNECTION_TIMEOUT_MILLISECONDS.toString()
29314
+ })
29315
+ );
29245
29316
  }
29246
29317
  signal.addEventListener("abort", onAbort, { once: true });
29247
29318
  socket.addEventListener("close", onClose, { once: true });
@@ -29253,6 +29324,142 @@ function connectRelay(url2, signal) {
29253
29324
  });
29254
29325
  }
29255
29326
 
29327
+ // src/relay-connection/diagnostics.ts
29328
+ var MAX_DIAGNOSTIC_TEXT_LENGTH = 256;
29329
+ function sanitizeDiagnosticText(value) {
29330
+ const printable = Array.from(value, (character) => {
29331
+ const code = character.charCodeAt(0);
29332
+ return code < 32 || code === 127 ? " " : character;
29333
+ }).join("");
29334
+ const withoutSecrets = printable.replace(/([?&](?:access_token|authorization|credential|key|secret|token)=)[^&\s]*/giu, "$1[redacted]").replace(/(?:https?|wss?):\/\/[^\s)]+/giu, (encodedUrl) => {
29335
+ try {
29336
+ return new URL(encodedUrl).origin;
29337
+ } catch {
29338
+ return "[redacted-url]";
29339
+ }
29340
+ });
29341
+ return withoutSecrets.slice(0, MAX_DIAGNOSTIC_TEXT_LENGTH);
29342
+ }
29343
+ function addFailureDetails(details, failure) {
29344
+ let current = failure;
29345
+ for (let depth = 0; depth < 3 && current instanceof Error; depth += 1) {
29346
+ const prefix = depth === 0 ? "failure" : `cause${depth.toString()}`;
29347
+ details[`${prefix}Name`] = current.name;
29348
+ details[`${prefix}Message`] = sanitizeDiagnosticText(current.message);
29349
+ const code = Reflect.get(current, "code");
29350
+ if (typeof code === "string" || typeof code === "number") {
29351
+ details[`${prefix}Code`] = String(code);
29352
+ }
29353
+ const statusCode = Reflect.get(current, "statusCode");
29354
+ if (typeof statusCode === "string" || typeof statusCode === "number") {
29355
+ details[`${prefix}StatusCode`] = String(statusCode);
29356
+ }
29357
+ const diagnosticDetails = Reflect.get(current, "diagnosticDetails");
29358
+ if (typeof diagnosticDetails === "object" && diagnosticDetails !== null) {
29359
+ for (const [key, value] of Object.entries(diagnosticDetails)) {
29360
+ if (typeof value === "string") {
29361
+ details[key] = sanitizeDiagnosticText(value);
29362
+ }
29363
+ }
29364
+ }
29365
+ current = current.cause;
29366
+ }
29367
+ }
29368
+ var RelayConnectionDiagnostics = class {
29369
+ #audit;
29370
+ #channelId;
29371
+ #lastClosedEpoch = 0;
29372
+ #lastFailedEpoch = 0;
29373
+ #relayOrigin;
29374
+ #route;
29375
+ constructor(audit, channelId, relayUrl) {
29376
+ this.#audit = audit;
29377
+ this.#channelId = channelId;
29378
+ this.#relayOrigin = relayUrl.origin;
29379
+ this.#route = describeEnvironmentProxyRoute(relayUrl);
29380
+ }
29381
+ catalogFailed = (failure) => {
29382
+ const details = { ...this.#route, relayOrigin: this.#relayOrigin };
29383
+ addFailureDetails(details, failure);
29384
+ this.#record("agent_catalog_publish_failed", "catalog", details, true);
29385
+ };
29386
+ connectionClosed = (connectionEpoch, code, reason, source) => {
29387
+ if (this.#lastClosedEpoch === connectionEpoch) {
29388
+ return;
29389
+ }
29390
+ this.#lastClosedEpoch = connectionEpoch;
29391
+ this.#record(
29392
+ "relay_connection_closed",
29393
+ connectionEpoch.toString(),
29394
+ {
29395
+ closeCode: code.toString(),
29396
+ closeReason: sanitizeDiagnosticText(reason),
29397
+ connectionEpoch: connectionEpoch.toString(),
29398
+ relayOrigin: this.#relayOrigin,
29399
+ source,
29400
+ ...this.#route
29401
+ },
29402
+ true
29403
+ );
29404
+ };
29405
+ connectionFailed = (connectionEpoch, failure, source) => {
29406
+ if (this.#lastFailedEpoch === connectionEpoch) {
29407
+ return;
29408
+ }
29409
+ this.#lastFailedEpoch = connectionEpoch;
29410
+ const details = {
29411
+ connectionEpoch: connectionEpoch.toString(),
29412
+ relayOrigin: this.#relayOrigin,
29413
+ source,
29414
+ ...this.#route
29415
+ };
29416
+ addFailureDetails(details, failure);
29417
+ this.#record("relay_connection_failed", connectionEpoch.toString(), details, true);
29418
+ };
29419
+ connectionOpened = (connectionEpoch) => {
29420
+ this.#record(
29421
+ "relay_connection_opened",
29422
+ connectionEpoch.toString(),
29423
+ {
29424
+ connectionEpoch: connectionEpoch.toString(),
29425
+ relayOrigin: this.#relayOrigin,
29426
+ ...this.#route
29427
+ },
29428
+ false
29429
+ );
29430
+ };
29431
+ heartbeatTimedOut = (connectionEpoch, heartbeatId, timeoutMilliseconds) => {
29432
+ this.#record(
29433
+ "relay_heartbeat_timed_out",
29434
+ connectionEpoch.toString(),
29435
+ {
29436
+ connectionEpoch: connectionEpoch.toString(),
29437
+ heartbeatId,
29438
+ relayOrigin: this.#relayOrigin,
29439
+ timeoutMilliseconds: timeoutMilliseconds.toString(),
29440
+ ...this.#route
29441
+ },
29442
+ true
29443
+ );
29444
+ };
29445
+ #record = (event, commandId, details, warning) => {
29446
+ this.#audit.record({
29447
+ commandId,
29448
+ details,
29449
+ event,
29450
+ sessionId: this.#channelId
29451
+ });
29452
+ const encoded = JSON.stringify({ channelId: this.#channelId, event, ...details });
29453
+ if (warning) {
29454
+ process.stderr.write(`Mobius relay diagnostic ${encoded}
29455
+ `);
29456
+ return;
29457
+ }
29458
+ process.stdout.write(`Mobius relay diagnostic ${encoded}
29459
+ `);
29460
+ };
29461
+ };
29462
+
29256
29463
  // src/relay-connection/liveness.ts
29257
29464
  import { randomUUID as randomUUID5 } from "node:crypto";
29258
29465
  var HEARTBEAT_ACK_TIMEOUT_MILLISECONDS = 1e4;
@@ -29329,9 +29536,10 @@ var RelayConnectionLiveness = class {
29329
29536
  if (!(this.#state instanceof AwaitingHeartbeatAckState)) {
29330
29537
  throw new Error("Heartbeat timeout occurred in an invalid state");
29331
29538
  }
29539
+ const heartbeatId = this.#state.heartbeatId;
29332
29540
  this.#state.timeout.cancel();
29333
29541
  this.#state = new FinishedRelayLivenessState();
29334
- this.#transport.unresponsive();
29542
+ this.#transport.unresponsive(heartbeatId, HEARTBEAT_ACK_TIMEOUT_MILLISECONDS);
29335
29543
  };
29336
29544
  #sendHeartbeat = () => {
29337
29545
  if (this.#state instanceof WaitingForHeartbeatState) {
@@ -29380,6 +29588,7 @@ var SessionEventOutbox = class {
29380
29588
  ordinal INTEGER PRIMARY KEY AUTOINCREMENT,
29381
29589
  event_id TEXT NOT NULL UNIQUE,
29382
29590
  session_id TEXT NOT NULL,
29591
+ source_key TEXT NOT NULL,
29383
29592
  envelope_json TEXT NOT NULL
29384
29593
  ) STRICT;
29385
29594
  CREATE INDEX IF NOT EXISTS relay_session_event_outbox_order
@@ -29407,6 +29616,27 @@ var SessionEventOutbox = class {
29407
29616
  )
29408
29617
  ) STRICT;
29409
29618
  `);
29619
+ const outboxColumns = this.#database.prepare("PRAGMA table_info(relay_session_event_outbox)").all();
29620
+ const hasSourceKey = outboxColumns.some(
29621
+ (column) => typeof column === "object" && column !== null && Reflect.get(column, "name") === "source_key"
29622
+ );
29623
+ if (!hasSourceKey) {
29624
+ this.#database.exec(`
29625
+ ALTER TABLE relay_session_event_outbox
29626
+ ADD COLUMN source_key TEXT NOT NULL DEFAULT '';
29627
+ `);
29628
+ }
29629
+ this.#database.exec(`
29630
+ UPDATE relay_session_event_outbox
29631
+ SET source_key = CASE json_extract(envelope_json, '$.source.kind')
29632
+ WHEN 'attempt' THEN
29633
+ 'attempt:' || session_id || ':' || json_extract(envelope_json, '$.source.attemptId')
29634
+ ELSE 'runtime:' || session_id
29635
+ END
29636
+ WHERE source_key = '';
29637
+ CREATE INDEX IF NOT EXISTS relay_session_event_outbox_source_order
29638
+ ON relay_session_event_outbox(source_key, ordinal);
29639
+ `);
29410
29640
  }
29411
29641
  claimExecution = (commandId, sessionId, commandJson) => {
29412
29642
  this.#database.exec("BEGIN IMMEDIATE");
@@ -29464,12 +29694,10 @@ var SessionEventOutbox = class {
29464
29694
  const state = typeof row === "object" && row !== null ? Reflect.get(row, "state") : void 0;
29465
29695
  return state === "succeeded" || state === "failed" || state === "cancelled";
29466
29696
  };
29467
- acknowledge = (acknowledgement) => {
29468
- this.#database.prepare(
29469
- `DELETE FROM relay_session_event_outbox
29697
+ acknowledge = (acknowledgement) => this.#database.prepare(
29698
+ `DELETE FROM relay_session_event_outbox
29470
29699
  WHERE event_id = ? AND session_id = ?`
29471
- ).run(acknowledgement.eventId, acknowledgement.sessionId);
29472
- };
29700
+ ).run(acknowledgement.eventId, acknowledgement.sessionId).changes === 1;
29473
29701
  quarantine = (rejection) => {
29474
29702
  this.#database.exec("BEGIN IMMEDIATE");
29475
29703
  try {
@@ -29536,9 +29764,9 @@ var SessionEventOutbox = class {
29536
29764
  ).run(sourceKey, nextSourceSeq + 1);
29537
29765
  this.#database.prepare(
29538
29766
  `INSERT INTO relay_session_event_outbox (
29539
- event_id, session_id, envelope_json
29540
- ) VALUES (?, ?, ?)`
29541
- ).run(envelope2.eventId, envelope2.sessionId, encoded);
29767
+ event_id, session_id, source_key, envelope_json
29768
+ ) VALUES (?, ?, ?, ?)`
29769
+ ).run(envelope2.eventId, envelope2.sessionId, sourceKey, encoded);
29542
29770
  this.#database.exec("COMMIT");
29543
29771
  return encoded;
29544
29772
  } catch (error61) {
@@ -29551,6 +29779,27 @@ var SessionEventOutbox = class {
29551
29779
  FROM relay_session_event_outbox
29552
29780
  ORDER BY ordinal`
29553
29781
  ).all().map(readPendingEnvelope);
29782
+ pendingBatch = (limit) => {
29783
+ if (!Number.isSafeInteger(limit) || limit < 1) {
29784
+ throw new Error("Session event replay batch limit must be a positive integer");
29785
+ }
29786
+ return this.#database.prepare(
29787
+ `WITH ranked_events AS (
29788
+ SELECT ordinal,
29789
+ ROW_NUMBER() OVER (
29790
+ PARTITION BY source_key
29791
+ ORDER BY ordinal
29792
+ ) AS source_position
29793
+ FROM relay_session_event_outbox
29794
+ )
29795
+ SELECT outbox.envelope_json
29796
+ FROM ranked_events AS ranked
29797
+ INNER JOIN relay_session_event_outbox AS outbox
29798
+ ON outbox.ordinal = ranked.ordinal
29799
+ ORDER BY ranked.source_position, outbox.ordinal
29800
+ LIMIT ?`
29801
+ ).all(limit).map(readPendingEnvelope);
29802
+ };
29554
29803
  };
29555
29804
  function isDaemonExecutionState(value) {
29556
29805
  return value === "cancelled" || value === "dispatching" || value === "failed" || value === "running" || value === "succeeded";
@@ -29567,7 +29816,7 @@ var systemDaemonTimer = new SystemDaemonTimer();
29567
29816
 
29568
29817
  // src/run-daemon.ts
29569
29818
  var RELAY_RECONNECT_DELAY_MILLISECONDS = 500;
29570
- var RELAY_CONNECTION_REFRESH_MILLISECONDS = 20 * 60 * 1e3;
29819
+ var AGENT_ACTIVITY_COALESCING_MILLISECONDS = 100;
29571
29820
  var ACTIVE_TURN_RECONCILIATION_TIMEOUT_MILLISECONDS = 2e4;
29572
29821
  var ACTIVE_TURN_RECONCILIATION_CLOSE_CODE = 4009;
29573
29822
  function botIpcSocketPath() {
@@ -29576,54 +29825,6 @@ function botIpcSocketPath() {
29576
29825
  }
29577
29826
  return join16("/tmp", `mobius-bot-${randomUUID6()}.sock`);
29578
29827
  }
29579
- var RelayConnectionRefreshState = class {
29580
- };
29581
- var ScheduledRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
29582
- };
29583
- var DueRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
29584
- };
29585
- var FinishedRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
29586
- };
29587
- var RelayConnectionRefresh = class {
29588
- #isSafe;
29589
- #refresh;
29590
- #scheduledTask;
29591
- #state = new ScheduledRelayConnectionRefreshState();
29592
- constructor(timer, isSafe, refresh) {
29593
- this.#isSafe = isSafe;
29594
- this.#refresh = refresh;
29595
- this.#scheduledTask = timer.schedule(this.#request, RELAY_CONNECTION_REFRESH_MILLISECONDS);
29596
- }
29597
- afterTurnFinished = () => {
29598
- if (this.#state instanceof DueRelayConnectionRefreshState) {
29599
- this.#request();
29600
- return;
29601
- }
29602
- if (this.#state instanceof ScheduledRelayConnectionRefreshState || this.#state instanceof FinishedRelayConnectionRefreshState) {
29603
- return;
29604
- }
29605
- throw new Error("Unknown relay connection refresh state");
29606
- };
29607
- close = () => {
29608
- this.#scheduledTask.cancel();
29609
- this.#state = new FinishedRelayConnectionRefreshState();
29610
- };
29611
- #request = () => {
29612
- if (this.#state instanceof FinishedRelayConnectionRefreshState) {
29613
- return;
29614
- }
29615
- if (!(this.#state instanceof ScheduledRelayConnectionRefreshState) && !(this.#state instanceof DueRelayConnectionRefreshState)) {
29616
- throw new Error("Unknown relay connection refresh state");
29617
- }
29618
- if (!this.#isSafe()) {
29619
- this.#state = new DueRelayConnectionRefreshState();
29620
- return;
29621
- }
29622
- this.#state = new FinishedRelayConnectionRefreshState();
29623
- this.#scheduledTask.cancel();
29624
- this.#refresh();
29625
- };
29626
- };
29627
29828
  var DaemonAcpObserver = class {
29628
29829
  #record;
29629
29830
  constructor(record2) {
@@ -29888,7 +30089,8 @@ var DaemonSessionEventConnectionState = class {
29888
30089
  };
29889
30090
  var DisconnectedDaemonSessionEventState = class extends DaemonSessionEventConnectionState {
29890
30091
  disconnect = () => this;
29891
- send = () => void 0;
30092
+ matches = () => false;
30093
+ send = () => false;
29892
30094
  };
29893
30095
  var ConnectedDaemonSessionEventState = class extends DaemonSessionEventConnectionState {
29894
30096
  #socket;
@@ -29897,38 +30099,155 @@ var ConnectedDaemonSessionEventState = class extends DaemonSessionEventConnectio
29897
30099
  this.#socket = socket;
29898
30100
  }
29899
30101
  disconnect = (socket) => socket === this.#socket ? new DisconnectedDaemonSessionEventState() : this;
30102
+ matches = (socket) => socket === this.#socket;
29900
30103
  send = (message) => {
29901
- if (this.#socket.readyState === WebSocket2.OPEN) {
29902
- try {
29903
- this.#socket.send(message);
29904
- } catch {
29905
- }
30104
+ if (this.#socket.readyState !== WebSocket2.OPEN) {
30105
+ return false;
30106
+ }
30107
+ try {
30108
+ this.#socket.send(message);
30109
+ return true;
30110
+ } catch {
30111
+ return false;
29906
30112
  }
29907
30113
  };
29908
30114
  };
30115
+ var SESSION_EVENT_REPLAY_WINDOW = 4;
29909
30116
  var DaemonSessionEventPublisher = class {
29910
30117
  #outbox;
30118
+ #inFlightEventIds = /* @__PURE__ */ new Set();
29911
30119
  #connection = new DisconnectedDaemonSessionEventState();
29912
30120
  constructor(outbox) {
29913
30121
  this.#outbox = outbox;
29914
30122
  }
29915
30123
  connect = (socket) => {
29916
- const connection = new ConnectedDaemonSessionEventState(socket);
29917
- this.#connection = connection;
29918
- for (const pending of this.#outbox.pending()) {
29919
- connection.send(pending);
29920
- }
30124
+ this.#connection = new ConnectedDaemonSessionEventState(socket);
30125
+ this.#inFlightEventIds.clear();
30126
+ this.#publishNextBatch();
29921
30127
  };
29922
30128
  disconnect = (socket) => {
30129
+ if (!this.#connection.matches(socket)) {
30130
+ return;
30131
+ }
29923
30132
  this.#connection = this.#connection.disconnect(socket);
30133
+ this.#inFlightEventIds.clear();
29924
30134
  };
29925
30135
  recordAttempt = (sessionId, payload, attemptId) => {
29926
- this.#connection.send(this.#outbox.enqueue(sessionId, payload, attemptId));
30136
+ this.#outbox.enqueue(sessionId, payload, attemptId);
30137
+ this.#publishNextBatch();
29927
30138
  };
29928
30139
  recordRuntime = (sessionId, payload) => {
29929
- this.#connection.send(this.#outbox.enqueue(sessionId, payload));
30140
+ this.#outbox.enqueue(sessionId, payload);
30141
+ this.#publishNextBatch();
30142
+ };
30143
+ acknowledge = (acknowledgement) => {
30144
+ if (!this.#outbox.acknowledge(acknowledgement)) {
30145
+ return;
30146
+ }
30147
+ this.#inFlightEventIds.delete(acknowledgement.eventId);
30148
+ this.#publishNextBatch();
30149
+ };
30150
+ quarantine = (rejection) => {
30151
+ const quarantined = this.#outbox.quarantine(rejection);
30152
+ if (!quarantined) {
30153
+ return false;
30154
+ }
30155
+ this.#inFlightEventIds.delete(rejection.eventId);
30156
+ this.#publishNextBatch();
30157
+ return true;
30158
+ };
30159
+ #publishNextBatch = () => {
30160
+ if (this.#inFlightEventIds.size > 0) {
30161
+ return;
30162
+ }
30163
+ const batch = this.#outbox.pendingBatch(SESSION_EVENT_REPLAY_WINDOW);
30164
+ for (const encoded of batch) {
30165
+ const envelope2 = decodeSessionEventEnvelope(encoded);
30166
+ if (!this.#connection.send(encoded)) {
30167
+ this.#inFlightEventIds.clear();
30168
+ return;
30169
+ }
30170
+ this.#inFlightEventIds.add(envelope2.eventId);
30171
+ }
30172
+ };
30173
+ };
30174
+ var CoalescingDaemonActivityPublisher = class {
30175
+ #pending = /* @__PURE__ */ new Map();
30176
+ #publish;
30177
+ #publishedActivityIds = /* @__PURE__ */ new Set();
30178
+ #scheduled = /* @__PURE__ */ new Map();
30179
+ #timer;
30180
+ constructor(timer, publish) {
30181
+ this.#publish = publish;
30182
+ this.#timer = timer;
30183
+ }
30184
+ flush = () => {
30185
+ const activities = [...this.#pending.values()].sort((left, right) => left.sequence - right.sequence);
30186
+ for (const task of this.#scheduled.values()) {
30187
+ task.cancel();
30188
+ }
30189
+ this.#pending.clear();
30190
+ this.#scheduled.clear();
30191
+ for (const activity of activities) {
30192
+ this.#publish(activity);
30193
+ }
30194
+ };
30195
+ record = (activity) => {
30196
+ if (activity.status !== "pending" && activity.status !== "running") {
30197
+ this.#discardPending(activity.activityId);
30198
+ this.#publishedActivityIds.add(activity.activityId);
30199
+ this.#publish(activity);
30200
+ return Promise.resolve();
30201
+ }
30202
+ if (!this.#publishedActivityIds.has(activity.activityId)) {
30203
+ this.#publishedActivityIds.add(activity.activityId);
30204
+ this.#publish(activity);
30205
+ return Promise.resolve();
30206
+ }
30207
+ this.#pending.set(activity.activityId, activity);
30208
+ if (!this.#scheduled.has(activity.activityId)) {
30209
+ this.#scheduled.set(
30210
+ activity.activityId,
30211
+ this.#timer.schedule(
30212
+ () => this.#flushActivity(activity.activityId),
30213
+ AGENT_ACTIVITY_COALESCING_MILLISECONDS
30214
+ )
30215
+ );
30216
+ }
30217
+ return Promise.resolve();
30218
+ };
30219
+ #discardPending = (activityId) => {
30220
+ this.#scheduled.get(activityId)?.cancel();
30221
+ this.#scheduled.delete(activityId);
30222
+ this.#pending.delete(activityId);
30223
+ };
30224
+ #flushActivity = (activityId) => {
30225
+ const activity = this.#pending.get(activityId);
30226
+ this.#discardPending(activityId);
30227
+ if (activity !== void 0) {
30228
+ this.#publish(activity);
30229
+ }
29930
30230
  };
29931
30231
  };
30232
+ function encodeDaemonActivity(command, activity) {
30233
+ return 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
+ }
29932
30251
  function reportFailure(publisher, command, code, message, attemptId) {
29933
30252
  publisher.recordAttempt(
29934
30253
  command.sessionId,
@@ -30105,7 +30424,10 @@ async function executeSessionConfigure(publisher, socket, command, config2, dire
30105
30424
  reportOperationFailure(socket, command.requestId, command.sessionId, error61);
30106
30425
  }
30107
30426
  }
30108
- async function executePrompt(outbox, publisher, command, config2, directories, audit, sessions, attemptId, signal) {
30427
+ async function executePrompt(outbox, publisher, command, config2, directories, audit, sessions, attemptId, timer, signal) {
30428
+ const activities = new CoalescingDaemonActivityPublisher(timer, (activity) => {
30429
+ publisher.recordAttempt(command.sessionId, encodeDaemonActivity(command, activity), attemptId);
30430
+ });
30109
30431
  try {
30110
30432
  const resolved = await resolveSessionDirectories(
30111
30433
  directories,
@@ -30162,30 +30484,7 @@ async function executePrompt(outbox, publisher, command, config2, directories, a
30162
30484
  cwd: resolved.cwd,
30163
30485
  disposition: command.disposition,
30164
30486
  images: attachments.images,
30165
- onActivity: (activity) => {
30166
- publisher.recordAttempt(
30167
- command.sessionId,
30168
- encodeAgentActivityEvent({
30169
- activityId: activity.activityId,
30170
- activityType: activity.activityType,
30171
- commandId: command.commandId,
30172
- content: activity.content,
30173
- detail: activity.detail,
30174
- input: activity.input,
30175
- kind: AGENT_ACTIVITY_KIND,
30176
- locations: activity.locations,
30177
- name: activity.name,
30178
- output: activity.output,
30179
- sequence: activity.sequence,
30180
- sessionId: command.sessionId,
30181
- status: activity.status,
30182
- title: activity.title,
30183
- toolKind: activity.toolKind
30184
- }),
30185
- attemptId
30186
- );
30187
- return Promise.resolve();
30188
- },
30487
+ onActivity: activities.record,
30189
30488
  onText: (text, messageId) => {
30190
30489
  publisher.recordAttempt(
30191
30490
  command.sessionId,
@@ -30208,6 +30507,7 @@ async function executePrompt(outbox, publisher, command, config2, directories, a
30208
30507
  });
30209
30508
  stopReason = result.stopReason;
30210
30509
  }
30510
+ activities.flush();
30211
30511
  publisher.recordAttempt(
30212
30512
  command.sessionId,
30213
30513
  encodeSessionCompletedEvent({
@@ -30225,6 +30525,7 @@ async function executePrompt(outbox, publisher, command, config2, directories, a
30225
30525
  sessionId: command.sessionId
30226
30526
  });
30227
30527
  } catch (error61) {
30528
+ activities.flush();
30228
30529
  audit.record({
30229
30530
  commandId: command.commandId,
30230
30531
  event: "session_failed",
@@ -30263,7 +30564,10 @@ function botExecutionAgent(agent, contextHandle, socketPath) {
30263
30564
  agent.capabilities
30264
30565
  );
30265
30566
  }
30266
- async function executeBotPrompt(outbox, publisher, command, config2, directories, audit, sessions, grants, contexts, applicator, ipc, attemptId, signal) {
30567
+ async function executeBotPrompt(outbox, publisher, command, config2, directories, audit, sessions, grants, contexts, applicator, ipc, attemptId, timer, signal) {
30568
+ const activities = new CoalescingDaemonActivityPublisher(timer, (activity) => {
30569
+ publisher.recordAttempt(command.sessionId, encodeDaemonActivity(command, activity), attemptId);
30570
+ });
30267
30571
  try {
30268
30572
  const deadline = Date.parse(command.executionDeadlineAt);
30269
30573
  const remaining = deadline - Date.now();
@@ -30347,30 +30651,7 @@ async function executeBotPrompt(outbox, publisher, command, config2, directories
30347
30651
  commandId: command.commandId,
30348
30652
  cwd: resolved.cwd,
30349
30653
  images: attachments.images,
30350
- onActivity: (activity) => {
30351
- publisher.recordAttempt(
30352
- command.sessionId,
30353
- encodeAgentActivityEvent({
30354
- activityId: activity.activityId,
30355
- activityType: activity.activityType,
30356
- commandId: command.commandId,
30357
- content: activity.content,
30358
- detail: activity.detail,
30359
- input: activity.input,
30360
- kind: AGENT_ACTIVITY_KIND,
30361
- locations: activity.locations,
30362
- name: activity.name,
30363
- output: activity.output,
30364
- sequence: activity.sequence,
30365
- sessionId: command.sessionId,
30366
- status: activity.status,
30367
- title: activity.title,
30368
- toolKind: activity.toolKind
30369
- }),
30370
- attemptId
30371
- );
30372
- return Promise.resolve();
30373
- },
30654
+ onActivity: activities.record,
30374
30655
  onText: (text, messageId) => {
30375
30656
  publisher.recordAttempt(
30376
30657
  command.sessionId,
@@ -30390,6 +30671,7 @@ async function executeBotPrompt(outbox, publisher, command, config2, directories
30390
30671
  sessionId: command.sessionId,
30391
30672
  signal: executionSignal
30392
30673
  });
30674
+ activities.flush();
30393
30675
  publisher.recordAttempt(
30394
30676
  command.sessionId,
30395
30677
  encodeSessionCompletedEvent({
@@ -30414,6 +30696,7 @@ async function executeBotPrompt(outbox, publisher, command, config2, directories
30414
30696
  lease.release();
30415
30697
  }
30416
30698
  } catch (error61) {
30699
+ activities.flush();
30417
30700
  const failure = Date.parse(command.executionDeadlineAt) <= Date.now() ? new LocalBotAuthorizationError("turn_expired", "The Bot execution deadline has expired", {
30418
30701
  cause: error61
30419
30702
  }) : error61;
@@ -30453,15 +30736,15 @@ function waitForRelayRetry(signal, timer) {
30453
30736
  signal.addEventListener("abort", finish, { once: true });
30454
30737
  });
30455
30738
  }
30456
- async function publishAgentCatalogUntilAvailable(config2, signal, timer) {
30739
+ async function publishAgentCatalogUntilAvailable(config2, diagnostics, signal, timer) {
30457
30740
  try {
30458
30741
  await publishAgentCatalog(config2, signal);
30459
30742
  return;
30460
- } catch {
30743
+ } catch (error61) {
30461
30744
  if (signal.aborted) {
30462
30745
  return;
30463
30746
  }
30464
- console.warn("Mobius daemon is waiting to publish its Agent catalog");
30747
+ diagnostics.catalogFailed(error61);
30465
30748
  }
30466
30749
  while (!signal.aborted) {
30467
30750
  await waitForRelayRetry(signal, timer);
@@ -30475,11 +30758,12 @@ async function publishAgentCatalogUntilAvailable(config2, signal, timer) {
30475
30758
  }
30476
30759
  }
30477
30760
  }
30478
- async function runRelayConnection(config2, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, onConnected, signal, timer) {
30761
+ async function runRelayConnection(config2, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, connectionEpoch, diagnostics, onConnected, signal, timer) {
30479
30762
  const relayUrl = new URL(config2.relayUrl);
30480
30763
  relayUrl.searchParams.set("liveness", "1");
30481
30764
  const socket = await connectRelay(relayUrl, signal);
30482
30765
  const reconnectingTurns = [...activeTurns.values()];
30766
+ diagnostics.connectionOpened(connectionEpoch);
30483
30767
  onConnected();
30484
30768
  const connectionController = new AbortController();
30485
30769
  const nativeRequests = /* @__PURE__ */ new Map();
@@ -30488,7 +30772,6 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30488
30772
  await new Promise((resolve6, reject) => {
30489
30773
  let finished = false;
30490
30774
  const cleanup = () => {
30491
- connectionRefresh.close();
30492
30775
  connectionLiveness.close();
30493
30776
  signal.removeEventListener("abort", stop);
30494
30777
  socket.removeEventListener("close", handleClose);
@@ -30506,61 +30789,70 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30506
30789
  cleanup();
30507
30790
  resolve6();
30508
30791
  };
30509
- const rejectConnection = () => {
30792
+ const rejectConnection = (failure) => {
30510
30793
  if (finished) {
30511
30794
  return;
30512
30795
  }
30513
30796
  finished = true;
30514
30797
  cleanup();
30515
- reject(new Error("Relay WebSocket failed"));
30798
+ reject(failure);
30516
30799
  };
30517
30800
  const stop = () => {
30801
+ diagnostics.connectionClosed(connectionEpoch, 1e3, "Daemon stopped", "daemon_stop");
30518
30802
  closeRelaySocket(socket, 1e3, "Daemon stopped");
30519
30803
  resolveConnection();
30520
30804
  };
30521
- const refreshConnection = () => {
30522
- closeRelaySocket(socket, 1e3, "Refreshing idle relay connection");
30523
- resolveConnection();
30805
+ const failAndClose = (failure, code, reason, source) => {
30806
+ diagnostics.connectionFailed(connectionEpoch, failure, source);
30807
+ diagnostics.connectionClosed(connectionEpoch, code, reason, source);
30808
+ closeRelaySocket(socket, code, reason);
30809
+ rejectConnection(failure);
30524
30810
  };
30525
30811
  const connectionLiveness = new RelayConnectionLiveness(timer, {
30526
30812
  send: (message) => send(socket, message),
30527
- unresponsive: () => {
30813
+ unresponsive: (heartbeatId, timeoutMilliseconds) => {
30814
+ diagnostics.heartbeatTimedOut(connectionEpoch, heartbeatId, timeoutMilliseconds);
30815
+ diagnostics.connectionClosed(
30816
+ connectionEpoch,
30817
+ 4001,
30818
+ "Relay liveness check timed out",
30819
+ "heartbeat_timeout"
30820
+ );
30528
30821
  closeRelaySocket(socket, 4001, "Relay liveness check timed out");
30529
30822
  resolveConnection();
30530
30823
  }
30531
30824
  });
30532
- const connectionRefresh = new RelayConnectionRefresh(
30533
- timer,
30534
- () => activeTurns.size === 0,
30535
- refreshConnection
30536
- );
30537
30825
  signal.addEventListener("abort", stop, { once: true });
30538
- const handleClose = () => {
30826
+ const handleClose = (event) => {
30827
+ diagnostics.connectionClosed(connectionEpoch, event.code, event.reason, "relay");
30539
30828
  resolveConnection();
30540
30829
  };
30541
- const handleError = () => {
30830
+ const handleError = (event) => {
30542
30831
  if (signal.aborted) {
30543
30832
  resolveConnection();
30544
30833
  return;
30545
30834
  }
30546
- rejectConnection();
30835
+ const cause = Reflect.get(event, "error");
30836
+ const failure = new Error("Relay WebSocket emitted an error", { cause: cause ?? event });
30837
+ diagnostics.connectionFailed(connectionEpoch, failure, "socket_error");
30838
+ diagnostics.connectionClosed(connectionEpoch, 1006, "WebSocket error", "socket_error");
30839
+ rejectConnection(failure);
30547
30840
  };
30548
30841
  const handleMessage = (event) => {
30549
30842
  const encoded = event.data;
30550
30843
  if (typeof encoded !== "string") {
30551
- closeRelaySocket(socket, 4003, "Text message required");
30552
- rejectConnection();
30844
+ failAndClose(new Error("Relay sent a non-text message"), 4003, "Text message required", "protocol");
30553
30845
  return;
30554
30846
  }
30555
30847
  try {
30556
30848
  const kind = decodeMessageKind(encoded);
30557
30849
  if (kind === SESSION_EVENT_ACK_KIND) {
30558
- outbox.acknowledge(decodeSessionEventAcknowledgement(encoded));
30850
+ publisher.acknowledge(decodeSessionEventAcknowledgement(encoded));
30559
30851
  return;
30560
30852
  }
30561
30853
  if (kind === SESSION_EVENT_REJECTED_KIND) {
30562
30854
  const rejection = decodeSessionEventRejection(encoded);
30563
- if (outbox.quarantine(rejection)) {
30855
+ if (publisher.quarantine(rejection)) {
30564
30856
  audit.record({
30565
30857
  commandId: rejection.eventId,
30566
30858
  details: { code: rejection.code },
@@ -30939,6 +31231,7 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30939
31231
  applicator,
30940
31232
  ipc,
30941
31233
  receipt2.attemptId,
31234
+ timer,
30942
31235
  turn2.signal
30943
31236
  ).catch((error61) => {
30944
31237
  console.error("Mobius daemon Bot turn failed", error61);
@@ -30948,7 +31241,6 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30948
31241
  activeAttemptIds.delete(command2.sessionId);
30949
31242
  }
30950
31243
  turn2.finish();
30951
- connectionRefresh.afterTurnFinished();
30952
31244
  });
30953
31245
  return;
30954
31246
  }
@@ -30977,6 +31269,7 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30977
31269
  audit,
30978
31270
  sessions,
30979
31271
  receipt.attemptId,
31272
+ timer,
30980
31273
  turn.signal
30981
31274
  ).catch((error61) => {
30982
31275
  console.error("Mobius daemon turn failed", error61);
@@ -30986,12 +31279,15 @@ async function runRelayConnection(config2, directories, audit, sessions, install
30986
31279
  activeAttemptIds.delete(command.sessionId);
30987
31280
  }
30988
31281
  turn.finish();
30989
- connectionRefresh.afterTurnFinished();
30990
31282
  });
30991
31283
  } catch (error61) {
30992
31284
  console.error("Mobius daemon rejected a message", error61);
30993
- closeRelaySocket(socket, 4008, "Invalid protocol message");
30994
- rejectConnection();
31285
+ failAndClose(
31286
+ new Error("Relay sent an invalid protocol message", { cause: error61 }),
31287
+ 4008,
31288
+ "Invalid protocol message",
31289
+ "protocol"
31290
+ );
30995
31291
  }
30996
31292
  };
30997
31293
  socket.addEventListener("close", handleClose);
@@ -31003,21 +31299,18 @@ async function runRelayConnection(config2, directories, audit, sessions, install
31003
31299
  return;
31004
31300
  }
31005
31301
  publisher.connect(socket);
31006
- void Promise.all(reconnectingTurns.map((turn) => turn.completion)).then(
31007
- connectionRefresh.afterTurnFinished
31008
- );
31009
31302
  },
31010
31303
  (error61) => {
31011
31304
  if (finished) {
31012
31305
  return;
31013
31306
  }
31014
31307
  console.error("Mobius daemon could not reconcile active Agent turns", error61);
31015
- closeRelaySocket(
31016
- socket,
31308
+ failAndClose(
31309
+ new Error("Active Agent turn reconciliation failed", { cause: error61 }),
31017
31310
  ACTIVE_TURN_RECONCILIATION_CLOSE_CODE,
31018
- "Active Agent turn reconciliation failed"
31311
+ "Active Agent turn reconciliation failed",
31312
+ "reconciliation"
31019
31313
  );
31020
- rejectConnection();
31021
31314
  }
31022
31315
  );
31023
31316
  if (signal.aborted) {
@@ -31061,6 +31354,7 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31061
31354
  const sessions = new AcpSessionManager(new DaemonAcpObserver(recordSessionEvent), sessionStore);
31062
31355
  const directories = new WorkspaceDirectoryService(config2.bindingId, config2.defaultWorkspace);
31063
31356
  const installationObserver = new DaemonInstallationObserver();
31357
+ const diagnostics = new RelayConnectionDiagnostics(audit, config2.channelId, config2.relayUrl);
31064
31358
  const nativeSessions = new NativeSessionService(config2, directories, sessions, audit);
31065
31359
  const installations = new AgentInstallationManager(
31066
31360
  specifications,
@@ -31068,7 +31362,7 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31068
31362
  installationObserver,
31069
31363
  installationAudit
31070
31364
  );
31071
- let retryAnnounced = false;
31365
+ let connectionEpoch = 0;
31072
31366
  let connectionAnnounced = false;
31073
31367
  const announceConnection = () => {
31074
31368
  if (connectionAnnounced) {
@@ -31084,10 +31378,11 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31084
31378
  `
31085
31379
  );
31086
31380
  };
31087
- const catalogPublication = publishAgentCatalogUntilAvailable(config2, signal, timer);
31381
+ const catalogPublication = publishAgentCatalogUntilAvailable(config2, diagnostics, signal, timer);
31088
31382
  try {
31089
31383
  await ipc.listen(signal);
31090
31384
  while (!signal.aborted) {
31385
+ connectionEpoch += 1;
31091
31386
  try {
31092
31387
  await runRelayConnection(
31093
31388
  config2,
@@ -31105,19 +31400,17 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
31105
31400
  ipc,
31106
31401
  activeTurns,
31107
31402
  activeAttemptIds,
31403
+ connectionEpoch,
31404
+ diagnostics,
31108
31405
  announceConnection,
31109
31406
  signal,
31110
31407
  timer
31111
31408
  );
31112
- retryAnnounced = false;
31113
- } catch {
31409
+ } catch (error61) {
31114
31410
  if (signal.aborted) {
31115
31411
  return;
31116
31412
  }
31117
- if (!retryAnnounced) {
31118
- console.warn("Mobius daemon is waiting for the relay");
31119
- retryAnnounced = true;
31120
- }
31413
+ diagnostics.connectionFailed(connectionEpoch, error61, "connection");
31121
31414
  }
31122
31415
  await waitForRelayRetry(signal, timer);
31123
31416
  }