@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/cli.js CHANGED
@@ -519,7 +519,7 @@ async function resumeLocalMachine(arguments_) {
519
519
  }
520
520
 
521
521
  // src/run-daemon.ts
522
- import { randomUUID as randomUUID2 } from "node:crypto";
522
+ import { randomUUID as randomUUID3 } from "node:crypto";
523
523
  import { basename as basename3 } from "node:path";
524
524
 
525
525
  // ../../node_modules/.pnpm/zod@4.5.4/node_modules/zod/v4/classic/external.js
@@ -19734,6 +19734,9 @@ var encodeAgentContentEvent = (message) => encode3(message, agentContentEventSch
19734
19734
  // ../protocol/src/messages.ts
19735
19735
  var SESSION_PROMPT_KIND = "session.prompt";
19736
19736
  var SESSION_CANCEL_KIND = "session.cancel";
19737
+ var RUNTIME_LIVENESS_READY_KIND = "runtime.liveness.ready";
19738
+ var RUNTIME_HEARTBEAT_KIND = "runtime.heartbeat";
19739
+ var RUNTIME_HEARTBEAT_ACK_KIND = "runtime.heartbeat.ack";
19737
19740
  var SESSION_STARTED_KIND = "session.started";
19738
19741
  var AGENT_MESSAGE_DELTA_KIND = "agent.message.delta";
19739
19742
  var AGENT_ACTIVITY_KIND = "agent.activity";
@@ -19956,6 +19959,38 @@ function decodeSessionCancelCommand(encoded) {
19956
19959
  sessionId: readIdentifier(value, "sessionId")
19957
19960
  };
19958
19961
  }
19962
+ function decodeRuntimeLivenessReadyEvent(encoded) {
19963
+ const value = parseJson2(encoded);
19964
+ assertExactKeys(value, ["kind", "protocolVersion"]);
19965
+ assertProtocolVersion(value);
19966
+ assertKind(value, RUNTIME_LIVENESS_READY_KIND);
19967
+ return {
19968
+ kind: RUNTIME_LIVENESS_READY_KIND,
19969
+ protocolVersion: MOBIUS_PROTOCOL_VERSION
19970
+ };
19971
+ }
19972
+ function decodeRuntimeHeartbeatEvent(encoded) {
19973
+ const value = parseJson2(encoded);
19974
+ assertExactKeys(value, ["heartbeatId", "kind", "protocolVersion"]);
19975
+ assertProtocolVersion(value);
19976
+ assertKind(value, RUNTIME_HEARTBEAT_KIND);
19977
+ return {
19978
+ heartbeatId: readIdentifier(value, "heartbeatId"),
19979
+ kind: RUNTIME_HEARTBEAT_KIND,
19980
+ protocolVersion: MOBIUS_PROTOCOL_VERSION
19981
+ };
19982
+ }
19983
+ function decodeRuntimeHeartbeatAckEvent(encoded) {
19984
+ const value = parseJson2(encoded);
19985
+ assertExactKeys(value, ["heartbeatId", "kind", "protocolVersion"]);
19986
+ assertProtocolVersion(value);
19987
+ assertKind(value, RUNTIME_HEARTBEAT_ACK_KIND);
19988
+ return {
19989
+ heartbeatId: readIdentifier(value, "heartbeatId"),
19990
+ kind: RUNTIME_HEARTBEAT_ACK_KIND,
19991
+ protocolVersion: MOBIUS_PROTOCOL_VERSION
19992
+ };
19993
+ }
19959
19994
  function decodeSessionStartedEvent(encoded) {
19960
19995
  const value = parseJson2(encoded);
19961
19996
  assertExactKeys(value, [
@@ -20073,6 +20108,9 @@ function decodeSessionFailedEvent(encoded) {
20073
20108
  sessionId: readIdentifier(value, "sessionId")
20074
20109
  };
20075
20110
  }
20111
+ function encodeRuntimeHeartbeatEvent(message) {
20112
+ return JSON.stringify(decodeRuntimeHeartbeatEvent(JSON.stringify(message)));
20113
+ }
20076
20114
  function encodeSessionStartedEvent(message) {
20077
20115
  return JSON.stringify(decodeSessionStartedEvent(JSON.stringify(message)));
20078
20116
  }
@@ -22610,8 +22648,179 @@ function connectRelay(url2, signal) {
22610
22648
  });
22611
22649
  }
22612
22650
 
22651
+ // src/relay-connection/liveness.ts
22652
+ import { randomUUID as randomUUID2 } from "node:crypto";
22653
+ var HEARTBEAT_ACK_TIMEOUT_MILLISECONDS = 1e4;
22654
+ var HEARTBEAT_INTERVAL_MILLISECONDS = 2e4;
22655
+ var RelayLivenessState = class {
22656
+ };
22657
+ var UnsupportedRelayLivenessState = class extends RelayLivenessState {
22658
+ };
22659
+ var AwaitingHeartbeatAckState = class extends RelayLivenessState {
22660
+ heartbeatId;
22661
+ timeout;
22662
+ constructor(heartbeatId, timeout) {
22663
+ super();
22664
+ this.heartbeatId = heartbeatId;
22665
+ this.timeout = timeout;
22666
+ }
22667
+ };
22668
+ var WaitingForHeartbeatState = class extends RelayLivenessState {
22669
+ schedule;
22670
+ constructor(schedule) {
22671
+ super();
22672
+ this.schedule = schedule;
22673
+ }
22674
+ };
22675
+ var FinishedRelayLivenessState = class extends RelayLivenessState {
22676
+ };
22677
+ var RelayConnectionLiveness = class {
22678
+ #timer;
22679
+ #transport;
22680
+ #state = new UnsupportedRelayLivenessState();
22681
+ constructor(timer, transport) {
22682
+ this.#timer = timer;
22683
+ this.#transport = transport;
22684
+ }
22685
+ acknowledge = (event) => {
22686
+ if (!(this.#state instanceof AwaitingHeartbeatAckState)) {
22687
+ throw new Error("Unexpected relay heartbeat acknowledgement");
22688
+ }
22689
+ if (event.heartbeatId !== this.#state.heartbeatId) {
22690
+ throw new Error("Relay acknowledged the wrong heartbeat");
22691
+ }
22692
+ this.#state.timeout.cancel();
22693
+ this.#state = new WaitingForHeartbeatState(
22694
+ this.#timer.schedule(
22695
+ this.#sendHeartbeat,
22696
+ HEARTBEAT_INTERVAL_MILLISECONDS
22697
+ )
22698
+ );
22699
+ };
22700
+ close = () => {
22701
+ if (this.#state instanceof AwaitingHeartbeatAckState) {
22702
+ this.#state.timeout.cancel();
22703
+ } else if (this.#state instanceof WaitingForHeartbeatState) {
22704
+ this.#state.schedule.cancel();
22705
+ } else if (!(this.#state instanceof UnsupportedRelayLivenessState) && !(this.#state instanceof FinishedRelayLivenessState)) {
22706
+ throw new Error("Unknown relay liveness state");
22707
+ }
22708
+ this.#state = new FinishedRelayLivenessState();
22709
+ };
22710
+ start = () => {
22711
+ if (this.#state instanceof UnsupportedRelayLivenessState) {
22712
+ this.#sendHeartbeat();
22713
+ return;
22714
+ }
22715
+ if (this.#state instanceof AwaitingHeartbeatAckState || this.#state instanceof WaitingForHeartbeatState) {
22716
+ return;
22717
+ }
22718
+ if (this.#state instanceof FinishedRelayLivenessState) {
22719
+ return;
22720
+ }
22721
+ throw new Error("Unknown relay liveness state");
22722
+ };
22723
+ #handleTimeout = () => {
22724
+ if (this.#state instanceof FinishedRelayLivenessState) {
22725
+ return;
22726
+ }
22727
+ if (!(this.#state instanceof AwaitingHeartbeatAckState)) {
22728
+ throw new Error("Heartbeat timeout occurred in an invalid state");
22729
+ }
22730
+ this.#state.timeout.cancel();
22731
+ this.#state = new FinishedRelayLivenessState();
22732
+ this.#transport.unresponsive();
22733
+ };
22734
+ #sendHeartbeat = () => {
22735
+ if (this.#state instanceof WaitingForHeartbeatState) {
22736
+ this.#state.schedule.cancel();
22737
+ } else if (!(this.#state instanceof UnsupportedRelayLivenessState)) {
22738
+ if (this.#state instanceof FinishedRelayLivenessState) {
22739
+ return;
22740
+ }
22741
+ throw new Error("Heartbeat was scheduled in an invalid state");
22742
+ }
22743
+ const heartbeatId = randomUUID2();
22744
+ this.#state = new AwaitingHeartbeatAckState(
22745
+ heartbeatId,
22746
+ this.#timer.schedule(
22747
+ this.#handleTimeout,
22748
+ HEARTBEAT_ACK_TIMEOUT_MILLISECONDS
22749
+ )
22750
+ );
22751
+ this.#transport.send(
22752
+ encodeRuntimeHeartbeatEvent({
22753
+ heartbeatId,
22754
+ kind: RUNTIME_HEARTBEAT_KIND,
22755
+ protocolVersion: MOBIUS_PROTOCOL_VERSION
22756
+ })
22757
+ );
22758
+ };
22759
+ };
22760
+
22761
+ // src/relay-connection/timer.ts
22762
+ var SystemDaemonTimer = class {
22763
+ schedule = (callback, milliseconds) => {
22764
+ const timeout = setTimeout(callback, milliseconds);
22765
+ return { cancel: () => clearTimeout(timeout) };
22766
+ };
22767
+ };
22768
+ var systemDaemonTimer = new SystemDaemonTimer();
22769
+
22613
22770
  // src/run-daemon.ts
22614
22771
  var RELAY_RECONNECT_DELAY_MILLISECONDS = 500;
22772
+ var RELAY_CONNECTION_REFRESH_MILLISECONDS = 20 * 60 * 1e3;
22773
+ var RelayConnectionRefreshState = class {
22774
+ };
22775
+ var ScheduledRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
22776
+ };
22777
+ var DueRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
22778
+ };
22779
+ var FinishedRelayConnectionRefreshState = class extends RelayConnectionRefreshState {
22780
+ };
22781
+ var RelayConnectionRefresh = class {
22782
+ #isSafe;
22783
+ #refresh;
22784
+ #scheduledTask;
22785
+ #state = new ScheduledRelayConnectionRefreshState();
22786
+ constructor(timer, isSafe, refresh) {
22787
+ this.#isSafe = isSafe;
22788
+ this.#refresh = refresh;
22789
+ this.#scheduledTask = timer.schedule(
22790
+ this.#request,
22791
+ RELAY_CONNECTION_REFRESH_MILLISECONDS
22792
+ );
22793
+ }
22794
+ afterTurnFinished = () => {
22795
+ if (this.#state instanceof DueRelayConnectionRefreshState) {
22796
+ this.#request();
22797
+ return;
22798
+ }
22799
+ if (this.#state instanceof ScheduledRelayConnectionRefreshState || this.#state instanceof FinishedRelayConnectionRefreshState) {
22800
+ return;
22801
+ }
22802
+ throw new Error("Unknown relay connection refresh state");
22803
+ };
22804
+ close = () => {
22805
+ this.#scheduledTask.cancel();
22806
+ this.#state = new FinishedRelayConnectionRefreshState();
22807
+ };
22808
+ #request = () => {
22809
+ if (this.#state instanceof FinishedRelayConnectionRefreshState) {
22810
+ return;
22811
+ }
22812
+ if (!(this.#state instanceof ScheduledRelayConnectionRefreshState) && !(this.#state instanceof DueRelayConnectionRefreshState)) {
22813
+ throw new Error("Unknown relay connection refresh state");
22814
+ }
22815
+ if (!this.#isSafe()) {
22816
+ this.#state = new DueRelayConnectionRefreshState();
22817
+ return;
22818
+ }
22819
+ this.#state = new FinishedRelayConnectionRefreshState();
22820
+ this.#scheduledTask.cancel();
22821
+ this.#refresh();
22822
+ };
22823
+ };
22615
22824
  var DaemonAcpObserver = class {
22616
22825
  #socket;
22617
22826
  constructor(socket) {
@@ -22889,7 +23098,7 @@ async function executePrompt(socket, command, config2, directories, audit, sessi
22889
23098
  command.additionalDirectoryIds
22890
23099
  );
22891
23100
  const agent = findLocalAgent(config2.agents, command.agentId);
22892
- const processAttemptId = randomUUID2();
23101
+ const processAttemptId = randomUUID3();
22893
23102
  audit.record({
22894
23103
  commandId: command.commandId,
22895
23104
  event: "session_authorized",
@@ -22982,28 +23191,32 @@ async function executePrompt(socket, command, config2, directories, audit, sessi
22982
23191
  console.error("Mobius daemon command failed", error61);
22983
23192
  }
22984
23193
  }
22985
- function waitForRelayRetry(signal) {
23194
+ function waitForRelayRetry(signal, timer) {
22986
23195
  if (signal.aborted) {
22987
23196
  return Promise.resolve();
22988
23197
  }
22989
23198
  return new Promise((resolve2) => {
22990
- const timeout = setTimeout(finish, RELAY_RECONNECT_DELAY_MILLISECONDS);
23199
+ const retry = timer.schedule(finish, RELAY_RECONNECT_DELAY_MILLISECONDS);
22991
23200
  function finish() {
22992
- clearTimeout(timeout);
23201
+ retry.cancel();
22993
23202
  signal.removeEventListener("abort", finish);
22994
23203
  resolve2();
22995
23204
  }
22996
23205
  signal.addEventListener("abort", finish, { once: true });
22997
23206
  });
22998
23207
  }
22999
- async function runRelayConnection(config2, directories, audit, sessions, signal) {
23000
- const socket = await connectRelay(config2.relayUrl, signal);
23208
+ async function runRelayConnection(config2, directories, audit, sessions, signal, timer) {
23209
+ const relayUrl = new URL(config2.relayUrl);
23210
+ relayUrl.searchParams.set("liveness", "1");
23211
+ const socket = await connectRelay(relayUrl, signal);
23001
23212
  const activeTurns = /* @__PURE__ */ new Map();
23002
23213
  const connectionController = new AbortController();
23003
23214
  sessions.setObserver(new DaemonAcpObserver(socket));
23004
23215
  await new Promise((resolve2, reject) => {
23005
23216
  let finished = false;
23006
23217
  const cleanup = () => {
23218
+ connectionRefresh.close();
23219
+ connectionLiveness.close();
23007
23220
  signal.removeEventListener("abort", stop);
23008
23221
  socket.removeEventListener("close", handleClose);
23009
23222
  socket.removeEventListener("error", handleError);
@@ -23034,6 +23247,22 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23034
23247
  socket.close(1e3, "Daemon stopped");
23035
23248
  resolveConnection();
23036
23249
  };
23250
+ const refreshConnection = () => {
23251
+ socket.close(1e3, "Refreshing idle relay connection");
23252
+ resolveConnection();
23253
+ };
23254
+ const connectionLiveness = new RelayConnectionLiveness(timer, {
23255
+ send: (message) => send(socket, message),
23256
+ unresponsive: () => {
23257
+ socket.close(4001, "Relay liveness check timed out");
23258
+ resolveConnection();
23259
+ }
23260
+ });
23261
+ const connectionRefresh = new RelayConnectionRefresh(
23262
+ timer,
23263
+ () => activeTurns.size === 0,
23264
+ refreshConnection
23265
+ );
23037
23266
  signal.addEventListener("abort", stop, { once: true });
23038
23267
  const handleClose = () => {
23039
23268
  resolveConnection();
@@ -23048,11 +23277,23 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23048
23277
  const handleMessage = (event) => {
23049
23278
  const encoded = event.data;
23050
23279
  if (typeof encoded !== "string") {
23051
- socket.close(1003, "Text message required");
23280
+ socket.close(4003, "Text message required");
23281
+ rejectConnection();
23052
23282
  return;
23053
23283
  }
23054
23284
  try {
23055
23285
  const kind = decodeMessageKind(encoded);
23286
+ if (kind === RUNTIME_LIVENESS_READY_KIND) {
23287
+ decodeRuntimeLivenessReadyEvent(encoded);
23288
+ connectionLiveness.start();
23289
+ return;
23290
+ }
23291
+ if (kind === RUNTIME_HEARTBEAT_ACK_KIND) {
23292
+ connectionLiveness.acknowledge(
23293
+ decodeRuntimeHeartbeatAckEvent(encoded)
23294
+ );
23295
+ return;
23296
+ }
23056
23297
  if (kind === SESSION_CANCEL_KIND) {
23057
23298
  const cancellation = decodeSessionCancelCommand(encoded);
23058
23299
  const turn2 = activeTurns.get(cancellation.commandId);
@@ -23146,8 +23387,7 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23146
23387
  return;
23147
23388
  }
23148
23389
  if (kind !== SESSION_PROMPT_KIND) {
23149
- socket.close(1008, "Unsupported protocol message");
23150
- return;
23390
+ throw new Error("Unsupported protocol message");
23151
23391
  }
23152
23392
  const command = decodeSessionPromptCommand(encoded);
23153
23393
  if (activeTurns.has(command.commandId)) {
@@ -23173,10 +23413,12 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23173
23413
  console.error("Mobius daemon turn failed", error61);
23174
23414
  }).finally(() => {
23175
23415
  activeTurns.delete(command.commandId);
23416
+ connectionRefresh.afterTurnFinished();
23176
23417
  });
23177
23418
  } catch (error61) {
23178
23419
  console.error("Mobius daemon rejected a message", error61);
23179
- socket.close(1008, "Invalid protocol message");
23420
+ socket.close(4008, "Invalid protocol message");
23421
+ rejectConnection();
23180
23422
  }
23181
23423
  };
23182
23424
  socket.addEventListener("close", handleClose);
@@ -23187,7 +23429,7 @@ async function runRelayConnection(config2, directories, audit, sessions, signal)
23187
23429
  }
23188
23430
  });
23189
23431
  }
23190
- async function runDaemon(config2, audit, signal) {
23432
+ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer) {
23191
23433
  const sessionStore = new SqliteAcpSessionStore(config2.auditDatabasePath);
23192
23434
  const sessions = new AcpSessionManager(
23193
23435
  new NullAcpSessionObserver(),
@@ -23201,7 +23443,14 @@ async function runDaemon(config2, audit, signal) {
23201
23443
  try {
23202
23444
  while (!signal.aborted) {
23203
23445
  try {
23204
- await runRelayConnection(config2, directories, audit, sessions, signal);
23446
+ await runRelayConnection(
23447
+ config2,
23448
+ directories,
23449
+ audit,
23450
+ sessions,
23451
+ signal,
23452
+ timer
23453
+ );
23205
23454
  retryAnnounced = false;
23206
23455
  } catch {
23207
23456
  if (signal.aborted) {
@@ -23212,7 +23461,7 @@ async function runDaemon(config2, audit, signal) {
23212
23461
  retryAnnounced = true;
23213
23462
  }
23214
23463
  }
23215
- await waitForRelayRetry(signal);
23464
+ await waitForRelayRetry(signal, timer);
23216
23465
  }
23217
23466
  } finally {
23218
23467
  await sessions.close();