lody 0.44.2-next.1 → 0.44.4-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 +474 -219
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -45,13 +45,13 @@ import require$$1$6 from "string_decoder";
45
45
  import * as http$2 from "http";
46
46
  import http__default from "http";
47
47
  import require$$1$7 from "https";
48
- import require$$0$a, { execSync, exec, execFile as execFile$1 } from "child_process";
48
+ import require$$0$a, { execSync, exec, execFileSync, execFile as execFile$1 } from "child_process";
49
49
  import { randomFillSync, randomUUID as randomUUID$1, createHash as createHash$1 } from "node:crypto";
50
50
  import require$$0$b from "net";
51
51
  import require$$4$3 from "tls";
52
52
  import { i as imports, _ as __wbg_set_wasm$1, r as rawWasm, L as LoroDoc, E as EphemeralStoreWasm, U as UndoManager, c as callPendingEvents$3, a as LoroTree, b as LoroText, d as LoroMovableList, e as LoroList, f as LoroMap, g as __vite__initWasm, V as VersionVector, h as decodeImportBlobMeta, __tla as __tla_0 } from "./chunks/loro_wasm_bg-DgxHrrrp.js";
53
53
  import * as fs$5 from "fs/promises";
54
- import fs__default$1, { stat as stat$1, readFile as readFile$1 } from "fs/promises";
54
+ import fs__default$1, { stat as stat$1, readFile as readFile$1, statfs } from "fs/promises";
55
55
  import fsPromises, { stat, open } from "node:fs/promises";
56
56
  import { fileURLToPath } from "node:url";
57
57
  import require$$2$7 from "assert";
@@ -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.44.2-next.1";
36823
+ const version$4 = "0.44.4-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";
@@ -78436,6 +78436,7 @@ Task description:
78436
78436
  "session_init_failed",
78437
78437
  "session_restore_failed",
78438
78438
  "session_not_found",
78439
+ "memory_pressure",
78439
78440
  "acp_not_ready",
78440
78441
  "agent_disconnected",
78441
78442
  "turn_pre_prompt_failed",
@@ -112230,6 +112231,9 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
112230
112231
  isTransportConnected() {
112231
112232
  return this.transportStatus === "connected";
112232
112233
  }
112234
+ isRecovering() {
112235
+ return !this.isCleanedUp && !this.isStreamsHealthy();
112236
+ }
112233
112237
  setTransportStatus(status) {
112234
112238
  this.transportStatus = status;
112235
112239
  if (status === "disconnected") {
@@ -113429,6 +113433,9 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
113429
113433
  isTransportConnected() {
113430
113434
  return this.connectionRecovery.isTransportConnected();
113431
113435
  }
113436
+ isTransportRecovering() {
113437
+ return this.connectionRecovery.isRecovering();
113438
+ }
113432
113439
  getConnectedRoomCount() {
113433
113440
  return this.sessions.size + this.codeSessions.size;
113434
113441
  }
@@ -120210,6 +120217,221 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120210
120217
  }
120211
120218
  return resolveAgentConfigEnvForSessionResume(repo, options.agentConfigId);
120212
120219
  };
120220
+ function getMemoryPressureSnapshot() {
120221
+ const windowsStatus = getWindowsMemoryStatus();
120222
+ const systemAvailable = getSystemAvailableMemoryBytes(windowsStatus);
120223
+ const cgroupAvailable = getCgroupAvailableMemoryBytes();
120224
+ const availableMemoryBytes = cgroupAvailable !== null ? Math.min(systemAvailable, cgroupAvailable) : systemAvailable;
120225
+ const effectiveMemoryLimitBytes = getEffectiveMemoryLimitBytes();
120226
+ return {
120227
+ availableMemoryBytes,
120228
+ effectiveMemoryLimitBytes,
120229
+ ...windowsStatus ? {
120230
+ availableCommitBytes: windowsStatus.availableCommitBytes,
120231
+ commitLimitBytes: windowsStatus.commitLimitBytes,
120232
+ committedBytes: windowsStatus.committedBytes
120233
+ } : {}
120234
+ };
120235
+ }
120236
+ function getEffectiveMemoryLimitBytes() {
120237
+ const totalMem = os__default.totalmem();
120238
+ const cgroupMax = getCgroupMemoryMaxBytes();
120239
+ if (cgroupMax !== null) {
120240
+ return Math.min(totalMem, cgroupMax);
120241
+ }
120242
+ return totalMem;
120243
+ }
120244
+ function getSystemAvailableMemoryBytes(windowsStatus) {
120245
+ if (windowsStatus) {
120246
+ return Math.max(windowsStatus.availableBytes, os__default.freemem());
120247
+ }
120248
+ const darwinAvailable = getDarwinAvailableMemoryBytes();
120249
+ if (darwinAvailable !== null) {
120250
+ return darwinAvailable;
120251
+ }
120252
+ try {
120253
+ const meminfo = readFileSync("/proc/meminfo", "utf8");
120254
+ const match5 = meminfo.match(/MemAvailable:\s+(\d+)/);
120255
+ if (match5?.[1]) {
120256
+ return parseInt(match5[1], 10) * 1024;
120257
+ }
120258
+ } catch {
120259
+ }
120260
+ return os__default.freemem();
120261
+ }
120262
+ function getWindowsMemoryStatus() {
120263
+ if (process.platform !== "win32") {
120264
+ return null;
120265
+ }
120266
+ try {
120267
+ const script = `
120268
+ $mem = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfOS_Memory |
120269
+ Select-Object AvailableBytes, CommitLimit, CommittedBytes
120270
+ $mem | ConvertTo-Json -Compress
120271
+ `;
120272
+ const output = execFileSync("powershell.exe", [
120273
+ "-NoProfile",
120274
+ "-NonInteractive",
120275
+ "-ExecutionPolicy",
120276
+ "Bypass",
120277
+ "-Command",
120278
+ script
120279
+ ], {
120280
+ encoding: "utf8"
120281
+ });
120282
+ return parseWindowsMemoryStatus(output);
120283
+ } catch {
120284
+ return null;
120285
+ }
120286
+ }
120287
+ function parseWindowsMemoryStatus(rawJson) {
120288
+ try {
120289
+ const parsed = JSON.parse(rawJson);
120290
+ const availableBytes = Number(parsed.AvailableBytes);
120291
+ const commitLimitBytes = Number(parsed.CommitLimit);
120292
+ const committedBytes = Number(parsed.CommittedBytes);
120293
+ if (!Number.isFinite(availableBytes) || availableBytes < 0 || !Number.isFinite(commitLimitBytes) || commitLimitBytes <= 0 || !Number.isFinite(committedBytes) || committedBytes < 0) {
120294
+ return null;
120295
+ }
120296
+ return {
120297
+ availableBytes,
120298
+ commitLimitBytes,
120299
+ committedBytes,
120300
+ availableCommitBytes: Math.max(0, commitLimitBytes - committedBytes)
120301
+ };
120302
+ } catch {
120303
+ return null;
120304
+ }
120305
+ }
120306
+ function getDarwinAvailableMemoryBytes() {
120307
+ if (process.platform !== "darwin") {
120308
+ return null;
120309
+ }
120310
+ try {
120311
+ const vmStatOutput = execFileSync("vm_stat", {
120312
+ encoding: "utf8"
120313
+ });
120314
+ const parsed = parseDarwinAvailableMemoryBytes(vmStatOutput);
120315
+ if (parsed !== null) {
120316
+ return Math.max(parsed, os__default.freemem());
120317
+ }
120318
+ } catch {
120319
+ }
120320
+ return null;
120321
+ }
120322
+ function parseDarwinAvailableMemoryBytes(vmStatOutput) {
120323
+ const pageSizeMatch = vmStatOutput.match(/page size of\s+(\d+)\s+bytes/i);
120324
+ if (!pageSizeMatch?.[1]) {
120325
+ return null;
120326
+ }
120327
+ const pageSize = parseInt(pageSizeMatch[1], 10);
120328
+ if (!Number.isFinite(pageSize) || pageSize <= 0) {
120329
+ return null;
120330
+ }
120331
+ const counters = /* @__PURE__ */ new Map();
120332
+ for (const rawLine of vmStatOutput.split("\n")) {
120333
+ const line3 = rawLine.trim();
120334
+ const match5 = line3.match(/^"?([^":]+?)"?:\s+(\d+)\.?$/);
120335
+ if (!match5?.[1] || !match5[2]) {
120336
+ continue;
120337
+ }
120338
+ counters.set(match5[1].toLowerCase(), parseInt(match5[2], 10));
120339
+ }
120340
+ const freePages = counters.get("pages free") ?? 0;
120341
+ const speculativePages = counters.get("pages speculative") ?? 0;
120342
+ const purgeablePages = counters.get("pages purgeable") ?? 0;
120343
+ const inactivePages = counters.get("pages inactive") ?? 0;
120344
+ const fileBackedPages = counters.get("file-backed pages");
120345
+ if (freePages === 0 && speculativePages === 0 && purgeablePages === 0 && inactivePages === 0) {
120346
+ return null;
120347
+ }
120348
+ const reclaimableCachedPages = fileBackedPages !== void 0 ? Math.min(fileBackedPages, inactivePages) : inactivePages;
120349
+ const availablePages = freePages + speculativePages + purgeablePages + reclaimableCachedPages;
120350
+ return availablePages * pageSize;
120351
+ }
120352
+ function getCgroupMemoryMaxBytes() {
120353
+ try {
120354
+ const cgroupPath = readSelfCgroupPath();
120355
+ if (cgroupPath === null) return null;
120356
+ let tightest = null;
120357
+ let current2 = cgroupPath;
120358
+ for (let depth = 0; depth < 20; depth++) {
120359
+ const memMaxPath = `/sys/fs/cgroup${current2 === "/" ? "" : current2}/memory.max`;
120360
+ const raw = readFileSafe(memMaxPath);
120361
+ if (raw !== null) {
120362
+ const trimmed = raw.trim();
120363
+ if (trimmed !== "max") {
120364
+ const value = parseInt(trimmed, 10);
120365
+ if (Number.isFinite(value) && value > 0) {
120366
+ tightest = tightest === null ? value : Math.min(tightest, value);
120367
+ }
120368
+ }
120369
+ }
120370
+ if (current2 === "/" || current2 === "") break;
120371
+ const parent = current2.substring(0, current2.lastIndexOf("/")) || "/";
120372
+ if (parent === current2) break;
120373
+ current2 = parent;
120374
+ }
120375
+ return tightest;
120376
+ } catch {
120377
+ return null;
120378
+ }
120379
+ }
120380
+ function getCgroupAvailableMemoryBytes() {
120381
+ try {
120382
+ const cgroupPath = readSelfCgroupPath();
120383
+ if (cgroupPath === null) return null;
120384
+ let tightestMax = null;
120385
+ let tightestPath = null;
120386
+ let current2 = cgroupPath;
120387
+ for (let depth = 0; depth < 20; depth++) {
120388
+ const prefix = `/sys/fs/cgroup${current2 === "/" ? "" : current2}`;
120389
+ const raw = readFileSafe(`${prefix}/memory.max`);
120390
+ if (raw !== null) {
120391
+ const trimmed = raw.trim();
120392
+ if (trimmed !== "max") {
120393
+ const value = parseInt(trimmed, 10);
120394
+ if (Number.isFinite(value) && value > 0) {
120395
+ if (tightestMax === null || value < tightestMax) {
120396
+ tightestMax = value;
120397
+ tightestPath = prefix;
120398
+ }
120399
+ }
120400
+ }
120401
+ }
120402
+ if (current2 === "/" || current2 === "") break;
120403
+ const parent = current2.substring(0, current2.lastIndexOf("/")) || "/";
120404
+ if (parent === current2) break;
120405
+ current2 = parent;
120406
+ }
120407
+ if (tightestMax === null || tightestPath === null) return null;
120408
+ const currentRaw = readFileSafe(`${tightestPath}/memory.current`);
120409
+ if (currentRaw === null) return null;
120410
+ const currentUsage = parseInt(currentRaw.trim(), 10);
120411
+ if (!Number.isFinite(currentUsage)) return null;
120412
+ return Math.max(0, tightestMax - currentUsage);
120413
+ } catch {
120414
+ return null;
120415
+ }
120416
+ }
120417
+ function readSelfCgroupPath() {
120418
+ try {
120419
+ const content = readFileSync("/proc/self/cgroup", "utf8");
120420
+ const line3 = content.split("\n").map((l) => l.trim()).find((l) => l.startsWith("0::"));
120421
+ if (!line3) return null;
120422
+ const cgroupPath = line3.slice(3).trim();
120423
+ return cgroupPath || "/";
120424
+ } catch {
120425
+ return null;
120426
+ }
120427
+ }
120428
+ function readFileSafe(filePath) {
120429
+ try {
120430
+ return readFileSync(filePath, "utf8");
120431
+ } catch {
120432
+ return null;
120433
+ }
120434
+ }
120213
120435
  class SessionTurnCancelled extends TaggedError("SessionTurnCancelled") {
120214
120436
  }
120215
120437
  class SessionTurnHalted extends TaggedError("SessionTurnHalted") {
@@ -120230,6 +120452,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120230
120452
  AUTH_REQUIRED: -32e3,
120231
120453
  RESOURCE_NOT_FOUND: -32002
120232
120454
  };
120455
+ const BYTES_PER_GIB = 1024 * 1024 * 1024;
120233
120456
  const shouldRedactEnvKey = (key2) => /token|secret|password|passwd|key/i.test(key2);
120234
120457
  const redactEnvForLog = (env2) => {
120235
120458
  if (!env2) {
@@ -120265,6 +120488,46 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120265
120488
  canceledTurnBySession = /* @__PURE__ */ new Map();
120266
120489
  currentTurnBySession = /* @__PURE__ */ new Map();
120267
120490
  turnRuntimeBySession = /* @__PURE__ */ new Map();
120491
+ formatGiB(bytes) {
120492
+ if (bytes === null || !Number.isFinite(bytes) || bytes < 0) {
120493
+ return "unavailable";
120494
+ }
120495
+ return `${(bytes / BYTES_PER_GIB).toFixed(1)}GB`;
120496
+ }
120497
+ formatPercent(value) {
120498
+ if (value === null || !Number.isFinite(value) || value < 0) {
120499
+ return "unavailable";
120500
+ }
120501
+ return `${value.toFixed(1)}%`;
120502
+ }
120503
+ async getFreeDiskBytes() {
120504
+ try {
120505
+ const stats = await statfs(os__default.homedir());
120506
+ const bsize = typeof stats.bsize === "bigint" ? Number(stats.bsize) : stats.bsize;
120507
+ const bavail = typeof stats.bavail === "bigint" ? Number(stats.bavail) : stats.bavail;
120508
+ if (!Number.isFinite(bsize) || !Number.isFinite(bavail) || bsize <= 0 || bavail < 0) {
120509
+ return null;
120510
+ }
120511
+ return bsize * bavail;
120512
+ } catch {
120513
+ return null;
120514
+ }
120515
+ }
120516
+ async logTurnStartResources(sessionId, mode2) {
120517
+ try {
120518
+ const memorySnapshot = getMemoryPressureSnapshot();
120519
+ const availableMemoryBytes = memorySnapshot.availableMemoryBytes;
120520
+ const memoryFreePercent = memorySnapshot.effectiveMemoryLimitBytes > 0 ? availableMemoryBytes / memorySnapshot.effectiveMemoryLimitBytes * 100 : null;
120521
+ const [resources, freeDiskBytes] = await Promise.all([
120522
+ this.deps.collectMachineResources().catch(() => null),
120523
+ this.getFreeDiskBytes()
120524
+ ]);
120525
+ const cpuUsagePercent = resources?.cpuUsagePercent ?? null;
120526
+ this.deps.logger.info(`[${sessionId}] Turn start resources (mode=${mode2}): memoryAvailable=${this.formatGiB(availableMemoryBytes)} memoryAvailablePercent=${this.formatPercent(memoryFreePercent)} cpuUsage=${this.formatPercent(cpuUsagePercent)} diskFree=${this.formatGiB(freeDiskBytes)}`);
120527
+ } catch (error2) {
120528
+ this.deps.logger.debug(`[${sessionId}] Failed to log turn start resources: ${formatErrorMessage(error2)}`);
120529
+ }
120530
+ }
120268
120531
  createTurnRuntime(sessionId, turnId, userTurnId, session) {
120269
120532
  return {
120270
120533
  sessionId,
@@ -120447,10 +120710,19 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120447
120710
  async recordKnownChatFailure(options) {
120448
120711
  await this.deps.recordChatFailure(options.sessionDoc, options.reason, options.message);
120449
120712
  if (options.userTurnId) {
120450
- await this.setDispatchError(options.sessionId, options.sessionDoc, options.userTurnId, options.code ?? options.reason, options.message);
120713
+ await this.markTurnFailed(options.sessionId, options.sessionDoc, options.userTurnId);
120451
120714
  }
120452
120715
  await options.sessionDoc.setStatus(SessionStatusFactory.idle());
120453
120716
  }
120717
+ formatMemoryPressureWarningMessage(result) {
120718
+ const availableMb = Math.round(result.availableMemoryBytes / 1024 / 1024);
120719
+ const thresholdMb = Math.round(result.thresholdBytes / 1024 / 1024);
120720
+ const commitText = result.availableCommitBytes !== void 0 && (result.pressureReason === "commit" || result.pressureReason === "physical_and_commit") && result.commitThresholdBytes !== void 0 ? ` Commit headroom is ${Math.round(result.availableCommitBytes / 1024 / 1024)}MB (threshold: ${Math.round(result.commitThresholdBytes / 1024 / 1024)}MB).` : "";
120721
+ return `The machine is under memory pressure (${availableMb}MB available, ${thresholdMb}MB required to start a turn). Proceeding anyway because the new turn may be used to free resources.${commitText}`;
120722
+ }
120723
+ warnOnMemoryPressure(sessionId, result) {
120724
+ this.deps.logger.warn(`[${sessionId}] ${this.formatMemoryPressureWarningMessage(result)}`);
120725
+ }
120454
120726
  recordKnownChatFailureAndHaltEffect(options) {
120455
120727
  return this.tryPromise(() => this.recordKnownChatFailure(options)).pipe(flatMap$1(() => fail(new SessionTurnHalted({
120456
120728
  sessionId: options.sessionId,
@@ -120474,7 +120746,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120474
120746
  }
120475
120747
  this.deps.logger.error(options.describe(options.error), options.error);
120476
120748
  if (options.userTurnId) {
120477
- await this.setDispatchError(options.sessionId, options.sessionDoc, options.userTurnId, options.code, formatErrorMessage(options.error));
120749
+ await this.markTurnFailed(options.sessionId, options.sessionDoc, options.userTurnId);
120478
120750
  }
120479
120751
  await this.handleTurnError(options.sessionId, options.sessionDoc, options.error);
120480
120752
  await options.onUnhandledError?.(options.error);
@@ -120810,8 +121082,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120810
121082
  await this.setUserTurnStatus(sessionDoc, userTurnId, "processing");
120811
121083
  await this.upsertSessionMeta(sessionId, {
120812
121084
  latestUserMsgId: userTurnId,
120813
- processingUserMsgId: userTurnId,
120814
- dispatchError: void 0
121085
+ processingUserMsgId: userTurnId
120815
121086
  });
120816
121087
  }
120817
121088
  async clearDispatchProcessing(sessionId) {
@@ -120860,34 +121131,31 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120860
121131
  await this.upsertSessionMeta(sessionId, {
120861
121132
  latestUserMsgId: existingMeta?.latestUserMsgId ?? cancelledUserMsgId,
120862
121133
  lastHandledUserMsgId: cancelledUserMsgId,
120863
- processingUserMsgId: void 0,
120864
- dispatchError: void 0
121134
+ processingUserMsgId: void 0
120865
121135
  });
120866
121136
  return;
120867
121137
  }
120868
121138
  await this.clearDispatchProcessing(sessionId);
120869
121139
  }
121140
+ resolveLatestUserMsgIdForTerminalTurn(meta, terminalUserTurnId) {
121141
+ return meta?.latestUserMsgId && meta.latestUserMsgId !== terminalUserTurnId ? meta.latestUserMsgId : terminalUserTurnId;
121142
+ }
120870
121143
  async setDispatchHandled(sessionId, sessionDoc, userTurnId) {
121144
+ const existingMeta = await this.getSessionMeta(sessionId);
120871
121145
  await this.setUserTurnStatus(sessionDoc, userTurnId, "handled");
120872
121146
  await this.upsertSessionMeta(sessionId, {
120873
- latestUserMsgId: userTurnId,
121147
+ latestUserMsgId: this.resolveLatestUserMsgIdForTerminalTurn(existingMeta, userTurnId),
120874
121148
  lastHandledUserMsgId: userTurnId,
120875
- processingUserMsgId: void 0,
120876
- dispatchError: void 0
121149
+ processingUserMsgId: void 0
120877
121150
  });
120878
121151
  }
120879
- async setDispatchError(sessionId, sessionDoc, userTurnId, code2, message) {
121152
+ async markTurnFailed(sessionId, sessionDoc, userTurnId) {
121153
+ const existingMeta = await this.getSessionMeta(sessionId);
120880
121154
  await this.setUserTurnStatus(sessionDoc, userTurnId, "failed");
120881
121155
  await this.upsertSessionMeta(sessionId, {
120882
- latestUserMsgId: userTurnId,
120883
- processingUserMsgId: void 0,
120884
- dispatchError: {
120885
- code: code2,
120886
- ...message ? {
120887
- message
120888
- } : {},
120889
- at: getServerNow()
120890
- }
121156
+ latestUserMsgId: this.resolveLatestUserMsgIdForTerminalTurn(existingMeta, userTurnId),
121157
+ lastHandledUserMsgId: userTurnId,
121158
+ processingUserMsgId: void 0
120891
121159
  });
120892
121160
  }
120893
121161
  resolveGitHubProjectBranch(meta, preferredBranch) {
@@ -120981,15 +121249,18 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120981
121249
  }
120982
121250
  async continueSession(message) {
120983
121251
  const { sessionId, acpSessionConfig, userId, userName, userEmail, userTurnId } = message;
120984
- await this.deps.evictForMemoryPressure(sessionId);
121252
+ const memoryPressureResult = await this.deps.evictForMemoryPressure(sessionId);
121253
+ if (memoryPressureResult.stillUnderPressure) {
121254
+ this.warnOnMemoryPressure(sessionId, memoryPressureResult);
121255
+ }
121256
+ await this.logTurnStartResources(sessionId, "continue");
121257
+ const sessionDoc = await this.deps.workspaceDocument.getOrCreateSessionDoc(sessionId);
120985
121258
  this.deps.touchSession(sessionId);
120986
121259
  this.deps.logger.info(`Session chat received: ${sessionId}`);
120987
121260
  this.deps.logger.debug(`[${sessionId}] Received chat request (userTurnId=${userTurnId})`);
120988
121261
  await this.upsertSessionMeta(sessionId, {
120989
- latestUserMsgId: userTurnId,
120990
- dispatchError: void 0
121262
+ latestUserMsgId: userTurnId
120991
121263
  });
120992
- const sessionDoc = await this.deps.workspaceDocument.getOrCreateSessionDoc(sessionId);
120993
121264
  const incomingProjectBranch = message.project?.branch?.trim();
120994
121265
  if (incomingProjectBranch) {
120995
121266
  await sessionDoc.setBaseBranch(incomingProjectBranch);
@@ -121326,7 +121597,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121326
121597
  }));
121327
121598
  }
121328
121599
  async startSession(message) {
121329
- await this.deps.evictForMemoryPressure(message.sessionId);
121600
+ const memoryPressureResult = await this.deps.evictForMemoryPressure(message.sessionId);
121330
121601
  const { sessionId, acpSessionConfig, workspaceId, env: env2 } = message;
121331
121602
  const userTurnId = typeof message.userTurnId === "string" && message.userTurnId.trim() ? message.userTurnId.trim() : void 0;
121332
121603
  const project = message.project;
@@ -121339,6 +121610,10 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121339
121610
  const promptText = agentConfig.prompt ?? "";
121340
121611
  const promptBytes = Buffer.byteLength(promptText, "utf8");
121341
121612
  const promptPreview = promptText.length > 200 ? `${promptText.slice(0, 200)}\u2026` : promptText;
121613
+ if (memoryPressureResult.stillUnderPressure) {
121614
+ this.warnOnMemoryPressure(sessionId, memoryPressureResult);
121615
+ }
121616
+ await this.logTurnStartResources(sessionId, "start");
121342
121617
  const sessionDoc = await this.deps.workspaceDocument.getOrCreateSessionDoc(sessionId);
121343
121618
  const existingMeta = await sessionDoc.getMetaState();
121344
121619
  const fromFeedbackPostId = message.meta?.fromFeedbackPostId?.trim() || existingMeta?.fromFeedbackPostId?.trim() || void 0;
@@ -121362,12 +121637,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121362
121637
  fromFeedbackPostId
121363
121638
  };
121364
121639
  this.deps.logger.debug(`[${sessionId}] session/create summary`, configForLog);
121365
- if (userTurnId) {
121366
- await this.upsertSessionMeta(sessionId, {
121367
- latestUserMsgId: userTurnId,
121368
- dispatchError: void 0
121369
- });
121370
- }
121371
121640
  const sessionConfig = {
121372
121641
  sessionId,
121373
121642
  workspaceId,
@@ -121388,6 +121657,11 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121388
121657
  if (project) {
121389
121658
  await sessionDoc.setProject(project);
121390
121659
  }
121660
+ if (userTurnId) {
121661
+ await this.upsertSessionMeta(sessionId, {
121662
+ latestUserMsgId: userTurnId
121663
+ });
121664
+ }
121391
121665
  if (branch) {
121392
121666
  await sessionDoc.setBaseBranch(branch);
121393
121667
  }
@@ -122051,9 +122325,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122051
122325
  if (meta.lastCanceledTurn && meta.lastCanceledTurn !== this.cancelSeenTurn.get(meta.id)) {
122052
122326
  return true;
122053
122327
  }
122054
- if (meta.dispatchError?.code === SessionDispatchWatcher.DISPATCH_HISTORY_SYNC_TIMEOUT_CODE) {
122055
- return false;
122056
- }
122057
122328
  if (!meta.lastHandledUserMsgId) {
122058
122329
  return true;
122059
122330
  }
@@ -122149,12 +122420,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122149
122420
  await this.deps.workspaceDocument.repo.upsertDocMeta?.(getSessionRoomId(sessionId), {
122150
122421
  latestUserMsgId: userTurnId,
122151
122422
  lastHandledUserMsgId: userTurnId,
122152
- processingUserMsgId: void 0,
122153
- dispatchError: {
122154
- code: reason === "cli_token_invalid" ? "cli_token_invalid" : "machine_access_denied",
122155
- message,
122156
- at: getServerNow()
122157
- }
122423
+ processingUserMsgId: void 0
122158
122424
  });
122159
122425
  await sessionDoc.setStatus(SessionStatusFactory.idle());
122160
122426
  }
@@ -122284,7 +122550,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122284
122550
  static HISTORY_SYNC_WAIT_TIMEOUT_MS = 5 * 6e4;
122285
122551
  static HISTORY_RECONNECT_JITTER_MIN_MS = 500;
122286
122552
  static HISTORY_RECONNECT_JITTER_MAX_MS = 1500;
122287
- static DISPATCH_HISTORY_SYNC_TIMEOUT_CODE = "dispatch_recovery_unhealthy";
122288
122553
  static setUnrefTimeout(callback, delayMs) {
122289
122554
  const timer2 = setTimeout(callback, delayMs);
122290
122555
  if (typeof timer2 === "object" && "unref" in timer2) {
@@ -122333,23 +122598,20 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122333
122598
  }
122334
122599
  return Boolean(meta.latestUserMsgId && meta.latestUserMsgId !== meta.lastHandledUserMsgId);
122335
122600
  }
122336
- async setHistorySyncWaitStatus(sessionId, sessionDoc, detail) {
122337
- try {
122338
- await sessionDoc.setStatus(SessionStatusFactory.initializing(void 0, detail));
122339
- } catch (error2) {
122340
- this.deps.logger.debug(`[${sessionId}] Failed to set history sync wait status (${detail}): ${formatErrorMessage(error2)}`);
122341
- }
122342
- }
122343
122601
  async waitForPendingUserTurnHistorySync(sessionId, sessionDoc, meta) {
122344
122602
  this.deps.logger.debug(`[${sessionId}] Pending user turn metadata is visible but history is missing it; waiting up to ${SessionDispatchWatcher.HISTORY_SYNC_WAIT_TIMEOUT_MS / 1e3}s for history CRDT sync`);
122345
- let currentWaitDetail = "joining-history";
122346
- await this.setHistorySyncWaitStatus(sessionId, sessionDoc, currentWaitDetail);
122347
122603
  try {
122348
122604
  await sessionDoc.ensureDocRoomJoined();
122349
122605
  } catch (error2) {
122350
122606
  this.deps.logger.debug(`[${sessionId}] Failed to ensure session history room is joined before waiting: ${formatErrorMessage(error2)}`);
122351
122607
  }
122352
- const turnAfterJoin = await this.checkHistoryAndQueue(sessionDoc, meta);
122608
+ await sessionDoc.waitUntilSynced();
122609
+ const freshMeta = await sessionDoc.getMetaState() ?? meta;
122610
+ if (!this.hasPendingUserTurnSignal(freshMeta)) {
122611
+ this.deps.logger.debug(`[${sessionId}] Pending user turn pointer cleared during pre-wait sync; exiting wait`);
122612
+ return null;
122613
+ }
122614
+ const turnAfterJoin = await this.checkHistoryAndQueue(sessionDoc, freshMeta);
122353
122615
  if (turnAfterJoin) {
122354
122616
  return turnAfterJoin;
122355
122617
  }
@@ -122367,18 +122629,11 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122367
122629
  unsubscribeMirror?.();
122368
122630
  unsubscribeStatus?.();
122369
122631
  };
122370
- const updateWaitStatus = (detail) => {
122371
- if (currentWaitDetail === detail) {
122372
- return;
122373
- }
122374
- currentWaitDetail = detail;
122375
- void this.setHistorySyncWaitStatus(sessionId, sessionDoc, detail);
122376
- };
122377
122632
  const checkForTurn = () => {
122378
122633
  if (settled) {
122379
122634
  return;
122380
122635
  }
122381
- void this.checkHistoryAndQueue(sessionDoc, meta).then((turn) => {
122636
+ void this.checkHistoryAndQueue(sessionDoc, freshMeta).then((turn) => {
122382
122637
  if (settled || !turn) {
122383
122638
  return;
122384
122639
  }
@@ -122398,7 +122653,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122398
122653
  }
122399
122654
  reconnectAttempted = true;
122400
122655
  void (async () => {
122401
- updateWaitStatus("reconnecting-history");
122402
122656
  const jitterMs = SessionDispatchWatcher.getReconnectJitterMs();
122403
122657
  this.deps.logger.debug(`[${sessionId}] Session history room ${reason}; attempting one rejoin in ${jitterMs}ms`);
122404
122658
  await SessionDispatchWatcher.sleep(jitterMs);
@@ -122417,14 +122671,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122417
122671
  if (settled || !status) {
122418
122672
  return;
122419
122673
  }
122420
- if (status === "connecting" || status === "joined") {
122421
- updateWaitStatus("joining-history");
122422
- return;
122423
- }
122424
- if (status === "reconnecting") {
122425
- updateWaitStatus("reconnecting-history");
122426
- return;
122427
- }
122428
122674
  if (status === "disconnected" || status === "error") {
122429
122675
  attemptReconnectOnce(status);
122430
122676
  }
@@ -122461,16 +122707,15 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122461
122707
  if (meta.machineId !== this.deps.machineId || meta.isArchived || !this.hasPendingUserTurnSignal(meta)) {
122462
122708
  return;
122463
122709
  }
122464
- await this.deps.workspaceDocument.repo.upsertDocMeta?.(roomId, {
122465
- status: SessionStatusFactory.idle(),
122466
- latestUserMsgId: void 0,
122467
- processingUserMsgId: void 0,
122468
- dispatchError: {
122469
- code: SessionDispatchWatcher.DISPATCH_HISTORY_SYNC_TIMEOUT_CODE,
122470
- message: "Dispatch recovery could not reconnect to this session after 5 minutes. Send a new message to retry.",
122471
- at: getServerNow()
122472
- }
122473
- });
122710
+ const pendingUserMsgId = meta.processingUserMsgId ?? meta.latestUserMsgId ?? meta.lastHandledUserMsgId;
122711
+ const recoveryPatch = {
122712
+ status: SessionStatusFactory.idle()
122713
+ };
122714
+ if (pendingUserMsgId) {
122715
+ recoveryPatch.lastHandledUserMsgId = pendingUserMsgId;
122716
+ recoveryPatch.latestUserMsgId = pendingUserMsgId;
122717
+ }
122718
+ await this.deps.workspaceDocument.repo.upsertDocMeta?.(roomId, recoveryPatch);
122474
122719
  const watched = this.watchedSessions.get(sessionId);
122475
122720
  watched?.unsubscribe();
122476
122721
  this.watchedSessions.delete(sessionId);
@@ -123217,8 +123462,14 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
123217
123462
  permissionRequestStartTimes = /* @__PURE__ */ new Map();
123218
123463
  machineHeartbeatTimer = null;
123219
123464
  static MACHINE_HEARTBEAT_INTERVAL_MS = 2e4;
123220
- evictForMemoryPressureFn = async () => {
123221
- };
123465
+ evictForMemoryPressureFn = async () => ({
123466
+ availableMemoryBytes: 0,
123467
+ thresholdBytes: 0,
123468
+ hadMemoryPressure: false,
123469
+ stillUnderPressure: false,
123470
+ evictedSessionIds: [],
123471
+ pressureReason: null
123472
+ });
123222
123473
  executionService;
123223
123474
  sessionDispatchWatcher;
123224
123475
  autoPromptRunner;
@@ -125230,6 +125481,16 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125230
125481
  hasPendingUpdates(sessionId) {
125231
125482
  return this.store.has(sessionId) && this.store.get(sessionId).acpUpdateBuffer.length > 0;
125232
125483
  }
125484
+ async hasPendingUserWork(sessionId) {
125485
+ const meta = (await this.workspaceDocument.repo.getDocMeta(getSessionRoomId(sessionId)))?.meta;
125486
+ if (!meta) {
125487
+ return false;
125488
+ }
125489
+ if (meta.processingUserMsgId) {
125490
+ return true;
125491
+ }
125492
+ return Boolean(meta.latestUserMsgId && meta.latestUserMsgId !== meta.lastHandledUserMsgId);
125493
+ }
125233
125494
  isArchiveInFlight(sessionId) {
125234
125495
  return this.archiveInFlight.has(sessionId);
125235
125496
  }
@@ -125586,116 +125847,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125586
125847
  }
125587
125848
  }
125588
125849
  }
125589
- function getAvailableMemoryBytes() {
125590
- const systemAvailable = getSystemAvailableMemoryBytes();
125591
- const cgroupAvailable = getCgroupAvailableMemoryBytes();
125592
- if (cgroupAvailable !== null) {
125593
- return Math.min(systemAvailable, cgroupAvailable);
125594
- }
125595
- return systemAvailable;
125596
- }
125597
- function getEffectiveMemoryLimitBytes() {
125598
- const totalMem = os__default.totalmem();
125599
- const cgroupMax = getCgroupMemoryMaxBytes();
125600
- if (cgroupMax !== null) {
125601
- return Math.min(totalMem, cgroupMax);
125602
- }
125603
- return totalMem;
125604
- }
125605
- function getSystemAvailableMemoryBytes() {
125606
- try {
125607
- const meminfo = readFileSync("/proc/meminfo", "utf8");
125608
- const match5 = meminfo.match(/MemAvailable:\s+(\d+)/);
125609
- if (match5?.[1]) {
125610
- return parseInt(match5[1], 10) * 1024;
125611
- }
125612
- } catch {
125613
- }
125614
- return os__default.freemem();
125615
- }
125616
- function getCgroupMemoryMaxBytes() {
125617
- try {
125618
- const cgroupPath = readSelfCgroupPath();
125619
- if (cgroupPath === null) return null;
125620
- let tightest = null;
125621
- let current2 = cgroupPath;
125622
- for (let depth = 0; depth < 20; depth++) {
125623
- const memMaxPath = `/sys/fs/cgroup${current2 === "/" ? "" : current2}/memory.max`;
125624
- const raw = readFileSafe(memMaxPath);
125625
- if (raw !== null) {
125626
- const trimmed = raw.trim();
125627
- if (trimmed !== "max") {
125628
- const value = parseInt(trimmed, 10);
125629
- if (Number.isFinite(value) && value > 0) {
125630
- tightest = tightest === null ? value : Math.min(tightest, value);
125631
- }
125632
- }
125633
- }
125634
- if (current2 === "/" || current2 === "") break;
125635
- const parent = current2.substring(0, current2.lastIndexOf("/")) || "/";
125636
- if (parent === current2) break;
125637
- current2 = parent;
125638
- }
125639
- return tightest;
125640
- } catch {
125641
- return null;
125642
- }
125643
- }
125644
- function getCgroupAvailableMemoryBytes() {
125645
- try {
125646
- const cgroupPath = readSelfCgroupPath();
125647
- if (cgroupPath === null) return null;
125648
- let tightestMax = null;
125649
- let tightestPath = null;
125650
- let current2 = cgroupPath;
125651
- for (let depth = 0; depth < 20; depth++) {
125652
- const prefix = `/sys/fs/cgroup${current2 === "/" ? "" : current2}`;
125653
- const raw = readFileSafe(`${prefix}/memory.max`);
125654
- if (raw !== null) {
125655
- const trimmed = raw.trim();
125656
- if (trimmed !== "max") {
125657
- const value = parseInt(trimmed, 10);
125658
- if (Number.isFinite(value) && value > 0) {
125659
- if (tightestMax === null || value < tightestMax) {
125660
- tightestMax = value;
125661
- tightestPath = prefix;
125662
- }
125663
- }
125664
- }
125665
- }
125666
- if (current2 === "/" || current2 === "") break;
125667
- const parent = current2.substring(0, current2.lastIndexOf("/")) || "/";
125668
- if (parent === current2) break;
125669
- current2 = parent;
125670
- }
125671
- if (tightestMax === null || tightestPath === null) return null;
125672
- const currentRaw = readFileSafe(`${tightestPath}/memory.current`);
125673
- if (currentRaw === null) return null;
125674
- const currentUsage = parseInt(currentRaw.trim(), 10);
125675
- if (!Number.isFinite(currentUsage)) return null;
125676
- return Math.max(0, tightestMax - currentUsage);
125677
- } catch {
125678
- return null;
125679
- }
125680
- }
125681
- function readSelfCgroupPath() {
125682
- try {
125683
- const content = readFileSync("/proc/self/cgroup", "utf8");
125684
- const line3 = content.split("\n").map((l) => l.trim()).find((l) => l.startsWith("0::"));
125685
- if (!line3) return null;
125686
- const cgroupPath = line3.slice(3).trim();
125687
- return cgroupPath || "/";
125688
- } catch {
125689
- return null;
125690
- }
125691
- }
125692
- function readFileSafe(filePath) {
125693
- try {
125694
- return readFileSync(filePath, "utf8");
125695
- } catch {
125696
- return null;
125697
- }
125698
- }
125699
125850
  function readEnvNumber(key2, fallback2) {
125700
125851
  const raw = process.env[key2];
125701
125852
  if (!raw) return fallback2;
@@ -125703,6 +125854,8 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125703
125854
  return Number.isFinite(parsed) ? parsed : fallback2;
125704
125855
  }
125705
125856
  const GIB = 1024 * 1024 * 1024;
125857
+ const WINDOWS_COMMIT_THRESHOLD_FLOOR_BYTES = 512 * 1024 * 1024;
125858
+ const WINDOWS_COMMIT_THRESHOLD_CEILING_BYTES = 2 * GIB;
125706
125859
  function defaultMemoryThresholdBytes() {
125707
125860
  const tenPercent = Math.floor(getEffectiveMemoryLimitBytes() * 0.1);
125708
125861
  return Math.max(GIB, Math.min(4 * GIB, tenPercent));
@@ -125715,6 +125868,9 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125715
125868
  memoryThresholdBytes: readEnvNumber("LODY_SESSION_GC_MEMORY_THRESHOLD_BYTES", defaultMemoryThresholdBytes())
125716
125869
  };
125717
125870
  }
125871
+ function getWindowsCommitThresholdBytes(memoryThresholdBytes) {
125872
+ return Math.max(WINDOWS_COMMIT_THRESHOLD_FLOOR_BYTES, Math.min(WINDOWS_COMMIT_THRESHOLD_CEILING_BYTES, memoryThresholdBytes));
125873
+ }
125718
125874
  class SessionGCManager {
125719
125875
  constructor(config2, deps) {
125720
125876
  this.config = config2;
@@ -125738,7 +125894,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125738
125894
  }
125739
125895
  async sweep() {
125740
125896
  const sweepStart = Date.now();
125741
- const candidates = this.getIdleCandidates();
125897
+ const candidates = await this.getIdleCandidates();
125742
125898
  if (candidates.length === 0) {
125743
125899
  return;
125744
125900
  }
@@ -125746,7 +125902,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125746
125902
  let cleaned = 0;
125747
125903
  let skipped = 0;
125748
125904
  for (const { sessionId } of candidates) {
125749
- if (!this.isStillEligibleForGC(sessionId)) {
125905
+ if (!await this.isStillEligibleForGC(sessionId)) {
125750
125906
  skipped++;
125751
125907
  continue;
125752
125908
  }
@@ -125764,19 +125920,50 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125764
125920
  this.deps.logger.debug(`[GC] Sweep completed: cleaned ${cleaned}/${candidates.length} sessions in ${sweepDuration}ms`);
125765
125921
  }
125766
125922
  async evictForMemoryPressure(excludeSessionId) {
125923
+ const thresholdBytes = this.config.memoryThresholdBytes;
125924
+ const commitThresholdBytes = getWindowsCommitThresholdBytes(thresholdBytes);
125925
+ let memorySnapshot = getMemoryPressureSnapshot();
125926
+ let availableMemory = memorySnapshot.availableMemoryBytes;
125927
+ let pressureReason = this.getPressureReason(memorySnapshot, thresholdBytes, commitThresholdBytes);
125767
125928
  if (!this.config.enabled) {
125768
- return;
125929
+ return {
125930
+ availableMemoryBytes: availableMemory,
125931
+ thresholdBytes,
125932
+ hadMemoryPressure: false,
125933
+ stillUnderPressure: false,
125934
+ evictedSessionIds: [],
125935
+ pressureReason: null,
125936
+ ...memorySnapshot.availableCommitBytes !== void 0 ? {
125937
+ availableCommitBytes: memorySnapshot.availableCommitBytes,
125938
+ commitThresholdBytes,
125939
+ commitLimitBytes: memorySnapshot.commitLimitBytes,
125940
+ committedBytes: memorySnapshot.committedBytes
125941
+ } : {}
125942
+ };
125769
125943
  }
125770
- let availableMemory = getAvailableMemoryBytes();
125771
- if (availableMemory >= this.config.memoryThresholdBytes) {
125772
- return;
125944
+ if (pressureReason === null) {
125945
+ return {
125946
+ availableMemoryBytes: availableMemory,
125947
+ thresholdBytes,
125948
+ hadMemoryPressure: false,
125949
+ stillUnderPressure: false,
125950
+ evictedSessionIds: [],
125951
+ pressureReason: null,
125952
+ ...memorySnapshot.availableCommitBytes !== void 0 ? {
125953
+ availableCommitBytes: memorySnapshot.availableCommitBytes,
125954
+ commitThresholdBytes,
125955
+ commitLimitBytes: memorySnapshot.commitLimitBytes,
125956
+ committedBytes: memorySnapshot.committedBytes
125957
+ } : {}
125958
+ };
125773
125959
  }
125774
- this.deps.logger.debug(`[GC] Memory pressure detected: ${Math.round(availableMemory / 1024 / 1024)}MB available (threshold: ${Math.round(this.config.memoryThresholdBytes / 1024 / 1024)}MB)`);
125960
+ const commitText = memorySnapshot.availableCommitBytes !== void 0 ? `, commit headroom ${Math.round(memorySnapshot.availableCommitBytes / 1024 / 1024)}MB (threshold: ${Math.round(commitThresholdBytes / 1024 / 1024)}MB)` : "";
125961
+ this.deps.logger.debug(`[GC] Memory pressure detected: ${Math.round(availableMemory / 1024 / 1024)}MB available (threshold: ${Math.round(thresholdBytes / 1024 / 1024)}MB)${commitText}`);
125775
125962
  const sessions = this.getSessionsWithIdleTime();
125776
125963
  sessions.sort((a, b) => b.idleMs - a.idleMs);
125777
- let evicted = 0;
125964
+ const evictedSessionIds = [];
125778
125965
  for (const { sessionId, idleMs } of sessions) {
125779
- if (availableMemory >= this.config.memoryThresholdBytes) {
125966
+ if (pressureReason === null) {
125780
125967
  break;
125781
125968
  }
125782
125969
  if (excludeSessionId && sessionId === excludeSessionId) {
@@ -125785,30 +125972,63 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125785
125972
  if (idleMs === 0) {
125786
125973
  continue;
125787
125974
  }
125788
- if (!this.isEligibleForCleanup(sessionId)) {
125975
+ if (!await this.isEligibleForCleanup(sessionId)) {
125789
125976
  continue;
125790
125977
  }
125791
125978
  try {
125792
125979
  this.deps.logger.debug(`[GC] Evicting session ${sessionId} (idle ${Math.round(idleMs / 1e3)}s) due to memory pressure`);
125793
125980
  await this.deps.cleanSession(sessionId);
125794
- evicted++;
125795
- availableMemory = getAvailableMemoryBytes();
125981
+ evictedSessionIds.push(sessionId);
125982
+ memorySnapshot = getMemoryPressureSnapshot();
125983
+ availableMemory = memorySnapshot.availableMemoryBytes;
125984
+ pressureReason = this.getPressureReason(memorySnapshot, thresholdBytes, commitThresholdBytes);
125796
125985
  } catch (error2) {
125797
125986
  this.deps.logger.error(`[GC] Failed to evict session ${sessionId}: ${formatErrorMessage(error2)}`);
125798
125987
  }
125799
125988
  }
125800
- if (evicted > 0) {
125801
- this.deps.logger.debug(`[GC] Memory pressure eviction complete: evicted ${evicted} sessions, available memory now ${Math.round(availableMemory / 1024 / 1024)}MB`);
125989
+ const stillUnderPressure = pressureReason !== null;
125990
+ if (evictedSessionIds.length > 0) {
125991
+ this.deps.logger.debug(`[GC] Memory pressure eviction complete: evicted ${evictedSessionIds.length} sessions, available memory now ${Math.round(availableMemory / 1024 / 1024)}MB`);
125992
+ } else if (stillUnderPressure) {
125993
+ this.deps.logger.debug("[GC] Memory pressure persists but no idle sessions were eligible for eviction");
125994
+ }
125995
+ return {
125996
+ availableMemoryBytes: availableMemory,
125997
+ thresholdBytes,
125998
+ hadMemoryPressure: true,
125999
+ stillUnderPressure,
126000
+ evictedSessionIds,
126001
+ pressureReason,
126002
+ ...memorySnapshot.availableCommitBytes !== void 0 ? {
126003
+ availableCommitBytes: memorySnapshot.availableCommitBytes,
126004
+ commitThresholdBytes,
126005
+ commitLimitBytes: memorySnapshot.commitLimitBytes,
126006
+ committedBytes: memorySnapshot.committedBytes
126007
+ } : {}
126008
+ };
126009
+ }
126010
+ getPressureReason(snapshot, thresholdBytes, commitThresholdBytes) {
126011
+ const physicalPressure = snapshot.availableMemoryBytes < thresholdBytes;
126012
+ const commitPressure = snapshot.availableCommitBytes !== void 0 && snapshot.availableCommitBytes < commitThresholdBytes;
126013
+ if (physicalPressure && commitPressure) {
126014
+ return "physical_and_commit";
126015
+ }
126016
+ if (physicalPressure) {
126017
+ return "physical";
125802
126018
  }
126019
+ if (commitPressure) {
126020
+ return "commit";
126021
+ }
126022
+ return null;
125803
126023
  }
125804
- getIdleCandidates() {
126024
+ async getIdleCandidates() {
125805
126025
  const sessions = this.getSessionsWithIdleTime();
125806
126026
  const candidates = [];
125807
126027
  for (const session of sessions) {
125808
126028
  if (session.idleMs < this.config.idleTimeoutMs) {
125809
126029
  continue;
125810
126030
  }
125811
- if (!this.isEligibleForCleanup(session.sessionId)) {
126031
+ if (!await this.isEligibleForCleanup(session.sessionId)) {
125812
126032
  continue;
125813
126033
  }
125814
126034
  candidates.push(session);
@@ -125836,20 +126056,23 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125836
126056
  }
125837
126057
  return result;
125838
126058
  }
125839
- isEligibleForCleanup(sessionId) {
126059
+ async isEligibleForCleanup(sessionId) {
125840
126060
  if (this.deps.hasActiveTurn(sessionId)) {
125841
126061
  return false;
125842
126062
  }
125843
126063
  if (this.deps.hasPendingUpdates(sessionId)) {
125844
126064
  return false;
125845
126065
  }
126066
+ if (await this.deps.hasPendingUserWork(sessionId)) {
126067
+ return false;
126068
+ }
125846
126069
  if (this.deps.isArchiveInFlight(sessionId)) {
125847
126070
  return false;
125848
126071
  }
125849
126072
  return true;
125850
126073
  }
125851
- isStillEligibleForGC(sessionId) {
125852
- if (!this.isEligibleForCleanup(sessionId)) {
126074
+ async isStillEligibleForGC(sessionId) {
126075
+ if (!await this.isEligibleForCleanup(sessionId)) {
125853
126076
  return false;
125854
126077
  }
125855
126078
  const lastActivity = this.deps.getSessionLastActivity(sessionId);
@@ -125977,6 +126200,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125977
126200
  getSessionLastActivity: (sessionId) => handler.getLastActivity(sessionId),
125978
126201
  hasActiveTurn: (sessionId) => handler.hasActiveTurn(sessionId),
125979
126202
  hasPendingUpdates: (sessionId) => handler.hasPendingUpdates(sessionId),
126203
+ hasPendingUserWork: async (sessionId) => await handler.hasPendingUserWork(sessionId),
125980
126204
  isArchiveInFlight: (sessionId) => handler.isArchiveInFlight(sessionId),
125981
126205
  cleanSession: (sessionId) => handler.cleanSessionForGC(sessionId),
125982
126206
  getSessionIds: () => handler.getTrackedSessionIds(),
@@ -125985,8 +126209,16 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125985
126209
  this.gcManager.start();
125986
126210
  handler.setEvictForMemoryPressure(async (excludeSessionId) => {
125987
126211
  if (this.gcManager) {
125988
- await this.gcManager.evictForMemoryPressure(excludeSessionId);
126212
+ return await this.gcManager.evictForMemoryPressure(excludeSessionId);
125989
126213
  }
126214
+ return {
126215
+ availableMemoryBytes: 0,
126216
+ thresholdBytes: 0,
126217
+ hadMemoryPressure: false,
126218
+ stillUnderPressure: false,
126219
+ evictedSessionIds: [],
126220
+ pressureReason: null
126221
+ };
125990
126222
  });
125991
126223
  }
125992
126224
  requireSessionManager() {
@@ -133184,6 +133416,9 @@ export PATH=${toSingleQuotedShellString(ghShimBinDir)}:"$PATH"
133184
133416
  isControlPlaneReady() {
133185
133417
  return this.documentManager.isTransportConnected();
133186
133418
  }
133419
+ isControlPlaneRecovering() {
133420
+ return this.documentManager.isTransportRecovering();
133421
+ }
133187
133422
  getActiveSessionCount() {
133188
133423
  return this.runtime.getActiveSessionCount();
133189
133424
  }
@@ -135013,23 +135248,26 @@ export PATH=${toSingleQuotedShellString(ghShimBinDir)}:"$PATH"
135013
135248
  refreshRuntimeState() {
135014
135249
  const desiredCount = this.desiredWorkspaces.size;
135015
135250
  let connectedCount = 0;
135251
+ let reconnectingCount = 0;
135016
135252
  let totalActiveSessions = 0;
135017
135253
  let totalConnectedRooms = 0;
135018
135254
  for (const runtime of this.runtimes.values()) {
135019
135255
  if (runtime.lody.isControlPlaneReady()) {
135020
135256
  connectedCount += 1;
135257
+ } else if (runtime.lody.isControlPlaneRecovering()) {
135258
+ reconnectingCount += 1;
135021
135259
  }
135022
135260
  totalActiveSessions += runtime.lody.getActiveSessionCount();
135023
135261
  totalConnectedRooms += runtime.lody.getConnectedRoomCount();
135024
135262
  }
135025
135263
  this.runtimeStateReporter.setActiveSessionCount(totalActiveSessions);
135026
135264
  this.runtimeStateReporter.setConnectedRoomCount(totalConnectedRooms);
135027
- const nextConnectivity = desiredCount === 0 ? "online" : connectedCount === 0 ? "offline" : connectedCount < desiredCount ? "reconnecting" : "online";
135265
+ const hasWorkspaceRetry = this.retryTimers.size > 0 || this.startInFlight.size > 0;
135266
+ const nextConnectivity = desiredCount === 0 ? "online" : connectedCount === desiredCount ? "online" : reconnectingCount > 0 || hasWorkspaceRetry ? "reconnecting" : "offline";
135028
135267
  if (this.lastConnectivity !== nextConnectivity) {
135029
135268
  this.lastConnectivity = nextConnectivity;
135030
135269
  this.runtimeStateReporter.setConnectivity(nextConnectivity);
135031
135270
  }
135032
- const hasWorkspaceRetry = this.retryTimers.size > 0 || this.startInFlight.size > 0;
135033
135271
  if (hasWorkspaceRetry && !this.hasWorkspaceRetryIssue) {
135034
135272
  this.hasWorkspaceRetryIssue = true;
135035
135273
  this.runtimeStateReporter.upsertIssue({
@@ -167312,10 +167550,18 @@ ${entry2.text}`).join("\n\n");
167312
167550
  }
167313
167551
  const DEFAULT_MIN_RETRY_MS$1 = 1e3;
167314
167552
  const DEFAULT_MAX_RETRY_MS$1 = 3e4;
167315
- function buildRetryDelay(attempt, minMs = DEFAULT_MIN_RETRY_MS$1, maxMs = DEFAULT_MAX_RETRY_MS$1) {
167553
+ const DEFAULT_JITTER_FRACTION = 0.2;
167554
+ function buildRetryDelay(attempt, minMs = DEFAULT_MIN_RETRY_MS$1, maxMs = DEFAULT_MAX_RETRY_MS$1, options = {}) {
167316
167555
  const exponent = Math.max(0, attempt);
167317
- const delay2 = minMs * 2 ** exponent;
167318
- return Math.min(delay2, maxMs);
167556
+ const baseDelay = Math.min(minMs * 2 ** exponent, maxMs);
167557
+ const jitterFraction = Math.max(0, options.jitterFraction ?? DEFAULT_JITTER_FRACTION);
167558
+ if (jitterFraction === 0) {
167559
+ return baseDelay;
167560
+ }
167561
+ const random2 = options.random ?? Math.random;
167562
+ const randomValue = Math.min(1, Math.max(0, random2()));
167563
+ const jitterMultiplier = 1 + (randomValue * 2 - 1) * jitterFraction;
167564
+ return Math.min(Math.max(0, Math.round(baseDelay * jitterMultiplier)), maxMs);
167319
167565
  }
167320
167566
  class FailureWindow {
167321
167567
  historyMs = [];
@@ -167376,6 +167622,7 @@ ${result.stderr}`;
167376
167622
  probeFailureThreshold;
167377
167623
  minRetryMs;
167378
167624
  maxRetryMs;
167625
+ retryRandom;
167379
167626
  failureWindow;
167380
167627
  triggered = false;
167381
167628
  inFlight = false;
@@ -167407,6 +167654,7 @@ ${result.stderr}`;
167407
167654
  this.probeFailureThreshold = options.probeFailureThreshold ?? DEFAULT_PROBE_FAILURE_THRESHOLD;
167408
167655
  this.minRetryMs = options.minRetryMs ?? DEFAULT_MIN_RETRY_MS;
167409
167656
  this.maxRetryMs = options.maxRetryMs ?? DEFAULT_MAX_RETRY_MS;
167657
+ this.retryRandom = options.retryRandom ?? Math.random;
167410
167658
  this.failureWindow = new FailureWindow(options.fatalFailureWindowMs ?? DEFAULT_FATAL_FAILURE_WINDOW_MS, options.fatalFailureThreshold ?? DEFAULT_FATAL_FAILURE_THRESHOLD);
167411
167659
  }
167412
167660
  getState() {
@@ -167486,6 +167734,9 @@ ${result.stderr}`;
167486
167734
  this.latestRuntimeState = runtimeState;
167487
167735
  this.probeFailureCount = 0;
167488
167736
  this.probeUnavailable = false;
167737
+ this.clearRetryTimer();
167738
+ this.attempt = 0;
167739
+ this.failureWindow.reset();
167489
167740
  this.lastStateMessage = void 0;
167490
167741
  } else {
167491
167742
  this.probeFailureCount += 1;
@@ -167506,7 +167757,9 @@ ${result.stderr}`;
167506
167757
  } else if (this.fatalReason) {
167507
167758
  phase = "fatal";
167508
167759
  } else if (runtime) {
167509
- phase = runtime.phase === "fatal" ? "fatal" : runtime.phase;
167760
+ phase = runtime.phase === "fatal" ? "fatal" : runtime.connectivity === "reconnecting" ? "reconnecting" : runtime.phase;
167761
+ } else if (this.retryTimer) {
167762
+ phase = "reconnecting";
167510
167763
  } else if (childRunning) {
167511
167764
  phase = this.probeUnavailable ? "offline" : "starting";
167512
167765
  } else if (this.probeUnavailable && !this.inFlight && !this.retryTimer) {
@@ -167563,7 +167816,9 @@ ${result.stderr}`;
167563
167816
  scheduleRetry(reason, recordFailure = true) {
167564
167817
  if (this.fatalReason) return;
167565
167818
  if (recordFailure && this.recordFailure(reason)) return;
167566
- const delay2 = buildRetryDelay(this.attempt, this.minRetryMs, this.maxRetryMs);
167819
+ const delay2 = buildRetryDelay(this.attempt, this.minRetryMs, this.maxRetryMs, {
167820
+ random: this.retryRandom
167821
+ });
167567
167822
  this.attempt += 1;
167568
167823
  this.clearRetryTimer();
167569
167824
  this.pendingRetryInMs = delay2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lody",
3
- "version": "0.44.2-next.1",
3
+ "version": "0.44.4-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",
77
- "@lody/loro-streams-rpc": "0.0.1",
78
76
  "@lody/shared": "0.0.1",
79
- "loro-code": "0.0.1"
77
+ "loro-code": "0.0.1",
78
+ "@lody/cli-supervisor": "0.0.1",
79
+ "@lody/loro-streams-rpc": "0.0.1"
80
80
  },
81
81
  "files": [
82
82
  "dist",