lody 0.46.0 → 0.46.2-next.1

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 (2) hide show
  1. package/dist/index.js +655 -31
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -22780,7 +22780,7 @@ Event: ${getEventDescription(event)}`);
22780
22780
  const mergedOptions = {
22781
22781
  ...options,
22782
22782
  dsn: options.dsn ?? "https://080f9de535ff335a1a0440d0e385f796@o4510491299086336.ingest.us.sentry.io/4510559045681152",
22783
- environment: options.environment ?? "production",
22783
+ environment: options.environment ?? "staging",
22784
22784
  sendClientReports: options.sendClientReports ?? true,
22785
22785
  transport: options.transport ?? makeNodeTransport,
22786
22786
  stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser),
@@ -36820,7 +36820,7 @@ Mongoose Error Code: ${error2.code}` : ""}`
36820
36820
  return client;
36821
36821
  }
36822
36822
  const name = "lody";
36823
- const version$4 = "0.46.0";
36823
+ const version$4 = "0.46.2-next.1";
36824
36824
  const description = "Lody Agent CLI tool for managing remote command execution";
36825
36825
  const type = "module";
36826
36826
  const main$3 = "dist/index.js";
@@ -37068,15 +37068,15 @@ Mongoose Error Code: ${error2.code}` : ""}`
37068
37068
  return "dev";
37069
37069
  }
37070
37070
  };
37071
- const getRuntimeEnv = () => normalizeRuntimeEnv("production");
37071
+ const getRuntimeEnv = () => normalizeRuntimeEnv("staging");
37072
37072
  const isDevEnv = () => getRuntimeEnv() === "dev";
37073
37073
  const runtimeEnv = getRuntimeEnv();
37074
- const environment$1 = "production";
37074
+ const environment$1 = "staging";
37075
37075
  const dsn = "https://080f9de535ff335a1a0440d0e385f796@o4510491299086336.ingest.us.sentry.io/4510559045681152";
37076
37076
  const postHogHost = process.env.LODY_POSTHOG_HOST ?? "https://us.i.posthog.com";
37077
37077
  const postHogKey = process.env.LODY_POSTHOG_KEY ?? "phc_LFS5i5WIwg4irAhrG5oJR04iYPhReVZ3DdFZOKqCkjG";
37078
- const tracesSampleRate = Number(process.env.SENTRY_TRACES_SAMPLE_RATE) || 0.2;
37079
- const profilesSampleRate = Number(process.env.SENTRY_PROFILES_SAMPLE_RATE) || 0.1;
37078
+ const tracesSampleRate = Number(process.env.SENTRY_TRACES_SAMPLE_RATE) || 1;
37079
+ const profilesSampleRate = Number(process.env.SENTRY_PROFILES_SAMPLE_RATE) || 0.25;
37080
37080
  const sentryEnabled = runtimeEnv !== "dev" && true;
37081
37081
  const postHogEnabled = runtimeEnv !== "dev" && process.env.LODY_POSTHOG_DISABLED !== "1";
37082
37082
  const release = `${name}@${version$4}`;
@@ -64608,16 +64608,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
64608
64608
  }
64609
64609
  return _v4(options, buf, offset2);
64610
64610
  }
64611
- let LODY_AUTH_URL = "https://nautical-curlew-181.convex.cloud";
64611
+ let LODY_AUTH_URL = "https://impressive-guineapig-165.convex.cloud";
64612
64612
  let LODY_AUTH_SITE_URL = "";
64613
- let LODY_SERVER_URL = "https://api.lody.ai";
64614
- let SITE_URL = "https://lody.ai";
64613
+ let LODY_SERVER_URL = "https://lody-server.lz-9c5.workers.dev";
64614
+ let SITE_URL = "https://main.lody.pages.dev";
64615
64615
  let SITE_APP_BASE_PATH = "";
64616
64616
  const loadEnv = () => {
64617
- LODY_AUTH_URL = "https://nautical-curlew-181.convex.cloud";
64617
+ LODY_AUTH_URL = "https://impressive-guineapig-165.convex.cloud";
64618
64618
  LODY_AUTH_SITE_URL = "";
64619
- LODY_SERVER_URL = "https://api.lody.ai";
64620
- SITE_URL = "https://lody.ai";
64619
+ LODY_SERVER_URL = "https://lody-server.lz-9c5.workers.dev";
64620
+ SITE_URL = "https://main.lody.pages.dev";
64621
64621
  SITE_APP_BASE_PATH = process.env["SITE_APP_BASE_PATH"] ?? "";
64622
64622
  };
64623
64623
  const MACHINE_ID_FILE_NAME = "machine-id";
@@ -74740,6 +74740,59 @@ Task description:
74740
74740
  }
74741
74741
  return CONTAINER_TYPES.includes(value.kind());
74742
74742
  }
74743
+ class EphemeralStore {
74744
+ constructor(timeout2 = 3e4) {
74745
+ this.inner = new EphemeralStoreWasm(timeout2);
74746
+ this.timeout = timeout2;
74747
+ }
74748
+ apply(bytes) {
74749
+ this.inner.apply(bytes);
74750
+ this.startTimerIfNotEmpty();
74751
+ }
74752
+ set(key2, value) {
74753
+ this.inner.set(key2, value);
74754
+ this.startTimerIfNotEmpty();
74755
+ }
74756
+ delete(key2) {
74757
+ this.inner.delete(key2);
74758
+ }
74759
+ get(key2) {
74760
+ return this.inner.get(key2);
74761
+ }
74762
+ getAllStates() {
74763
+ return this.inner.getAllStates();
74764
+ }
74765
+ encode(key2) {
74766
+ return this.inner.encode(key2);
74767
+ }
74768
+ encodeAll() {
74769
+ return this.inner.encodeAll();
74770
+ }
74771
+ keys() {
74772
+ return this.inner.keys();
74773
+ }
74774
+ destroy() {
74775
+ clearInterval(this.timer);
74776
+ }
74777
+ subscribe(listener) {
74778
+ return this.inner.subscribe(listener);
74779
+ }
74780
+ subscribeLocalUpdates(listener) {
74781
+ return this.inner.subscribeLocalUpdates(listener);
74782
+ }
74783
+ startTimerIfNotEmpty() {
74784
+ if (this.inner.isEmpty() || this.timer != null) {
74785
+ return;
74786
+ }
74787
+ this.timer = setInterval(() => {
74788
+ this.inner.removeOutdated();
74789
+ if (this.inner.isEmpty()) {
74790
+ clearInterval(this.timer);
74791
+ this.timer = void 0;
74792
+ }
74793
+ }, this.timeout / 2);
74794
+ }
74795
+ }
74743
74796
  LoroDoc.prototype.toJsonWithReplacer = function(replacer) {
74744
74797
  const processed = /* @__PURE__ */ new Set();
74745
74798
  const doc = this;
@@ -82790,6 +82843,58 @@ ${tailedOutput}` : null;
82790
82843
  compress: compressStreamsSnapshot,
82791
82844
  decompress: decompressStreamsSnapshot
82792
82845
  };
82846
+ const LODY_PRESENCE_CHANNEL = "presence";
82847
+ const LODY_PRESENCE_TTL_MS = 6e4;
82848
+ const LODY_PRESENCE_HEARTBEAT_MS = 2e4;
82849
+ const LODY_PRESENCE_STREAM_HEARTBEAT_MS = 15e3;
82850
+ const ActiveSessionStatusSchema = discriminatedUnion("type", [
82851
+ object({
82852
+ type: literal("running")
82853
+ }),
82854
+ object({
82855
+ type: literal("requestPermission"),
82856
+ requestId: string$2().optional(),
82857
+ toolCallId: string$2().optional(),
82858
+ toolTitle: string$2().optional()
82859
+ }),
82860
+ object({
82861
+ type: literal("initializing"),
82862
+ stage: _enum([
82863
+ "git-clone",
82864
+ "acp",
82865
+ "resuming"
82866
+ ]).optional(),
82867
+ detail: string$2().optional()
82868
+ })
82869
+ ]);
82870
+ const PresenceInstanceIdSchema = string$2().min(1).transform((value) => {
82871
+ return value;
82872
+ });
82873
+ const PresenceMachineStateSchema = object({
82874
+ kind: literal("machine"),
82875
+ machineId: string$2().min(1).transform((value) => value),
82876
+ instanceId: PresenceInstanceIdSchema,
82877
+ updatedAt: number$3().finite()
82878
+ });
82879
+ const PresenceSessionStateSchema = object({
82880
+ kind: literal("session"),
82881
+ sessionId: string$2().min(1).transform((value) => value),
82882
+ machineId: string$2().min(1).transform((value) => value),
82883
+ instanceId: PresenceInstanceIdSchema,
82884
+ status: ActiveSessionStatusSchema,
82885
+ updatedAt: number$3().finite()
82886
+ });
82887
+ discriminatedUnion("kind", [
82888
+ PresenceMachineStateSchema,
82889
+ PresenceSessionStateSchema
82890
+ ]);
82891
+ const getLodyMachinePresenceKey = (machineId, instanceId) => `machine:${encodeURIComponent(machineId)}:${encodeURIComponent(instanceId)}`;
82892
+ const getLodySessionPresenceKey = (sessionId, instanceId) => `session:${encodeURIComponent(sessionId)}:${encodeURIComponent(instanceId)}`;
82893
+ const toLodyPresenceStreamUrl = (durableStreamUrl) => {
82894
+ const url = new URL(durableStreamUrl);
82895
+ url.searchParams.set("ephemeral", LODY_PRESENCE_CHANNEL);
82896
+ return url.toString();
82897
+ };
82793
82898
  const isLoroRepoDocDeleted = (value) => {
82794
82899
  if (!value || typeof value !== "object") {
82795
82900
  return false;
@@ -92966,6 +93071,368 @@ stream:${scope2.streamId}`;
92966
93071
  };
92967
93072
  }
92968
93073
  };
93074
+ function createNoopAdapter() {
93075
+ return {
93076
+ emptyVersion: () => ({}),
93077
+ mergeVersions: (base) => base,
93078
+ exportSnapshot: () => new Uint8Array(),
93079
+ applySnapshot: () => ({}),
93080
+ exportUpdates: () => void 0,
93081
+ applyRemoteUpdates: () => ({}),
93082
+ subscribeLocalUpdates: () => () => void 0
93083
+ };
93084
+ }
93085
+ function normalizeEphemeralUpdate(update2) {
93086
+ if (update2 == null || update2.byteLength === 0) return null;
93087
+ return update2;
93088
+ }
93089
+ function toError$1(error2) {
93090
+ return error2 instanceof Error ? error2 : new Error(String(error2));
93091
+ }
93092
+ function closedSubscriptionError() {
93093
+ return new Error("subscription closed");
93094
+ }
93095
+ var EphemeralStreamCrdt = class {
93096
+ streamUrl;
93097
+ adaptor;
93098
+ client;
93099
+ reconnectConfig;
93100
+ heartbeatMs;
93101
+ debug;
93102
+ joinState;
93103
+ constructor(options) {
93104
+ this.streamUrl = options.streamUrl;
93105
+ this.adaptor = options.adaptor;
93106
+ this.reconnectConfig = {
93107
+ ...DEFAULT_RECONNECT_CONFIG,
93108
+ ...options.reconnectConfig
93109
+ };
93110
+ this.heartbeatMs = options.heartbeatMs ?? 15e3;
93111
+ this.debug = options.debug ?? false;
93112
+ this.client = new StreamsTransportClient({
93113
+ streamUrl: options.streamUrl,
93114
+ adapter: createNoopAdapter(),
93115
+ authProvider: normalizeAuthProvider(options.auth),
93116
+ fetchImpl: options.fetch ?? globalThis.fetch.bind(globalThis),
93117
+ reconnectConfig: this.reconnectConfig
93118
+ });
93119
+ }
93120
+ async join(params) {
93121
+ const existing = this.joinState;
93122
+ if (existing != null) try {
93123
+ await existing.started;
93124
+ return {
93125
+ ok: true,
93126
+ value: this.createSubscription(existing, params)
93127
+ };
93128
+ } catch (error2) {
93129
+ return {
93130
+ ok: false,
93131
+ error: toTransportError(error2)
93132
+ };
93133
+ }
93134
+ const state2 = this.createJoinState();
93135
+ state2.started = this.startJoin(state2);
93136
+ this.joinState = state2;
93137
+ try {
93138
+ await state2.started;
93139
+ return {
93140
+ ok: true,
93141
+ value: this.createSubscription(state2, params)
93142
+ };
93143
+ } catch (error2) {
93144
+ await this.disposeJoinState(state2);
93145
+ return {
93146
+ ok: false,
93147
+ error: toTransportError(error2)
93148
+ };
93149
+ }
93150
+ }
93151
+ rejoin() {
93152
+ const state2 = this.joinState;
93153
+ if (state2 == null || state2.closed) return;
93154
+ state2.retryAttempt = 0;
93155
+ state2.retrySleepController.abort();
93156
+ state2.retrySleepController = new AbortController();
93157
+ const previousController = state2.requestController;
93158
+ state2.requestController = new AbortController();
93159
+ previousController?.abort();
93160
+ if (state2.readStatus === "disconnected" || state2.readStatus === "error") {
93161
+ this.setReadStatus(state2, "reconnecting");
93162
+ this.runReadLoop(state2);
93163
+ }
93164
+ if (state2.writeStatus === "error") {
93165
+ this.setWriteStatus(state2, "reconnecting");
93166
+ this.flushPendingLocal(state2);
93167
+ }
93168
+ }
93169
+ async close() {
93170
+ const state2 = this.joinState;
93171
+ if (state2 != null) await this.disposeJoinState(state2);
93172
+ }
93173
+ createJoinState() {
93174
+ return {
93175
+ refCount: 0,
93176
+ closed: false,
93177
+ status: "connecting",
93178
+ readStatus: "connecting",
93179
+ writeStatus: "connecting",
93180
+ statusListeners: /* @__PURE__ */ new Set(),
93181
+ pendingLocal: new Deque(),
93182
+ syncWaiters: [],
93183
+ flushingLocal: false,
93184
+ retryAttempt: 0,
93185
+ retrySleepController: new AbortController(),
93186
+ firstOpenSettled: false,
93187
+ started: Promise.resolve()
93188
+ };
93189
+ }
93190
+ async startJoin(state2) {
93191
+ state2.requestController = new AbortController();
93192
+ const firstOpen = this.waitForFirstOpen(state2);
93193
+ this.runReadLoop(state2);
93194
+ await firstOpen;
93195
+ if (state2.closed) return;
93196
+ state2.unsubscribeLocal = this.adaptor.subscribeLocalUpdates((update2) => {
93197
+ this.enqueueLocal(state2, normalizeEphemeralUpdate(update2));
93198
+ });
93199
+ this.setWriteStatus(state2, "ok");
93200
+ const fullState = normalizeEphemeralUpdate(await this.adaptor.encodeAll());
93201
+ if (fullState != null) state2.pendingLocal.pushBack(fullState);
93202
+ this.startHeartbeat(state2);
93203
+ await this.flushPendingLocal(state2);
93204
+ if (state2.status === "error" || state2.status === "disconnected") throw new Error(`ephemeral stream failed to join: ${state2.status}`);
93205
+ }
93206
+ waitForFirstOpen(state2) {
93207
+ return new Promise((resolve2, reject) => {
93208
+ state2.firstOpenResolve = resolve2;
93209
+ state2.firstOpenReject = reject;
93210
+ });
93211
+ }
93212
+ createSubscription(state2, params) {
93213
+ state2.refCount += 1;
93214
+ const statusListener = params?.onStatusChange;
93215
+ if (statusListener != null) {
93216
+ state2.statusListeners.add(statusListener);
93217
+ statusListener(state2.status);
93218
+ }
93219
+ let unsubscribed = false;
93220
+ return {
93221
+ unsubscribe: () => {
93222
+ if (unsubscribed) return;
93223
+ unsubscribed = true;
93224
+ if (statusListener != null) state2.statusListeners.delete(statusListener);
93225
+ state2.refCount = Math.max(0, state2.refCount - 1);
93226
+ if (state2.refCount === 0 && !state2.closed) this.disposeJoinState(state2);
93227
+ },
93228
+ get connected() {
93229
+ return state2.status === "joined";
93230
+ },
93231
+ get status() {
93232
+ return state2.status;
93233
+ },
93234
+ onStatusChange: (listener) => this.onStatusChange(state2, listener),
93235
+ waitUntilSynced: async () => {
93236
+ if (state2.closed) throw closedSubscriptionError();
93237
+ const remaining = state2.pendingLocal.length;
93238
+ if (remaining === 0) return;
93239
+ return new Promise((resolve2, reject) => {
93240
+ state2.syncWaiters.push({
93241
+ remaining,
93242
+ resolve: resolve2,
93243
+ reject
93244
+ });
93245
+ });
93246
+ }
93247
+ };
93248
+ }
93249
+ onStatusChange(state2, listener) {
93250
+ state2.statusListeners.add(listener);
93251
+ listener(state2.status);
93252
+ return () => {
93253
+ state2.statusListeners.delete(listener);
93254
+ };
93255
+ }
93256
+ async disposeJoinState(state2) {
93257
+ if (state2.closed) return;
93258
+ state2.closed = true;
93259
+ state2.unsubscribeLocal?.();
93260
+ if (state2.heartbeatTimer != null) clearInterval(state2.heartbeatTimer);
93261
+ state2.requestController?.abort();
93262
+ state2.retrySleepController.abort();
93263
+ this.rejectFirstOpen(state2, closedSubscriptionError());
93264
+ this.rejectSyncWaiters(state2, closedSubscriptionError());
93265
+ if (this.joinState === state2) this.joinState = void 0;
93266
+ }
93267
+ resolveFirstOpen(state2) {
93268
+ if (state2.firstOpenSettled) return;
93269
+ state2.firstOpenSettled = true;
93270
+ state2.firstOpenResolve?.();
93271
+ }
93272
+ rejectFirstOpen(state2, error2) {
93273
+ if (state2.firstOpenSettled) return;
93274
+ state2.firstOpenSettled = true;
93275
+ state2.firstOpenReject?.(error2);
93276
+ }
93277
+ async runReadLoop(state2) {
93278
+ while (!state2.closed) {
93279
+ state2.requestController ??= new AbortController();
93280
+ const capturedController = state2.requestController;
93281
+ try {
93282
+ const result = await this.client.readEphemeralSse(async (bootstrap) => {
93283
+ if (bootstrap.byteLength > 0) await this.adaptor.applyRemoteUpdates([
93284
+ bootstrap
93285
+ ]);
93286
+ this.setReadStatus(state2, "ok");
93287
+ this.resolveFirstOpen(state2);
93288
+ if (state2.unsubscribeLocal != null) this.enqueueFullState(state2);
93289
+ }, async (update2) => {
93290
+ if (update2.byteLength > 0) await this.adaptor.applyRemoteUpdates([
93291
+ update2
93292
+ ]);
93293
+ }, capturedController.signal);
93294
+ if (state2.closed) return;
93295
+ if (result.kind === "ok") {
93296
+ state2.retryAttempt = 0;
93297
+ this.setReadStatus(state2, "reconnecting");
93298
+ await sleep$1(0);
93299
+ continue;
93300
+ }
93301
+ if (result.kind === "unsupported") throw new Error("ephemeral stream requires SSE support");
93302
+ if (result.kind === "not-found") throw new HttpError("ephemeral stream not found", 404);
93303
+ } catch (error2) {
93304
+ if (state2.closed) return;
93305
+ if (capturedController.signal.aborted) {
93306
+ state2.requestController = new AbortController();
93307
+ this.setReadStatus(state2, "reconnecting");
93308
+ continue;
93309
+ }
93310
+ const classified = classifyFetchError(error2);
93311
+ if (!classified.retriable || isAuthOrProtocolError(error2)) {
93312
+ this.logError("ephemeral live read failed", error2, {
93313
+ streamUrl: this.streamUrl,
93314
+ code: classified.code
93315
+ });
93316
+ this.setReadStatus(state2, "error");
93317
+ this.rejectFirstOpen(state2, toError$1(error2));
93318
+ return;
93319
+ }
93320
+ this.setReadStatus(state2, "reconnecting");
93321
+ if (state2.retryAttempt >= this.reconnectConfig.maxAttempts) {
93322
+ this.logError("ephemeral live reconnect exhausted", error2, {
93323
+ streamUrl: this.streamUrl,
93324
+ attempts: state2.retryAttempt
93325
+ });
93326
+ this.setReadStatus(state2, "disconnected");
93327
+ this.rejectFirstOpen(state2, toError$1(error2));
93328
+ return;
93329
+ }
93330
+ const delay2 = computeRetryDelay(state2.retryAttempt, this.reconnectConfig);
93331
+ state2.retryAttempt += 1;
93332
+ await sleep$1(delay2, state2.retrySleepController.signal);
93333
+ }
93334
+ }
93335
+ }
93336
+ startHeartbeat(state2) {
93337
+ if (this.heartbeatMs === false || this.heartbeatMs <= 0) return;
93338
+ state2.heartbeatTimer = setInterval(() => {
93339
+ this.enqueueFullState(state2);
93340
+ }, this.heartbeatMs);
93341
+ }
93342
+ async enqueueFullState(state2) {
93343
+ try {
93344
+ const update2 = await this.adaptor.encodeAll();
93345
+ this.enqueueLocal(state2, normalizeEphemeralUpdate(update2));
93346
+ } catch (error2) {
93347
+ this.logError("ephemeral encodeAll failed", error2, {
93348
+ streamUrl: this.streamUrl
93349
+ });
93350
+ this.setWriteStatus(state2, "error");
93351
+ }
93352
+ }
93353
+ enqueueLocal(state2, update2) {
93354
+ if (state2.closed || update2 == null || update2.byteLength === 0) return;
93355
+ state2.pendingLocal.pushBack(update2);
93356
+ this.flushPendingLocal(state2);
93357
+ }
93358
+ async flushPendingLocal(state2) {
93359
+ if (state2.flushingLocal || state2.closed) return;
93360
+ state2.flushingLocal = true;
93361
+ try {
93362
+ while (!state2.closed) {
93363
+ const update2 = state2.pendingLocal.peekFront();
93364
+ if (update2 == null) return;
93365
+ let attempt = 0;
93366
+ while (!state2.closed) try {
93367
+ await this.client.appendEphemeralUpdate(update2);
93368
+ state2.pendingLocal.popFront();
93369
+ this.notifySyncWaiters(state2, 1);
93370
+ this.setWriteStatus(state2, "ok");
93371
+ break;
93372
+ } catch (error2) {
93373
+ const classified = classifyFetchError(error2);
93374
+ if (!classified.retriable || isAuthOrProtocolError(error2)) {
93375
+ this.rejectSyncWaiters(state2, new Error(`ephemeral append failed: ${classified.code}`));
93376
+ this.setWriteStatus(state2, "error");
93377
+ return;
93378
+ }
93379
+ if (attempt >= this.reconnectConfig.maxAttempts) {
93380
+ this.rejectSyncWaiters(state2, new Error("ephemeral append retry budget exhausted"));
93381
+ this.setWriteStatus(state2, "error");
93382
+ return;
93383
+ }
93384
+ this.setWriteStatus(state2, "reconnecting");
93385
+ const delay2 = computeRetryDelay(attempt, this.reconnectConfig);
93386
+ attempt += 1;
93387
+ await sleep$1(delay2, state2.retrySleepController.signal);
93388
+ }
93389
+ }
93390
+ } finally {
93391
+ state2.flushingLocal = false;
93392
+ }
93393
+ }
93394
+ notifySyncWaiters(state2, flushedCount) {
93395
+ let i2 = 0;
93396
+ while (i2 < state2.syncWaiters.length) {
93397
+ const waiter = state2.syncWaiters[i2];
93398
+ waiter.remaining -= flushedCount;
93399
+ if (waiter.remaining <= 0) {
93400
+ state2.syncWaiters.splice(i2, 1);
93401
+ waiter.resolve();
93402
+ } else i2 += 1;
93403
+ }
93404
+ }
93405
+ rejectSyncWaiters(state2, error2) {
93406
+ for (const waiter of state2.syncWaiters.splice(0)) waiter.reject(error2);
93407
+ }
93408
+ setReadStatus(state2, readStatus) {
93409
+ if (state2.readStatus === readStatus) return;
93410
+ state2.readStatus = readStatus;
93411
+ this.recomputeStatus(state2);
93412
+ }
93413
+ setWriteStatus(state2, writeStatus) {
93414
+ if (state2.writeStatus === writeStatus) return;
93415
+ state2.writeStatus = writeStatus;
93416
+ this.recomputeStatus(state2);
93417
+ }
93418
+ recomputeStatus(state2) {
93419
+ const status = this.computeStatus(state2);
93420
+ if (state2.status === status) return;
93421
+ state2.status = status;
93422
+ for (const listener of state2.statusListeners) listener(status);
93423
+ }
93424
+ computeStatus(state2) {
93425
+ if (state2.readStatus === "error" || state2.writeStatus === "error") return "error";
93426
+ if (state2.readStatus === "disconnected") return "disconnected";
93427
+ if (state2.readStatus === "connecting" || state2.writeStatus === "connecting") return "connecting";
93428
+ if (state2.readStatus === "reconnecting" || state2.writeStatus === "reconnecting") return "reconnecting";
93429
+ return "joined";
93430
+ }
93431
+ logError(message, error2, detail) {
93432
+ if (!this.debug) return;
93433
+ console.error(`[streams-crdt] ${message}`, error2, detail);
93434
+ }
93435
+ };
92969
93436
  function composeBeforeRemoteCursorSaveHooks(...hooks2) {
92970
93437
  const active2 = hooks2.filter((hook) => hook != null);
92971
93438
  if (active2.length === 0) return;
@@ -94494,6 +94961,15 @@ stream:${scope2.streamId}`;
94494
94961
  }
94495
94962
  };
94496
94963
  }
94964
+ function EphemeralStoreAdaptor(store) {
94965
+ return {
94966
+ encodeAll: () => store.encodeAll(),
94967
+ applyRemoteUpdates: (updates) => {
94968
+ for (const update2 of updates) store.apply(update2);
94969
+ },
94970
+ subscribeLocalUpdates: (listener) => store.subscribeLocalUpdates(listener)
94971
+ };
94972
+ }
94497
94973
  function isAfter(entry2, current2) {
94498
94974
  return entry2.physicalTime > current2.physicalTime || entry2.physicalTime === current2.physicalTime && entry2.logicalCounter > current2.logicalCounter;
94499
94975
  }
@@ -95362,6 +95838,133 @@ stream:${scope2.streamId}`;
95362
95838
  }
95363
95839
  }
95364
95840
  };
95841
+ const formatErrorMessage = (error2, options = {}) => {
95842
+ if (error2 instanceof Error) {
95843
+ return options.includeStack ? error2.stack ?? error2.message : error2.message;
95844
+ }
95845
+ if (typeof error2 === "string") {
95846
+ return error2;
95847
+ }
95848
+ return inspect$2(error2, {
95849
+ depth: 5,
95850
+ maxArrayLength: 50,
95851
+ breakLength: 120
95852
+ });
95853
+ };
95854
+ class CliPresenceRuntime {
95855
+ constructor(options) {
95856
+ this.options = options;
95857
+ const durableStreamUrl = createStreamUrl({
95858
+ bucketId: LORO_STREAMS_BUCKET_ID,
95859
+ streamId: getLoroMetaStreamId(options.workspaceId),
95860
+ baseUrl: options.streamsBaseUrl
95861
+ });
95862
+ this.transport = new EphemeralStreamCrdt({
95863
+ streamUrl: toLodyPresenceStreamUrl(durableStreamUrl),
95864
+ auth: options.auth,
95865
+ adaptor: EphemeralStoreAdaptor(this.store),
95866
+ heartbeatMs: LODY_PRESENCE_STREAM_HEARTBEAT_MS
95867
+ });
95868
+ this.machineTimer = setInterval(() => {
95869
+ this.writeMachineHeartbeat();
95870
+ }, LODY_PRESENCE_HEARTBEAT_MS);
95871
+ this.machineTimer.unref?.();
95872
+ }
95873
+ instanceId = v4();
95874
+ store = new EphemeralStore(LODY_PRESENCE_TTL_MS);
95875
+ transport;
95876
+ machineTimer;
95877
+ subscription = null;
95878
+ machineId = null;
95879
+ machineKey = null;
95880
+ sessionKeys = /* @__PURE__ */ new Map();
95881
+ started = false;
95882
+ stopped = false;
95883
+ start() {
95884
+ if (this.started || this.stopped) return;
95885
+ this.started = true;
95886
+ void this.transport.join({
95887
+ onStatusChange: (status) => {
95888
+ this.options.logger.debug(`[${this.options.workspaceId}] Loro presence room status: ${status}`);
95889
+ }
95890
+ }).then((result) => {
95891
+ if (this.stopped) {
95892
+ if (result.ok) result.value.unsubscribe();
95893
+ return;
95894
+ }
95895
+ if (result.ok) {
95896
+ this.subscription = result.value;
95897
+ return;
95898
+ }
95899
+ this.options.logger.debug(`[${this.options.workspaceId}] Failed to join Loro presence room: ${formatErrorMessage(result.error)}`);
95900
+ }).catch((error2) => {
95901
+ if (this.stopped) return;
95902
+ this.options.logger.debug(`[${this.options.workspaceId}] Failed to start Loro presence room: ${formatErrorMessage(error2)}`);
95903
+ });
95904
+ }
95905
+ async stop() {
95906
+ if (this.stopped) return;
95907
+ this.stopped = true;
95908
+ clearInterval(this.machineTimer);
95909
+ this.clearMachinePresence();
95910
+ for (const sessionId of Array.from(this.sessionKeys.keys())) {
95911
+ this.clearSessionPresence(sessionId);
95912
+ }
95913
+ this.subscription?.unsubscribe();
95914
+ this.subscription = null;
95915
+ await this.transport.close();
95916
+ this.store.destroy();
95917
+ }
95918
+ setMachineOnline(machineId) {
95919
+ if (this.stopped) return;
95920
+ this.machineId = machineId;
95921
+ this.machineKey = getLodyMachinePresenceKey(machineId, this.instanceId);
95922
+ this.writeMachineHeartbeat();
95923
+ }
95924
+ writeMachineHeartbeat() {
95925
+ if (this.stopped || !this.machineId || !this.machineKey) return;
95926
+ const state2 = {
95927
+ kind: "machine",
95928
+ machineId: this.machineId,
95929
+ instanceId: this.instanceId,
95930
+ updatedAt: getServerNow()
95931
+ };
95932
+ this.store.set(this.machineKey, state2);
95933
+ }
95934
+ setSessionPresence(args2) {
95935
+ if (this.stopped) return;
95936
+ if (!args2.status || args2.status.type === "idle") {
95937
+ this.clearSessionPresence(args2.sessionId);
95938
+ return;
95939
+ }
95940
+ if (!args2.machineId) {
95941
+ return;
95942
+ }
95943
+ const key2 = getLodySessionPresenceKey(args2.sessionId, this.instanceId);
95944
+ this.sessionKeys.set(args2.sessionId, key2);
95945
+ const state2 = {
95946
+ kind: "session",
95947
+ sessionId: args2.sessionId,
95948
+ machineId: args2.machineId,
95949
+ instanceId: this.instanceId,
95950
+ status: args2.status,
95951
+ updatedAt: getServerNow()
95952
+ };
95953
+ this.store.set(key2, state2);
95954
+ }
95955
+ clearSessionPresence(sessionId) {
95956
+ const key2 = this.sessionKeys.get(sessionId);
95957
+ if (!key2) return;
95958
+ this.sessionKeys.delete(sessionId);
95959
+ this.store.delete(key2);
95960
+ }
95961
+ clearMachinePresence() {
95962
+ if (!this.machineKey) return;
95963
+ this.store.delete(this.machineKey);
95964
+ this.machineKey = null;
95965
+ this.machineId = null;
95966
+ }
95967
+ }
95365
95968
  const findLatestUserHistoryEntry = (history) => {
95366
95969
  for (let i2 = history.length - 1; i2 >= 0; i2--) {
95367
95970
  const entry2 = history[i2];
@@ -112520,19 +113123,6 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
112520
113123
  const shutdown = shutdown$1;
112521
113124
  const unsafeOffer = unsafeOffer$1;
112522
113125
  const take$1 = take$2;
112523
- const formatErrorMessage = (error2, options = {}) => {
112524
- if (error2 instanceof Error) {
112525
- return options.includeStack ? error2.stack ?? error2.message : error2.message;
112526
- }
112527
- if (typeof error2 === "string") {
112528
- return error2;
112529
- }
112530
- return inspect$2(error2, {
112531
- depth: 5,
112532
- maxArrayLength: 50,
112533
- breakLength: 120
112534
- });
112535
- };
112536
113126
  const isRecoverableMetaRoomStatus = (status) => status === "disconnected" || status === "error";
112537
113127
  class LoroConnectionRecoveryController {
112538
113128
  recoveryEvents = runSync(unbounded());
@@ -113640,12 +114230,13 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
113640
114230
  };
113641
114231
  };
113642
114232
  class LoroDocumentManager {
113643
- constructor(repo, workspaceId, userId, metaSub, logger2, detachTransportLogger, initialTransportStatus = "disconnected", initialMetaSyncPromise = Promise.resolve(false), initialMetaSyncCompleted = false) {
114233
+ constructor(repo, workspaceId, userId, metaSub, logger2, detachTransportLogger, initialTransportStatus = "disconnected", initialMetaSyncPromise = Promise.resolve(false), initialMetaSyncCompleted = false, presenceRuntime = null) {
113644
114234
  this.repo = repo;
113645
114235
  this.workspaceId = workspaceId;
113646
114236
  this.userId = userId;
113647
114237
  this.logger = logger2;
113648
114238
  this.detachTransportLogger = detachTransportLogger;
114239
+ this.presenceRuntime = presenceRuntime;
113649
114240
  this.initialMetaSyncPromise = initialMetaSyncPromise.then((completed) => {
113650
114241
  if (completed) {
113651
114242
  this.initialMetaSyncCompleted = true;
@@ -113673,6 +114264,7 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
113673
114264
  connectionRecovery;
113674
114265
  initialMetaSyncCompleted = false;
113675
114266
  initialMetaSyncPromise;
114267
+ presenceRuntime;
113676
114268
  static async create(workspaceId, userId, token2, logger2) {
113677
114269
  const authBaseUrl = LODY_AUTH_SITE_URL ? normalizeBaseUrl(LODY_AUTH_SITE_URL) : LODY_AUTH_URL ? deriveConvexSiteUrl(LODY_AUTH_URL) : null;
113678
114270
  if (!authBaseUrl) {
@@ -113770,7 +114362,14 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
113770
114362
  } else {
113771
114363
  initialMetaSync.resolve(false);
113772
114364
  }
113773
- manager = new LoroDocumentManager(repo, workspaceId, userId, metaSub, logger2, detachTransportLogger, transportAdapter.getStatus?.() ?? "disconnected", initialMetaSync.promise, initialMetaSyncCompleted);
114365
+ const presenceRuntime = new CliPresenceRuntime({
114366
+ workspaceId,
114367
+ streamsBaseUrl: streamsTokenProvider.getGatewayBaseUrl() ?? streamsBaseUrl,
114368
+ auth: streamsTokenProvider.createAuthCallback(),
114369
+ logger: logger2
114370
+ });
114371
+ presenceRuntime.start();
114372
+ manager = new LoroDocumentManager(repo, workspaceId, userId, metaSub, logger2, detachTransportLogger, transportAdapter.getStatus?.() ?? "disconnected", initialMetaSync.promise, initialMetaSyncCompleted, presenceRuntime);
113774
114373
  return manager;
113775
114374
  }
113776
114375
  setTransportStatus(status) {
@@ -113840,7 +114439,7 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
113840
114439
  return pending2;
113841
114440
  }
113842
114441
  const initPromise2 = (async () => {
113843
- const sessionDoc = new SessionDocument(this.repo, sessionId, this.logger);
114442
+ const sessionDoc = new SessionDocument(this.repo, sessionId, this.logger, this.presenceRuntime);
113844
114443
  await sessionDoc.init();
113845
114444
  if (sessionDoc.isDestroyed) {
113846
114445
  throw new Error(`Session doc ${sessionId} was destroyed during init`);
@@ -113856,7 +114455,7 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
113856
114455
  }
113857
114456
  }
113858
114457
  async getSessionHistorySnapshot(sessionId) {
113859
- const sessionDoc = new SessionDocument(this.repo, sessionId, this.logger);
114458
+ const sessionDoc = new SessionDocument(this.repo, sessionId, this.logger, null);
113860
114459
  await sessionDoc.init({
113861
114460
  skipAutoRead: true
113862
114461
  });
@@ -113887,6 +114486,11 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
113887
114486
  sessionMeta.title = sanitizedTitle;
113888
114487
  }
113889
114488
  await this.repo.upsertDocMeta(sessionDoc.roomId, sessionMeta);
114489
+ this.presenceRuntime?.setSessionPresence({
114490
+ sessionId,
114491
+ machineId: sessionMeta.machineId,
114492
+ status: sessionMeta.status
114493
+ });
113890
114494
  return sessionId;
113891
114495
  }
113892
114496
  async getOrOpenSessionCode(options) {
@@ -113965,6 +114569,7 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
113965
114569
  ...machine,
113966
114570
  lastSeen: getServerNow()
113967
114571
  });
114572
+ this.presenceRuntime?.setMachineOnline(machineId);
113968
114573
  }
113969
114574
  async updateRateLimits(machineId, cliType, limits) {
113970
114575
  if (!this.machine) {
@@ -113994,6 +114599,7 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
113994
114599
  return;
113995
114600
  }
113996
114601
  await this.machine.sendHeartbeat();
114602
+ this.presenceRuntime?.writeMachineHeartbeat();
113997
114603
  }
113998
114604
  async restoreMachineDocument(machineId) {
113999
114605
  const machineRoomId = getMachineRoomId(machineId);
@@ -114019,6 +114625,11 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
114019
114625
  this.logger.debug(`Started watching document existence for machine ${machineId}`);
114020
114626
  }
114021
114627
  async cleanUp(options = {}) {
114628
+ try {
114629
+ await this.presenceRuntime?.stop();
114630
+ } catch (error2) {
114631
+ this.logger.debug(`[${this.workspaceId}] Failed to stop Loro presence runtime: ${formatErrorMessage(error2)}`);
114632
+ }
114022
114633
  await this.connectionRecovery.cleanUp();
114023
114634
  for (const [sessionId, pending2] of this.pendingSessionDocs) {
114024
114635
  try {
@@ -114081,10 +114692,11 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
114081
114692
  }
114082
114693
  }
114083
114694
  class SessionDocument {
114084
- constructor(repo, sessionId, logger2 = getLogger("loro")) {
114695
+ constructor(repo, sessionId, logger2 = getLogger("loro"), presenceRuntime = null) {
114085
114696
  this.repo = repo;
114086
114697
  this.sessionId = sessionId;
114087
114698
  this.logger = logger2;
114699
+ this.presenceRuntime = presenceRuntime;
114088
114700
  this.roomId = getSessionRoomId(this.sessionId);
114089
114701
  }
114090
114702
  mirror = null;
@@ -114510,9 +115122,15 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
114510
115122
  }
114511
115123
  const current2 = await this.repo.getDocMeta(this.roomId);
114512
115124
  if (isLoroRepoDocDeleted(current2)) return;
115125
+ const currentMeta = current2?.meta;
114513
115126
  await this.repo.upsertDocMeta(this.roomId, {
114514
115127
  lastRunningSeen: getServerNow()
114515
115128
  });
115129
+ this.presenceRuntime?.setSessionPresence({
115130
+ sessionId: this.sessionId,
115131
+ machineId: currentMeta?.machineId,
115132
+ status: currentMeta?.status
115133
+ });
114516
115134
  }
114517
115135
  async getStatus() {
114518
115136
  if (!this.mirror) {
@@ -114538,6 +115156,7 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
114538
115156
  this.logger.debug(`[${this.sessionId}] setStatus: doc deleted, skipping`);
114539
115157
  return;
114540
115158
  }
115159
+ const currentMeta = current2?.meta;
114541
115160
  this.logger.debug(`[${this.sessionId}] setStatus: calling upsertDocMeta (status.type=${status.type})`);
114542
115161
  const patch2 = {
114543
115162
  status
@@ -114546,6 +115165,11 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
114546
115165
  patch2.lastRunningSeen = getServerNow();
114547
115166
  }
114548
115167
  await withSlowOperationWarning(this.repo.upsertDocMeta(this.roomId, patch2), this.logger, `repo.upsertDocMeta(status=${status.type})`, this.sessionId);
115168
+ this.presenceRuntime?.setSessionPresence({
115169
+ sessionId: this.sessionId,
115170
+ machineId: currentMeta?.machineId,
115171
+ status
115172
+ });
114549
115173
  this.logger.debug(`[${this.sessionId}] setStatus: upsertDocMeta complete`);
114550
115174
  }
114551
115175
  async getHistory() {
@@ -114789,7 +115413,7 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
114789
115413
  this.historyAutoReadHandle?.dispose();
114790
115414
  this.historyAutoReadHandle = null;
114791
115415
  const status = await this.getStatus();
114792
- if (!options.preserveStatus && (status?.type === "running" || status?.type === "requestPermission")) {
115416
+ if (!options.preserveStatus && (status?.type === "running" || status?.type === "requestPermission" || status?.type === "initializing")) {
114793
115417
  await this.setStatus(SessionStatusFactory.idle());
114794
115418
  }
114795
115419
  await this.repo.unloadDoc(this.roomId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lody",
3
- "version": "0.46.0",
3
+ "version": "0.46.2-next.1",
4
4
  "description": "Lody Agent CLI tool for managing remote command execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -72,11 +72,11 @@
72
72
  "winston-transport": "^4.7.1",
73
73
  "ws": "^8.18.3",
74
74
  "zod": "^4.1.5",
75
- "@lody/cli-supervisor": "0.0.1",
76
75
  "@lody/convex": "0.0.1",
76
+ "@lody/cli-supervisor": "0.0.1",
77
77
  "@lody/loro-streams-rpc": "0.0.1",
78
- "loro-code": "0.0.1",
79
- "@lody/shared": "0.0.1"
78
+ "@lody/shared": "0.0.1",
79
+ "loro-code": "0.0.1"
80
80
  },
81
81
  "files": [
82
82
  "dist",