open-multi-agent-kit 0.78.6 → 0.78.8

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 (170) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +38 -0
  3. package/MATURITY.md +2 -2
  4. package/README.md +19 -4
  5. package/dist/brand/night-city.theme.json +80 -0
  6. package/dist/brand/palette.d.ts +73 -177
  7. package/dist/brand/palette.js +115 -39
  8. package/dist/brand/rust-forge.theme.json +216 -0
  9. package/dist/brand/theme-compiled.d.ts +61 -0
  10. package/dist/brand/theme-compiled.js +241 -0
  11. package/dist/brand/theme.js +50 -37
  12. package/dist/cli/release-promotion-gate.d.ts +8 -1
  13. package/dist/cli/release-promotion-gate.js +20 -9
  14. package/dist/cli/root.d.ts +1 -0
  15. package/dist/cli/root.js +2 -1
  16. package/dist/cli/theme/index.d.ts +11 -2
  17. package/dist/cli/theme/index.js +6 -1
  18. package/dist/cli/theme/oklab-quantize.d.ts +29 -0
  19. package/dist/cli/theme/oklab-quantize.js +105 -0
  20. package/dist/cli/theme/render-table.d.ts +59 -0
  21. package/dist/cli/theme/render-table.js +112 -0
  22. package/dist/cli/theme/status-frame.d.ts +10 -0
  23. package/dist/cli/theme/status-frame.js +22 -0
  24. package/dist/cli/theme/terminal-capability.d.ts +6 -1
  25. package/dist/cli/theme/terminal-capability.js +20 -4
  26. package/dist/cli/theme/theme-doc.d.ts +22 -0
  27. package/dist/cli/theme/theme-doc.js +97 -0
  28. package/dist/cli/theme/theme-registry.js +39 -18
  29. package/dist/cli/theme/tier-explain.d.ts +21 -0
  30. package/dist/cli/theme/tier-explain.js +60 -0
  31. package/dist/cli/ui/green-rain-renderer.js +3 -2
  32. package/dist/cli/ui/neon-grid-renderer.js +4 -3
  33. package/dist/cli/ui/rust-forge-renderer.js +17 -3
  34. package/dist/cli/ui/system24-renderer.d.ts +18 -1
  35. package/dist/cli/ui/system24-renderer.js +39 -16
  36. package/dist/cli/v2/chat-repl.js +3 -1
  37. package/dist/cli/v2/cli-v2-skeleton.d.ts +6 -0
  38. package/dist/cli/v2/cli-v2-skeleton.js +121 -9
  39. package/dist/cli/v2/interactive-prompt.d.ts +1 -0
  40. package/dist/cli/v2/interactive-prompt.js +6 -2
  41. package/dist/commands/chat/core.js +2 -2
  42. package/dist/commands/chat/runtime.js +2 -0
  43. package/dist/commands/chat/utils.js +12 -4
  44. package/dist/commands/cockpit/render.js +102 -64
  45. package/dist/commands/cockpit/utils.d.ts +2 -0
  46. package/dist/commands/design.js +18 -16
  47. package/dist/commands/doctor/checks.d.ts +0 -1
  48. package/dist/commands/doctor/checks.js +1 -36
  49. package/dist/commands/doctor/report.js +0 -11
  50. package/dist/commands/doctor/utils.js +2 -2
  51. package/dist/commands/init/content.d.ts +1 -1
  52. package/dist/commands/init/content.js +3 -11
  53. package/dist/commands/init.js +2 -2
  54. package/dist/commands/mcp/config.d.ts +32 -0
  55. package/dist/commands/mcp/config.js +264 -0
  56. package/dist/commands/mcp/doctor-fix.d.ts +12 -0
  57. package/dist/commands/mcp/doctor-fix.js +261 -0
  58. package/dist/commands/mcp/doctor.d.ts +55 -0
  59. package/dist/commands/mcp/doctor.js +401 -0
  60. package/dist/commands/mcp/list.d.ts +1 -0
  61. package/dist/commands/mcp/list.js +56 -0
  62. package/dist/commands/mcp/shared.d.ts +47 -0
  63. package/dist/commands/mcp/shared.js +192 -0
  64. package/dist/commands/mcp/test.d.ts +5 -0
  65. package/dist/commands/mcp/test.js +457 -0
  66. package/dist/commands/mcp.d.ts +8 -104
  67. package/dist/commands/mcp.js +5 -1603
  68. package/dist/contracts/proof.d.ts +1 -1
  69. package/dist/goal/control-loop.d.ts +15 -0
  70. package/dist/goal/control-loop.js +39 -5
  71. package/dist/hud/render.d.ts +2 -0
  72. package/dist/hud/render.js +7 -1
  73. package/dist/mcp/secret-scanner.d.ts +6 -0
  74. package/dist/mcp/secret-scanner.js +44 -11
  75. package/dist/memory/graph-delta-log.d.ts +137 -0
  76. package/dist/memory/graph-delta-log.js +650 -0
  77. package/dist/memory/graph-viewer.js +13 -32
  78. package/dist/memory/local-graph-memory-store.d.ts +36 -3
  79. package/dist/memory/local-graph-memory-store.js +255 -10
  80. package/dist/orchestration/log-streamer.js +13 -8
  81. package/dist/orchestration/routing.d.ts +4 -0
  82. package/dist/orchestration/routing.js +28 -3
  83. package/dist/orchestration/state-persister.js +8 -1
  84. package/dist/providers/anthropic-messages-runner.d.ts +17 -0
  85. package/dist/providers/anthropic-messages-runner.js +197 -0
  86. package/dist/providers/model-registry.d.ts +1 -1
  87. package/dist/providers/model-registry.js +37 -3
  88. package/dist/providers/provider-runtime.js +27 -11
  89. package/dist/providers/thinking-levels.js +4 -1
  90. package/dist/providers/types.d.ts +3 -3
  91. package/dist/runtime/contracts/weakness-remediation.d.ts +9 -1
  92. package/dist/runtime/contracts/weakness-remediation.js +9 -1
  93. package/dist/runtime/router-v2-scoring.d.ts +8 -3
  94. package/dist/runtime/router-v2-scoring.js +27 -22
  95. package/dist/runtime/runtime-router.d.ts +5 -0
  96. package/dist/runtime/runtime-router.js +40 -8
  97. package/dist/runtime/sandbox-profile.d.ts +29 -0
  98. package/dist/runtime/sandbox-profile.js +101 -0
  99. package/dist/runtime/slash-commands.js +300 -2
  100. package/dist/runtime/tool-dispatch-contracts.d.ts +15 -0
  101. package/dist/runtime/tool-dispatch-contracts.js +12 -0
  102. package/dist/runtime/weights-config.d.ts +90 -0
  103. package/dist/runtime/weights-config.js +249 -0
  104. package/dist/schema/envelope.schema.d.ts +18 -18
  105. package/dist/schema/evidence.schema.d.ts +2 -2
  106. package/dist/schema/proof-bundle.schema.d.ts +6 -6
  107. package/dist/schema/proof-bundle.schema.js +0 -1
  108. package/dist/schema/provider.schema.d.ts +2 -2
  109. package/dist/schema/run-manifest.schema.d.ts +16 -16
  110. package/dist/theme/ansi.js +9 -2
  111. package/dist/theme/extended-palette.d.ts +85 -0
  112. package/dist/theme/extended-palette.js +108 -0
  113. package/dist/theme/layout.js +29 -8
  114. package/dist/theme/metrics.js +5 -5
  115. package/dist/theme/parallel.js +25 -15
  116. package/dist/ui/omk-sigil.js +5 -14
  117. package/dist/ui/omk-working-sweep.js +5 -15
  118. package/dist/util/chat-agent-mode.d.ts +11 -0
  119. package/dist/util/chat-agent-mode.js +41 -8
  120. package/dist/util/chat-cockpit.d.ts +9 -0
  121. package/dist/util/chat-cockpit.js +77 -20
  122. package/dist/util/fs/core.d.ts +5 -0
  123. package/dist/util/fs/core.js +35 -0
  124. package/dist/util/fs/internal.d.ts +14 -0
  125. package/dist/util/fs/internal.js +169 -0
  126. package/dist/util/fs/kimi-sync.d.ts +41 -0
  127. package/dist/util/fs/kimi-sync.js +495 -0
  128. package/dist/util/fs/logo.d.ts +2 -0
  129. package/dist/util/fs/logo.js +78 -0
  130. package/dist/util/fs/manifest.d.ts +6 -0
  131. package/dist/util/fs/manifest.js +40 -0
  132. package/dist/util/fs/mcp-diagnose.d.ts +15 -0
  133. package/dist/util/fs/mcp-diagnose.js +204 -0
  134. package/dist/util/fs/mcp-runtime-config.d.ts +23 -0
  135. package/dist/util/fs/mcp-runtime-config.js +412 -0
  136. package/dist/util/fs/paths.d.ts +13 -0
  137. package/dist/util/fs/paths.js +82 -0
  138. package/dist/util/fs/preflight.d.ts +27 -0
  139. package/dist/util/fs/preflight.js +271 -0
  140. package/dist/util/fs.d.ts +8 -128
  141. package/dist/util/fs.js +8 -1757
  142. package/dist/util/terminal-layout.js +5 -1
  143. package/dist/util/version.js +2 -1
  144. package/docs/2026-06-11/critical-issues.md +20 -0
  145. package/docs/2026-06-11/improvements.md +14 -0
  146. package/docs/2026-06-11/init-checklist.md +25 -0
  147. package/docs/2026-06-11/plan.md +20 -0
  148. package/docs/ARCHITECTURE_ANALYSIS_CODEGRAPH.md +223 -0
  149. package/docs/adr/0001-no-native-rust-lane.md +52 -0
  150. package/docs/codex-oauth-setup.md +14 -0
  151. package/docs/decisions/2026-06-10-git-history-bloat.md +61 -0
  152. package/docs/decisions/ADR-theme-dark-only-assets.md +43 -0
  153. package/docs/getting-started.md +1 -1
  154. package/docs/headroom-omk-final-summary.md +188 -0
  155. package/docs/headroom-omk-integration.md +394 -0
  156. package/docs/headroom-omk-setup-guide.md +422 -0
  157. package/docs/headroom-omk-usage-examples.md +371 -0
  158. package/docs/provider-maturity.md +1 -1
  159. package/docs/versioning.md +3 -3
  160. package/package.json +10 -10
  161. package/readmeasset/.npmignore +2 -0
  162. package/readmeasset/ASSET_INDEX.md +7 -5
  163. package/readmeasset/ASSET_PROVENANCE.md +21 -5
  164. package/readmeasset/omk-control-surfaces.svg +55 -0
  165. package/readmeasset/omk-core-loop.svg +1 -0
  166. package/readmeasset/omk-logo-mark.svg +1 -0
  167. package/readmeasset/omk-release-assertions.svg +51 -0
  168. package/dist/native/linux-x64/omk-safety +0 -0
  169. package/dist/util/native-safety.d.ts +0 -28
  170. package/dist/util/native-safety.js +0 -118
@@ -0,0 +1,650 @@
1
+ import { createHash } from "node:crypto";
2
+ import { closeSync, mkdirSync, openSync, readFileSync, statSync, truncateSync, unlinkSync, writeSync } from "node:fs";
3
+ import { open as fsOpen, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
4
+ import { hostname } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ // ── Path helpers ────────────────────────────────────────────────────────────
7
+ function deltaDir(graphPath) {
8
+ return dirname(graphPath);
9
+ }
10
+ function snapshotPath(graphPath) {
11
+ return join(deltaDir(graphPath), "graph.snapshot.json");
12
+ }
13
+ function deltaLogPath(graphPath) {
14
+ return join(deltaDir(graphPath), "graph.delta.jsonl");
15
+ }
16
+ function manifestPath(graphPath) {
17
+ return join(deltaDir(graphPath), "graph.manifest.json");
18
+ }
19
+ // ── Durability mode detection ───────────────────────────────────────────────
20
+ export function resolveDurabilityMode(env = process.env) {
21
+ const raw = (env.OMK_MEMORY_DURABILITY ?? "").trim().toLowerCase();
22
+ if (raw === "delta")
23
+ return "delta";
24
+ return "legacy";
25
+ }
26
+ // ── Write helpers ───────────────────────────────────────────────────────────
27
+ async function writeFileAtomic(path, payload) {
28
+ await mkdir(dirname(path), { recursive: true });
29
+ const hash = createHash("sha256").update(payload).digest("hex").slice(0, 8);
30
+ const tempPath = `${path}.${process.pid}.${Date.now()}.${hash}.tmp`;
31
+ try {
32
+ await writeFile(tempPath, payload, "utf-8");
33
+ await rename(tempPath, path);
34
+ }
35
+ catch (err) {
36
+ await unlink(tempPath).catch(() => { });
37
+ throw err;
38
+ }
39
+ }
40
+ function errorCode(err) {
41
+ if (typeof err !== "object" || err === null || !("code" in err))
42
+ return undefined;
43
+ const code = err.code;
44
+ return typeof code === "string" ? code : undefined;
45
+ }
46
+ // ── CRC / hashing ───────────────────────────────────────────────────────────
47
+ function computeRecordCrc(record) {
48
+ // Canonical: JSON.stringify(recordWithoutCrc) + "\n", then SHA256
49
+ const payload = `${JSON.stringify(record)}\n`;
50
+ return createHash("sha256").update(payload).digest("hex");
51
+ }
52
+ function verifyRecordCrc(record) {
53
+ // Rebuild record without crc, re-serialize, and compare against the stored crc.
54
+ const { crc, ...withoutCrc } = record;
55
+ const expected = computeRecordCrc(withoutCrc);
56
+ return expected === crc;
57
+ }
58
+ // ── Delta computation ───────────────────────────────────────────────────────
59
+ /**
60
+ * Compute an order-faithful delta between pre-mutation and post-mutation state.
61
+ *
62
+ * The legacy full-rewrite produces array orders of the form
63
+ * [survivors kept in pre-order (some updated in place)] ++ [appended at end]
64
+ * because `upsertNode`/`upsertEdge` update existing entries in place but push
65
+ * brand-new (or filter-then-re-added, e.g. `replaceGeneratedMindmap`) entries
66
+ * to the end. A naive `Map.set` replay keeps the OLD position for an existing
67
+ * key and therefore diverges when an entry is filtered out then re-added.
68
+ *
69
+ * To reproduce the EXACT raw array order on replay we:
70
+ * 1. Keep the longest front prefix of the final array whose entries exist in
71
+ * pre-state with strictly increasing pre-positions (these stay in place).
72
+ * 2. Treat every entry from the first break point onward as "repositioned":
73
+ * emit a `del` (so replay removes it from its old Map slot) then a `put`
74
+ * in final order (so replay re-inserts it at the array end).
75
+ * Replay applies all `del` then all `put`, so del-before-put moves repositioned
76
+ * keys to the end exactly as the legacy filter+repush does.
77
+ */
78
+ function computeOrderedDelta(pre, finalArr) {
79
+ // Pre-position index (Map iteration order == insertion order == pre array order).
80
+ const prePos = new Map();
81
+ let position = 0;
82
+ for (const id of pre.keys())
83
+ prePos.set(id, position++);
84
+ // Longest in-place prefix: exists in pre AND pre-position strictly increases.
85
+ let lastPrePos = -1;
86
+ let splitIdx = finalArr.length;
87
+ for (let i = 0; i < finalArr.length; i += 1) {
88
+ const pos = prePos.get(finalArr[i].id);
89
+ if (pos !== undefined && pos > lastPrePos) {
90
+ lastPrePos = pos;
91
+ }
92
+ else {
93
+ splitIdx = i;
94
+ break;
95
+ }
96
+ }
97
+ // Final index for suffix (repositioned) detection.
98
+ const finalIndex = new Map();
99
+ for (let i = 0; i < finalArr.length; i += 1)
100
+ finalIndex.set(finalArr[i].id, i);
101
+ // Deletes: pre entries dropped entirely, or kept but in the repositioned suffix.
102
+ const del = [];
103
+ for (const id of pre.keys()) {
104
+ const fi = finalIndex.get(id);
105
+ if (fi === undefined || fi >= splitIdx)
106
+ del.push(id);
107
+ }
108
+ // Puts (final order): suffix entries always re-put (append); prefix entries
109
+ // only when their value changed (in-place update keeps position).
110
+ const put = [];
111
+ for (let i = 0; i < finalArr.length; i += 1) {
112
+ const item = finalArr[i];
113
+ if (i >= splitIdx) {
114
+ put.push(item);
115
+ }
116
+ else if (pre.get(item.id) !== item) {
117
+ put.push(item);
118
+ }
119
+ }
120
+ return { del, put };
121
+ }
122
+ /**
123
+ * Compute an order-faithful delta between pre-mutation and post-mutation state.
124
+ * `put` entries are ordered by their index in the final array and repositioned
125
+ * entries are deleted-before-put, so Map-based replay reproduces the EXACT raw
126
+ * array order of the legacy full-rewrite (see `computeOrderedDelta`).
127
+ */
128
+ export function computeDelta(preNodes, preEdges, state) {
129
+ return {
130
+ nodes: computeOrderedDelta(preNodes, state.nodes),
131
+ edges: computeOrderedDelta(preEdges, state.edges),
132
+ };
133
+ }
134
+ // ── Manifest I/O ────────────────────────────────────────────────────────────
135
+ export async function readManifest(graphPath) {
136
+ const path = manifestPath(graphPath);
137
+ try {
138
+ const raw = await readFile(path, "utf-8");
139
+ const parsed = JSON.parse(raw);
140
+ if (parsed.formatVersion === 2 &&
141
+ typeof parsed.snapshot === "string" &&
142
+ typeof parsed.snapshotEpoch === "number" &&
143
+ typeof parsed.delta === "string") {
144
+ return {
145
+ formatVersion: 2,
146
+ snapshot: parsed.snapshot,
147
+ snapshotEpoch: parsed.snapshotEpoch,
148
+ delta: parsed.delta,
149
+ deltaOpCount: typeof parsed.deltaOpCount === "number" ? parsed.deltaOpCount : 0,
150
+ };
151
+ }
152
+ return null;
153
+ }
154
+ catch (err) {
155
+ if (errorCode(err) === "ENOENT")
156
+ return null;
157
+ throw err;
158
+ }
159
+ }
160
+ export async function writeManifest(graphPath, manifest) {
161
+ const payload = `${JSON.stringify(manifest, null, 2)}\n`;
162
+ await writeFileAtomic(manifestPath(graphPath), payload);
163
+ }
164
+ // ── Snapshot I/O ────────────────────────────────────────────────────────────
165
+ async function writeSnapshot(graphPath, state) {
166
+ const payload = `${JSON.stringify(state, null, 2)}\n`;
167
+ await writeFileAtomic(snapshotPath(graphPath), payload);
168
+ }
169
+ async function readSnapshot(graphPath) {
170
+ const path = snapshotPath(graphPath);
171
+ try {
172
+ const raw = await readFile(path, "utf-8");
173
+ const parsed = JSON.parse(raw);
174
+ if (parsed.version === 1 && Array.isArray(parsed.nodes) && Array.isArray(parsed.edges)) {
175
+ return {
176
+ version: 1,
177
+ ontology: parsed.ontology ?? { version: "", classes: [], relationTypes: [], description: "" },
178
+ project: parsed.project ?? { key: "", name: "", root: "" },
179
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString(),
180
+ nodes: parsed.nodes,
181
+ edges: parsed.edges,
182
+ };
183
+ }
184
+ return null;
185
+ }
186
+ catch (err) {
187
+ if (errorCode(err) === "ENOENT")
188
+ return null;
189
+ throw err;
190
+ }
191
+ }
192
+ // ── Delta append ────────────────────────────────────────────────────────────
193
+ let deltaFd = null;
194
+ let deltaFdPath = null;
195
+ async function getDeltaFd(path) {
196
+ if (deltaFd && deltaFdPath === path)
197
+ return deltaFd;
198
+ if (deltaFd) {
199
+ await deltaFd.close().catch(() => { });
200
+ deltaFd = null;
201
+ deltaFdPath = null;
202
+ }
203
+ deltaFd = await fsOpen(path, "a"); // O_APPEND
204
+ deltaFdPath = path;
205
+ return deltaFd;
206
+ }
207
+ function closeDeltaFd() {
208
+ if (deltaFd) {
209
+ deltaFd.close().catch(() => { });
210
+ deltaFd = null;
211
+ deltaFdPath = null;
212
+ }
213
+ }
214
+ /**
215
+ * Close the cached append fd and await the close. Used before a physical
216
+ * truncate so no stale O_APPEND handle survives the recovery rewrite.
217
+ */
218
+ async function closeDeltaFdAsync() {
219
+ if (deltaFd) {
220
+ const fd = deltaFd;
221
+ deltaFd = null;
222
+ deltaFdPath = null;
223
+ await fd.close().catch(() => { });
224
+ }
225
+ }
226
+ /**
227
+ * Append a single delta record to the journal + fdatasync.
228
+ * Returns the seq number assigned to the record.
229
+ */
230
+ export async function appendDelta(graphPath, epoch, seq, ts, meta, nodesDelta, edgesDelta) {
231
+ const recordWithoutCrc = {
232
+ v: 2,
233
+ epoch,
234
+ seq,
235
+ ts,
236
+ meta,
237
+ nodes: nodesDelta,
238
+ edges: edgesDelta,
239
+ };
240
+ const crc = computeRecordCrc(recordWithoutCrc);
241
+ const record = { ...recordWithoutCrc, crc };
242
+ const line = `${JSON.stringify(record)}\n`;
243
+ const logPath = deltaLogPath(graphPath);
244
+ await mkdir(dirname(logPath), { recursive: true });
245
+ const fd = await getDeltaFd(logPath);
246
+ await fd.write(line, null, "utf-8");
247
+ await fd.datasync();
248
+ }
249
+ // ── Replay ──────────────────────────────────────────────────────────────────
250
+ /**
251
+ * Load snapshot from disk plus replay all valid delta records.
252
+ *
253
+ * Replay correctness lemma:
254
+ * 1. Base: nodes/edges loaded from snapshot into insertion-ordered Map.
255
+ * 2. For each record with epoch === currentEpoch:
256
+ * a. Apply all `del` ids: Map.delete(key).
257
+ * b. Apply all `put` entries in array order: Map.set(key, entry).
258
+ * Map.set on existing key keeps position; on new/deleted key appends.
259
+ * 3. Materialize: `[...map.values()]` produces the exact same array order
260
+ * as the full-rewrite saveState().
261
+ *
262
+ * Crash recovery: the last line of the delta log may be torn. If JSON.parse
263
+ * fails AND it is the last line, we treat it as a torn tail and discard it.
264
+ * A CRC mismatch on a non-final line is corruption → throws in strict mode.
265
+ */
266
+ export function replayDeltas(baseState, records, epoch) {
267
+ const nodeMap = new Map();
268
+ for (const node of baseState.nodes)
269
+ nodeMap.set(node.id, node);
270
+ const edgeMap = new Map();
271
+ for (const edge of baseState.edges)
272
+ edgeMap.set(edge.id, edge);
273
+ for (const record of records) {
274
+ if (record.epoch !== epoch)
275
+ continue;
276
+ // Apply deletes
277
+ for (const id of record.nodes.del)
278
+ nodeMap.delete(id);
279
+ for (const id of record.edges.del)
280
+ edgeMap.delete(id);
281
+ // Apply puts (order is preserved by the Map)
282
+ for (const node of record.nodes.put)
283
+ nodeMap.set(node.id, node);
284
+ for (const edge of record.edges.put)
285
+ edgeMap.set(edge.id, edge);
286
+ // Update meta from the last record
287
+ baseState.updatedAt = record.meta.updatedAt;
288
+ baseState.project = record.meta.project;
289
+ baseState.ontology = {
290
+ version: record.meta.ontology,
291
+ classes: [],
292
+ relationTypes: [],
293
+ description: "",
294
+ };
295
+ }
296
+ return {
297
+ ...baseState,
298
+ nodes: [...nodeMap.values()],
299
+ edges: [...edgeMap.values()],
300
+ };
301
+ }
302
+ /**
303
+ * Byte-accurate parse of the delta log. Walks the raw bytes record-by-record
304
+ * (records are `\n`-framed) and tracks the byte offset of the last VALID record
305
+ * boundary so a recovering writer can truncate/append at exactly that offset.
306
+ *
307
+ * Framing policy:
308
+ * - A trailing segment WITHOUT a terminating newline is a torn tail (a crash
309
+ * mid-append): it is reported via `torn=true` and excluded from `validBytes`
310
+ * so the caller can physically truncate it away before any new append.
311
+ * - A newline-terminated line that fails JSON.parse or CRC is MID-FILE
312
+ * corruption: strict mode throws; non-strict mode stops replay at the first
313
+ * corruption (leaving `validBytes` at the last good boundary). Either way the
314
+ * bytes are never silently interleaved with a resumed write.
315
+ */
316
+ async function parseDeltaLog(graphPath, strict) {
317
+ const path = deltaLogPath(graphPath);
318
+ let buf;
319
+ try {
320
+ buf = await readFile(path);
321
+ }
322
+ catch (err) {
323
+ if (errorCode(err) === "ENOENT")
324
+ return { records: [], validBytes: 0, torn: false };
325
+ throw err;
326
+ }
327
+ const records = [];
328
+ const len = buf.length;
329
+ const NEWLINE = 0x0a;
330
+ let cursor = 0;
331
+ let validBytes = 0;
332
+ let torn = false;
333
+ while (cursor < len) {
334
+ const nl = buf.indexOf(NEWLINE, cursor);
335
+ if (nl === -1) {
336
+ // No terminating newline: trailing partial (torn) record from a crash.
337
+ torn = true;
338
+ break;
339
+ }
340
+ const recordEnd = nl + 1; // inclusive of the framing newline
341
+ const line = buf.toString("utf-8", cursor, nl);
342
+ if (line === "") {
343
+ // Blank line: tolerated framing artifact; advance the valid boundary.
344
+ validBytes = recordEnd;
345
+ cursor = recordEnd;
346
+ continue;
347
+ }
348
+ let record;
349
+ try {
350
+ record = JSON.parse(line);
351
+ }
352
+ catch {
353
+ // Newline-terminated but unparseable => mid-file corruption.
354
+ if (strict) {
355
+ throw new Error(`Delta log corruption: unparseable record at byte ${cursor}: ${line.slice(0, 80)}...`);
356
+ }
357
+ break; // non-strict: stop replay at first corruption (no interleave)
358
+ }
359
+ if (!verifyRecordCrc(record)) {
360
+ if (strict) {
361
+ throw new Error(`Delta log corruption: CRC mismatch at byte ${cursor} (seq=${record.seq})`);
362
+ }
363
+ break; // non-strict: stop replay at first corruption (no interleave)
364
+ }
365
+ records.push(record);
366
+ validBytes = recordEnd;
367
+ cursor = recordEnd;
368
+ }
369
+ return { records, validBytes, torn };
370
+ }
371
+ /**
372
+ * Full cold load in delta mode: manifest → snapshot → replay delta tail.
373
+ *
374
+ * Torn-tail recovery: if the log ends in a partial (non-newline-terminated)
375
+ * record, physically truncate the file to the last valid record boundary BEFORE
376
+ * returning, so the next O_APPEND write can never merge into the half-written
377
+ * line. Any cached append fd is closed first to drop the stale handle.
378
+ */
379
+ export async function loadStateViaDelta(graphPath, strict, emptyState) {
380
+ const manifest = await readManifest(graphPath);
381
+ if (!manifest) {
382
+ // No manifest: try legacy migration path
383
+ return { state: emptyState, epoch: 1, lastSeq: 0, deltaOpCount: 0, validBytes: 0 };
384
+ }
385
+ let baseState;
386
+ try {
387
+ const snap = await readSnapshot(graphPath);
388
+ baseState = snap ?? emptyState;
389
+ }
390
+ catch {
391
+ baseState = emptyState;
392
+ }
393
+ const { records, validBytes, torn } = await parseDeltaLog(graphPath, strict);
394
+ if (torn) {
395
+ // Drop the torn trailing bytes at the last valid record boundary so a
396
+ // resumed append starts on a clean record frame (no mid-file interleave).
397
+ await closeDeltaFdAsync();
398
+ try {
399
+ truncateSync(deltaLogPath(graphPath), validBytes);
400
+ }
401
+ catch (err) {
402
+ if (errorCode(err) !== "ENOENT")
403
+ throw err;
404
+ }
405
+ }
406
+ const filtered = records.filter((r) => r.epoch === manifest.snapshotEpoch);
407
+ const state = replayDeltas(baseState, filtered, manifest.snapshotEpoch);
408
+ return {
409
+ state,
410
+ epoch: manifest.snapshotEpoch,
411
+ lastSeq: filtered.length > 0 ? filtered[filtered.length - 1].seq : 0,
412
+ deltaOpCount: filtered.length,
413
+ validBytes,
414
+ };
415
+ }
416
+ // ── Setup / migration ───────────────────────────────────────────────────────
417
+ /**
418
+ * One-time setup: load legacy graph-state.json (or empty), write snapshot,
419
+ * write manifest, create empty delta log. Does NOT delete the legacy file.
420
+ * If manifest already exists, this is a no-op.
421
+ */
422
+ export async function setupDeltaMode(graphPath, legacyState) {
423
+ const existing = await readManifest(graphPath);
424
+ if (existing)
425
+ return existing;
426
+ const manifest = {
427
+ formatVersion: 2,
428
+ snapshot: "graph.snapshot.json",
429
+ snapshotEpoch: 1,
430
+ delta: "graph.delta.jsonl",
431
+ deltaOpCount: 0,
432
+ };
433
+ await writeSnapshot(graphPath, legacyState);
434
+ await writeFile(deltaLogPath(graphPath), "", "utf-8").catch(async (err) => {
435
+ if (errorCode(err) === "ENOENT") {
436
+ await mkdir(dirname(deltaLogPath(graphPath)), { recursive: true });
437
+ await writeFile(deltaLogPath(graphPath), "", "utf-8");
438
+ return;
439
+ }
440
+ throw err;
441
+ });
442
+ await writeManifest(graphPath, manifest);
443
+ return manifest;
444
+ }
445
+ export function resolveCompactionThresholds(env = process.env) {
446
+ const maxBytes = Number.parseInt(env.OMK_MEMORY_COMPACT_BYTES ?? "", 10) || 8 * 1024 * 1024; // 8MB
447
+ const maxOps = Number.parseInt(env.OMK_MEMORY_COMPACT_OPS ?? "", 10) || 1000;
448
+ return { maxBytes, maxOps };
449
+ }
450
+ /**
451
+ * Check if compaction is needed and perform it if so.
452
+ * Procedure: write snapshot → write new manifest (commit pivot) → truncate delta.
453
+ * The manifest is the single commit point; crash before manifest pivot is safe.
454
+ */
455
+ export async function compactIfNeeded(graphPath, state, currentEpoch, currentOpCount, thresholds) {
456
+ const logPath = deltaLogPath(graphPath);
457
+ let deltaBytes = 0;
458
+ try {
459
+ deltaBytes = statSync(logPath).size;
460
+ }
461
+ catch {
462
+ // File doesn't exist or can't be stat'd
463
+ }
464
+ const shouldCompact = deltaBytes > thresholds.maxBytes || currentOpCount > thresholds.maxOps;
465
+ if (!shouldCompact) {
466
+ return { compacted: false, newEpoch: currentEpoch, newOpCount: currentOpCount };
467
+ }
468
+ const newEpoch = currentEpoch + 1;
469
+ // 1. Write new snapshot (covers all state up to now)
470
+ await writeSnapshot(graphPath, state);
471
+ // 2. Write new manifest (commit pivot)
472
+ // Close the old delta fd so we can truncate
473
+ closeDeltaFd();
474
+ const manifest = {
475
+ formatVersion: 2,
476
+ snapshot: "graph.snapshot.json",
477
+ snapshotEpoch: newEpoch,
478
+ delta: "graph.delta.jsonl",
479
+ deltaOpCount: 0,
480
+ };
481
+ await writeManifest(graphPath, manifest);
482
+ // 3. Truncate delta log (records with old epoch are now folded into snapshot)
483
+ try {
484
+ await writeFile(logPath, "", "utf-8");
485
+ }
486
+ catch {
487
+ // If truncate fails, re-create
488
+ await unlink(logPath).catch(() => { });
489
+ await writeFile(logPath, "", "utf-8");
490
+ }
491
+ return { compacted: true, newEpoch, newOpCount: 0 };
492
+ }
493
+ export function statDeltaFiles(graphPath) {
494
+ try {
495
+ const snapStat = statSync(snapshotPath(graphPath));
496
+ let deltaSize = 0;
497
+ try {
498
+ deltaSize = statSync(deltaLogPath(graphPath)).size;
499
+ }
500
+ catch {
501
+ // delta file may not exist yet
502
+ }
503
+ return {
504
+ snapshotMtimeMs: snapStat.mtimeMs,
505
+ snapshotSize: snapStat.size,
506
+ snapshotCtimeMs: snapStat.ctimeMs,
507
+ snapshotIno: snapStat.ino,
508
+ deltaSize,
509
+ };
510
+ }
511
+ catch {
512
+ return null;
513
+ }
514
+ }
515
+ export function deltaStatsMatch(a, b) {
516
+ if (!a || !b)
517
+ return false;
518
+ return (a.snapshotMtimeMs === b.snapshotMtimeMs &&
519
+ a.snapshotSize === b.snapshotSize &&
520
+ a.snapshotCtimeMs === b.snapshotCtimeMs &&
521
+ a.snapshotIno === b.snapshotIno &&
522
+ a.deltaSize === b.deltaSize);
523
+ }
524
+ const LOCK_TTL_MS = 30000;
525
+ const LOCK_MAX_WAIT_MS = 30000;
526
+ const LOCK_BASE_BACKOFF_MS = 50;
527
+ const LOCK_MAX_BACKOFF_MS = 1000;
528
+ function deltaLockPath(graphPath) {
529
+ return `${graphPath}.delta.lock`;
530
+ }
531
+ function isLockHolderDead(holder) {
532
+ const started = Date.parse(holder.startedAt);
533
+ if (Number.isNaN(started))
534
+ return true;
535
+ if (Date.now() - started > LOCK_TTL_MS)
536
+ return true;
537
+ if (holder.hostname === hostname()) {
538
+ try {
539
+ process.kill(holder.pid, 0);
540
+ return false;
541
+ }
542
+ catch (err) {
543
+ const code = errorCode(err);
544
+ if (code === "ESRCH")
545
+ return true;
546
+ // EPERM or other errors mean the process exists but we cannot signal it;
547
+ // treat as alive to stay safe.
548
+ return false;
549
+ }
550
+ }
551
+ return false;
552
+ }
553
+ export async function acquireDeltaLock(graphPath, maxWaitMs = LOCK_MAX_WAIT_MS) {
554
+ const path = deltaLockPath(graphPath);
555
+ mkdirSync(dirname(path), { recursive: true });
556
+ const deadline = Date.now() + maxWaitMs;
557
+ while (true) {
558
+ try {
559
+ const record = {
560
+ pid: process.pid,
561
+ hostname: hostname(),
562
+ startedAt: new Date().toISOString(),
563
+ };
564
+ const fd = openSync(path, "wx");
565
+ try {
566
+ writeSync(fd, JSON.stringify(record));
567
+ }
568
+ finally {
569
+ closeSync(fd);
570
+ }
571
+ return;
572
+ }
573
+ catch (err) {
574
+ const code = errorCode(err);
575
+ if (code !== "EEXIST")
576
+ throw err;
577
+ let holder = null;
578
+ try {
579
+ const raw = readFileSync(path, "utf-8");
580
+ const parsed = JSON.parse(raw);
581
+ if (typeof parsed === "object" &&
582
+ parsed !== null &&
583
+ "pid" in parsed &&
584
+ "hostname" in parsed &&
585
+ "startedAt" in parsed) {
586
+ holder = parsed;
587
+ }
588
+ }
589
+ catch {
590
+ // unreadable / unparseable → treat as stale below
591
+ }
592
+ if (holder && isLockHolderDead(holder)) {
593
+ try {
594
+ unlinkSync(path);
595
+ }
596
+ catch (unlinkErr) {
597
+ if (errorCode(unlinkErr) !== "ENOENT")
598
+ throw unlinkErr;
599
+ }
600
+ continue;
601
+ }
602
+ if (Date.now() >= deadline) {
603
+ throw new Error(`Delta lock acquisition timed out after ${maxWaitMs}ms on ${path}` +
604
+ (holder ? ` (held by pid=${holder.pid} host=${holder.hostname} since=${holder.startedAt})` : ""));
605
+ }
606
+ const backoff = Math.min(LOCK_MAX_BACKOFF_MS, LOCK_BASE_BACKOFF_MS * (1 + Math.random()));
607
+ await new Promise((resolve) => setTimeout(resolve, backoff));
608
+ }
609
+ }
610
+ }
611
+ export function releaseDeltaLock(graphPath) {
612
+ const path = deltaLockPath(graphPath);
613
+ let holder = null;
614
+ try {
615
+ const raw = readFileSync(path, "utf-8");
616
+ const parsed = JSON.parse(raw);
617
+ if (typeof parsed === "object" &&
618
+ parsed !== null &&
619
+ "pid" in parsed &&
620
+ "hostname" in parsed &&
621
+ "startedAt" in parsed) {
622
+ holder = parsed;
623
+ }
624
+ }
625
+ catch {
626
+ return;
627
+ }
628
+ if (holder && (holder.pid !== process.pid || holder.hostname !== hostname())) {
629
+ return;
630
+ }
631
+ try {
632
+ unlinkSync(path);
633
+ }
634
+ catch (err) {
635
+ if (errorCode(err) !== "ENOENT")
636
+ throw err;
637
+ }
638
+ }
639
+ export async function withDeltaLock(graphPath, fn, env = process.env, maxWaitMs = LOCK_MAX_WAIT_MS) {
640
+ if (resolveDurabilityMode(env) === "legacy") {
641
+ return fn();
642
+ }
643
+ await acquireDeltaLock(graphPath, maxWaitMs);
644
+ try {
645
+ return await fn();
646
+ }
647
+ finally {
648
+ releaseDeltaLock(graphPath);
649
+ }
650
+ }