lody 0.45.1 → 0.46.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.
Files changed (2) hide show
  1. package/dist/index.js +924 -307
  2. package/package.json +6 -6
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.45.1";
36823
+ const version$4 = "0.46.0";
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";
@@ -36864,7 +36864,7 @@ Mongoose Error Code: ${error2.code}` : ""}`
36864
36864
  };
36865
36865
  const optionalDependencies = {
36866
36866
  "acp-extension-claude": "0.30.0",
36867
- "acp-extension-codex": "0.11.2"
36867
+ "acp-extension-codex": "0.12.0"
36868
36868
  };
36869
36869
  const devDependencies = {
36870
36870
  "@agentclientprotocol/sdk": "catalog:",
@@ -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",
@@ -81990,6 +81991,59 @@ ${tailedOutput}` : null;
81990
81991
  gatewayBaseUrl: string$2().url().optional()
81991
81992
  });
81992
81993
  const LORO_STREAMS_TOKEN_REFRESH_SKEW_MS = 3e4;
81994
+ const LORO_STREAMS_TOKEN_STORAGE_KEY_PREFIX = "lody:loroStreamsToken";
81995
+ function getTokenStorageKey(workspaceId) {
81996
+ return `${LORO_STREAMS_TOKEN_STORAGE_KEY_PREFIX}:${workspaceId}`;
81997
+ }
81998
+ function getLocalStorage() {
81999
+ try {
82000
+ const storage = globalThis.localStorage;
82001
+ if (storage && typeof storage.getItem === "function" && typeof storage.setItem === "function" && typeof storage.removeItem === "function") {
82002
+ return storage;
82003
+ }
82004
+ } catch {
82005
+ }
82006
+ return void 0;
82007
+ }
82008
+ function readCachedTokenFromStorage(workspaceId) {
82009
+ const storage = getLocalStorage();
82010
+ if (!storage) return null;
82011
+ try {
82012
+ const raw = storage.getItem(getTokenStorageKey(workspaceId));
82013
+ if (!raw) return null;
82014
+ const parsed = JSON.parse(raw);
82015
+ if (typeof parsed === "object" && parsed !== null && "token" in parsed && typeof parsed.token === "string" && "expiresAtMs" in parsed && typeof parsed.expiresAtMs === "number") {
82016
+ const result = {
82017
+ token: parsed.token,
82018
+ expiresAtMs: parsed.expiresAtMs
82019
+ };
82020
+ const gatewayBaseUrl = parsed.gatewayBaseUrl;
82021
+ if (typeof gatewayBaseUrl === "string") {
82022
+ result.gatewayBaseUrl = gatewayBaseUrl;
82023
+ }
82024
+ return result;
82025
+ }
82026
+ return null;
82027
+ } catch {
82028
+ return null;
82029
+ }
82030
+ }
82031
+ function writeCachedTokenToStorage(workspaceId, token2) {
82032
+ const storage = getLocalStorage();
82033
+ if (!storage) return;
82034
+ try {
82035
+ storage.setItem(getTokenStorageKey(workspaceId), JSON.stringify(token2));
82036
+ } catch {
82037
+ }
82038
+ }
82039
+ function clearCachedTokenFromStorage(workspaceId) {
82040
+ const storage = getLocalStorage();
82041
+ if (!storage) return;
82042
+ try {
82043
+ storage.removeItem(getTokenStorageKey(workspaceId));
82044
+ } catch {
82045
+ }
82046
+ }
81993
82047
  const buildLoroStreamsTokenEndpoint = (baseUrl) => `${baseUrl.replace(/\/+$/g, "")}/api/loro-streams/token`;
81994
82048
  class LoroStreamsTokenAuthError extends Error {
81995
82049
  constructor(message, status, detail) {
@@ -82002,7 +82056,7 @@ ${tailedOutput}` : null;
82002
82056
  function createLoroStreamsTokenProvider(options) {
82003
82057
  const fetchImpl = options.fetchImpl ?? fetch;
82004
82058
  const refreshSkewMs = options.refreshSkewMs ?? LORO_STREAMS_TOKEN_REFRESH_SKEW_MS;
82005
- let cached2 = null;
82059
+ let cached2 = readCachedTokenFromStorage(options.workspaceId);
82006
82060
  let inFlight = null;
82007
82061
  let generation = 0;
82008
82062
  const resolveAuthToken = async () => {
@@ -82049,6 +82103,7 @@ ${tailedOutput}` : null;
82049
82103
  inFlight = fetchToken().then((nextToken) => {
82050
82104
  if (generation === fetchGeneration) {
82051
82105
  cached2 = nextToken;
82106
+ writeCachedTokenToStorage(options.workspaceId, nextToken);
82052
82107
  }
82053
82108
  return nextToken;
82054
82109
  }).finally(() => {
@@ -82063,6 +82118,7 @@ ${tailedOutput}` : null;
82063
82118
  cached2 = null;
82064
82119
  inFlight = null;
82065
82120
  generation++;
82121
+ clearCachedTokenFromStorage(options.workspaceId);
82066
82122
  };
82067
82123
  return {
82068
82124
  getToken: async () => {
@@ -89037,8 +89093,20 @@ ${val.stack}`;
89037
89093
  const patchObject = patch2;
89038
89094
  for (const key2 of Object.keys(patchObject)) {
89039
89095
  const rawValue = patchObject[key2];
89040
- if (rawValue === void 0) continue;
89041
- if (jsonEquals(base ? base[key2] : void 0, rawValue)) continue;
89096
+ const hadKey = base ? key2 in base : false;
89097
+ if (rawValue === void 0) {
89098
+ if (!hadKey) continue;
89099
+ this.metaFlock.delete([
89100
+ "m",
89101
+ docId,
89102
+ key2
89103
+ ]);
89104
+ delete next[key2];
89105
+ outPatch[key2] = null;
89106
+ changed = true;
89107
+ continue;
89108
+ }
89109
+ if (jsonEquals(hadKey ? base[key2] : void 0, rawValue)) continue;
89042
89110
  this.metaFlock.put([
89043
89111
  "m",
89044
89112
  docId,
@@ -91144,7 +91212,7 @@ ${val.stack}`;
91144
91212
  live: "sse"
91145
91213
  });
91146
91214
  const controller = new AbortController();
91147
- const signal = mergeAbortSignals(input2.signal, controller.signal);
91215
+ const signal = mergeAbortSignals$2(input2.signal, controller.signal);
91148
91216
  try {
91149
91217
  const response = await this.http.fetchAuthorized(requestUrl, {
91150
91218
  method: "GET",
@@ -91647,7 +91715,7 @@ ${val.stack}`;
91647
91715
  }
91648
91716
  };
91649
91717
  }
91650
- function mergeAbortSignals(...signals2) {
91718
+ function mergeAbortSignals$2(...signals2) {
91651
91719
  const active2 = signals2.filter((signal) => signal != null);
91652
91720
  if (active2.length === 0) return;
91653
91721
  if (active2.length === 1) return active2[0];
@@ -91666,84 +91734,6 @@ ${val.stack}`;
91666
91734
  }
91667
91735
  return controller.signal;
91668
91736
  }
91669
- function composeBeforeRemoteCursorSaveHooks(...hooks2) {
91670
- const active2 = hooks2.filter((hook) => hook != null);
91671
- if (active2.length === 0) return;
91672
- return async (context2) => {
91673
- for (const hook of active2) await hook(context2);
91674
- };
91675
- }
91676
- async function persistRemoteCursor(input2) {
91677
- const next = {
91678
- ...input2.cursor,
91679
- updatedAtMs: (input2.now ?? Date.now)()
91680
- };
91681
- if (input2.beforeRemoteCursorSave != null) await input2.beforeRemoteCursorSave({
91682
- cursor: next,
91683
- source: input2.source
91684
- });
91685
- await input2.remoteCursorStore.save(next);
91686
- return next;
91687
- }
91688
- function key(streamUrl) {
91689
- return streamUrl;
91690
- }
91691
- function cloneJsonValue(value) {
91692
- return structuredClone(value);
91693
- }
91694
- function cloneRemoteCursor(cursor) {
91695
- return {
91696
- ...cursor,
91697
- serverLowerBoundVersion: cloneJsonValue(cursor.serverLowerBoundVersion)
91698
- };
91699
- }
91700
- var InMemoryRemoteCursorStore = class {
91701
- data = /* @__PURE__ */ new Map();
91702
- async load(streamUrl) {
91703
- const value = this.data.get(key(streamUrl));
91704
- return value == null ? null : cloneRemoteCursor(value);
91705
- }
91706
- async save(cursor) {
91707
- this.data.set(key(cursor.streamUrl), cloneRemoteCursor(cursor));
91708
- }
91709
- async delete(streamUrl) {
91710
- this.data.delete(key(streamUrl));
91711
- }
91712
- };
91713
- function createInitialRemoteCursor(input2) {
91714
- return {
91715
- streamUrl: input2.streamUrl,
91716
- nextOffset: input2.nextOffset,
91717
- serverLowerBoundVersion: cloneJsonValue(input2.serverLowerBoundVersion),
91718
- updatedAtMs: Date.now()
91719
- };
91720
- }
91721
- const DEFAULT_RECONNECT_CONFIG = {
91722
- delays: [
91723
- 0,
91724
- 500,
91725
- 1e3,
91726
- 2e3,
91727
- 4e3,
91728
- 8e3,
91729
- 15e3
91730
- ],
91731
- jitterFraction: 0.2,
91732
- maxAttempts: Infinity,
91733
- connectTimeoutMs: 1e4,
91734
- pollTimeoutMs: 3e4,
91735
- liveInactivityTimeoutMs: 45e3
91736
- };
91737
- function applyJitter(baseMs, fraction) {
91738
- if (baseMs === 0) return 0;
91739
- const jitter = baseMs * fraction * (2 * Math.random() - 1);
91740
- return Math.max(0, Math.round(baseMs + jitter));
91741
- }
91742
- function computeRetryDelay(attempt, config2 = DEFAULT_RECONNECT_CONFIG) {
91743
- const { delays, jitterFraction } = config2;
91744
- const base = delays[Math.min(attempt, delays.length - 1)];
91745
- return applyJitter(base, jitterFraction);
91746
- }
91747
91737
  const DEFAULT_COMPACT_THRESHOLD = 64;
91748
91738
  const DEFAULT_MIN_CAPACITY = 16;
91749
91739
  var Deque = class {
@@ -91863,6 +91853,32 @@ ${val.stack}`;
91863
91853
  this.tail = length2;
91864
91854
  }
91865
91855
  };
91856
+ const DEFAULT_RECONNECT_CONFIG = {
91857
+ delays: [
91858
+ 0,
91859
+ 500,
91860
+ 1e3,
91861
+ 2e3,
91862
+ 4e3,
91863
+ 8e3,
91864
+ 15e3
91865
+ ],
91866
+ jitterFraction: 0.2,
91867
+ maxAttempts: Infinity,
91868
+ connectTimeoutMs: 1e4,
91869
+ pollTimeoutMs: 3e4,
91870
+ liveInactivityTimeoutMs: 45e3
91871
+ };
91872
+ function applyJitter(baseMs, fraction) {
91873
+ if (baseMs === 0) return 0;
91874
+ const jitter = baseMs * fraction * (2 * Math.random() - 1);
91875
+ return Math.max(0, Math.round(baseMs + jitter));
91876
+ }
91877
+ function computeRetryDelay(attempt, config2 = DEFAULT_RECONNECT_CONFIG) {
91878
+ const { delays, jitterFraction } = config2;
91879
+ const base = delays[Math.min(attempt, delays.length - 1)];
91880
+ return applyJitter(base, jitterFraction);
91881
+ }
91866
91882
  const BINARY_CONTENT_TYPE = "application/octet-stream";
91867
91883
  function encodeItems(items) {
91868
91884
  const total = items.reduce((sum, item) => sum + 4 + item.byteLength, 0);
@@ -92305,6 +92321,25 @@ stream:${scope2.streamId}`;
92305
92321
  });
92306
92322
  });
92307
92323
  }
92324
+ function mergeAbortSignals(...signals2) {
92325
+ const active2 = signals2.filter((signal) => signal != null);
92326
+ if (active2.length === 0) return;
92327
+ if (active2.length === 1) return active2[0];
92328
+ if (typeof AbortSignal.any === "function") return AbortSignal.any(active2);
92329
+ const controller = new AbortController();
92330
+ for (const signal of active2) {
92331
+ if (signal.aborted) {
92332
+ controller.abort(signal.reason);
92333
+ break;
92334
+ }
92335
+ signal.addEventListener("abort", () => {
92336
+ controller.abort(signal.reason);
92337
+ }, {
92338
+ once: true
92339
+ });
92340
+ }
92341
+ return controller.signal;
92342
+ }
92308
92343
  function classifyFetchError(err2) {
92309
92344
  const message = err2 instanceof Error ? err2.message : String(err2);
92310
92345
  const status = err2 instanceof HttpError ? err2.status : void 0;
@@ -92508,7 +92543,10 @@ stream:${scope2.streamId}`;
92508
92543
  streamUrl;
92509
92544
  adapter;
92510
92545
  client;
92546
+ fetchImpl;
92547
+ authProvider;
92511
92548
  reconnectConfig;
92549
+ authRefreshInFlight;
92512
92550
  constructor(options) {
92513
92551
  this.streamUrl = options.streamUrl;
92514
92552
  this.adapter = options.adapter;
@@ -92524,6 +92562,8 @@ stream:${scope2.streamId}`;
92524
92562
  pollTimeoutMs: options.reconnectConfig.pollTimeoutMs
92525
92563
  }
92526
92564
  });
92565
+ this.fetchImpl = options.fetchImpl;
92566
+ this.authProvider = options.authProvider;
92527
92567
  this.reconnectConfig = options.reconnectConfig;
92528
92568
  }
92529
92569
  async createStreamRequest() {
@@ -92625,6 +92665,166 @@ stream:${scope2.streamId}`;
92625
92665
  };
92626
92666
  throw this.toTransportFailure(error2);
92627
92667
  }
92668
+ async appendItems(updates) {
92669
+ const result = await this.client.append({
92670
+ part: {
92671
+ contentType: BINARY_CONTENT_TYPE,
92672
+ body: encodeItems(updates)
92673
+ }
92674
+ });
92675
+ if (result.ok) return result.result.nextOffset;
92676
+ throw this.toTransportFailure(result.result);
92677
+ }
92678
+ async appendEphemeralUpdate(update2, signal) {
92679
+ if (update2.byteLength === 0) return;
92680
+ const response = await this.fetchAuthorized(this.streamUrl, {
92681
+ method: "POST",
92682
+ headers: {
92683
+ "Content-Type": BINARY_CONTENT_TYPE
92684
+ },
92685
+ body: update2,
92686
+ signal
92687
+ }, this.reconnectConfig.connectTimeoutMs, "connect");
92688
+ if (response.status === 200 || response.status === 204) return;
92689
+ throw await this.responseToEphemeralFailure(response, "ephemeral append");
92690
+ }
92691
+ async readEphemeralSse(onBootstrap, onData, signal) {
92692
+ const requestUrl = this.ephemeralReadEndpoint();
92693
+ const response = await this.fetchAuthorized(requestUrl, {
92694
+ method: "GET",
92695
+ headers: {
92696
+ Accept: "text/event-stream"
92697
+ },
92698
+ signal
92699
+ }, this.reconnectConfig.connectTimeoutMs, "connect");
92700
+ if (response.status === 404) return {
92701
+ kind: "not-found"
92702
+ };
92703
+ if (!response.ok) {
92704
+ if (response.status === 400) {
92705
+ const message = await response.text();
92706
+ if (isSseUnsupportedErrorText(message)) return {
92707
+ kind: "unsupported"
92708
+ };
92709
+ throw new HttpError(message.length > 0 ? `ephemeral live read failed: ${message}` : "ephemeral live read failed", response.status);
92710
+ }
92711
+ throw await this.responseToEphemeralFailure(response, "ephemeral live read");
92712
+ }
92713
+ if (response.body == null) throw new ProtocolError("ephemeral sse response missing body");
92714
+ if (!(response.headers.get("Content-Type") ?? "").toLowerCase().includes("text/event-stream")) throw new ProtocolError("ephemeral sse response must use text/event-stream content type");
92715
+ const dataEncoding = response.headers.get("Stream-SSE-Data-Encoding") ?? void 0;
92716
+ const consume = async () => {
92717
+ let sawBootstrap = false;
92718
+ for await (const event of readSseEvents(response.body)) {
92719
+ if (!sawBootstrap) {
92720
+ if (event.event !== "bootstrap") throw new ProtocolError(`ephemeral sse must start with bootstrap, got '${event.event}'`);
92721
+ await onBootstrap(decodeSsePayload(event.data, dataEncoding));
92722
+ sawBootstrap = true;
92723
+ continue;
92724
+ }
92725
+ if (event.event === "data") {
92726
+ await onData(decodeSsePayload(event.data, dataEncoding));
92727
+ continue;
92728
+ }
92729
+ throw new ProtocolError(`unexpected ephemeral sse event '${event.event}'`);
92730
+ }
92731
+ if (!sawBootstrap) throw new ProtocolError("ephemeral sse ended before bootstrap");
92732
+ return {
92733
+ kind: "ok"
92734
+ };
92735
+ };
92736
+ let abortHandler;
92737
+ const abortRace = signal != null && !signal.aborted ? new Promise((_2, reject) => {
92738
+ abortHandler = () => {
92739
+ response.body?.cancel().catch(() => {
92740
+ });
92741
+ reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
92742
+ };
92743
+ signal.addEventListener("abort", abortHandler, {
92744
+ once: true
92745
+ });
92746
+ }) : null;
92747
+ try {
92748
+ if (signal?.aborted) {
92749
+ await response.body.cancel().catch(() => {
92750
+ });
92751
+ throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
92752
+ }
92753
+ return abortRace == null ? await consume() : await Promise.race([
92754
+ consume(),
92755
+ abortRace
92756
+ ]);
92757
+ } finally {
92758
+ if (abortHandler != null && signal != null) signal.removeEventListener("abort", abortHandler);
92759
+ }
92760
+ }
92761
+ async fetchAuthorized(input2, init2, timeoutMs, phase) {
92762
+ const firstToken = await this.resolveToken({
92763
+ reason: "request"
92764
+ });
92765
+ const firstResponse = await this.fetchWithTimeout(input2, {
92766
+ ...init2,
92767
+ headers: this.attachAuthorization(init2.headers, firstToken)
92768
+ }, timeoutMs, phase);
92769
+ if (this.authProvider == null || firstResponse.status !== 401 && firstResponse.status !== 403) return firstResponse;
92770
+ await firstResponse.body?.cancel().catch(() => {
92771
+ });
92772
+ const refreshedToken = await this.refreshTokenAfterFailure(firstResponse.status, firstToken);
92773
+ return this.fetchWithTimeout(input2, {
92774
+ ...init2,
92775
+ headers: this.attachAuthorization(init2.headers, refreshedToken)
92776
+ }, timeoutMs, phase);
92777
+ }
92778
+ async fetchWithTimeout(input2, init2, timeoutMs, phase) {
92779
+ const controller = new AbortController();
92780
+ const signal = mergeAbortSignals(init2.signal, controller.signal);
92781
+ const timer2 = setTimeout(() => {
92782
+ controller.abort(new TimeoutError(phase, timeoutMs));
92783
+ }, timeoutMs);
92784
+ try {
92785
+ return await this.fetchImpl(input2, {
92786
+ ...init2,
92787
+ signal
92788
+ });
92789
+ } catch (error2) {
92790
+ if (controller.signal.aborted && controller.signal.reason instanceof TimeoutError) throw controller.signal.reason;
92791
+ throw error2;
92792
+ } finally {
92793
+ clearTimeout(timer2);
92794
+ }
92795
+ }
92796
+ async resolveToken(context2) {
92797
+ return await this.authProvider?.(context2);
92798
+ }
92799
+ async refreshTokenAfterFailure(status, previousToken) {
92800
+ if (this.authProvider == null) return;
92801
+ if (this.authRefreshInFlight == null) this.authRefreshInFlight = this.resolveToken({
92802
+ reason: "unauthorized",
92803
+ status,
92804
+ previousToken
92805
+ }).finally(() => {
92806
+ this.authRefreshInFlight = void 0;
92807
+ });
92808
+ return await this.authRefreshInFlight;
92809
+ }
92810
+ attachAuthorization(base, token2) {
92811
+ const headers = new Headers(base);
92812
+ if (token2 == null || token2.length === 0) {
92813
+ headers.delete("Authorization");
92814
+ return headers;
92815
+ }
92816
+ headers.set("Authorization", `Bearer ${token2}`);
92817
+ return headers;
92818
+ }
92819
+ async responseToEphemeralFailure(response, context2) {
92820
+ const message = await response.text().catch(() => "");
92821
+ return new HttpError(message.length > 0 ? `${context2} failed: ${message}` : `${context2} failed with status ${response.status}`, response.status);
92822
+ }
92823
+ ephemeralReadEndpoint() {
92824
+ const target = new URL(this.streamUrl);
92825
+ target.searchParams.set("live", "sse");
92826
+ return target.toString();
92827
+ }
92628
92828
  toTransportFailure(error2) {
92629
92829
  switch (error2.code) {
92630
92830
  case "bad_request":
@@ -92766,6 +92966,58 @@ stream:${scope2.streamId}`;
92766
92966
  };
92767
92967
  }
92768
92968
  };
92969
+ function composeBeforeRemoteCursorSaveHooks(...hooks2) {
92970
+ const active2 = hooks2.filter((hook) => hook != null);
92971
+ if (active2.length === 0) return;
92972
+ return async (context2) => {
92973
+ for (const hook of active2) await hook(context2);
92974
+ };
92975
+ }
92976
+ async function persistRemoteCursor(input2) {
92977
+ const next = {
92978
+ ...input2.cursor,
92979
+ updatedAtMs: (input2.now ?? Date.now)()
92980
+ };
92981
+ if (input2.beforeRemoteCursorSave != null) await input2.beforeRemoteCursorSave({
92982
+ cursor: next,
92983
+ source: input2.source
92984
+ });
92985
+ await input2.remoteCursorStore.save(next);
92986
+ return next;
92987
+ }
92988
+ function key(streamUrl) {
92989
+ return streamUrl;
92990
+ }
92991
+ function cloneJsonValue(value) {
92992
+ return structuredClone(value);
92993
+ }
92994
+ function cloneRemoteCursor(cursor) {
92995
+ return {
92996
+ ...cursor,
92997
+ serverLowerBoundVersion: cloneJsonValue(cursor.serverLowerBoundVersion)
92998
+ };
92999
+ }
93000
+ var InMemoryRemoteCursorStore = class {
93001
+ data = /* @__PURE__ */ new Map();
93002
+ async load(streamUrl) {
93003
+ const value = this.data.get(key(streamUrl));
93004
+ return value == null ? null : cloneRemoteCursor(value);
93005
+ }
93006
+ async save(cursor) {
93007
+ this.data.set(key(cursor.streamUrl), cloneRemoteCursor(cursor));
93008
+ }
93009
+ async delete(streamUrl) {
93010
+ this.data.delete(key(streamUrl));
93011
+ }
93012
+ };
93013
+ function createInitialRemoteCursor(input2) {
93014
+ return {
93015
+ streamUrl: input2.streamUrl,
93016
+ nextOffset: input2.nextOffset,
93017
+ serverLowerBoundVersion: cloneJsonValue(input2.serverLowerBoundVersion),
93018
+ updatedAtMs: Date.now()
93019
+ };
93020
+ }
92769
93021
  var TransportCursorManager = class {
92770
93022
  adapter;
92771
93023
  decodeSnapshot;
@@ -93023,6 +93275,8 @@ stream:${scope2.streamId}`;
93023
93275
  producerId = createProducerId();
93024
93276
  producerEpoch = 0;
93025
93277
  nextProducerSeq = 0;
93278
+ frozenLocalAppend;
93279
+ committedPendingCount;
93026
93280
  pendingWriteOnly = new Deque();
93027
93281
  writeOnlyCursor;
93028
93282
  unsubscribeWriteOnlyLocal;
@@ -93202,6 +93456,8 @@ stream:${scope2.streamId}`;
93202
93456
  await this.remoteCursorStore.delete?.(this.streamUrl);
93203
93457
  this.producerEpoch = 0;
93204
93458
  this.nextProducerSeq = 0;
93459
+ this.frozenLocalAppend = void 0;
93460
+ this.committedPendingCount = void 0;
93205
93461
  return deleted;
93206
93462
  })
93207
93463
  }
@@ -93242,15 +93498,17 @@ stream:${scope2.streamId}`;
93242
93498
  localVersion: this.writeOnlyCursor.serverLowerBoundVersion
93243
93499
  }
93244
93500
  };
93245
- const nextCursor = await this.appendLocalBatch(this.writeOnlyCursor, drained.batch, false);
93246
- for (let i2 = 0; i2 < drained.count; i2 += 1) this.pendingWriteOnly.popFront();
93247
- this.writeOnlyCursor = nextCursor;
93501
+ const appended = await this.appendLocalBatchRemote(this.writeOnlyCursor, drained.batch, drained.count, "write-only");
93502
+ const committedCount = appended.pendingCount ?? drained.count;
93503
+ for (let i2 = 0; i2 < committedCount; i2 += 1) this.pendingWriteOnly.popFront();
93504
+ this.writeOnlyCursor = appended.cursor;
93505
+ this.commitProducerAck(appended.producerAck);
93248
93506
  return {
93249
93507
  ok: true,
93250
93508
  value: {
93251
93509
  appended: true,
93252
- nextOffset: nextCursor.nextOffset,
93253
- localVersion: nextCursor.serverLowerBoundVersion
93510
+ nextOffset: appended.cursor.nextOffset,
93511
+ localVersion: appended.cursor.serverLowerBoundVersion
93254
93512
  }
93255
93513
  };
93256
93514
  } catch (error2) {
@@ -93425,7 +93683,7 @@ stream:${scope2.streamId}`;
93425
93683
  }
93426
93684
  }
93427
93685
  async syncOnce() {
93428
- let cursor = (await this.resolveInitialRemoteState()).cursor;
93686
+ let cursor = (await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState())).cursor;
93429
93687
  const localBatch = this.exportUpdates(cursor.serverLowerBoundVersion);
93430
93688
  if (localBatch.batch != null) {
93431
93689
  cursor = await this.appendLocalBatch(cursor, localBatch.batch);
@@ -93434,7 +93692,7 @@ stream:${scope2.streamId}`;
93434
93692
  return cursor;
93435
93693
  }
93436
93694
  async performInitialJoinSync(preferredLiveMode) {
93437
- const initial = await this.resolveInitialRemoteState();
93695
+ const initial = await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState());
93438
93696
  let cursor = initial.cursor;
93439
93697
  let localExportRefVersion = cursor.serverLowerBoundVersion;
93440
93698
  let streamCursor = initial.streamCursor;
@@ -93534,17 +93792,52 @@ stream:${scope2.streamId}`;
93534
93792
  };
93535
93793
  }
93536
93794
  }
93537
- async appendLocalBatch(cursor, batch, persist = true) {
93538
- let producerSession = {
93539
- producerId: this.producerId,
93540
- producerEpoch: this.producerEpoch,
93541
- nextProducerSeq: this.nextProducerSeq
93795
+ async appendLocalBatch(cursor, batch, pendingCount) {
93796
+ const source = pendingCount == null ? "direct" : "pending";
93797
+ const committed = await this.appendLocalBatchDurably(cursor, batch, pendingCount, source);
93798
+ this.committedPendingCount = committed.pendingCount;
93799
+ return committed.cursor;
93800
+ }
93801
+ async appendLocalBatchDurably(cursor, batch, pendingCount, source = pendingCount == null ? "direct" : "pending") {
93802
+ const frozen = await this.getOrCreateFrozenLocalAppend(cursor, batch, pendingCount, source);
93803
+ const appended = await this.appendFrozenLocalBatchRemote(frozen);
93804
+ const saved = await this.saveRemoteCursor(appended.cursor, "local");
93805
+ this.commitProducerAck(appended.producerAck);
93806
+ return {
93807
+ cursor: saved,
93808
+ pendingCount: frozen.pendingCount
93809
+ };
93810
+ }
93811
+ async appendLocalBatchRemote(cursor, batch, pendingCount, source = pendingCount == null ? "direct" : "pending") {
93812
+ const frozen = await this.getOrCreateFrozenLocalAppend(cursor, batch, pendingCount, source);
93813
+ return {
93814
+ ...await this.appendFrozenLocalBatchRemote(frozen),
93815
+ pendingCount: frozen.pendingCount
93542
93816
  };
93817
+ }
93818
+ async getOrCreateFrozenLocalAppend(cursor, batch, pendingCount, source = pendingCount == null ? "direct" : "pending") {
93819
+ const existing = this.frozenLocalAppend;
93820
+ if (existing != null) {
93821
+ if (existing.source !== source) throw new Error(`${existing.source} local append must be finalized before ${source} append`);
93822
+ return existing;
93823
+ }
93824
+ const frozen = {
93825
+ source,
93826
+ baseCursor: cursor,
93827
+ batch,
93828
+ pendingCount,
93829
+ wireUpdates: this.encodeUpdateItems == null ? batch.updates : await this.encodeUpdateItems(batch.updates),
93830
+ producerSession: this.createCurrentProducerSession()
93831
+ };
93832
+ this.frozenLocalAppend = frozen;
93833
+ return frozen;
93834
+ }
93835
+ async appendFrozenLocalBatchRemote(frozen) {
93836
+ if (frozen.acknowledged != null) return frozen.acknowledged;
93543
93837
  for (let attempt = 0; attempt < 3; attempt += 1) {
93544
93838
  let result;
93545
93839
  try {
93546
- const wireUpdates = this.encodeUpdateItems == null ? batch.updates : await this.encodeUpdateItems(batch.updates);
93547
- result = await this.client.appendUpdates(wireUpdates, producerSession);
93840
+ result = await this.client.appendUpdates(frozen.wireUpdates, frozen.producerSession);
93548
93841
  } catch (error2) {
93549
93842
  if (isStreamNotFoundError(error2) && this.createStreamIfMissing) {
93550
93843
  await this.ensureStreamExists();
@@ -93553,30 +93846,66 @@ stream:${scope2.streamId}`;
93553
93846
  throw error2;
93554
93847
  }
93555
93848
  if (result.kind === "ok") {
93556
- this.producerEpoch = result.ackEpoch;
93557
- this.nextProducerSeq = result.ackSeq + 1;
93558
- const next = {
93559
- ...cursor,
93560
- nextOffset: result.nextOffset,
93561
- serverLowerBoundVersion: this.adapter.mergeVersions(cursor.serverLowerBoundVersion, batch.version)
93849
+ const appended = {
93850
+ cursor: {
93851
+ ...frozen.baseCursor,
93852
+ nextOffset: result.nextOffset,
93853
+ serverLowerBoundVersion: this.adapter.mergeVersions(frozen.baseCursor.serverLowerBoundVersion, frozen.batch.version)
93854
+ },
93855
+ producerAck: {
93856
+ epoch: result.ackEpoch,
93857
+ seq: result.ackSeq
93858
+ }
93562
93859
  };
93563
- return persist ? await this.saveRemoteCursor(next, "local") : next;
93860
+ frozen.acknowledged = appended;
93861
+ return appended;
93564
93862
  }
93565
93863
  if (result.kind === "seq-gap") {
93566
- producerSession = {
93567
- ...producerSession,
93864
+ frozen.producerSession = {
93865
+ ...frozen.producerSession,
93568
93866
  nextProducerSeq: result.expectedSeq
93569
93867
  };
93570
93868
  continue;
93571
93869
  }
93572
- producerSession = {
93573
- ...producerSession,
93870
+ frozen.producerSession = {
93871
+ ...frozen.producerSession,
93574
93872
  producerEpoch: result.serverEpoch + 1,
93575
93873
  nextProducerSeq: 0
93576
93874
  };
93577
93875
  }
93578
93876
  throw new Error("append retry budget exhausted");
93579
93877
  }
93878
+ async finalizeDirectLocalAppendIfNeeded(state2) {
93879
+ const frozen = this.frozenLocalAppend;
93880
+ if (frozen == null || frozen.pendingCount != null) return state2;
93881
+ const appended = await this.appendFrozenLocalBatchRemote(frozen);
93882
+ const saved = await this.saveRemoteCursor(appended.cursor, "local");
93883
+ this.commitProducerAck(appended.producerAck);
93884
+ return await this.catchup(saved, void 0, state2.streamCursor);
93885
+ }
93886
+ createCurrentProducerSession() {
93887
+ return {
93888
+ producerId: this.producerId,
93889
+ producerEpoch: this.producerEpoch,
93890
+ nextProducerSeq: this.nextProducerSeq
93891
+ };
93892
+ }
93893
+ commitProducerAck(ack) {
93894
+ this.producerEpoch = ack.epoch;
93895
+ this.nextProducerSeq = ack.seq + 1;
93896
+ this.frozenLocalAppend = void 0;
93897
+ }
93898
+ abandonFrozenLocalAppend(frozen) {
93899
+ if (this.frozenLocalAppend !== frozen) return;
93900
+ this.frozenLocalAppend = void 0;
93901
+ this.producerEpoch = Math.max(this.producerEpoch, frozen.producerSession.producerEpoch + 1);
93902
+ this.nextProducerSeq = 0;
93903
+ }
93904
+ consumeCommittedPendingCount() {
93905
+ const count2 = this.committedPendingCount;
93906
+ this.committedPendingCount = void 0;
93907
+ return count2;
93908
+ }
93580
93909
  async ensureStreamExists() {
93581
93910
  try {
93582
93911
  await this.client.createStreamRequest();
@@ -93621,9 +93950,17 @@ stream:${scope2.streamId}`;
93621
93950
  const inferredRemote = await this.bootstrapState(bootstrapCursor, {
93622
93951
  cursorManager
93623
93952
  });
93624
- const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
93625
- if (localBatch.batch != null) await this.appendLocalBatch(inferredRemote.cursor, localBatch.batch, false);
93626
- return await this.bootstrapState(bootstrapCursor);
93953
+ let pendingAppend;
93954
+ if (this.frozenLocalAppend != null) {
93955
+ if (this.frozenLocalAppend.pendingCount != null) throw new Error("pending local append must be finalized by pending flush");
93956
+ pendingAppend = await this.appendFrozenLocalBatchRemote(this.frozenLocalAppend);
93957
+ } else {
93958
+ const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
93959
+ if (localBatch.batch != null) pendingAppend = await this.appendLocalBatchRemote(inferredRemote.cursor, localBatch.batch);
93960
+ }
93961
+ const result = await this.bootstrapState(bootstrapCursor);
93962
+ if (pendingAppend != null) this.commitProducerAck(pendingAppend.producerAck);
93963
+ return result;
93627
93964
  }
93628
93965
  async recoverFromGoneWithLocalBootstrap(cursor) {
93629
93966
  const isolatedCursorManager = await this.createIsolatedCursorManager();
@@ -93641,9 +93978,17 @@ stream:${scope2.streamId}`;
93641
93978
  });
93642
93979
  await this.adapter.applySnapshot(preservedLocalSnapshot);
93643
93980
  restoredLocalSnapshot = true;
93644
- const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
93645
- if (localBatch.batch != null) await this.appendLocalBatch(inferredRemote.cursor, localBatch.batch, false);
93646
- return await this.bootstrapState(bootstrapCursor);
93981
+ let pendingAppend;
93982
+ if (this.frozenLocalAppend != null) {
93983
+ if (this.frozenLocalAppend.pendingCount != null) throw new Error("pending local append must be finalized by pending flush");
93984
+ pendingAppend = await this.appendFrozenLocalBatchRemote(this.frozenLocalAppend);
93985
+ } else {
93986
+ const localBatch = this.exportUpdates(inferredRemote.cursor.serverLowerBoundVersion);
93987
+ if (localBatch.batch != null) pendingAppend = await this.appendLocalBatchRemote(inferredRemote.cursor, localBatch.batch);
93988
+ }
93989
+ const result = await this.bootstrapState(bootstrapCursor);
93990
+ if (pendingAppend != null) this.commitProducerAck(pendingAppend.producerAck);
93991
+ return result;
93647
93992
  } catch (error2) {
93648
93993
  if (!restoredLocalSnapshot) await this.adapter.applySnapshot(preservedLocalSnapshot);
93649
93994
  throw error2;
@@ -93855,7 +94200,6 @@ stream:${scope2.streamId}`;
93855
94200
  let mergedVersion = first2.version;
93856
94201
  const allUpdates = Array.from(first2.updates);
93857
94202
  let count2 = 1;
93858
- await this.measureAppendBodyByteLength(allUpdates);
93859
94203
  while (count2 < pendingLocal.length) {
93860
94204
  const next = pendingLocal.at(count2);
93861
94205
  const candidateUpdates = [
@@ -93894,9 +94238,10 @@ stream:${scope2.streamId}`;
93894
94238
  if (drained == null) return;
93895
94239
  let attempts = 0;
93896
94240
  while (!state2.closed) try {
93897
- state2.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state2.cursor, drained.batch));
93898
- for (let i2 = 0; i2 < drained.count; i2 += 1) state2.pendingLocal.popFront();
93899
- this.notifySyncWaiters(state2, drained.count);
94241
+ state2.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state2.cursor, drained.batch, drained.count));
94242
+ const committedCount = this.consumeCommittedPendingCount() ?? drained.count;
94243
+ for (let i2 = 0; i2 < committedCount; i2 += 1) state2.pendingLocal.popFront();
94244
+ this.notifySyncWaiters(state2, committedCount);
93900
94245
  this.setWriteSubStatus(state2, "ok");
93901
94246
  this.snapshotManager.schedule(state2);
93902
94247
  break;
@@ -93986,6 +94331,8 @@ stream:${scope2.streamId}`;
93986
94331
  return updates.reduce((sum, update2) => sum + 4 + update2.byteLength, 0);
93987
94332
  }
93988
94333
  disableWriteOnlyMode(reason) {
94334
+ const frozen = this.frozenLocalAppend;
94335
+ if (frozen?.source === "write-only") this.abandonFrozenLocalAppend(frozen);
93989
94336
  if (this.writeOnlyDisabledReason != null) return;
93990
94337
  this.writeOnlyDisabledReason = reason;
93991
94338
  this.pendingWriteOnly.clear();
@@ -112230,6 +112577,9 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
112230
112577
  isTransportConnected() {
112231
112578
  return this.transportStatus === "connected";
112232
112579
  }
112580
+ isRecovering() {
112581
+ return !this.isCleanedUp && !this.isStreamsHealthy();
112582
+ }
112233
112583
  setTransportStatus(status) {
112234
112584
  this.transportStatus = status;
112235
112585
  if (status === "disconnected") {
@@ -113429,6 +113779,9 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
113429
113779
  isTransportConnected() {
113430
113780
  return this.connectionRecovery.isTransportConnected();
113431
113781
  }
113782
+ isTransportRecovering() {
113783
+ return this.connectionRecovery.isRecovering();
113784
+ }
113432
113785
  getConnectedRoomCount() {
113433
113786
  return this.sessions.size + this.codeSessions.size;
113434
113787
  }
@@ -116919,7 +117272,7 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
116919
117272
  },
116920
117273
  codex: {
116921
117274
  packageName: "acp-extension-codex",
116922
- version: "0.11.2",
117275
+ version: "0.12.0",
116923
117276
  binName: "acp-extension-codex",
116924
117277
  args: [
116925
117278
  "-c",
@@ -120210,6 +120563,221 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120210
120563
  }
120211
120564
  return resolveAgentConfigEnvForSessionResume(repo, options.agentConfigId);
120212
120565
  };
120566
+ function getMemoryPressureSnapshot() {
120567
+ const windowsStatus = getWindowsMemoryStatus();
120568
+ const systemAvailable = getSystemAvailableMemoryBytes(windowsStatus);
120569
+ const cgroupAvailable = getCgroupAvailableMemoryBytes();
120570
+ const availableMemoryBytes = cgroupAvailable !== null ? Math.min(systemAvailable, cgroupAvailable) : systemAvailable;
120571
+ const effectiveMemoryLimitBytes = getEffectiveMemoryLimitBytes();
120572
+ return {
120573
+ availableMemoryBytes,
120574
+ effectiveMemoryLimitBytes,
120575
+ ...windowsStatus ? {
120576
+ availableCommitBytes: windowsStatus.availableCommitBytes,
120577
+ commitLimitBytes: windowsStatus.commitLimitBytes,
120578
+ committedBytes: windowsStatus.committedBytes
120579
+ } : {}
120580
+ };
120581
+ }
120582
+ function getEffectiveMemoryLimitBytes() {
120583
+ const totalMem = os__default.totalmem();
120584
+ const cgroupMax = getCgroupMemoryMaxBytes();
120585
+ if (cgroupMax !== null) {
120586
+ return Math.min(totalMem, cgroupMax);
120587
+ }
120588
+ return totalMem;
120589
+ }
120590
+ function getSystemAvailableMemoryBytes(windowsStatus) {
120591
+ if (windowsStatus) {
120592
+ return Math.max(windowsStatus.availableBytes, os__default.freemem());
120593
+ }
120594
+ const darwinAvailable = getDarwinAvailableMemoryBytes();
120595
+ if (darwinAvailable !== null) {
120596
+ return darwinAvailable;
120597
+ }
120598
+ try {
120599
+ const meminfo = readFileSync("/proc/meminfo", "utf8");
120600
+ const match5 = meminfo.match(/MemAvailable:\s+(\d+)/);
120601
+ if (match5?.[1]) {
120602
+ return parseInt(match5[1], 10) * 1024;
120603
+ }
120604
+ } catch {
120605
+ }
120606
+ return os__default.freemem();
120607
+ }
120608
+ function getWindowsMemoryStatus() {
120609
+ if (process.platform !== "win32") {
120610
+ return null;
120611
+ }
120612
+ try {
120613
+ const script = `
120614
+ $mem = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfOS_Memory |
120615
+ Select-Object AvailableBytes, CommitLimit, CommittedBytes
120616
+ $mem | ConvertTo-Json -Compress
120617
+ `;
120618
+ const output = execFileSync("powershell.exe", [
120619
+ "-NoProfile",
120620
+ "-NonInteractive",
120621
+ "-ExecutionPolicy",
120622
+ "Bypass",
120623
+ "-Command",
120624
+ script
120625
+ ], {
120626
+ encoding: "utf8"
120627
+ });
120628
+ return parseWindowsMemoryStatus(output);
120629
+ } catch {
120630
+ return null;
120631
+ }
120632
+ }
120633
+ function parseWindowsMemoryStatus(rawJson) {
120634
+ try {
120635
+ const parsed = JSON.parse(rawJson);
120636
+ const availableBytes = Number(parsed.AvailableBytes);
120637
+ const commitLimitBytes = Number(parsed.CommitLimit);
120638
+ const committedBytes = Number(parsed.CommittedBytes);
120639
+ if (!Number.isFinite(availableBytes) || availableBytes < 0 || !Number.isFinite(commitLimitBytes) || commitLimitBytes <= 0 || !Number.isFinite(committedBytes) || committedBytes < 0) {
120640
+ return null;
120641
+ }
120642
+ return {
120643
+ availableBytes,
120644
+ commitLimitBytes,
120645
+ committedBytes,
120646
+ availableCommitBytes: Math.max(0, commitLimitBytes - committedBytes)
120647
+ };
120648
+ } catch {
120649
+ return null;
120650
+ }
120651
+ }
120652
+ function getDarwinAvailableMemoryBytes() {
120653
+ if (process.platform !== "darwin") {
120654
+ return null;
120655
+ }
120656
+ try {
120657
+ const vmStatOutput = execFileSync("vm_stat", {
120658
+ encoding: "utf8"
120659
+ });
120660
+ const parsed = parseDarwinAvailableMemoryBytes(vmStatOutput);
120661
+ if (parsed !== null) {
120662
+ return Math.max(parsed, os__default.freemem());
120663
+ }
120664
+ } catch {
120665
+ }
120666
+ return null;
120667
+ }
120668
+ function parseDarwinAvailableMemoryBytes(vmStatOutput) {
120669
+ const pageSizeMatch = vmStatOutput.match(/page size of\s+(\d+)\s+bytes/i);
120670
+ if (!pageSizeMatch?.[1]) {
120671
+ return null;
120672
+ }
120673
+ const pageSize = parseInt(pageSizeMatch[1], 10);
120674
+ if (!Number.isFinite(pageSize) || pageSize <= 0) {
120675
+ return null;
120676
+ }
120677
+ const counters = /* @__PURE__ */ new Map();
120678
+ for (const rawLine of vmStatOutput.split("\n")) {
120679
+ const line3 = rawLine.trim();
120680
+ const match5 = line3.match(/^"?([^":]+?)"?:\s+(\d+)\.?$/);
120681
+ if (!match5?.[1] || !match5[2]) {
120682
+ continue;
120683
+ }
120684
+ counters.set(match5[1].toLowerCase(), parseInt(match5[2], 10));
120685
+ }
120686
+ const freePages = counters.get("pages free") ?? 0;
120687
+ const speculativePages = counters.get("pages speculative") ?? 0;
120688
+ const purgeablePages = counters.get("pages purgeable") ?? 0;
120689
+ const inactivePages = counters.get("pages inactive") ?? 0;
120690
+ const fileBackedPages = counters.get("file-backed pages");
120691
+ if (freePages === 0 && speculativePages === 0 && purgeablePages === 0 && inactivePages === 0) {
120692
+ return null;
120693
+ }
120694
+ const reclaimableCachedPages = fileBackedPages !== void 0 ? Math.min(fileBackedPages, inactivePages) : inactivePages;
120695
+ const availablePages = freePages + speculativePages + purgeablePages + reclaimableCachedPages;
120696
+ return availablePages * pageSize;
120697
+ }
120698
+ function getCgroupMemoryMaxBytes() {
120699
+ try {
120700
+ const cgroupPath = readSelfCgroupPath();
120701
+ if (cgroupPath === null) return null;
120702
+ let tightest = null;
120703
+ let current2 = cgroupPath;
120704
+ for (let depth = 0; depth < 20; depth++) {
120705
+ const memMaxPath = `/sys/fs/cgroup${current2 === "/" ? "" : current2}/memory.max`;
120706
+ const raw = readFileSafe(memMaxPath);
120707
+ if (raw !== null) {
120708
+ const trimmed = raw.trim();
120709
+ if (trimmed !== "max") {
120710
+ const value = parseInt(trimmed, 10);
120711
+ if (Number.isFinite(value) && value > 0) {
120712
+ tightest = tightest === null ? value : Math.min(tightest, value);
120713
+ }
120714
+ }
120715
+ }
120716
+ if (current2 === "/" || current2 === "") break;
120717
+ const parent = current2.substring(0, current2.lastIndexOf("/")) || "/";
120718
+ if (parent === current2) break;
120719
+ current2 = parent;
120720
+ }
120721
+ return tightest;
120722
+ } catch {
120723
+ return null;
120724
+ }
120725
+ }
120726
+ function getCgroupAvailableMemoryBytes() {
120727
+ try {
120728
+ const cgroupPath = readSelfCgroupPath();
120729
+ if (cgroupPath === null) return null;
120730
+ let tightestMax = null;
120731
+ let tightestPath = null;
120732
+ let current2 = cgroupPath;
120733
+ for (let depth = 0; depth < 20; depth++) {
120734
+ const prefix = `/sys/fs/cgroup${current2 === "/" ? "" : current2}`;
120735
+ const raw = readFileSafe(`${prefix}/memory.max`);
120736
+ if (raw !== null) {
120737
+ const trimmed = raw.trim();
120738
+ if (trimmed !== "max") {
120739
+ const value = parseInt(trimmed, 10);
120740
+ if (Number.isFinite(value) && value > 0) {
120741
+ if (tightestMax === null || value < tightestMax) {
120742
+ tightestMax = value;
120743
+ tightestPath = prefix;
120744
+ }
120745
+ }
120746
+ }
120747
+ }
120748
+ if (current2 === "/" || current2 === "") break;
120749
+ const parent = current2.substring(0, current2.lastIndexOf("/")) || "/";
120750
+ if (parent === current2) break;
120751
+ current2 = parent;
120752
+ }
120753
+ if (tightestMax === null || tightestPath === null) return null;
120754
+ const currentRaw = readFileSafe(`${tightestPath}/memory.current`);
120755
+ if (currentRaw === null) return null;
120756
+ const currentUsage = parseInt(currentRaw.trim(), 10);
120757
+ if (!Number.isFinite(currentUsage)) return null;
120758
+ return Math.max(0, tightestMax - currentUsage);
120759
+ } catch {
120760
+ return null;
120761
+ }
120762
+ }
120763
+ function readSelfCgroupPath() {
120764
+ try {
120765
+ const content = readFileSync("/proc/self/cgroup", "utf8");
120766
+ const line3 = content.split("\n").map((l) => l.trim()).find((l) => l.startsWith("0::"));
120767
+ if (!line3) return null;
120768
+ const cgroupPath = line3.slice(3).trim();
120769
+ return cgroupPath || "/";
120770
+ } catch {
120771
+ return null;
120772
+ }
120773
+ }
120774
+ function readFileSafe(filePath) {
120775
+ try {
120776
+ return readFileSync(filePath, "utf8");
120777
+ } catch {
120778
+ return null;
120779
+ }
120780
+ }
120213
120781
  class SessionTurnCancelled extends TaggedError("SessionTurnCancelled") {
120214
120782
  }
120215
120783
  class SessionTurnHalted extends TaggedError("SessionTurnHalted") {
@@ -120230,6 +120798,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120230
120798
  AUTH_REQUIRED: -32e3,
120231
120799
  RESOURCE_NOT_FOUND: -32002
120232
120800
  };
120801
+ const BYTES_PER_GIB = 1024 * 1024 * 1024;
120233
120802
  const shouldRedactEnvKey = (key2) => /token|secret|password|passwd|key/i.test(key2);
120234
120803
  const redactEnvForLog = (env2) => {
120235
120804
  if (!env2) {
@@ -120265,6 +120834,46 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120265
120834
  canceledTurnBySession = /* @__PURE__ */ new Map();
120266
120835
  currentTurnBySession = /* @__PURE__ */ new Map();
120267
120836
  turnRuntimeBySession = /* @__PURE__ */ new Map();
120837
+ formatGiB(bytes) {
120838
+ if (bytes === null || !Number.isFinite(bytes) || bytes < 0) {
120839
+ return "unavailable";
120840
+ }
120841
+ return `${(bytes / BYTES_PER_GIB).toFixed(1)}GB`;
120842
+ }
120843
+ formatPercent(value) {
120844
+ if (value === null || !Number.isFinite(value) || value < 0) {
120845
+ return "unavailable";
120846
+ }
120847
+ return `${value.toFixed(1)}%`;
120848
+ }
120849
+ async getFreeDiskBytes() {
120850
+ try {
120851
+ const stats = await statfs(os__default.homedir());
120852
+ const bsize = typeof stats.bsize === "bigint" ? Number(stats.bsize) : stats.bsize;
120853
+ const bavail = typeof stats.bavail === "bigint" ? Number(stats.bavail) : stats.bavail;
120854
+ if (!Number.isFinite(bsize) || !Number.isFinite(bavail) || bsize <= 0 || bavail < 0) {
120855
+ return null;
120856
+ }
120857
+ return bsize * bavail;
120858
+ } catch {
120859
+ return null;
120860
+ }
120861
+ }
120862
+ async logTurnStartResources(sessionId, mode2) {
120863
+ try {
120864
+ const memorySnapshot = getMemoryPressureSnapshot();
120865
+ const availableMemoryBytes = memorySnapshot.availableMemoryBytes;
120866
+ const memoryFreePercent = memorySnapshot.effectiveMemoryLimitBytes > 0 ? availableMemoryBytes / memorySnapshot.effectiveMemoryLimitBytes * 100 : null;
120867
+ const [resources, freeDiskBytes] = await Promise.all([
120868
+ this.deps.collectMachineResources().catch(() => null),
120869
+ this.getFreeDiskBytes()
120870
+ ]);
120871
+ const cpuUsagePercent = resources?.cpuUsagePercent ?? null;
120872
+ 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)}`);
120873
+ } catch (error2) {
120874
+ this.deps.logger.debug(`[${sessionId}] Failed to log turn start resources: ${formatErrorMessage(error2)}`);
120875
+ }
120876
+ }
120268
120877
  createTurnRuntime(sessionId, turnId, userTurnId, session) {
120269
120878
  return {
120270
120879
  sessionId,
@@ -120447,10 +121056,19 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120447
121056
  async recordKnownChatFailure(options) {
120448
121057
  await this.deps.recordChatFailure(options.sessionDoc, options.reason, options.message);
120449
121058
  if (options.userTurnId) {
120450
- await this.setDispatchError(options.sessionId, options.sessionDoc, options.userTurnId, options.code ?? options.reason, options.message);
121059
+ await this.markTurnFailed(options.sessionId, options.sessionDoc, options.userTurnId);
120451
121060
  }
120452
121061
  await options.sessionDoc.setStatus(SessionStatusFactory.idle());
120453
121062
  }
121063
+ formatMemoryPressureWarningMessage(result) {
121064
+ const availableMb = Math.round(result.availableMemoryBytes / 1024 / 1024);
121065
+ const thresholdMb = Math.round(result.thresholdBytes / 1024 / 1024);
121066
+ 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).` : "";
121067
+ 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}`;
121068
+ }
121069
+ warnOnMemoryPressure(sessionId, result) {
121070
+ this.deps.logger.warn(`[${sessionId}] ${this.formatMemoryPressureWarningMessage(result)}`);
121071
+ }
120454
121072
  recordKnownChatFailureAndHaltEffect(options) {
120455
121073
  return this.tryPromise(() => this.recordKnownChatFailure(options)).pipe(flatMap$1(() => fail(new SessionTurnHalted({
120456
121074
  sessionId: options.sessionId,
@@ -120474,7 +121092,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120474
121092
  }
120475
121093
  this.deps.logger.error(options.describe(options.error), options.error);
120476
121094
  if (options.userTurnId) {
120477
- await this.setDispatchError(options.sessionId, options.sessionDoc, options.userTurnId, options.code, formatErrorMessage(options.error));
121095
+ await this.markTurnFailed(options.sessionId, options.sessionDoc, options.userTurnId);
120478
121096
  }
120479
121097
  await this.handleTurnError(options.sessionId, options.sessionDoc, options.error);
120480
121098
  await options.onUnhandledError?.(options.error);
@@ -120810,8 +121428,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120810
121428
  await this.setUserTurnStatus(sessionDoc, userTurnId, "processing");
120811
121429
  await this.upsertSessionMeta(sessionId, {
120812
121430
  latestUserMsgId: userTurnId,
120813
- processingUserMsgId: userTurnId,
120814
- dispatchError: void 0
121431
+ processingUserMsgId: userTurnId
120815
121432
  });
120816
121433
  }
120817
121434
  async clearDispatchProcessing(sessionId) {
@@ -120860,34 +121477,31 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120860
121477
  await this.upsertSessionMeta(sessionId, {
120861
121478
  latestUserMsgId: existingMeta?.latestUserMsgId ?? cancelledUserMsgId,
120862
121479
  lastHandledUserMsgId: cancelledUserMsgId,
120863
- processingUserMsgId: void 0,
120864
- dispatchError: void 0
121480
+ processingUserMsgId: void 0
120865
121481
  });
120866
121482
  return;
120867
121483
  }
120868
121484
  await this.clearDispatchProcessing(sessionId);
120869
121485
  }
121486
+ resolveLatestUserMsgIdForTerminalTurn(meta, terminalUserTurnId) {
121487
+ return meta?.latestUserMsgId && meta.latestUserMsgId !== terminalUserTurnId ? meta.latestUserMsgId : terminalUserTurnId;
121488
+ }
120870
121489
  async setDispatchHandled(sessionId, sessionDoc, userTurnId) {
121490
+ const existingMeta = await this.getSessionMeta(sessionId);
120871
121491
  await this.setUserTurnStatus(sessionDoc, userTurnId, "handled");
120872
121492
  await this.upsertSessionMeta(sessionId, {
120873
- latestUserMsgId: userTurnId,
121493
+ latestUserMsgId: this.resolveLatestUserMsgIdForTerminalTurn(existingMeta, userTurnId),
120874
121494
  lastHandledUserMsgId: userTurnId,
120875
- processingUserMsgId: void 0,
120876
- dispatchError: void 0
121495
+ processingUserMsgId: void 0
120877
121496
  });
120878
121497
  }
120879
- async setDispatchError(sessionId, sessionDoc, userTurnId, code2, message) {
121498
+ async markTurnFailed(sessionId, sessionDoc, userTurnId) {
121499
+ const existingMeta = await this.getSessionMeta(sessionId);
120880
121500
  await this.setUserTurnStatus(sessionDoc, userTurnId, "failed");
120881
121501
  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
- }
121502
+ latestUserMsgId: this.resolveLatestUserMsgIdForTerminalTurn(existingMeta, userTurnId),
121503
+ lastHandledUserMsgId: userTurnId,
121504
+ processingUserMsgId: void 0
120891
121505
  });
120892
121506
  }
120893
121507
  resolveGitHubProjectBranch(meta, preferredBranch) {
@@ -120981,15 +121595,18 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120981
121595
  }
120982
121596
  async continueSession(message) {
120983
121597
  const { sessionId, acpSessionConfig, userId, userName, userEmail, userTurnId } = message;
120984
- await this.deps.evictForMemoryPressure(sessionId);
121598
+ const memoryPressureResult = await this.deps.evictForMemoryPressure(sessionId);
121599
+ if (memoryPressureResult.stillUnderPressure) {
121600
+ this.warnOnMemoryPressure(sessionId, memoryPressureResult);
121601
+ }
121602
+ await this.logTurnStartResources(sessionId, "continue");
121603
+ const sessionDoc = await this.deps.workspaceDocument.getOrCreateSessionDoc(sessionId);
120985
121604
  this.deps.touchSession(sessionId);
120986
121605
  this.deps.logger.info(`Session chat received: ${sessionId}`);
120987
121606
  this.deps.logger.debug(`[${sessionId}] Received chat request (userTurnId=${userTurnId})`);
120988
121607
  await this.upsertSessionMeta(sessionId, {
120989
- latestUserMsgId: userTurnId,
120990
- dispatchError: void 0
121608
+ latestUserMsgId: userTurnId
120991
121609
  });
120992
- const sessionDoc = await this.deps.workspaceDocument.getOrCreateSessionDoc(sessionId);
120993
121610
  const incomingProjectBranch = message.project?.branch?.trim();
120994
121611
  if (incomingProjectBranch) {
120995
121612
  await sessionDoc.setBaseBranch(incomingProjectBranch);
@@ -121326,7 +121943,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121326
121943
  }));
121327
121944
  }
121328
121945
  async startSession(message) {
121329
- await this.deps.evictForMemoryPressure(message.sessionId);
121946
+ const memoryPressureResult = await this.deps.evictForMemoryPressure(message.sessionId);
121330
121947
  const { sessionId, acpSessionConfig, workspaceId, env: env2 } = message;
121331
121948
  const userTurnId = typeof message.userTurnId === "string" && message.userTurnId.trim() ? message.userTurnId.trim() : void 0;
121332
121949
  const project = message.project;
@@ -121339,6 +121956,10 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121339
121956
  const promptText = agentConfig.prompt ?? "";
121340
121957
  const promptBytes = Buffer.byteLength(promptText, "utf8");
121341
121958
  const promptPreview = promptText.length > 200 ? `${promptText.slice(0, 200)}\u2026` : promptText;
121959
+ if (memoryPressureResult.stillUnderPressure) {
121960
+ this.warnOnMemoryPressure(sessionId, memoryPressureResult);
121961
+ }
121962
+ await this.logTurnStartResources(sessionId, "start");
121342
121963
  const sessionDoc = await this.deps.workspaceDocument.getOrCreateSessionDoc(sessionId);
121343
121964
  const existingMeta = await sessionDoc.getMetaState();
121344
121965
  const fromFeedbackPostId = message.meta?.fromFeedbackPostId?.trim() || existingMeta?.fromFeedbackPostId?.trim() || void 0;
@@ -121362,12 +121983,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121362
121983
  fromFeedbackPostId
121363
121984
  };
121364
121985
  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
121986
  const sessionConfig = {
121372
121987
  sessionId,
121373
121988
  workspaceId,
@@ -121388,6 +122003,11 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121388
122003
  if (project) {
121389
122004
  await sessionDoc.setProject(project);
121390
122005
  }
122006
+ if (userTurnId) {
122007
+ await this.upsertSessionMeta(sessionId, {
122008
+ latestUserMsgId: userTurnId
122009
+ });
122010
+ }
121391
122011
  if (branch) {
121392
122012
  await sessionDoc.setBaseBranch(branch);
121393
122013
  }
@@ -122051,9 +122671,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122051
122671
  if (meta.lastCanceledTurn && meta.lastCanceledTurn !== this.cancelSeenTurn.get(meta.id)) {
122052
122672
  return true;
122053
122673
  }
122054
- if (meta.dispatchError?.code === SessionDispatchWatcher.DISPATCH_HISTORY_SYNC_TIMEOUT_CODE) {
122055
- return false;
122056
- }
122057
122674
  if (!meta.lastHandledUserMsgId) {
122058
122675
  return true;
122059
122676
  }
@@ -122149,12 +122766,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122149
122766
  await this.deps.workspaceDocument.repo.upsertDocMeta?.(getSessionRoomId(sessionId), {
122150
122767
  latestUserMsgId: userTurnId,
122151
122768
  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
- }
122769
+ processingUserMsgId: void 0
122158
122770
  });
122159
122771
  await sessionDoc.setStatus(SessionStatusFactory.idle());
122160
122772
  }
@@ -122284,7 +122896,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122284
122896
  static HISTORY_SYNC_WAIT_TIMEOUT_MS = 5 * 6e4;
122285
122897
  static HISTORY_RECONNECT_JITTER_MIN_MS = 500;
122286
122898
  static HISTORY_RECONNECT_JITTER_MAX_MS = 1500;
122287
- static DISPATCH_HISTORY_SYNC_TIMEOUT_CODE = "dispatch_recovery_unhealthy";
122288
122899
  static setUnrefTimeout(callback, delayMs) {
122289
122900
  const timer2 = setTimeout(callback, delayMs);
122290
122901
  if (typeof timer2 === "object" && "unref" in timer2) {
@@ -122445,11 +123056,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122445
123056
  const pendingUserMsgId = meta.processingUserMsgId ?? meta.latestUserMsgId ?? meta.lastHandledUserMsgId;
122446
123057
  const recoveryPatch = {
122447
123058
  status: SessionStatusFactory.idle(),
122448
- dispatchError: {
122449
- code: SessionDispatchWatcher.DISPATCH_HISTORY_SYNC_TIMEOUT_CODE,
122450
- message: "Dispatch recovery could not reconnect to this session after 5 minutes. Send a new message to retry.",
122451
- at: getServerNow()
122452
- }
123059
+ processingUserMsgId: void 0
122453
123060
  };
122454
123061
  if (pendingUserMsgId) {
122455
123062
  recoveryPatch.lastHandledUserMsgId = pendingUserMsgId;
@@ -123202,8 +123809,14 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
123202
123809
  permissionRequestStartTimes = /* @__PURE__ */ new Map();
123203
123810
  machineHeartbeatTimer = null;
123204
123811
  static MACHINE_HEARTBEAT_INTERVAL_MS = 2e4;
123205
- evictForMemoryPressureFn = async () => {
123206
- };
123812
+ evictForMemoryPressureFn = async () => ({
123813
+ availableMemoryBytes: 0,
123814
+ thresholdBytes: 0,
123815
+ hadMemoryPressure: false,
123816
+ stillUnderPressure: false,
123817
+ evictedSessionIds: [],
123818
+ pressureReason: null
123819
+ });
123207
123820
  executionService;
123208
123821
  sessionDispatchWatcher;
123209
123822
  autoPromptRunner;
@@ -125215,6 +125828,16 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125215
125828
  hasPendingUpdates(sessionId) {
125216
125829
  return this.store.has(sessionId) && this.store.get(sessionId).acpUpdateBuffer.length > 0;
125217
125830
  }
125831
+ async hasPendingUserWork(sessionId) {
125832
+ const meta = (await this.workspaceDocument.repo.getDocMeta(getSessionRoomId(sessionId)))?.meta;
125833
+ if (!meta) {
125834
+ return false;
125835
+ }
125836
+ if (meta.processingUserMsgId) {
125837
+ return true;
125838
+ }
125839
+ return Boolean(meta.latestUserMsgId && meta.latestUserMsgId !== meta.lastHandledUserMsgId);
125840
+ }
125218
125841
  isArchiveInFlight(sessionId) {
125219
125842
  return this.archiveInFlight.has(sessionId);
125220
125843
  }
@@ -125571,116 +126194,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125571
126194
  }
125572
126195
  }
125573
126196
  }
125574
- function getAvailableMemoryBytes() {
125575
- const systemAvailable = getSystemAvailableMemoryBytes();
125576
- const cgroupAvailable = getCgroupAvailableMemoryBytes();
125577
- if (cgroupAvailable !== null) {
125578
- return Math.min(systemAvailable, cgroupAvailable);
125579
- }
125580
- return systemAvailable;
125581
- }
125582
- function getEffectiveMemoryLimitBytes() {
125583
- const totalMem = os__default.totalmem();
125584
- const cgroupMax = getCgroupMemoryMaxBytes();
125585
- if (cgroupMax !== null) {
125586
- return Math.min(totalMem, cgroupMax);
125587
- }
125588
- return totalMem;
125589
- }
125590
- function getSystemAvailableMemoryBytes() {
125591
- try {
125592
- const meminfo = readFileSync("/proc/meminfo", "utf8");
125593
- const match5 = meminfo.match(/MemAvailable:\s+(\d+)/);
125594
- if (match5?.[1]) {
125595
- return parseInt(match5[1], 10) * 1024;
125596
- }
125597
- } catch {
125598
- }
125599
- return os__default.freemem();
125600
- }
125601
- function getCgroupMemoryMaxBytes() {
125602
- try {
125603
- const cgroupPath = readSelfCgroupPath();
125604
- if (cgroupPath === null) return null;
125605
- let tightest = null;
125606
- let current2 = cgroupPath;
125607
- for (let depth = 0; depth < 20; depth++) {
125608
- const memMaxPath = `/sys/fs/cgroup${current2 === "/" ? "" : current2}/memory.max`;
125609
- const raw = readFileSafe(memMaxPath);
125610
- if (raw !== null) {
125611
- const trimmed = raw.trim();
125612
- if (trimmed !== "max") {
125613
- const value = parseInt(trimmed, 10);
125614
- if (Number.isFinite(value) && value > 0) {
125615
- tightest = tightest === null ? value : Math.min(tightest, value);
125616
- }
125617
- }
125618
- }
125619
- if (current2 === "/" || current2 === "") break;
125620
- const parent = current2.substring(0, current2.lastIndexOf("/")) || "/";
125621
- if (parent === current2) break;
125622
- current2 = parent;
125623
- }
125624
- return tightest;
125625
- } catch {
125626
- return null;
125627
- }
125628
- }
125629
- function getCgroupAvailableMemoryBytes() {
125630
- try {
125631
- const cgroupPath = readSelfCgroupPath();
125632
- if (cgroupPath === null) return null;
125633
- let tightestMax = null;
125634
- let tightestPath = null;
125635
- let current2 = cgroupPath;
125636
- for (let depth = 0; depth < 20; depth++) {
125637
- const prefix = `/sys/fs/cgroup${current2 === "/" ? "" : current2}`;
125638
- const raw = readFileSafe(`${prefix}/memory.max`);
125639
- if (raw !== null) {
125640
- const trimmed = raw.trim();
125641
- if (trimmed !== "max") {
125642
- const value = parseInt(trimmed, 10);
125643
- if (Number.isFinite(value) && value > 0) {
125644
- if (tightestMax === null || value < tightestMax) {
125645
- tightestMax = value;
125646
- tightestPath = prefix;
125647
- }
125648
- }
125649
- }
125650
- }
125651
- if (current2 === "/" || current2 === "") break;
125652
- const parent = current2.substring(0, current2.lastIndexOf("/")) || "/";
125653
- if (parent === current2) break;
125654
- current2 = parent;
125655
- }
125656
- if (tightestMax === null || tightestPath === null) return null;
125657
- const currentRaw = readFileSafe(`${tightestPath}/memory.current`);
125658
- if (currentRaw === null) return null;
125659
- const currentUsage = parseInt(currentRaw.trim(), 10);
125660
- if (!Number.isFinite(currentUsage)) return null;
125661
- return Math.max(0, tightestMax - currentUsage);
125662
- } catch {
125663
- return null;
125664
- }
125665
- }
125666
- function readSelfCgroupPath() {
125667
- try {
125668
- const content = readFileSync("/proc/self/cgroup", "utf8");
125669
- const line3 = content.split("\n").map((l) => l.trim()).find((l) => l.startsWith("0::"));
125670
- if (!line3) return null;
125671
- const cgroupPath = line3.slice(3).trim();
125672
- return cgroupPath || "/";
125673
- } catch {
125674
- return null;
125675
- }
125676
- }
125677
- function readFileSafe(filePath) {
125678
- try {
125679
- return readFileSync(filePath, "utf8");
125680
- } catch {
125681
- return null;
125682
- }
125683
- }
125684
126197
  function readEnvNumber(key2, fallback2) {
125685
126198
  const raw = process.env[key2];
125686
126199
  if (!raw) return fallback2;
@@ -125688,6 +126201,8 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125688
126201
  return Number.isFinite(parsed) ? parsed : fallback2;
125689
126202
  }
125690
126203
  const GIB = 1024 * 1024 * 1024;
126204
+ const WINDOWS_COMMIT_THRESHOLD_FLOOR_BYTES = 512 * 1024 * 1024;
126205
+ const WINDOWS_COMMIT_THRESHOLD_CEILING_BYTES = 2 * GIB;
125691
126206
  function defaultMemoryThresholdBytes() {
125692
126207
  const tenPercent = Math.floor(getEffectiveMemoryLimitBytes() * 0.1);
125693
126208
  return Math.max(GIB, Math.min(4 * GIB, tenPercent));
@@ -125700,6 +126215,9 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125700
126215
  memoryThresholdBytes: readEnvNumber("LODY_SESSION_GC_MEMORY_THRESHOLD_BYTES", defaultMemoryThresholdBytes())
125701
126216
  };
125702
126217
  }
126218
+ function getWindowsCommitThresholdBytes(memoryThresholdBytes) {
126219
+ return Math.max(WINDOWS_COMMIT_THRESHOLD_FLOOR_BYTES, Math.min(WINDOWS_COMMIT_THRESHOLD_CEILING_BYTES, memoryThresholdBytes));
126220
+ }
125703
126221
  class SessionGCManager {
125704
126222
  constructor(config2, deps) {
125705
126223
  this.config = config2;
@@ -125723,7 +126241,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125723
126241
  }
125724
126242
  async sweep() {
125725
126243
  const sweepStart = Date.now();
125726
- const candidates = this.getIdleCandidates();
126244
+ const candidates = await this.getIdleCandidates();
125727
126245
  if (candidates.length === 0) {
125728
126246
  return;
125729
126247
  }
@@ -125731,7 +126249,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125731
126249
  let cleaned = 0;
125732
126250
  let skipped = 0;
125733
126251
  for (const { sessionId } of candidates) {
125734
- if (!this.isStillEligibleForGC(sessionId)) {
126252
+ if (!await this.isStillEligibleForGC(sessionId)) {
125735
126253
  skipped++;
125736
126254
  continue;
125737
126255
  }
@@ -125749,19 +126267,50 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125749
126267
  this.deps.logger.debug(`[GC] Sweep completed: cleaned ${cleaned}/${candidates.length} sessions in ${sweepDuration}ms`);
125750
126268
  }
125751
126269
  async evictForMemoryPressure(excludeSessionId) {
126270
+ const thresholdBytes = this.config.memoryThresholdBytes;
126271
+ const commitThresholdBytes = getWindowsCommitThresholdBytes(thresholdBytes);
126272
+ let memorySnapshot = getMemoryPressureSnapshot();
126273
+ let availableMemory = memorySnapshot.availableMemoryBytes;
126274
+ let pressureReason = this.getPressureReason(memorySnapshot, thresholdBytes, commitThresholdBytes);
125752
126275
  if (!this.config.enabled) {
125753
- return;
126276
+ return {
126277
+ availableMemoryBytes: availableMemory,
126278
+ thresholdBytes,
126279
+ hadMemoryPressure: false,
126280
+ stillUnderPressure: false,
126281
+ evictedSessionIds: [],
126282
+ pressureReason: null,
126283
+ ...memorySnapshot.availableCommitBytes !== void 0 ? {
126284
+ availableCommitBytes: memorySnapshot.availableCommitBytes,
126285
+ commitThresholdBytes,
126286
+ commitLimitBytes: memorySnapshot.commitLimitBytes,
126287
+ committedBytes: memorySnapshot.committedBytes
126288
+ } : {}
126289
+ };
125754
126290
  }
125755
- let availableMemory = getAvailableMemoryBytes();
125756
- if (availableMemory >= this.config.memoryThresholdBytes) {
125757
- return;
126291
+ if (pressureReason === null) {
126292
+ return {
126293
+ availableMemoryBytes: availableMemory,
126294
+ thresholdBytes,
126295
+ hadMemoryPressure: false,
126296
+ stillUnderPressure: false,
126297
+ evictedSessionIds: [],
126298
+ pressureReason: null,
126299
+ ...memorySnapshot.availableCommitBytes !== void 0 ? {
126300
+ availableCommitBytes: memorySnapshot.availableCommitBytes,
126301
+ commitThresholdBytes,
126302
+ commitLimitBytes: memorySnapshot.commitLimitBytes,
126303
+ committedBytes: memorySnapshot.committedBytes
126304
+ } : {}
126305
+ };
125758
126306
  }
125759
- this.deps.logger.debug(`[GC] Memory pressure detected: ${Math.round(availableMemory / 1024 / 1024)}MB available (threshold: ${Math.round(this.config.memoryThresholdBytes / 1024 / 1024)}MB)`);
126307
+ const commitText = memorySnapshot.availableCommitBytes !== void 0 ? `, commit headroom ${Math.round(memorySnapshot.availableCommitBytes / 1024 / 1024)}MB (threshold: ${Math.round(commitThresholdBytes / 1024 / 1024)}MB)` : "";
126308
+ this.deps.logger.debug(`[GC] Memory pressure detected: ${Math.round(availableMemory / 1024 / 1024)}MB available (threshold: ${Math.round(thresholdBytes / 1024 / 1024)}MB)${commitText}`);
125760
126309
  const sessions = this.getSessionsWithIdleTime();
125761
126310
  sessions.sort((a, b) => b.idleMs - a.idleMs);
125762
- let evicted = 0;
126311
+ const evictedSessionIds = [];
125763
126312
  for (const { sessionId, idleMs } of sessions) {
125764
- if (availableMemory >= this.config.memoryThresholdBytes) {
126313
+ if (pressureReason === null) {
125765
126314
  break;
125766
126315
  }
125767
126316
  if (excludeSessionId && sessionId === excludeSessionId) {
@@ -125770,30 +126319,63 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125770
126319
  if (idleMs === 0) {
125771
126320
  continue;
125772
126321
  }
125773
- if (!this.isEligibleForCleanup(sessionId)) {
126322
+ if (!await this.isEligibleForCleanup(sessionId)) {
125774
126323
  continue;
125775
126324
  }
125776
126325
  try {
125777
126326
  this.deps.logger.debug(`[GC] Evicting session ${sessionId} (idle ${Math.round(idleMs / 1e3)}s) due to memory pressure`);
125778
126327
  await this.deps.cleanSession(sessionId);
125779
- evicted++;
125780
- availableMemory = getAvailableMemoryBytes();
126328
+ evictedSessionIds.push(sessionId);
126329
+ memorySnapshot = getMemoryPressureSnapshot();
126330
+ availableMemory = memorySnapshot.availableMemoryBytes;
126331
+ pressureReason = this.getPressureReason(memorySnapshot, thresholdBytes, commitThresholdBytes);
125781
126332
  } catch (error2) {
125782
126333
  this.deps.logger.error(`[GC] Failed to evict session ${sessionId}: ${formatErrorMessage(error2)}`);
125783
126334
  }
125784
126335
  }
125785
- if (evicted > 0) {
125786
- this.deps.logger.debug(`[GC] Memory pressure eviction complete: evicted ${evicted} sessions, available memory now ${Math.round(availableMemory / 1024 / 1024)}MB`);
126336
+ const stillUnderPressure = pressureReason !== null;
126337
+ if (evictedSessionIds.length > 0) {
126338
+ this.deps.logger.debug(`[GC] Memory pressure eviction complete: evicted ${evictedSessionIds.length} sessions, available memory now ${Math.round(availableMemory / 1024 / 1024)}MB`);
126339
+ } else if (stillUnderPressure) {
126340
+ this.deps.logger.debug("[GC] Memory pressure persists but no idle sessions were eligible for eviction");
126341
+ }
126342
+ return {
126343
+ availableMemoryBytes: availableMemory,
126344
+ thresholdBytes,
126345
+ hadMemoryPressure: true,
126346
+ stillUnderPressure,
126347
+ evictedSessionIds,
126348
+ pressureReason,
126349
+ ...memorySnapshot.availableCommitBytes !== void 0 ? {
126350
+ availableCommitBytes: memorySnapshot.availableCommitBytes,
126351
+ commitThresholdBytes,
126352
+ commitLimitBytes: memorySnapshot.commitLimitBytes,
126353
+ committedBytes: memorySnapshot.committedBytes
126354
+ } : {}
126355
+ };
126356
+ }
126357
+ getPressureReason(snapshot, thresholdBytes, commitThresholdBytes) {
126358
+ const physicalPressure = snapshot.availableMemoryBytes < thresholdBytes;
126359
+ const commitPressure = snapshot.availableCommitBytes !== void 0 && snapshot.availableCommitBytes < commitThresholdBytes;
126360
+ if (physicalPressure && commitPressure) {
126361
+ return "physical_and_commit";
126362
+ }
126363
+ if (physicalPressure) {
126364
+ return "physical";
125787
126365
  }
126366
+ if (commitPressure) {
126367
+ return "commit";
126368
+ }
126369
+ return null;
125788
126370
  }
125789
- getIdleCandidates() {
126371
+ async getIdleCandidates() {
125790
126372
  const sessions = this.getSessionsWithIdleTime();
125791
126373
  const candidates = [];
125792
126374
  for (const session of sessions) {
125793
126375
  if (session.idleMs < this.config.idleTimeoutMs) {
125794
126376
  continue;
125795
126377
  }
125796
- if (!this.isEligibleForCleanup(session.sessionId)) {
126378
+ if (!await this.isEligibleForCleanup(session.sessionId)) {
125797
126379
  continue;
125798
126380
  }
125799
126381
  candidates.push(session);
@@ -125821,20 +126403,23 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125821
126403
  }
125822
126404
  return result;
125823
126405
  }
125824
- isEligibleForCleanup(sessionId) {
126406
+ async isEligibleForCleanup(sessionId) {
125825
126407
  if (this.deps.hasActiveTurn(sessionId)) {
125826
126408
  return false;
125827
126409
  }
125828
126410
  if (this.deps.hasPendingUpdates(sessionId)) {
125829
126411
  return false;
125830
126412
  }
126413
+ if (await this.deps.hasPendingUserWork(sessionId)) {
126414
+ return false;
126415
+ }
125831
126416
  if (this.deps.isArchiveInFlight(sessionId)) {
125832
126417
  return false;
125833
126418
  }
125834
126419
  return true;
125835
126420
  }
125836
- isStillEligibleForGC(sessionId) {
125837
- if (!this.isEligibleForCleanup(sessionId)) {
126421
+ async isStillEligibleForGC(sessionId) {
126422
+ if (!await this.isEligibleForCleanup(sessionId)) {
125838
126423
  return false;
125839
126424
  }
125840
126425
  const lastActivity = this.deps.getSessionLastActivity(sessionId);
@@ -125962,6 +126547,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125962
126547
  getSessionLastActivity: (sessionId) => handler.getLastActivity(sessionId),
125963
126548
  hasActiveTurn: (sessionId) => handler.hasActiveTurn(sessionId),
125964
126549
  hasPendingUpdates: (sessionId) => handler.hasPendingUpdates(sessionId),
126550
+ hasPendingUserWork: async (sessionId) => await handler.hasPendingUserWork(sessionId),
125965
126551
  isArchiveInFlight: (sessionId) => handler.isArchiveInFlight(sessionId),
125966
126552
  cleanSession: (sessionId) => handler.cleanSessionForGC(sessionId),
125967
126553
  getSessionIds: () => handler.getTrackedSessionIds(),
@@ -125970,8 +126556,16 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125970
126556
  this.gcManager.start();
125971
126557
  handler.setEvictForMemoryPressure(async (excludeSessionId) => {
125972
126558
  if (this.gcManager) {
125973
- await this.gcManager.evictForMemoryPressure(excludeSessionId);
126559
+ return await this.gcManager.evictForMemoryPressure(excludeSessionId);
125974
126560
  }
126561
+ return {
126562
+ availableMemoryBytes: 0,
126563
+ thresholdBytes: 0,
126564
+ hadMemoryPressure: false,
126565
+ stillUnderPressure: false,
126566
+ evictedSessionIds: [],
126567
+ pressureReason: null
126568
+ };
125975
126569
  });
125976
126570
  }
125977
126571
  requireSessionManager() {
@@ -133169,6 +133763,9 @@ export PATH=${toSingleQuotedShellString(ghShimBinDir)}:"$PATH"
133169
133763
  isControlPlaneReady() {
133170
133764
  return this.documentManager.isTransportConnected();
133171
133765
  }
133766
+ isControlPlaneRecovering() {
133767
+ return this.documentManager.isTransportRecovering();
133768
+ }
133172
133769
  getActiveSessionCount() {
133173
133770
  return this.runtime.getActiveSessionCount();
133174
133771
  }
@@ -134998,23 +135595,26 @@ export PATH=${toSingleQuotedShellString(ghShimBinDir)}:"$PATH"
134998
135595
  refreshRuntimeState() {
134999
135596
  const desiredCount = this.desiredWorkspaces.size;
135000
135597
  let connectedCount = 0;
135598
+ let reconnectingCount = 0;
135001
135599
  let totalActiveSessions = 0;
135002
135600
  let totalConnectedRooms = 0;
135003
135601
  for (const runtime of this.runtimes.values()) {
135004
135602
  if (runtime.lody.isControlPlaneReady()) {
135005
135603
  connectedCount += 1;
135604
+ } else if (runtime.lody.isControlPlaneRecovering()) {
135605
+ reconnectingCount += 1;
135006
135606
  }
135007
135607
  totalActiveSessions += runtime.lody.getActiveSessionCount();
135008
135608
  totalConnectedRooms += runtime.lody.getConnectedRoomCount();
135009
135609
  }
135010
135610
  this.runtimeStateReporter.setActiveSessionCount(totalActiveSessions);
135011
135611
  this.runtimeStateReporter.setConnectedRoomCount(totalConnectedRooms);
135012
- const nextConnectivity = desiredCount === 0 ? "online" : connectedCount === 0 ? "offline" : connectedCount < desiredCount ? "reconnecting" : "online";
135612
+ const hasWorkspaceRetry = this.retryTimers.size > 0 || this.startInFlight.size > 0;
135613
+ const nextConnectivity = desiredCount === 0 ? "online" : connectedCount === desiredCount ? "online" : reconnectingCount > 0 || hasWorkspaceRetry ? "reconnecting" : "offline";
135013
135614
  if (this.lastConnectivity !== nextConnectivity) {
135014
135615
  this.lastConnectivity = nextConnectivity;
135015
135616
  this.runtimeStateReporter.setConnectivity(nextConnectivity);
135016
135617
  }
135017
- const hasWorkspaceRetry = this.retryTimers.size > 0 || this.startInFlight.size > 0;
135018
135618
  if (hasWorkspaceRetry && !this.hasWorkspaceRetryIssue) {
135019
135619
  this.hasWorkspaceRetryIssue = true;
135020
135620
  this.runtimeStateReporter.upsertIssue({
@@ -167297,10 +167897,18 @@ ${entry2.text}`).join("\n\n");
167297
167897
  }
167298
167898
  const DEFAULT_MIN_RETRY_MS$1 = 1e3;
167299
167899
  const DEFAULT_MAX_RETRY_MS$1 = 3e4;
167300
- function buildRetryDelay(attempt, minMs = DEFAULT_MIN_RETRY_MS$1, maxMs = DEFAULT_MAX_RETRY_MS$1) {
167900
+ const DEFAULT_JITTER_FRACTION = 0.2;
167901
+ function buildRetryDelay(attempt, minMs = DEFAULT_MIN_RETRY_MS$1, maxMs = DEFAULT_MAX_RETRY_MS$1, options = {}) {
167301
167902
  const exponent = Math.max(0, attempt);
167302
- const delay2 = minMs * 2 ** exponent;
167303
- return Math.min(delay2, maxMs);
167903
+ const baseDelay = Math.min(minMs * 2 ** exponent, maxMs);
167904
+ const jitterFraction = Math.max(0, options.jitterFraction ?? DEFAULT_JITTER_FRACTION);
167905
+ if (jitterFraction === 0) {
167906
+ return baseDelay;
167907
+ }
167908
+ const random2 = options.random ?? Math.random;
167909
+ const randomValue = Math.min(1, Math.max(0, random2()));
167910
+ const jitterMultiplier = 1 + (randomValue * 2 - 1) * jitterFraction;
167911
+ return Math.min(Math.max(0, Math.round(baseDelay * jitterMultiplier)), maxMs);
167304
167912
  }
167305
167913
  class FailureWindow {
167306
167914
  historyMs = [];
@@ -167361,6 +167969,7 @@ ${result.stderr}`;
167361
167969
  probeFailureThreshold;
167362
167970
  minRetryMs;
167363
167971
  maxRetryMs;
167972
+ retryRandom;
167364
167973
  failureWindow;
167365
167974
  triggered = false;
167366
167975
  inFlight = false;
@@ -167392,6 +168001,7 @@ ${result.stderr}`;
167392
168001
  this.probeFailureThreshold = options.probeFailureThreshold ?? DEFAULT_PROBE_FAILURE_THRESHOLD;
167393
168002
  this.minRetryMs = options.minRetryMs ?? DEFAULT_MIN_RETRY_MS;
167394
168003
  this.maxRetryMs = options.maxRetryMs ?? DEFAULT_MAX_RETRY_MS;
168004
+ this.retryRandom = options.retryRandom ?? Math.random;
167395
168005
  this.failureWindow = new FailureWindow(options.fatalFailureWindowMs ?? DEFAULT_FATAL_FAILURE_WINDOW_MS, options.fatalFailureThreshold ?? DEFAULT_FATAL_FAILURE_THRESHOLD);
167396
168006
  }
167397
168007
  getState() {
@@ -167471,6 +168081,9 @@ ${result.stderr}`;
167471
168081
  this.latestRuntimeState = runtimeState;
167472
168082
  this.probeFailureCount = 0;
167473
168083
  this.probeUnavailable = false;
168084
+ this.clearRetryTimer();
168085
+ this.attempt = 0;
168086
+ this.failureWindow.reset();
167474
168087
  this.lastStateMessage = void 0;
167475
168088
  } else {
167476
168089
  this.probeFailureCount += 1;
@@ -167491,7 +168104,9 @@ ${result.stderr}`;
167491
168104
  } else if (this.fatalReason) {
167492
168105
  phase = "fatal";
167493
168106
  } else if (runtime) {
167494
- phase = runtime.phase === "fatal" ? "fatal" : runtime.phase;
168107
+ phase = runtime.phase === "fatal" ? "fatal" : runtime.connectivity === "reconnecting" ? "reconnecting" : runtime.phase;
168108
+ } else if (this.retryTimer) {
168109
+ phase = "reconnecting";
167495
168110
  } else if (childRunning) {
167496
168111
  phase = this.probeUnavailable ? "offline" : "starting";
167497
168112
  } else if (this.probeUnavailable && !this.inFlight && !this.retryTimer) {
@@ -167548,7 +168163,9 @@ ${result.stderr}`;
167548
168163
  scheduleRetry(reason, recordFailure = true) {
167549
168164
  if (this.fatalReason) return;
167550
168165
  if (recordFailure && this.recordFailure(reason)) return;
167551
- const delay2 = buildRetryDelay(this.attempt, this.minRetryMs, this.maxRetryMs);
168166
+ const delay2 = buildRetryDelay(this.attempt, this.minRetryMs, this.maxRetryMs, {
168167
+ random: this.retryRandom
168168
+ });
167552
168169
  this.attempt += 1;
167553
168170
  this.clearRetryTimer();
167554
168171
  this.pendingRetryInMs = delay2;