@moxt-ai/mobius 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/agent-catalog/publish-agent-catalog.d.ts.map +1 -1
  2. package/dist/agent-catalog/publish-agent-catalog.js +12 -7
  3. package/dist/agent-catalog/publish-agent-catalog.js.map +1 -1
  4. package/dist/cli.js +685 -290
  5. package/dist/cli.js.map +4 -4
  6. package/dist/config.d.ts +1 -1
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +3 -3
  9. package/dist/config.js.map +1 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +746 -254
  13. package/dist/index.js.map +4 -4
  14. package/dist/network/environment-proxy.d.ts +1 -0
  15. package/dist/network/environment-proxy.d.ts.map +1 -1
  16. package/dist/network/environment-proxy.js +47 -0
  17. package/dist/network/environment-proxy.js.map +1 -1
  18. package/dist/pairing/pair-local-machine.d.ts +8 -0
  19. package/dist/pairing/pair-local-machine.d.ts.map +1 -1
  20. package/dist/pairing/pair-local-machine.js +64 -37
  21. package/dist/pairing/pair-local-machine.js.map +1 -1
  22. package/dist/prompt-artifact-resolver.d.ts.map +1 -1
  23. package/dist/prompt-artifact-resolver.js +11 -3
  24. package/dist/prompt-artifact-resolver.js.map +1 -1
  25. package/dist/relay-connection/connect.d.ts.map +1 -1
  26. package/dist/relay-connection/connect.js +25 -9
  27. package/dist/relay-connection/connect.js.map +1 -1
  28. package/dist/relay-connection/diagnostics.d.ts +11 -0
  29. package/dist/relay-connection/diagnostics.d.ts.map +1 -0
  30. package/dist/relay-connection/diagnostics.js +122 -0
  31. package/dist/relay-connection/diagnostics.js.map +1 -0
  32. package/dist/relay-connection/liveness.d.ts +1 -1
  33. package/dist/relay-connection/liveness.d.ts.map +1 -1
  34. package/dist/relay-connection/liveness.js +2 -1
  35. package/dist/relay-connection/liveness.js.map +1 -1
  36. package/dist/relay-connection/session-event-outbox.d.ts +2 -1
  37. package/dist/relay-connection/session-event-outbox.d.ts.map +1 -1
  38. package/dist/relay-connection/session-event-outbox.js +48 -8
  39. package/dist/relay-connection/session-event-outbox.js.map +1 -1
  40. package/dist/run-daemon.d.ts.map +1 -1
  41. package/dist/run-daemon.js +193 -149
  42. package/dist/run-daemon.js.map +1 -1
  43. package/dist/runtime.d.ts +23 -0
  44. package/dist/runtime.d.ts.map +1 -0
  45. package/dist/runtime.js +31339 -0
  46. package/dist/runtime.js.map +7 -0
  47. package/package.json +6 -2
@@ -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, decodeRuntimeHeartbeatAckEvent, decodeRuntimeLivenessReadyEvent, decodeSessionCancelCommand, decodeSessionConfigureCommand, decodeSessionElicitationRespondCommand, decodeSessionEventAcknowledgement, decodeSessionEventEnvelope, 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";
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,13 @@ 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
25
  import { SessionEventOutbox } from "./relay-connection/session-event-outbox.js";
25
26
  import { systemDaemonTimer } from "./relay-connection/timer.js";
26
27
  import { readStateDirectory } from "./storage/state-directory.js";
27
28
  const RELAY_RECONNECT_DELAY_MILLISECONDS = 500;
28
- const RELAY_CONNECTION_REFRESH_MILLISECONDS = 20 * 60 * 1_000;
29
+ const AGENT_ACTIVITY_COALESCING_MILLISECONDS = 100;
29
30
  const ACTIVE_TURN_RECONCILIATION_TIMEOUT_MILLISECONDS = 20_000;
30
31
  const ACTIVE_TURN_RECONCILIATION_CLOSE_CODE = 4009;
31
32
  function botIpcSocketPath() {
@@ -34,56 +35,6 @@ function botIpcSocketPath() {
34
35
  }
35
36
  return join("/tmp", `mobius-bot-${randomUUID()}.sock`);
36
37
  }
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
38
  class DaemonAcpObserver {
88
39
  #record;
89
40
  constructor(record) {
@@ -266,10 +217,18 @@ class ActiveTurnReconciliationError extends Error {
266
217
  function activeTurnReconciliationEndpoint(config, turn) {
267
218
  const endpoint = new URL(config.relayUrl);
268
219
  endpoint.protocol = endpoint.protocol === "wss:" ? "https:" : "http:";
269
- if (!endpoint.pathname.endsWith("/daemon")) {
270
- throw new ActiveTurnReconciliationError("The Relay URL is invalid");
220
+ let channelPath = "";
221
+ if (endpoint.pathname.endsWith("/daemon")) {
222
+ channelPath = endpoint.pathname.slice(0, -"/daemon".length);
223
+ }
224
+ else {
225
+ const runtimePath = "/v1/runtimes/connect";
226
+ if (!endpoint.pathname.endsWith(runtimePath)) {
227
+ throw new ActiveTurnReconciliationError("The Relay URL is invalid");
228
+ }
229
+ channelPath = `${endpoint.pathname.slice(0, -runtimePath.length)}/channels/${encodeURIComponent(config.channelId)}`;
271
230
  }
272
- endpoint.pathname = `${endpoint.pathname.slice(0, -"/daemon".length)}/sessions/${encodeURIComponent(turn.command.sessionId)}/attempts/${encodeURIComponent(turn.attemptId)}/reconciliation`;
231
+ endpoint.pathname = `${channelPath}/sessions/${encodeURIComponent(turn.command.sessionId)}/attempts/${encodeURIComponent(turn.attemptId)}/reconciliation`;
273
232
  endpoint.searchParams.delete("liveness");
274
233
  return endpoint;
275
234
  }
@@ -327,7 +286,8 @@ class DaemonSessionEventConnectionState {
327
286
  }
328
287
  class DisconnectedDaemonSessionEventState extends DaemonSessionEventConnectionState {
329
288
  disconnect = () => this;
330
- send = () => undefined;
289
+ matches = () => false;
290
+ send = () => false;
331
291
  }
332
292
  class ConnectedDaemonSessionEventState extends DaemonSessionEventConnectionState {
333
293
  #socket;
@@ -336,40 +296,151 @@ class ConnectedDaemonSessionEventState extends DaemonSessionEventConnectionState
336
296
  this.#socket = socket;
337
297
  }
338
298
  disconnect = (socket) => socket === this.#socket ? new DisconnectedDaemonSessionEventState() : this;
299
+ matches = (socket) => socket === this.#socket;
339
300
  send = (message) => {
340
- if (this.#socket.readyState === WebSocket.OPEN) {
341
- try {
342
- this.#socket.send(message);
343
- }
344
- catch {
345
- // The durable outbox retains the event for the replacement connection.
346
- }
301
+ if (this.#socket.readyState !== WebSocket.OPEN) {
302
+ return false;
303
+ }
304
+ try {
305
+ this.#socket.send(message);
306
+ return true;
307
+ }
308
+ catch {
309
+ // The durable outbox retains the event for the replacement connection.
310
+ return false;
347
311
  }
348
312
  };
349
313
  }
314
+ const SESSION_EVENT_REPLAY_WINDOW = 4;
350
315
  class DaemonSessionEventPublisher {
351
316
  #outbox;
317
+ #inFlightEventIds = new Set();
352
318
  #connection = new DisconnectedDaemonSessionEventState();
353
319
  constructor(outbox) {
354
320
  this.#outbox = outbox;
355
321
  }
356
322
  connect = (socket) => {
357
- const connection = new ConnectedDaemonSessionEventState(socket);
358
- this.#connection = connection;
359
- for (const pending of this.#outbox.pending()) {
360
- connection.send(pending);
361
- }
323
+ this.#connection = new ConnectedDaemonSessionEventState(socket);
324
+ this.#inFlightEventIds.clear();
325
+ this.#publishNextBatch();
362
326
  };
363
327
  disconnect = (socket) => {
328
+ if (!this.#connection.matches(socket)) {
329
+ return;
330
+ }
364
331
  this.#connection = this.#connection.disconnect(socket);
332
+ this.#inFlightEventIds.clear();
365
333
  };
366
334
  recordAttempt = (sessionId, payload, attemptId) => {
367
- this.#connection.send(this.#outbox.enqueue(sessionId, payload, attemptId));
335
+ this.#outbox.enqueue(sessionId, payload, attemptId);
336
+ this.#publishNextBatch();
368
337
  };
369
338
  recordRuntime = (sessionId, payload) => {
370
- this.#connection.send(this.#outbox.enqueue(sessionId, payload));
339
+ this.#outbox.enqueue(sessionId, payload);
340
+ this.#publishNextBatch();
341
+ };
342
+ acknowledge = (acknowledgement) => {
343
+ if (!this.#outbox.acknowledge(acknowledgement)) {
344
+ return;
345
+ }
346
+ this.#inFlightEventIds.delete(acknowledgement.eventId);
347
+ this.#publishNextBatch();
348
+ };
349
+ quarantine = (rejection) => {
350
+ const quarantined = this.#outbox.quarantine(rejection);
351
+ if (!quarantined) {
352
+ return false;
353
+ }
354
+ this.#inFlightEventIds.delete(rejection.eventId);
355
+ this.#publishNextBatch();
356
+ return true;
357
+ };
358
+ #publishNextBatch = () => {
359
+ if (this.#inFlightEventIds.size > 0) {
360
+ return;
361
+ }
362
+ const batch = this.#outbox.pendingBatch(SESSION_EVENT_REPLAY_WINDOW);
363
+ for (const encoded of batch) {
364
+ const envelope = decodeSessionEventEnvelope(encoded);
365
+ if (!this.#connection.send(encoded)) {
366
+ this.#inFlightEventIds.clear();
367
+ return;
368
+ }
369
+ this.#inFlightEventIds.add(envelope.eventId);
370
+ }
371
+ };
372
+ }
373
+ class CoalescingDaemonActivityPublisher {
374
+ #pending = new Map();
375
+ #publish;
376
+ #publishedActivityIds = new Set();
377
+ #scheduled = new Map();
378
+ #timer;
379
+ constructor(timer, publish) {
380
+ this.#publish = publish;
381
+ this.#timer = timer;
382
+ }
383
+ flush = () => {
384
+ const activities = [...this.#pending.values()].sort((left, right) => left.sequence - right.sequence);
385
+ for (const task of this.#scheduled.values()) {
386
+ task.cancel();
387
+ }
388
+ this.#pending.clear();
389
+ this.#scheduled.clear();
390
+ for (const activity of activities) {
391
+ this.#publish(activity);
392
+ }
393
+ };
394
+ record = (activity) => {
395
+ if (activity.status !== "pending" && activity.status !== "running") {
396
+ this.#discardPending(activity.activityId);
397
+ this.#publishedActivityIds.add(activity.activityId);
398
+ this.#publish(activity);
399
+ return Promise.resolve();
400
+ }
401
+ if (!this.#publishedActivityIds.has(activity.activityId)) {
402
+ this.#publishedActivityIds.add(activity.activityId);
403
+ this.#publish(activity);
404
+ return Promise.resolve();
405
+ }
406
+ this.#pending.set(activity.activityId, activity);
407
+ if (!this.#scheduled.has(activity.activityId)) {
408
+ this.#scheduled.set(activity.activityId, this.#timer.schedule(() => this.#flushActivity(activity.activityId), AGENT_ACTIVITY_COALESCING_MILLISECONDS));
409
+ }
410
+ return Promise.resolve();
411
+ };
412
+ #discardPending = (activityId) => {
413
+ this.#scheduled.get(activityId)?.cancel();
414
+ this.#scheduled.delete(activityId);
415
+ this.#pending.delete(activityId);
416
+ };
417
+ #flushActivity = (activityId) => {
418
+ const activity = this.#pending.get(activityId);
419
+ this.#discardPending(activityId);
420
+ if (activity !== undefined) {
421
+ this.#publish(activity);
422
+ }
371
423
  };
372
424
  }
425
+ function encodeDaemonActivity(command, activity) {
426
+ return encodeAgentActivityEvent({
427
+ activityId: activity.activityId,
428
+ activityType: activity.activityType,
429
+ commandId: command.commandId,
430
+ content: activity.content,
431
+ detail: activity.detail,
432
+ input: activity.input,
433
+ kind: AGENT_ACTIVITY_KIND,
434
+ locations: activity.locations,
435
+ name: activity.name,
436
+ output: activity.output,
437
+ sequence: activity.sequence,
438
+ sessionId: command.sessionId,
439
+ status: activity.status,
440
+ title: activity.title,
441
+ toolKind: activity.toolKind,
442
+ });
443
+ }
373
444
  function reportFailure(publisher, command, code, message, attemptId) {
374
445
  publisher.recordAttempt(command.sessionId, encodeSessionFailedEvent({
375
446
  code,
@@ -529,7 +600,10 @@ async function executeSessionConfigure(publisher, socket, command, config, direc
529
600
  reportOperationFailure(socket, command.requestId, command.sessionId, error);
530
601
  }
531
602
  }
532
- async function executePrompt(outbox, publisher, command, config, directories, audit, sessions, attemptId, signal) {
603
+ async function executePrompt(outbox, publisher, command, config, directories, audit, sessions, attemptId, timer, signal) {
604
+ const activities = new CoalescingDaemonActivityPublisher(timer, (activity) => {
605
+ publisher.recordAttempt(command.sessionId, encodeDaemonActivity(command, activity), attemptId);
606
+ });
533
607
  try {
534
608
  const resolved = await resolveSessionDirectories(directories, config.bindingId, command.directoryId, command.additionalDirectoryIds, signal);
535
609
  const agent = config.catalog.find(command.agentId);
@@ -574,26 +648,7 @@ async function executePrompt(outbox, publisher, command, config, directories, au
574
648
  cwd: resolved.cwd,
575
649
  disposition: command.disposition,
576
650
  images: attachments.images,
577
- onActivity: (activity) => {
578
- publisher.recordAttempt(command.sessionId, encodeAgentActivityEvent({
579
- activityId: activity.activityId,
580
- activityType: activity.activityType,
581
- commandId: command.commandId,
582
- content: activity.content,
583
- detail: activity.detail,
584
- input: activity.input,
585
- kind: AGENT_ACTIVITY_KIND,
586
- locations: activity.locations,
587
- name: activity.name,
588
- output: activity.output,
589
- sequence: activity.sequence,
590
- sessionId: command.sessionId,
591
- status: activity.status,
592
- title: activity.title,
593
- toolKind: activity.toolKind,
594
- }), attemptId);
595
- return Promise.resolve();
596
- },
651
+ onActivity: activities.record,
597
652
  onText: (text, messageId) => {
598
653
  publisher.recordAttempt(command.sessionId, encodeAgentMessageDeltaEvent({
599
654
  commandId: command.commandId,
@@ -612,6 +667,7 @@ async function executePrompt(outbox, publisher, command, config, directories, au
612
667
  });
613
668
  stopReason = result.stopReason;
614
669
  }
670
+ activities.flush();
615
671
  publisher.recordAttempt(command.sessionId, encodeSessionCompletedEvent({
616
672
  commandId: command.commandId,
617
673
  kind: SESSION_COMPLETED_KIND,
@@ -626,6 +682,7 @@ async function executePrompt(outbox, publisher, command, config, directories, au
626
682
  });
627
683
  }
628
684
  catch (error) {
685
+ activities.flush();
629
686
  audit.record({
630
687
  commandId: command.commandId,
631
688
  event: "session_failed",
@@ -648,7 +705,10 @@ function botExecutionAgent(agent, contextHandle, socketPath) {
648
705
  MOBIUS_RELAY_URL: "",
649
706
  }, agent.adapterVersion, agent.agentVersion, agent.adapterIdentity, agent.agentIdentity, agent.promptTimeoutMilliseconds, agent.capabilities);
650
707
  }
651
- async function executeBotPrompt(outbox, publisher, command, config, directories, audit, sessions, grants, contexts, applicator, ipc, attemptId, signal) {
708
+ async function executeBotPrompt(outbox, publisher, command, config, directories, audit, sessions, grants, contexts, applicator, ipc, attemptId, timer, signal) {
709
+ const activities = new CoalescingDaemonActivityPublisher(timer, (activity) => {
710
+ publisher.recordAttempt(command.sessionId, encodeDaemonActivity(command, activity), attemptId);
711
+ });
652
712
  try {
653
713
  const deadline = Date.parse(command.executionDeadlineAt);
654
714
  const remaining = deadline - Date.now();
@@ -705,26 +765,7 @@ async function executeBotPrompt(outbox, publisher, command, config, directories,
705
765
  commandId: command.commandId,
706
766
  cwd: resolved.cwd,
707
767
  images: attachments.images,
708
- onActivity: (activity) => {
709
- publisher.recordAttempt(command.sessionId, encodeAgentActivityEvent({
710
- activityId: activity.activityId,
711
- activityType: activity.activityType,
712
- commandId: command.commandId,
713
- content: activity.content,
714
- detail: activity.detail,
715
- input: activity.input,
716
- kind: AGENT_ACTIVITY_KIND,
717
- locations: activity.locations,
718
- name: activity.name,
719
- output: activity.output,
720
- sequence: activity.sequence,
721
- sessionId: command.sessionId,
722
- status: activity.status,
723
- title: activity.title,
724
- toolKind: activity.toolKind,
725
- }), attemptId);
726
- return Promise.resolve();
727
- },
768
+ onActivity: activities.record,
728
769
  onText: (text, messageId) => {
729
770
  publisher.recordAttempt(command.sessionId, encodeAgentMessageDeltaEvent({
730
771
  commandId: command.commandId,
@@ -740,6 +781,7 @@ async function executeBotPrompt(outbox, publisher, command, config, directories,
740
781
  sessionId: command.sessionId,
741
782
  signal: executionSignal,
742
783
  });
784
+ activities.flush();
743
785
  publisher.recordAttempt(command.sessionId, encodeSessionCompletedEvent({
744
786
  commandId: command.commandId,
745
787
  kind: SESSION_COMPLETED_KIND,
@@ -759,6 +801,7 @@ async function executeBotPrompt(outbox, publisher, command, config, directories,
759
801
  }
760
802
  }
761
803
  catch (error) {
804
+ activities.flush();
762
805
  const failure = Date.parse(command.executionDeadlineAt) <= Date.now()
763
806
  ? new LocalBotAuthorizationError("turn_expired", "The Bot execution deadline has expired", {
764
807
  cause: error,
@@ -796,16 +839,16 @@ function waitForRelayRetry(signal, timer) {
796
839
  signal.addEventListener("abort", finish, { once: true });
797
840
  });
798
841
  }
799
- async function publishAgentCatalogUntilAvailable(config, signal, timer) {
842
+ async function publishAgentCatalogUntilAvailable(config, diagnostics, signal, timer) {
800
843
  try {
801
844
  await publishAgentCatalog(config, signal);
802
845
  return;
803
846
  }
804
- catch {
847
+ catch (error) {
805
848
  if (signal.aborted) {
806
849
  return;
807
850
  }
808
- console.warn("Mobius daemon is waiting to publish its Agent catalog");
851
+ diagnostics.catalogFailed(error);
809
852
  }
810
853
  while (!signal.aborted) {
811
854
  await waitForRelayRetry(signal, timer);
@@ -820,11 +863,12 @@ async function publishAgentCatalogUntilAvailable(config, signal, timer) {
820
863
  }
821
864
  }
822
865
  }
823
- async function runRelayConnection(config, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, onConnected, signal, timer) {
866
+ async function runRelayConnection(config, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, connectionEpoch, diagnostics, onConnected, signal, timer) {
824
867
  const relayUrl = new URL(config.relayUrl);
825
868
  relayUrl.searchParams.set("liveness", "1");
826
869
  const socket = await connectRelay(relayUrl, signal);
827
870
  const reconnectingTurns = [...activeTurns.values()];
871
+ diagnostics.connectionOpened(connectionEpoch);
828
872
  onConnected();
829
873
  const connectionController = new AbortController();
830
874
  const nativeRequests = new Map();
@@ -833,7 +877,6 @@ async function runRelayConnection(config, directories, audit, sessions, installa
833
877
  await new Promise((resolve, reject) => {
834
878
  let finished = false;
835
879
  const cleanup = () => {
836
- connectionRefresh.close();
837
880
  connectionLiveness.close();
838
881
  signal.removeEventListener("abort", stop);
839
882
  socket.removeEventListener("close", handleClose);
@@ -851,57 +894,65 @@ async function runRelayConnection(config, directories, audit, sessions, installa
851
894
  cleanup();
852
895
  resolve();
853
896
  };
854
- const rejectConnection = () => {
897
+ const rejectConnection = (failure) => {
855
898
  if (finished) {
856
899
  return;
857
900
  }
858
901
  finished = true;
859
902
  cleanup();
860
- reject(new Error("Relay WebSocket failed"));
903
+ reject(failure);
861
904
  };
862
905
  const stop = () => {
906
+ diagnostics.connectionClosed(connectionEpoch, 1000, "Daemon stopped", "daemon_stop");
863
907
  closeRelaySocket(socket, 1000, "Daemon stopped");
864
908
  resolveConnection();
865
909
  };
866
- const refreshConnection = () => {
867
- closeRelaySocket(socket, 1000, "Refreshing idle relay connection");
868
- resolveConnection();
910
+ const failAndClose = (failure, code, reason, source) => {
911
+ diagnostics.connectionFailed(connectionEpoch, failure, source);
912
+ diagnostics.connectionClosed(connectionEpoch, code, reason, source);
913
+ closeRelaySocket(socket, code, reason);
914
+ rejectConnection(failure);
869
915
  };
870
916
  const connectionLiveness = new RelayConnectionLiveness(timer, {
871
917
  send: (message) => send(socket, message),
872
- unresponsive: () => {
918
+ unresponsive: (heartbeatId, timeoutMilliseconds) => {
919
+ diagnostics.heartbeatTimedOut(connectionEpoch, heartbeatId, timeoutMilliseconds);
920
+ diagnostics.connectionClosed(connectionEpoch, 4001, "Relay liveness check timed out", "heartbeat_timeout");
873
921
  closeRelaySocket(socket, 4001, "Relay liveness check timed out");
874
922
  resolveConnection();
875
923
  },
876
924
  });
877
- const connectionRefresh = new RelayConnectionRefresh(timer, () => activeTurns.size === 0, refreshConnection);
878
925
  signal.addEventListener("abort", stop, { once: true });
879
- const handleClose = () => {
926
+ const handleClose = (event) => {
927
+ diagnostics.connectionClosed(connectionEpoch, event.code, event.reason, "relay");
880
928
  resolveConnection();
881
929
  };
882
- const handleError = () => {
930
+ const handleError = (event) => {
883
931
  if (signal.aborted) {
884
932
  resolveConnection();
885
933
  return;
886
934
  }
887
- rejectConnection();
935
+ const cause = Reflect.get(event, "error");
936
+ const failure = new Error("Relay WebSocket emitted an error", { cause: cause ?? event });
937
+ diagnostics.connectionFailed(connectionEpoch, failure, "socket_error");
938
+ diagnostics.connectionClosed(connectionEpoch, 1006, "WebSocket error", "socket_error");
939
+ rejectConnection(failure);
888
940
  };
889
941
  const handleMessage = (event) => {
890
942
  const encoded = event.data;
891
943
  if (typeof encoded !== "string") {
892
- closeRelaySocket(socket, 4003, "Text message required");
893
- rejectConnection();
944
+ failAndClose(new Error("Relay sent a non-text message"), 4003, "Text message required", "protocol");
894
945
  return;
895
946
  }
896
947
  try {
897
948
  const kind = decodeMessageKind(encoded);
898
949
  if (kind === SESSION_EVENT_ACK_KIND) {
899
- outbox.acknowledge(decodeSessionEventAcknowledgement(encoded));
950
+ publisher.acknowledge(decodeSessionEventAcknowledgement(encoded));
900
951
  return;
901
952
  }
902
953
  if (kind === SESSION_EVENT_REJECTED_KIND) {
903
954
  const rejection = decodeSessionEventRejection(encoded);
904
- if (outbox.quarantine(rejection)) {
955
+ if (publisher.quarantine(rejection)) {
905
956
  audit.record({
906
957
  commandId: rejection.eventId,
907
958
  details: { code: rejection.code },
@@ -1197,7 +1248,7 @@ async function runRelayConnection(config, directories, audit, sessions, installa
1197
1248
  const turn = new ActiveDaemonTurn(command, receipt.attemptId);
1198
1249
  activeTurns.set(command.commandId, turn);
1199
1250
  activeAttemptIds.set(command.sessionId, receipt.attemptId);
1200
- void executeBotPrompt(outbox, publisher, command, config, directories, audit, sessions, grants, contexts, applicator, ipc, receipt.attemptId, turn.signal)
1251
+ void executeBotPrompt(outbox, publisher, command, config, directories, audit, sessions, grants, contexts, applicator, ipc, receipt.attemptId, timer, turn.signal)
1201
1252
  .catch((error) => {
1202
1253
  console.error("Mobius daemon Bot turn failed", error);
1203
1254
  })
@@ -1207,7 +1258,6 @@ async function runRelayConnection(config, directories, audit, sessions, installa
1207
1258
  activeAttemptIds.delete(command.sessionId);
1208
1259
  }
1209
1260
  turn.finish();
1210
- connectionRefresh.afterTurnFinished();
1211
1261
  });
1212
1262
  return;
1213
1263
  }
@@ -1227,7 +1277,7 @@ async function runRelayConnection(config, directories, audit, sessions, installa
1227
1277
  if (command.disposition !== SessionPromptDisposition.Steer) {
1228
1278
  activeAttemptIds.set(command.sessionId, receipt.attemptId);
1229
1279
  }
1230
- void executePrompt(outbox, publisher, command, config, directories, audit, sessions, receipt.attemptId, turn.signal)
1280
+ void executePrompt(outbox, publisher, command, config, directories, audit, sessions, receipt.attemptId, timer, turn.signal)
1231
1281
  .catch((error) => {
1232
1282
  console.error("Mobius daemon turn failed", error);
1233
1283
  })
@@ -1237,13 +1287,11 @@ async function runRelayConnection(config, directories, audit, sessions, installa
1237
1287
  activeAttemptIds.delete(command.sessionId);
1238
1288
  }
1239
1289
  turn.finish();
1240
- connectionRefresh.afterTurnFinished();
1241
1290
  });
1242
1291
  }
1243
1292
  catch (error) {
1244
1293
  console.error("Mobius daemon rejected a message", error);
1245
- closeRelaySocket(socket, 4008, "Invalid protocol message");
1246
- rejectConnection();
1294
+ failAndClose(new Error("Relay sent an invalid protocol message", { cause: error }), 4008, "Invalid protocol message", "protocol");
1247
1295
  }
1248
1296
  };
1249
1297
  socket.addEventListener("close", handleClose);
@@ -1254,14 +1302,12 @@ async function runRelayConnection(config, directories, audit, sessions, installa
1254
1302
  return;
1255
1303
  }
1256
1304
  publisher.connect(socket);
1257
- void Promise.all(reconnectingTurns.map((turn) => turn.completion)).then(connectionRefresh.afterTurnFinished);
1258
1305
  }, (error) => {
1259
1306
  if (finished) {
1260
1307
  return;
1261
1308
  }
1262
1309
  console.error("Mobius daemon could not reconcile active Agent turns", error);
1263
- closeRelaySocket(socket, ACTIVE_TURN_RECONCILIATION_CLOSE_CODE, "Active Agent turn reconciliation failed");
1264
- rejectConnection();
1310
+ failAndClose(new Error("Active Agent turn reconciliation failed", { cause: error }), ACTIVE_TURN_RECONCILIATION_CLOSE_CODE, "Active Agent turn reconciliation failed", "reconciliation");
1265
1311
  });
1266
1312
  if (signal.aborted) {
1267
1313
  stop();
@@ -1305,9 +1351,10 @@ export async function runDaemon(config, audit, signal, timer = systemDaemonTimer
1305
1351
  const sessions = new AcpSessionManager(new DaemonAcpObserver(recordSessionEvent), sessionStore);
1306
1352
  const directories = new WorkspaceDirectoryService(config.bindingId, config.defaultWorkspace);
1307
1353
  const installationObserver = new DaemonInstallationObserver();
1354
+ const diagnostics = new RelayConnectionDiagnostics(audit, config.channelId, config.relayUrl);
1308
1355
  const nativeSessions = new NativeSessionService(config, directories, sessions, audit);
1309
1356
  const installations = new AgentInstallationManager(specifications, installationExecutor, installationObserver, installationAudit);
1310
- let retryAnnounced = false;
1357
+ let connectionEpoch = 0;
1311
1358
  let connectionAnnounced = false;
1312
1359
  const announceConnection = () => {
1313
1360
  if (connectionAnnounced) {
@@ -1318,22 +1365,19 @@ export async function runDaemon(config, audit, signal, timer = systemDaemonTimer
1318
1365
  process.stdout.write("Mobius daemon connected to the relay.\n\n");
1319
1366
  process.stdout.write(`${styleText(["bold", "yellow"], `Reconnect this channel later:\n ${reconnectCommand}`)}\n\n`);
1320
1367
  };
1321
- const catalogPublication = publishAgentCatalogUntilAvailable(config, signal, timer);
1368
+ const catalogPublication = publishAgentCatalogUntilAvailable(config, diagnostics, signal, timer);
1322
1369
  try {
1323
1370
  await ipc.listen(signal);
1324
1371
  while (!signal.aborted) {
1372
+ connectionEpoch += 1;
1325
1373
  try {
1326
- await runRelayConnection(config, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, announceConnection, signal, timer);
1327
- retryAnnounced = false;
1374
+ await runRelayConnection(config, directories, audit, sessions, installations, installationObserver, nativeSessions, outbox, publisher, grants, contexts, applicator, ipc, activeTurns, activeAttemptIds, connectionEpoch, diagnostics, announceConnection, signal, timer);
1328
1375
  }
1329
- catch {
1376
+ catch (error) {
1330
1377
  if (signal.aborted) {
1331
1378
  return;
1332
1379
  }
1333
- if (!retryAnnounced) {
1334
- console.warn("Mobius daemon is waiting for the relay");
1335
- retryAnnounced = true;
1336
- }
1380
+ diagnostics.connectionFailed(connectionEpoch, error, "connection");
1337
1381
  }
1338
1382
  await waitForRelayRetry(signal, timer);
1339
1383
  }