omp-fabric 1.25.28 → 1.25.34

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 (38) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/actors/manager.d.ts.map +1 -1
  3. package/dist/chunks/{chunk-TDO2BRZM.js → chunk-3FCNSDDD.js} +2 -2
  4. package/dist/chunks/{chunk-KW6EUFD2.js → chunk-HOKJTRI4.js} +243 -67
  5. package/dist/chunks/chunk-HOKJTRI4.js.map +7 -0
  6. package/dist/compaction/lcm-maintenance.d.ts +6 -3
  7. package/dist/compaction/lcm-maintenance.d.ts.map +1 -1
  8. package/dist/compaction/lcm-runtime.d.ts +7 -1
  9. package/dist/compaction/lcm-runtime.d.ts.map +1 -1
  10. package/dist/compaction/lcm-runtime.js +296 -59
  11. package/dist/compaction/lcm-runtime.js.map +4 -4
  12. package/dist/core/event-loop-slice.d.ts +11 -0
  13. package/dist/core/event-loop-slice.d.ts.map +1 -0
  14. package/dist/fabric-runtime-state.d.ts.map +1 -1
  15. package/dist/fabric-runtime-state.js +65 -13
  16. package/dist/fabric-runtime-state.js.map +3 -3
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +11 -9
  19. package/dist/index.js.map +2 -2
  20. package/dist/mesh/store.d.ts +3 -0
  21. package/dist/mesh/store.d.ts.map +1 -1
  22. package/dist/providers/omp-tools-provider.d.ts +11 -1
  23. package/dist/providers/omp-tools-provider.d.ts.map +1 -1
  24. package/dist/residency/host.js +2 -2
  25. package/dist/residency/omp-entry.js +2 -2
  26. package/dist/storage/lcm-directory.d.ts +1 -0
  27. package/dist/storage/lcm-directory.d.ts.map +1 -1
  28. package/dist/storage/lcm-ledger.d.ts +9 -1
  29. package/dist/storage/lcm-ledger.d.ts.map +1 -1
  30. package/dist/storage/lcm-migration.d.ts +6 -2
  31. package/dist/storage/lcm-migration.d.ts.map +1 -1
  32. package/dist/storage/lcm-offthread.d.ts +9 -0
  33. package/dist/storage/lcm-offthread.d.ts.map +1 -0
  34. package/dist/topology/control-plane.d.ts.map +1 -1
  35. package/dist/topology/participant-directory.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/dist/chunks/chunk-KW6EUFD2.js.map +0 -7
  38. /package/dist/chunks/{chunk-TDO2BRZM.js.map → chunk-3FCNSDDD.js.map} +0 -0
@@ -57,13 +57,119 @@ import fs2 from "node:fs";
57
57
  import path2 from "node:path";
58
58
  import crypto from "node:crypto";
59
59
 
60
+ // src/storage/lcm-offthread.ts
61
+ import { Worker } from "node:worker_threads";
62
+ var WORKER_SOURCE = String.raw`
63
+ const { parentPort, workerData } = require("node:worker_threads");
64
+ const openReadOnly = (file) => {
65
+ try {
66
+ const { DatabaseSync } = require("node:sqlite");
67
+ return new DatabaseSync(file, { readOnly: true });
68
+ } catch {}
69
+ const { Database } = require("bun:sqlite");
70
+ return new Database(file, { readonly: true });
71
+ };
72
+ let db;
73
+ try {
74
+ db = openReadOnly(workerData.file);
75
+ } catch (error) {
76
+ parentPort.postMessage({ unavailable: error instanceof Error ? error.message : String(error) });
77
+ }
78
+ if (db) {
79
+ try {
80
+ if (workerData.snapshot) db.exec("BEGIN");
81
+ const rows = workerData.statements.map((sql) => ({ ...(db.prepare(sql).get() ?? {}) }));
82
+ if (workerData.snapshot) db.exec("COMMIT");
83
+ parentPort.postMessage({ rows });
84
+ } catch (error) {
85
+ parentPort.postMessage({ error: error instanceof Error ? error.message : String(error) });
86
+ } finally {
87
+ try { db.close(); } catch {}
88
+ }
89
+ }
90
+ `;
91
+ var queryLedgerOffThread = (query) => new Promise((resolve, reject) => {
92
+ if (query.signal?.aborted) {
93
+ reject(new Error("ledger closed"));
94
+ return;
95
+ }
96
+ let worker;
97
+ try {
98
+ worker = new Worker(WORKER_SOURCE, {
99
+ eval: true,
100
+ workerData: { file: query.file, statements: [...query.statements], snapshot: query.snapshot === true }
101
+ });
102
+ } catch {
103
+ resolve(void 0);
104
+ return;
105
+ }
106
+ let settled = false;
107
+ const finish = (settle) => {
108
+ if (settled) return;
109
+ settled = true;
110
+ query.signal?.removeEventListener("abort", onAbort);
111
+ void worker.terminate();
112
+ settle();
113
+ };
114
+ const onAbort = () => finish(() => reject(new Error("ledger closed")));
115
+ query.signal?.addEventListener("abort", onAbort, { once: true });
116
+ worker.once("message", (message) => {
117
+ if (message.unavailable !== void 0) finish(() => resolve(void 0));
118
+ else if (message.error !== void 0) finish(() => reject(new Error(message.error)));
119
+ else if (Array.isArray(message.rows) && message.rows.length === query.statements.length) finish(() => resolve(message.rows));
120
+ else finish(() => resolve(void 0));
121
+ });
122
+ worker.once("error", () => finish(() => resolve(void 0)));
123
+ worker.once("exit", () => finish(() => resolve(void 0)));
124
+ });
125
+
60
126
  // src/storage/lcm-directory.ts
61
127
  import fs from "node:fs";
128
+ import os from "node:os";
62
129
  import path from "node:path";
63
130
  var DIRECTORY_FILE = "projects.json";
64
131
  var DIRECTORY_VERSION = 1;
65
132
  var SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
66
133
  var RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
134
+ var TEMP_RETENTION_MS = 2 * 24 * 60 * 60 * 1e3;
135
+ var tempRootCandidates = () => {
136
+ const roots = /* @__PURE__ */ new Set();
137
+ for (const root of [os.tmpdir(), "/tmp", "/var/tmp"]) {
138
+ const resolved = path.resolve(root);
139
+ roots.add(resolved);
140
+ try {
141
+ roots.add(fs.realpathSync.native(resolved));
142
+ } catch {
143
+ }
144
+ }
145
+ return [...roots];
146
+ };
147
+ var isUnderTempRoot = (target) => {
148
+ const resolved = path.resolve(target);
149
+ const candidates = /* @__PURE__ */ new Set([resolved]);
150
+ try {
151
+ candidates.add(fs.realpathSync.native(resolved));
152
+ } catch {
153
+ let probe = resolved;
154
+ while (true) {
155
+ const parent = path.dirname(probe);
156
+ if (parent === probe) break;
157
+ probe = parent;
158
+ try {
159
+ const real = fs.realpathSync.native(probe);
160
+ candidates.add(path.join(real, path.relative(probe, resolved)));
161
+ break;
162
+ } catch {
163
+ }
164
+ }
165
+ }
166
+ const normalize = (value) => process.platform === "win32" ? value.toLowerCase() : value;
167
+ const normalized = [...candidates].map(normalize);
168
+ return tempRootCandidates().some((root) => {
169
+ const base = normalize(root.endsWith(path.sep) ? root.slice(0, -1) : root);
170
+ return normalized.some((candidate) => candidate === base || candidate.startsWith(base + path.sep));
171
+ });
172
+ };
67
173
  var empty = () => ({ version: DIRECTORY_VERSION, sweptAt: 0, projects: [] });
68
174
  var directoryFile = (rootDir) => path.join(rootDir, DIRECTORY_FILE);
69
175
  var read = (rootDir) => {
@@ -112,6 +218,7 @@ var stableProjectKey = (rootDir, identity, now = Date.now()) => {
112
218
  var sweepLedgers = (rootDir, options) => {
113
219
  const now = options.now ?? Date.now();
114
220
  const retentionMs = options.retentionMs ?? RETENTION_MS;
221
+ const tempRetentionMs = options.tempRetentionMs ?? TEMP_RETENTION_MS;
115
222
  const file = read(rootDir);
116
223
  if (!options.force && now - file.sweptAt < SWEEP_INTERVAL_MS) return { removed: [], bytes: 0, skipped: true };
117
224
  const removed = [];
@@ -123,9 +230,12 @@ var sweepLedgers = (rootDir, options) => {
123
230
  try {
124
231
  stats = fs.statSync(ledger);
125
232
  } catch {
233
+ if (record2.key !== options.keepKey) continue;
234
+ kept.push(record2);
126
235
  continue;
127
236
  }
128
- const abandoned = record2.key !== options.keepKey && !fs.existsSync(record2.path) && now - Math.max(stats.mtimeMs, record2.updatedAt) >= retentionMs;
237
+ const retention = isUnderTempRoot(record2.path) ? tempRetentionMs : retentionMs;
238
+ const abandoned = record2.key !== options.keepKey && !fs.existsSync(record2.path) && now - Math.max(stats.mtimeMs, record2.updatedAt) >= retention;
129
239
  if (!abandoned) {
130
240
  kept.push(record2);
131
241
  continue;
@@ -260,6 +370,8 @@ var LcmLedger = class {
260
370
  closed = false;
261
371
  fts = false;
262
372
  startupChecksRun = false;
373
+ startupChecksPending;
374
+ closing = new AbortController();
263
375
  migrations = [];
264
376
  constructor(options = {}) {
265
377
  this.now = options.now ?? Date.now;
@@ -295,18 +407,36 @@ var LcmLedger = class {
295
407
  get startupChecksComplete() {
296
408
  return this.startupChecksRun;
297
409
  }
410
+ /** Whole-file scans run on a worker thread so a multi-gigabyte ledger never stalls the host event loop. */
298
411
  runStartupChecks() {
299
- if (this.startupChecksRun) return;
412
+ if (this.startupChecksRun) return Promise.resolve();
413
+ this.startupChecksPending ??= this.checkStartup().finally(() => {
414
+ this.startupChecksPending = void 0;
415
+ });
416
+ return this.startupChecksPending;
417
+ }
418
+ async checkStartup() {
300
419
  try {
301
- this.migrateSchema();
302
- const result = this.db.prepare("PRAGMA integrity_check").get();
303
- if (result.integrity_check !== "ok") throw new Error(String(result.integrity_check));
420
+ await this.migrateSchema();
421
+ const [result] = await this.scan(["PRAGMA integrity_check"]);
422
+ if (result?.integrity_check !== "ok") throw new Error(String(result?.integrity_check));
304
423
  } catch (error) {
305
424
  this.degraded = true;
306
425
  throw new LedgerDegradedError("ledger startup checks failed", error);
307
426
  }
308
427
  this.startupChecksRun = true;
309
428
  }
429
+ async scan(statements, snapshot = false) {
430
+ const offThread = await queryLedgerOffThread({ file: this.dbPath, statements, snapshot, signal: this.closing.signal });
431
+ if (offThread) return offThread;
432
+ this.guard();
433
+ if (snapshot && this.transactionDepth === 0) this.db.exec("BEGIN");
434
+ try {
435
+ return statements.map((sql) => ({ ...this.db.prepare(sql).get() ?? {} }));
436
+ } finally {
437
+ if (snapshot && this.transactionDepth === 0) this.db.exec("COMMIT");
438
+ }
439
+ }
310
440
  readRepairLadder(projectKey = this.project.key) {
311
441
  return this.readOnly((db) => new Map(db.prepare("SELECT fault,attempts,next_at,detail,updated_at FROM repair_ladder WHERE project_key=?").all(projectKey).map((row) => [row.fault, { attempts: Number(row.attempts), nextAt: Number(row.next_at), detail: row.detail, updatedAt: Number(row.updated_at) }])));
312
442
  }
@@ -363,15 +493,16 @@ var LcmLedger = class {
363
493
  hasObject(type, name) {
364
494
  return this.db.prepare("SELECT 1 FROM sqlite_master WHERE type=? AND name=?").get(type, name) !== void 0;
365
495
  }
366
- indexOutOfStep() {
496
+ async indexOutOfStep() {
367
497
  if (this.fts !== this.hasObject("table", "raw_entries_fts")) return true;
368
498
  if (!this.fts) return false;
369
499
  if (!this.hasObject("trigger", "raw_entries_fts_insert")) return true;
370
- return Number(this.db.prepare("SELECT count(*) n FROM raw_entries_fts").get().n) !== Number(this.db.prepare("SELECT count(*) n FROM raw_entries").get().n);
500
+ const [indexed, stored] = await this.scan(["SELECT count(*) n FROM raw_entries_fts", "SELECT count(*) n FROM raw_entries"], true);
501
+ return Number(indexed?.n) !== Number(stored?.n);
371
502
  }
372
- migrateSchema() {
503
+ async migrateSchema() {
373
504
  const version = this.schemaVersion();
374
- if (version < 2 || this.indexOutOfStep()) this.migrations.push(this.migrateFullTextIndex());
505
+ if (version < 2 || await this.indexOutOfStep()) this.migrations.push(this.migrateFullTextIndex());
375
506
  if (version < 3 || this.db.prepare("PRAGMA foreign_key_list(summary_edges)").all().length === 0) this.migrations.push(this.migrateDerivedForeignKeys());
376
507
  if (version !== LEDGER_SCHEMA_VERSION) this.db.prepare("INSERT INTO schema_metadata(key,value) VALUES('version',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(String(LEDGER_SCHEMA_VERSION));
377
508
  }
@@ -507,6 +638,20 @@ ALTER TABLE summary_node_revisions_next RENAME TO summary_node_revisions;`);
507
638
  const rows = sessionId ? this.db.prepare("SELECT * FROM raw_entries WHERE project_key=? AND session_id=? ORDER BY created_at,revision,rowid").all(projectKey, sessionId) : this.db.prepare("SELECT * FROM raw_entries WHERE project_key=? ORDER BY created_at,session_id,entry_id,revision").all(projectKey);
508
639
  return toRawEntries(rows);
509
640
  }
641
+ /** The same rows in the same order as readRaw for one session, read in keyset pages that yield between batches. */
642
+ async readSessionRaw(sessionId, slice) {
643
+ const entries = [];
644
+ let cursor;
645
+ for (; ; ) {
646
+ this.guard();
647
+ const rows = cursor ? this.db.prepare("SELECT rowid AS lcm_rowid,* FROM raw_entries WHERE project_key=? AND session_id=? AND (created_at,revision,rowid)>(?,?,?) ORDER BY created_at,revision,rowid LIMIT ?").all(this.project.key, sessionId, cursor.createdAt, cursor.revision, cursor.rowid, LCM_SCAN_BATCH) : this.db.prepare("SELECT rowid AS lcm_rowid,* FROM raw_entries WHERE project_key=? AND session_id=? ORDER BY created_at,revision,rowid LIMIT ?").all(this.project.key, sessionId, LCM_SCAN_BATCH);
648
+ for (const row of rows) entries.push(toRawEntry(row));
649
+ const last = rows.at(-1);
650
+ if (!last || rows.length < LCM_SCAN_BATCH) return entries;
651
+ cursor = { createdAt: Number(last.created_at), revision: Number(last.revision), rowid: Number(last.lcm_rowid) };
652
+ await slice.pause();
653
+ }
654
+ }
510
655
  readRawPage(projectKey = this.project.key, sessionId, offset = 0, limit = 100) {
511
656
  if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1) throw new Error("invalid raw page");
512
657
  this.guard();
@@ -729,6 +874,7 @@ ALTER TABLE summary_node_revisions_next RENAME TO summary_node_revisions;`);
729
874
  }
730
875
  close() {
731
876
  if (this.closed) return;
877
+ this.closing.abort();
732
878
  this.db.close();
733
879
  this.closed = true;
734
880
  }
@@ -738,6 +884,33 @@ ALTER TABLE summary_node_revisions_next RENAME TO summary_node_revisions;`);
738
884
  import crypto2 from "node:crypto";
739
885
  import fs3 from "node:fs";
740
886
  import path3 from "node:path";
887
+
888
+ // src/core/event-loop-slice.ts
889
+ var EVENT_LOOP_SLICE_MS = 20;
890
+ var yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
891
+ var EventLoopSlice = class {
892
+ constructor(budgetMs = EVENT_LOOP_SLICE_MS) {
893
+ this.budgetMs = budgetMs;
894
+ }
895
+ #started = performance.now();
896
+ #yields = 0;
897
+ get due() {
898
+ return performance.now() - this.#started >= this.budgetMs;
899
+ }
900
+ get yields() {
901
+ return this.#yields;
902
+ }
903
+ async yield() {
904
+ this.#yields += 1;
905
+ await yieldToEventLoop();
906
+ this.#started = performance.now();
907
+ }
908
+ async pause() {
909
+ if (this.due) await this.yield();
910
+ }
911
+ };
912
+
913
+ // src/storage/lcm-migration.ts
741
914
  var plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
742
915
  var megabytes = (bytes) => `${(bytes / 1024 ** 2).toFixed(1)} MB`;
743
916
  var migrationDropReasons = (drops) => {
@@ -879,7 +1052,8 @@ var CHECKPOINT_SCHEMA = "CREATE TABLE IF NOT EXISTS migration_checkpoints (proje
879
1052
  function reconcileSession(options) {
880
1053
  return migrateSessions({ ...options, onDemand: true });
881
1054
  }
882
- function migrateSessions(options) {
1055
+ async function migrateSessions(options) {
1056
+ const slice = options.slice ?? new EventLoopSlice();
883
1057
  const window = migrationWindow(options);
884
1058
  const maxFileBytes = positive(options.maxFileBytes, 64 * 1024 ** 2);
885
1059
  const maxLineBytes = positive(options.maxLineBytes, 8 * 1024 ** 2);
@@ -901,6 +1075,8 @@ function migrateSessions(options) {
901
1075
  const drops = result.drops;
902
1076
  const drySeen = /* @__PURE__ */ new Set();
903
1077
  files: for (const [position, file] of discovery.files.entries()) {
1078
+ await slice.pause();
1079
+ if (options.signal?.aborted) break;
904
1080
  const read2 = readSource(file, maxFileBytes, maxTotalBytes - result.bytesScanned);
905
1081
  counts.raced += read2.raced;
906
1082
  if (read2.kind !== "data") {
@@ -944,6 +1120,8 @@ function migrateSessions(options) {
944
1120
  let ordinal = 0;
945
1121
  try {
946
1122
  for (let start = 0; start < data.length; ) {
1123
+ if (slice.due) await slice.yield();
1124
+ if (options.signal?.aborted) break;
947
1125
  ordinal++;
948
1126
  const newline = data.indexOf(10, start);
949
1127
  const end = newline < 0 ? data.length : newline;
@@ -996,7 +1174,7 @@ function migrateSessions(options) {
996
1174
  if (options.ledger) ledger = options.ledger;
997
1175
  else if (options.ledgerRoot) ledger = new LcmLedger({ rootDir: options.ledgerRoot, project: { recordedCwd: cwd } });
998
1176
  else ledger = new LcmLedger({ project: { recordedCwd: cwd } });
999
- ledger.runStartupChecks();
1177
+ await ledger.runStartupChecks();
1000
1178
  database = ledger.db;
1001
1179
  database.exec(CHECKPOINT_SCHEMA);
1002
1180
  } else if (options.ledger) database = options.ledger.db;
@@ -1074,6 +1252,8 @@ function migrateSessions(options) {
1074
1252
  import crypto3 from "node:crypto";
1075
1253
  var DEFAULT_LCM_BUDGET = { calls: Number.POSITIVE_INFINITY, inputTokens: Number.POSITIVE_INFINITY, outputTokens: Number.POSITIVE_INFINITY, cost: Number.POSITIVE_INFINITY, wallMs: Number.POSITIVE_INFINITY, sessionCalls: Number.POSITIVE_INFINITY };
1076
1254
  var DEFAULT_LEAF_ENTRIES = 32;
1255
+ var NODE_SCAN_LIMIT = 1e5;
1256
+ var NODE_PAGE_SIZE = 500;
1077
1257
  var EMPTY_TAINT = /* @__PURE__ */ new Set();
1078
1258
  var DEFAULT_MAINTENANCE_CONCURRENCY = 3;
1079
1259
  var LEASE_MS = 3e4;
@@ -1161,8 +1341,7 @@ var LcmMaintenance = class {
1161
1341
  revisionsOf(nodeId) {
1162
1342
  return this.ledger.readOnly((db) => db.prepare("SELECT revision, text, model_hash, created_at FROM summary_node_revisions WHERE node_id=? AND project_key=? ORDER BY revision").all(nodeId, this.projectKey).map((row) => ({ revision: Number(row.revision), text: row.text, modelHash: row.model_hash, createdAt: Number(row.created_at) })));
1163
1343
  }
1164
- selectUpgrades(sessionId, activeSources, limit = 1) {
1165
- const all = this.listNodes(1e5);
1344
+ selectUpgrades(sessionId, activeSources, limit = 1, all = this.listNodes(NODE_SCAN_LIMIT)) {
1166
1345
  const taint = this.tainted(all);
1167
1346
  return all.filter((node) => !taint.has(node.nodeId) && node.state === "ready" && node.modelHash === "emergency" && node.policyHash === this.policyHash && (!sessionId || node.sessionId === sessionId) && (!activeSources || node.sources.every((source) => activeSources.has(this.sourceKey(source))))).sort((left, right) => left.depth - right.depth || left.nodeId.localeCompare(right.nodeId)).slice(0, limit);
1168
1347
  }
@@ -1173,7 +1352,7 @@ var LcmMaintenance = class {
1173
1352
  const order = [];
1174
1353
  while (stack.length > 0) {
1175
1354
  const current = stack.pop();
1176
- const parents = db.prepare("SELECT e.parent_id FROM summary_edges e JOIN summary_nodes parent ON parent.node_id=e.parent_id WHERE e.child_id=? AND parent.project_key=?").all(current, this.projectKey);
1355
+ const parents = db.prepare("SELECT e.parent_id FROM summary_edges e CROSS JOIN summary_nodes parent ON parent.node_id=e.parent_id WHERE e.child_id=? AND parent.project_key=?").all(current, this.projectKey);
1177
1356
  for (const { parent_id: parent } of parents) {
1178
1357
  if (seen.has(parent)) continue;
1179
1358
  seen.add(parent);
@@ -1204,6 +1383,25 @@ var LcmMaintenance = class {
1204
1383
  return node;
1205
1384
  }));
1206
1385
  }
1386
+ /** The same nodes in the same order as listNodes(NODE_SCAN_LIMIT), read in keyset pages that yield between batches. */
1387
+ async loadNodes(slice) {
1388
+ const nodes = [];
1389
+ let cursor;
1390
+ while (nodes.length < NODE_SCAN_LIMIT) {
1391
+ const limit = Math.min(NODE_PAGE_SIZE, NODE_SCAN_LIMIT - nodes.length);
1392
+ const rows = this.ledger.readOnly((db) => cursor ? db.prepare("SELECT payload,created_at,node_id FROM summary_nodes WHERE project_key=? AND (created_at,node_id)>(?,?) ORDER BY created_at,node_id LIMIT ?").all(this.projectKey, cursor.createdAt, cursor.nodeId, limit) : db.prepare("SELECT payload,created_at,node_id FROM summary_nodes WHERE project_key=? ORDER BY created_at,node_id LIMIT ?").all(this.projectKey, limit));
1393
+ for (const row of rows) {
1394
+ const node = parse(row.payload);
1395
+ if (node.projectKey !== this.projectKey) throw new Error("node project does not match ledger project");
1396
+ nodes.push(node);
1397
+ }
1398
+ const last = rows.at(-1);
1399
+ if (!last || rows.length < limit) break;
1400
+ cursor = { createdAt: last.created_at, nodeId: last.node_id };
1401
+ await slice.pause();
1402
+ }
1403
+ return nodes;
1404
+ }
1207
1405
  getNode(nodeId) {
1208
1406
  return this.ledger.readOnly((db) => {
1209
1407
  const row = db.prepare("SELECT payload FROM summary_nodes WHERE node_id=? AND project_key=?").get(nodeId, this.projectKey);
@@ -1231,7 +1429,7 @@ var LcmMaintenance = class {
1231
1429
  }
1232
1430
  /** Scoped by the live source set when given; drops a condensed child and a contained node. */
1233
1431
  getFrontier(sessionId, activeSources) {
1234
- const all = this.listNodes(1e5);
1432
+ const all = this.listNodes(NODE_SCAN_LIMIT);
1235
1433
  const taint = this.tainted(all);
1236
1434
  const nodes = all.filter((n) => !taint.has(n.nodeId) && n.state === "ready" && n.policyHash === this.policyHash && (!sessionId || activeSources !== void 0 || n.sessionId === sessionId) && (!activeSources || n.sources.every((s) => activeSources.has(this.sourceKey(s)))));
1237
1435
  const condensed = new Set(nodes.flatMap((n) => n.children));
@@ -1363,10 +1561,9 @@ var LcmMaintenance = class {
1363
1561
  return next;
1364
1562
  });
1365
1563
  }
1366
- selectLeaf(entries = this.ledger.readRaw(this.projectKey), activeSources) {
1564
+ selectLeaf(entries = this.ledger.readRaw(this.projectKey), activeSources, all = this.listNodes(NODE_SCAN_LIMIT)) {
1367
1565
  const first = entries[0];
1368
1566
  if (!first) return [];
1369
- const all = this.listNodes(1e5);
1370
1567
  const taint = this.tainted(all);
1371
1568
  const nodes = all.filter((n) => !taint.has(n.nodeId) && n.state !== "failed" && n.policyHash === this.policyHash && (n.sessionId === first.sessionId || activeSources !== void 0 && n.state === "ready") && (!activeSources || n.sources.every((s) => activeSources.has(this.sourceKey(s)))));
1372
1569
  const covered = new Set(nodes.flatMap((n) => n.sources.map((s) => this.sourceKey(s))));
@@ -1382,12 +1579,11 @@ var LcmMaintenance = class {
1382
1579
  }
1383
1580
  return picked;
1384
1581
  }
1385
- selectCondensation(sessionId, activeSources) {
1386
- const all = this.listNodes(1e5);
1582
+ selectCondensation(sessionId, activeSources, all = this.listNodes(NODE_SCAN_LIMIT)) {
1387
1583
  const taint = this.tainted(all);
1388
1584
  const nodes = all.filter((n) => !taint.has(n.nodeId) && n.state === "ready" && n.policyHash === this.policyHash && (!sessionId || n.sessionId === sessionId) && (!activeSources || n.sources.every((s) => activeSources.has(this.sourceKey(s)))));
1389
1585
  const eligible = new Set(nodes.map((n) => n.nodeId));
1390
- const edges = this.ledger.readOnly((db) => db.prepare("SELECT e.parent_id,e.child_id FROM summary_edges e JOIN summary_nodes parent ON parent.node_id=e.parent_id AND parent.project_key=? JOIN summary_nodes child ON child.node_id=e.child_id AND child.project_key=parent.project_key WHERE parent.project_key=?").all(this.projectKey, this.projectKey));
1586
+ const edges = this.ledger.readOnly((db) => db.prepare("SELECT e.parent_id,e.child_id FROM summary_edges e CROSS JOIN summary_nodes parent ON parent.node_id=e.parent_id AND parent.project_key=? CROSS JOIN summary_nodes child ON child.node_id=e.child_id AND child.project_key=parent.project_key WHERE parent.project_key=?").all(this.projectKey, this.projectKey));
1391
1587
  const consumed = new Set(edges.filter((e) => eligible.has(e.parent_id)).map((e) => e.child_id));
1392
1588
  const candidates = nodes.filter((n) => !consumed.has(n.nodeId));
1393
1589
  const first = candidates[0];
@@ -1801,8 +1997,13 @@ var LcmRuntime = class {
1801
1997
  persisted = /* @__PURE__ */ new WeakMap();
1802
1998
  persistedSessionId;
1803
1999
  lastReconciliation;
2000
+ reconciled = false;
2001
+ /** Subagent sessions share the parent's ledger; only the top-level session migrates and maintains it. */
2002
+ owner;
1804
2003
  constructor(context, options = {}) {
1805
2004
  this.context = context;
2005
+ const agent = context.agent;
2006
+ this.owner = agent?.kind !== "sub" && (agent?.depth ?? 0) === 0;
1806
2007
  this.resolveOptions = typeof options === "function" ? options : () => options;
1807
2008
  const initial = this.resolveOptions();
1808
2009
  const recorded = context.sessionManager.getRecordedCwd?.();
@@ -1824,10 +2025,13 @@ var LcmRuntime = class {
1824
2025
  });
1825
2026
  }
1826
2027
  get ready() {
1827
- return this.ledger.startupChecksComplete;
2028
+ return this.owner && this.reconciled && this.ledger.startupChecksComplete;
1828
2029
  }
1829
2030
  open() {
1830
- this.ledger.runStartupChecks();
2031
+ return this.owner ? this.ledger.runStartupChecks() : Promise.resolve();
2032
+ }
2033
+ slice() {
2034
+ return new EventLoopSlice(this.options.sliceMs);
1831
2035
  }
1832
2036
  get options() {
1833
2037
  return this.resolveOptions();
@@ -1893,39 +2097,50 @@ var LcmRuntime = class {
1893
2097
  this.frontierCache = void 0;
1894
2098
  }
1895
2099
  reconcileSelectedSession() {
1896
- if (this.closed) return Promise.resolve();
1897
- return this.enqueueWrite(() => {
1898
- const getSessionFile = this.context.sessionManager.getSessionFile;
1899
- if (typeof getSessionFile !== "function") return;
1900
- const sessionFile = getSessionFile.call(this.context.sessionManager);
1901
- if (!sessionFile) return;
1902
- const recorded = this.context.sessionManager.getRecordedCwd?.();
1903
- const cwd = recorded || this.context.cwd;
1904
- const result = reconcileSession({
1905
- agentDir: this.options.rootDir ?? process.env.PI_CODING_AGENT_DIR ?? `${process.env.HOME ?? "."}/.omp/agent`,
1906
- ledger: this.ledger,
1907
- files: [sessionFile],
1908
- liveCwd: cwd,
1909
- projectCwd: cwd,
1910
- apply: true
1911
- });
1912
- this.lastReconciliation = {
1913
- degraded: result.degraded,
1914
- errors: result.counts.errors,
1915
- raced: result.counts.raced,
1916
- absent: result.counts.absent,
1917
- drops: result.drops,
1918
- reasons: migrationDropReasons(result.drops)
1919
- };
1920
- const sessionId = this.context.sessionManager.getSessionId();
1921
- this.refreshActiveSources(sessionId, this.context.sessionManager.getBranch());
2100
+ if (this.closed || !this.owner) return Promise.resolve();
2101
+ return this.enqueueWrite(async () => {
2102
+ try {
2103
+ await this.reconcileSessionFile();
2104
+ } finally {
2105
+ this.reconciled = true;
2106
+ }
2107
+ });
2108
+ }
2109
+ async reconcileSessionFile() {
2110
+ const getSessionFile = this.context.sessionManager.getSessionFile;
2111
+ if (typeof getSessionFile !== "function") return;
2112
+ const sessionFile = getSessionFile.call(this.context.sessionManager);
2113
+ if (!sessionFile) return;
2114
+ const recorded = this.context.sessionManager.getRecordedCwd?.();
2115
+ const cwd = recorded || this.context.cwd;
2116
+ const result = await reconcileSession({
2117
+ agentDir: this.options.rootDir ?? process.env.PI_CODING_AGENT_DIR ?? `${process.env.HOME ?? "."}/.omp/agent`,
2118
+ ledger: this.ledger,
2119
+ files: [sessionFile],
2120
+ liveCwd: cwd,
2121
+ projectCwd: cwd,
2122
+ apply: true,
2123
+ slice: this.slice(),
2124
+ signal: this.signal
1922
2125
  });
2126
+ if (this.closed) return;
2127
+ this.lastReconciliation = {
2128
+ degraded: result.degraded,
2129
+ errors: result.counts.errors,
2130
+ raced: result.counts.raced,
2131
+ absent: result.counts.absent,
2132
+ drops: result.drops,
2133
+ reasons: migrationDropReasons(result.drops)
2134
+ };
2135
+ const sessionId = this.context.sessionManager.getSessionId();
2136
+ this.refreshActiveSources(sessionId, this.context.sessionManager.getBranch());
1923
2137
  }
1924
2138
  /** Appends only the entries this session has not stored yet; the rest are already addressed. */
1925
2139
  readback() {
1926
- if (this.closed) return Promise.resolve();
1927
- return this.enqueueWrite(() => {
2140
+ if (this.closed || !this.owner) return Promise.resolve();
2141
+ return this.enqueueWrite(async () => {
1928
2142
  if (this.closed) return;
2143
+ const slice = this.slice();
1929
2144
  const entries = this.context.sessionManager.getBranch();
1930
2145
  const sessionId = this.context.sessionManager.getSessionId();
1931
2146
  const recorded = this.context.sessionManager.getRecordedCwd?.();
@@ -1936,15 +2151,20 @@ var LcmRuntime = class {
1936
2151
  const active = /* @__PURE__ */ new Set();
1937
2152
  const pending = [];
1938
2153
  for (const entry of entries) {
2154
+ if (slice.due) await slice.yield();
1939
2155
  const payloadJson = canonicalLcmPayload(entry);
1940
2156
  const contentHash = hash(canonicalLcmEntry(entry));
1941
2157
  const known = this.persisted.get(entry);
1942
2158
  if (known?.contentHash === contentHash) active.add(known.key);
1943
2159
  else pending.push({ entry, payloadJson, contentHash });
1944
2160
  }
1945
- if (pending.length > 0) {
2161
+ for (let next = 0; next < pending.length; ) {
2162
+ if (slice.due) await slice.yield();
2163
+ if (this.closed) return;
2164
+ const first = next;
1946
2165
  this.ledger.transaction(() => {
1947
- for (const { entry, payloadJson, contentHash } of pending) {
2166
+ for (; next < pending.length && (next === first || !slice.due); next += 1) {
2167
+ const { entry, payloadJson, contentHash } = pending[next];
1948
2168
  const message = entry.type === "message" ? entry.message : void 0;
1949
2169
  const stored = this.ledger.appendRaw({
1950
2170
  projectKey: this.projectKey,
@@ -1976,7 +2196,7 @@ var LcmRuntime = class {
1976
2196
  });
1977
2197
  }
1978
2198
  async syncAndSchedule() {
1979
- if (this.closed) return;
2199
+ if (this.closed || !this.owner) return;
1980
2200
  const sessionId = this.context.sessionManager.getSessionId();
1981
2201
  if (this.dirty || !this.activeSessionId || this.activeSessionId !== sessionId || this.activeSources.size === 0) {
1982
2202
  await this.readback();
@@ -2022,7 +2242,7 @@ var LcmRuntime = class {
2022
2242
  }
2023
2243
  /** Reclaims ledgers whose project directory is gone; bounded to one sweep a day. */
2024
2244
  sweepLedgers() {
2025
- if (this.closed) return { removed: [], bytes: 0, skipped: true };
2245
+ if (this.closed || !this.owner) return { removed: [], bytes: 0, skipped: true };
2026
2246
  try {
2027
2247
  return sweepLedgers(this.options.rootDir ?? defaultLedgerRoot(), { keepKey: this.projectKey });
2028
2248
  } catch {
@@ -2070,7 +2290,7 @@ var LcmRuntime = class {
2070
2290
  return this.syncAndSchedule();
2071
2291
  }
2072
2292
  scheduleMaintenance() {
2073
- if (this.closed) return;
2293
+ if (this.closed || !this.owner) return;
2074
2294
  if (this.maintenanceRunning) {
2075
2295
  this.maintenanceQueued = true;
2076
2296
  return;
@@ -2135,9 +2355,13 @@ var LcmRuntime = class {
2135
2355
  const fanIn = Math.max(2, this.options.maxCondenseChildren ?? 4);
2136
2356
  const passes = Math.max(1, this.options.maxMaintenancePasses ?? 4);
2137
2357
  const runDeadline = Date.now() + Math.max(1, this.options.maintenanceRunSeconds ?? 60) * 1e3;
2138
- const raw = this.ledger.readRaw(this.projectKey, sessionId).filter(
2358
+ const slice = this.slice();
2359
+ await slice.pause();
2360
+ if (this.closed) return;
2361
+ const raw = (await this.ledger.readSessionRaw(sessionId, slice)).filter(
2139
2362
  (entry) => this.activeSources.has(this.sourceKey(entry))
2140
2363
  );
2364
+ if (this.closed) return;
2141
2365
  const inputFor = (node) => {
2142
2366
  if (node.kind === "condensed") return node.children.map((id) => this.maintenance.getNode(id)?.text ?? "").filter(Boolean).join("\n\n");
2143
2367
  return node.sources.map((source) => {
@@ -2166,6 +2390,8 @@ var LcmRuntime = class {
2166
2390
  let next = 0;
2167
2391
  const worker = async (slot) => {
2168
2392
  while (!this.closed && Date.now() < runDeadline && slot < this.maintenance.concurrencyLimit) {
2393
+ await slice.pause();
2394
+ if (this.closed) return;
2169
2395
  const item = items[next];
2170
2396
  if (!item) return;
2171
2397
  next += 1;
@@ -2188,7 +2414,10 @@ var LcmRuntime = class {
2188
2414
  const leaves = [];
2189
2415
  const seen = /* @__PURE__ */ new Set();
2190
2416
  while (leaves.length < this.maintenance.concurrencyLimit) {
2191
- const rows = this.maintenance.selectLeaf(raw, this.activeSources);
2417
+ const nodes2 = await this.maintenance.loadNodes(slice);
2418
+ await slice.pause();
2419
+ if (this.closed) return;
2420
+ const rows = this.maintenance.selectLeaf(raw, this.activeSources, nodes2);
2192
2421
  if (rows.length === 0) break;
2193
2422
  const created = this.maintenance.createLeaf(rows);
2194
2423
  const node = created ? this.maintenance.getNode(created.nodeId) : void 0;
@@ -2206,13 +2435,18 @@ var LcmRuntime = class {
2206
2435
  queued.add(job.jobId);
2207
2436
  }
2208
2437
  }
2438
+ await slice.pause();
2439
+ if (this.closed) return;
2209
2440
  const pending = this.actionableJobs(sessionId, passes);
2210
2441
  for (const item of pending) if (!queued.has(item.job.jobId)) {
2211
2442
  queued.add(item.job.jobId);
2212
2443
  batch.push(item);
2213
2444
  }
2214
2445
  await dispatch(batch);
2215
- const children = this.maintenance.selectCondensation(sessionId, this.activeSources);
2446
+ const nodes = await this.maintenance.loadNodes(slice);
2447
+ await slice.pause();
2448
+ if (this.closed) return;
2449
+ const children = this.maintenance.selectCondensation(sessionId, this.activeSources, nodes);
2216
2450
  if (children.length >= fanIn) {
2217
2451
  const node = this.maintenance.createCondensed(children);
2218
2452
  if (node) {
@@ -2221,7 +2455,10 @@ var LcmRuntime = class {
2221
2455
  }
2222
2456
  }
2223
2457
  if (leaves.length === 0 && pending.length === 0 && children.length < fanIn) {
2224
- const upgrade = this.maintenance.selectUpgrades(sessionId, this.activeSources, UPGRADE_CANDIDATES).find(fitsBound);
2458
+ const nodes2 = await this.maintenance.loadNodes(slice);
2459
+ await slice.pause();
2460
+ if (this.closed) return;
2461
+ const upgrade = this.maintenance.selectUpgrades(sessionId, this.activeSources, UPGRADE_CANDIDATES, nodes2).find(fitsBound);
2225
2462
  if (!upgrade) break;
2226
2463
  const job = this.maintenance.reopen(upgrade.nodeId);
2227
2464
  await runJob(job, upgrade);