@hilbras/omninode 2.0.0-alpha.11 → 2.0.0-alpha.12

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.
package/README.md CHANGED
@@ -18,7 +18,7 @@ existing intelligence; it does not try to become another model.
18
18
 
19
19
  ## Status
20
20
 
21
- **v2.0.0-alpha.11 — v2 Phase 11: Planner v2**
21
+ **v2.0.0-alpha.12 — v2 Phase 12: Persistence Layer v2**
22
22
  (the reliability & interoperability line: architecture → execution →
23
23
  protocol → adapters → pipelines → providers → OmniHilbras → memory → reports) (the v2
24
24
  roadmap is [docs/ROADMAP_V2.md](docs/ROADMAP_V2.md); the v1 plan is complete:
@@ -31,6 +31,12 @@ for the plan and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design).
31
31
 
32
32
  ### What's new in v2 (so far)
33
33
 
34
+ - **Phase 12 — Persistence Layer v2**: writes are now **fsynced and atomic**
35
+ (temp → fsync → rename → directory fsync), corrupt store files are
36
+ **quarantined instead of lost**, documents carry a **schema version** with
37
+ v1 data migrated in place (`omninode migrate [--check]`), and files from a
38
+ newer OmniNode are refused rather than misread. Details:
39
+ [PERSISTENCE.md](docs/PERSISTENCE.md).
34
40
  - **Phase 11 — Planner v2**: the planner now receives the full §15 input —
35
41
  task, project context, agent reports, **aggregated findings (consensus and
36
42
  conflicts included)**, memory and **constraints** (settable per pipeline
@@ -359,7 +365,7 @@ required for the core engine.
359
365
  - [Architecture](docs/ARCHITECTURE.md) — module map, workflow, local state
360
366
  - [Security model](docs/SECURITY.md) — secrets, process execution, env isolation, audit log
361
367
  - [API stability policy](docs/API_STABILITY.md) and [public API inventory](docs/API.md)
362
- - [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Memory](docs/MEMORY.md) · [Reports](docs/REPORTS.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
368
+ - [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Memory](docs/MEMORY.md) · [Reports](docs/REPORTS.md) · [Persistence](docs/PERSISTENCE.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
363
369
  - [Example project](examples/demo/) — a runnable end-to-end demo (`./demo.sh`)
364
370
 
365
371
  Prefer a container? `docker build -t omninode .` then
package/dist/cli/index.js CHANGED
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/version.ts
9
- var OMNINODE_VERSION = "2.0.0-alpha.11";
9
+ var OMNINODE_VERSION = "2.0.0-alpha.12";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -2108,51 +2108,119 @@ function registerRoleCommands(program) {
2108
2108
  }
2109
2109
 
2110
2110
  // src/persistence/json-file-store.ts
2111
- import { mkdir, readFile, rename, writeFile } from "fs/promises";
2111
+ import { open, mkdir, readFile, rename, stat } from "fs/promises";
2112
2112
  import { dirname } from "path";
2113
+ var SCHEMA_VERSION = 2;
2114
+ async function writeFileAtomic(target, contents) {
2115
+ const tmp = `${target}.tmp`;
2116
+ await mkdir(dirname(target), { recursive: true });
2117
+ const handle = await open(tmp, "w");
2118
+ try {
2119
+ await handle.writeFile(contents, "utf8");
2120
+ await handle.sync();
2121
+ } finally {
2122
+ await handle.close();
2123
+ }
2124
+ await rename(tmp, target);
2125
+ try {
2126
+ const dir = await open(dirname(target), "r");
2127
+ try {
2128
+ await dir.sync();
2129
+ } finally {
2130
+ await dir.close();
2131
+ }
2132
+ } catch {
2133
+ }
2134
+ }
2135
+ async function inspectDocument(path) {
2136
+ let raw;
2137
+ try {
2138
+ await stat(path);
2139
+ } catch {
2140
+ return { status: "missing" };
2141
+ }
2142
+ try {
2143
+ raw = await readFile(path, "utf8");
2144
+ } catch (error) {
2145
+ return { status: "corrupt", detail: `unreadable: ${error instanceof Error ? error.message : String(error)}` };
2146
+ }
2147
+ if (raw.trim().length === 0) return { status: "empty" };
2148
+ let parsed;
2149
+ try {
2150
+ parsed = JSON.parse(raw);
2151
+ } catch (error) {
2152
+ return { status: "corrupt", detail: error instanceof Error ? error.message : "invalid JSON" };
2153
+ }
2154
+ if (Array.isArray(parsed)) return { status: "v1-legacy" };
2155
+ if (parsed === null || typeof parsed !== "object") return { status: "corrupt", detail: "not an object" };
2156
+ const doc = parsed;
2157
+ if (!Array.isArray(doc.items)) return { status: "corrupt", detail: "missing items array" };
2158
+ const version = typeof doc.schemaVersion === "number" ? doc.schemaVersion : 1;
2159
+ if (version > SCHEMA_VERSION) return { status: "future", schemaVersion: version };
2160
+ if (version < SCHEMA_VERSION) return { status: "v1-legacy" };
2161
+ return { status: "current", schemaVersion: version, count: doc.items.length };
2162
+ }
2163
+ async function readDocument(path, onCorrupt, quarantine = true) {
2164
+ const state = await inspectDocument(path);
2165
+ if (state.status === "missing" || state.status === "empty") return [];
2166
+ if (state.status === "future") {
2167
+ const error = new Error(
2168
+ `${path} was written by a newer OmniNode (schema v${state.schemaVersion} > v${SCHEMA_VERSION})`
2169
+ );
2170
+ onCorrupt?.(path, error);
2171
+ return [];
2172
+ }
2173
+ let raw;
2174
+ try {
2175
+ raw = await readFile(path, "utf8");
2176
+ } catch (error) {
2177
+ onCorrupt?.(path, error);
2178
+ return [];
2179
+ }
2180
+ try {
2181
+ const parsed = JSON.parse(raw);
2182
+ if (Array.isArray(parsed)) return parsed;
2183
+ return parsed.items ?? [];
2184
+ } catch (error) {
2185
+ if (quarantine) {
2186
+ const quarantinePath = `${path}.corrupt-${Date.now()}`;
2187
+ try {
2188
+ await rename(path, quarantinePath);
2189
+ logger.child({ component: "persistence" }).warn(
2190
+ `Quarantined unreadable store file ${path} \u2192 ${quarantinePath}`
2191
+ );
2192
+ } catch {
2193
+ }
2194
+ }
2195
+ onCorrupt?.(path, error);
2196
+ return [];
2197
+ }
2198
+ }
2113
2199
  var JsonFileStore = class {
2114
2200
  filePath;
2115
2201
  schemaVersion;
2116
2202
  onCorrupt;
2203
+ quarantine;
2117
2204
  log;
2118
2205
  queue = Promise.resolve();
2119
2206
  constructor(options) {
2120
2207
  this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
2121
- this.schemaVersion = options.schemaVersion ?? 1;
2208
+ this.schemaVersion = options.schemaVersion ?? SCHEMA_VERSION;
2122
2209
  this.onCorrupt = options.onCorrupt;
2210
+ this.quarantine = options.quarantine ?? true;
2123
2211
  this.log = logger.child({ component: "persistence", file: options.fileName });
2124
2212
  }
2125
2213
  get path() {
2126
2214
  return this.filePath;
2127
2215
  }
2128
- /** All items, or [] when missing/corrupt. */
2216
+ /** All items, or [] when missing/migrated-from-v1/corrupt. */
2129
2217
  async readAll() {
2130
- let raw;
2131
- try {
2132
- raw = await readFile(this.filePath, "utf8");
2133
- } catch {
2134
- return [];
2135
- }
2136
- try {
2137
- const parsed = JSON.parse(raw);
2138
- if (Array.isArray(parsed)) return parsed;
2139
- if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.items)) {
2140
- return parsed.items;
2141
- }
2142
- throw new Error("persisted file has an unrecognized shape");
2143
- } catch (error) {
2144
- this.log.warn(`Treating ${this.filePath} as empty \u2014 it could not be parsed.`);
2145
- this.onCorrupt?.(this.filePath, error);
2146
- return [];
2147
- }
2218
+ return readDocument(this.filePath, this.onCorrupt, this.quarantine);
2148
2219
  }
2149
2220
  async writeAll(items) {
2150
2221
  const document = { schemaVersion: this.schemaVersion, items };
2151
- const tmp = `${this.filePath}.tmp`;
2152
- await mkdir(dirname(this.filePath), { recursive: true });
2153
- await writeFile(tmp, `${JSON.stringify(document, null, 2)}
2154
- `, "utf8");
2155
- await rename(tmp, this.filePath);
2222
+ await writeFileAtomic(this.filePath, `${JSON.stringify(document, null, 2)}
2223
+ `);
2156
2224
  }
2157
2225
  /** Serializes an async operation against this file. */
2158
2226
  synchronized(operation) {
@@ -2455,7 +2523,7 @@ function sleep(ms) {
2455
2523
  }
2456
2524
 
2457
2525
  // src/audit/index.ts
2458
- import { appendFile, mkdir as mkdir2 } from "fs/promises";
2526
+ import { mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
2459
2527
  import { dirname as dirname2 } from "path";
2460
2528
  var FileAuditLog = class {
2461
2529
  filePath;
@@ -2467,8 +2535,15 @@ var FileAuditLog = class {
2467
2535
  async record(event) {
2468
2536
  try {
2469
2537
  await mkdir2(dirname2(this.filePath), { recursive: true });
2470
- await appendFile(this.filePath, `${JSON.stringify(event)}
2538
+ const { open: open2 } = await import("fs/promises");
2539
+ const handle = await open2(this.filePath, "a");
2540
+ try {
2541
+ await handle.appendFile(`${JSON.stringify(event)}
2471
2542
  `, "utf8");
2543
+ await handle.sync();
2544
+ } finally {
2545
+ await handle.close();
2546
+ }
2472
2547
  } catch (error) {
2473
2548
  this.log.warn(
2474
2549
  `Audit write failed: ${error instanceof Error ? error.message : String(error)}`
@@ -2477,7 +2552,6 @@ var FileAuditLog = class {
2477
2552
  }
2478
2553
  /** Recent events, oldest first; used by the CLI. */
2479
2554
  async recent(limit = 20) {
2480
- const { readFile: readFile2 } = await import("fs/promises");
2481
2555
  let raw;
2482
2556
  try {
2483
2557
  raw = await readFile2(this.filePath, "utf8");
@@ -4809,6 +4883,78 @@ function registerAuditCommand(program) {
4809
4883
  });
4810
4884
  }
4811
4885
 
4886
+ // src/persistence/index.ts
4887
+ var STORE_FILES = [
4888
+ "tasks.json",
4889
+ "pipelines.json",
4890
+ "reports.json",
4891
+ "plans.json",
4892
+ "memory.json"
4893
+ ];
4894
+ async function migrateProjectStores(directory = `${process.cwd()}/.omninode`, options = {}) {
4895
+ const reports = [];
4896
+ for (const file of STORE_FILES) {
4897
+ const path = `${directory}/${file}`;
4898
+ const state = await inspectDocument(path);
4899
+ if (state.status === "missing") {
4900
+ reports.push({ file, state: state.status, migrated: false });
4901
+ continue;
4902
+ }
4903
+ if (state.status === "current") {
4904
+ reports.push({ file, state: state.status, before: state.count, after: state.count, migrated: false });
4905
+ continue;
4906
+ }
4907
+ if (state.status === "future") {
4908
+ reports.push({
4909
+ file,
4910
+ state: state.status,
4911
+ migrated: false,
4912
+ note: `written by a newer OmniNode (schema v${state.schemaVersion}); not touched`
4913
+ });
4914
+ continue;
4915
+ }
4916
+ if (state.status === "corrupt") {
4917
+ reports.push({ file, state: state.status, migrated: false, note: state.detail });
4918
+ continue;
4919
+ }
4920
+ const items = await readDocument(path, void 0, false);
4921
+ if (!options.checkOnly) {
4922
+ await writeFileAtomic(path, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, items }, null, 2)}
4923
+ `);
4924
+ }
4925
+ const beforeCount = items.length;
4926
+ reports.push({
4927
+ file,
4928
+ state: state.status,
4929
+ before: beforeCount,
4930
+ after: items.length,
4931
+ migrated: !options.checkOnly,
4932
+ ...options.checkOnly ? { note: "would migrate" } : {}
4933
+ });
4934
+ }
4935
+ return reports;
4936
+ }
4937
+
4938
+ // src/cli/commands/migrate.ts
4939
+ function registerMigrateCommand(program) {
4940
+ program.command("migrate").description(
4941
+ `Inspect and upgrade persisted store files to schema v${SCHEMA_VERSION} (v1 data is migrated in place).`
4942
+ ).option("--check", "Only report what would change; do not write.").action(async (options) => {
4943
+ const reports = await migrateProjectStores(`${process.cwd()}/.omninode`, {
4944
+ checkOnly: options.check ?? false
4945
+ });
4946
+ let changed = 0;
4947
+ for (const report of reports) {
4948
+ if (report.migrated || report.state === "v1-legacy") changed += 1;
4949
+ const detail = report.state === "corrupt" ? ` (corrupt: ${report.note})` : report.state === "future" ? ` (${report.note})` : report.state === "v1-legacy" ? ` ${report.migrated ? "migrated" : "would migrate"} ${report.before ?? 0} \u2192 ${report.after ?? 0} item(s)` : report.state === "current" ? ` ${report.after ?? 0} item(s)` : "";
4950
+ console.log(`${report.file.padEnd(16)} ${report.state}${detail}`);
4951
+ }
4952
+ console.log(
4953
+ changed === 0 ? `All stores are at schema v${SCHEMA_VERSION}.` : `${options.check ? "Would migrate" : "Migrated"} ${changed} store file(s) to schema v${SCHEMA_VERSION}.`
4954
+ );
4955
+ });
4956
+ }
4957
+
4812
4958
  // src/cli/index.ts
4813
4959
  function createProgram() {
4814
4960
  const program = new Command();
@@ -4828,6 +4974,7 @@ function createProgram() {
4828
4974
  registerPlanCommands(program);
4829
4975
  registerStatusCommand(program);
4830
4976
  registerAuditCommand(program);
4977
+ registerMigrateCommand(program);
4831
4978
  registerRunAlias(program);
4832
4979
  return program;
4833
4980
  }