lody 0.72.0 → 0.73.0

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
@@ -58,7 +58,7 @@ import require$$1$5 from "node:worker_threads";
58
58
  import require$$1$7, { fileURLToPath } from "node:url";
59
59
  import require$$1$8 from "node:console";
60
60
  import require$$1$9 from "node:dns";
61
- import { C as CodeCollabV2OpenTextRequestSchema, a as CodeCollabV2RefreshTextRequestSchema, b as CodeCollabV2SaveTextRequestSchema, c as CodeCollabV2OpenCurrentDiffRequestSchema, d as CodeCollabV2OpenAllChangesDiffRequestSchema, e as CodeCollabV2OpenTurnDiffRequestSchema, f as CodeCollabV2InitDirectoryRequestSchema, g as CodeCollabV2OpenTextOkSchema, h as CodeCollabV2RefreshTextResponseSchema, i as CodeCollabV2SaveTextResponseSchema, j as CodeCollabV2OpenCurrentDiffResponseSchema, k as CodeCollabV2OpenAllChangesDiffResponseSchema, l as CodeCollabV2OpenTurnDiffResponseSchema, m as CodeCollabV2InitDirectoryOkSchema, n as CodeCollabV2LspUnsupportedSchema, o as CodeCollabV2ErrorSchema, p as CodeCollabV2RpcContentEnvelopeSchema, q as deriveCodeCollabV2ContentKeyId, r as CodeCollabV2ErrorCodeSchema, s as CodeCollabV2RpcResponseSchema, t as deriveCodeCollabV2ContentKeyBytes, u as buildCodeCollabFileIndexState, v as CODE_COLLAB_V2_TEXT_LIMITS, w as CODE_COLLAB_V2_ALL_CHANGES_DIFF_LIMITS, x as computeAllChanges, y as codeCollabFileIndexStatesEqual, z as scanGitDirectoryEntries, A as closeDirectoryQuietly, B as readCodeCollabFileIndexSignalFromFlock, D as writeCodeCollabFileIndexSignalToFlock, E as writeCodeCollabFileIndexToFlock } from "./chunks/directory-handle-D57OdGy1.js";
61
+ import { C as CodeCollabV2OpenTextRequestSchema, a as CodeCollabV2RefreshTextRequestSchema, b as CodeCollabV2SaveTextRequestSchema, c as CodeCollabV2OpenCurrentDiffRequestSchema, d as CodeCollabV2OpenAllChangesDiffRequestSchema, e as CodeCollabV2OpenTurnDiffRequestSchema, f as CodeCollabV2InitDirectoryRequestSchema, g as CodeCollabV2OpenTextOkSchema, h as CodeCollabV2RefreshTextResponseSchema, i as CodeCollabV2SaveTextResponseSchema, j as CodeCollabV2OpenCurrentDiffResponseSchema, k as CodeCollabV2OpenAllChangesDiffResponseSchema, l as CodeCollabV2OpenTurnDiffResponseSchema, m as CodeCollabV2InitDirectoryOkSchema, n as CodeCollabV2LspUnsupportedSchema, o as CodeCollabV2ErrorSchema, p as CodeCollabV2RpcContentEnvelopeSchema, q as deriveCodeCollabV2ContentKeyId, r as CodeCollabV2ErrorCodeSchema, s as CodeCollabV2RpcResponseSchema, t as deriveCodeCollabV2ContentKeyBytes, u as readCodeCollabFileIndexSignalFromFlock, w as writeCodeCollabFileIndexSignalToFlock, v as writeCodeCollabFileIndexToFlock, x as buildCodeCollabFileIndexState, y as CODE_COLLAB_V2_TEXT_LIMITS, z as CODE_COLLAB_V2_ALL_CHANGES_DIFF_LIMITS, A as computeAllChanges, B as codeCollabFileIndexStatesEqual, D as scanGitDirectoryEntries, E as closeDirectoryQuietly } from "./chunks/directory-handle-QE16b3Ol.js";
62
62
  import * as fs$2 from "node:fs/promises";
63
63
  import fs__default$3, { mkdir as mkdir$1, mkdtemp, chmod, writeFile, rm, rename, stat, lstat, symlink, readdir, unlink, readFile, open, realpath, opendir } from "node:fs/promises";
64
64
  import { pipeline } from "node:stream/promises";
@@ -4190,7 +4190,7 @@ let __tla = Promise.all([
4190
4190
  }
4191
4191
  }
4192
4192
  const name$1 = "lody";
4193
- const version$7 = "0.72.0";
4193
+ const version$7 = "0.73.0";
4194
4194
  const description$1 = "Lody Agent CLI tool for managing remote command execution";
4195
4195
  const type$2 = "module";
4196
4196
  const main$4 = "dist/index.js";
@@ -4617,22 +4617,22 @@ let __tla = Promise.all([
4617
4617
  const CODEX_REASONING_OPTIONS = [
4618
4618
  {
4619
4619
  value: "low",
4620
- name: "low",
4620
+ name: "Low",
4621
4621
  description: "Fastest responses"
4622
4622
  },
4623
4623
  {
4624
4624
  value: "medium",
4625
- name: "medium",
4625
+ name: "Medium",
4626
4626
  description: "Balanced reasoning"
4627
4627
  },
4628
4628
  {
4629
4629
  value: "high",
4630
- name: "high",
4630
+ name: "High",
4631
4631
  description: "More reasoning for difficult tasks"
4632
4632
  },
4633
4633
  {
4634
4634
  value: "xhigh",
4635
- name: "xhigh",
4635
+ name: "XHigh",
4636
4636
  description: "Extra reasoning for complex tasks"
4637
4637
  }
4638
4638
  ];
@@ -74726,6 +74726,7 @@ ${val.stack}`;
74726
74726
  return new Promise((resolve2) => setTimeout(resolve2, 0));
74727
74727
  }
74728
74728
  const YIELD_BUDGET_MS = 5;
74729
+ const DEFAULT_COMPACTION_BYTE_THRESHOLD = 100 * 1024;
74729
74730
  const nowMs = typeof globalThis.performance?.now === "function" ? globalThis.performance.now.bind(globalThis.performance) : Date.now;
74730
74731
  function stableVersion(vector) {
74731
74732
  const sorted = {};
@@ -74752,6 +74753,7 @@ ${val.stack}`;
74752
74753
  ensureFlockDocExists;
74753
74754
  retentionMs;
74754
74755
  persistDebounceMs;
74756
+ compactionByteThreshold;
74755
74757
  flocks = /* @__PURE__ */ new Map();
74756
74758
  subscriptions = /* @__PURE__ */ new Map();
74757
74759
  persistedVersions = /* @__PURE__ */ new Map();
@@ -74759,6 +74761,8 @@ ${val.stack}`;
74759
74761
  dirtyFlockDocs = /* @__PURE__ */ new Set();
74760
74762
  existenceMaterialized = /* @__PURE__ */ new Set();
74761
74763
  docsWithVisibleContent = /* @__PURE__ */ new Set();
74764
+ updateBytesSinceCompaction = /* @__PURE__ */ new Map();
74765
+ queuedCompactions = /* @__PURE__ */ new Set();
74762
74766
  debounceTimer;
74763
74767
  flushScheduled = false;
74764
74768
  operationPromise = Promise.resolve();
@@ -74769,6 +74773,8 @@ ${val.stack}`;
74769
74773
  this.retentionMs = options.retentionMs;
74770
74774
  const configured = options.persistDebounceMs;
74771
74775
  this.persistDebounceMs = typeof configured === "number" && Number.isFinite(configured) && configured >= 0 ? configured : 0;
74776
+ const configuredCompactionThreshold = options.compactionByteThreshold;
74777
+ this.compactionByteThreshold = typeof configuredCompactionThreshold === "number" && Number.isFinite(configuredCompactionThreshold) && configuredCompactionThreshold >= 0 ? Math.floor(configuredCompactionThreshold) : DEFAULT_COMPACTION_BYTE_THRESHOLD;
74772
74778
  }
74773
74779
  async openFlockDoc(flockDocId) {
74774
74780
  return this.ensureFlockDoc(flockDocId);
@@ -74822,16 +74828,26 @@ ${val.stack}`;
74822
74828
  throw error2;
74823
74829
  }
74824
74830
  }
74831
+ async persistFlockDocChanges(flockDocId, flock) {
74832
+ await this.enqueueOperation(async () => {
74833
+ if (this.flocks.get(flockDocId) !== flock) return;
74834
+ await this.persistFlockDocUpdate(flockDocId, flock);
74835
+ });
74836
+ }
74825
74837
  async unloadFlockDoc(flockDocId) {
74826
- const flock = this.flocks.get(flockDocId);
74827
- if (!flock) return;
74828
- await this.persistFlockDocUpdate(flockDocId, flock);
74829
- this.subscriptions.get(flockDocId)?.();
74830
- this.subscriptions.delete(flockDocId);
74831
- this.flocks.delete(flockDocId);
74832
- this.persistedVersions.delete(flockDocId);
74833
- this.existenceMaterialized.delete(flockDocId);
74834
- this.docsWithVisibleContent.delete(flockDocId);
74838
+ await this.enqueueOperation(async () => {
74839
+ const flock = this.flocks.get(flockDocId);
74840
+ if (!flock) return;
74841
+ await this.persistFlockDocUpdate(flockDocId, flock);
74842
+ this.subscriptions.get(flockDocId)?.();
74843
+ this.subscriptions.delete(flockDocId);
74844
+ this.flocks.delete(flockDocId);
74845
+ this.persistedVersions.delete(flockDocId);
74846
+ this.dirtyFlockDocs.delete(flockDocId);
74847
+ this.existenceMaterialized.delete(flockDocId);
74848
+ this.docsWithVisibleContent.delete(flockDocId);
74849
+ this.updateBytesSinceCompaction.delete(flockDocId);
74850
+ });
74835
74851
  }
74836
74852
  async dropFlockDoc(flockDocId) {
74837
74853
  await this.enqueueOperation(async () => {
@@ -74842,6 +74858,7 @@ ${val.stack}`;
74842
74858
  this.dirtyFlockDocs.delete(flockDocId);
74843
74859
  this.existenceMaterialized.delete(flockDocId);
74844
74860
  this.docsWithVisibleContent.delete(flockDocId);
74861
+ this.updateBytesSinceCompaction.delete(flockDocId);
74845
74862
  await this.storage?.deleteFlockDoc?.(flockDocId);
74846
74863
  });
74847
74864
  }
@@ -74850,6 +74867,7 @@ ${val.stack}`;
74850
74867
  this.flushScheduled = false;
74851
74868
  while (true) {
74852
74869
  await this.enqueueOperation(() => this.drainDirtyFlockDocs());
74870
+ await this.operationPromise;
74853
74871
  if (this.dirtyFlockDocs.size === 0 && !this.flushScheduled) break;
74854
74872
  this.flushScheduled = false;
74855
74873
  }
@@ -74871,6 +74889,8 @@ ${val.stack}`;
74871
74889
  this.dirtyFlockDocs.clear();
74872
74890
  this.existenceMaterialized.clear();
74873
74891
  this.docsWithVisibleContent.clear();
74892
+ this.updateBytesSinceCompaction.clear();
74893
+ this.queuedCompactions.clear();
74874
74894
  }
74875
74895
  }
74876
74896
  async loadOrCreateFlockDoc(flockDocId) {
@@ -74919,22 +74939,23 @@ ${val.stack}`;
74919
74939
  }
74920
74940
  if (versionEquals(previousVersion, nextVersion)) {
74921
74941
  await this.materializeExistence(flockDocId, flock);
74922
- this.dirtyFlockDocs.delete(flockDocId);
74942
+ this.clearDirtyIfStable(flockDocId, nextVersion, flock);
74923
74943
  return;
74924
74944
  }
74925
74945
  const bundle = await exportJsonUpdate(flock, previousVersion);
74926
74946
  if (!hasBundleEntries(bundle)) {
74927
74947
  await this.materializeExistence(flockDocId, flock);
74928
74948
  this.persistedVersions.set(flockDocId, nextVersion);
74929
- this.dirtyFlockDocs.delete(flockDocId);
74949
+ this.clearDirtyIfStable(flockDocId, nextVersion, flock);
74930
74950
  return;
74931
74951
  }
74952
+ const update2 = encodeMetaJsonUpdate(bundle);
74932
74953
  this.persistedVersions.set(flockDocId, nextVersion);
74933
74954
  try {
74934
74955
  await this.storage.save({
74935
74956
  type: "flock-doc-update",
74936
74957
  flockDocId,
74937
- update: encodeMetaJsonUpdate(bundle)
74958
+ update: update2
74938
74959
  });
74939
74960
  await this.materializeExistence(flockDocId, flock);
74940
74961
  this.clearDirtyIfStable(flockDocId, nextVersion, flock);
@@ -74942,6 +74963,31 @@ ${val.stack}`;
74942
74963
  this.persistedVersions.set(flockDocId, previousVersion);
74943
74964
  throw error2;
74944
74965
  }
74966
+ this.scheduleFlockDocCompaction(flockDocId, flock, update2.byteLength);
74967
+ }
74968
+ scheduleFlockDocCompaction(flockDocId, flock, updateBytes) {
74969
+ if (this.compactionByteThreshold === 0 || !this.storage?.compactFlockDoc) return;
74970
+ const total = (this.updateBytesSinceCompaction.get(flockDocId) ?? 0) + updateBytes;
74971
+ this.updateBytesSinceCompaction.set(flockDocId, total);
74972
+ if (total < this.compactionByteThreshold) return;
74973
+ this.queueFlockDocCompaction(flockDocId, flock);
74974
+ }
74975
+ queueFlockDocCompaction(flockDocId, flock) {
74976
+ if (this.queuedCompactions.has(flockDocId)) return;
74977
+ const storage = this.storage;
74978
+ if (!storage?.compactFlockDoc) return;
74979
+ this.queuedCompactions.add(flockDocId);
74980
+ this.enqueueOperation(async () => {
74981
+ try {
74982
+ await yieldToMacroTask();
74983
+ if (this.flocks.get(flockDocId) !== flock) return;
74984
+ await storage.compactFlockDoc?.(flockDocId);
74985
+ if (this.flocks.has(flockDocId)) this.updateBytesSinceCompaction.set(flockDocId, 0);
74986
+ else this.updateBytesSinceCompaction.delete(flockDocId);
74987
+ } finally {
74988
+ this.queuedCompactions.delete(flockDocId);
74989
+ }
74990
+ }).catch(logAsyncError(`Flock document "${flockDocId}" compaction`));
74945
74991
  }
74946
74992
  async materializeExistence(flockDocId, flock) {
74947
74993
  if (this.existenceMaterialized.has(flockDocId)) return;
@@ -75940,7 +75986,7 @@ ${val.stack}`;
75940
75986
  } finally {
75941
75987
  this.eventBus.popEventBy();
75942
75988
  }
75943
- await this.flockDocManager.persistFlockDoc(flockDocId, flock);
75989
+ await this.flockDocManager.persistFlockDocChanges(flockDocId, flock);
75944
75990
  }
75945
75991
  }
75946
75992
  async buildSyncPlan(options, buildOptions = {}) {
@@ -76423,7 +76469,8 @@ ${val.stack}`;
76423
76469
  storage: this.storage,
76424
76470
  ensureFlockDocExists: (flockDocId, expiresAtMs) => this.flockDocRegistry.ensureExists(flockDocId, expiresAtMs),
76425
76471
  retentionMs: getFlockDocRetentionMs,
76426
- persistDebounceMs: options.docPersistDebounceMs
76472
+ persistDebounceMs: options.docPersistDebounceMs,
76473
+ compactionByteThreshold: options.flockDocCompactionByteThreshold
76427
76474
  });
76428
76475
  if (this.storage) this.metaPersister = new MetaPersister({
76429
76476
  getMetaFlock,
@@ -85100,6 +85147,7 @@ stream:${scope2.streamId}`;
85100
85147
  return {
85101
85148
  save: (payload) => this.saveStorage(payload),
85102
85149
  compactMeta: () => this.compactMeta(),
85150
+ compactFlockDoc: (flockDocId) => this.compactFlockDoc(flockDocId),
85103
85151
  loadDoc: (docId) => this.loadDoc(docId),
85104
85152
  loadFlockDoc: (flockDocId) => this.loadFlockDoc(flockDocId),
85105
85153
  deleteDoc: (docId) => this.deleteDoc(docId),
@@ -85148,6 +85196,9 @@ stream:${scope2.streamId}`;
85148
85196
  if (state2.needsCompaction) this.finalizeFlockDocCompaction(flockDocId, state2, await exportFlockFileSnapshot(state2.flock));
85149
85197
  return state2.flock;
85150
85198
  }
85199
+ async compactFlockDoc(flockDocId) {
85200
+ await this.loadFlockDoc(flockDocId);
85201
+ }
85151
85202
  loadDocBody(docId) {
85152
85203
  const snapshotRow = this.stmts.selectDocSnapshot.get(docId);
85153
85204
  const updateRows = this.stmts.selectDocUpdates.all(docId);
@@ -107450,16 +107501,23 @@ ${value}`;
107450
107501
  },
107451
107502
  readJsonLive: async (streamId, state2, onBatch, liveOptions) => {
107452
107503
  const { client: streamClient, context: context2 } = getStreamClient(streamId, "live");
107504
+ const modePolicy = liveOptions?.modePolicy;
107505
+ const requestedMode = modePolicy?.selectRequestMode() ?? options.liveMode ?? "sse";
107506
+ let observedTransport;
107507
+ let deliveredBatch = false;
107508
+ let readError;
107453
107509
  const watchdog = createLiveIdleWatchdog(options.liveIdleTimeoutMs ?? DEFAULT_LIVE_IDLE_TIMEOUT_MS, liveOptions?.signal);
107454
107510
  try {
107455
107511
  for await (const event of streamClient.live({
107456
107512
  offset: state2.nextOffset ?? "now",
107457
- mode: options.liveMode ?? "sse",
107513
+ mode: requestedMode,
107458
107514
  signal: watchdog.signal
107459
107515
  })) {
107460
107516
  watchdog.reset();
107517
+ observedTransport = event.mode;
107461
107518
  switch (event.type) {
107462
107519
  case "data":
107520
+ deliveredBatch = true;
107463
107521
  state2.nextOffset = event.nextOffset;
107464
107522
  state2.cursor = event.cursor;
107465
107523
  await onBatch({
@@ -107470,6 +107528,7 @@ ${value}`;
107470
107528
  });
107471
107529
  break;
107472
107530
  case "up_to_date": {
107531
+ deliveredBatch = true;
107473
107532
  state2.nextOffset = event.nextOffset;
107474
107533
  state2.cursor = event.cursor;
107475
107534
  await onBatch({
@@ -107481,6 +107540,7 @@ ${value}`;
107481
107540
  break;
107482
107541
  }
107483
107542
  case "eof": {
107543
+ deliveredBatch = true;
107484
107544
  state2.nextOffset = event.nextOffset;
107485
107545
  state2.cursor = void 0;
107486
107546
  await onBatch({
@@ -107498,8 +107558,19 @@ ${value}`;
107498
107558
  throw toLoroGatewayError("read live updates from", streamId, event.error, context2);
107499
107559
  }
107500
107560
  }
107561
+ } catch (error2) {
107562
+ readError = error2;
107563
+ throw error2;
107501
107564
  } finally {
107502
107565
  watchdog.dispose();
107566
+ if (modePolicy && liveOptions?.signal?.aborted !== true) {
107567
+ modePolicy.noteReadOutcome({
107568
+ requestedMode,
107569
+ observedTransport,
107570
+ deliveredBatch,
107571
+ error: readError
107572
+ });
107573
+ }
107503
107574
  }
107504
107575
  }
107505
107576
  };
@@ -107962,6 +108033,8 @@ ${value}`;
107962
108033
  constructor(options) {
107963
108034
  this.options = options;
107964
108035
  this.responseStreamId = options.responseStreamId ?? `${getLoroWorkspaceRpcResponseStreamId(options.workspaceId)}:${crypto.randomUUID()}`;
108036
+ this.liveModePolicy = options.liveModePolicy;
108037
+ this.lastReportedTransport = options.liveModePolicy?.getDiagnostics().transport;
107965
108038
  }
107966
108039
  responseStreamId;
107967
108040
  responseState = {
@@ -107969,8 +108042,14 @@ ${value}`;
107969
108042
  };
107970
108043
  pending = /* @__PURE__ */ new Map();
107971
108044
  stopController = new AbortController();
108045
+ liveModePolicy;
108046
+ currentReadController = null;
108047
+ lastReportedTransport;
107972
108048
  startedPromise = null;
107973
108049
  stopped = false;
108050
+ getLiveModeDiagnostics() {
108051
+ return this.liveModePolicy?.getDiagnostics() ?? null;
108052
+ }
107974
108053
  getResponseStreamId() {
107975
108054
  return this.responseStreamId;
107976
108055
  }
@@ -107987,6 +108066,7 @@ ${value}`;
107987
108066
  return new Promise((resolve2) => {
107988
108067
  const timeoutId = setTimeout(() => {
107989
108068
  this.pending.delete(requestId);
108069
+ this.noteLiveModeResponseTimeout();
107990
108070
  resolve2(null);
107991
108071
  }, registration.timeoutMs);
107992
108072
  this.pending.set(requestId, {
@@ -108033,8 +108113,39 @@ ${value}`;
108033
108113
  this.pending.delete(requestId);
108034
108114
  }
108035
108115
  }
108116
+ noteLiveModeResponseTimeout() {
108117
+ if (!this.liveModePolicy) {
108118
+ return;
108119
+ }
108120
+ this.liveModePolicy.noteResponseTimeout();
108121
+ if (!this.reportLiveModeChange()) {
108122
+ return;
108123
+ }
108124
+ this.currentReadController?.abort();
108125
+ }
108126
+ reportLiveModeChange() {
108127
+ const diagnostics2 = this.liveModePolicy?.getDiagnostics();
108128
+ if (!diagnostics2 || diagnostics2.transport === this.lastReportedTransport) {
108129
+ return false;
108130
+ }
108131
+ this.lastReportedTransport = diagnostics2.transport;
108132
+ const label2 = this.options.logLabel ?? this.options.workspaceId;
108133
+ this.options.trace?.("machine rpc live transport changed", {
108134
+ workspaceId: this.options.workspaceId,
108135
+ responseStreamId: this.responseStreamId,
108136
+ ...diagnostics2
108137
+ });
108138
+ this.options.logger?.warn?.(`[rpc-client:${label2}] live transport -> ${diagnostics2.transport} (${diagnostics2.reason})`);
108139
+ return true;
108140
+ }
108036
108141
  async runResponseLoop() {
108037
108142
  while (!this.stopped) {
108143
+ const readController = new AbortController();
108144
+ const onStop = () => readController.abort();
108145
+ this.stopController.signal.addEventListener("abort", onStop, {
108146
+ once: true
108147
+ });
108148
+ this.currentReadController = readController;
108038
108149
  try {
108039
108150
  await this.options.streamClient.readJsonLive(this.responseStreamId, this.responseState, async (batch) => {
108040
108151
  this.responseState.nextOffset = batch.nextOffset ?? this.responseState.nextOffset;
@@ -108043,7 +108154,8 @@ ${value}`;
108043
108154
  await this.handleRawResponse(raw2);
108044
108155
  }
108045
108156
  }, {
108046
- signal: this.stopController.signal
108157
+ signal: readController.signal,
108158
+ modePolicy: this.liveModePolicy
108047
108159
  });
108048
108160
  } catch (error2) {
108049
108161
  if (this.stopped) {
@@ -108065,6 +108177,10 @@ ${value}`;
108065
108177
  const message = error2 instanceof Error ? error2.message : String(error2);
108066
108178
  this.options.logger?.warn?.(`[rpc-client:${this.options.logLabel ?? this.options.workspaceId}] response loop error: ${message}`);
108067
108179
  await delay$4(1e3);
108180
+ } finally {
108181
+ this.stopController.signal.removeEventListener("abort", onStop);
108182
+ this.currentReadController = null;
108183
+ this.reportLiveModeChange();
108068
108184
  }
108069
108185
  }
108070
108186
  }
@@ -108078,6 +108194,7 @@ ${value}`;
108078
108194
  if (!pending2) {
108079
108195
  return;
108080
108196
  }
108197
+ this.liveModePolicy?.noteResponseReceived();
108081
108198
  const receivedAtMs = Date.now();
108082
108199
  this.options.trace?.("machine rpc transport response received", {
108083
108200
  workspaceId: this.options.workspaceId,
@@ -109840,6 +109957,50 @@ ${value}`;
109840
109957
  }
109841
109958
  }
109842
109959
  }
109960
+ class CodeCollabFileIndexChangedPublishError extends Error {
109961
+ constructor(cause) {
109962
+ super("Code Collab file-index publication failed after changing local Flock state.", {
109963
+ cause
109964
+ });
109965
+ this.name = "CodeCollabFileIndexChangedPublishError";
109966
+ }
109967
+ }
109968
+ async function publishCodeCollabFileIndexFlock(args2) {
109969
+ const handle = await args2.repo.openFlockDoc(args2.flockDocId);
109970
+ if (args2.reconcileRemote) {
109971
+ await handle.syncOnce();
109972
+ }
109973
+ const changed = writeCodeCollabFileIndexToFlock(handle.flock, args2.fileIndex, args2.updatedAtMs);
109974
+ if (!changed) {
109975
+ return {
109976
+ changed: false
109977
+ };
109978
+ }
109979
+ try {
109980
+ await args2.repo.flush();
109981
+ await handle.syncOnce();
109982
+ } catch (error2) {
109983
+ throw new CodeCollabFileIndexChangedPublishError(error2);
109984
+ }
109985
+ return {
109986
+ changed: true
109987
+ };
109988
+ }
109989
+ async function publishCodeCollabFileIndexSignalFlock(args2) {
109990
+ const handle = await args2.repo.openFlockDoc(args2.flockDocId);
109991
+ await handle.syncOnce();
109992
+ const previous = readCodeCollabFileIndexSignalFromFlock(handle.flock);
109993
+ const revision = (previous?.r ?? 0) + 1;
109994
+ const changed = writeCodeCollabFileIndexSignalToFlock(handle.flock, revision, args2.updatedAtMs);
109995
+ if (changed) {
109996
+ await args2.repo.flush();
109997
+ await handle.syncOnce();
109998
+ }
109999
+ return {
110000
+ changed,
110001
+ revision
110002
+ };
110003
+ }
109843
110004
  const WorkspaceSummarySchema = object$1({
109844
110005
  id: string$1(),
109845
110006
  name: string$1(),
@@ -135149,6 +135310,30 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
135149
135310
  const localProjects = await readMachineLocalProjects(repo, workspaceId, machineId);
135150
135311
  return localProjects[localProjectId] ?? null;
135151
135312
  }
135313
+ const defaultSleep$1 = (ms2) => new Promise((resolve2) => setTimeout(resolve2, ms2));
135314
+ async function resolveWorkspaceLocalProjectRootPathWithRetry(repo, workspaceId, machineId, localProjectId, options = {}) {
135315
+ const maxAttempts = Math.max(1, options.maxAttempts ?? readTimeoutEnv("LODY_LOCAL_PROJECT_RESOLVE_MAX_ATTEMPTS", 4));
135316
+ const retryDelayMs = options.retryDelayMs ?? readTimeoutEnv("LODY_LOCAL_PROJECT_RESOLVE_RETRY_DELAY_MS", 400);
135317
+ const syncTimeoutMs = options.syncTimeoutMs ?? readTimeoutEnv("LODY_LOCAL_PROJECT_RESOLVE_SYNC_TIMEOUT_MS", 1500);
135318
+ const sleep2 = options.sleep ?? defaultSleep$1;
135319
+ for (let attempt = 1; ; attempt += 1) {
135320
+ const rootPath = await resolveWorkspaceLocalProjectRootPath(repo, workspaceId, machineId, localProjectId);
135321
+ if (rootPath !== null) {
135322
+ return rootPath;
135323
+ }
135324
+ if (attempt >= maxAttempts) {
135325
+ return null;
135326
+ }
135327
+ options.onRetry?.(attempt, maxAttempts);
135328
+ if (options.requestSync) {
135329
+ try {
135330
+ await withTimeout$3(options.requestSync(), syncTimeoutMs, `Timeout waiting for machine Flock sync (localProject=${localProjectId})`);
135331
+ } catch {
135332
+ }
135333
+ }
135334
+ await sleep2(retryDelayMs);
135335
+ }
135336
+ }
135152
135337
  class ConcurrentQueue {
135153
135338
  constructor(maxConcurrency = 30) {
135154
135339
  this.maxConcurrency = maxConcurrency;
@@ -136455,6 +136640,15 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
136455
136640
  }
136456
136641
  return (await checkoutLocalProjectBranchAtRootPath(workdir, branch)).currentBranch;
136457
136642
  }
136643
+ resolveLocalProjectWorkdirForTurn(localProjectId) {
136644
+ return resolveWorkspaceLocalProjectRootPathWithRetry(this.deps.workspaceDocument.repo, this.deps.workspaceId, this.deps.machineId, localProjectId, {
136645
+ requestSync: () => this.deps.workspaceDocument.syncMachineFlockDoc(this.deps.machineId, {
136646
+ reason: "session-local-project-resolve",
136647
+ timeoutMs: readTimeoutEnv("LODY_LOCAL_PROJECT_RESOLVE_SYNC_TIMEOUT_MS", 1500)
136648
+ }),
136649
+ onRetry: (attempt, maxAttempts) => this.deps.logger.debug(`Local project ${localProjectId} not visible in machine Flock yet; pulling machine Flock and retrying (attempt ${attempt}/${maxAttempts})`)
136650
+ });
136651
+ }
136458
136652
  async handleTurnError(sessionId, sessionDoc, error2) {
136459
136653
  await this.deps.turnFinalization.finalizeACPState(sessionId);
136460
136654
  await this.persistCodeCollabTurnDiffsAfterACPFinalization(sessionId, this.currentTurnBySession.get(sessionId));
@@ -137171,7 +137365,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
137171
137365
  const meta = yield* self2.tryPromise(() => sessionDoc.getMetaState());
137172
137366
  project = project ?? self2.resolveProjectFromMeta(meta, message.project?.branch);
137173
137367
  const localProjectId = project?.kind === "local" ? project.localProjectId : void 0;
137174
- const restoreWorkdir = localProjectId ? (yield* self2.tryPromise(() => resolveWorkspaceLocalProjectRootPath(self2.deps.workspaceDocument.repo, self2.deps.workspaceId, self2.deps.machineId, localProjectId))) ?? void 0 : void 0;
137368
+ const restoreWorkdir = localProjectId ? (yield* self2.tryPromise(() => self2.resolveLocalProjectWorkdirForTurn(localProjectId))) ?? void 0 : void 0;
137175
137369
  if (project?.kind === "local" && !restoreWorkdir) {
137176
137370
  const missingMessage = `Local project not found in workspace: ${project.localProjectId}`;
137177
137371
  self2.deps.logger.warn(`[${sessionId}] ${missingMessage}`);
@@ -137649,7 +137843,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
137649
137843
  const githubRepoFullName = resolveProjectGitHubRepo(project);
137650
137844
  const shouldPrepareWorktree = project?.kind === "github" && !!githubRepoFullName || project?.kind === "local" && project.useWorktree === true;
137651
137845
  let branch = project?.branch?.trim() || void 0;
137652
- const workdir = project?.kind === "local" ? await resolveWorkspaceLocalProjectRootPath(this.deps.workspaceDocument.repo, this.deps.workspaceId, this.deps.machineId, project.localProjectId) ?? void 0 : void 0;
137846
+ const workdir = project?.kind === "local" ? await this.resolveLocalProjectWorkdirForTurn(project.localProjectId) ?? void 0 : void 0;
137653
137847
  this.deps.logger.info(`Session create received: ${sessionId}`);
137654
137848
  const agentConfig = acpSessionConfig;
137655
137849
  const promptText = agentConfig.prompt ?? "";
@@ -138617,67 +138811,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
138617
138811
  this.releaseWaiters();
138618
138812
  }
138619
138813
  }
138620
- class SessionUserResolver {
138621
- constructor(logger2, workspaceId) {
138622
- this.logger = logger2;
138623
- this.workspaceId = workspaceId;
138624
- this.client = LODY_AUTH_URL ? new ConvexHttpClient(LODY_AUTH_URL) : null;
138625
- }
138626
- client;
138627
- cache = /* @__PURE__ */ new Map();
138628
- async resolve(userId) {
138629
- const normalizedUserId = userId.trim();
138630
- if (!normalizedUserId) {
138631
- return {
138632
- id: userId,
138633
- name: userId,
138634
- email: buildMissingEmail("lody", userId)
138635
- };
138636
- }
138637
- const existing = this.cache.get(normalizedUserId);
138638
- if (existing) {
138639
- return await existing;
138640
- }
138641
- const request2 = this.fetchUser(normalizedUserId).catch((error2) => {
138642
- this.cache.delete(normalizedUserId);
138643
- throw error2;
138644
- });
138645
- this.cache.set(normalizedUserId, request2);
138646
- return await request2;
138647
- }
138648
- clear() {
138649
- this.cache.clear();
138650
- }
138651
- async fetchUser(userId) {
138652
- if (!this.client) {
138653
- return this.fallbackUser(userId);
138654
- }
138655
- try {
138656
- const user = await this.client.query(api$1.auth.getUserById, {
138657
- userId,
138658
- workspaceId: this.workspaceId
138659
- });
138660
- const email = typeof user?.email === "string" && user.email.trim() ? user.email.trim() : buildMissingEmail("lody", userId);
138661
- const name2 = typeof user?.name === "string" && user.name.trim() ? user.name.trim() : email;
138662
- return {
138663
- id: userId,
138664
- name: name2,
138665
- email
138666
- };
138667
- } catch (error2) {
138668
- this.logger.debug(`[session-user-resolver] Failed to resolve user ${userId} in workspace ${this.workspaceId}: ${formatErrorMessage(error2)}`);
138669
- return this.fallbackUser(userId);
138670
- }
138671
- }
138672
- fallbackUser(userId) {
138673
- const email = buildMissingEmail("lody", userId);
138674
- return {
138675
- id: userId,
138676
- name: email,
138677
- email
138678
- };
138679
- }
138680
- }
138681
138814
  class AccessDenied extends TaggedError("AccessDenied") {
138682
138815
  }
138683
138816
  class AccessIndeterminate extends TaggedError("AccessIndeterminate") {
@@ -138720,6 +138853,26 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
138720
138853
  const policy = whileInput(jittered$12, (error2) => error2._tag === "AccessIndeterminate");
138721
138854
  yield* retry$2(attempt, policy).pipe(catchTag("AccessIndeterminate", () => _void));
138722
138855
  });
138856
+ async function mapWithConcurrency$1(items2, concurrency, worker) {
138857
+ if (items2.length === 0) {
138858
+ return [];
138859
+ }
138860
+ const limit2 = Math.max(1, Math.floor(concurrency));
138861
+ const results = new Array(items2.length);
138862
+ let nextIndex = 0;
138863
+ const runWorker = async () => {
138864
+ while (nextIndex < items2.length) {
138865
+ const currentIndex = nextIndex;
138866
+ nextIndex += 1;
138867
+ results[currentIndex] = await worker(items2[currentIndex], currentIndex);
138868
+ }
138869
+ };
138870
+ await Promise.all(Array.from({
138871
+ length: Math.min(limit2, items2.length)
138872
+ }, runWorker));
138873
+ return results;
138874
+ }
138875
+ const SESSION_BOOTSTRAP_RECONCILE_CONCURRENCY = 4;
138723
138876
  const isConfigOptionValueRecord = (value) => {
138724
138877
  if (!value || typeof value !== "object" || Array.isArray(value)) {
138725
138878
  return false;
@@ -138729,7 +138882,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
138729
138882
  class SessionDispatchWatcher {
138730
138883
  constructor(deps) {
138731
138884
  this.deps = deps;
138732
- this.userResolver = deps.userResolver ?? new SessionUserResolver(deps.logger, deps.workspaceId);
138885
+ this.userResolver = deps.userResolver;
138733
138886
  }
138734
138887
  watchedSessions = /* @__PURE__ */ new Map();
138735
138888
  sessionCheckChains = /* @__PURE__ */ new Map();
@@ -139030,9 +139183,9 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
139030
139183
  if (!this.started) {
139031
139184
  return;
139032
139185
  }
139033
- const reconcileFailures = await Promise.all([
139186
+ const reconcileFailures = await mapWithConcurrency$1([
139034
139187
  ...sessionRoomIds
139035
- ].map(async (roomId) => {
139188
+ ], SESSION_BOOTSTRAP_RECONCILE_CONCURRENCY, async (roomId) => {
139036
139189
  if (!this.started) {
139037
139190
  return false;
139038
139191
  }
@@ -139043,7 +139196,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
139043
139196
  } catch {
139044
139197
  return true;
139045
139198
  }
139046
- }));
139199
+ });
139047
139200
  const failedCount = reconcileFailures.filter(Boolean).length;
139048
139201
  if (failedCount > 0) {
139049
139202
  this.deps.logger.warn(`[dispatch] Owned-session bootstrap completed with ${failedCount}/${sessionRoomIds.size} session reconcile failure(s) (reason=${reason})`);
@@ -139966,6 +140119,148 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
139966
140119
  return true;
139967
140120
  }
139968
140121
  }
140122
+ const DEFAULT_AI_GIT_AUTHOR_NAME = "LodyAI";
140123
+ const DEFAULT_AI_GIT_AUTHOR_EMAIL = "agent@lody.ai";
140124
+ const trimNonEmpty$1 = (value) => {
140125
+ const trimmed2 = value?.trim();
140126
+ return trimmed2 !== void 0 && trimmed2.length > 0 ? trimmed2 : void 0;
140127
+ };
140128
+ const isUsableEmail = (email) => {
140129
+ const trimmed2 = trimNonEmpty$1(email);
140130
+ return trimmed2 !== void 0 && !isMissingEmail(trimmed2);
140131
+ };
140132
+ const buildGitHubNoreplyEmail = (githubAccountId, githubLogin) => {
140133
+ const accountId = trimNonEmpty$1(githubAccountId);
140134
+ const login = trimNonEmpty$1(githubLogin);
140135
+ if (!accountId || !login || !/^\d+$/.test(accountId)) {
140136
+ return void 0;
140137
+ }
140138
+ return `${accountId}+${login}@users.noreply.github.com`;
140139
+ };
140140
+ const normalizeName = (name2, email) => {
140141
+ if (name2 !== void 0 && !isMissingEmail(name2)) {
140142
+ return name2;
140143
+ }
140144
+ return email;
140145
+ };
140146
+ const readGitConfig = (key2, cwd) => {
140147
+ try {
140148
+ const output = execFileSync("git", [
140149
+ "config",
140150
+ key2
140151
+ ], {
140152
+ cwd,
140153
+ encoding: "utf8",
140154
+ stdio: [
140155
+ "ignore",
140156
+ "pipe",
140157
+ "ignore"
140158
+ ]
140159
+ });
140160
+ return trimNonEmpty$1(output);
140161
+ } catch {
140162
+ return void 0;
140163
+ }
140164
+ };
140165
+ const readHostDefaultGitIdentity = (cwd) => ({
140166
+ name: trimNonEmpty$1(process.env.GIT_AUTHOR_NAME) ?? trimNonEmpty$1(process.env.GIT_COMMITTER_NAME) ?? readGitConfig("user.name", cwd),
140167
+ email: trimNonEmpty$1(process.env.GIT_AUTHOR_EMAIL) ?? trimNonEmpty$1(process.env.GIT_COMMITTER_EMAIL) ?? readGitConfig("user.email", cwd)
140168
+ });
140169
+ const resolveSessionGitIdentity = (requested, defaultIdentity, cwd) => {
140170
+ const resolvedDefaultIdentity = readHostDefaultGitIdentity(cwd);
140171
+ const requestedEmail = trimNonEmpty$1(requested.email);
140172
+ if (isUsableEmail(requestedEmail)) {
140173
+ return {
140174
+ name: normalizeName(trimNonEmpty$1(requested.name), requestedEmail),
140175
+ email: requestedEmail
140176
+ };
140177
+ }
140178
+ const defaultEmail = trimNonEmpty$1(resolvedDefaultIdentity.email);
140179
+ if (isUsableEmail(defaultEmail)) {
140180
+ return {
140181
+ name: normalizeName(trimNonEmpty$1(resolvedDefaultIdentity.name), defaultEmail),
140182
+ email: defaultEmail
140183
+ };
140184
+ }
140185
+ return {
140186
+ name: DEFAULT_AI_GIT_AUTHOR_NAME,
140187
+ email: DEFAULT_AI_GIT_AUTHOR_EMAIL
140188
+ };
140189
+ };
140190
+ const trimNonEmpty = (value) => {
140191
+ const trimmed2 = value?.trim();
140192
+ return trimmed2 !== void 0 && trimmed2.length > 0 ? trimmed2 : void 0;
140193
+ };
140194
+ class SessionUserResolver {
140195
+ constructor(logger2, workspaceId, cliToken, options) {
140196
+ this.logger = logger2;
140197
+ this.workspaceId = workspaceId;
140198
+ this.cliToken = cliToken;
140199
+ this.client = LODY_AUTH_URL ? new ConvexHttpClient(LODY_AUTH_URL) : null;
140200
+ this.queryProfile = options?.queryProfile ?? (async (args2) => await this.client?.query(api$1.auth.getWorkspaceUserProfileForCliToken, args2) ?? null);
140201
+ }
140202
+ client;
140203
+ queryProfile;
140204
+ cache = /* @__PURE__ */ new Map();
140205
+ async resolve(userId) {
140206
+ const normalizedUserId = userId.trim();
140207
+ if (!normalizedUserId) {
140208
+ return {
140209
+ id: userId,
140210
+ name: userId,
140211
+ email: buildMissingEmail("lody", userId)
140212
+ };
140213
+ }
140214
+ const existing = this.cache.get(normalizedUserId);
140215
+ if (existing) {
140216
+ return await existing;
140217
+ }
140218
+ const request2 = this.fetchUser(normalizedUserId).catch((error2) => {
140219
+ this.cache.delete(normalizedUserId);
140220
+ throw error2;
140221
+ });
140222
+ this.cache.set(normalizedUserId, request2);
140223
+ return await request2;
140224
+ }
140225
+ clear() {
140226
+ this.cache.clear();
140227
+ }
140228
+ async fetchUser(userId) {
140229
+ try {
140230
+ const profile = await this.queryProfile({
140231
+ cliToken: this.cliToken,
140232
+ workspaceId: this.workspaceId,
140233
+ userId
140234
+ });
140235
+ if (!profile) {
140236
+ this.logger.debug(`[session-user-resolver] User ${userId} not resolvable in workspace ${this.workspaceId}; using placeholder identity`);
140237
+ return this.fallbackUser(userId);
140238
+ }
140239
+ return this.toSessionUserProfile(userId, profile);
140240
+ } catch (error2) {
140241
+ this.logger.debug(`[session-user-resolver] Failed to resolve user ${userId} in workspace ${this.workspaceId}: ${formatErrorMessage(error2)}`);
140242
+ return this.fallbackUser(userId);
140243
+ }
140244
+ }
140245
+ toSessionUserProfile(userId, profile) {
140246
+ const accountEmail = trimNonEmpty(profile.email);
140247
+ const email = (accountEmail && !isMissingEmail(accountEmail) ? accountEmail : void 0) ?? buildGitHubNoreplyEmail(profile.githubAccountId, profile.githubLogin) ?? accountEmail ?? buildMissingEmail("lody", userId);
140248
+ const name2 = trimNonEmpty(profile.name) ?? trimNonEmpty(profile.githubLogin) ?? email;
140249
+ return {
140250
+ id: userId,
140251
+ name: name2,
140252
+ email
140253
+ };
140254
+ }
140255
+ fallbackUser(userId) {
140256
+ const email = buildMissingEmail("lody", userId);
140257
+ return {
140258
+ id: userId,
140259
+ name: email,
140260
+ email
140261
+ };
140262
+ }
140263
+ }
139969
140264
  const DAY_MS$1 = 24 * 60 * 60 * 1e3;
139970
140265
  const TERMINAL_RETENTION_MS = 7 * DAY_MS$1;
139971
140266
  const MATERIALIZATION_CLAIM_MS = 6e4;
@@ -141043,6 +141338,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
141043
141338
  return;
141044
141339
  }
141045
141340
  const frozen = operation.frozenContinuationConfig.inputConfig;
141341
+ const requester = await this.resolveRequesterIdentity(operation.requesterUserId);
141046
141342
  await this.options.executionService.continueSession({
141047
141343
  type: "session/chat",
141048
141344
  sessionId: delivery.requesterSessionId,
@@ -141074,8 +141370,8 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
141074
141370
  },
141075
141371
  userTurnId: delivery.systemTurnId,
141076
141372
  userId: operation.requesterUserId,
141077
- userName: operation.requesterUserId,
141078
- userEmail: buildMissingEmail("lody", operation.requesterUserId)
141373
+ userName: requester.name,
141374
+ userEmail: requester.email
141079
141375
  }, {
141080
141376
  dispatchSource: "delivery",
141081
141377
  onTurnClaimed: async () => {
@@ -141087,6 +141383,26 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
141087
141383
  this.withStore((store) => store.consumeDelivery(delivery.requesterSessionId, delivery.operationId));
141088
141384
  }
141089
141385
  }
141386
+ async resolveRequesterIdentity(requesterUserId) {
141387
+ const fallback2 = {
141388
+ name: requesterUserId,
141389
+ email: buildMissingEmail("lody", requesterUserId)
141390
+ };
141391
+ const resolver = this.options.userResolver;
141392
+ if (!resolver) {
141393
+ return fallback2;
141394
+ }
141395
+ try {
141396
+ const profile = await resolver.resolve(requesterUserId);
141397
+ return {
141398
+ name: profile.name,
141399
+ email: profile.email
141400
+ };
141401
+ } catch (error2) {
141402
+ this.options.logger.debug(`[operation-coordinator] Failed to resolve requester identity ${requesterUserId}: ${error2 instanceof Error ? error2.message : String(error2)}`);
141403
+ return fallback2;
141404
+ }
141405
+ }
141090
141406
  async resolveFrozenConfiguration(operation) {
141091
141407
  const agentConfigId = operation.frozenContinuationConfig.agentConfigId;
141092
141408
  if (!agentConfigId) return "available";
@@ -147784,6 +148100,7 @@ ${entry.text}`).join("\n\n");
147784
148100
  function isCodeCollabV2ServiceError(error2) {
147785
148101
  return error2 instanceof CodeCollabV2ServiceError;
147786
148102
  }
148103
+ const DEFAULT_CODE_COLLAB_REPUBLISH_CONCURRENCY = 4;
147787
148104
  class CodeCollabV2Service {
147788
148105
  constructor(deps) {
147789
148106
  this.deps = deps;
@@ -147792,9 +148109,16 @@ ${entry.text}`).join("\n\n");
147792
148109
  publishedStateByOwnerSessionId = /* @__PURE__ */ new Map();
147793
148110
  watchByOwnerSessionId = /* @__PURE__ */ new Map();
147794
148111
  refreshQueueByOwnerSessionId = /* @__PURE__ */ new Map();
148112
+ publishChainByOwnerSessionId = /* @__PURE__ */ new Map();
148113
+ pendingFileIndexSignalByOwnerSessionId = /* @__PURE__ */ new Set();
148114
+ republishRequestedGeneration = 0;
148115
+ republishCompletedGeneration = 0;
148116
+ republishDrainPromise = null;
148117
+ disposed = false;
147795
148118
  logger = getLogger("code-collab");
147796
148119
  writeChainByAbsolutePath = /* @__PURE__ */ new Map();
147797
148120
  dispose() {
148121
+ this.disposed = true;
147798
148122
  for (const ownerSessionId of this.watchByOwnerSessionId.keys()) {
147799
148123
  this.releaseWorkspaceWatch(ownerSessionId);
147800
148124
  }
@@ -148063,25 +148387,79 @@ ${entry.text}`).join("\n\n");
148063
148387
  ...this.stateByOwnerSessionId.keys(),
148064
148388
  ...this.watchByOwnerSessionId.keys()
148065
148389
  ]);
148066
- await Promise.allSettled([
148390
+ await mapWithConcurrency$1([
148067
148391
  ...ownerSessionIds
148068
- ].map(async (sessionId) => {
148069
- await this.refreshSharedState({
148070
- sessionId
148071
- }, {
148072
- forcePublish: true
148073
- });
148074
- }));
148392
+ ], DEFAULT_CODE_COLLAB_REPUBLISH_CONCURRENCY, async (sessionId) => {
148393
+ try {
148394
+ await this.refreshSharedState({
148395
+ sessionId
148396
+ }, {
148397
+ forcePublish: true
148398
+ });
148399
+ } catch (error2) {
148400
+ this.logger.debug(`[code-collab] known file-index refresh failed ownerSessionId=${sessionId}: ${formatErrorMessage(error2)}`);
148401
+ }
148402
+ });
148075
148403
  }
148076
148404
  async republishKnownSharedStates() {
148077
- await Promise.allSettled([
148078
- ...this.stateByOwnerSessionId.entries()
148079
- ].map(async ([ownerSessionId, state2]) => {
148080
- await this.publishOwnerState(ownerSessionId, state2, {
148081
- forcePublish: true,
148082
- persistAllChangesDiffStats: true
148405
+ if (this.disposed) {
148406
+ return;
148407
+ }
148408
+ const requestedGeneration = this.republishRequestedGeneration + 1;
148409
+ this.republishRequestedGeneration = requestedGeneration;
148410
+ while (!this.disposed && this.republishCompletedGeneration < requestedGeneration) {
148411
+ this.ensureRepublishDrain();
148412
+ await this.republishDrainPromise;
148413
+ }
148414
+ }
148415
+ ensureRepublishDrain() {
148416
+ if (this.republishDrainPromise || this.disposed) {
148417
+ return;
148418
+ }
148419
+ const drain = Promise.resolve().then(async () => await this.drainRepublishRequests());
148420
+ const beforeCleanup = this.deps.testOnlyBeforeRepublishDrainCleanup;
148421
+ const drainAfterCleanupBoundary = beforeCleanup ? drain.then(beforeCleanup, async (error2) => {
148422
+ await beforeCleanup();
148423
+ throw error2;
148424
+ }) : drain;
148425
+ const managedDrain = drainAfterCleanupBoundary.finally(() => {
148426
+ if (this.republishDrainPromise === managedDrain) {
148427
+ this.republishDrainPromise = null;
148428
+ if (!this.disposed && this.republishCompletedGeneration < this.republishRequestedGeneration) {
148429
+ this.ensureRepublishDrain();
148430
+ }
148431
+ }
148432
+ });
148433
+ this.republishDrainPromise = managedDrain;
148434
+ }
148435
+ async drainRepublishRequests() {
148436
+ while (!this.disposed) {
148437
+ const targetGeneration = this.republishRequestedGeneration;
148438
+ if (this.republishCompletedGeneration >= targetGeneration) {
148439
+ return;
148440
+ }
148441
+ const ownerSessionIds = [
148442
+ ...this.stateByOwnerSessionId.keys()
148443
+ ];
148444
+ await mapWithConcurrency$1(ownerSessionIds, DEFAULT_CODE_COLLAB_REPUBLISH_CONCURRENCY, async (ownerSessionId) => {
148445
+ if (this.disposed) {
148446
+ return;
148447
+ }
148448
+ const state2 = this.stateByOwnerSessionId.get(ownerSessionId);
148449
+ if (!state2) {
148450
+ return;
148451
+ }
148452
+ try {
148453
+ await this.publishOwnerState(ownerSessionId, state2, {
148454
+ forcePublish: true,
148455
+ persistAllChangesDiffStats: true
148456
+ });
148457
+ } catch (error2) {
148458
+ this.logger.debug(`[code-collab] file-index republish failed ownerSessionId=${ownerSessionId}: ${formatErrorMessage(error2)}`);
148459
+ }
148083
148460
  });
148084
- }));
148461
+ this.republishCompletedGeneration = targetGeneration;
148462
+ }
148085
148463
  }
148086
148464
  enqueueSharedStateRefresh(ownerSessionId, refresh) {
148087
148465
  let queue2 = this.refreshQueueByOwnerSessionId.get(ownerSessionId);
@@ -148660,7 +149038,7 @@ ${entry.text}`).join("\n\n");
148660
149038
  if (missingWorkspacePaths.length === 0) {
148661
149039
  return;
148662
149040
  }
148663
- await Promise.all(missingWorkspacePaths.map(async (workspacePath) => {
149041
+ await mapWithConcurrency$1(missingWorkspacePaths, DEFAULT_CODE_COLLAB_REPUBLISH_CONCURRENCY, async (workspacePath) => {
148664
149042
  const absolutePath = path__default$1.resolve(resolved.workspaceRoot, workspacePath);
148665
149043
  const value = await classifyPathForFileTree(absolutePath).catch((error2) => {
148666
149044
  const code2 = errorCode$1(error2);
@@ -148679,7 +149057,7 @@ ${entry.text}`).join("\n\n");
148679
149057
  if (!isLazyDirectoryValue(value)) {
148680
149058
  deleteDescendantFileTreeEntries(state2.fileTree, workspacePath, fileTreeIndex);
148681
149059
  }
148682
- }));
149060
+ });
148683
149061
  }
148684
149062
  async computeAllChangesFromDiffStore(resolved) {
148685
149063
  const store = this.deps.diffStore;
@@ -148893,24 +149271,34 @@ ${entry.text}`).join("\n\n");
148893
149271
  throw new Error(`Unsupported Code Collab v2 diff snapshot: ${String(exhaustive)}`);
148894
149272
  }
148895
149273
  async publishOwnerState(ownerSessionId, state2, options = {}) {
148896
- const startedAtMs = Date.now();
148897
- if (!this.deps.publishFileIndex) {
148898
- return;
148899
- }
148900
- const cloneStartedAtMs = Date.now();
148901
- const fileTree = cloneFileTreeState(state2.fileTree);
148902
- const allChanges = cloneAllChangesState(state2.allChanges);
148903
- const cloneMs = Date.now() - cloneStartedAtMs;
148904
- const buildStartedAtMs = Date.now();
148905
- const fileIndex = buildCodeCollabFileIndexState(fileTree, allChanges);
148906
- const buildMs = Date.now() - buildStartedAtMs;
148907
- await this.publishPreparedOwnerFileIndex(ownerSessionId, fileIndex, options, {
148908
- startedAtMs,
148909
- cloneMs,
148910
- buildMs
149274
+ await this.enqueueOwnerPublication(ownerSessionId, async () => {
149275
+ const startedAtMs = Date.now();
149276
+ if (!this.deps.publishFileIndex || this.disposed) {
149277
+ return;
149278
+ }
149279
+ const cloneStartedAtMs = Date.now();
149280
+ const fileTree = cloneFileTreeState(state2.fileTree);
149281
+ const allChanges = cloneAllChangesState(state2.allChanges);
149282
+ const cloneMs = Date.now() - cloneStartedAtMs;
149283
+ const buildStartedAtMs = Date.now();
149284
+ const fileIndex = buildCodeCollabFileIndexState(fileTree, allChanges);
149285
+ const buildMs = Date.now() - buildStartedAtMs;
149286
+ await this.publishPreparedOwnerFileIndexNow(ownerSessionId, fileIndex, options, {
149287
+ startedAtMs,
149288
+ cloneMs,
149289
+ buildMs
149290
+ });
148911
149291
  });
148912
149292
  }
148913
149293
  async publishPreparedOwnerFileIndex(ownerSessionId, fileIndex, options = {}, metrics = {}) {
149294
+ await this.enqueueOwnerPublication(ownerSessionId, async () => {
149295
+ if (this.disposed) {
149296
+ return;
149297
+ }
149298
+ await this.publishPreparedOwnerFileIndexNow(ownerSessionId, fileIndex, options, metrics);
149299
+ });
149300
+ }
149301
+ async publishPreparedOwnerFileIndexNow(ownerSessionId, fileIndex, options, metrics) {
148914
149302
  const startedAtMs = metrics.startedAtMs ?? Date.now();
148915
149303
  const publishFileIndex = this.deps.publishFileIndex;
148916
149304
  const publishFileIndexSignal = this.deps.publishFileIndexSignal;
@@ -148935,18 +149323,30 @@ ${entry.text}`).join("\n\n");
148935
149323
  this.publishedStateByOwnerSessionId.set(ownerSessionId, nextPublishedState);
148936
149324
  const publishStartedAtMs = Date.now();
148937
149325
  try {
148938
- await publishFileIndex({
148939
- ownerSessionId,
148940
- fileIndex,
148941
- allChangesDiffStats: summarizeFileIndexAllChanges(fileIndex),
148942
- persistAllChangesDiffStats: options.persistAllChangesDiffStats === true,
148943
- updatedAtMs
148944
- });
148945
- if (publishFileIndexSignal && (options.forcePublish === true || sameFileIndex !== true)) {
149326
+ let publishResult;
149327
+ try {
149328
+ publishResult = await publishFileIndex({
149329
+ ownerSessionId,
149330
+ fileIndex,
149331
+ allChangesDiffStats: summarizeFileIndexAllChanges(fileIndex),
149332
+ persistAllChangesDiffStats: options.persistAllChangesDiffStats === true,
149333
+ updatedAtMs,
149334
+ reconcileRemote: options.forcePublish === true
149335
+ });
149336
+ } catch (error2) {
149337
+ if (error2 instanceof CodeCollabFileIndexChangedPublishError) {
149338
+ this.pendingFileIndexSignalByOwnerSessionId.add(ownerSessionId);
149339
+ }
149340
+ throw error2;
149341
+ }
149342
+ const shouldPublishSignal = publishResult?.changed !== false || sameFileIndex !== true || this.pendingFileIndexSignalByOwnerSessionId.has(ownerSessionId);
149343
+ if (publishFileIndexSignal && shouldPublishSignal) {
149344
+ this.pendingFileIndexSignalByOwnerSessionId.add(ownerSessionId);
148946
149345
  await publishFileIndexSignal({
148947
149346
  ownerSessionId,
148948
149347
  updatedAtMs
148949
149348
  });
149349
+ this.pendingFileIndexSignalByOwnerSessionId.delete(ownerSessionId);
148950
149350
  }
148951
149351
  const publishMs = Date.now() - publishStartedAtMs;
148952
149352
  this.logger.info(`[code-collab] file-index publish completed ownerSessionId=${ownerSessionId} paths=${pathCount} durationMs=${Date.now() - startedAtMs} cloneMs=${metrics.cloneMs ?? 0} buildMs=${metrics.buildMs ?? 0} equalityMs=${equalityMs} publishMs=${publishMs} force=${options.forcePublish === true}${metrics.workerMs === void 0 ? "" : ` workerMs=${metrics.workerMs}`}`);
@@ -148961,6 +149361,18 @@ ${entry.text}`).join("\n\n");
148961
149361
  throw error2;
148962
149362
  }
148963
149363
  }
149364
+ enqueueOwnerPublication(ownerSessionId, task) {
149365
+ const previous = this.publishChainByOwnerSessionId.get(ownerSessionId) ?? Promise.resolve();
149366
+ const result = previous.catch(() => void 0).then(task);
149367
+ const settled = result.then(() => void 0, () => void 0);
149368
+ this.publishChainByOwnerSessionId.set(ownerSessionId, settled);
149369
+ void settled.finally(() => {
149370
+ if (this.publishChainByOwnerSessionId.get(ownerSessionId) === settled) {
149371
+ this.publishChainByOwnerSessionId.delete(ownerSessionId);
149372
+ }
149373
+ });
149374
+ return result;
149375
+ }
148964
149376
  }
148965
149377
  function cloneFileTreeValue(value) {
148966
149378
  return value === true ? true : {
@@ -156716,18 +157128,13 @@ ${escapeHtmlScriptContent(VISUAL_ANNOTATION_INSPECTOR_BROWSER_SCRIPT)}
156716
157128
  publishFileIndex: async (state2) => {
156717
157129
  const publishStartedAtMs = Date.now();
156718
157130
  const pathCount = Object.keys(state2.fileIndex).length;
156719
- const openStartedAtMs = Date.now();
156720
- const handle = await this.workspaceDocument.repo.openFlockDoc(getCodeCollabFileIndexFlockDocId(this.workspaceId, state2.ownerSessionId));
156721
- const openMs = Date.now() - openStartedAtMs;
156722
- const writeStartedAtMs = Date.now();
156723
- const changed = writeCodeCollabFileIndexToFlock(handle.flock, state2.fileIndex, state2.updatedAtMs);
156724
- const writeMs = Date.now() - writeStartedAtMs;
156725
- const flushStartedAtMs = Date.now();
156726
- await this.workspaceDocument.repo.flush();
156727
- const flushMs = Date.now() - flushStartedAtMs;
156728
- const syncStartedAtMs = Date.now();
156729
- await handle.syncOnce();
156730
- const syncMs = Date.now() - syncStartedAtMs;
157131
+ const result = await publishCodeCollabFileIndexFlock({
157132
+ repo: this.workspaceDocument.repo,
157133
+ flockDocId: getCodeCollabFileIndexFlockDocId(this.workspaceId, state2.ownerSessionId),
157134
+ fileIndex: state2.fileIndex,
157135
+ updatedAtMs: state2.updatedAtMs,
157136
+ reconcileRemote: state2.reconcileRemote
157137
+ });
156731
157138
  if (state2.persistAllChangesDiffStats && state2.allChangesDiffStats) {
156732
157139
  try {
156733
157140
  await this.persistCodeCollabAllChangesDiffStats(state2.ownerSessionId, state2.allChangesDiffStats);
@@ -156735,28 +157142,17 @@ ${escapeHtmlScriptContent(VISUAL_ANNOTATION_INSPECTOR_BROWSER_SCRIPT)}
156735
157142
  this.logger.debug(`[code-collab-v2] Failed to persist All Changes diffStats ownerSessionId=${state2.ownerSessionId}: ${formatErrorMessage(error2)}`);
156736
157143
  }
156737
157144
  }
156738
- this.logger.info(`[code-collab-v2] file-index flock publish completed ownerSessionId=${state2.ownerSessionId} paths=${pathCount} changed=${changed} durationMs=${Date.now() - publishStartedAtMs} openMs=${openMs} writeMs=${writeMs} flushMs=${flushMs} syncMs=${syncMs}`);
157145
+ this.logger.info(`[code-collab-v2] file-index flock publish completed ownerSessionId=${state2.ownerSessionId} paths=${pathCount} changed=${result.changed} reconcileRemote=${state2.reconcileRemote} durationMs=${Date.now() - publishStartedAtMs}`);
157146
+ return result;
156739
157147
  },
156740
157148
  publishFileIndexSignal: async (state2) => {
156741
157149
  const publishStartedAtMs = Date.now();
156742
- const openStartedAtMs = Date.now();
156743
- const handle = await this.workspaceDocument.repo.openFlockDoc(getCodeCollabFileIndexSignalFlockDocId(this.workspaceId, state2.ownerSessionId));
156744
- const openMs = Date.now() - openStartedAtMs;
156745
- const preSyncStartedAtMs = Date.now();
156746
- await handle.syncOnce();
156747
- const preSyncMs = Date.now() - preSyncStartedAtMs;
156748
- const writeStartedAtMs = Date.now();
156749
- const previous = readCodeCollabFileIndexSignalFromFlock(handle.flock);
156750
- const revision = (previous?.r ?? 0) + 1;
156751
- const changed = writeCodeCollabFileIndexSignalToFlock(handle.flock, revision, state2.updatedAtMs);
156752
- const writeMs = Date.now() - writeStartedAtMs;
156753
- const flushStartedAtMs = Date.now();
156754
- await this.workspaceDocument.repo.flush();
156755
- const flushMs = Date.now() - flushStartedAtMs;
156756
- const syncStartedAtMs = Date.now();
156757
- await handle.syncOnce();
156758
- const syncMs = Date.now() - syncStartedAtMs;
156759
- this.logger.info(`[code-collab-v2] file-index signal publish completed ownerSessionId=${state2.ownerSessionId} revision=${revision} changed=${changed} durationMs=${Date.now() - publishStartedAtMs} openMs=${openMs} preSyncMs=${preSyncMs} writeMs=${writeMs} flushMs=${flushMs} syncMs=${syncMs}`);
157150
+ const result = await publishCodeCollabFileIndexSignalFlock({
157151
+ repo: this.workspaceDocument.repo,
157152
+ flockDocId: getCodeCollabFileIndexSignalFlockDocId(this.workspaceId, state2.ownerSessionId),
157153
+ updatedAtMs: state2.updatedAtMs
157154
+ });
157155
+ this.logger.info(`[code-collab-v2] file-index signal publish completed ownerSessionId=${state2.ownerSessionId} revision=${result.revision} changed=${result.changed} durationMs=${Date.now() - publishStartedAtMs}`);
156760
157156
  }
156761
157157
  });
156762
157158
  if (typeof this.workspaceDocument.onMetaRoomSynced === "function") {
@@ -157047,11 +157443,13 @@ ${escapeHtmlScriptContent(VISUAL_ANNOTATION_INSPECTOR_BROWSER_SCRIPT)}
157047
157443
  } else {
157048
157444
  this.logger.debug("Streams RPC disabled: missing LODY_AUTH_URL / LODY_AUTH_SITE_URL");
157049
157445
  }
157446
+ this.sessionUserResolver = new SessionUserResolver(this.logger, this.workspaceId, this.token);
157050
157447
  this.sessionDispatchWatcher = new SessionDispatchWatcher({
157051
157448
  logger: this.logger,
157052
157449
  machineId: this.machineId,
157053
157450
  workspaceId: this.workspaceId,
157054
157451
  currentUserId: this.userId,
157452
+ userResolver: this.sessionUserResolver,
157055
157453
  workspaceDocument: this.workspaceDocument,
157056
157454
  executionService: this.executionService,
157057
157455
  getPreparedSessionLaunchConfig: (input2) => this.sessionManager.getPreparedSessionLaunchConfig(input2),
@@ -157081,6 +157479,7 @@ ${escapeHtmlScriptContent(VISUAL_ANNOTATION_INSPECTOR_BROWSER_SCRIPT)}
157081
157479
  workspaceDocument: this.workspaceDocument,
157082
157480
  executionService: this.executionService,
157083
157481
  dispatchWatcher: this.sessionDispatchWatcher,
157482
+ userResolver: this.sessionUserResolver,
157084
157483
  logger: this.logger,
157085
157484
  materializeTarget: async (operation, item, index) => await this.materializeOperationTarget(operation, item, index)
157086
157485
  });
@@ -157151,6 +157550,7 @@ ${escapeHtmlScriptContent(VISUAL_ANNOTATION_INSPECTOR_BROWSER_SCRIPT)}
157151
157550
  providerSetupManager;
157152
157551
  previewService;
157153
157552
  sessionDispatchWatcher;
157553
+ sessionUserResolver;
157154
157554
  operationCoordinator;
157155
157555
  autoPromptRunner;
157156
157556
  turnPostProcessingService;
@@ -171153,66 +171553,6 @@ export PATH=${toSingleQuotedShellString(ghShimBinDir)}:"$PATH"
171153
171553
  }
171154
171554
  return child.pid;
171155
171555
  }
171156
- const DEFAULT_AI_GIT_AUTHOR_NAME = "LodyAI";
171157
- const DEFAULT_AI_GIT_AUTHOR_EMAIL = "agent@lody.ai";
171158
- const trimNonEmpty = (value) => {
171159
- const trimmed2 = value?.trim();
171160
- return trimmed2 !== void 0 && trimmed2.length > 0 ? trimmed2 : void 0;
171161
- };
171162
- const isUsableEmail = (email) => {
171163
- const trimmed2 = trimNonEmpty(email);
171164
- return trimmed2 !== void 0 && !isMissingEmail(trimmed2);
171165
- };
171166
- const normalizeName = (name2, email) => {
171167
- if (name2 !== void 0 && !isMissingEmail(name2)) {
171168
- return name2;
171169
- }
171170
- return email;
171171
- };
171172
- const readGitConfig = (key2, cwd) => {
171173
- try {
171174
- const output = execFileSync("git", [
171175
- "config",
171176
- key2
171177
- ], {
171178
- cwd,
171179
- encoding: "utf8",
171180
- stdio: [
171181
- "ignore",
171182
- "pipe",
171183
- "ignore"
171184
- ]
171185
- });
171186
- return trimNonEmpty(output);
171187
- } catch {
171188
- return void 0;
171189
- }
171190
- };
171191
- const readHostDefaultGitIdentity = (cwd) => ({
171192
- name: trimNonEmpty(process.env.GIT_AUTHOR_NAME) ?? trimNonEmpty(process.env.GIT_COMMITTER_NAME) ?? readGitConfig("user.name", cwd),
171193
- email: trimNonEmpty(process.env.GIT_AUTHOR_EMAIL) ?? trimNonEmpty(process.env.GIT_COMMITTER_EMAIL) ?? readGitConfig("user.email", cwd)
171194
- });
171195
- const resolveSessionGitIdentity = (requested, defaultIdentity, cwd) => {
171196
- const resolvedDefaultIdentity = readHostDefaultGitIdentity(cwd);
171197
- const requestedEmail = trimNonEmpty(requested.email);
171198
- if (isUsableEmail(requestedEmail)) {
171199
- return {
171200
- name: normalizeName(trimNonEmpty(requested.name), requestedEmail),
171201
- email: requestedEmail
171202
- };
171203
- }
171204
- const defaultEmail = trimNonEmpty(resolvedDefaultIdentity.email);
171205
- if (isUsableEmail(defaultEmail)) {
171206
- return {
171207
- name: normalizeName(trimNonEmpty(resolvedDefaultIdentity.name), defaultEmail),
171208
- email: defaultEmail
171209
- };
171210
- }
171211
- return {
171212
- name: DEFAULT_AI_GIT_AUTHOR_NAME,
171213
- email: DEFAULT_AI_GIT_AUTHOR_EMAIL
171214
- };
171215
- };
171216
171556
  const getDefaultSessionWorkdir = (sessionId) => path__default.join(os__default.homedir(), ".lody", "chats", sessionId);
171217
171557
  const ensureDefaultSessionWorkdir = (sessionId) => {
171218
171558
  const dir = getDefaultSessionWorkdir(sessionId);
@@ -172036,7 +172376,7 @@ export PATH=${toSingleQuotedShellString(ghShimBinDir)}:"$PATH"
172036
172376
  this.sessionSandboxFactory = options?.sessionSandboxFactory ?? createSessionSandboxFactory({
172037
172377
  logger: this.logger
172038
172378
  });
172039
- this.preparationUserResolver = new SessionUserResolver(this.logger, this.workspaceId);
172379
+ this.preparationUserResolver = new SessionUserResolver(this.logger, this.workspaceId, this.token);
172040
172380
  this.preparationService = new SessionPreparationService(this.logger, {
172041
172381
  hardTtlMs: SESSION_PREPARATION_HARD_TTL_MS,
172042
172382
  maxConcurrent: MAX_CONCURRENT_SESSION_PREPARATIONS
@@ -180020,152 +180360,17 @@ Shutting down gracefully${reason}...`);
180020
180360
  throw error2;
180021
180361
  }
180022
180362
  }
180023
- async function exitLogin(code2) {
180024
- await flushTelemetry();
180025
- process.exit(code2);
180026
- }
180027
- const loginCommand = new Command("login").description("Login to Lody using device authorization flow or an API key").option("-d, --debug", "enable debug output").option("--machine-name <name>", "Machine name to register (defaults to hostname)").option("--auth <api-key>", "Use a CLI API key for non-interactive login").action(async (options) => {
180028
- if (options.debug) {
180029
- rootLogger.setDebug(true);
180030
- }
180031
- const logger2 = getLogger("login");
180032
- const authClient = new AuthClient(logger2);
180033
- const providedAuth = options.auth?.trim();
180034
- const authMethod = providedAuth ? "api_key" : "device_auth";
180035
- try {
180036
- if (options.auth !== void 0 && !providedAuth) {
180037
- captureAuthEvent("login_failed", {
180038
- auth_method: "api_key",
180039
- reason_code: "missing_api_key"
180040
- });
180041
- logger2.error("Missing API key for --auth.");
180042
- await exitLogin(1);
180043
- }
180044
- captureAuthEvent("login_started", {
180045
- auth_method: authMethod
180046
- });
180047
- if (providedAuth) {
180048
- const result2 = await performLoginWithApiKey(authClient, logger2, {
180049
- apiKey: providedAuth,
180050
- machineName: options.machineName
180051
- });
180052
- if (result2.success) {
180053
- captureAuthEvent("login_succeeded", {
180054
- auth_method: "api_key"
180055
- });
180056
- logger2.success("\n" + chalk.green("\u2713") + " Successfully logged in!");
180057
- logger2.info(" User: " + chalk.cyan(result2.user.name || result2.user.email));
180058
- logger2.info(" Email: " + chalk.cyan(result2.user.email));
180059
- } else {
180060
- captureAuthEvent("login_failed", {
180061
- auth_method: "api_key"
180062
- });
180063
- await reportError("login", result2.error, {
180064
- message: "Login failed using --auth",
180065
- logger: logger2
180066
- });
180067
- await exitLogin(1);
180068
- }
180069
- await flushTelemetry();
180070
- return;
180071
- }
180072
- const existingAuth = authClient.getAuthInfo();
180073
- if (existingAuth) {
180074
- logger2.debug("Found existing authentication, checking validity...");
180075
- const validation2 = await authClient.validateToken(existingAuth.token);
180076
- if (validation2.valid && validation2.user) {
180077
- captureAuthEvent("login_succeeded", {
180078
- auth_method: "device_auth",
180079
- already_logged_in: true
180080
- });
180081
- logger2.success("\n" + chalk.green("\u2713") + " You are already logged in!");
180082
- logger2.info(" User: " + chalk.cyan(validation2.user.name || validation2.user.email));
180083
- logger2.info(" Email: " + chalk.cyan(validation2.user.email));
180084
- await flushTelemetry();
180085
- return;
180086
- } else if (isRetryableTokenValidationFailure(validation2)) {
180087
- captureAuthEvent("login_failed", {
180088
- auth_method: "device_auth",
180089
- reason_code: "auth_service_unreachable"
180090
- });
180091
- logger2.error(`Unable to validate existing authentication because the auth service is unreachable: ${validation2.error ?? "network error"}`);
180092
- await exitLogin(1);
180093
- } else if (isTokenValidationUnavailable(validation2)) {
180094
- captureAuthEvent("login_failed", {
180095
- auth_method: "device_auth",
180096
- reason_code: "auth_validation_unavailable"
180097
- });
180098
- logger2.error(`Unable to validate existing authentication: ${validation2.error ?? validation2.reason}`);
180099
- await exitLogin(1);
180100
- } else {
180101
- logger2.debug("Existing authentication is invalid, proceeding with new login...");
180102
- }
180103
- }
180104
- logger2.info("Initializing device authorization...");
180105
- const result = await performLogin(authClient, logger2, {
180106
- machineName: options.machineName
180107
- });
180108
- if (result.success) {
180109
- captureAuthEvent("login_succeeded", {
180110
- auth_method: "device_auth"
180111
- });
180112
- logger2.success("\n" + chalk.green("\u2713") + " Successfully logged in!");
180113
- logger2.info(" User: " + chalk.cyan(result.user.name || result.user.email));
180114
- logger2.info(" Email: " + chalk.cyan(result.user.email));
180115
- } else {
180116
- captureAuthEvent("login_failed", {
180117
- auth_method: "device_auth"
180118
- });
180119
- await reportError("login", result.error, {
180120
- message: "Login failed during device authorization",
180121
- logger: logger2
180122
- });
180123
- await exitLogin(1);
180124
- }
180125
- await flushTelemetry();
180126
- } catch (error2) {
180127
- captureAuthEvent("login_failed", {
180128
- auth_method: authMethod,
180129
- reason_code: "exception"
180130
- });
180131
- await reportError("login", error2, {
180132
- message: "Login command failed",
180133
- logger: logger2
180134
- });
180135
- await exitLogin(1);
180136
- }
180137
- });
180138
- const logoutCommand = new Command("logout").description("Logout from Lody and clear saved authentication").option("-d, --debug", "enable debug output").action(async (options) => {
180139
- if (options.debug) {
180140
- rootLogger.setDebug(true);
180141
- }
180142
- const logger2 = getLogger("logout");
180143
- const authClient = new AuthClient(logger2);
180144
- const logoutResult = authClient.logout();
180145
- if (!logoutResult.success) {
180146
- captureAuthEvent("logout_failed");
180147
- await reportError("logout", logoutResult.error, {
180148
- message: "Logout failed",
180149
- logger: logger2
180150
- });
180151
- await flushTelemetry();
180152
- process.exit(1);
180153
- }
180154
- if (!logoutResult.user) {
180155
- captureAuthEvent("logout_succeeded", {
180156
- was_logged_in: false
180157
- });
180158
- logger2.info("You are not logged in.");
180159
- await flushTelemetry();
180160
- return;
180161
- }
180162
- captureAuthEvent("logout_succeeded", {
180163
- was_logged_in: true
180363
+ function buildOpenLocalProjectDeepLink(input2) {
180364
+ const search2 = new URLSearchParams({
180365
+ machine: input2.machineId,
180366
+ project: input2.localProjectId
180164
180367
  });
180165
- logger2.info(`Logging out user: ${chalk.cyan(logoutResult.user.email)}`);
180166
- logger2.success("\n" + chalk.green("\u2713") + " Successfully logged out!");
180167
- await flushTelemetry();
180168
- });
180368
+ const workspaceSlug = input2.workspaceSlug?.trim();
180369
+ if (workspaceSlug) {
180370
+ search2.set("workspaceSlug", workspaceSlug);
180371
+ }
180372
+ return `lody://chat/new?${search2.toString()}`;
180373
+ }
180169
180374
  const isUpKey = (key2) => key2.name === "up" || key2.name === "k" || key2.ctrl && key2.name === "p";
180170
180375
  const isDownKey = (key2) => key2.name === "down" || key2.name === "j" || key2.ctrl && key2.name === "n";
180171
180376
  const isSpaceKey = (key2) => key2.name === "space";
@@ -205610,21 +205815,6 @@ ${page}${helpTipBottom}${choiceDescription}${ansiEscapes.cursorHide}`;
205610
205815
  Separator
205611
205816
  };
205612
205817
  const LOCAL_CONTROL_TIMEOUT_MS = 15e3;
205613
- function setDebugIfEnabled(options) {
205614
- if (options.debug) {
205615
- rootLogger.setDebug(true);
205616
- }
205617
- }
205618
- function resolveMachineIdOrExit() {
205619
- const logger2 = getLogger("project");
205620
- const authClient = new AuthClient(logger2);
205621
- const authInfo = authClient.getAuthInfo();
205622
- if (!authInfo) {
205623
- logger2.error("Not logged in. Run `lody login` first.");
205624
- process.exit(1);
205625
- }
205626
- return authInfo.machine.machineId;
205627
- }
205628
205818
  async function sendLocalProjectControl(message) {
205629
205819
  try {
205630
205820
  await ensureDaemonReachable();
@@ -205688,8 +205878,8 @@ ${page}${helpTipBottom}${choiceDescription}${ansiEscapes.cursorHide}`;
205688
205878
  }
205689
205879
  return candidates.length > 0 ? candidates : null;
205690
205880
  }
205691
- function printWorkspaceCandidates(candidates) {
205692
- const logger2 = getLogger("project");
205881
+ function printWorkspaceCandidates(candidates, loggerName) {
205882
+ const logger2 = getLogger(loggerName);
205693
205883
  logger2.info("Available workspaces:");
205694
205884
  for (const item of candidates) {
205695
205885
  const slug = item.slug ? ` (${item.slug})` : "";
@@ -205697,8 +205887,8 @@ ${page}${helpTipBottom}${choiceDescription}${ansiEscapes.cursorHide}`;
205697
205887
  }
205698
205888
  logger2.info("Use --workspace <id|slug|name>.");
205699
205889
  }
205700
- async function promptWorkspaceSelection(candidates) {
205701
- const logger2 = getLogger("project");
205890
+ async function promptWorkspaceSelection(candidates, loggerName) {
205891
+ const logger2 = getLogger(loggerName);
205702
205892
  const choices = candidates.map((candidate) => {
205703
205893
  const slug = candidate.slug ? ` (${candidate.slug})` : "";
205704
205894
  return {
@@ -205722,13 +205912,264 @@ ${page}${helpTipBottom}${choiceDescription}${ansiEscapes.cursorHide}`;
205722
205912
  return null;
205723
205913
  }
205724
205914
  }
205915
+ async function resolveLocalProjectForApp(args2) {
205916
+ const buildRequest = (workspace) => ({
205917
+ type: "local-project/add",
205918
+ machineId: args2.machineId,
205919
+ rootPath: args2.rootPath,
205920
+ ...workspace ? {
205921
+ workspace
205922
+ } : {}
205923
+ });
205924
+ let response2 = await args2.send(buildRequest(args2.workspaceSelector));
205925
+ let workspaceSlug = null;
205926
+ if (!response2.ok && response2.error === "workspace_required" && args2.canPrompt) {
205927
+ const candidates = extractWorkspaceCandidates(response2);
205928
+ if (candidates) {
205929
+ const selectedWorkspaceId = await args2.promptWorkspace(candidates);
205930
+ if (!selectedWorkspaceId) {
205931
+ throw new Error("Workspace selection is required to open a local project.");
205932
+ }
205933
+ workspaceSlug = candidates.find((c) => c.id === selectedWorkspaceId)?.slug ?? null;
205934
+ response2 = await args2.send(buildRequest(selectedWorkspaceId));
205935
+ }
205936
+ }
205937
+ if (!response2.ok) {
205938
+ if (response2.error === "daemon_unavailable") {
205939
+ return {
205940
+ localProjectId: createLocalProjectId(args2.rootPath),
205941
+ name: getLocalProjectNameFromRootPath(args2.rootPath),
205942
+ workspaceSlug: null,
205943
+ registered: false
205944
+ };
205945
+ }
205946
+ const candidates = extractWorkspaceCandidates(response2);
205947
+ if (candidates) {
205948
+ throw new Error(`${response2.message} Pass --workspace <id|slug|name>. Candidates: ${candidates.map(formatWorkspaceCandidate).join(", ")}`);
205949
+ }
205950
+ throw new Error(response2.message);
205951
+ }
205952
+ if (response2.type !== "local-project/add") {
205953
+ throw new Error(`Unexpected response type: ${response2.type}`);
205954
+ }
205955
+ return {
205956
+ localProjectId: response2.result.localProjectId,
205957
+ name: response2.result.name,
205958
+ workspaceSlug,
205959
+ registered: true
205960
+ };
205961
+ }
205962
+ const appCommand = new Command("app").description("Open the Lody desktop app on a new chat with a local directory selected (registers it if needed)").argument("[path]", "Local project directory to select", ".").option("--workspace <selector>", "Target workspace id, slug, or name").option("--json", "Print JSON output").option("-d, --debug", "Enable debug output").action(async (projectPath, options) => {
205963
+ await runOneShotCommand("app", options, async () => {
205964
+ const logger2 = getLogger("app");
205965
+ const auth = getAuthContextOrThrow$1("app");
205966
+ const rootPath = ensureLocalProjectRootPath(path__default$1.resolve(projectPath ?? "."));
205967
+ const target = await resolveLocalProjectForApp({
205968
+ machineId: auth.machineId,
205969
+ rootPath,
205970
+ workspaceSelector: options.workspace,
205971
+ canPrompt: !options.json && !options.workspace && !!process.stdin.isTTY && !!process.stdout.isTTY,
205972
+ send: sendLocalProjectControl,
205973
+ promptWorkspace: (candidates) => promptWorkspaceSelection(candidates, "app")
205974
+ });
205975
+ const deepLink = buildOpenLocalProjectDeepLink({
205976
+ machineId: auth.machineId,
205977
+ localProjectId: target.localProjectId,
205978
+ workspaceSlug: target.workspaceSlug
205979
+ });
205980
+ try {
205981
+ await openBrowser(deepLink);
205982
+ } catch (error2) {
205983
+ throw new Error(`Could not hand the link to the operating system. Is the Lody desktop app installed? Open it manually: ${deepLink}`, {
205984
+ cause: error2
205985
+ });
205986
+ }
205987
+ if (options.json) {
205988
+ printJson({
205989
+ ok: true,
205990
+ machineId: auth.machineId,
205991
+ localProjectId: target.localProjectId,
205992
+ name: target.name,
205993
+ rootPath,
205994
+ registered: target.registered,
205995
+ deepLink
205996
+ });
205997
+ return;
205998
+ }
205999
+ if (!target.registered) {
206000
+ logger2.warn("\u26A0\uFE0F Local CLI daemon is not running, so this directory could not be registered. If it is new, add it from the desktop app after it starts.");
206001
+ }
206002
+ logger2.success(`\u2705 Opening Lody desktop: ${target.name} (${rootPath})`);
206003
+ });
206004
+ });
206005
+ async function exitLogin(code2) {
206006
+ await flushTelemetry();
206007
+ process.exit(code2);
206008
+ }
206009
+ const loginCommand = new Command("login").description("Login to Lody using device authorization flow or an API key").option("-d, --debug", "enable debug output").option("--machine-name <name>", "Machine name to register (defaults to hostname)").option("--auth <api-key>", "Use a CLI API key for non-interactive login").action(async (options) => {
206010
+ if (options.debug) {
206011
+ rootLogger.setDebug(true);
206012
+ }
206013
+ const logger2 = getLogger("login");
206014
+ const authClient = new AuthClient(logger2);
206015
+ const providedAuth = options.auth?.trim();
206016
+ const authMethod = providedAuth ? "api_key" : "device_auth";
206017
+ try {
206018
+ if (options.auth !== void 0 && !providedAuth) {
206019
+ captureAuthEvent("login_failed", {
206020
+ auth_method: "api_key",
206021
+ reason_code: "missing_api_key"
206022
+ });
206023
+ logger2.error("Missing API key for --auth.");
206024
+ await exitLogin(1);
206025
+ }
206026
+ captureAuthEvent("login_started", {
206027
+ auth_method: authMethod
206028
+ });
206029
+ if (providedAuth) {
206030
+ const result2 = await performLoginWithApiKey(authClient, logger2, {
206031
+ apiKey: providedAuth,
206032
+ machineName: options.machineName
206033
+ });
206034
+ if (result2.success) {
206035
+ captureAuthEvent("login_succeeded", {
206036
+ auth_method: "api_key"
206037
+ });
206038
+ logger2.success("\n" + chalk.green("\u2713") + " Successfully logged in!");
206039
+ logger2.info(" User: " + chalk.cyan(result2.user.name || result2.user.email));
206040
+ logger2.info(" Email: " + chalk.cyan(result2.user.email));
206041
+ } else {
206042
+ captureAuthEvent("login_failed", {
206043
+ auth_method: "api_key"
206044
+ });
206045
+ await reportError("login", result2.error, {
206046
+ message: "Login failed using --auth",
206047
+ logger: logger2
206048
+ });
206049
+ await exitLogin(1);
206050
+ }
206051
+ await flushTelemetry();
206052
+ return;
206053
+ }
206054
+ const existingAuth = authClient.getAuthInfo();
206055
+ if (existingAuth) {
206056
+ logger2.debug("Found existing authentication, checking validity...");
206057
+ const validation2 = await authClient.validateToken(existingAuth.token);
206058
+ if (validation2.valid && validation2.user) {
206059
+ captureAuthEvent("login_succeeded", {
206060
+ auth_method: "device_auth",
206061
+ already_logged_in: true
206062
+ });
206063
+ logger2.success("\n" + chalk.green("\u2713") + " You are already logged in!");
206064
+ logger2.info(" User: " + chalk.cyan(validation2.user.name || validation2.user.email));
206065
+ logger2.info(" Email: " + chalk.cyan(validation2.user.email));
206066
+ await flushTelemetry();
206067
+ return;
206068
+ } else if (isRetryableTokenValidationFailure(validation2)) {
206069
+ captureAuthEvent("login_failed", {
206070
+ auth_method: "device_auth",
206071
+ reason_code: "auth_service_unreachable"
206072
+ });
206073
+ logger2.error(`Unable to validate existing authentication because the auth service is unreachable: ${validation2.error ?? "network error"}`);
206074
+ await exitLogin(1);
206075
+ } else if (isTokenValidationUnavailable(validation2)) {
206076
+ captureAuthEvent("login_failed", {
206077
+ auth_method: "device_auth",
206078
+ reason_code: "auth_validation_unavailable"
206079
+ });
206080
+ logger2.error(`Unable to validate existing authentication: ${validation2.error ?? validation2.reason}`);
206081
+ await exitLogin(1);
206082
+ } else {
206083
+ logger2.debug("Existing authentication is invalid, proceeding with new login...");
206084
+ }
206085
+ }
206086
+ logger2.info("Initializing device authorization...");
206087
+ const result = await performLogin(authClient, logger2, {
206088
+ machineName: options.machineName
206089
+ });
206090
+ if (result.success) {
206091
+ captureAuthEvent("login_succeeded", {
206092
+ auth_method: "device_auth"
206093
+ });
206094
+ logger2.success("\n" + chalk.green("\u2713") + " Successfully logged in!");
206095
+ logger2.info(" User: " + chalk.cyan(result.user.name || result.user.email));
206096
+ logger2.info(" Email: " + chalk.cyan(result.user.email));
206097
+ } else {
206098
+ captureAuthEvent("login_failed", {
206099
+ auth_method: "device_auth"
206100
+ });
206101
+ await reportError("login", result.error, {
206102
+ message: "Login failed during device authorization",
206103
+ logger: logger2
206104
+ });
206105
+ await exitLogin(1);
206106
+ }
206107
+ await flushTelemetry();
206108
+ } catch (error2) {
206109
+ captureAuthEvent("login_failed", {
206110
+ auth_method: authMethod,
206111
+ reason_code: "exception"
206112
+ });
206113
+ await reportError("login", error2, {
206114
+ message: "Login command failed",
206115
+ logger: logger2
206116
+ });
206117
+ await exitLogin(1);
206118
+ }
206119
+ });
206120
+ const logoutCommand = new Command("logout").description("Logout from Lody and clear saved authentication").option("-d, --debug", "enable debug output").action(async (options) => {
206121
+ if (options.debug) {
206122
+ rootLogger.setDebug(true);
206123
+ }
206124
+ const logger2 = getLogger("logout");
206125
+ const authClient = new AuthClient(logger2);
206126
+ const logoutResult = authClient.logout();
206127
+ if (!logoutResult.success) {
206128
+ captureAuthEvent("logout_failed");
206129
+ await reportError("logout", logoutResult.error, {
206130
+ message: "Logout failed",
206131
+ logger: logger2
206132
+ });
206133
+ await flushTelemetry();
206134
+ process.exit(1);
206135
+ }
206136
+ if (!logoutResult.user) {
206137
+ captureAuthEvent("logout_succeeded", {
206138
+ was_logged_in: false
206139
+ });
206140
+ logger2.info("You are not logged in.");
206141
+ await flushTelemetry();
206142
+ return;
206143
+ }
206144
+ captureAuthEvent("logout_succeeded", {
206145
+ was_logged_in: true
206146
+ });
206147
+ logger2.info(`Logging out user: ${chalk.cyan(logoutResult.user.email)}`);
206148
+ logger2.success("\n" + chalk.green("\u2713") + " Successfully logged out!");
206149
+ await flushTelemetry();
206150
+ });
206151
+ function setDebugIfEnabled(options) {
206152
+ if (options.debug) {
206153
+ rootLogger.setDebug(true);
206154
+ }
206155
+ }
206156
+ function resolveMachineIdOrExit() {
206157
+ const logger2 = getLogger("project");
206158
+ const authClient = new AuthClient(logger2);
206159
+ const authInfo = authClient.getAuthInfo();
206160
+ if (!authInfo) {
206161
+ logger2.error("Not logged in. Run `lody login` first.");
206162
+ process.exit(1);
206163
+ }
206164
+ return authInfo.machine.machineId;
206165
+ }
205725
206166
  function printProjectControlError(action, response2) {
205726
206167
  const logger2 = getLogger("project");
205727
206168
  logger2.error(`Failed to ${action}: ${response2.message}`);
205728
206169
  if (response2.error === "workspace_required") {
205729
206170
  const candidates = extractWorkspaceCandidates(response2);
205730
206171
  if (candidates && candidates.length > 0) {
205731
- printWorkspaceCandidates(candidates);
206172
+ printWorkspaceCandidates(candidates, "project");
205732
206173
  }
205733
206174
  }
205734
206175
  }
@@ -205826,7 +206267,7 @@ ${page}${helpTipBottom}${choiceDescription}${ansiEscapes.cursorHide}`;
205826
206267
  if (!response2.ok && response2.error === "workspace_required" && canPromptForWorkspace) {
205827
206268
  const candidates = extractWorkspaceCandidates(response2);
205828
206269
  if (candidates && candidates.length > 0) {
205829
- const selectedWorkspace = await promptWorkspaceSelection(candidates);
206270
+ const selectedWorkspace = await promptWorkspaceSelection(candidates, "project");
205830
206271
  if (!selectedWorkspace) {
205831
206272
  process.exit(1);
205832
206273
  }
@@ -205989,25 +206430,6 @@ ${page}${helpTipBottom}${choiceDescription}${ansiEscapes.cursorHide}`;
205989
206430
  ], rows));
205990
206431
  });
205991
206432
  const projectCommand = new Command("project").description("Manage local projects").addCommand(projectAddCommand).addCommand(projectDeleteCommand).addCommand(projectListCommand);
205992
- async function mapWithConcurrency$1(items2, concurrency, worker) {
205993
- if (items2.length === 0) {
205994
- return [];
205995
- }
205996
- const limit2 = Math.max(1, Math.floor(concurrency));
205997
- const results = new Array(items2.length);
205998
- let nextIndex = 0;
205999
- const runWorker = async () => {
206000
- while (nextIndex < items2.length) {
206001
- const currentIndex = nextIndex;
206002
- nextIndex += 1;
206003
- results[currentIndex] = await worker(items2[currentIndex], currentIndex);
206004
- }
206005
- };
206006
- await Promise.all(Array.from({
206007
- length: Math.min(limit2, items2.length)
206008
- }, () => runWorker()));
206009
- return results;
206010
- }
206011
206433
  const DEFAULT_SYNC_CONCURRENCY = 4;
206012
206434
  function parsePositiveIntOption(value) {
206013
206435
  const parsed = Number.parseInt(value, 10);
@@ -207258,7 +207680,7 @@ ${page}${helpTipBottom}${choiceDescription}${ansiEscapes.cursorHide}`;
207258
207680
  data = createReviewBundleSnapshot(bundle);
207259
207681
  }
207260
207682
  const { injectReviewSnapshot } = await import("./chunks/index-VoI6Ds2-.js");
207261
- const { resolveReviewViewerTemplate } = await import("./chunks/review-viewer-CkoswOhv.js");
207683
+ const { resolveReviewViewerTemplate } = await import("./chunks/review-viewer-BbX0BXrV.js");
207262
207684
  const template = await resolveReviewViewerTemplate();
207263
207685
  const html = injectReviewSnapshot(template, data);
207264
207686
  const outputPath = options.output ? path__default$1.resolve(options.output) : defaultHtmlOutputPath(inputPath);
@@ -227337,6 +227759,7 @@ ${lines2.join("\n")}` : ""}${suffix}`);
227337
227759
  program.addCommand(loginCommand);
227338
227760
  program.addCommand(logoutCommand);
227339
227761
  program.addCommand(startCommand);
227762
+ program.addCommand(appCommand);
227340
227763
  program.addCommand(projectCommand);
227341
227764
  program.addCommand(sessionCommand);
227342
227765
  program.addCommand(syncCommand);