@moxt-ai/mobius 0.0.152 → 0.0.154

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 (35) hide show
  1. package/dist/cli.js +126 -284
  2. package/dist/cli.js.map +4 -4
  3. package/dist/daemon-active-turn.d.ts +1 -0
  4. package/dist/daemon-active-turn.d.ts.map +1 -1
  5. package/dist/daemon-active-turn.js +1 -0
  6. package/dist/daemon-active-turn.js.map +1 -1
  7. package/dist/daemon-turn-registry.d.ts +2 -1
  8. package/dist/daemon-turn-registry.d.ts.map +1 -1
  9. package/dist/daemon-turn-registry.js +32 -34
  10. package/dist/daemon-turn-registry.js.map +1 -1
  11. package/dist/index.js +116 -281
  12. package/dist/index.js.map +4 -4
  13. package/dist/native-sessions/live-history-page.d.ts.map +1 -1
  14. package/dist/native-sessions/live-history-page.js +2 -0
  15. package/dist/native-sessions/live-history-page.js.map +1 -1
  16. package/dist/relay-connection/run-relay-connection.js +1 -1
  17. package/dist/relay-connection/run-relay-connection.js.map +1 -1
  18. package/dist/relay-connection/session-event-publisher.d.ts +3 -4
  19. package/dist/relay-connection/session-event-publisher.d.ts.map +1 -1
  20. package/dist/relay-connection/session-event-publisher.js +14 -22
  21. package/dist/relay-connection/session-event-publisher.js.map +1 -1
  22. package/dist/relay-connection/session-event-queue.d.ts +14 -0
  23. package/dist/relay-connection/session-event-queue.d.ts.map +1 -0
  24. package/dist/relay-connection/session-event-queue.js +75 -0
  25. package/dist/relay-connection/session-event-queue.js.map +1 -0
  26. package/dist/run-daemon.d.ts.map +1 -1
  27. package/dist/run-daemon.js +1 -3
  28. package/dist/run-daemon.js.map +1 -1
  29. package/dist/runtime.js +118 -283
  30. package/dist/runtime.js.map +4 -4
  31. package/package.json +10 -3
  32. package/dist/relay-connection/session-event-outbox.d.ts +0 -20
  33. package/dist/relay-connection/session-event-outbox.d.ts.map +0 -1
  34. package/dist/relay-connection/session-event-outbox.js +0 -243
  35. package/dist/relay-connection/session-event-outbox.js.map +0 -1
package/dist/cli.js CHANGED
@@ -3381,7 +3381,7 @@ var program = new Command();
3381
3381
  // package.json
3382
3382
  var package_default = {
3383
3383
  name: "@moxt-ai/mobius",
3384
- version: "0.0.152",
3384
+ version: "0.0.154",
3385
3385
  description: "Mobius CLI for pairing and running local AI agent services.",
3386
3386
  type: "module",
3387
3387
  bin: {
@@ -3416,7 +3416,9 @@ var package_default = {
3416
3416
  dev: "node --watch src/cli.ts",
3417
3417
  prepack: "pnpm build",
3418
3418
  start: "node dist/cli.js",
3419
- typecheck: "tsc --project tsconfig.json"
3419
+ typecheck: "tsc --project tsconfig.json && tsc --project tests/tsconfig.json",
3420
+ "test:controlled": "vitest run tests/sessions tests/runtime --maxWorkers=4",
3421
+ "test:agents": "vitest run tests/agents --maxWorkers=2"
3420
3422
  },
3421
3423
  dependencies: {
3422
3424
  "@agentclientprotocol/sdk": "^1.4.0",
@@ -3430,7 +3432,12 @@ var package_default = {
3430
3432
  "@types/better-sqlite3": "7.6.13",
3431
3433
  "@types/node": "^24.13.5",
3432
3434
  "drizzle-orm": "^0.45.2",
3433
- esbuild: "0.28.2"
3435
+ esbuild: "0.28.2",
3436
+ "@moxt-ai/mobius-sdk": "workspace:*",
3437
+ "@moxt-ai/mobius-test": "workspace:*",
3438
+ valibot: "1.5.0",
3439
+ ws: "^8.21.3",
3440
+ "@types/ws": "^8.18.1"
3434
3441
  },
3435
3442
  main: "dist/index.js",
3436
3443
  keywords: [],
@@ -29554,6 +29561,11 @@ var assistantHistoryMessageSchema = external_exports.strictObject({
29554
29561
  }).readonly();
29555
29562
  var historyMessage = external_exports.discriminatedUnion("role", [userHistoryMessage, assistantHistoryMessageSchema]);
29556
29563
  var availableSessionHistoryObjectSchema = external_exports.strictObject({
29564
+ streamPosition: external_exports.strictObject({
29565
+ commandId: identifierStringSchema,
29566
+ epoch: identifierStringSchema,
29567
+ sequence: external_exports.number().int().nonnegative()
29568
+ }).readonly().optional(),
29557
29569
  status: external_exports.literal("available"),
29558
29570
  messages: external_exports.array(historyMessage).max(SESSION_HISTORY_MESSAGE_LIMIT).readonly(),
29559
29571
  truncated: external_exports.boolean(),
@@ -42587,6 +42599,7 @@ var ActiveDaemonTurn = class {
42587
42599
  commandId: decodeIdentifier(this.command.commandId),
42588
42600
  epoch: this.#epoch,
42589
42601
  messages: this.timelineMessages(),
42602
+ sequence: this.#sequence,
42590
42603
  state: "running",
42591
42604
  truncated: false
42592
42605
  };
@@ -42614,7 +42627,7 @@ var ActiveDaemonTurn = class {
42614
42627
  };
42615
42628
 
42616
42629
  // src/daemon-turn-registry.ts
42617
- var COMPLETED_TURN_RECOVERY_LIMIT = 32;
42630
+ var MAX_SESSIONS_WITH_COMPLETED_RECOVERY = 32;
42618
42631
  var waitForTurnCompletion = (turn, waitSignal) => {
42619
42632
  waitSignal.throwIfAborted();
42620
42633
  return new Promise((resolve9, reject) => {
@@ -42640,46 +42653,46 @@ var waitForTurnCompletion = (turn, waitSignal) => {
42640
42653
  });
42641
42654
  };
42642
42655
  var DaemonTurnRegistry = class {
42643
- #turns = /* @__PURE__ */ new Map();
42644
- #completed = /* @__PURE__ */ new Map();
42656
+ #inFlightByCommandId = /* @__PURE__ */ new Map();
42657
+ #latestCompletedBySessionId = /* @__PURE__ */ new Map();
42645
42658
  #operations;
42646
42659
  constructor(operations) {
42647
42660
  this.#operations = operations;
42648
42661
  }
42649
42662
  start = (command, attemptId) => {
42650
42663
  const turn = new ActiveDaemonTurn(command, attemptId);
42651
- this.#turns.set(command.commandId, turn);
42664
+ this.#inFlightByCommandId.set(command.commandId, turn);
42652
42665
  return turn;
42653
42666
  };
42654
- get = (commandId) => this.#turns.get(commandId);
42667
+ get = (commandId) => this.#inFlightByCommandId.get(commandId);
42655
42668
  finish = (turn) => {
42656
42669
  const recovery = turn.recovery();
42657
- this.#turns.delete(turn.command.commandId);
42670
+ this.#inFlightByCommandId.delete(turn.command.commandId);
42658
42671
  if (recovery !== null) {
42659
- this.#completed.delete(turn.command.sessionId);
42660
- this.#completed.set(turn.command.sessionId, {
42661
- command: turn.command,
42662
- recovery: { ...recovery, state: "completed" }
42663
- });
42664
- while (this.#completed.size > COMPLETED_TURN_RECOVERY_LIMIT) {
42665
- const oldestSessionId = this.#completed.keys().next().value;
42666
- if (oldestSessionId === void 0) {
42667
- break;
42668
- }
42669
- this.#completed.delete(oldestSessionId);
42670
- }
42672
+ this.retainLatestCompletedRecovery(turn.command, { ...recovery, state: "completed" });
42671
42673
  }
42672
42674
  turn.finish();
42673
42675
  };
42676
+ retainLatestCompletedRecovery = (command, recovery) => {
42677
+ this.#latestCompletedBySessionId.delete(command.sessionId);
42678
+ this.#latestCompletedBySessionId.set(command.sessionId, { command, recovery });
42679
+ while (this.#latestCompletedBySessionId.size > MAX_SESSIONS_WITH_COMPLETED_RECOVERY) {
42680
+ const oldestSessionId = this.#latestCompletedBySessionId.keys().next().value;
42681
+ if (oldestSessionId === void 0) {
42682
+ break;
42683
+ }
42684
+ this.#latestCompletedBySessionId.delete(oldestSessionId);
42685
+ }
42686
+ };
42674
42687
  recordEvent = (event) => {
42675
42688
  if ("commandId" in event) {
42676
- const turn = this.#turns.get(event.commandId);
42689
+ const turn = this.#inFlightByCommandId.get(event.commandId);
42677
42690
  if (turn !== void 0 && turn.command.sessionId === event.sessionId) {
42678
42691
  return turn.recordEvent(event);
42679
42692
  }
42680
42693
  }
42681
42694
  if (event.kind === SESSION_PERMISSION_RESOLVED_KIND || event.kind === SESSION_ELICITATION_RESOLVED_KIND) {
42682
- for (const turn of this.#turns.values()) {
42695
+ for (const turn of this.#inFlightByCommandId.values()) {
42683
42696
  if (turn.matchesInteraction(event.interactionId, event.sessionId)) {
42684
42697
  return turn.recordEvent(event);
42685
42698
  }
@@ -42688,17 +42701,17 @@ var DaemonTurnRegistry = class {
42688
42701
  return event;
42689
42702
  };
42690
42703
  recordSteeringPrompt = (command) => {
42691
- const target = this.#turns.get(command.targetCommandId);
42704
+ const target = this.#inFlightByCommandId.get(command.targetCommandId);
42692
42705
  return target?.recordSteeringPrompt(command) ?? false;
42693
42706
  };
42694
42707
  executionSnapshot = async (matches) => {
42695
42708
  const matching = [];
42696
- for (const turn of this.#turns.values()) {
42709
+ for (const turn of this.#inFlightByCommandId.values()) {
42697
42710
  if (await matches(turn.command)) {
42698
42711
  matching.push(turn);
42699
42712
  }
42700
42713
  }
42701
- const current = matching.filter((turn) => this.#turns.get(turn.command.commandId) === turn);
42714
+ const current = matching.filter((turn) => this.#inFlightByCommandId.get(turn.command.commandId) === turn);
42702
42715
  return {
42703
42716
  turn: current.map((turn) => turn.snapshot()).find((snapshot) => snapshot !== null) ?? null,
42704
42717
  queued: current.filter((turn) => turn.isQueued()).map(({ command }) => ({
@@ -42709,7 +42722,7 @@ var DaemonTurnRegistry = class {
42709
42722
  };
42710
42723
  };
42711
42724
  snapshot = (matches) => {
42712
- for (const turn of this.#turns.values()) {
42725
+ for (const turn of this.#inFlightByCommandId.values()) {
42713
42726
  if (!matches(turn.command)) {
42714
42727
  continue;
42715
42728
  }
@@ -42721,7 +42734,7 @@ var DaemonTurnRegistry = class {
42721
42734
  return null;
42722
42735
  };
42723
42736
  recovery = (matches) => {
42724
- for (const turn of this.#turns.values()) {
42737
+ for (const turn of this.#inFlightByCommandId.values()) {
42725
42738
  if (!matches(turn.command)) {
42726
42739
  continue;
42727
42740
  }
@@ -42730,7 +42743,7 @@ var DaemonTurnRegistry = class {
42730
42743
  return recovery;
42731
42744
  }
42732
42745
  }
42733
- for (const completed of this.#completed.values()) {
42746
+ for (const completed of this.#latestCompletedBySessionId.values()) {
42734
42747
  if (matches(completed.command)) {
42735
42748
  return completed.recovery;
42736
42749
  }
@@ -42738,17 +42751,17 @@ var DaemonTurnRegistry = class {
42738
42751
  return null;
42739
42752
  };
42740
42753
  close = async () => {
42741
- const turns = [...this.#turns.values()];
42754
+ const turns = [...this.#inFlightByCommandId.values()];
42742
42755
  for (const turn of turns) {
42743
42756
  turn.abortDaemon();
42744
42757
  }
42745
42758
  await Promise.all(turns.map((turn) => turn.completion));
42746
- this.#completed.clear();
42759
+ this.#latestCompletedBySessionId.clear();
42747
42760
  };
42748
42761
  admitAndWait = async (turn, waitSignal, onQueued) => {
42749
42762
  const command = turn.command;
42750
42763
  waitSignal.throwIfAborted();
42751
- const head = () => [...this.#turns.values()].find(
42764
+ const head = () => [...this.#inFlightByCommandId.values()].find(
42752
42765
  (turn2) => turn2.command.sessionId === command.sessionId && turn2.command.disposition !== SessionPromptDisposition.Steer
42753
42766
  );
42754
42767
  if (command.disposition === SessionPromptDisposition.Queue) {
@@ -45318,6 +45331,7 @@ function paginateActiveTurn(history, activeTurn, sessionId, endIndex, limit) {
45318
45331
  historicalCharacterLimit
45319
45332
  ) : null;
45320
45333
  return {
45334
+ streamPosition: { commandId: activeTurn.commandId, epoch: activeTurn.epoch, sequence: activeTurn.sequence },
45321
45335
  coveredCommandIds: historical?.coveredCommandIds ?? [],
45322
45336
  coveredMessageIds: [
45323
45337
  ...historical?.coveredMessageIds ?? [],
@@ -45350,6 +45364,7 @@ function mergeLiveSessionHistory(history, activeTurn, sessionId) {
45350
45364
  }
45351
45365
  return {
45352
45366
  coveredCommandIds: page.coveredCommandIds,
45367
+ ...page.streamPosition === void 0 ? {} : { streamPosition: page.streamPosition },
45353
45368
  coveredMessageIds: page.coveredMessageIds,
45354
45369
  messages: page.messages,
45355
45370
  status: "available",
@@ -46965,7 +46980,7 @@ async function runRelayConnection({
46965
46980
  return;
46966
46981
  }
46967
46982
  if (kind === SESSION_EVENT_REJECTED_KIND) {
46968
- publisher.quarantine(message);
46983
+ publisher.reject(message);
46969
46984
  return;
46970
46985
  }
46971
46986
  commands.handle(message);
@@ -46992,250 +47007,84 @@ async function runRelayConnection({
46992
47007
  }
46993
47008
  }
46994
47009
 
46995
- // src/relay-connection/session-event-outbox.ts
47010
+ // src/relay-connection/session-event-publisher.ts
47011
+ import { WebSocket as WebSocket9 } from "undici";
47012
+
47013
+ // src/relay-connection/session-event-queue.ts
46996
47014
  import { randomUUID as randomUUID7 } from "node:crypto";
46997
- import { mkdirSync as mkdirSync3 } from "node:fs";
46998
- import { dirname as dirname6 } from "node:path";
46999
- import { DatabaseSync } from "node:sqlite";
47000
- var DEFAULT_STANDARD_EVENT_BYTES = 96 * 1024 * 1024;
47001
- var DEFAULT_CRITICAL_EVENT_BYTES = 128 * 1024 * 1024;
47002
- var MAX_DEAD_LETTERS = 1024;
47003
- var DEFAULT_LIMITS = {
47004
- criticalEventBytes: DEFAULT_CRITICAL_EVENT_BYTES,
47005
- standardEventBytes: DEFAULT_STANDARD_EVENT_BYTES
47006
- };
47007
- var SessionEventOutboxCapacityError = class extends Error {
47008
- code = "session_event_outbox_full";
47015
+ var STANDARD_EVENT_BYTES = 96 * 1024 * 1024;
47016
+ var CRITICAL_EVENT_BYTES = 128 * 1024 * 1024;
47017
+ var SessionEventQueueCapacityError = class extends Error {
47018
+ code = "session_event_queue_full";
47009
47019
  constructor() {
47010
- super("The persistent Session event outbox is full");
47011
- this.name = "SessionEventOutboxCapacityError";
47020
+ super("The in-memory Session event queue is full");
47021
+ this.name = "SessionEventQueueCapacityError";
47012
47022
  }
47013
47023
  };
47014
- function rowNumber(row, field) {
47015
- const value = typeof row === "object" && row !== null ? Reflect.get(row, field) : void 0;
47016
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
47017
- throw new Error(`SQLite returned an invalid ${field} value`);
47018
- }
47019
- return value;
47020
- }
47021
- function rowString(row, field) {
47022
- const value = typeof row === "object" && row !== null ? Reflect.get(row, field) : void 0;
47023
- if (typeof value !== "string" || value.length === 0) {
47024
- throw new Error(`SQLite returned an invalid ${field} value`);
47025
- }
47026
- return value;
47027
- }
47028
- function validateLimits(limits) {
47029
- if (!Number.isSafeInteger(limits.standardEventBytes) || limits.standardEventBytes < 1 || !Number.isSafeInteger(limits.criticalEventBytes) || limits.criticalEventBytes < limits.standardEventBytes) {
47030
- throw new Error("Session event outbox limits are invalid");
47031
- }
47032
- }
47033
- var SessionEventOutbox = class {
47034
- #database;
47035
- #limits;
47036
- constructor(path3, limits = DEFAULT_LIMITS) {
47037
- validateLimits(limits);
47038
- mkdirSync3(dirname6(path3), { mode: 448, recursive: true });
47039
- this.#limits = limits;
47040
- this.#database = new DatabaseSync(path3);
47041
- this.#database.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL; PRAGMA busy_timeout = 5000;");
47042
- this.#database.exec(`
47043
- CREATE TABLE IF NOT EXISTS relay_session_event_outbox (
47044
- ordinal INTEGER PRIMARY KEY AUTOINCREMENT,
47045
- event_id TEXT NOT NULL UNIQUE,
47046
- session_id TEXT NOT NULL,
47047
- source_seq INTEGER NOT NULL UNIQUE,
47048
- byte_length INTEGER NOT NULL,
47049
- envelope_json TEXT NOT NULL
47050
- ) STRICT;
47051
- CREATE INDEX IF NOT EXISTS relay_session_event_outbox_order
47052
- ON relay_session_event_outbox(source_seq);
47053
- CREATE TABLE IF NOT EXISTS relay_session_event_outbox_state (
47054
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
47055
- stream_id TEXT NOT NULL,
47056
- next_source_seq INTEGER NOT NULL,
47057
- total_bytes INTEGER NOT NULL
47058
- ) STRICT;
47059
- CREATE TABLE IF NOT EXISTS relay_session_event_dead_letters (
47060
- ordinal INTEGER PRIMARY KEY AUTOINCREMENT,
47061
- event_id TEXT NOT NULL UNIQUE,
47062
- session_id TEXT NOT NULL,
47063
- rejection_code TEXT NOT NULL,
47064
- envelope_json TEXT NOT NULL
47065
- ) STRICT;
47066
- `);
47067
- this.#database.prepare(
47068
- `INSERT OR IGNORE INTO relay_session_event_outbox_state (
47069
- singleton, stream_id, next_source_seq, total_bytes
47070
- ) VALUES (1, ?, 1, 0)`
47071
- ).run(randomUUID7());
47072
- }
47073
- acknowledge = (acknowledgement) => this.#remove(acknowledgement.eventId, acknowledgement.sessionId);
47074
- close = () => {
47075
- this.#database.close();
47076
- };
47077
- enqueue = (sessionId, payload, critical) => {
47078
- const envelope2 = this.enqueueBatch(sessionId, [payload], critical)[0];
47079
- if (envelope2 === void 0) {
47080
- throw new Error("The Session event outbox did not enqueue the event");
47081
- }
47082
- return envelope2;
47083
- };
47024
+ var SessionEventQueue = class {
47025
+ #streamId = randomUUID7();
47026
+ #events = /* @__PURE__ */ new Map();
47027
+ #nextSourceSeq = 1;
47028
+ #totalBytes = 0;
47084
47029
  enqueueBatch = (sessionId, payloads, critical) => {
47085
47030
  if (payloads.length < 1 || payloads.length > 32) {
47086
47031
  throw new Error("A Session event batch must contain between 1 and 32 events");
47087
47032
  }
47088
- this.#database.exec("BEGIN IMMEDIATE");
47089
- try {
47090
- const state = this.#database.prepare(
47091
- `SELECT stream_id, next_source_seq, total_bytes
47092
- FROM relay_session_event_outbox_state
47093
- WHERE singleton = 1`
47094
- ).get();
47095
- const sourceSeq = rowNumber(state, "next_source_seq");
47096
- if (sourceSeq < 1 || !Number.isSafeInteger(sourceSeq + payloads.length)) {
47097
- throw new Error("The Session event source sequence is exhausted");
47098
- }
47099
- const streamId = rowString(state, "stream_id");
47100
- const stored = payloads.map((payload, index) => {
47101
- const envelope2 = {
47102
- delivery: {
47103
- eventId: randomUUID7(),
47104
- kind: "reliable",
47105
- sourceSeq: (sourceSeq + index).toString(),
47106
- streamId
47107
- },
47108
- kind: SESSION_EVENT_KIND,
47109
- payload,
47110
- sessionId
47111
- };
47112
- const encoded = encodeSessionEventEnvelope(envelope2);
47113
- return { byteLength: Buffer.byteLength(encoded, "utf8"), encoded, envelope: envelope2 };
47114
- });
47115
- const byteLength = stored.reduce((total, entry) => total + entry.byteLength, 0);
47116
- const maximumBytes = critical ? this.#limits.criticalEventBytes : this.#limits.standardEventBytes;
47117
- if (rowNumber(state, "total_bytes") + byteLength > maximumBytes) {
47118
- throw new SessionEventOutboxCapacityError();
47119
- }
47120
- const insert = this.#database.prepare(
47121
- `INSERT INTO relay_session_event_outbox (
47122
- event_id, session_id, source_seq, byte_length, envelope_json
47123
- ) VALUES (?, ?, ?, ?, ?)`
47124
- );
47125
- for (const entry of stored) {
47126
- insert.run(
47127
- entry.envelope.delivery.eventId,
47128
- sessionId,
47129
- Number(entry.envelope.delivery.sourceSeq),
47130
- entry.byteLength,
47131
- entry.encoded
47132
- );
47133
- }
47134
- this.#database.prepare(
47135
- `UPDATE relay_session_event_outbox_state
47136
- SET next_source_seq = next_source_seq + ?,
47137
- total_bytes = total_bytes + ?
47138
- WHERE singleton = 1`
47139
- ).run(stored.length, byteLength);
47140
- this.#database.exec("COMMIT");
47141
- return stored.map((entry) => entry.envelope);
47142
- } catch (error62) {
47143
- this.#database.exec("ROLLBACK");
47144
- throw error62;
47033
+ if (!Number.isSafeInteger(this.#nextSourceSeq + payloads.length)) {
47034
+ throw new Error("The Session event source sequence is exhausted");
47145
47035
  }
47146
- };
47147
- pending = (limit, excludedEventIds) => {
47148
- if (!Number.isSafeInteger(limit) || limit < 1) {
47149
- throw new Error("Session event replay limit must be a positive integer");
47150
- }
47151
- const exclusion = excludedEventIds.length === 0 ? "" : `WHERE event_id NOT IN (${excludedEventIds.map(() => "?").join(", ")})`;
47152
- return this.#database.prepare(
47153
- `SELECT envelope_json
47154
- FROM relay_session_event_outbox
47155
- ${exclusion}
47156
- ORDER BY source_seq
47157
- LIMIT ?`
47158
- ).all(...excludedEventIds, limit).map((row) => {
47159
- const envelope2 = decodeSessionEventEnvelope(rowString(row, "envelope_json"));
47160
- if (!("delivery" in envelope2)) {
47161
- throw new Error("SQLite returned an invalid reliable Session event envelope");
47162
- }
47163
- return envelope2;
47036
+ const entries = payloads.map((payload, index) => {
47037
+ const envelope2 = {
47038
+ delivery: {
47039
+ eventId: randomUUID7(),
47040
+ kind: "reliable",
47041
+ sourceSeq: (this.#nextSourceSeq + index).toString(),
47042
+ streamId: this.#streamId
47043
+ },
47044
+ kind: SESSION_EVENT_KIND,
47045
+ payload,
47046
+ sessionId
47047
+ };
47048
+ return { envelope: envelope2, byteLength: Buffer.byteLength(encodeSessionEventEnvelope(envelope2), "utf8") };
47164
47049
  });
47050
+ const byteLength = entries.reduce((total, entry) => total + entry.byteLength, 0);
47051
+ if (this.#totalBytes + byteLength > (critical ? CRITICAL_EVENT_BYTES : STANDARD_EVENT_BYTES)) {
47052
+ throw new SessionEventQueueCapacityError();
47053
+ }
47054
+ for (const entry of entries) {
47055
+ this.#events.set(entry.envelope.delivery.eventId, entry);
47056
+ }
47057
+ this.#nextSourceSeq += entries.length;
47058
+ this.#totalBytes += byteLength;
47165
47059
  };
47166
- quarantine = (rejection) => {
47167
- this.#database.exec("BEGIN IMMEDIATE");
47168
- try {
47169
- const stored = this.#database.prepare(
47170
- `SELECT byte_length, envelope_json
47171
- FROM relay_session_event_outbox
47172
- WHERE event_id = ? AND session_id = ?`
47173
- ).get(rejection.eventId, rejection.sessionId);
47174
- if (stored === void 0) {
47175
- this.#database.exec("COMMIT");
47176
- return false;
47060
+ pending = (limit, excludedEventIds) => {
47061
+ const result = [];
47062
+ for (const [eventId, entry] of this.#events) {
47063
+ if (!excludedEventIds.has(eventId)) {
47064
+ result.push(entry.envelope);
47065
+ if (result.length === limit) {
47066
+ break;
47067
+ }
47177
47068
  }
47178
- this.#database.prepare(
47179
- `INSERT INTO relay_session_event_dead_letters (
47180
- event_id, session_id, rejection_code, envelope_json
47181
- ) VALUES (?, ?, ?, ?)`
47182
- ).run(rejection.eventId, rejection.sessionId, rejection.code, rowString(stored, "envelope_json"));
47183
- this.#deleteStored(rejection.eventId, rejection.sessionId, rowNumber(stored, "byte_length"));
47184
- this.#database.prepare(
47185
- `DELETE FROM relay_session_event_dead_letters
47186
- WHERE ordinal NOT IN (
47187
- SELECT ordinal FROM relay_session_event_dead_letters
47188
- ORDER BY ordinal DESC LIMIT ?
47189
- )`
47190
- ).run(MAX_DEAD_LETTERS);
47191
- this.#database.exec("COMMIT");
47192
- return true;
47193
- } catch (error62) {
47194
- this.#database.exec("ROLLBACK");
47195
- throw error62;
47196
47069
  }
47070
+ return result;
47197
47071
  };
47198
- #deleteStored = (eventId, sessionId, byteLength) => {
47199
- const deletion = this.#database.prepare(
47200
- `DELETE FROM relay_session_event_outbox
47201
- WHERE event_id = ? AND session_id = ?`
47202
- ).run(eventId, sessionId);
47203
- if (deletion.changes !== 1) {
47204
- throw new Error("The Session event could not be removed from the outbox");
47205
- }
47206
- const state = this.#database.prepare(
47207
- `UPDATE relay_session_event_outbox_state
47208
- SET total_bytes = total_bytes - ?
47209
- WHERE singleton = 1 AND total_bytes >= ?`
47210
- ).run(byteLength, byteLength);
47211
- if (state.changes !== 1) {
47212
- throw new Error("The Session event outbox capacity state is inconsistent");
47072
+ remove = (eventId, sessionId) => {
47073
+ const entry = this.#events.get(eventId);
47074
+ if (entry === void 0 || entry.envelope.sessionId !== sessionId) {
47075
+ return false;
47213
47076
  }
47077
+ this.#events.delete(eventId);
47078
+ this.#totalBytes -= entry.byteLength;
47079
+ return true;
47214
47080
  };
47215
- #remove = (eventId, sessionId) => {
47216
- this.#database.exec("BEGIN IMMEDIATE");
47217
- try {
47218
- const stored = this.#database.prepare(
47219
- `SELECT byte_length
47220
- FROM relay_session_event_outbox
47221
- WHERE event_id = ? AND session_id = ?`
47222
- ).get(eventId, sessionId);
47223
- if (stored === void 0) {
47224
- this.#database.exec("COMMIT");
47225
- return false;
47226
- }
47227
- this.#deleteStored(eventId, sessionId, rowNumber(stored, "byte_length"));
47228
- this.#database.exec("COMMIT");
47229
- return true;
47230
- } catch (error62) {
47231
- this.#database.exec("ROLLBACK");
47232
- throw error62;
47233
- }
47081
+ clear = () => {
47082
+ this.#events.clear();
47083
+ this.#totalBytes = 0;
47234
47084
  };
47235
47085
  };
47236
47086
 
47237
47087
  // src/relay-connection/session-event-publisher.ts
47238
- import { WebSocket as WebSocket9 } from "undici";
47239
47088
  var SESSION_EVENT_REPLAY_WINDOW = 16;
47240
47089
  var SESSION_EVENT_REPLAY_BYTE_LIMIT = 32 * 1024 * 1024;
47241
47090
  function reservesSessionEventCapacity(event) {
@@ -47246,18 +47095,17 @@ function reservesSessionEventCapacity(event) {
47246
47095
  }
47247
47096
  var DaemonSessionEventPublisher = class {
47248
47097
  #inFlight = /* @__PURE__ */ new Map();
47249
- #outbox;
47098
+ #queue = new SessionEventQueue();
47250
47099
  #reportedCapacityKinds = /* @__PURE__ */ new Set();
47251
47100
  #timer;
47252
47101
  #connection = { kind: "disconnected" };
47253
47102
  #deferredReplay = null;
47254
47103
  #inFlightBytes = 0;
47255
- constructor(outbox, timer) {
47256
- this.#outbox = outbox;
47104
+ constructor(timer) {
47257
47105
  this.#timer = timer;
47258
47106
  }
47259
47107
  acknowledge = (acknowledgement) => {
47260
- if (!this.#outbox.acknowledge(acknowledgement)) {
47108
+ if (!this.#queue.remove(acknowledgement.eventId, acknowledgement.sessionId)) {
47261
47109
  return;
47262
47110
  }
47263
47111
  this.#removeInFlight(acknowledgement.eventId);
@@ -47268,7 +47116,7 @@ var DaemonSessionEventPublisher = class {
47268
47116
  this.#deferredReplay = null;
47269
47117
  this.#connection = { kind: "disconnected" };
47270
47118
  this.#clearInFlight();
47271
- this.#outbox.close();
47119
+ this.#queue.clear();
47272
47120
  };
47273
47121
  connect = (socket, delivery) => {
47274
47122
  this.#connection = { delivery, kind: "connected", socket };
@@ -47297,16 +47145,18 @@ var DaemonSessionEventPublisher = class {
47297
47145
  };
47298
47146
  publish = (sessionId, event) => {
47299
47147
  try {
47300
- this.#outbox.enqueue(sessionId, encodeRecordedSessionEvent(event), reservesSessionEventCapacity(event));
47148
+ this.#queue.enqueueBatch(
47149
+ sessionId,
47150
+ [encodeRecordedSessionEvent(event)],
47151
+ reservesSessionEventCapacity(event)
47152
+ );
47301
47153
  } catch (error62) {
47302
- if (!(error62 instanceof SessionEventOutboxCapacityError)) {
47154
+ if (!(error62 instanceof SessionEventQueueCapacityError)) {
47303
47155
  throw error62;
47304
47156
  }
47305
47157
  if (!this.#reportedCapacityKinds.has(event.kind)) {
47306
47158
  this.#reportedCapacityKinds.add(event.kind);
47307
- console.warn(
47308
- `Mobius daemon could not persist "${event.kind}" because the Session event outbox is full`
47309
- );
47159
+ console.warn(`Mobius daemon could not buffer "${event.kind}" because the Session event queue is full`);
47310
47160
  }
47311
47161
  return;
47312
47162
  }
@@ -47314,11 +47164,11 @@ var DaemonSessionEventPublisher = class {
47314
47164
  };
47315
47165
  publishBatch = (sessionId, events) => {
47316
47166
  const critical = events.some(reservesSessionEventCapacity);
47317
- this.#outbox.enqueueBatch(sessionId, events.map(encodeRecordedSessionEvent), critical);
47167
+ this.#queue.enqueueBatch(sessionId, events.map(encodeRecordedSessionEvent), critical);
47318
47168
  this.#publishNextBatch();
47319
47169
  };
47320
- quarantine = (rejection) => {
47321
- if (!this.#outbox.quarantine(rejection)) {
47170
+ reject = (rejection) => {
47171
+ if (!this.#queue.remove(rejection.eventId, rejection.sessionId)) {
47322
47172
  return;
47323
47173
  }
47324
47174
  this.#removeInFlight(rejection.eventId);
@@ -47334,9 +47184,10 @@ var DaemonSessionEventPublisher = class {
47334
47184
  return;
47335
47185
  }
47336
47186
  while (this.#inFlight.size < SESSION_EVENT_REPLAY_WINDOW) {
47337
- const batch = this.#outbox.pending(SESSION_EVENT_REPLAY_WINDOW - this.#inFlight.size, [
47338
- ...this.#inFlight.keys()
47339
- ]);
47187
+ const batch = this.#queue.pending(
47188
+ SESSION_EVENT_REPLAY_WINDOW - this.#inFlight.size,
47189
+ new Set(this.#inFlight.keys())
47190
+ );
47340
47191
  let sent = 0;
47341
47192
  for (const envelope2 of batch) {
47342
47193
  const encoded = encodeDaemonToServiceMessage(
@@ -47355,12 +47206,7 @@ var DaemonSessionEventPublisher = class {
47355
47206
  return;
47356
47207
  }
47357
47208
  if (this.#connection.delivery === "ephemeral") {
47358
- this.#outbox.acknowledge({
47359
- eventId: envelope2.delivery.eventId,
47360
- kind: "session.event.ack",
47361
- outcome: { kind: "relayed" },
47362
- sessionId: envelope2.sessionId
47363
- });
47209
+ this.#queue.remove(envelope2.delivery.eventId, envelope2.sessionId);
47364
47210
  sent += 1;
47365
47211
  continue;
47366
47212
  }
@@ -47473,11 +47319,7 @@ async function runDaemon(config2, audit, signal, timer = systemDaemonTimer, opti
47473
47319
  join18(stateDirectory, "sessions", `${createHash6("sha256").update(config2.channelId).digest("hex")}.sqlite`)
47474
47320
  );
47475
47321
  const operations = new DaemonOperationState();
47476
- const channelHash = createHash6("sha256").update(config2.channelId).digest("hex");
47477
- const publisher = new DaemonSessionEventPublisher(
47478
- new SessionEventOutbox(join18(stateDirectory, "events", `${channelHash}.sqlite`)),
47479
- timer
47480
- );
47322
+ const publisher = new DaemonSessionEventPublisher(timer);
47481
47323
  const activeTurns = new DaemonTurnRegistry(operations);
47482
47324
  const recordSessionEvent = (sessionId, event) => {
47483
47325
  publisher.publish(sessionId, activeTurns.recordEvent(event));