omp-fabric 1.24.0 → 1.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/dist/chunks/{chunk-K5YUDXMV.js → chunk-3LMPHRDQ.js} +2 -2
  3. package/dist/chunks/{chunk-6DJFJLM6.js → chunk-4XJHCLTX.js} +2 -2
  4. package/dist/chunks/{chunk-RT6AUW5S.js → chunk-66PGUYJJ.js} +3 -3
  5. package/dist/chunks/{chunk-MJOGV6CR.js → chunk-AEZ7VF5S.js} +2 -2
  6. package/dist/chunks/{chunk-2DY7NDAZ.js → chunk-B7HOWHPZ.js} +2 -2
  7. package/dist/chunks/{chunk-2HOIGAHB.js → chunk-BQLRDSNL.js} +3 -3
  8. package/dist/chunks/{chunk-5P4VR2IZ.js → chunk-DRIWQ6PD.js} +4 -4
  9. package/dist/chunks/chunk-G5FBLYOY.js +895 -0
  10. package/dist/chunks/chunk-G5FBLYOY.js.map +7 -0
  11. package/dist/chunks/{chunk-5LNZXB4X.js → chunk-GE53G4VR.js} +2 -2
  12. package/dist/chunks/{chunk-QYDOKSGI.js → chunk-ICADZLLJ.js} +4 -4
  13. package/dist/chunks/{chunk-5EZWO4C5.js → chunk-IE5HUDWQ.js} +3 -3
  14. package/dist/chunks/{chunk-IH3JJDYL.js → chunk-JXNXHUIJ.js} +2 -2
  15. package/dist/chunks/{chunk-E4HNDTLM.js → chunk-MZUWSNZX.js} +2 -69
  16. package/dist/chunks/{chunk-E4HNDTLM.js.map → chunk-MZUWSNZX.js.map} +3 -3
  17. package/dist/chunks/chunk-NIWOSO22.js +74 -0
  18. package/dist/chunks/chunk-NIWOSO22.js.map +7 -0
  19. package/dist/chunks/{chunk-S3CWAHSO.js → chunk-OHPTGVDK.js} +7 -12
  20. package/dist/chunks/chunk-OHPTGVDK.js.map +7 -0
  21. package/dist/chunks/{chunk-H63XBYD3.js → chunk-ON7LE7QA.js} +6 -3
  22. package/dist/chunks/chunk-ON7LE7QA.js.map +7 -0
  23. package/dist/chunks/{chunk-7OEFSXWZ.js → chunk-QP3SVWUV.js} +6 -4
  24. package/dist/chunks/{chunk-7OEFSXWZ.js.map → chunk-QP3SVWUV.js.map} +1 -1
  25. package/dist/chunks/{chunk-I5ZKJNKL.js → chunk-SWHJQ6XH.js} +4 -4
  26. package/dist/compaction/hook.js +6 -5
  27. package/dist/compaction/lcm-maintenance.d.ts +3 -1
  28. package/dist/compaction/lcm-maintenance.d.ts.map +1 -1
  29. package/dist/compaction/lcm-runtime.d.ts +2 -2
  30. package/dist/compaction/lcm-runtime.d.ts.map +1 -1
  31. package/dist/compaction/lcm-runtime.js +23 -609
  32. package/dist/compaction/lcm-runtime.js.map +4 -4
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/core/action-registry.js +3 -3
  35. package/dist/core/omp-tools.d.ts +3 -4
  36. package/dist/core/omp-tools.d.ts.map +1 -1
  37. package/dist/core/tool-ownership.d.ts.map +1 -1
  38. package/dist/entropy/index.js +6 -6
  39. package/dist/fabric-runtime-state.js +16 -15
  40. package/dist/fabric-runtime-state.js.map +1 -1
  41. package/dist/index.js +22 -22
  42. package/dist/index.js.map +2 -2
  43. package/dist/memory/discovery.js +5 -5
  44. package/dist/memory/normalize.js +4 -4
  45. package/dist/memory/search.js +5 -5
  46. package/dist/providers/memory-provider.js +7 -7
  47. package/dist/residency/host.js +11 -10
  48. package/dist/residency/omp-entry.js +11 -10
  49. package/dist/residency/omp-entry.js.map +1 -1
  50. package/dist/runtime/core-override-guest-types.js +1 -1
  51. package/dist/runtime/node-process-runtime.js +1 -1
  52. package/dist/ui/settings.js +6 -2
  53. package/dist/ui/settings.js.map +1 -1
  54. package/docs/compaction.md +2 -0
  55. package/package.json +1 -1
  56. package/dist/chunks/chunk-CBQOS3IW.js +0 -291
  57. package/dist/chunks/chunk-CBQOS3IW.js.map +0 -7
  58. package/dist/chunks/chunk-H63XBYD3.js.map +0 -7
  59. package/dist/chunks/chunk-S3CWAHSO.js.map +0 -7
  60. /package/dist/chunks/{chunk-K5YUDXMV.js.map → chunk-3LMPHRDQ.js.map} +0 -0
  61. /package/dist/chunks/{chunk-6DJFJLM6.js.map → chunk-4XJHCLTX.js.map} +0 -0
  62. /package/dist/chunks/{chunk-RT6AUW5S.js.map → chunk-66PGUYJJ.js.map} +0 -0
  63. /package/dist/chunks/{chunk-MJOGV6CR.js.map → chunk-AEZ7VF5S.js.map} +0 -0
  64. /package/dist/chunks/{chunk-2DY7NDAZ.js.map → chunk-B7HOWHPZ.js.map} +0 -0
  65. /package/dist/chunks/{chunk-2HOIGAHB.js.map → chunk-BQLRDSNL.js.map} +0 -0
  66. /package/dist/chunks/{chunk-5P4VR2IZ.js.map → chunk-DRIWQ6PD.js.map} +0 -0
  67. /package/dist/chunks/{chunk-5LNZXB4X.js.map → chunk-GE53G4VR.js.map} +0 -0
  68. /package/dist/chunks/{chunk-QYDOKSGI.js.map → chunk-ICADZLLJ.js.map} +0 -0
  69. /package/dist/chunks/{chunk-5EZWO4C5.js.map → chunk-IE5HUDWQ.js.map} +0 -0
  70. /package/dist/chunks/{chunk-IH3JJDYL.js.map → chunk-JXNXHUIJ.js.map} +0 -0
  71. /package/dist/chunks/{chunk-I5ZKJNKL.js.map → chunk-SWHJQ6XH.js.map} +0 -0
@@ -10,16 +10,20 @@ import {
10
10
  import {
11
11
  sessionDirNamesForCwd,
12
12
  sessionsDirRoot
13
- } from "../chunks/chunk-MJOGV6CR.js";
14
- import "../chunks/chunk-5LNZXB4X.js";
13
+ } from "../chunks/chunk-AEZ7VF5S.js";
14
+ import "../chunks/chunk-GE53G4VR.js";
15
15
  import {
16
16
  DEFAULT_LCM_MAX_INPUT_CHARS,
17
+ DEFAULT_LEAF_ENTRIES,
18
+ DEFAULT_MAINTENANCE_CONCURRENCY,
19
+ LcmMaintenance,
17
20
  LcmModelAdapter,
18
21
  activeRequestBlock,
19
- buildLcmPrompt,
20
22
  emergencyReduce,
23
+ isLcmCapacityRejection,
24
+ isLcmRejection,
21
25
  tailDigest
22
- } from "../chunks/chunk-CBQOS3IW.js";
26
+ } from "../chunks/chunk-G5FBLYOY.js";
23
27
  import {
24
28
  canonicalLcmEntry,
25
29
  canonicalLcmPayload,
@@ -27,7 +31,6 @@ import {
27
31
  defaultLedgerPath,
28
32
  defaultLedgerRoot,
29
33
  hash,
30
- hashLcmPayload,
31
34
  isPrunedLcmEntry,
32
35
  lcmSummaryAddress,
33
36
  renderLcmChildAddresses,
@@ -37,17 +40,19 @@ import {
37
40
  writeJsonAtomic
38
41
  } from "../chunks/chunk-KAD7UWT2.js";
39
42
  import {
40
- LCM_RECOVERY_POINTER,
41
43
  decodeCompactionInstructions
42
- } from "../chunks/chunk-E4HNDTLM.js";
44
+ } from "../chunks/chunk-MZUWSNZX.js";
45
+ import {
46
+ LCM_RECOVERY_POINTER
47
+ } from "../chunks/chunk-NIWOSO22.js";
43
48
  import {
44
49
  MAX_SUMMARY_BYTES,
45
50
  clipUtf8,
46
51
  utf8Bytes
47
52
  } from "../chunks/chunk-3XPR2AZL.js";
48
- import "../chunks/chunk-IH3JJDYL.js";
49
- import "../chunks/chunk-K5YUDXMV.js";
50
- import "../chunks/chunk-S3CWAHSO.js";
53
+ import "../chunks/chunk-JXNXHUIJ.js";
54
+ import "../chunks/chunk-3LMPHRDQ.js";
55
+ import "../chunks/chunk-OHPTGVDK.js";
51
56
 
52
57
  // src/compaction/lcm-runtime.ts
53
58
  import fs4 from "node:fs";
@@ -219,7 +224,7 @@ var probeFts5 = (db) => {
219
224
  }
220
225
  };
221
226
  var foldToken = (value) => value.normalize("NFD").replace(new RegExp("\\p{M}+", "gu"), "").toLowerCase();
222
- var tokenize = (value) => value.split(/[^\p{L}\p{N}]+/u).map(foldToken).filter((token2) => token2.length > 0);
227
+ var tokenize = (value) => value.split(/[^\p{L}\p{N}]+/u).map(foldToken).filter((token) => token.length > 0);
223
228
  var searchPhrases = (query, mode) => (mode === "phrase" ? [query] : query.split(/\s+/)).map(tokenize).filter((phrase) => phrase.length > 0);
224
229
  var ftsExpression = (phrases, match) => phrases.map((phrase) => `"${phrase.join(" ")}"`).join(match === "all" ? " AND " : " OR ");
225
230
  var containsPhrase = (tokens, phrase) => {
@@ -675,9 +680,9 @@ ALTER TABLE summary_node_revisions_next RENAME TO summary_node_revisions;`);
675
680
  if (dangling > 0) unattributed.summary_edges = dangling;
676
681
  return { remaining, unattributed };
677
682
  }
678
- deleteProject(token2, backupManifest) {
683
+ deleteProject(token, backupManifest) {
679
684
  this.guard();
680
- if (token2.__brand !== "DeleteConfirmationToken" || token2.projectKey !== this.project.key || token2.value !== hash(`delete:${this.project.key}`)) throw new Error("invalid delete confirmation token");
685
+ if (token.__brand !== "DeleteConfirmationToken" || token.projectKey !== this.project.key || token.value !== hash(`delete:${this.project.key}`)) throw new Error("invalid delete confirmation token");
681
686
  if (backupManifest.version !== BACKUP_MANIFEST_VERSION) throw new Error(`backup manifest version ${backupManifest.version} predates this ledger (expected ${BACKUP_MANIFEST_VERSION}); take a fresh backup`);
682
687
  const backupDb = fs2.existsSync(backupManifest.destination) ? new (sqliteDriver())(backupManifest.destination) : void 0;
683
688
  let backupStateSha256;
@@ -1051,602 +1056,6 @@ function migrateSessions(options) {
1051
1056
  return result;
1052
1057
  }
1053
1058
 
1054
- // src/compaction/lcm-maintenance.ts
1055
- import crypto3 from "node:crypto";
1056
- 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 };
1057
- var DEFAULT_LEAF_ENTRIES = 32;
1058
- var EMPTY_TAINT = /* @__PURE__ */ new Set();
1059
- var DEFAULT_MAINTENANCE_CONCURRENCY = 3;
1060
- var LEASE_MS = 3e4;
1061
- var LEASE_SWEEP_GRACE_MS = 6e4;
1062
- var LcmRejection = class extends Error {
1063
- reason;
1064
- constructor(reason) {
1065
- super(reason);
1066
- this.name = "LcmRejection";
1067
- this.reason = reason;
1068
- }
1069
- };
1070
- var isLcmRejection = (error) => error instanceof LcmRejection;
1071
- var isLcmCapacityRejection = (error) => isLcmRejection(error) && (error.reason === "project lease held" || error.reason === "budget exhausted");
1072
- var LEGACY_CONTENTION_RETIREMENTS = /* @__PURE__ */ new Set(["job lease held", "project lease held", "LcmRejection: job lease held", "LcmRejection: project lease held"]);
1073
- var hash3 = (v) => hashLcmPayload(v);
1074
- var parse = (v) => JSON.parse(String(v));
1075
- var rawHash = (e) => e.payloadHash;
1076
- var token = () => crypto3.randomUUID();
1077
- var LCM_MODEL_LEVELS = [{ mode: "detail", share: 1 }, { mode: "bullets", share: 0.5 }];
1078
- var CLAIMABLE_JOBS_SQL = `SELECT j.payload FROM maintenance_jobs j
1079
- JOIN summary_nodes n ON n.node_id=json_extract(j.payload,'$.nodeId') AND n.project_key=j.project_key
1080
- WHERE j.project_key=?
1081
- AND (j.status='pending' OR (j.status='running' AND CAST(coalesce(json_extract(j.payload,'$.leaseUntil'),0) AS INTEGER)<=?))
1082
- AND CAST(json_extract(j.payload,'$.eligibleAt') AS INTEGER)<=?
1083
- AND CAST(json_extract(j.payload,'$.nextRetryAt') AS INTEGER)<=?
1084
- AND (? IS NULL OR json_extract(n.payload,'$.sessionId')=?)
1085
- ORDER BY CAST(json_extract(j.payload,'$.priority') AS INTEGER) DESC, j.created_at, j.job_id
1086
- LIMIT ?`;
1087
- var LIVE_LEASES_SQL = `SELECT coalesce(json_extract(n.payload,'$.sessionId'),'') sessionId
1088
- FROM maintenance_jobs j
1089
- LEFT JOIN summary_nodes n ON n.node_id=json_extract(j.payload,'$.nodeId') AND n.project_key=j.project_key
1090
- WHERE j.project_key=? AND j.status='running' AND j.job_id<>? AND CAST(coalesce(json_extract(j.payload,'$.leaseUntil'),0) AS INTEGER)>?`;
1091
- var LcmMaintenance = class {
1092
- constructor(ledger, options = {}) {
1093
- this.ledger = ledger;
1094
- this.maxConcurrentJobs = options.maxConcurrentJobs ?? DEFAULT_MAINTENANCE_CONCURRENCY;
1095
- this.projectKey = ledger.project.key;
1096
- this.now = options.now ?? Date.now;
1097
- this.ownerId = options.ownerId ?? crypto3.randomUUID();
1098
- this.policyHash = options.policyHash ?? hash3("lcm-policy-v1");
1099
- this.maxLeaf = options.maxLeafEntries ?? DEFAULT_LEAF_ENTRIES;
1100
- this.maxChildren = options.maxCondenseChildren ?? 4;
1101
- this.maxInputChars = options.maxInputChars ?? 2e5;
1102
- this.maxOutputChars = options.maxOutputChars ?? 4096;
1103
- this.modelTimeoutMs = options.modelTimeoutMs ?? 12e4;
1104
- this.budget = { ...DEFAULT_LCM_BUDGET, ...options.budget };
1105
- }
1106
- projectKey;
1107
- now;
1108
- ownerId;
1109
- policyHash;
1110
- maxLeaf;
1111
- maxChildren;
1112
- maxInputChars;
1113
- maxOutputChars;
1114
- budget;
1115
- modelTimeoutMs;
1116
- maxConcurrentJobs;
1117
- budgetPolicy() {
1118
- return { ...this.budget };
1119
- }
1120
- get concurrencyLimit() {
1121
- const raw = typeof this.maxConcurrentJobs === "function" ? this.maxConcurrentJobs() : this.maxConcurrentJobs;
1122
- return Number.isFinite(raw) ? Math.max(1, Math.floor(raw)) : DEFAULT_MAINTENANCE_CONCURRENCY;
1123
- }
1124
- liveLeases(db, at, excludeJobId) {
1125
- return db.prepare(LIVE_LEASES_SQL).all(this.projectKey, excludeJobId ?? "", at - LEASE_SWEEP_GRACE_MS);
1126
- }
1127
- admits(db, at, sessionId, leases) {
1128
- const day = this.day(at);
1129
- const project = this.usage(db, day);
1130
- const session = this.usage(db, day, sessionId);
1131
- let sessionReserved = 0;
1132
- for (const lease of leases) if (lease.sessionId === sessionId) sessionReserved += 1;
1133
- const reserved = leases.length + 1;
1134
- const fits = (used, cap) => used < cap && used + (project.calls > 0 ? used / project.calls * reserved : 0) <= cap;
1135
- return project.calls + leases.length < this.budget.calls && fits(project.inputTokens, this.budget.inputTokens) && fits(project.outputTokens, this.budget.outputTokens) && fits(project.cost, this.budget.cost) && fits(project.wallMs, this.budget.wallMs) && session.calls + sessionReserved < this.budget.sessionCalls;
1136
- }
1137
- recordRevision(db, node) {
1138
- if (!node.text) return;
1139
- const row = db.prepare("SELECT coalesce(max(revision),0) n FROM summary_node_revisions WHERE node_id=?").get(node.nodeId);
1140
- db.prepare("INSERT OR IGNORE INTO summary_node_revisions(node_id,revision,project_key,text,model_hash,created_at) VALUES(?,?,?,?,?,?)").run(node.nodeId, Number(row.n) + 1, this.projectKey, node.text, node.modelHash, this.now());
1141
- }
1142
- revisionsOf(nodeId) {
1143
- 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) })));
1144
- }
1145
- selectUpgrades(sessionId, activeSources, limit = 1) {
1146
- return this.listNodes(1e5).filter((node) => 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);
1147
- }
1148
- ancestorsOf(nodeId) {
1149
- return this.ledger.readOnly((db) => {
1150
- const seen = /* @__PURE__ */ new Set();
1151
- const stack = [nodeId];
1152
- const order = [];
1153
- while (stack.length > 0) {
1154
- const current = stack.pop();
1155
- 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);
1156
- for (const { parent_id: parent } of parents) {
1157
- if (seen.has(parent)) continue;
1158
- seen.add(parent);
1159
- order.push(parent);
1160
- stack.push(parent);
1161
- }
1162
- }
1163
- return order;
1164
- });
1165
- }
1166
- reopen(nodeId, force = false) {
1167
- return this.ledger.transaction((db) => {
1168
- const row = db.prepare("SELECT payload FROM summary_nodes WHERE node_id=? AND project_key=?").get(nodeId, this.projectKey);
1169
- if (!row?.payload) throw new Error("node not found");
1170
- const node = parse(row.payload);
1171
- if (node.projectKey !== this.projectKey) throw new Error("node project does not match ledger project");
1172
- if (!force && node.modelHash !== "emergency") throw new Error("node already carries a model summary");
1173
- const t = this.now();
1174
- const job = { ...this.job(node), jobId: `job:upgrade:${token()}:${node.nodeId}`, createdAt: t, updatedAt: t };
1175
- db.prepare("INSERT INTO maintenance_jobs(job_id,project_key,status,payload,created_at,updated_at) VALUES(?,?,?,?,?,?)").run(job.jobId, this.projectKey, job.state, JSON.stringify(job), job.createdAt, job.updatedAt);
1176
- return job;
1177
- });
1178
- }
1179
- listNodes(limit = 100, offset = 0) {
1180
- return this.ledger.readOnly((db) => db.prepare("SELECT payload FROM summary_nodes WHERE project_key=? ORDER BY created_at,node_id LIMIT ? OFFSET ?").all(this.projectKey, limit, offset).map((r) => {
1181
- const node = parse(r.payload);
1182
- if (node.projectKey !== this.projectKey) throw new Error("node project does not match ledger project");
1183
- return node;
1184
- }));
1185
- }
1186
- getNode(nodeId) {
1187
- return this.ledger.readOnly((db) => {
1188
- const row = db.prepare("SELECT payload FROM summary_nodes WHERE node_id=? AND project_key=?").get(nodeId, this.projectKey);
1189
- if (!row?.payload) return void 0;
1190
- const node = parse(row.payload);
1191
- if (node.projectKey !== this.projectKey) throw new Error("node project does not match ledger project");
1192
- return node;
1193
- });
1194
- }
1195
- /** Scoped by the live source set when given; drops a condensed child and a contained node. */
1196
- /** The oversized deterministic leaves and every ancestor that inherited one. */
1197
- tainted(nodes) {
1198
- const oversized = nodes.filter((node) => node.kind === "leaf" && node.modelHash === "emergency" && node.sources.length > this.maxLeaf).map((node) => node.nodeId);
1199
- if (oversized.length === 0) return EMPTY_TAINT;
1200
- const parents = /* @__PURE__ */ new Map();
1201
- for (const node of nodes) for (const child of node.children) parents.set(child, [...parents.get(child) ?? [], node.nodeId]);
1202
- const excluded = /* @__PURE__ */ new Set();
1203
- const queue = [...oversized];
1204
- while (queue.length > 0) {
1205
- const nodeId = queue.pop();
1206
- if (excluded.has(nodeId)) continue;
1207
- excluded.add(nodeId);
1208
- for (const parent of parents.get(nodeId) ?? []) queue.push(parent);
1209
- }
1210
- return excluded;
1211
- }
1212
- getFrontier(sessionId, activeSources) {
1213
- const all = this.listNodes(1e5);
1214
- const taint = this.tainted(all);
1215
- 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)))));
1216
- const condensed = new Set(nodes.flatMap((n) => n.children));
1217
- const standing = nodes.filter((n) => !condensed.has(n.nodeId));
1218
- const ranked = [...standing].sort((a, b) => b.sources.length - a.sources.length || a.nodeId.localeCompare(b.nodeId));
1219
- const kept = [];
1220
- const shadowed = /* @__PURE__ */ new Set();
1221
- for (const node of ranked) {
1222
- const keys = node.sources.map((s) => this.sourceKey(s));
1223
- if (kept.some((other) => {
1224
- const held = new Set(other.sources.map((s) => this.sourceKey(s)));
1225
- return keys.every((key) => held.has(key));
1226
- })) {
1227
- shadowed.add(node.nodeId);
1228
- continue;
1229
- }
1230
- kept.push(node);
1231
- }
1232
- return standing.filter((n) => !shadowed.has(n.nodeId));
1233
- }
1234
- recentJobs(limit = 200) {
1235
- return this.ledger.readOnly((db) => db.prepare("SELECT payload FROM maintenance_jobs WHERE project_key=? ORDER BY CASE status WHEN 'failed' THEN 0 WHEN 'running' THEN 1 WHEN 'pending' THEN 2 ELSE 3 END, updated_at DESC LIMIT ?").all(this.projectKey, limit).map((r) => {
1236
- const job = parse(r.payload);
1237
- if (job.projectKey !== this.projectKey) throw new Error("job project does not match ledger project");
1238
- return job;
1239
- }));
1240
- }
1241
- listJobs(limit = 100) {
1242
- return this.ledger.readOnly((db) => db.prepare("SELECT payload FROM maintenance_jobs WHERE project_key=? ORDER BY created_at,job_id LIMIT ?").all(this.projectKey, limit).map((r) => {
1243
- const job = parse(r.payload);
1244
- if (job.projectKey !== this.projectKey) throw new Error("job project does not match ledger project");
1245
- return job;
1246
- }));
1247
- }
1248
- jobForNode(nodeId) {
1249
- return this.ledger.readOnly((db) => {
1250
- const row = db.prepare("SELECT payload FROM maintenance_jobs WHERE job_id=? AND project_key=?").get(`job:${nodeId}`, this.projectKey);
1251
- if (!row?.payload) return void 0;
1252
- const job = parse(row.payload);
1253
- if (job.projectKey !== this.projectKey) throw new Error("job project does not match ledger project");
1254
- return job;
1255
- });
1256
- }
1257
- claimableJobs(sessionId, limit = 64) {
1258
- const t = this.now();
1259
- const scope = sessionId ?? null;
1260
- return this.ledger.readOnly((db) => db.prepare(CLAIMABLE_JOBS_SQL).all(this.projectKey, t - LEASE_SWEEP_GRACE_MS, t, t, scope, scope, limit).map((r) => {
1261
- const job = parse(r.payload);
1262
- if (job.projectKey !== this.projectKey) throw new Error("job project does not match ledger project");
1263
- return job;
1264
- }));
1265
- }
1266
- countJobs(state, updatedSince = 0) {
1267
- return this.ledger.readOnly((db) => Number(db.prepare("SELECT count(*) n FROM maintenance_jobs WHERE project_key=? AND status=? AND updated_at>=?").get(this.projectKey, state, updatedSince).n));
1268
- }
1269
- isClaimable(job) {
1270
- const t = this.now();
1271
- return (job.state === "pending" || job.state === "running" && (job.leaseUntil ?? 0) + LEASE_SWEEP_GRACE_MS <= t) && job.eligibleAt <= t && job.nextRetryAt <= t;
1272
- }
1273
- sweepExpiredLeases() {
1274
- return this.ledger.transaction((db) => {
1275
- const t = this.now() - LEASE_SWEEP_GRACE_MS;
1276
- const rows = db.prepare("SELECT payload FROM maintenance_jobs WHERE project_key=? AND status=?").all(this.projectKey, "running");
1277
- const swept = [];
1278
- for (const row of rows) {
1279
- const old = parse(row.payload);
1280
- if (old.projectKey !== this.projectKey) throw new Error("job project does not match ledger project");
1281
- if ((old.leaseUntil ?? 0) > t) continue;
1282
- const now = this.now();
1283
- const { ownerId: _ownerId, leaseToken: _leaseToken, leaseUntil: _leaseUntil, ...base } = old;
1284
- const job = { ...base, state: "pending", error: "lease expired", eligibleAt: now, nextRetryAt: now, updatedAt: now };
1285
- db.prepare("UPDATE maintenance_jobs SET status=?,payload=?,updated_at=? WHERE job_id=? AND project_key=?").run(job.state, JSON.stringify(job), job.updatedAt, job.jobId, this.projectKey);
1286
- swept.push(job);
1287
- }
1288
- return swept;
1289
- });
1290
- }
1291
- expiredLeases() {
1292
- const t = this.now() - LEASE_SWEEP_GRACE_MS;
1293
- return this.ledger.readOnly((db) => Number(db.prepare("SELECT count(*) n FROM maintenance_jobs WHERE project_key=? AND status=? AND coalesce(json_extract(payload,'$.leaseUntil'),0)<=?").get(this.projectKey, "running", t).n));
1294
- }
1295
- retryFailedJobs() {
1296
- return this.ledger.transaction((db) => {
1297
- const rows = db.prepare("SELECT payload FROM maintenance_jobs WHERE project_key=? AND status=?").all(this.projectKey, "failed");
1298
- const retried = [];
1299
- for (const row of rows) {
1300
- const old = parse(row.payload);
1301
- if (old.projectKey !== this.projectKey) throw new Error("job project does not match ledger project");
1302
- const t = this.now();
1303
- const { error: _error, ownerId: _ownerId, leaseToken: _leaseToken, leaseUntil: _leaseUntil, ...base } = old;
1304
- const job = { ...base, state: "pending", attempts: 0, eligibleAt: t, nextRetryAt: t, updatedAt: t };
1305
- db.prepare("UPDATE maintenance_jobs SET status=?,payload=?,updated_at=? WHERE job_id=? AND project_key=?").run(job.state, JSON.stringify(job), job.updatedAt, job.jobId, this.projectKey);
1306
- retried.push(job);
1307
- }
1308
- return retried;
1309
- });
1310
- }
1311
- recoverLegacyContentionRetirements() {
1312
- return this.ledger.transaction((db) => {
1313
- const rows = db.prepare("SELECT payload FROM maintenance_jobs WHERE project_key=? AND status=?").all(this.projectKey, "failed");
1314
- const recovered = [];
1315
- for (const row of rows) {
1316
- const old = parse(row.payload);
1317
- if (old.projectKey !== this.projectKey) throw new Error("job project does not match ledger project");
1318
- if (old.legacyRecovery || !LEGACY_CONTENTION_RETIREMENTS.has(old.error ?? "")) continue;
1319
- const t = this.now();
1320
- const { error: _error, ...base } = old;
1321
- const job = { ...base, state: "pending", attempts: 0, legacyRecovery: true, eligibleAt: t, nextRetryAt: t, updatedAt: t };
1322
- db.prepare("UPDATE maintenance_jobs SET status=?,payload=?,updated_at=? WHERE job_id=? AND project_key=?").run(job.state, JSON.stringify(job), job.updatedAt, job.jobId, this.projectKey);
1323
- recovered.push(job);
1324
- }
1325
- return recovered;
1326
- });
1327
- }
1328
- recordFailure(job, error) {
1329
- return this.ledger.transaction((db) => {
1330
- const row = db.prepare("SELECT payload FROM maintenance_jobs WHERE job_id=? AND project_key=?").get(job.jobId, this.projectKey);
1331
- if (!row?.payload) return void 0;
1332
- const old = parse(row.payload);
1333
- if (old.projectKey !== this.projectKey) throw new Error("job project does not match ledger project");
1334
- const t = this.now();
1335
- if (old.state === "completed" || old.attempts !== job.attempts) return old;
1336
- if (old.state === "running" && (old.leaseUntil ?? 0) > t && old.leaseToken !== job.leaseToken) return old;
1337
- const attempts = old.attempts + 1;
1338
- const delay = Math.min(9e5, 3e4 * 2 ** (attempts - 1));
1339
- const { ownerId: _ownerId, leaseToken: _leaseToken, leaseUntil: _leaseUntil, ...base } = old;
1340
- const next = { ...base, attempts, error: String(error), state: attempts >= 3 ? "failed" : "pending", eligibleAt: t + delay, nextRetryAt: t + delay, updatedAt: t };
1341
- db.prepare("UPDATE maintenance_jobs SET status=?,payload=?,updated_at=? WHERE job_id=? AND project_key=?").run(next.state, JSON.stringify(next), next.updatedAt, next.jobId, this.projectKey);
1342
- return next;
1343
- });
1344
- }
1345
- selectLeaf(entries = this.ledger.readRaw(this.projectKey), activeSources) {
1346
- const first = entries[0];
1347
- if (!first) return [];
1348
- const all = this.listNodes(1e5);
1349
- const taint = this.tainted(all);
1350
- 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)))));
1351
- const covered = new Set(nodes.flatMap((n) => n.sources.map((s) => this.sourceKey(s))));
1352
- const candidates = entries.filter((e) => e.sessionId === first.sessionId && !covered.has(this.sourceKey(e)));
1353
- const picked = [];
1354
- let chars = 0;
1355
- for (const entry of candidates) {
1356
- if (picked.length >= this.maxLeaf) break;
1357
- const size = entry.payloadJson.length;
1358
- if (picked.length > 0 && chars + size > this.maxInputChars) break;
1359
- picked.push(entry);
1360
- chars += size;
1361
- }
1362
- return picked;
1363
- }
1364
- selectCondensation(sessionId, activeSources) {
1365
- const all = this.listNodes(1e5);
1366
- const taint = this.tainted(all);
1367
- 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)))));
1368
- const eligible = new Set(nodes.map((n) => n.nodeId));
1369
- 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));
1370
- const consumed = new Set(edges.filter((e) => eligible.has(e.parent_id)).map((e) => e.child_id));
1371
- const candidates = nodes.filter((n) => !consumed.has(n.nodeId));
1372
- const first = candidates[0];
1373
- if (!first) return [];
1374
- const depth = Math.min(...candidates.map((n) => n.depth));
1375
- const sameDepth = candidates.filter((n) => n.depth === depth);
1376
- if (depth > 0 && sameDepth.length < 2) return [];
1377
- return candidates.filter((n) => n.projectKey === first.projectKey && n.sessionId === first.sessionId && n.depth === depth).sort((a, b) => a.nodeId.localeCompare(b.nodeId)).slice(0, this.maxChildren);
1378
- }
1379
- createLeaf(entries) {
1380
- if (!entries.length) return void 0;
1381
- const source = entries.map((e, index) => {
1382
- if (e.projectKey !== this.projectKey) throw new Error("entry project does not match ledger project");
1383
- const payload = parse(e.payloadJson);
1384
- if (typeof payload.type !== "string" || !payload.type || payload.id !== e.entryId) throw new Error("invalid raw payload");
1385
- if (hash3(payload) !== rawHash(e)) throw new Error("payload hash mismatch");
1386
- return { sessionId: e.sessionId, entryId: e.entryId, revision: e.revision, payloadHash: rawHash(e) };
1387
- });
1388
- this.validateSources(source);
1389
- const node = { nodeId: `leaf:${hash3({ projectKey: this.projectKey, source, policyHash: this.policyHash })}`, projectKey: this.projectKey, sessionId: entries[0].sessionId, kind: "leaf", sources: source, children: [], depth: 0, sourceHash: hash3(source.map((s) => s.payloadHash)), policyHash: this.policyHash, modelHash: "", state: "pending", createdAt: this.now() };
1390
- this.publish(node);
1391
- return node;
1392
- }
1393
- createCondensed(children) {
1394
- if (!children.length) return void 0;
1395
- if (children.some((c) => c.state !== "ready")) throw new Error("condensation requires ready children");
1396
- if (new Set(children.map((c) => c.sessionId)).size !== 1 || new Set(children.map((c) => c.depth)).size !== 1) throw new Error("mixed session or depth");
1397
- const storedChildren = children.map((child) => this.getNode(child.nodeId));
1398
- if (storedChildren.some((child) => !child || child.projectKey !== this.projectKey)) throw new Error("child project does not match ledger project");
1399
- if (storedChildren.some((child, index) => JSON.stringify(child) !== JSON.stringify(children[index]))) throw new Error("child payload changed");
1400
- const source = [...new Map(storedChildren.flatMap((c) => c.sources).map((value) => [this.sourceKey(value), value])).values()];
1401
- this.validateSources(source);
1402
- const node = { nodeId: `condensed:${hash3({ projectKey: this.projectKey, children: storedChildren.map((c) => c.nodeId), policyHash: this.policyHash })}`, projectKey: this.projectKey, sessionId: children[0].sessionId, kind: "condensed", sources: source, children: children.map((c) => c.nodeId), depth: Math.max(...children.map((c) => c.depth)) + 1, sourceHash: hash3(children.map((c) => c.sourceHash)), policyHash: this.policyHash, modelHash: "", state: "pending", createdAt: this.now() };
1403
- this.publish(node);
1404
- return node;
1405
- }
1406
- validateSources(source) {
1407
- const seen = /* @__PURE__ */ new Set();
1408
- for (const s of source) {
1409
- if (seen.has(this.sourceKey(s))) throw new Error("duplicate source identity");
1410
- seen.add(this.sourceKey(s));
1411
- }
1412
- if (new Set(source.map((s) => s.sessionId)).size !== 1) throw new Error("cross-session ranges");
1413
- }
1414
- sourceKey(source) {
1415
- return `${source.entryId}:${source.payloadHash}`;
1416
- }
1417
- publish(node) {
1418
- this.ledger.transaction((db) => {
1419
- if (node.children.includes(node.nodeId)) throw new Error("cycle");
1420
- for (const child of node.children) if (!db.prepare("SELECT 1 FROM summary_nodes WHERE node_id=? AND project_key=?").get(child, this.projectKey)) throw new Error("missing child");
1421
- db.prepare("INSERT OR IGNORE INTO summary_nodes(node_id,project_key,payload,created_at) VALUES(?,?,?,?)").run(node.nodeId, this.projectKey, JSON.stringify(node), node.createdAt);
1422
- for (const child of node.children) db.prepare("INSERT OR IGNORE INTO summary_edges(parent_id,child_id) VALUES(?,?)").run(node.nodeId, child);
1423
- const job = this.job(node);
1424
- db.prepare("INSERT OR IGNORE INTO maintenance_jobs(job_id,project_key,status,payload,created_at,updated_at) VALUES(?,?,?,?,?,?)").run(job.jobId, this.projectKey, job.state, JSON.stringify(job), job.createdAt, job.updatedAt);
1425
- });
1426
- }
1427
- job(node) {
1428
- const t = this.now();
1429
- return { jobId: `job:${node.nodeId}`, projectKey: this.projectKey, nodeId: node.nodeId, priority: node.kind === "condensed" ? 20 : 10, eligibleAt: t, state: "pending", attempts: 0, nextRetryAt: t, createdAt: t, updatedAt: t };
1430
- }
1431
- claim(jobId, ownerId = this.ownerId) {
1432
- return this.ledger.transaction((db) => {
1433
- const row = db.prepare("SELECT payload FROM maintenance_jobs WHERE job_id=? AND project_key=?").get(jobId, this.projectKey);
1434
- if (!row?.payload) throw new Error("job not found");
1435
- const old = parse(row.payload);
1436
- if (old.projectKey !== this.projectKey) throw new Error("job project does not match ledger project");
1437
- const t = this.now();
1438
- const expired = old.state === "running" && (old.leaseUntil ?? 0) + LEASE_SWEEP_GRACE_MS <= t;
1439
- if (old.state === "running" && !expired) throw new LcmRejection("job lease held");
1440
- if (old.state !== "pending" && !expired || old.eligibleAt > t || old.nextRetryAt > t) throw new LcmRejection("job not eligible");
1441
- const leases = this.liveLeases(db, t, old.jobId);
1442
- if (leases.length >= this.concurrencyLimit) throw new LcmRejection("project lease held");
1443
- const nodeRow = db.prepare("SELECT payload FROM summary_nodes WHERE node_id=? AND project_key=?").get(old.nodeId, this.projectKey);
1444
- const sessionId = nodeRow?.payload ? parse(nodeRow.payload).sessionId : "";
1445
- if (!this.admits(db, t, sessionId, leases)) throw new LcmRejection("budget exhausted");
1446
- const out = { ...old, state: "running", ownerId, leaseToken: token(), leaseUntil: t + LEASE_MS, updatedAt: t };
1447
- db.prepare("UPDATE maintenance_jobs SET status=?,payload=?,updated_at=? WHERE job_id=? AND project_key=?").run(out.state, JSON.stringify(out), t, jobId, this.projectKey);
1448
- return out;
1449
- });
1450
- }
1451
- claimEmergency(jobId, ownerId = this.ownerId) {
1452
- return this.ledger.transaction((db) => {
1453
- const row = db.prepare("SELECT payload FROM maintenance_jobs WHERE job_id=? AND project_key=?").get(jobId, this.projectKey);
1454
- if (!row?.payload) throw new Error("job not found");
1455
- const old = parse(row.payload);
1456
- if (old.projectKey !== this.projectKey) throw new Error("job project does not match ledger project");
1457
- const t = this.now();
1458
- const expired = old.state === "running" && (old.leaseUntil ?? 0) <= t;
1459
- if (old.state === "running" && !expired) throw new LcmRejection("job lease held");
1460
- if (old.state === "completed") throw new Error("job already completed");
1461
- const out = { ...old, state: "running", ownerId, leaseToken: token(), leaseUntil: t + LEASE_MS, updatedAt: t };
1462
- db.prepare("UPDATE maintenance_jobs SET status=?,payload=?,updated_at=? WHERE job_id=? AND project_key=?").run(out.state, JSON.stringify(out), t, jobId, this.projectKey);
1463
- return out;
1464
- });
1465
- }
1466
- renew(job) {
1467
- return this.fenced(job, (old) => ({ ...old, leaseUntil: this.now() + LEASE_MS, updatedAt: this.now() }));
1468
- }
1469
- validateText(text) {
1470
- if (typeof text !== "string" || text.length === 0) throw new Error("invalid summary text");
1471
- let length = 0;
1472
- for (const _ of text) {
1473
- length += 1;
1474
- if (length > this.maxOutputChars) throw new Error("summary exceeds output bound");
1475
- }
1476
- }
1477
- validateShrink(text, inputBytes) {
1478
- if (!Number.isSafeInteger(inputBytes) || inputBytes < 0) throw new Error("invalid input bound");
1479
- if (utf8Bytes(text) >= inputBytes) throw new Error("summary does not shrink its input");
1480
- }
1481
- withinBudget(sessionId, excludeJobId) {
1482
- return this.ledger.readOnly((db) => {
1483
- const at = this.now();
1484
- return this.admits(db, at, sessionId, this.liveLeases(db, at, excludeJobId));
1485
- });
1486
- }
1487
- accountRejected(sessionId, result) {
1488
- if (![result.inputTokens, result.outputTokens, result.cost, result.wallMs].every((value) => Number.isFinite(value) && value >= 0)) return;
1489
- try {
1490
- this.ledger.transaction((db) => this.account(db, sessionId, result));
1491
- } catch {
1492
- }
1493
- }
1494
- complete(job, result, inputBytes) {
1495
- this.validateText(result.text);
1496
- this.validateShrink(result.text, inputBytes);
1497
- return this.ledger.transaction((db) => {
1498
- const current = this.readJob(db, job.jobId);
1499
- this.assertLease(current, job);
1500
- if (current.nodeId !== job.nodeId) throw new Error("job node mismatch");
1501
- const row = db.prepare("SELECT payload FROM summary_nodes WHERE node_id=? AND project_key=?").get(job.nodeId, this.projectKey);
1502
- if (!row?.payload) throw new Error("node not found");
1503
- const node = parse(row.payload);
1504
- if (node.projectKey !== this.projectKey || node.nodeId !== current.nodeId) throw new Error("node project does not match ledger project");
1505
- for (const source of node.sources) {
1506
- const raw = db.prepare("SELECT payload_json,session_id FROM raw_entries WHERE project_key=? AND session_id=? AND entry_id=? AND revision=?").get(this.projectKey, source.sessionId, source.entryId, source.revision);
1507
- if (!raw?.payload_json || hash3(parse(raw.payload_json)) !== source.payloadHash || raw.session_id !== source.sessionId) throw new Error("stale source");
1508
- }
1509
- if (![result.inputTokens, result.outputTokens, result.cost, result.wallMs].every((value) => Number.isFinite(value) && value >= 0)) throw new Error("invalid model usage");
1510
- const projectUsage = this.usage(db, this.day(this.now()));
1511
- const sessionUsage = this.usage(db, this.day(this.now()), node.sessionId);
1512
- if (projectUsage.calls + 1 > this.budget.calls || projectUsage.inputTokens + result.inputTokens > this.budget.inputTokens || projectUsage.outputTokens + result.outputTokens > this.budget.outputTokens || projectUsage.cost + result.cost > this.budget.cost || projectUsage.wallMs + result.wallMs > this.budget.wallMs || sessionUsage.calls + 1 > this.budget.sessionCalls) throw new Error("budget exhausted");
1513
- this.recordRevision(db, node);
1514
- const out = { ...node, state: "ready", text: result.text, modelHash: result.modelHash };
1515
- this.account(db, node.sessionId, result);
1516
- db.prepare("UPDATE summary_nodes SET payload=? WHERE node_id=? AND project_key=?").run(JSON.stringify(out), node.nodeId, this.projectKey);
1517
- const { ownerId: _ownerId, leaseToken: _leaseToken, leaseUntil: _leaseUntil, ...base } = current;
1518
- const done = { ...base, state: "completed", updatedAt: this.now() };
1519
- db.prepare("UPDATE maintenance_jobs SET status=?,payload=?,updated_at=? WHERE job_id=? AND project_key=?").run(done.state, JSON.stringify(done), done.updatedAt, done.jobId, this.projectKey);
1520
- return out;
1521
- });
1522
- }
1523
- completeEmergency(job, text) {
1524
- this.validateText(text);
1525
- return this.ledger.transaction((db) => {
1526
- const current = this.readJob(db, job.jobId);
1527
- this.assertLease(current, job);
1528
- if (current.nodeId !== job.nodeId) throw new Error("job node mismatch");
1529
- const row = db.prepare("SELECT payload FROM summary_nodes WHERE node_id=? AND project_key=?").get(job.nodeId, this.projectKey);
1530
- if (!row?.payload) throw new Error("node not found");
1531
- const node = parse(row.payload);
1532
- if (node.projectKey !== this.projectKey || node.nodeId !== current.nodeId) throw new Error("node project does not match ledger project");
1533
- for (const source of node.sources) {
1534
- const raw = db.prepare("SELECT payload_json,session_id FROM raw_entries WHERE project_key=? AND session_id=? AND entry_id=? AND revision=?").get(this.projectKey, source.sessionId, source.entryId, source.revision);
1535
- if (!raw?.payload_json || hash3(parse(raw.payload_json)) !== source.payloadHash || raw.session_id !== source.sessionId) throw new Error("stale source");
1536
- }
1537
- this.recordRevision(db, node);
1538
- const out = { ...node, state: "ready", text, modelHash: "emergency" };
1539
- db.prepare("UPDATE summary_nodes SET payload=? WHERE node_id=? AND project_key=?").run(JSON.stringify(out), node.nodeId, this.projectKey);
1540
- const { ownerId: _ownerId, leaseToken: _leaseToken, leaseUntil: _leaseUntil, ...base } = current;
1541
- const done = { ...base, state: "completed", updatedAt: this.now() };
1542
- db.prepare("UPDATE maintenance_jobs SET status=?,payload=?,updated_at=? WHERE job_id=? AND project_key=?").run(done.state, JSON.stringify(done), done.updatedAt, done.jobId, this.projectKey);
1543
- return out;
1544
- });
1545
- }
1546
- fail(job, error) {
1547
- return this.fenced(job, (old) => {
1548
- const attempts = old.attempts + 1;
1549
- const delay = Math.min(9e5, 3e4 * 2 ** (attempts - 1));
1550
- const { ownerId: _ownerId, leaseToken: _leaseToken, leaseUntil: _leaseUntil, ...base } = old;
1551
- return { ...base, attempts, error, state: attempts >= 3 ? "failed" : "pending", eligibleAt: this.now() + delay, nextRetryAt: this.now() + delay, updatedAt: this.now() };
1552
- });
1553
- }
1554
- async run(job, model, input, signal = new AbortController().signal) {
1555
- let sources = [];
1556
- let claimed;
1557
- const controller = new AbortController();
1558
- let renew;
1559
- const timer = setTimeout(() => controller.abort(), this.modelTimeoutMs);
1560
- const abort = () => controller.abort();
1561
- signal.addEventListener("abort", abort, { once: true });
1562
- const inputBytes = utf8Bytes(input);
1563
- try {
1564
- claimed = this.claim(job.jobId, job.ownerId ?? this.ownerId);
1565
- const node = this.getNode(claimed.nodeId);
1566
- sources = node?.sources ?? [];
1567
- const kind = node?.kind ?? "leaf";
1568
- const sessionId = node?.sessionId ?? "";
1569
- renew = setInterval(() => {
1570
- try {
1571
- if (claimed) claimed = this.renew(claimed);
1572
- } catch {
1573
- }
1574
- }, 1e4);
1575
- let lastError = new Error("LCM summarization did not converge");
1576
- for (const [index, level] of LCM_MODEL_LEVELS.entries()) {
1577
- if (controller.signal.aborted) break;
1578
- if (index > 0 && !this.withinBudget(sessionId, job.jobId)) break;
1579
- const target = Math.max(1, Math.min(Math.floor(this.maxOutputChars * level.share), inputBytes - 1));
1580
- let result;
1581
- try {
1582
- result = await Promise.race([
1583
- model.generate({ prompt: buildLcmPrompt(kind, input, this.maxInputChars, level.mode, target), sessionId, signal: controller.signal }),
1584
- new Promise((_, reject) => controller.signal.addEventListener("abort", () => reject(new Error("LCM timeout")), { once: true }))
1585
- ]);
1586
- } catch (error) {
1587
- lastError = error;
1588
- continue;
1589
- }
1590
- if (utf8Bytes(result.text) >= inputBytes) {
1591
- this.accountRejected(sessionId, result);
1592
- lastError = new Error("summary does not shrink its input");
1593
- continue;
1594
- }
1595
- return this.complete(claimed, result, inputBytes);
1596
- }
1597
- throw lastError;
1598
- } catch (error) {
1599
- if (!claimed) throw error;
1600
- const fallback = emergencyReduce(input, this.maxOutputChars, sources);
1601
- try {
1602
- return this.completeEmergency(claimed, fallback);
1603
- } catch (fenced) {
1604
- if (isLcmRejection(fenced) && fenced.reason === "lease fenced") throw fenced;
1605
- try {
1606
- this.fail(claimed, String(error));
1607
- } catch {
1608
- }
1609
- throw error;
1610
- }
1611
- } finally {
1612
- clearTimeout(timer);
1613
- if (renew) clearInterval(renew);
1614
- signal.removeEventListener("abort", abort);
1615
- }
1616
- }
1617
- day(ms) {
1618
- return new Date(ms).toISOString().slice(0, 10);
1619
- }
1620
- usage(db, day, sessionId) {
1621
- const q = sessionId === void 0 ? "SELECT COALESCE(SUM(calls),0) calls,COALESCE(SUM(input_tokens),0) inputTokens,COALESCE(SUM(output_tokens),0) outputTokens,COALESCE(SUM(cost),0) cost,COALESCE(SUM(wall_ms),0) wallMs FROM maintenance_usage WHERE project_key=? AND day=?" : "SELECT COALESCE(SUM(calls),0) calls,COALESCE(SUM(input_tokens),0) inputTokens,COALESCE(SUM(output_tokens),0) outputTokens,COALESCE(SUM(cost),0) cost,COALESCE(SUM(wall_ms),0) wallMs FROM maintenance_usage WHERE project_key=? AND day=? AND session_id=?";
1622
- const r = sessionId === void 0 ? db.prepare(q).get(this.projectKey, day) : db.prepare(q).get(this.projectKey, day, sessionId);
1623
- return r;
1624
- }
1625
- account(db, sessionId, result) {
1626
- const d = this.day(this.now());
1627
- db.prepare("INSERT INTO maintenance_usage(project_key,day,session_id,calls,input_tokens,output_tokens,cost,wall_ms) VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(project_key,day,session_id) DO UPDATE SET calls=calls+excluded.calls,input_tokens=input_tokens+excluded.input_tokens,output_tokens=output_tokens+excluded.output_tokens,cost=cost+excluded.cost,wall_ms=wall_ms+excluded.wall_ms").run(this.projectKey, d, sessionId, 1, result.inputTokens, result.outputTokens, result.cost, result.wallMs);
1628
- }
1629
- readJob(db, id) {
1630
- const row = db.prepare("SELECT payload FROM maintenance_jobs WHERE job_id=? AND project_key=?").get(id, this.projectKey);
1631
- if (!row) throw new Error("job not found");
1632
- const job = parse(row.payload);
1633
- if (job.projectKey !== this.projectKey) throw new Error("job project does not match ledger project");
1634
- return job;
1635
- }
1636
- assertLease(current, expected) {
1637
- if (current.ownerId !== expected.ownerId || current.leaseToken !== expected.leaseToken || current.state !== "running" || (current.leaseUntil ?? 0) < this.now()) throw new LcmRejection("lease fenced");
1638
- }
1639
- fenced(job, update) {
1640
- return this.ledger.transaction((db) => {
1641
- const old = this.readJob(db, job.jobId);
1642
- this.assertLease(old, job);
1643
- const out = update(old);
1644
- db.prepare("UPDATE maintenance_jobs SET status=?,payload=?,updated_at=? WHERE job_id=? AND project_key=?").run(out.state, JSON.stringify(out), out.updatedAt, out.jobId, this.projectKey);
1645
- return out;
1646
- });
1647
- }
1648
- };
1649
-
1650
1059
  // src/compaction/lcm-runtime.ts
1651
1060
  var SHUTDOWN_GRACE_MS = 500;
1652
1061
  var MAX_AUTO_REPAIR_ATTEMPTS = 5;
@@ -2057,7 +1466,12 @@ var LcmRuntime = class {
2057
1466
  }
2058
1467
  }).finally(() => {
2059
1468
  this.maintenanceRunning = false;
1469
+ if (!this.maintenanceQueued || this.closed) {
1470
+ this.maintenanceQueued = false;
1471
+ return;
1472
+ }
2060
1473
  this.maintenanceQueued = false;
1474
+ this.scheduleMaintenance();
2061
1475
  });
2062
1476
  this.maintenancePending = run.catch((error) => {
2063
1477
  this.degradedError = error;