@moxt-ai/mobius 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/agent-catalog/publish-agent-catalog.js +3 -3
  2. package/dist/agent-catalog/publish-agent-catalog.js.map +1 -1
  3. package/dist/cli.js +1194 -205
  4. package/dist/cli.js.map +4 -4
  5. package/dist/index.js +1186 -204
  6. package/dist/index.js.map +4 -4
  7. package/dist/network/environment-proxy.d.ts +1 -0
  8. package/dist/network/environment-proxy.d.ts.map +1 -1
  9. package/dist/network/environment-proxy.js +47 -0
  10. package/dist/network/environment-proxy.js.map +1 -1
  11. package/dist/prompt-artifact-resolver.d.ts.map +1 -1
  12. package/dist/prompt-artifact-resolver.js +7 -4
  13. package/dist/prompt-artifact-resolver.js.map +1 -1
  14. package/dist/relay-connection/connect.d.ts.map +1 -1
  15. package/dist/relay-connection/connect.js +19 -14
  16. package/dist/relay-connection/connect.js.map +1 -1
  17. package/dist/relay-connection/diagnostics.d.ts +11 -0
  18. package/dist/relay-connection/diagnostics.d.ts.map +1 -0
  19. package/dist/relay-connection/diagnostics.js +122 -0
  20. package/dist/relay-connection/diagnostics.js.map +1 -0
  21. package/dist/relay-connection/liveness.d.ts +1 -1
  22. package/dist/relay-connection/liveness.d.ts.map +1 -1
  23. package/dist/relay-connection/liveness.js +2 -1
  24. package/dist/relay-connection/liveness.js.map +1 -1
  25. package/dist/relay-connection/runtime-authentication.d.ts +3 -0
  26. package/dist/relay-connection/runtime-authentication.d.ts.map +1 -0
  27. package/dist/relay-connection/runtime-authentication.js +14 -0
  28. package/dist/relay-connection/runtime-authentication.js.map +1 -0
  29. package/dist/relay-connection/session-event-outbox.d.ts +34 -3
  30. package/dist/relay-connection/session-event-outbox.d.ts.map +1 -1
  31. package/dist/relay-connection/session-event-outbox.js +404 -13
  32. package/dist/relay-connection/session-event-outbox.js.map +1 -1
  33. package/dist/run-daemon.d.ts.map +1 -1
  34. package/dist/run-daemon.js +437 -151
  35. package/dist/run-daemon.js.map +1 -1
  36. package/dist/runtime.js +1186 -204
  37. package/dist/runtime.js.map +4 -4
  38. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { basename, join } from "node:path";
3
3
  import { styleText } from "node:util";
4
- import { AGENT_ACTIVITY_KIND, AGENT_INSTALL_CANCEL_KIND, AGENT_INSTALL_CANCELLED_KIND, AGENT_INSTALL_COMPLETED_KIND, AGENT_INSTALL_FAILED_KIND, AGENT_INSTALL_PROGRESS_KIND, AGENT_INSTALL_REQUEST_KIND, AGENT_INSTALL_STARTED_KIND, AGENT_MESSAGE_DELTA_KIND, BOT_BINDING_APPROVAL_REQUEST_KIND, BOT_BINDING_APPROVED_KIND, BOT_BINDING_UNAVAILABLE_KIND, BOT_SESSION_PROMPT_KIND, BotExecutionBindingUnavailableEvent, decodeAgentInstallCancelCommand, decodeAgentInstallRequestCommand, decodeBotExecutionBindingApprovalRequest, decodeBotExecutionBindingApprovedEvent, decodeBotExecutionBindingUnavailableEvent, decodeBotSessionPromptCommand, decodeMessageKind, decodeNativeSessionCancel, decodeNativeSessionCommand, decodeNativeSessionTurnForkCommand, decodeRuntimeHeartbeatAckEvent, decodeRuntimeLivenessReadyEvent, decodeSessionCancelCommand, decodeSessionConfigureCommand, decodeSessionElicitationRespondCommand, decodeSessionEventAcknowledgement, decodeSessionEventRejection, decodeSessionPermissionRespondCommand, decodeSessionPrepareCommand, decodeSessionPromptCommand, decodeWorkspaceListCommand, encodeAgentActivityEvent, encodeAgentContentEvent, encodeAgentInstallCancelledEvent, encodeAgentInstallCompletedEvent, encodeAgentInstallFailedEvent, encodeAgentInstallProgressEvent, encodeAgentInstallStartedEvent, encodeAgentMessageDeltaEvent, encodeBotExecutionBindingApprovedEvent, encodeBotExecutionBindingUnavailableEvent, encodeNativeSessionResult, encodeSessionCommandsUpdatedEvent, encodeSessionCompletedEvent, encodeSessionConfigUpdatedEvent, encodeSessionElicitationRequestedEvent, encodeSessionElicitationResolvedEvent, encodeSessionFailedEvent, encodeSessionInfoUpdatedEvent, encodeSessionOperationFailedEvent, encodeSessionPermissionRequestedEvent, encodeSessionPermissionResolvedEvent, encodeSessionReadyEvent, encodeSessionStartedEvent, encodeSessionUsageUpdatedEvent, encodeWorkspaceEntriesEvent, NATIVE_SESSION_CANCEL_KIND, NATIVE_SESSION_REQUEST_KIND, NATIVE_SESSION_TURN_FORK_KIND, OwnerBotReplyRoute, RUNTIME_HEARTBEAT_ACK_KIND, RUNTIME_LIVENESS_READY_KIND, SESSION_CANCEL_KIND, SESSION_COMPLETED_KIND, SESSION_CONFIGURE_KIND, SESSION_ELICITATION_RESPOND_KIND, SESSION_EVENT_ACK_KIND, SESSION_EVENT_REJECTED_KIND, SESSION_FAILED_KIND, SESSION_PERMISSION_RESPOND_KIND, SESSION_PREPARE_KIND, SESSION_PROMPT_KIND, SESSION_STARTED_KIND, SessionPromptDisposition, WORKSPACE_LIST_KIND, } from "@moxt-ai/mobius-protocol";
4
+ import { AGENT_ACTIVITY_KIND, AGENT_INSTALL_CANCEL_KIND, AGENT_INSTALL_CANCELLED_KIND, AGENT_INSTALL_COMPLETED_KIND, AGENT_INSTALL_FAILED_KIND, AGENT_INSTALL_PROGRESS_KIND, AGENT_INSTALL_REQUEST_KIND, AGENT_INSTALL_STARTED_KIND, AGENT_MESSAGE_DELTA_KIND, BOT_BINDING_APPROVAL_REQUEST_KIND, BOT_BINDING_APPROVED_KIND, BOT_BINDING_UNAVAILABLE_KIND, BOT_SESSION_PROMPT_KIND, BotExecutionBindingUnavailableEvent, decodeAgentInstallCancelCommand, decodeAgentInstallRequestCommand, decodeBotExecutionBindingApprovalRequest, decodeBotExecutionBindingApprovedEvent, decodeBotExecutionBindingUnavailableEvent, decodeBotSessionPromptCommand, decodeMessageKind, decodeNativeSessionCancel, decodeNativeSessionCommand, decodeNativeSessionTurnForkCommand, decodeRuntimeDelivery, decodeRuntimeDeliveryCommitted, decodeRuntimeHeartbeatAckEvent, decodeRuntimeLivenessReadyEvent, decodeSessionCancelCommand, decodeSessionConfigureCommand, decodeSessionElicitationRespondCommand, decodeSessionEventAcknowledgement, decodeSessionEventEnvelope, decodeSessionEventRejection, decodeSessionPermissionRespondCommand, decodeSessionPrepareCommand, decodeSessionPromptCommand, decodeWorkspaceListCommand, encodeAgentActivityEvent, encodeAgentContentEvent, encodeAgentInstallCancelledEvent, encodeAgentInstallCompletedEvent, encodeAgentInstallFailedEvent, encodeAgentInstallProgressEvent, encodeAgentInstallStartedEvent, encodeAgentMessageDeltaEvent, encodeBotExecutionBindingApprovedEvent, encodeBotExecutionBindingUnavailableEvent, encodeNativeSessionResult, encodeRuntimeDeliveryReceived, encodeSessionCommandsUpdatedEvent, encodeSessionCompletedEvent, encodeSessionConfigUpdatedEvent, encodeSessionElicitationRequestedEvent, encodeSessionElicitationResolvedEvent, encodeSessionFailedEvent, encodeSessionInfoUpdatedEvent, encodeSessionOperationFailedEvent, encodeSessionPermissionRequestedEvent, encodeSessionPermissionResolvedEvent, encodeSessionReadyEvent, encodeSessionStartedEvent, encodeSessionUsageUpdatedEvent, encodeWorkspaceEntriesEvent, NATIVE_SESSION_CANCEL_KIND, NATIVE_SESSION_REQUEST_KIND, NATIVE_SESSION_TURN_FORK_KIND, OwnerBotReplyRoute, RUNTIME_DELIVERY_COMMITTED_KIND, RUNTIME_DELIVERY_KIND, RUNTIME_DELIVERY_RECEIVED_KIND, RUNTIME_HEARTBEAT_ACK_KIND, RUNTIME_LIVENESS_READY_KIND, SESSION_CANCEL_KIND, SESSION_COMPLETED_KIND, SESSION_CONFIGURE_KIND, SESSION_ELICITATION_RESPOND_KIND, SESSION_EVENT_ACK_KIND, SESSION_EVENT_REJECTED_KIND, SESSION_FAILED_KIND, SESSION_PERMISSION_RESPOND_KIND, SESSION_PREPARE_KIND, SESSION_PROMPT_KIND, SESSION_STARTED_KIND, SessionPromptDisposition, WORKSPACE_LIST_KIND, } from "@moxt-ai/mobius-protocol";
5
5
  import { fetch, WebSocket } from "undici";
6
6
  import { AcpExecutionError, AcpSessionManager } from "./acp/session-manager.js";
7
7
  import { SqliteAcpSessionStore } from "./acp/session-store.js";
@@ -20,12 +20,14 @@ import { NativeSessionService } from "./native-sessions/service.js";
20
20
  import { environmentProxyDispatcher } from "./network/environment-proxy.js";
21
21
  import { PromptArtifactError, resolvePromptArtifacts } from "./prompt-artifact-resolver.js";
22
22
  import { connectRelay } from "./relay-connection/connect.js";
23
+ import { RelayConnectionDiagnostics } from "./relay-connection/diagnostics.js";
23
24
  import { RelayConnectionLiveness } from "./relay-connection/liveness.js";
24
- import { SessionEventOutbox } from "./relay-connection/session-event-outbox.js";
25
+ import { relayHttpEndpoint, runtimeAuthorization } from "./relay-connection/runtime-authentication.js";
26
+ import { RuntimeDeliveryInboxCapacityError, SessionEventOutbox } from "./relay-connection/session-event-outbox.js";
25
27
  import { systemDaemonTimer } from "./relay-connection/timer.js";
26
28
  import { readStateDirectory } from "./storage/state-directory.js";
27
29
  const RELAY_RECONNECT_DELAY_MILLISECONDS = 500;
28
- const RELAY_CONNECTION_REFRESH_MILLISECONDS = 20 * 60 * 1_000;
30
+ const AGENT_ACTIVITY_COALESCING_MILLISECONDS = 100;
29
31
  const ACTIVE_TURN_RECONCILIATION_TIMEOUT_MILLISECONDS = 20_000;
30
32
  const ACTIVE_TURN_RECONCILIATION_CLOSE_CODE = 4009;
31
33
  function botIpcSocketPath() {
@@ -34,56 +36,6 @@ function botIpcSocketPath() {
34
36
  }
35
37
  return join("/tmp", `mobius-bot-${randomUUID()}.sock`);
36
38
  }
37
- class RelayConnectionRefreshState {
38
- }
39
- class ScheduledRelayConnectionRefreshState extends RelayConnectionRefreshState {
40
- }
41
- class DueRelayConnectionRefreshState extends RelayConnectionRefreshState {
42
- }
43
- class FinishedRelayConnectionRefreshState extends RelayConnectionRefreshState {
44
- }
45
- class RelayConnectionRefresh {
46
- #isSafe;
47
- #refresh;
48
- #scheduledTask;
49
- #state = new ScheduledRelayConnectionRefreshState();
50
- constructor(timer, isSafe, refresh) {
51
- this.#isSafe = isSafe;
52
- this.#refresh = refresh;
53
- this.#scheduledTask = timer.schedule(this.#request, RELAY_CONNECTION_REFRESH_MILLISECONDS);
54
- }
55
- afterTurnFinished = () => {
56
- if (this.#state instanceof DueRelayConnectionRefreshState) {
57
- this.#request();
58
- return;
59
- }
60
- if (this.#state instanceof ScheduledRelayConnectionRefreshState ||
61
- this.#state instanceof FinishedRelayConnectionRefreshState) {
62
- return;
63
- }
64
- throw new Error("Unknown relay connection refresh state");
65
- };
66
- close = () => {
67
- this.#scheduledTask.cancel();
68
- this.#state = new FinishedRelayConnectionRefreshState();
69
- };
70
- #request = () => {
71
- if (this.#state instanceof FinishedRelayConnectionRefreshState) {
72
- return;
73
- }
74
- if (!(this.#state instanceof ScheduledRelayConnectionRefreshState) &&
75
- !(this.#state instanceof DueRelayConnectionRefreshState)) {
76
- throw new Error("Unknown relay connection refresh state");
77
- }
78
- if (!this.#isSafe()) {
79
- this.#state = new DueRelayConnectionRefreshState();
80
- return;
81
- }
82
- this.#state = new FinishedRelayConnectionRefreshState();
83
- this.#scheduledTask.cancel();
84
- this.#refresh();
85
- };
86
- }
87
39
  class DaemonAcpObserver {
88
40
  #record;
89
41
  constructor(record) {
@@ -264,8 +216,7 @@ class ActiveTurnReconciliationError extends Error {
264
216
  }
265
217
  }
266
218
  function activeTurnReconciliationEndpoint(config, turn) {
267
- const endpoint = new URL(config.relayUrl);
268
- endpoint.protocol = endpoint.protocol === "wss:" ? "https:" : "http:";
219
+ const endpoint = relayHttpEndpoint(config.relayUrl);
269
220
  let channelPath = "";
270
221
  if (endpoint.pathname.endsWith("/daemon")) {
271
222
  channelPath = endpoint.pathname.slice(0, -"/daemon".length);
@@ -278,7 +229,6 @@ function activeTurnReconciliationEndpoint(config, turn) {
278
229
  channelPath = `${endpoint.pathname.slice(0, -runtimePath.length)}/channels/${encodeURIComponent(config.channelId)}`;
279
230
  }
280
231
  endpoint.pathname = `${channelPath}/sessions/${encodeURIComponent(turn.command.sessionId)}/attempts/${encodeURIComponent(turn.attemptId)}/reconciliation`;
281
- endpoint.searchParams.delete("liveness");
282
232
  return endpoint;
283
233
  }
284
234
  async function reconcileActiveTurn(config, turn, signal) {
@@ -290,7 +240,7 @@ async function reconcileActiveTurn(config, turn, signal) {
290
240
  response = await fetch(activeTurnReconciliationEndpoint(config, turn), {
291
241
  body: JSON.stringify({ outcome: { kind: "running" } }),
292
242
  dispatcher: environmentProxyDispatcher,
293
- headers: { "content-type": "application/json" },
243
+ headers: { ...runtimeAuthorization(config.relayUrl), "content-type": "application/json" },
294
244
  method: "POST",
295
245
  signal: requestSignal,
296
246
  });
@@ -335,7 +285,8 @@ class DaemonSessionEventConnectionState {
335
285
  }
336
286
  class DisconnectedDaemonSessionEventState extends DaemonSessionEventConnectionState {
337
287
  disconnect = () => this;
338
- send = () => undefined;
288
+ matches = () => false;
289
+ send = () => false;
339
290
  }
340
291
  class ConnectedDaemonSessionEventState extends DaemonSessionEventConnectionState {
341
292
  #socket;
@@ -344,40 +295,207 @@ class ConnectedDaemonSessionEventState extends DaemonSessionEventConnectionState
344
295
  this.#socket = socket;
345
296
  }
346
297
  disconnect = (socket) => socket === this.#socket ? new DisconnectedDaemonSessionEventState() : this;
298
+ matches = (socket) => socket === this.#socket;
347
299
  send = (message) => {
348
- if (this.#socket.readyState === WebSocket.OPEN) {
349
- try {
350
- this.#socket.send(message);
351
- }
352
- catch {
353
- // The durable outbox retains the event for the replacement connection.
354
- }
300
+ if (this.#socket.readyState !== WebSocket.OPEN) {
301
+ return false;
302
+ }
303
+ try {
304
+ this.#socket.send(message);
305
+ return true;
306
+ }
307
+ catch {
308
+ // The durable outbox retains the event for the replacement connection.
309
+ return false;
355
310
  }
356
311
  };
357
312
  }
313
+ const SESSION_EVENT_REPLAY_WINDOW = 16;
314
+ const SESSION_EVENT_SOURCE_REPLAY_WINDOW = 4;
315
+ const SESSION_EVENT_REPLAY_BYTE_LIMIT = 32 * 1024 * 1024;
316
+ class InFlightSessionEvent {
317
+ byteLength;
318
+ sourceKey;
319
+ constructor(sourceKey, byteLength) {
320
+ this.byteLength = byteLength;
321
+ this.sourceKey = sourceKey;
322
+ }
323
+ }
324
+ function sessionEventSourceKey(envelope) {
325
+ return envelope.source.kind === "attempt"
326
+ ? `attempt:${envelope.sessionId}:${envelope.source.attemptId}`
327
+ : `runtime:${envelope.sessionId}`;
328
+ }
358
329
  class DaemonSessionEventPublisher {
359
330
  #outbox;
331
+ #inFlightEvents = new Map();
332
+ #inFlightBytes = 0;
360
333
  #connection = new DisconnectedDaemonSessionEventState();
361
334
  constructor(outbox) {
362
335
  this.#outbox = outbox;
363
336
  }
364
337
  connect = (socket) => {
365
- const connection = new ConnectedDaemonSessionEventState(socket);
366
- this.#connection = connection;
367
- for (const pending of this.#outbox.pending()) {
368
- connection.send(pending);
369
- }
338
+ this.#connection = new ConnectedDaemonSessionEventState(socket);
339
+ this.#clearInFlight();
340
+ this.#publishNextBatch();
370
341
  };
371
342
  disconnect = (socket) => {
343
+ if (!this.#connection.matches(socket)) {
344
+ return;
345
+ }
372
346
  this.#connection = this.#connection.disconnect(socket);
347
+ this.#clearInFlight();
373
348
  };
374
349
  recordAttempt = (sessionId, payload, attemptId) => {
375
- this.#connection.send(this.#outbox.enqueue(sessionId, payload, attemptId));
350
+ const kind = decodeMessageKind(payload);
351
+ if (kind === SESSION_COMPLETED_KIND || kind === SESSION_FAILED_KIND) {
352
+ this.#outbox.enqueueCritical(sessionId, payload, attemptId);
353
+ }
354
+ else {
355
+ this.#outbox.enqueue(sessionId, payload, attemptId);
356
+ }
357
+ this.#publishNextBatch();
376
358
  };
377
359
  recordRuntime = (sessionId, payload) => {
378
- this.#connection.send(this.#outbox.enqueue(sessionId, payload));
360
+ this.#outbox.enqueue(sessionId, payload);
361
+ this.#publishNextBatch();
362
+ };
363
+ acknowledge = (acknowledgement) => {
364
+ if (!this.#outbox.acknowledge(acknowledgement)) {
365
+ return;
366
+ }
367
+ this.#removeInFlight(acknowledgement.eventId);
368
+ this.#publishNextBatch();
369
+ };
370
+ quarantine = (rejection) => {
371
+ const quarantined = this.#outbox.quarantine(rejection);
372
+ if (!quarantined) {
373
+ return false;
374
+ }
375
+ this.#removeInFlight(rejection.eventId);
376
+ this.#publishNextBatch();
377
+ return true;
378
+ };
379
+ #publishNextBatch = () => {
380
+ while (this.#inFlightEvents.size < SESSION_EVENT_REPLAY_WINDOW) {
381
+ const sourceCounts = new Map();
382
+ for (const inFlight of this.#inFlightEvents.values()) {
383
+ sourceCounts.set(inFlight.sourceKey, (sourceCounts.get(inFlight.sourceKey) ?? 0) + 1);
384
+ }
385
+ const saturatedSources = [...sourceCounts.entries()]
386
+ .filter(([, count]) => count >= SESSION_EVENT_SOURCE_REPLAY_WINDOW)
387
+ .map(([sourceKey]) => sourceKey);
388
+ const batch = this.#outbox.pendingSourceHeads(SESSION_EVENT_REPLAY_WINDOW - this.#inFlightEvents.size, [...this.#inFlightEvents.keys()], saturatedSources);
389
+ let sent = 0;
390
+ for (const encoded of batch) {
391
+ const envelope = decodeSessionEventEnvelope(encoded);
392
+ const sourceKey = sessionEventSourceKey(envelope);
393
+ const sourceCount = sourceCounts.get(sourceKey) ?? 0;
394
+ if (sourceCount >= SESSION_EVENT_SOURCE_REPLAY_WINDOW) {
395
+ continue;
396
+ }
397
+ const byteLength = new TextEncoder().encode(encoded).byteLength;
398
+ if (this.#inFlightBytes + byteLength > SESSION_EVENT_REPLAY_BYTE_LIMIT) {
399
+ continue;
400
+ }
401
+ if (!this.#connection.send(encoded)) {
402
+ this.#clearInFlight();
403
+ return;
404
+ }
405
+ this.#inFlightEvents.set(envelope.eventId, new InFlightSessionEvent(sourceKey, byteLength));
406
+ this.#inFlightBytes += byteLength;
407
+ sourceCounts.set(sourceKey, sourceCount + 1);
408
+ sent += 1;
409
+ }
410
+ if (sent === 0) {
411
+ return;
412
+ }
413
+ }
414
+ };
415
+ #removeInFlight = (eventId) => {
416
+ const inFlight = this.#inFlightEvents.get(eventId);
417
+ if (inFlight === undefined) {
418
+ return;
419
+ }
420
+ this.#inFlightEvents.delete(eventId);
421
+ this.#inFlightBytes -= inFlight.byteLength;
422
+ };
423
+ #clearInFlight = () => {
424
+ this.#inFlightEvents.clear();
425
+ this.#inFlightBytes = 0;
379
426
  };
380
427
  }
428
+ class CoalescingDaemonActivityPublisher {
429
+ #pending = new Map();
430
+ #publish;
431
+ #publishedActivityIds = new Set();
432
+ #scheduled = new Map();
433
+ #timer;
434
+ constructor(timer, publish) {
435
+ this.#publish = publish;
436
+ this.#timer = timer;
437
+ }
438
+ flush = () => {
439
+ const activities = [...this.#pending.values()].sort((left, right) => left.sequence - right.sequence);
440
+ for (const task of this.#scheduled.values()) {
441
+ task.cancel();
442
+ }
443
+ this.#pending.clear();
444
+ this.#scheduled.clear();
445
+ for (const activity of activities) {
446
+ this.#publish(activity);
447
+ }
448
+ };
449
+ record = (activity) => {
450
+ if (activity.status !== "pending" && activity.status !== "running") {
451
+ this.#discardPending(activity.activityId);
452
+ this.#publishedActivityIds.add(activity.activityId);
453
+ this.#publish(activity);
454
+ return Promise.resolve();
455
+ }
456
+ if (!this.#publishedActivityIds.has(activity.activityId)) {
457
+ this.#publishedActivityIds.add(activity.activityId);
458
+ this.#publish(activity);
459
+ return Promise.resolve();
460
+ }
461
+ this.#pending.set(activity.activityId, activity);
462
+ if (!this.#scheduled.has(activity.activityId)) {
463
+ this.#scheduled.set(activity.activityId, this.#timer.schedule(() => this.#flushActivity(activity.activityId), AGENT_ACTIVITY_COALESCING_MILLISECONDS));
464
+ }
465
+ return Promise.resolve();
466
+ };
467
+ #discardPending = (activityId) => {
468
+ this.#scheduled.get(activityId)?.cancel();
469
+ this.#scheduled.delete(activityId);
470
+ this.#pending.delete(activityId);
471
+ };
472
+ #flushActivity = (activityId) => {
473
+ const activity = this.#pending.get(activityId);
474
+ this.#discardPending(activityId);
475
+ if (activity !== undefined) {
476
+ this.#publish(activity);
477
+ }
478
+ };
479
+ }
480
+ function encodeDaemonActivity(command, activity) {
481
+ return encodeAgentActivityEvent({
482
+ activityId: activity.activityId,
483
+ activityType: activity.activityType,
484
+ commandId: command.commandId,
485
+ content: activity.content,
486
+ detail: activity.detail,
487
+ input: activity.input,
488
+ kind: AGENT_ACTIVITY_KIND,
489
+ locations: activity.locations,
490
+ name: activity.name,
491
+ output: activity.output,
492
+ sequence: activity.sequence,
493
+ sessionId: command.sessionId,
494
+ status: activity.status,
495
+ title: activity.title,
496
+ toolKind: activity.toolKind,
497
+ });
498
+ }
381
499
  function reportFailure(publisher, command, code, message, attemptId) {
382
500
  publisher.recordAttempt(command.sessionId, encodeSessionFailedEvent({
383
501
  code,
@@ -534,10 +652,16 @@ async function executeSessionConfigure(publisher, socket, command, config, direc
534
652
  }));
535
653
  }
536
654
  catch (error) {
655
+ if (signal.aborted) {
656
+ signal.throwIfAborted();
657
+ }
537
658
  reportOperationFailure(socket, command.requestId, command.sessionId, error);
538
659
  }
539
660
  }
540
- async function executePrompt(outbox, publisher, command, config, directories, audit, sessions, attemptId, signal) {
661
+ async function executePrompt(outbox, publisher, command, config, directories, audit, sessions, attemptId, timer, signal) {
662
+ const activities = new CoalescingDaemonActivityPublisher(timer, (activity) => {
663
+ publisher.recordAttempt(command.sessionId, encodeDaemonActivity(command, activity), attemptId);
664
+ });
541
665
  try {
542
666
  const resolved = await resolveSessionDirectories(directories, config.bindingId, command.directoryId, command.additionalDirectoryIds, signal);
543
667
  const agent = config.catalog.find(command.agentId);
@@ -582,26 +706,7 @@ async function executePrompt(outbox, publisher, command, config, directories, au
582
706
  cwd: resolved.cwd,
583
707
  disposition: command.disposition,
584
708
  images: attachments.images,
585
- onActivity: (activity) => {
586
- publisher.recordAttempt(command.sessionId, encodeAgentActivityEvent({
587
- activityId: activity.activityId,
588
- activityType: activity.activityType,
589
- commandId: command.commandId,
590
- content: activity.content,
591
- detail: activity.detail,
592
- input: activity.input,
593
- kind: AGENT_ACTIVITY_KIND,
594
- locations: activity.locations,
595
- name: activity.name,
596
- output: activity.output,
597
- sequence: activity.sequence,
598
- sessionId: command.sessionId,
599
- status: activity.status,
600
- title: activity.title,
601
- toolKind: activity.toolKind,
602
- }), attemptId);
603
- return Promise.resolve();
604
- },
709
+ onActivity: activities.record,
605
710
  onText: (text, messageId) => {
606
711
  publisher.recordAttempt(command.sessionId, encodeAgentMessageDeltaEvent({
607
712
  commandId: command.commandId,
@@ -620,6 +725,7 @@ async function executePrompt(outbox, publisher, command, config, directories, au
620
725
  });
621
726
  stopReason = result.stopReason;
622
727
  }
728
+ activities.flush();
623
729
  publisher.recordAttempt(command.sessionId, encodeSessionCompletedEvent({
624
730
  commandId: command.commandId,
625
731
  kind: SESSION_COMPLETED_KIND,
@@ -634,6 +740,7 @@ async function executePrompt(outbox, publisher, command, config, directories, au
634
740
  });
635
741
  }
636
742
  catch (error) {
743
+ activities.flush();
637
744
  audit.record({
638
745
  commandId: command.commandId,
639
746
  event: "session_failed",
@@ -656,7 +763,10 @@ function botExecutionAgent(agent, contextHandle, socketPath) {
656
763
  MOBIUS_RELAY_URL: "",
657
764
  }, agent.adapterVersion, agent.agentVersion, agent.adapterIdentity, agent.agentIdentity, agent.promptTimeoutMilliseconds, agent.capabilities);
658
765
  }
659
- async function executeBotPrompt(outbox, publisher, command, config, directories, audit, sessions, grants, contexts, applicator, ipc, attemptId, signal) {
766
+ async function executeBotPrompt(outbox, publisher, command, config, directories, audit, sessions, grants, contexts, applicator, ipc, attemptId, timer, signal) {
767
+ const activities = new CoalescingDaemonActivityPublisher(timer, (activity) => {
768
+ publisher.recordAttempt(command.sessionId, encodeDaemonActivity(command, activity), attemptId);
769
+ });
660
770
  try {
661
771
  const deadline = Date.parse(command.executionDeadlineAt);
662
772
  const remaining = deadline - Date.now();
@@ -713,26 +823,7 @@ async function executeBotPrompt(outbox, publisher, command, config, directories,
713
823
  commandId: command.commandId,
714
824
  cwd: resolved.cwd,
715
825
  images: attachments.images,
716
- onActivity: (activity) => {
717
- publisher.recordAttempt(command.sessionId, encodeAgentActivityEvent({
718
- activityId: activity.activityId,
719
- activityType: activity.activityType,
720
- commandId: command.commandId,
721
- content: activity.content,
722
- detail: activity.detail,
723
- input: activity.input,
724
- kind: AGENT_ACTIVITY_KIND,
725
- locations: activity.locations,
726
- name: activity.name,
727
- output: activity.output,
728
- sequence: activity.sequence,
729
- sessionId: command.sessionId,
730
- status: activity.status,
731
- title: activity.title,
732
- toolKind: activity.toolKind,
733
- }), attemptId);
734
- return Promise.resolve();
735
- },
826
+ onActivity: activities.record,
736
827
  onText: (text, messageId) => {
737
828
  publisher.recordAttempt(command.sessionId, encodeAgentMessageDeltaEvent({
738
829
  commandId: command.commandId,
@@ -748,6 +839,7 @@ async function executeBotPrompt(outbox, publisher, command, config, directories,
748
839
  sessionId: command.sessionId,
749
840
  signal: executionSignal,
750
841
  });
842
+ activities.flush();
751
843
  publisher.recordAttempt(command.sessionId, encodeSessionCompletedEvent({
752
844
  commandId: command.commandId,
753
845
  kind: SESSION_COMPLETED_KIND,
@@ -767,6 +859,7 @@ async function executeBotPrompt(outbox, publisher, command, config, directories,
767
859
  }
768
860
  }
769
861
  catch (error) {
862
+ activities.flush();
770
863
  const failure = Date.parse(command.executionDeadlineAt) <= Date.now()
771
864
  ? new LocalBotAuthorizationError("turn_expired", "The Bot execution deadline has expired", {
772
865
  cause: error,
@@ -804,16 +897,16 @@ function waitForRelayRetry(signal, timer) {
804
897
  signal.addEventListener("abort", finish, { once: true });
805
898
  });
806
899
  }
807
- async function publishAgentCatalogUntilAvailable(config, signal, timer) {
900
+ async function publishAgentCatalogUntilAvailable(config, diagnostics, signal, timer) {
808
901
  try {
809
902
  await publishAgentCatalog(config, signal);
810
903
  return;
811
904
  }
812
- catch {
905
+ catch (error) {
813
906
  if (signal.aborted) {
814
907
  return;
815
908
  }
816
- console.warn("Mobius daemon is waiting to publish its Agent catalog");
909
+ diagnostics.catalogFailed(error);
817
910
  }
818
911
  while (!signal.aborted) {
819
912
  await waitForRelayRetry(signal, timer);
@@ -828,11 +921,13 @@ async function publishAgentCatalogUntilAvailable(config, signal, timer) {
828
921
  }
829
922
  }
830
923
  }
831
- async function runRelayConnection(config, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, onConnected, signal, timer) {
924
+ async function runRelayConnection(config, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, connectionEpoch, diagnostics, onConnected, signal, timer) {
832
925
  const relayUrl = new URL(config.relayUrl);
926
+ relayUrl.searchParams.set("deliveryReceipts", "1");
833
927
  relayUrl.searchParams.set("liveness", "1");
834
928
  const socket = await connectRelay(relayUrl, signal);
835
929
  const reconnectingTurns = [...activeTurns.values()];
930
+ diagnostics.connectionOpened(connectionEpoch);
836
931
  onConnected();
837
932
  const connectionController = new AbortController();
838
933
  const nativeRequests = new Map();
@@ -841,7 +936,6 @@ async function runRelayConnection(config, directories, audit, sessions, installa
841
936
  await new Promise((resolve, reject) => {
842
937
  let finished = false;
843
938
  const cleanup = () => {
844
- connectionRefresh.close();
845
939
  connectionLiveness.close();
846
940
  signal.removeEventListener("abort", stop);
847
941
  socket.removeEventListener("close", handleClose);
@@ -859,57 +953,251 @@ async function runRelayConnection(config, directories, audit, sessions, installa
859
953
  cleanup();
860
954
  resolve();
861
955
  };
862
- const rejectConnection = () => {
956
+ const rejectConnection = (failure) => {
863
957
  if (finished) {
864
958
  return;
865
959
  }
866
960
  finished = true;
867
961
  cleanup();
868
- reject(new Error("Relay WebSocket failed"));
962
+ reject(failure);
869
963
  };
870
964
  const stop = () => {
965
+ diagnostics.connectionClosed(connectionEpoch, 1000, "Daemon stopped", "daemon_stop");
871
966
  closeRelaySocket(socket, 1000, "Daemon stopped");
872
967
  resolveConnection();
873
968
  };
874
- const refreshConnection = () => {
875
- closeRelaySocket(socket, 1000, "Refreshing idle relay connection");
876
- resolveConnection();
969
+ const failAndClose = (failure, code, reason, source) => {
970
+ diagnostics.connectionFailed(connectionEpoch, failure, source);
971
+ diagnostics.connectionClosed(connectionEpoch, code, reason, source);
972
+ closeRelaySocket(socket, code, reason);
973
+ rejectConnection(failure);
974
+ };
975
+ const processingDeliveryIds = new Set();
976
+ const processDeliveredPrompt = async (delivery) => {
977
+ const command = decodeSessionPromptCommand(delivery.payload);
978
+ if (command.sessionId !== delivery.sessionId) {
979
+ throw new Error("Runtime delivery Session identity does not match its payload");
980
+ }
981
+ const active = activeTurns.get(command.commandId);
982
+ if (active !== undefined) {
983
+ await active.completion;
984
+ return;
985
+ }
986
+ const receipt = outbox.claimExecution(command.commandId, command.sessionId, delivery.payload);
987
+ if (receipt.kind === "duplicate") {
988
+ if (receipt.state === "running") {
989
+ reportFailure(publisher, command, "agent_execution_interrupted", "The local Agent execution was interrupted before it completed", receipt.attemptId);
990
+ outbox.transitionExecution(command.commandId, "failed");
991
+ return;
992
+ }
993
+ if (receipt.state !== "dispatching") {
994
+ return;
995
+ }
996
+ }
997
+ const turn = new ActiveDaemonTurn(command, receipt.attemptId);
998
+ activeTurns.set(command.commandId, turn);
999
+ if (command.disposition !== SessionPromptDisposition.Steer) {
1000
+ activeAttemptIds.set(command.sessionId, receipt.attemptId);
1001
+ }
1002
+ try {
1003
+ await executePrompt(outbox, publisher, command, config, directories, audit, sessions, receipt.attemptId, timer, turn.signal);
1004
+ }
1005
+ finally {
1006
+ activeTurns.delete(command.commandId);
1007
+ if (activeAttemptIds.get(command.sessionId) === receipt.attemptId) {
1008
+ activeAttemptIds.delete(command.sessionId);
1009
+ }
1010
+ turn.finish();
1011
+ }
1012
+ };
1013
+ const processDeliveredPayload = async (delivery) => {
1014
+ const kind = decodeMessageKind(delivery.payload);
1015
+ if (kind === SESSION_CANCEL_KIND) {
1016
+ const cancellation = decodeSessionCancelCommand(delivery.payload);
1017
+ if (cancellation.sessionId !== delivery.sessionId) {
1018
+ throw new Error("Runtime delivery Session identity does not match its payload");
1019
+ }
1020
+ const turn = activeTurns.get(cancellation.commandId);
1021
+ if (turn === undefined || !turn.matches(cancellation) || !turn.requestCancellation()) {
1022
+ return;
1023
+ }
1024
+ audit.record({
1025
+ commandId: cancellation.commandId,
1026
+ event: "session_cancellation_requested",
1027
+ sessionId: cancellation.sessionId,
1028
+ });
1029
+ await sessions.cancelPrompt(cancellation.sessionId);
1030
+ return;
1031
+ }
1032
+ if (kind === SESSION_CONFIGURE_KIND) {
1033
+ const command = decodeSessionConfigureCommand(delivery.payload);
1034
+ if (command.sessionId !== delivery.sessionId) {
1035
+ throw new Error("Runtime delivery Session identity does not match its payload");
1036
+ }
1037
+ await executeSessionConfigure(publisher, socket, command, config, directories, audit, sessions, connectionController.signal);
1038
+ return;
1039
+ }
1040
+ if (kind === SESSION_PERMISSION_RESPOND_KIND) {
1041
+ const command = decodeSessionPermissionRespondCommand(delivery.payload);
1042
+ if (command.sessionId !== delivery.sessionId) {
1043
+ throw new Error("Runtime delivery Session identity does not match its payload");
1044
+ }
1045
+ try {
1046
+ await sessions.respondPermission(command);
1047
+ audit.record({
1048
+ commandId: command.interactionId,
1049
+ event: "session_permission_resolved",
1050
+ sessionId: command.sessionId,
1051
+ });
1052
+ }
1053
+ catch (error) {
1054
+ reportOperationFailure(socket, command.interactionId, command.sessionId, error);
1055
+ }
1056
+ return;
1057
+ }
1058
+ if (kind === SESSION_ELICITATION_RESPOND_KIND) {
1059
+ const command = decodeSessionElicitationRespondCommand(delivery.payload);
1060
+ if (command.sessionId !== delivery.sessionId) {
1061
+ throw new Error("Runtime delivery Session identity does not match its payload");
1062
+ }
1063
+ try {
1064
+ await sessions.respondElicitation(command);
1065
+ audit.record({
1066
+ commandId: command.interactionId,
1067
+ event: "session_elicitation_resolved",
1068
+ sessionId: command.sessionId,
1069
+ });
1070
+ }
1071
+ catch (error) {
1072
+ reportOperationFailure(socket, command.interactionId, command.sessionId, error);
1073
+ }
1074
+ return;
1075
+ }
1076
+ if (kind === SESSION_PROMPT_KIND) {
1077
+ await processDeliveredPrompt(delivery);
1078
+ return;
1079
+ }
1080
+ throw new Error("Runtime delivery contains an unsupported protocol message");
1081
+ };
1082
+ const processRuntimeDelivery = (delivery, state) => {
1083
+ if (state === "processed" || processingDeliveryIds.has(delivery.deliveryId)) {
1084
+ return;
1085
+ }
1086
+ processingDeliveryIds.add(delivery.deliveryId);
1087
+ void processDeliveredPayload(delivery)
1088
+ .then(() => {
1089
+ outbox.markRuntimeDeliveryProcessed(delivery.deliveryId, delivery.sessionId);
1090
+ })
1091
+ .catch((error) => {
1092
+ if (connectionController.signal.aborted) {
1093
+ return;
1094
+ }
1095
+ console.error("Mobius daemon could not process a durable Runtime delivery", error);
1096
+ failAndClose(new Error("Runtime delivery processing failed", { cause: error }), 4008, "Invalid Runtime delivery", "delivery");
1097
+ })
1098
+ .finally(() => processingDeliveryIds.delete(delivery.deliveryId));
1099
+ };
1100
+ const receiveRuntimeDelivery = (delivery) => {
1101
+ const payloadKind = decodeMessageKind(delivery.payload);
1102
+ let payloadSessionId = "";
1103
+ if (payloadKind === SESSION_CANCEL_KIND) {
1104
+ payloadSessionId = decodeSessionCancelCommand(delivery.payload).sessionId;
1105
+ }
1106
+ else if (payloadKind === SESSION_CONFIGURE_KIND) {
1107
+ payloadSessionId = decodeSessionConfigureCommand(delivery.payload).sessionId;
1108
+ }
1109
+ else if (payloadKind === SESSION_PERMISSION_RESPOND_KIND) {
1110
+ payloadSessionId = decodeSessionPermissionRespondCommand(delivery.payload).sessionId;
1111
+ }
1112
+ else if (payloadKind === SESSION_ELICITATION_RESPOND_KIND) {
1113
+ payloadSessionId = decodeSessionElicitationRespondCommand(delivery.payload).sessionId;
1114
+ }
1115
+ else if (payloadKind === SESSION_PROMPT_KIND) {
1116
+ payloadSessionId = decodeSessionPromptCommand(delivery.payload).sessionId;
1117
+ }
1118
+ else {
1119
+ throw new Error("Runtime delivery contains an unsupported protocol message");
1120
+ }
1121
+ if (payloadSessionId !== delivery.sessionId) {
1122
+ throw new Error("Runtime delivery Session identity does not match its payload");
1123
+ }
1124
+ const stored = outbox.claimRuntimeDelivery(delivery);
1125
+ send(socket, encodeRuntimeDeliveryReceived({
1126
+ deliveryId: delivery.deliveryId,
1127
+ kind: RUNTIME_DELIVERY_RECEIVED_KIND,
1128
+ sessionId: delivery.sessionId,
1129
+ }));
1130
+ processRuntimeDelivery(delivery, stored.state);
1131
+ };
1132
+ const resumeRuntimeDeliveries = () => {
1133
+ for (const stored of outbox.runtimeDeliveries()) {
1134
+ const delivery = {
1135
+ deliveryId: stored.deliveryId,
1136
+ kind: RUNTIME_DELIVERY_KIND,
1137
+ payload: stored.payload,
1138
+ sessionId: stored.sessionId,
1139
+ };
1140
+ if (stored.state !== "relay_committed") {
1141
+ send(socket, encodeRuntimeDeliveryReceived({
1142
+ deliveryId: stored.deliveryId,
1143
+ kind: RUNTIME_DELIVERY_RECEIVED_KIND,
1144
+ sessionId: stored.sessionId,
1145
+ }));
1146
+ }
1147
+ processRuntimeDelivery(delivery, stored.state);
1148
+ }
877
1149
  };
878
1150
  const connectionLiveness = new RelayConnectionLiveness(timer, {
879
1151
  send: (message) => send(socket, message),
880
- unresponsive: () => {
1152
+ unresponsive: (heartbeatId, timeoutMilliseconds) => {
1153
+ diagnostics.heartbeatTimedOut(connectionEpoch, heartbeatId, timeoutMilliseconds);
1154
+ diagnostics.connectionClosed(connectionEpoch, 4001, "Relay liveness check timed out", "heartbeat_timeout");
881
1155
  closeRelaySocket(socket, 4001, "Relay liveness check timed out");
882
1156
  resolveConnection();
883
1157
  },
884
1158
  });
885
- const connectionRefresh = new RelayConnectionRefresh(timer, () => activeTurns.size === 0, refreshConnection);
886
1159
  signal.addEventListener("abort", stop, { once: true });
887
- const handleClose = () => {
1160
+ const handleClose = (event) => {
1161
+ diagnostics.connectionClosed(connectionEpoch, event.code, event.reason, "relay");
888
1162
  resolveConnection();
889
1163
  };
890
- const handleError = () => {
1164
+ const handleError = (event) => {
891
1165
  if (signal.aborted) {
892
1166
  resolveConnection();
893
1167
  return;
894
1168
  }
895
- rejectConnection();
1169
+ const cause = Reflect.get(event, "error");
1170
+ const failure = new Error("Relay WebSocket emitted an error", { cause: cause ?? event });
1171
+ diagnostics.connectionFailed(connectionEpoch, failure, "socket_error");
1172
+ diagnostics.connectionClosed(connectionEpoch, 1006, "WebSocket error", "socket_error");
1173
+ rejectConnection(failure);
896
1174
  };
897
1175
  const handleMessage = (event) => {
898
1176
  const encoded = event.data;
899
1177
  if (typeof encoded !== "string") {
900
- closeRelaySocket(socket, 4003, "Text message required");
901
- rejectConnection();
1178
+ failAndClose(new Error("Relay sent a non-text message"), 4003, "Text message required", "protocol");
902
1179
  return;
903
1180
  }
904
1181
  try {
905
1182
  const kind = decodeMessageKind(encoded);
1183
+ if (kind === RUNTIME_DELIVERY_COMMITTED_KIND) {
1184
+ const committed = decodeRuntimeDeliveryCommitted(encoded);
1185
+ if (!outbox.markRuntimeDeliveryCommitted(committed.deliveryId, committed.sessionId)) {
1186
+ console.warn("Mobius daemon ignored confirmation for an unknown Runtime delivery");
1187
+ }
1188
+ return;
1189
+ }
1190
+ if (kind === RUNTIME_DELIVERY_KIND) {
1191
+ receiveRuntimeDelivery(decodeRuntimeDelivery(encoded));
1192
+ return;
1193
+ }
906
1194
  if (kind === SESSION_EVENT_ACK_KIND) {
907
- outbox.acknowledge(decodeSessionEventAcknowledgement(encoded));
1195
+ publisher.acknowledge(decodeSessionEventAcknowledgement(encoded));
908
1196
  return;
909
1197
  }
910
1198
  if (kind === SESSION_EVENT_REJECTED_KIND) {
911
1199
  const rejection = decodeSessionEventRejection(encoded);
912
- if (outbox.quarantine(rejection)) {
1200
+ if (publisher.quarantine(rejection)) {
913
1201
  audit.record({
914
1202
  commandId: rejection.eventId,
915
1203
  details: { code: rejection.code },
@@ -1205,7 +1493,7 @@ async function runRelayConnection(config, directories, audit, sessions, installa
1205
1493
  const turn = new ActiveDaemonTurn(command, receipt.attemptId);
1206
1494
  activeTurns.set(command.commandId, turn);
1207
1495
  activeAttemptIds.set(command.sessionId, receipt.attemptId);
1208
- void executeBotPrompt(outbox, publisher, command, config, directories, audit, sessions, grants, contexts, applicator, ipc, receipt.attemptId, turn.signal)
1496
+ void executeBotPrompt(outbox, publisher, command, config, directories, audit, sessions, grants, contexts, applicator, ipc, receipt.attemptId, timer, turn.signal)
1209
1497
  .catch((error) => {
1210
1498
  console.error("Mobius daemon Bot turn failed", error);
1211
1499
  })
@@ -1215,7 +1503,6 @@ async function runRelayConnection(config, directories, audit, sessions, installa
1215
1503
  activeAttemptIds.delete(command.sessionId);
1216
1504
  }
1217
1505
  turn.finish();
1218
- connectionRefresh.afterTurnFinished();
1219
1506
  });
1220
1507
  return;
1221
1508
  }
@@ -1235,7 +1522,7 @@ async function runRelayConnection(config, directories, audit, sessions, installa
1235
1522
  if (command.disposition !== SessionPromptDisposition.Steer) {
1236
1523
  activeAttemptIds.set(command.sessionId, receipt.attemptId);
1237
1524
  }
1238
- void executePrompt(outbox, publisher, command, config, directories, audit, sessions, receipt.attemptId, turn.signal)
1525
+ void executePrompt(outbox, publisher, command, config, directories, audit, sessions, receipt.attemptId, timer, turn.signal)
1239
1526
  .catch((error) => {
1240
1527
  console.error("Mobius daemon turn failed", error);
1241
1528
  })
@@ -1245,13 +1532,15 @@ async function runRelayConnection(config, directories, audit, sessions, installa
1245
1532
  activeAttemptIds.delete(command.sessionId);
1246
1533
  }
1247
1534
  turn.finish();
1248
- connectionRefresh.afterTurnFinished();
1249
1535
  });
1250
1536
  }
1251
1537
  catch (error) {
1252
1538
  console.error("Mobius daemon rejected a message", error);
1253
- closeRelaySocket(socket, 4008, "Invalid protocol message");
1254
- rejectConnection();
1539
+ if (error instanceof RuntimeDeliveryInboxCapacityError) {
1540
+ failAndClose(error, 4013, "Runtime delivery queue full", "delivery_backpressure");
1541
+ return;
1542
+ }
1543
+ failAndClose(new Error("Relay sent an invalid protocol message", { cause: error }), 4008, "Invalid protocol message", "protocol");
1255
1544
  }
1256
1545
  };
1257
1546
  socket.addEventListener("close", handleClose);
@@ -1262,14 +1551,13 @@ async function runRelayConnection(config, directories, audit, sessions, installa
1262
1551
  return;
1263
1552
  }
1264
1553
  publisher.connect(socket);
1265
- void Promise.all(reconnectingTurns.map((turn) => turn.completion)).then(connectionRefresh.afterTurnFinished);
1554
+ resumeRuntimeDeliveries();
1266
1555
  }, (error) => {
1267
1556
  if (finished) {
1268
1557
  return;
1269
1558
  }
1270
1559
  console.error("Mobius daemon could not reconcile active Agent turns", error);
1271
- closeRelaySocket(socket, ACTIVE_TURN_RECONCILIATION_CLOSE_CODE, "Active Agent turn reconciliation failed");
1272
- rejectConnection();
1560
+ failAndClose(new Error("Active Agent turn reconciliation failed", { cause: error }), ACTIVE_TURN_RECONCILIATION_CLOSE_CODE, "Active Agent turn reconciliation failed", "reconciliation");
1273
1561
  });
1274
1562
  if (signal.aborted) {
1275
1563
  stop();
@@ -1313,9 +1601,10 @@ export async function runDaemon(config, audit, signal, timer = systemDaemonTimer
1313
1601
  const sessions = new AcpSessionManager(new DaemonAcpObserver(recordSessionEvent), sessionStore);
1314
1602
  const directories = new WorkspaceDirectoryService(config.bindingId, config.defaultWorkspace);
1315
1603
  const installationObserver = new DaemonInstallationObserver();
1604
+ const diagnostics = new RelayConnectionDiagnostics(audit, config.channelId, config.relayUrl);
1316
1605
  const nativeSessions = new NativeSessionService(config, directories, sessions, audit);
1317
1606
  const installations = new AgentInstallationManager(specifications, installationExecutor, installationObserver, installationAudit);
1318
- let retryAnnounced = false;
1607
+ let connectionEpoch = 0;
1319
1608
  let connectionAnnounced = false;
1320
1609
  const announceConnection = () => {
1321
1610
  if (connectionAnnounced) {
@@ -1326,22 +1615,19 @@ export async function runDaemon(config, audit, signal, timer = systemDaemonTimer
1326
1615
  process.stdout.write("Mobius daemon connected to the relay.\n\n");
1327
1616
  process.stdout.write(`${styleText(["bold", "yellow"], `Reconnect this channel later:\n ${reconnectCommand}`)}\n\n`);
1328
1617
  };
1329
- const catalogPublication = publishAgentCatalogUntilAvailable(config, signal, timer);
1618
+ const catalogPublication = publishAgentCatalogUntilAvailable(config, diagnostics, signal, timer);
1330
1619
  try {
1331
1620
  await ipc.listen(signal);
1332
1621
  while (!signal.aborted) {
1622
+ connectionEpoch += 1;
1333
1623
  try {
1334
- await runRelayConnection(config, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, announceConnection, signal, timer);
1335
- retryAnnounced = false;
1624
+ await runRelayConnection(config, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, connectionEpoch, diagnostics, announceConnection, signal, timer);
1336
1625
  }
1337
- catch {
1626
+ catch (error) {
1338
1627
  if (signal.aborted) {
1339
1628
  return;
1340
1629
  }
1341
- if (!retryAnnounced) {
1342
- console.warn("Mobius daemon is waiting for the relay");
1343
- retryAnnounced = true;
1344
- }
1630
+ diagnostics.connectionFailed(connectionEpoch, error, "connection");
1345
1631
  }
1346
1632
  await waitForRelayRetry(signal, timer);
1347
1633
  }