lody 0.45.0 → 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 +1114 -463
  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.0";
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
93542
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
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
  }
@@ -113879,6 +114232,34 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
113879
114232
  async waitForRemoteSync() {
113880
114233
  await this.remoteSyncReady;
113881
114234
  }
114235
+ async ensureDocRoomJoined() {
114236
+ if (this.destroyed) {
114237
+ return;
114238
+ }
114239
+ if (!this.docSub) {
114240
+ this.remoteSyncReady = this.startDocRoomSync();
114241
+ }
114242
+ await this.remoteSyncReady;
114243
+ }
114244
+ getDocRoomStatus() {
114245
+ return this.docSub?.status;
114246
+ }
114247
+ onDocRoomStatusChange(listener) {
114248
+ return this.docSub?.onStatusChange(listener) ?? (() => {
114249
+ });
114250
+ }
114251
+ async rejoinDocRoom() {
114252
+ if (this.destroyed) {
114253
+ return;
114254
+ }
114255
+ if (!this.docSub) {
114256
+ await this.ensureDocRoomJoined();
114257
+ return;
114258
+ }
114259
+ if (this.docSub.rejoin) {
114260
+ await this.docSub.rejoin();
114261
+ }
114262
+ }
113882
114263
  async initOffline(initialState) {
113883
114264
  this.destroyed = false;
113884
114265
  this.handle = await this.repo.openPersistedDoc(this.roomId);
@@ -116891,7 +117272,7 @@ ${this.stack.split("\n").slice(1).join("\n")}` : this.toString();
116891
117272
  },
116892
117273
  codex: {
116893
117274
  packageName: "acp-extension-codex",
116894
- version: "0.11.2",
117275
+ version: "0.12.0",
116895
117276
  binName: "acp-extension-codex",
116896
117277
  args: [
116897
117278
  "-c",
@@ -120182,6 +120563,221 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120182
120563
  }
120183
120564
  return resolveAgentConfigEnvForSessionResume(repo, options.agentConfigId);
120184
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
+ }
120185
120781
  class SessionTurnCancelled extends TaggedError("SessionTurnCancelled") {
120186
120782
  }
120187
120783
  class SessionTurnHalted extends TaggedError("SessionTurnHalted") {
@@ -120202,6 +120798,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120202
120798
  AUTH_REQUIRED: -32e3,
120203
120799
  RESOURCE_NOT_FOUND: -32002
120204
120800
  };
120801
+ const BYTES_PER_GIB = 1024 * 1024 * 1024;
120205
120802
  const shouldRedactEnvKey = (key2) => /token|secret|password|passwd|key/i.test(key2);
120206
120803
  const redactEnvForLog = (env2) => {
120207
120804
  if (!env2) {
@@ -120237,6 +120834,46 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120237
120834
  canceledTurnBySession = /* @__PURE__ */ new Map();
120238
120835
  currentTurnBySession = /* @__PURE__ */ new Map();
120239
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
+ }
120240
120877
  createTurnRuntime(sessionId, turnId, userTurnId, session) {
120241
120878
  return {
120242
120879
  sessionId,
@@ -120419,10 +121056,19 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120419
121056
  async recordKnownChatFailure(options) {
120420
121057
  await this.deps.recordChatFailure(options.sessionDoc, options.reason, options.message);
120421
121058
  if (options.userTurnId) {
120422
- 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);
120423
121060
  }
120424
121061
  await options.sessionDoc.setStatus(SessionStatusFactory.idle());
120425
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
+ }
120426
121072
  recordKnownChatFailureAndHaltEffect(options) {
120427
121073
  return this.tryPromise(() => this.recordKnownChatFailure(options)).pipe(flatMap$1(() => fail(new SessionTurnHalted({
120428
121074
  sessionId: options.sessionId,
@@ -120446,7 +121092,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120446
121092
  }
120447
121093
  this.deps.logger.error(options.describe(options.error), options.error);
120448
121094
  if (options.userTurnId) {
120449
- await this.setDispatchError(options.sessionId, options.sessionDoc, options.userTurnId, options.code, formatErrorMessage(options.error));
121095
+ await this.markTurnFailed(options.sessionId, options.sessionDoc, options.userTurnId);
120450
121096
  }
120451
121097
  await this.handleTurnError(options.sessionId, options.sessionDoc, options.error);
120452
121098
  await options.onUnhandledError?.(options.error);
@@ -120782,8 +121428,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120782
121428
  await this.setUserTurnStatus(sessionDoc, userTurnId, "processing");
120783
121429
  await this.upsertSessionMeta(sessionId, {
120784
121430
  latestUserMsgId: userTurnId,
120785
- processingUserMsgId: userTurnId,
120786
- dispatchError: void 0
121431
+ processingUserMsgId: userTurnId
120787
121432
  });
120788
121433
  }
120789
121434
  async clearDispatchProcessing(sessionId) {
@@ -120832,34 +121477,31 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120832
121477
  await this.upsertSessionMeta(sessionId, {
120833
121478
  latestUserMsgId: existingMeta?.latestUserMsgId ?? cancelledUserMsgId,
120834
121479
  lastHandledUserMsgId: cancelledUserMsgId,
120835
- processingUserMsgId: void 0,
120836
- dispatchError: void 0
121480
+ processingUserMsgId: void 0
120837
121481
  });
120838
121482
  return;
120839
121483
  }
120840
121484
  await this.clearDispatchProcessing(sessionId);
120841
121485
  }
121486
+ resolveLatestUserMsgIdForTerminalTurn(meta, terminalUserTurnId) {
121487
+ return meta?.latestUserMsgId && meta.latestUserMsgId !== terminalUserTurnId ? meta.latestUserMsgId : terminalUserTurnId;
121488
+ }
120842
121489
  async setDispatchHandled(sessionId, sessionDoc, userTurnId) {
121490
+ const existingMeta = await this.getSessionMeta(sessionId);
120843
121491
  await this.setUserTurnStatus(sessionDoc, userTurnId, "handled");
120844
121492
  await this.upsertSessionMeta(sessionId, {
120845
- latestUserMsgId: userTurnId,
121493
+ latestUserMsgId: this.resolveLatestUserMsgIdForTerminalTurn(existingMeta, userTurnId),
120846
121494
  lastHandledUserMsgId: userTurnId,
120847
- processingUserMsgId: void 0,
120848
- dispatchError: void 0
121495
+ processingUserMsgId: void 0
120849
121496
  });
120850
121497
  }
120851
- async setDispatchError(sessionId, sessionDoc, userTurnId, code2, message) {
121498
+ async markTurnFailed(sessionId, sessionDoc, userTurnId) {
121499
+ const existingMeta = await this.getSessionMeta(sessionId);
120852
121500
  await this.setUserTurnStatus(sessionDoc, userTurnId, "failed");
120853
121501
  await this.upsertSessionMeta(sessionId, {
120854
- latestUserMsgId: userTurnId,
120855
- processingUserMsgId: void 0,
120856
- dispatchError: {
120857
- code: code2,
120858
- ...message ? {
120859
- message
120860
- } : {},
120861
- at: getServerNow()
120862
- }
121502
+ latestUserMsgId: this.resolveLatestUserMsgIdForTerminalTurn(existingMeta, userTurnId),
121503
+ lastHandledUserMsgId: userTurnId,
121504
+ processingUserMsgId: void 0
120863
121505
  });
120864
121506
  }
120865
121507
  resolveGitHubProjectBranch(meta, preferredBranch) {
@@ -120953,15 +121595,18 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
120953
121595
  }
120954
121596
  async continueSession(message) {
120955
121597
  const { sessionId, acpSessionConfig, userId, userName, userEmail, userTurnId } = message;
120956
- 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);
120957
121604
  this.deps.touchSession(sessionId);
120958
121605
  this.deps.logger.info(`Session chat received: ${sessionId}`);
120959
121606
  this.deps.logger.debug(`[${sessionId}] Received chat request (userTurnId=${userTurnId})`);
120960
121607
  await this.upsertSessionMeta(sessionId, {
120961
- latestUserMsgId: userTurnId,
120962
- dispatchError: void 0
121608
+ latestUserMsgId: userTurnId
120963
121609
  });
120964
- const sessionDoc = await this.deps.workspaceDocument.getOrCreateSessionDoc(sessionId);
120965
121610
  const incomingProjectBranch = message.project?.branch?.trim();
120966
121611
  if (incomingProjectBranch) {
120967
121612
  await sessionDoc.setBaseBranch(incomingProjectBranch);
@@ -121298,7 +121943,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121298
121943
  }));
121299
121944
  }
121300
121945
  async startSession(message) {
121301
- await this.deps.evictForMemoryPressure(message.sessionId);
121946
+ const memoryPressureResult = await this.deps.evictForMemoryPressure(message.sessionId);
121302
121947
  const { sessionId, acpSessionConfig, workspaceId, env: env2 } = message;
121303
121948
  const userTurnId = typeof message.userTurnId === "string" && message.userTurnId.trim() ? message.userTurnId.trim() : void 0;
121304
121949
  const project = message.project;
@@ -121311,6 +121956,10 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121311
121956
  const promptText = agentConfig.prompt ?? "";
121312
121957
  const promptBytes = Buffer.byteLength(promptText, "utf8");
121313
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");
121314
121963
  const sessionDoc = await this.deps.workspaceDocument.getOrCreateSessionDoc(sessionId);
121315
121964
  const existingMeta = await sessionDoc.getMetaState();
121316
121965
  const fromFeedbackPostId = message.meta?.fromFeedbackPostId?.trim() || existingMeta?.fromFeedbackPostId?.trim() || void 0;
@@ -121334,12 +121983,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121334
121983
  fromFeedbackPostId
121335
121984
  };
121336
121985
  this.deps.logger.debug(`[${sessionId}] session/create summary`, configForLog);
121337
- if (userTurnId) {
121338
- await this.upsertSessionMeta(sessionId, {
121339
- latestUserMsgId: userTurnId,
121340
- dispatchError: void 0
121341
- });
121342
- }
121343
121986
  const sessionConfig = {
121344
121987
  sessionId,
121345
121988
  workspaceId,
@@ -121360,6 +122003,11 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121360
122003
  if (project) {
121361
122004
  await sessionDoc.setProject(project);
121362
122005
  }
122006
+ if (userTurnId) {
122007
+ await this.upsertSessionMeta(sessionId, {
122008
+ latestUserMsgId: userTurnId
122009
+ });
122010
+ }
121363
122011
  if (branch) {
121364
122012
  await sessionDoc.setBaseBranch(branch);
121365
122013
  }
@@ -121853,9 +122501,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121853
122501
  watchedSessions = /* @__PURE__ */ new Map();
121854
122502
  sessionCheckChains = /* @__PURE__ */ new Map();
121855
122503
  cancelCheckChains = /* @__PURE__ */ new Map();
121856
- dispatchRetryTimers = /* @__PURE__ */ new Map();
121857
- dispatchRetryAttempts = /* @__PURE__ */ new Map();
121858
- dispatchRetryStartedAt = /* @__PURE__ */ new Map();
121859
122504
  cancelSeenTurn = /* @__PURE__ */ new Map();
121860
122505
  metadataWatchHandle = null;
121861
122506
  userResolver;
@@ -121899,15 +122544,9 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121899
122544
  for (const watched of this.watchedSessions.values()) {
121900
122545
  watched.unsubscribe();
121901
122546
  }
121902
- for (const timer2 of this.dispatchRetryTimers.values()) {
121903
- clearTimeout(timer2);
121904
- }
121905
122547
  this.watchedSessions.clear();
121906
122548
  this.sessionCheckChains.clear();
121907
122549
  this.cancelCheckChains.clear();
121908
- this.dispatchRetryTimers.clear();
121909
- this.dispatchRetryAttempts.clear();
121910
- this.dispatchRetryStartedAt.clear();
121911
122550
  this.cancelSeenTurn.clear();
121912
122551
  this.userResolver.clear();
121913
122552
  this.started = false;
@@ -121983,14 +122622,15 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
121983
122622
  }));
121984
122623
  }
121985
122624
  async reconcileSessionWatch(sessionId) {
121986
- const meta = await this.readSessionMeta(sessionId);
122625
+ const roomId = getSessionRoomId(sessionId);
122626
+ const record2 = await this.deps.workspaceDocument.repo.getDocMeta(roomId);
122627
+ const meta = isLoroRepoDocDeleted(record2) ? void 0 : record2?.meta;
121987
122628
  const isOwned = meta?.machineId === this.deps.machineId && !meta?.isArchived;
121988
122629
  if (!isOwned) {
121989
122630
  const watched = this.watchedSessions.get(sessionId);
121990
122631
  watched?.unsubscribe();
121991
122632
  this.watchedSessions.delete(sessionId);
121992
122633
  this.cancelCheckChains.delete(sessionId);
121993
- this.clearDispatchRetry(sessionId);
121994
122634
  this.cancelSeenTurn.delete(sessionId);
121995
122635
  return;
121996
122636
  }
@@ -122001,7 +122641,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122001
122641
  this.watchedSessions.delete(sessionId);
122002
122642
  this.deps.logger.debug(`[${sessionId}] Unwatching idle session (no pending work)`);
122003
122643
  }
122004
- this.clearDispatchRetry(sessionId);
122005
122644
  return;
122006
122645
  }
122007
122646
  if (!this.watchedSessions.has(sessionId)) {
@@ -122018,11 +122657,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122018
122657
  this.enqueueCancelCheck(sessionId);
122019
122658
  this.enqueueSessionCheck(sessionId);
122020
122659
  }
122021
- async readSessionMeta(sessionId) {
122022
- const roomId = getSessionRoomId(sessionId);
122023
- const record2 = await this.deps.workspaceDocument.repo.getDocMeta(roomId);
122024
- return isLoroRepoDocDeleted(record2) ? void 0 : record2?.meta;
122025
- }
122026
122660
  sessionNeedsActiveWatch(meta) {
122027
122661
  const statusType = meta.status?.type;
122028
122662
  if (statusType === "running" || statusType === "initializing" || statusType === "requestPermission") {
@@ -122037,155 +122671,11 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122037
122671
  if (meta.lastCanceledTurn && meta.lastCanceledTurn !== this.cancelSeenTurn.get(meta.id)) {
122038
122672
  return true;
122039
122673
  }
122040
- if (this.isDispatchRecoveryUnhealthy(meta)) {
122041
- return false;
122042
- }
122043
122674
  if (!meta.lastHandledUserMsgId) {
122044
122675
  return true;
122045
122676
  }
122046
122677
  return false;
122047
122678
  }
122048
- static RECOVERABLE_DISPATCH_RETRY_INITIAL_DELAY_MS = 1e3;
122049
- static RECOVERABLE_DISPATCH_RETRY_MAX_DELAY_MS = 6e4;
122050
- static RECOVERABLE_DISPATCH_RETRY_MAX_ELAPSED_MS = 5 * 6e4;
122051
- static DISPATCH_RECOVERY_UNHEALTHY_CODE = "dispatch_recovery_unhealthy";
122052
- hasRecoverableDispatchSignal(meta) {
122053
- const statusType = meta.status?.type;
122054
- if (statusType === "running" || statusType === "initializing" || statusType === "requestPermission") {
122055
- return true;
122056
- }
122057
- if (meta.processingUserMsgId) {
122058
- return true;
122059
- }
122060
- return !!meta.latestUserMsgId && meta.latestUserMsgId !== meta.lastHandledUserMsgId;
122061
- }
122062
- isDispatchRecoveryUnhealthy(meta) {
122063
- return meta.dispatchError?.code === SessionDispatchWatcher.DISPATCH_RECOVERY_UNHEALTHY_CODE;
122064
- }
122065
- clearDispatchRetry(sessionId) {
122066
- const timer2 = this.dispatchRetryTimers.get(sessionId);
122067
- if (timer2) {
122068
- clearTimeout(timer2);
122069
- this.dispatchRetryTimers.delete(sessionId);
122070
- }
122071
- this.dispatchRetryAttempts.delete(sessionId);
122072
- this.dispatchRetryStartedAt.delete(sessionId);
122073
- }
122074
- scheduleRecoverableDispatchRetry(sessionId, meta) {
122075
- if (!this.hasRecoverableDispatchSignal(meta)) {
122076
- this.clearDispatchRetry(sessionId);
122077
- return;
122078
- }
122079
- if (this.dispatchRetryTimers.has(sessionId)) {
122080
- return;
122081
- }
122082
- const startedAt = this.dispatchRetryStartedAt.get(sessionId) ?? Date.now();
122083
- this.dispatchRetryStartedAt.set(sessionId, startedAt);
122084
- const elapsedMs = Date.now() - startedAt;
122085
- if (elapsedMs >= SessionDispatchWatcher.RECOVERABLE_DISPATCH_RETRY_MAX_ELAPSED_MS) {
122086
- void this.markDispatchRecoveryUnhealthy(sessionId, meta, elapsedMs).catch((error2) => {
122087
- this.deps.logger.error(`[${sessionId}] Failed to mark dispatch recovery unhealthy: ${formatErrorMessage(error2)}`);
122088
- });
122089
- return;
122090
- }
122091
- const attempt = this.dispatchRetryAttempts.get(sessionId) ?? 0;
122092
- const delayMs = this.calculateRecoverableDispatchRetryDelay(attempt);
122093
- const attemptNumber = attempt + 1;
122094
- this.dispatchRetryAttempts.set(sessionId, attemptNumber);
122095
- this.deps.logger.debug(`[${sessionId}] Dispatch metadata is present but no user turn is visible; rebuilding session doc watch in ${delayMs}ms (attempt=${attemptNumber}, elapsedMs=${elapsedMs})`);
122096
- const timer2 = setTimeout(() => {
122097
- this.dispatchRetryTimers.delete(sessionId);
122098
- if (!this.started) {
122099
- return;
122100
- }
122101
- this.refreshSessionDocWatch(sessionId, `recoverable-dispatch-missing-turn:${attemptNumber}`).catch((error2) => {
122102
- void this.handleDispatchRetryRefreshFailure(sessionId, meta, error2);
122103
- });
122104
- }, delayMs);
122105
- const maybeUnrefTimer = timer2;
122106
- maybeUnrefTimer.unref?.();
122107
- this.dispatchRetryTimers.set(sessionId, timer2);
122108
- }
122109
- calculateRecoverableDispatchRetryDelay(attempt) {
122110
- const exponentialDelayMs = Math.min(SessionDispatchWatcher.RECOVERABLE_DISPATCH_RETRY_INITIAL_DELAY_MS * 2 ** attempt, SessionDispatchWatcher.RECOVERABLE_DISPATCH_RETRY_MAX_DELAY_MS);
122111
- const halfDelayMs = exponentialDelayMs / 2;
122112
- return Math.floor(halfDelayMs + Math.random() * halfDelayMs);
122113
- }
122114
- async handleDispatchRetryRefreshFailure(sessionId, previousMeta, error2) {
122115
- this.deps.logger.error(`[${sessionId}] Failed to refresh session doc watch for dispatch retry: ${formatErrorMessage(error2)}`);
122116
- if (!this.started) {
122117
- return;
122118
- }
122119
- let meta = previousMeta;
122120
- try {
122121
- const freshMeta = await this.readSessionMeta(sessionId);
122122
- if (!freshMeta) {
122123
- this.clearDispatchRetry(sessionId);
122124
- return;
122125
- }
122126
- meta = freshMeta;
122127
- } catch (metaError) {
122128
- this.deps.logger.debug(`[${sessionId}] Failed to read fresh metadata after dispatch retry refresh failure; using previous retry signal: ${formatErrorMessage(metaError)}`);
122129
- }
122130
- if (!this.started) {
122131
- return;
122132
- }
122133
- if (meta.machineId !== this.deps.machineId || meta.isArchived) {
122134
- this.clearDispatchRetry(sessionId);
122135
- return;
122136
- }
122137
- this.scheduleRecoverableDispatchRetry(sessionId, meta);
122138
- }
122139
- async markDispatchRecoveryUnhealthy(sessionId, previousMeta, elapsedMs) {
122140
- this.clearDispatchRetry(sessionId);
122141
- if (!this.started) {
122142
- return;
122143
- }
122144
- let meta = previousMeta;
122145
- try {
122146
- const freshMeta = await this.readSessionMeta(sessionId);
122147
- if (!freshMeta) {
122148
- return;
122149
- }
122150
- meta = freshMeta;
122151
- } catch (error2) {
122152
- this.deps.logger.debug(`[${sessionId}] Failed to read fresh metadata before marking dispatch recovery unhealthy; using previous retry signal: ${formatErrorMessage(error2)}`);
122153
- }
122154
- if (!this.hasRecoverableDispatchSignal(meta) || meta.machineId !== this.deps.machineId || meta.isArchived) {
122155
- return;
122156
- }
122157
- this.deps.logger.warn(`[${sessionId}] Dispatch recovery could not find a user turn after ${elapsedMs}ms; marking recovery unhealthy`);
122158
- await this.deps.workspaceDocument.repo.upsertDocMeta?.(getSessionRoomId(sessionId), {
122159
- status: SessionStatusFactory.idle(),
122160
- latestUserMsgId: void 0,
122161
- processingUserMsgId: void 0,
122162
- dispatchError: {
122163
- code: SessionDispatchWatcher.DISPATCH_RECOVERY_UNHEALTHY_CODE,
122164
- message: "Dispatch recovery could not reconnect to this session after 5 minutes. Send a new message to retry.",
122165
- at: getServerNow()
122166
- }
122167
- });
122168
- const watched = this.watchedSessions.get(sessionId);
122169
- watched?.unsubscribe();
122170
- this.watchedSessions.delete(sessionId);
122171
- this.sessionCheckChains.delete(sessionId);
122172
- await this.deps.workspaceDocument.cleanSessionDoc(sessionId, {
122173
- preserveStatus: true
122174
- });
122175
- }
122176
- async refreshSessionDocWatch(sessionId, reason) {
122177
- const watched = this.watchedSessions.get(sessionId);
122178
- watched?.unsubscribe();
122179
- this.watchedSessions.delete(sessionId);
122180
- this.deps.logger.debug(`[${sessionId}] Refreshing session doc watch (${reason})`);
122181
- await this.deps.workspaceDocument.cleanSessionDoc(sessionId, {
122182
- preserveStatus: true
122183
- });
122184
- if (!this.started) {
122185
- return;
122186
- }
122187
- await this.reconcileSessionWatch(sessionId);
122188
- }
122189
122679
  async maybeHandleSession(sessionId) {
122190
122680
  const sessionDoc = await this.deps.workspaceDocument.getOrCreateSessionDoc(sessionId);
122191
122681
  const meta = await sessionDoc.getMetaState();
@@ -122208,10 +122698,11 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122208
122698
  }
122209
122699
  const nextUserTurn = await this.findOrAwaitDispatchableTurn(sessionId, sessionDoc, meta);
122210
122700
  if (!nextUserTurn) {
122211
- this.scheduleRecoverableDispatchRetry(sessionId, meta);
122701
+ if (this.hasPendingUserTurnSignal(meta)) {
122702
+ await this.markMissingUserTurnRecovery(sessionId, meta);
122703
+ }
122212
122704
  return;
122213
122705
  }
122214
- this.clearDispatchRetry(sessionId);
122215
122706
  const freshMeta = await sessionDoc.getMetaState();
122216
122707
  if (!freshMeta) {
122217
122708
  return;
@@ -122275,12 +122766,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122275
122766
  await this.deps.workspaceDocument.repo.upsertDocMeta?.(getSessionRoomId(sessionId), {
122276
122767
  latestUserMsgId: userTurnId,
122277
122768
  lastHandledUserMsgId: userTurnId,
122278
- processingUserMsgId: void 0,
122279
- dispatchError: {
122280
- code: reason === "cli_token_invalid" ? "cli_token_invalid" : "machine_access_denied",
122281
- message,
122282
- at: getServerNow()
122283
- }
122769
+ processingUserMsgId: void 0
122284
122770
  });
122285
122771
  await sessionDoc.setStatus(SessionStatusFactory.idle());
122286
122772
  }
@@ -122407,6 +122893,26 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122407
122893
  }
122408
122894
  static REALTIME_WAIT_TIMEOUT_MS = 3e4;
122409
122895
  static REMOTE_SYNC_TIMEOUT_MS = 15e3;
122896
+ static HISTORY_SYNC_WAIT_TIMEOUT_MS = 5 * 6e4;
122897
+ static HISTORY_RECONNECT_JITTER_MIN_MS = 500;
122898
+ static HISTORY_RECONNECT_JITTER_MAX_MS = 1500;
122899
+ static setUnrefTimeout(callback, delayMs) {
122900
+ const timer2 = setTimeout(callback, delayMs);
122901
+ if (typeof timer2 === "object" && "unref" in timer2) {
122902
+ timer2.unref();
122903
+ }
122904
+ return timer2;
122905
+ }
122906
+ static sleep(delayMs) {
122907
+ return new Promise((resolve2) => {
122908
+ SessionDispatchWatcher.setUnrefTimeout(resolve2, delayMs);
122909
+ });
122910
+ }
122911
+ static getReconnectJitterMs() {
122912
+ const min2 = SessionDispatchWatcher.HISTORY_RECONNECT_JITTER_MIN_MS;
122913
+ const max2 = SessionDispatchWatcher.HISTORY_RECONNECT_JITTER_MAX_MS;
122914
+ return min2 + Math.floor(Math.random() * (max2 - min2 + 1));
122915
+ }
122410
122916
  async findOrAwaitDispatchableTurn(sessionId, sessionDoc, meta) {
122411
122917
  const history = await sessionDoc.getHistory();
122412
122918
  let turn = findNextDispatchableUserTurn(history, meta);
@@ -122417,9 +122923,12 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122417
122923
  if (turn) {
122418
122924
  return turn;
122419
122925
  }
122926
+ if (this.hasPendingUserTurnSignal(meta)) {
122927
+ return await this.waitForPendingUserTurnHistorySync(sessionId, sessionDoc, meta);
122928
+ }
122420
122929
  await Promise.race([
122421
122930
  sessionDoc.waitForRemoteSync(),
122422
- new Promise((resolve2) => setTimeout(resolve2, SessionDispatchWatcher.REMOTE_SYNC_TIMEOUT_MS))
122931
+ new Promise((resolve2) => SessionDispatchWatcher.setUnrefTimeout(resolve2, SessionDispatchWatcher.REMOTE_SYNC_TIMEOUT_MS))
122423
122932
  ]);
122424
122933
  turn = await this.checkHistoryAndQueue(sessionDoc, meta);
122425
122934
  if (turn) {
@@ -122429,6 +122938,138 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122429
122938
  turn = await this.waitForDispatchableTurnFromMirror(sessionId, sessionDoc, meta);
122430
122939
  return turn;
122431
122940
  }
122941
+ hasPendingUserTurnSignal(meta) {
122942
+ if (meta.processingUserMsgId) {
122943
+ return true;
122944
+ }
122945
+ return Boolean(meta.latestUserMsgId && meta.latestUserMsgId !== meta.lastHandledUserMsgId);
122946
+ }
122947
+ async waitForPendingUserTurnHistorySync(sessionId, sessionDoc, meta) {
122948
+ 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`);
122949
+ try {
122950
+ await sessionDoc.ensureDocRoomJoined();
122951
+ } catch (error2) {
122952
+ this.deps.logger.debug(`[${sessionId}] Failed to ensure session history room is joined before waiting: ${formatErrorMessage(error2)}`);
122953
+ }
122954
+ await sessionDoc.waitUntilSynced();
122955
+ const freshMeta = await sessionDoc.getMetaState() ?? meta;
122956
+ if (!this.hasPendingUserTurnSignal(freshMeta)) {
122957
+ this.deps.logger.debug(`[${sessionId}] Pending user turn pointer cleared during pre-wait sync; exiting wait`);
122958
+ return null;
122959
+ }
122960
+ const turnAfterJoin = await this.checkHistoryAndQueue(sessionDoc, freshMeta);
122961
+ if (turnAfterJoin) {
122962
+ return turnAfterJoin;
122963
+ }
122964
+ return await new Promise((resolve2) => {
122965
+ let settled = false;
122966
+ let reconnectAttempted = false;
122967
+ let unsubscribeMirror;
122968
+ let unsubscribeStatus;
122969
+ const cleanup = () => {
122970
+ if (settled) {
122971
+ return;
122972
+ }
122973
+ settled = true;
122974
+ clearTimeout(timer2);
122975
+ unsubscribeMirror?.();
122976
+ unsubscribeStatus?.();
122977
+ };
122978
+ const checkForTurn = () => {
122979
+ if (settled) {
122980
+ return;
122981
+ }
122982
+ void this.checkHistoryAndQueue(sessionDoc, freshMeta).then((turn) => {
122983
+ if (settled || !turn) {
122984
+ return;
122985
+ }
122986
+ cleanup();
122987
+ this.deps.logger.debug(`[${sessionId}] Found dispatchable turn after waiting for history CRDT sync`);
122988
+ resolve2(turn);
122989
+ }).catch((error2) => {
122990
+ if (settled) {
122991
+ return;
122992
+ }
122993
+ this.deps.logger.debug(`[${sessionId}] Error checking history during history sync wait: ${formatErrorMessage(error2)}`);
122994
+ });
122995
+ };
122996
+ const attemptReconnectOnce = (reason) => {
122997
+ if (settled || reconnectAttempted) {
122998
+ return;
122999
+ }
123000
+ reconnectAttempted = true;
123001
+ void (async () => {
123002
+ const jitterMs = SessionDispatchWatcher.getReconnectJitterMs();
123003
+ this.deps.logger.debug(`[${sessionId}] Session history room ${reason}; attempting one rejoin in ${jitterMs}ms`);
123004
+ await SessionDispatchWatcher.sleep(jitterMs);
123005
+ if (settled) {
123006
+ return;
123007
+ }
123008
+ try {
123009
+ await sessionDoc.rejoinDocRoom();
123010
+ } catch (error2) {
123011
+ this.deps.logger.debug(`[${sessionId}] Session history room rejoin failed: ${formatErrorMessage(error2)}`);
123012
+ }
123013
+ checkForTurn();
123014
+ })();
123015
+ };
123016
+ const handleRoomStatus = (status) => {
123017
+ if (settled || !status) {
123018
+ return;
123019
+ }
123020
+ if (status === "disconnected" || status === "error") {
123021
+ attemptReconnectOnce(status);
123022
+ }
123023
+ };
123024
+ const timer2 = SessionDispatchWatcher.setUnrefTimeout(() => {
123025
+ cleanup();
123026
+ this.deps.logger.warn(`[${sessionId}] User turn did not arrive in history after ${SessionDispatchWatcher.HISTORY_SYNC_WAIT_TIMEOUT_MS / 1e3}s; entering dispatch recovery`);
123027
+ resolve2(null);
123028
+ }, SessionDispatchWatcher.HISTORY_SYNC_WAIT_TIMEOUT_MS);
123029
+ unsubscribeMirror = sessionDoc.mirror?.subscribe(checkForTurn);
123030
+ if (!unsubscribeMirror) {
123031
+ this.deps.logger.debug(`[${sessionId}] Session mirror is unavailable during history sync wait`);
123032
+ }
123033
+ unsubscribeStatus = sessionDoc.onDocRoomStatusChange(handleRoomStatus);
123034
+ const currentStatus = sessionDoc.getDocRoomStatus();
123035
+ if (currentStatus) {
123036
+ handleRoomStatus(currentStatus);
123037
+ } else {
123038
+ attemptReconnectOnce("has no active subscription");
123039
+ }
123040
+ });
123041
+ }
123042
+ async markMissingUserTurnRecovery(sessionId, previousMeta) {
123043
+ const roomId = getSessionRoomId(sessionId);
123044
+ let meta = previousMeta;
123045
+ try {
123046
+ const record2 = await this.deps.workspaceDocument.repo.getDocMeta(roomId);
123047
+ if (!isLoroRepoDocDeleted(record2) && record2?.meta) {
123048
+ meta = record2.meta;
123049
+ }
123050
+ } catch (error2) {
123051
+ this.deps.logger.debug(`[${sessionId}] Failed to refresh metadata before dispatch recovery: ${formatErrorMessage(error2)}`);
123052
+ }
123053
+ if (meta.machineId !== this.deps.machineId || meta.isArchived || !this.hasPendingUserTurnSignal(meta)) {
123054
+ return;
123055
+ }
123056
+ const pendingUserMsgId = meta.processingUserMsgId ?? meta.latestUserMsgId ?? meta.lastHandledUserMsgId;
123057
+ const recoveryPatch = {
123058
+ status: SessionStatusFactory.idle(),
123059
+ processingUserMsgId: void 0
123060
+ };
123061
+ if (pendingUserMsgId) {
123062
+ recoveryPatch.lastHandledUserMsgId = pendingUserMsgId;
123063
+ recoveryPatch.latestUserMsgId = pendingUserMsgId;
123064
+ }
123065
+ await this.deps.workspaceDocument.repo.upsertDocMeta?.(roomId, recoveryPatch);
123066
+ const watched = this.watchedSessions.get(sessionId);
123067
+ watched?.unsubscribe();
123068
+ this.watchedSessions.delete(sessionId);
123069
+ await this.deps.workspaceDocument.cleanSessionDoc(sessionId, {
123070
+ preserveStatus: true
123071
+ });
123072
+ }
122432
123073
  async waitForDispatchableTurnFromMirror(sessionId, sessionDoc, meta) {
122433
123074
  return new Promise((resolve2) => {
122434
123075
  let settled = false;
@@ -122437,7 +123078,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
122437
123078
  clearTimeout(timer2);
122438
123079
  unsubscribe2?.();
122439
123080
  };
122440
- const timer2 = setTimeout(() => {
123081
+ const timer2 = SessionDispatchWatcher.setUnrefTimeout(() => {
122441
123082
  if (settled) return;
122442
123083
  cleanup();
122443
123084
  this.deps.logger.debug(`[${sessionId}] No dispatchable turn found after ${SessionDispatchWatcher.REALTIME_WAIT_TIMEOUT_MS / 1e3}s timeout`);
@@ -123168,8 +123809,14 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
123168
123809
  permissionRequestStartTimes = /* @__PURE__ */ new Map();
123169
123810
  machineHeartbeatTimer = null;
123170
123811
  static MACHINE_HEARTBEAT_INTERVAL_MS = 2e4;
123171
- evictForMemoryPressureFn = async () => {
123172
- };
123812
+ evictForMemoryPressureFn = async () => ({
123813
+ availableMemoryBytes: 0,
123814
+ thresholdBytes: 0,
123815
+ hadMemoryPressure: false,
123816
+ stillUnderPressure: false,
123817
+ evictedSessionIds: [],
123818
+ pressureReason: null
123819
+ });
123173
123820
  executionService;
123174
123821
  sessionDispatchWatcher;
123175
123822
  autoPromptRunner;
@@ -125181,6 +125828,16 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125181
125828
  hasPendingUpdates(sessionId) {
125182
125829
  return this.store.has(sessionId) && this.store.get(sessionId).acpUpdateBuffer.length > 0;
125183
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
+ }
125184
125841
  isArchiveInFlight(sessionId) {
125185
125842
  return this.archiveInFlight.has(sessionId);
125186
125843
  }
@@ -125537,116 +126194,6 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125537
126194
  }
125538
126195
  }
125539
126196
  }
125540
- function getAvailableMemoryBytes() {
125541
- const systemAvailable = getSystemAvailableMemoryBytes();
125542
- const cgroupAvailable = getCgroupAvailableMemoryBytes();
125543
- if (cgroupAvailable !== null) {
125544
- return Math.min(systemAvailable, cgroupAvailable);
125545
- }
125546
- return systemAvailable;
125547
- }
125548
- function getEffectiveMemoryLimitBytes() {
125549
- const totalMem = os__default.totalmem();
125550
- const cgroupMax = getCgroupMemoryMaxBytes();
125551
- if (cgroupMax !== null) {
125552
- return Math.min(totalMem, cgroupMax);
125553
- }
125554
- return totalMem;
125555
- }
125556
- function getSystemAvailableMemoryBytes() {
125557
- try {
125558
- const meminfo = readFileSync("/proc/meminfo", "utf8");
125559
- const match5 = meminfo.match(/MemAvailable:\s+(\d+)/);
125560
- if (match5?.[1]) {
125561
- return parseInt(match5[1], 10) * 1024;
125562
- }
125563
- } catch {
125564
- }
125565
- return os__default.freemem();
125566
- }
125567
- function getCgroupMemoryMaxBytes() {
125568
- try {
125569
- const cgroupPath = readSelfCgroupPath();
125570
- if (cgroupPath === null) return null;
125571
- let tightest = null;
125572
- let current2 = cgroupPath;
125573
- for (let depth = 0; depth < 20; depth++) {
125574
- const memMaxPath = `/sys/fs/cgroup${current2 === "/" ? "" : current2}/memory.max`;
125575
- const raw = readFileSafe(memMaxPath);
125576
- if (raw !== null) {
125577
- const trimmed = raw.trim();
125578
- if (trimmed !== "max") {
125579
- const value = parseInt(trimmed, 10);
125580
- if (Number.isFinite(value) && value > 0) {
125581
- tightest = tightest === null ? value : Math.min(tightest, value);
125582
- }
125583
- }
125584
- }
125585
- if (current2 === "/" || current2 === "") break;
125586
- const parent = current2.substring(0, current2.lastIndexOf("/")) || "/";
125587
- if (parent === current2) break;
125588
- current2 = parent;
125589
- }
125590
- return tightest;
125591
- } catch {
125592
- return null;
125593
- }
125594
- }
125595
- function getCgroupAvailableMemoryBytes() {
125596
- try {
125597
- const cgroupPath = readSelfCgroupPath();
125598
- if (cgroupPath === null) return null;
125599
- let tightestMax = null;
125600
- let tightestPath = null;
125601
- let current2 = cgroupPath;
125602
- for (let depth = 0; depth < 20; depth++) {
125603
- const prefix = `/sys/fs/cgroup${current2 === "/" ? "" : current2}`;
125604
- const raw = readFileSafe(`${prefix}/memory.max`);
125605
- if (raw !== null) {
125606
- const trimmed = raw.trim();
125607
- if (trimmed !== "max") {
125608
- const value = parseInt(trimmed, 10);
125609
- if (Number.isFinite(value) && value > 0) {
125610
- if (tightestMax === null || value < tightestMax) {
125611
- tightestMax = value;
125612
- tightestPath = prefix;
125613
- }
125614
- }
125615
- }
125616
- }
125617
- if (current2 === "/" || current2 === "") break;
125618
- const parent = current2.substring(0, current2.lastIndexOf("/")) || "/";
125619
- if (parent === current2) break;
125620
- current2 = parent;
125621
- }
125622
- if (tightestMax === null || tightestPath === null) return null;
125623
- const currentRaw = readFileSafe(`${tightestPath}/memory.current`);
125624
- if (currentRaw === null) return null;
125625
- const currentUsage = parseInt(currentRaw.trim(), 10);
125626
- if (!Number.isFinite(currentUsage)) return null;
125627
- return Math.max(0, tightestMax - currentUsage);
125628
- } catch {
125629
- return null;
125630
- }
125631
- }
125632
- function readSelfCgroupPath() {
125633
- try {
125634
- const content = readFileSync("/proc/self/cgroup", "utf8");
125635
- const line3 = content.split("\n").map((l) => l.trim()).find((l) => l.startsWith("0::"));
125636
- if (!line3) return null;
125637
- const cgroupPath = line3.slice(3).trim();
125638
- return cgroupPath || "/";
125639
- } catch {
125640
- return null;
125641
- }
125642
- }
125643
- function readFileSafe(filePath) {
125644
- try {
125645
- return readFileSync(filePath, "utf8");
125646
- } catch {
125647
- return null;
125648
- }
125649
- }
125650
126197
  function readEnvNumber(key2, fallback2) {
125651
126198
  const raw = process.env[key2];
125652
126199
  if (!raw) return fallback2;
@@ -125654,6 +126201,8 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125654
126201
  return Number.isFinite(parsed) ? parsed : fallback2;
125655
126202
  }
125656
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;
125657
126206
  function defaultMemoryThresholdBytes() {
125658
126207
  const tenPercent = Math.floor(getEffectiveMemoryLimitBytes() * 0.1);
125659
126208
  return Math.max(GIB, Math.min(4 * GIB, tenPercent));
@@ -125666,6 +126215,9 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125666
126215
  memoryThresholdBytes: readEnvNumber("LODY_SESSION_GC_MEMORY_THRESHOLD_BYTES", defaultMemoryThresholdBytes())
125667
126216
  };
125668
126217
  }
126218
+ function getWindowsCommitThresholdBytes(memoryThresholdBytes) {
126219
+ return Math.max(WINDOWS_COMMIT_THRESHOLD_FLOOR_BYTES, Math.min(WINDOWS_COMMIT_THRESHOLD_CEILING_BYTES, memoryThresholdBytes));
126220
+ }
125669
126221
  class SessionGCManager {
125670
126222
  constructor(config2, deps) {
125671
126223
  this.config = config2;
@@ -125689,7 +126241,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125689
126241
  }
125690
126242
  async sweep() {
125691
126243
  const sweepStart = Date.now();
125692
- const candidates = this.getIdleCandidates();
126244
+ const candidates = await this.getIdleCandidates();
125693
126245
  if (candidates.length === 0) {
125694
126246
  return;
125695
126247
  }
@@ -125697,7 +126249,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125697
126249
  let cleaned = 0;
125698
126250
  let skipped = 0;
125699
126251
  for (const { sessionId } of candidates) {
125700
- if (!this.isStillEligibleForGC(sessionId)) {
126252
+ if (!await this.isStillEligibleForGC(sessionId)) {
125701
126253
  skipped++;
125702
126254
  continue;
125703
126255
  }
@@ -125715,19 +126267,50 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125715
126267
  this.deps.logger.debug(`[GC] Sweep completed: cleaned ${cleaned}/${candidates.length} sessions in ${sweepDuration}ms`);
125716
126268
  }
125717
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);
125718
126275
  if (!this.config.enabled) {
125719
- 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
+ };
125720
126290
  }
125721
- let availableMemory = getAvailableMemoryBytes();
125722
- if (availableMemory >= this.config.memoryThresholdBytes) {
125723
- 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
+ };
125724
126306
  }
125725
- 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}`);
125726
126309
  const sessions = this.getSessionsWithIdleTime();
125727
126310
  sessions.sort((a, b) => b.idleMs - a.idleMs);
125728
- let evicted = 0;
126311
+ const evictedSessionIds = [];
125729
126312
  for (const { sessionId, idleMs } of sessions) {
125730
- if (availableMemory >= this.config.memoryThresholdBytes) {
126313
+ if (pressureReason === null) {
125731
126314
  break;
125732
126315
  }
125733
126316
  if (excludeSessionId && sessionId === excludeSessionId) {
@@ -125736,30 +126319,63 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125736
126319
  if (idleMs === 0) {
125737
126320
  continue;
125738
126321
  }
125739
- if (!this.isEligibleForCleanup(sessionId)) {
126322
+ if (!await this.isEligibleForCleanup(sessionId)) {
125740
126323
  continue;
125741
126324
  }
125742
126325
  try {
125743
126326
  this.deps.logger.debug(`[GC] Evicting session ${sessionId} (idle ${Math.round(idleMs / 1e3)}s) due to memory pressure`);
125744
126327
  await this.deps.cleanSession(sessionId);
125745
- evicted++;
125746
- availableMemory = getAvailableMemoryBytes();
126328
+ evictedSessionIds.push(sessionId);
126329
+ memorySnapshot = getMemoryPressureSnapshot();
126330
+ availableMemory = memorySnapshot.availableMemoryBytes;
126331
+ pressureReason = this.getPressureReason(memorySnapshot, thresholdBytes, commitThresholdBytes);
125747
126332
  } catch (error2) {
125748
126333
  this.deps.logger.error(`[GC] Failed to evict session ${sessionId}: ${formatErrorMessage(error2)}`);
125749
126334
  }
125750
126335
  }
125751
- if (evicted > 0) {
125752
- 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";
126365
+ }
126366
+ if (commitPressure) {
126367
+ return "commit";
125753
126368
  }
126369
+ return null;
125754
126370
  }
125755
- getIdleCandidates() {
126371
+ async getIdleCandidates() {
125756
126372
  const sessions = this.getSessionsWithIdleTime();
125757
126373
  const candidates = [];
125758
126374
  for (const session of sessions) {
125759
126375
  if (session.idleMs < this.config.idleTimeoutMs) {
125760
126376
  continue;
125761
126377
  }
125762
- if (!this.isEligibleForCleanup(session.sessionId)) {
126378
+ if (!await this.isEligibleForCleanup(session.sessionId)) {
125763
126379
  continue;
125764
126380
  }
125765
126381
  candidates.push(session);
@@ -125787,20 +126403,23 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125787
126403
  }
125788
126404
  return result;
125789
126405
  }
125790
- isEligibleForCleanup(sessionId) {
126406
+ async isEligibleForCleanup(sessionId) {
125791
126407
  if (this.deps.hasActiveTurn(sessionId)) {
125792
126408
  return false;
125793
126409
  }
125794
126410
  if (this.deps.hasPendingUpdates(sessionId)) {
125795
126411
  return false;
125796
126412
  }
126413
+ if (await this.deps.hasPendingUserWork(sessionId)) {
126414
+ return false;
126415
+ }
125797
126416
  if (this.deps.isArchiveInFlight(sessionId)) {
125798
126417
  return false;
125799
126418
  }
125800
126419
  return true;
125801
126420
  }
125802
- isStillEligibleForGC(sessionId) {
125803
- if (!this.isEligibleForCleanup(sessionId)) {
126421
+ async isStillEligibleForGC(sessionId) {
126422
+ if (!await this.isEligibleForCleanup(sessionId)) {
125804
126423
  return false;
125805
126424
  }
125806
126425
  const lastActivity = this.deps.getSessionLastActivity(sessionId);
@@ -125928,6 +126547,7 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125928
126547
  getSessionLastActivity: (sessionId) => handler.getLastActivity(sessionId),
125929
126548
  hasActiveTurn: (sessionId) => handler.hasActiveTurn(sessionId),
125930
126549
  hasPendingUpdates: (sessionId) => handler.hasPendingUpdates(sessionId),
126550
+ hasPendingUserWork: async (sessionId) => await handler.hasPendingUserWork(sessionId),
125931
126551
  isArchiveInFlight: (sessionId) => handler.isArchiveInFlight(sessionId),
125932
126552
  cleanSession: (sessionId) => handler.cleanSessionForGC(sessionId),
125933
126553
  getSessionIds: () => handler.getTrackedSessionIds(),
@@ -125936,8 +126556,16 @@ The postId is ${normalizedFeedbackPostId}. Use the feedback-progress-reporter sk
125936
126556
  this.gcManager.start();
125937
126557
  handler.setEvictForMemoryPressure(async (excludeSessionId) => {
125938
126558
  if (this.gcManager) {
125939
- await this.gcManager.evictForMemoryPressure(excludeSessionId);
126559
+ return await this.gcManager.evictForMemoryPressure(excludeSessionId);
125940
126560
  }
126561
+ return {
126562
+ availableMemoryBytes: 0,
126563
+ thresholdBytes: 0,
126564
+ hadMemoryPressure: false,
126565
+ stillUnderPressure: false,
126566
+ evictedSessionIds: [],
126567
+ pressureReason: null
126568
+ };
125941
126569
  });
125942
126570
  }
125943
126571
  requireSessionManager() {
@@ -133135,6 +133763,9 @@ export PATH=${toSingleQuotedShellString(ghShimBinDir)}:"$PATH"
133135
133763
  isControlPlaneReady() {
133136
133764
  return this.documentManager.isTransportConnected();
133137
133765
  }
133766
+ isControlPlaneRecovering() {
133767
+ return this.documentManager.isTransportRecovering();
133768
+ }
133138
133769
  getActiveSessionCount() {
133139
133770
  return this.runtime.getActiveSessionCount();
133140
133771
  }
@@ -134964,23 +135595,26 @@ export PATH=${toSingleQuotedShellString(ghShimBinDir)}:"$PATH"
134964
135595
  refreshRuntimeState() {
134965
135596
  const desiredCount = this.desiredWorkspaces.size;
134966
135597
  let connectedCount = 0;
135598
+ let reconnectingCount = 0;
134967
135599
  let totalActiveSessions = 0;
134968
135600
  let totalConnectedRooms = 0;
134969
135601
  for (const runtime of this.runtimes.values()) {
134970
135602
  if (runtime.lody.isControlPlaneReady()) {
134971
135603
  connectedCount += 1;
135604
+ } else if (runtime.lody.isControlPlaneRecovering()) {
135605
+ reconnectingCount += 1;
134972
135606
  }
134973
135607
  totalActiveSessions += runtime.lody.getActiveSessionCount();
134974
135608
  totalConnectedRooms += runtime.lody.getConnectedRoomCount();
134975
135609
  }
134976
135610
  this.runtimeStateReporter.setActiveSessionCount(totalActiveSessions);
134977
135611
  this.runtimeStateReporter.setConnectedRoomCount(totalConnectedRooms);
134978
- 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";
134979
135614
  if (this.lastConnectivity !== nextConnectivity) {
134980
135615
  this.lastConnectivity = nextConnectivity;
134981
135616
  this.runtimeStateReporter.setConnectivity(nextConnectivity);
134982
135617
  }
134983
- const hasWorkspaceRetry = this.retryTimers.size > 0 || this.startInFlight.size > 0;
134984
135618
  if (hasWorkspaceRetry && !this.hasWorkspaceRetryIssue) {
134985
135619
  this.hasWorkspaceRetryIssue = true;
134986
135620
  this.runtimeStateReporter.upsertIssue({
@@ -167263,10 +167897,18 @@ ${entry2.text}`).join("\n\n");
167263
167897
  }
167264
167898
  const DEFAULT_MIN_RETRY_MS$1 = 1e3;
167265
167899
  const DEFAULT_MAX_RETRY_MS$1 = 3e4;
167266
- 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 = {}) {
167267
167902
  const exponent = Math.max(0, attempt);
167268
- const delay2 = minMs * 2 ** exponent;
167269
- 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);
167270
167912
  }
167271
167913
  class FailureWindow {
167272
167914
  historyMs = [];
@@ -167327,6 +167969,7 @@ ${result.stderr}`;
167327
167969
  probeFailureThreshold;
167328
167970
  minRetryMs;
167329
167971
  maxRetryMs;
167972
+ retryRandom;
167330
167973
  failureWindow;
167331
167974
  triggered = false;
167332
167975
  inFlight = false;
@@ -167358,6 +168001,7 @@ ${result.stderr}`;
167358
168001
  this.probeFailureThreshold = options.probeFailureThreshold ?? DEFAULT_PROBE_FAILURE_THRESHOLD;
167359
168002
  this.minRetryMs = options.minRetryMs ?? DEFAULT_MIN_RETRY_MS;
167360
168003
  this.maxRetryMs = options.maxRetryMs ?? DEFAULT_MAX_RETRY_MS;
168004
+ this.retryRandom = options.retryRandom ?? Math.random;
167361
168005
  this.failureWindow = new FailureWindow(options.fatalFailureWindowMs ?? DEFAULT_FATAL_FAILURE_WINDOW_MS, options.fatalFailureThreshold ?? DEFAULT_FATAL_FAILURE_THRESHOLD);
167362
168006
  }
167363
168007
  getState() {
@@ -167437,6 +168081,9 @@ ${result.stderr}`;
167437
168081
  this.latestRuntimeState = runtimeState;
167438
168082
  this.probeFailureCount = 0;
167439
168083
  this.probeUnavailable = false;
168084
+ this.clearRetryTimer();
168085
+ this.attempt = 0;
168086
+ this.failureWindow.reset();
167440
168087
  this.lastStateMessage = void 0;
167441
168088
  } else {
167442
168089
  this.probeFailureCount += 1;
@@ -167457,7 +168104,9 @@ ${result.stderr}`;
167457
168104
  } else if (this.fatalReason) {
167458
168105
  phase = "fatal";
167459
168106
  } else if (runtime) {
167460
- 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";
167461
168110
  } else if (childRunning) {
167462
168111
  phase = this.probeUnavailable ? "offline" : "starting";
167463
168112
  } else if (this.probeUnavailable && !this.inFlight && !this.retryTimer) {
@@ -167514,7 +168163,9 @@ ${result.stderr}`;
167514
168163
  scheduleRetry(reason, recordFailure = true) {
167515
168164
  if (this.fatalReason) return;
167516
168165
  if (recordFailure && this.recordFailure(reason)) return;
167517
- const delay2 = buildRetryDelay(this.attempt, this.minRetryMs, this.maxRetryMs);
168166
+ const delay2 = buildRetryDelay(this.attempt, this.minRetryMs, this.maxRetryMs, {
168167
+ random: this.retryRandom
168168
+ });
167518
168169
  this.attempt += 1;
167519
168170
  this.clearRetryTimer();
167520
168171
  this.pendingRetryInMs = delay2;