@moxt-ai/mobius 0.0.3 → 0.0.4

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
@@ -19329,6 +19329,9 @@ var encodeAgentContentEvent = (message) => encode3(message, agentContentEventSch
19329
19329
  // ../protocol/src/messages.ts
19330
19330
  var SESSION_PROMPT_KIND = "session.prompt";
19331
19331
  var SESSION_CANCEL_KIND = "session.cancel";
19332
+ var RUNTIME_LIVENESS_READY_KIND = "runtime.liveness.ready";
19333
+ var RUNTIME_HEARTBEAT_KIND = "runtime.heartbeat";
19334
+ var RUNTIME_HEARTBEAT_ACK_KIND = "runtime.heartbeat.ack";
19332
19335
  var SESSION_STARTED_KIND = "session.started";
19333
19336
  var AGENT_MESSAGE_DELTA_KIND = "agent.message.delta";
19334
19337
  var AGENT_ACTIVITY_KIND = "agent.activity";
@@ -19551,6 +19554,38 @@ function decodeSessionCancelCommand(encoded) {
19551
19554
  sessionId: readIdentifier(value, "sessionId")
19552
19555
  };
19553
19556
  }
19557
+ function decodeRuntimeLivenessReadyEvent(encoded) {
19558
+ const value = parseJson2(encoded);
19559
+ assertExactKeys(value, ["kind", "protocolVersion"]);
19560
+ assertProtocolVersion(value);
19561
+ assertKind(value, RUNTIME_LIVENESS_READY_KIND);
19562
+ return {
19563
+ kind: RUNTIME_LIVENESS_READY_KIND,
19564
+ protocolVersion: MOBIUS_PROTOCOL_VERSION
19565
+ };
19566
+ }
19567
+ function decodeRuntimeHeartbeatEvent(encoded) {
19568
+ const value = parseJson2(encoded);
19569
+ assertExactKeys(value, ["heartbeatId", "kind", "protocolVersion"]);
19570
+ assertProtocolVersion(value);
19571
+ assertKind(value, RUNTIME_HEARTBEAT_KIND);
19572
+ return {
19573
+ heartbeatId: readIdentifier(value, "heartbeatId"),
19574
+ kind: RUNTIME_HEARTBEAT_KIND,
19575
+ protocolVersion: MOBIUS_PROTOCOL_VERSION
19576
+ };
19577
+ }
19578
+ function decodeRuntimeHeartbeatAckEvent(encoded) {
19579
+ const value = parseJson2(encoded);
19580
+ assertExactKeys(value, ["heartbeatId", "kind", "protocolVersion"]);
19581
+ assertProtocolVersion(value);
19582
+ assertKind(value, RUNTIME_HEARTBEAT_ACK_KIND);
19583
+ return {
19584
+ heartbeatId: readIdentifier(value, "heartbeatId"),
19585
+ kind: RUNTIME_HEARTBEAT_ACK_KIND,
19586
+ protocolVersion: MOBIUS_PROTOCOL_VERSION
19587
+ };
19588
+ }
19554
19589
  function decodeSessionStartedEvent(encoded) {
19555
19590
  const value = parseJson2(encoded);
19556
19591
  assertExactKeys(value, [
@@ -19668,6 +19703,9 @@ function decodeSessionFailedEvent(encoded) {
19668
19703
  sessionId: readIdentifier(value, "sessionId")
19669
19704
  };
19670
19705
  }
19706
+ function encodeRuntimeHeartbeatEvent(message) {
19707
+ return JSON.stringify(decodeRuntimeHeartbeatEvent(JSON.stringify(message)));
19708
+ }
19671
19709
  function encodeSessionStartedEvent(message) {
19672
19710
  return JSON.stringify(decodeSessionStartedEvent(JSON.stringify(message)));
19673
19711
  }
@@ -22418,7 +22456,7 @@ async function resumeLocalMachine(arguments_) {
22418
22456
  }
22419
22457
 
22420
22458
  // src/run-daemon.ts
22421
- import { randomUUID as randomUUID2 } from "node:crypto";
22459
+ import { randomUUID as randomUUID3 } from "node:crypto";
22422
22460
  import { basename as basename3 } from "node:path";
22423
22461
  import { WebSocket as WebSocket2 } from "undici";
22424
22462
 
@@ -22680,8 +22718,179 @@ function connectRelay(url2, signal) {
22680
22718
  });
22681
22719
  }
22682
22720
 
22721
+ // src/relay-connection/liveness.ts
22722
+ import { randomUUID as randomUUID2 } from "node:crypto";
22723
+ var HEARTBEAT_ACK_TIMEOUT_MILLISECONDS = 1e4;
22724
+ var HEARTBEAT_INTERVAL_MILLISECONDS = 2e4;
22725
+ var RelayLivenessState = class {
22726
+ };
22727
+ var UnsupportedRelayLivenessState = class extends RelayLivenessState {
22728
+ };
22729
+ var AwaitingHeartbeatAckState = class extends RelayLivenessState {
22730
+ heartbeatId;
22731
+ timeout;
22732
+ constructor(heartbeatId, timeout) {
22733
+ super();
22734
+ this.heartbeatId = heartbeatId;
22735
+ this.timeout = timeout;
22736
+ }
22737
+ };
22738
+ var WaitingForHeartbeatState = class extends RelayLivenessState {
22739
+ schedule;
22740
+ constructor(schedule) {
22741
+ super();
22742
+ this.schedule = schedule;
22743
+ }
22744
+ };
22745
+ var FinishedRelayLivenessState = class extends RelayLivenessState {
22746
+ };
22747
+ var RelayConnectionLiveness = class {
22748
+ #timer;
22749
+ #transport;
22750
+ #state = new UnsupportedRelayLivenessState();
22751
+ constructor(timer, transport) {
22752
+ this.#timer = timer;
22753
+ this.#transport = transport;
22754
+ }
22755
+ acknowledge = (event) => {
22756
+ if (!(this.#state instanceof AwaitingHeartbeatAckState)) {
22757
+ throw new Error("Unexpected relay heartbeat acknowledgement");
22758
+ }
22759
+ if (event.heartbeatId !== this.#state.heartbeatId) {
22760
+ throw new Error("Relay acknowledged the wrong heartbeat");
22761
+ }
22762
+ this.#state.timeout.cancel();
22763
+ this.#state = new WaitingForHeartbeatState(
22764
+ this.#timer.schedule(
22765
+ this.#sendHeartbeat,
22766
+ HEARTBEAT_INTERVAL_MILLISECONDS
22767
+ )
22768
+ );
22769
+ };
22770
+ close = () => {
22771
+ if (this.#state instanceof AwaitingHeartbeatAckState) {
22772
+ this.#state.timeout.cancel();
22773
+ } else if (this.#state instanceof WaitingForHeartbeatState) {
22774
+ this.#state.schedule.cancel();
22775
+ } else if (!(this.#state instanceof UnsupportedRelayLivenessState) && !(this.#state instanceof FinishedRelayLivenessState)) {
22776
+ throw new Error("Unknown relay liveness state");
22777
+ }
22778
+ this.#state = new FinishedRelayLivenessState();
22779
+ };
22780
+ start = () => {
22781
+ if (this.#state instanceof UnsupportedRelayLivenessState) {
22782
+ this.#sendHeartbeat();
22783
+ return;
22784
+ }
22785
+ if (this.#state instanceof AwaitingHeartbeatAckState || this.#state instanceof WaitingForHeartbeatState) {
22786
+ return;
22787
+ }
22788
+ if (this.#state instanceof FinishedRelayLivenessState) {
22789
+ return;
22790
+ }
22791
+ throw new Error("Unknown relay liveness state");
22792
+ };
22793
+ #handleTimeout = () => {
22794
+ if (this.#state instanceof FinishedRelayLivenessState) {
22795
+ return;
22796
+ }
22797
+ if (!(this.#state instanceof AwaitingHeartbeatAckState)) {
22798
+ throw new Error("Heartbeat timeout occurred in an invalid state");
22799
+ }
22800
+ this.#state.timeout.cancel();
22801
+ this.#state = new FinishedRelayLivenessState();
22802
+ this.#transport.unresponsive();
22803
+ };
22804
+ #sendHeartbeat = () => {
22805
+ if (this.#state instanceof WaitingForHeartbeatState) {
22806
+ this.#state.schedule.cancel();
22807
+ } else if (!(this.#state instanceof UnsupportedRelayLivenessState)) {
22808
+ if (this.#state instanceof FinishedRelayLivenessState) {
22809
+ return;
22810
+ }
22811
+ throw new Error("Heartbeat was scheduled in an invalid state");
22812
+ }
22813
+ const heartbeatId = randomUUID2();
22814
+ this.#state = new AwaitingHeartbeatAckState(
22815
+ heartbeatId,
22816
+ this.#timer.schedule(
22817
+ this.#handleTimeout,
22818
+ HEARTBEAT_ACK_TIMEOUT_MILLISECONDS
22819
+ )
22820
+ );
22821
+ this.#transport.send(
22822
+ encodeRuntimeHeartbeatEvent({
22823
+ heartbeatId,
22824
+ kind: RUNTIME_HEARTBEAT_KIND,
22825
+ protocolVersion: MOBIUS_PROTOCOL_VERSION
22826
+ })
22827
+ );
22828
+ };
22829
+ };
22830
+
22831
+ // src/relay-connection/timer.ts
22832
+ var SystemDaemonTimer = class {
22833
+ schedule = (callback, milliseconds) => {
22834
+ const timeout = setTimeout(callback, milliseconds);
22835
+ return { cancel: () => clearTimeout(timeout) };
22836
+ };
22837
+ };
22838
+ var systemDaemonTimer = new SystemDaemonTimer();
22839
+
22683
22840
  // src/run-daemon.ts
22684
22841
  var RELAY_RECONNECT_DELAY_MILLISECONDS = 500;
22842
+ var RELAY_CONNECTION_REFRESH_MILLISECONDS = 20 * 60 * 1e3;
22843
+ var RelayConnectionRefreshState = class {
22844
+ };
22845
+ var ScheduledRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
22846
+ };
22847
+ var DueRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
22848
+ };
22849
+ var FinishedRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
22850
+ };
22851
+ var RelayConnectionRefresh = class {
22852
+ #isSafe;
22853
+ #refresh;
22854
+ #scheduledTask;
22855
+ #state = new ScheduledRelayConnectionRefreshState();
22856
+ constructor(timer, isSafe, refresh) {
22857
+ this.#isSafe = isSafe;
22858
+ this.#refresh = refresh;
22859
+ this.#scheduledTask = timer.schedule(
22860
+ this.#request,
22861
+ RELAY_CONNECTION_REFRESH_MILLISECONDS
22862
+ );
22863
+ }
22864
+ afterTurnFinished = () => {
22865
+ if (this.#state instanceof DueRelayConnectionRefreshState) {
22866
+ this.#request();
22867
+ return;
22868
+ }
22869
+ if (this.#state instanceof ScheduledRelayConnectionRefreshState || this.#state instanceof FinishedRelayConnectionRefreshState) {
22870
+ return;
22871
+ }
22872
+ throw new Error("Unknown relay connection refresh state");
22873
+ };
22874
+ close = () => {
22875
+ this.#scheduledTask.cancel();
22876
+ this.#state = new FinishedRelayConnectionRefreshState();
22877
+ };
22878
+ #request = () => {
22879
+ if (this.#state instanceof FinishedRelayConnectionRefreshState) {
22880
+ return;
22881
+ }
22882
+ if (!(this.#state instanceof ScheduledRelayConnectionRefreshState) && !(this.#state instanceof DueRelayConnectionRefreshState)) {
22883
+ throw new Error("Unknown relay connection refresh state");
22884
+ }
22885
+ if (!this.#isSafe()) {
22886
+ this.#state = new DueRelayConnectionRefreshState();
22887
+ return;
22888
+ }
22889
+ this.#state = new FinishedRelayConnectionRefreshState();
22890
+ this.#scheduledTask.cancel();
22891
+ this.#refresh();
22892
+ };
22893
+ };
22685
22894
  var DaemonAcpObserver = class {
22686
22895
  #socket;
22687
22896
  constructor(socket) {
@@ -22959,7 +23168,7 @@ async function executePrompt(socket, command, config2, directories, audit, sessi
22959
23168
  command.additionalDirectoryIds
22960
23169
  );
22961
23170
  const agent = findLocalAgent(config2.agents, command.agentId);
22962
- const processAttemptId = randomUUID2();
23171
+ const processAttemptId = randomUUID3();
22963
23172
  audit.record({
22964
23173
  commandId: command.commandId,
22965
23174
  event: "session_authorized",
@@ -23052,28 +23261,32 @@ async function executePrompt(socket, command, config2, directories, audit, sessi
23052
23261
  console.error("Mobius daemon command failed", error61);
23053
23262
  }
23054
23263
  }
23055
- function waitForRelayRetry(signal) {
23264
+ function waitForRelayRetry(signal, timer) {
23056
23265
  if (signal.aborted) {
23057
23266
  return Promise.resolve();
23058
23267
  }
23059
23268
  return new Promise((resolve2) => {
23060
- const timeout = setTimeout(finish, RELAY_RECONNECT_DELAY_MILLISECONDS);
23269
+ const retry = timer.schedule(finish, RELAY_RECONNECT_DELAY_MILLISECONDS);
23061
23270
  function finish() {
23062
- clearTimeout(timeout);
23271
+ retry.cancel();
23063
23272
  signal.removeEventListener("abort", finish);
23064
23273
  resolve2();
23065
23274
  }
23066
23275
  signal.addEventListener("abort", finish, { once: true });
23067
23276
  });
23068
23277
  }
23069
- async function runRelayConnection(config2, directories, audit, sessions, signal) {
23070
- const socket = await connectRelay(config2.relayUrl, signal);
23278
+ async function runRelayConnection(config2, directories, audit, sessions, signal, timer) {
23279
+ const relayUrl = new URL(config2.relayUrl);
23280
+ relayUrl.searchParams.set("liveness", "1");
23281
+ const socket = await connectRelay(relayUrl, signal);
23071
23282
  const activeTurns = /* @__PURE__ */ new Map();
23072
23283
  const connectionController = new AbortController();
23073
23284
  sessions.setObserver(new DaemonAcpObserver(socket));
23074
23285
  await new Promise((resolve2, reject) => {
23075
23286
  let finished = false;
23076
23287
  const cleanup = () => {
23288
+ connectionRefresh.close();
23289
+ connectionLiveness.close();
23077
23290
  signal.removeEventListener("abort", stop);
23078
23291
  socket.removeEventListener("close", handleClose);
23079
23292
  socket.removeEventListener("error", handleError);
@@ -23104,6 +23317,22 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23104
23317
  socket.close(1e3, "Daemon stopped");
23105
23318
  resolveConnection();
23106
23319
  };
23320
+ const refreshConnection = () => {
23321
+ socket.close(1e3, "Refreshing idle relay connection");
23322
+ resolveConnection();
23323
+ };
23324
+ const connectionLiveness = new RelayConnectionLiveness(timer, {
23325
+ send: (message) => send(socket, message),
23326
+ unresponsive: () => {
23327
+ socket.close(4001, "Relay liveness check timed out");
23328
+ resolveConnection();
23329
+ }
23330
+ });
23331
+ const connectionRefresh = new RelayConnectionRefresh(
23332
+ timer,
23333
+ () => activeTurns.size === 0,
23334
+ refreshConnection
23335
+ );
23107
23336
  signal.addEventListener("abort", stop, { once: true });
23108
23337
  const handleClose = () => {
23109
23338
  resolveConnection();
@@ -23118,11 +23347,23 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23118
23347
  const handleMessage = (event) => {
23119
23348
  const encoded = event.data;
23120
23349
  if (typeof encoded !== "string") {
23121
- socket.close(1003, "Text message required");
23350
+ socket.close(4003, "Text message required");
23351
+ rejectConnection();
23122
23352
  return;
23123
23353
  }
23124
23354
  try {
23125
23355
  const kind = decodeMessageKind(encoded);
23356
+ if (kind === RUNTIME_LIVENESS_READY_KIND) {
23357
+ decodeRuntimeLivenessReadyEvent(encoded);
23358
+ connectionLiveness.start();
23359
+ return;
23360
+ }
23361
+ if (kind === RUNTIME_HEARTBEAT_ACK_KIND) {
23362
+ connectionLiveness.acknowledge(
23363
+ decodeRuntimeHeartbeatAckEvent(encoded)
23364
+ );
23365
+ return;
23366
+ }
23126
23367
  if (kind === SESSION_CANCEL_KIND) {
23127
23368
  const cancellation = decodeSessionCancelCommand(encoded);
23128
23369
  const turn2 = activeTurns.get(cancellation.commandId);
@@ -23216,8 +23457,7 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23216
23457
  return;
23217
23458
  }
23218
23459
  if (kind !== SESSION_PROMPT_KIND) {
23219
- socket.close(1008, "Unsupported protocol message");
23220
- return;
23460
+ throw new Error("Unsupported protocol message");
23221
23461
  }
23222
23462
  const command = decodeSessionPromptCommand(encoded);
23223
23463
  if (activeTurns.has(command.commandId)) {
@@ -23243,10 +23483,12 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23243
23483
  console.error("Mobius daemon turn failed", error61);
23244
23484
  }).finally(() => {
23245
23485
  activeTurns.delete(command.commandId);
23486
+ connectionRefresh.afterTurnFinished();
23246
23487
  });
23247
23488
  } catch (error61) {
23248
23489
  console.error("Mobius daemon rejected a message", error61);
23249
- socket.close(1008, "Invalid protocol message");
23490
+ socket.close(4008, "Invalid protocol message");
23491
+ rejectConnection();
23250
23492
  }
23251
23493
  };
23252
23494
  socket.addEventListener("close", handleClose);
@@ -23257,7 +23499,7 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23257
23499
  }
23258
23500
  });
23259
23501
  }
23260
- async function runDaemon(config2, audit, signal) {
23502
+ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
23261
23503
  const sessionStore = new SqliteAcpSessionStore(config2.auditDatabasePath);
23262
23504
  const sessions = new AcpSessionManager(
23263
23505
  new NullAcpSessionObserver(),
@@ -23271,7 +23513,14 @@ async function runDaemon(config2, audit, signal) {
23271
23513
  try {
23272
23514
  while (!signal.aborted) {
23273
23515
  try {
23274
- await runRelayConnection(config2, directories, audit, sessions, signal);
23516
+ await runRelayConnection(
23517
+ config2,
23518
+ directories,
23519
+ audit,
23520
+ sessions,
23521
+ signal,
23522
+ timer
23523
+ );
23275
23524
  retryAnnounced = false;
23276
23525
  } catch {
23277
23526
  if (signal.aborted) {
@@ -23282,7 +23531,7 @@ async function runDaemon(config2, audit, signal) {
23282
23531
  retryAnnounced = true;
23283
23532
  }
23284
23533
  }
23285
- await waitForRelayRetry(signal);
23534
+ await waitForRelayRetry(signal, timer);
23286
23535
  }
23287
23536
  } finally {
23288
23537
  await sessions.close();