@hilbras/omninode 1.0.0 → 2.0.0-alpha.2

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/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 = "1.0.0";
9
+ var OMNINODE_VERSION = "2.0.0-alpha.2";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -1037,7 +1037,19 @@ var ProcessAgent = class {
1037
1037
  new AgentError(
1038
1038
  "AGENT_TIMEOUT",
1039
1039
  `Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
1040
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 4e3), stderr: stderr.slice(0, 2e3) } }
1040
+ {
1041
+ details: {
1042
+ operation: "agent.run",
1043
+ agent: this.config.name,
1044
+ durationMs: timeoutMs,
1045
+ // The process was started and may have performed side
1046
+ // effects before being killed — the outcome is unknown,
1047
+ // not failed (roadmap §6.3).
1048
+ dispatched: true,
1049
+ stdout: stdout.slice(0, 4e3),
1050
+ stderr: stderr.slice(0, 2e3)
1051
+ }
1052
+ }
1041
1053
  )
1042
1054
  );
1043
1055
  return;
@@ -1302,40 +1314,54 @@ function registerRoleCommands(program) {
1302
1314
  });
1303
1315
  }
1304
1316
 
1305
- // src/tasks/store.ts
1317
+ // src/persistence/json-file-store.ts
1306
1318
  import { mkdir, readFile, rename, writeFile } from "fs/promises";
1307
1319
  import { dirname } from "path";
1308
- var FileTaskStore = class {
1320
+ var JsonFileStore = class {
1309
1321
  filePath;
1310
- /**
1311
- * Read-modify-write operations are serialized: parallel pipeline tasks
1312
- * would otherwise race on the JSON file (last writer wins).
1313
- */
1322
+ schemaVersion;
1323
+ onCorrupt;
1324
+ log;
1314
1325
  queue = Promise.resolve();
1315
- constructor(directory = `${process.cwd()}/.omninode`) {
1316
- this.filePath = `${directory}/tasks.json`;
1326
+ constructor(options) {
1327
+ this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
1328
+ this.schemaVersion = options.schemaVersion ?? 1;
1329
+ this.onCorrupt = options.onCorrupt;
1330
+ this.log = logger.child({ component: "persistence", file: options.fileName });
1317
1331
  }
1318
- async save(task) {
1319
- return this.synchronized(async () => {
1320
- const tasks = await this.load();
1321
- const index = tasks.findIndex((t) => t.id === task.id);
1322
- if (index >= 0) tasks[index] = task;
1323
- else tasks.push(task);
1324
- await this.write(tasks);
1325
- });
1332
+ get path() {
1333
+ return this.filePath;
1326
1334
  }
1327
- async get(id) {
1328
- return this.synchronized(async () => {
1329
- const tasks = await this.load();
1330
- return tasks.find((t) => t.id === id);
1331
- });
1335
+ /** All items, or [] when missing/corrupt. */
1336
+ async readAll() {
1337
+ let raw;
1338
+ try {
1339
+ raw = await readFile(this.filePath, "utf8");
1340
+ } catch {
1341
+ return [];
1342
+ }
1343
+ try {
1344
+ const parsed = JSON.parse(raw);
1345
+ if (Array.isArray(parsed)) return parsed;
1346
+ if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.items)) {
1347
+ return parsed.items;
1348
+ }
1349
+ throw new Error("persisted file has an unrecognized shape");
1350
+ } catch (error) {
1351
+ this.log.warn(`Treating ${this.filePath} as empty \u2014 it could not be parsed.`);
1352
+ this.onCorrupt?.(this.filePath, error);
1353
+ return [];
1354
+ }
1332
1355
  }
1333
- async list(filter = {}) {
1334
- return this.synchronized(async () => {
1335
- const tasks = await this.load();
1336
- return tasks.filter((task) => filter.status ? task.status === filter.status : true).filter((task) => filter.project ? task.project === filter.project : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1337
- });
1356
+ async writeAll(items) {
1357
+ const document = { schemaVersion: this.schemaVersion, items };
1358
+ const tmp = `${this.filePath}.tmp`;
1359
+ await mkdir(dirname(this.filePath), { recursive: true });
1360
+ await writeFile(tmp, `${JSON.stringify(document, null, 2)}
1361
+ `, "utf8");
1362
+ await rename(tmp, this.filePath);
1338
1363
  }
1364
+ /** Serializes an async operation against this file. */
1339
1365
  synchronized(operation) {
1340
1366
  const result = this.queue.then(operation, operation);
1341
1367
  this.queue = result.then(
@@ -1344,21 +1370,37 @@ var FileTaskStore = class {
1344
1370
  );
1345
1371
  return result;
1346
1372
  }
1347
- async load() {
1348
- try {
1349
- const raw = await readFile(this.filePath, "utf8");
1350
- const parsed = JSON.parse(raw);
1351
- return Array.isArray(parsed) ? parsed : [];
1352
- } catch {
1353
- return [];
1354
- }
1373
+ /** Read-modify-write helper: applies `mutate` to the current items. */
1374
+ async mutate(mutate) {
1375
+ return this.synchronized(async () => {
1376
+ const items = await this.readAll();
1377
+ const [next, result] = await mutate(items);
1378
+ await this.writeAll(next);
1379
+ return result;
1380
+ });
1355
1381
  }
1356
- async write(tasks) {
1357
- const tmp = `${this.filePath}.tmp`;
1358
- await mkdir(dirname(this.filePath), { recursive: true });
1359
- await writeFile(tmp, `${JSON.stringify(tasks, null, 2)}
1360
- `, "utf8");
1361
- await rename(tmp, this.filePath);
1382
+ };
1383
+
1384
+ // src/tasks/store.ts
1385
+ var FileTaskStore = class extends JsonFileStore {
1386
+ constructor(directory) {
1387
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "tasks.json" });
1388
+ }
1389
+ save(task) {
1390
+ return this.mutate((tasks) => {
1391
+ const index = tasks.findIndex((t) => t.id === task.id);
1392
+ if (index >= 0) tasks[index] = task;
1393
+ else tasks.push(task);
1394
+ return [tasks, void 0];
1395
+ });
1396
+ }
1397
+ async get(id) {
1398
+ const tasks = await this.readAll();
1399
+ return tasks.find((t) => t.id === id);
1400
+ }
1401
+ async list(filter = {}) {
1402
+ const tasks = await this.readAll();
1403
+ return tasks.filter((task) => filter.status ? task.status === filter.status : true).filter((task) => filter.project ? task.project === filter.project : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1362
1404
  }
1363
1405
  };
1364
1406
 
@@ -1367,12 +1409,18 @@ import { randomBytes } from "crypto";
1367
1409
  var ALLOWED_TRANSITIONS = {
1368
1410
  created: ["queued", "running", "cancelled"],
1369
1411
  queued: ["running", "cancelled"],
1370
- running: ["completed", "failed", "cancelled"],
1412
+ running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
1371
1413
  waiting: ["running", "cancelled"],
1372
1414
  completed: [],
1373
- failed: [],
1374
- cancelled: []
1415
+ // failed → running is the only re-entry: bounded automatic retry (§6.4).
1416
+ failed: ["running"],
1417
+ timed_out: [],
1418
+ cancelled: [],
1419
+ unknown: [],
1420
+ partially_completed: []
1375
1421
  };
1422
+ var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
1423
+ var EXECUTION_HISTORY_LIMIT = 10;
1376
1424
  var TaskEngine = class {
1377
1425
  constructor(options) {
1378
1426
  this.options = options;
@@ -1404,6 +1452,7 @@ var TaskEngine = class {
1404
1452
  id: generateTaskId(),
1405
1453
  objective: input.objective.trim(),
1406
1454
  status: "created",
1455
+ attempt: 0,
1407
1456
  createdAt: now,
1408
1457
  updatedAt: now,
1409
1458
  ...input.project !== void 0 ? { project: input.project } : {},
@@ -1420,8 +1469,10 @@ var TaskEngine = class {
1420
1469
  const task = await this.mustGet(id);
1421
1470
  return this.transition(task, "queued");
1422
1471
  }
1423
- /** Runs the task through its assigned agent and records the result. */
1424
- async run(id) {
1472
+ /** Runs the task through its assigned agent and records the result.
1473
+ * `maxAttempts` enables bounded automatic retries of *retryable* errors
1474
+ * with linear backoff; unknown/timed_out outcomes are never auto-retried. */
1475
+ async run(id, options = {}) {
1425
1476
  const task = await this.mustGet(id);
1426
1477
  if (task.status !== "created" && task.status !== "queued") {
1427
1478
  throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
@@ -1434,49 +1485,84 @@ var TaskEngine = class {
1434
1485
  throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
1435
1486
  }
1436
1487
  const role = task.role ? this.options.roles.get(task.role) : void 0;
1437
- const running = await this.transition(task, "running");
1438
1488
  this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1439
- let background = running.context?.background;
1489
+ let background = task.context?.background;
1440
1490
  if (this.options.memory) {
1441
1491
  const memoryContext = await this.options.memory.gatherContext({
1442
- objective: running.objective,
1443
- ...running.role ? { role: running.role } : {},
1444
- ...running.project ? { project: running.project } : {}
1492
+ objective: task.objective,
1493
+ ...task.role ? { role: task.role } : {},
1494
+ ...task.project ? { project: task.project } : {}
1445
1495
  });
1446
1496
  if (memoryContext.length > 0) {
1447
1497
  background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
1448
1498
  }
1449
1499
  }
1450
- let output;
1451
- try {
1452
- output = await agent.run({
1453
- taskId: running.id,
1454
- objective: running.objective,
1455
- ...role ? { role } : {},
1456
- ...background !== void 0 ? { context: background } : {}
1457
- });
1458
- } catch (error) {
1459
- const message = error instanceof Error ? error.message : String(error);
1460
- this.log.error(`Task ${task.id} failed: ${message}`);
1461
- return this.transition(running, "failed", {
1462
- result: { error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }
1500
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
1501
+ const backoffMs = options.retryBackoffMs ?? 0;
1502
+ let current = task;
1503
+ for (let i = 0; i < maxAttempts; i += 1) {
1504
+ const attempt = current.attempt + 1;
1505
+ const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1506
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1507
+ current = await this.transition(current, "running", { attempt });
1508
+ let output;
1509
+ let thrown;
1510
+ try {
1511
+ output = await agent.run({
1512
+ taskId: current.id,
1513
+ objective: current.objective,
1514
+ ...role ? { role } : {},
1515
+ ...background !== void 0 ? { context: background } : {}
1516
+ });
1517
+ } catch (error) {
1518
+ thrown = error;
1519
+ }
1520
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1521
+ if (thrown !== void 0 || output === void 0) {
1522
+ const outcome = classifyThrown(thrown);
1523
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
1524
+ const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
1525
+ current = await this.transition(current, outcome, {
1526
+ attempt,
1527
+ lastError: message,
1528
+ execution: {
1529
+ executionId,
1530
+ attempt,
1531
+ startedAt,
1532
+ finishedAt,
1533
+ outcome,
1534
+ error: message
1535
+ }
1536
+ });
1537
+ this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
1538
+ if (retryable) {
1539
+ if (backoffMs > 0) await sleep(backoffMs * attempt);
1540
+ continue;
1541
+ }
1542
+ if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
1543
+ return current;
1544
+ }
1545
+ const result = {
1546
+ ...output.summary !== void 0 ? { summary: output.summary } : {},
1547
+ ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1548
+ // Keep the verbatim output (bounded) for report extraction and audit.
1549
+ ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1550
+ ...output.error !== void 0 ? { error: output.error } : {},
1551
+ finishedAt
1552
+ };
1553
+ const finalStatus = output.status === "completed" ? "completed" : "failed";
1554
+ current = await this.transition(current, finalStatus, {
1555
+ attempt,
1556
+ result,
1557
+ execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
1463
1558
  });
1559
+ this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
1560
+ break;
1464
1561
  }
1465
- const result = {
1466
- ...output.summary !== void 0 ? { summary: output.summary } : {},
1467
- ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1468
- // Keep the verbatim output (bounded) for report extraction and audit.
1469
- ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1470
- ...output.error !== void 0 ? { error: output.error } : {},
1471
- finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1472
- };
1473
- const finalStatus = output.status === "completed" ? "completed" : "failed";
1474
- const finished = await this.transition(running, finalStatus, { result });
1475
- this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
1476
1562
  if (this.options.memory) {
1477
- await this.options.memory.recordTaskOutcome(finished);
1563
+ await this.options.memory.recordTaskOutcome(current);
1478
1564
  }
1479
- return finished;
1565
+ return current;
1480
1566
  }
1481
1567
  async cancel(id) {
1482
1568
  const task = await this.mustGet(id);
@@ -1489,13 +1575,14 @@ var TaskEngine = class {
1489
1575
  }
1490
1576
  return this.transition(task, "cancelled");
1491
1577
  }
1492
- /** Error recovery (§27 Phase 9): reset a failed/cancelled task so it can run again. */
1578
+ /** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
1493
1579
  async retry(id) {
1494
1580
  const task = await this.mustGet(id);
1495
- if (task.status !== "failed" && task.status !== "cancelled") {
1581
+ const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
1582
+ if (!retryableStates.includes(task.status)) {
1496
1583
  throw new TaskError(
1497
1584
  "TASK_INVALID",
1498
- `Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1585
+ `Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1499
1586
  );
1500
1587
  }
1501
1588
  const { result: _discarded, ...rest } = task;
@@ -1527,10 +1614,14 @@ var TaskEngine = class {
1527
1614
  if (!allowed.includes(to)) {
1528
1615
  throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
1529
1616
  }
1617
+ const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
1530
1618
  const updated = {
1531
1619
  ...task,
1532
1620
  status: to,
1533
1621
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1622
+ ...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
1623
+ ...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
1624
+ ...executions !== void 0 ? { executions } : {},
1534
1625
  ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
1535
1626
  };
1536
1627
  await this.options.store.save(updated);
@@ -1551,6 +1642,19 @@ var TaskEngine = class {
1551
1642
  function generateTaskId() {
1552
1643
  return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1553
1644
  }
1645
+ function errorCode(error) {
1646
+ return error?.code ?? "";
1647
+ }
1648
+ function classifyThrown(error) {
1649
+ if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
1650
+ const dispatched = error.details?.dispatched;
1651
+ return dispatched === false ? "timed_out" : "unknown";
1652
+ }
1653
+ return "failed";
1654
+ }
1655
+ function sleep(ms) {
1656
+ return new Promise((resolve) => setTimeout(resolve, ms));
1657
+ }
1554
1658
 
1555
1659
  // src/audit/index.ts
1556
1660
  import { appendFile, mkdir as mkdir2 } from "fs/promises";
@@ -1575,10 +1679,10 @@ var FileAuditLog = class {
1575
1679
  }
1576
1680
  /** Recent events, oldest first; used by the CLI. */
1577
1681
  async recent(limit = 20) {
1578
- const { readFile: readFile6 } = await import("fs/promises");
1682
+ const { readFile: readFile2 } = await import("fs/promises");
1579
1683
  let raw;
1580
1684
  try {
1581
- raw = await readFile6(this.filePath, "utf8");
1685
+ raw = await readFile2(this.filePath, "utf8");
1582
1686
  } catch {
1583
1687
  return [];
1584
1688
  }
@@ -1594,8 +1698,6 @@ var FileAuditLog = class {
1594
1698
  };
1595
1699
 
1596
1700
  // src/memory/local.ts
1597
- import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1598
- import { dirname as dirname3 } from "path";
1599
1701
  var STOPWORDS = /* @__PURE__ */ new Set([
1600
1702
  "the",
1601
1703
  "and",
@@ -1652,29 +1754,24 @@ function relevanceScore(queryKeywords, entry2) {
1652
1754
  }
1653
1755
  return overlap * 2 + tagMatches * 1.5;
1654
1756
  }
1655
- var LocalMemoryProvider = class {
1757
+ var LocalMemoryProvider = class extends JsonFileStore {
1656
1758
  name = "local";
1657
- filePath;
1658
- queue = Promise.resolve();
1659
- constructor(directory = `${process.cwd()}/.omninode`) {
1660
- this.filePath = `${directory}/memory.json`;
1759
+ constructor(directory) {
1760
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
1661
1761
  }
1662
1762
  async query(query) {
1663
- return this.synchronized(async () => {
1664
- const entries = await this.load();
1665
- const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter(
1666
- (entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
1667
- );
1668
- if (!query.text || query.text.trim().length === 0) {
1669
- return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
1670
- }
1671
- const queryKeywords = keywords(query.text);
1672
- return filtered.map((entry2) => ({ entry: entry2, score: relevanceScore(queryKeywords, entry2) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry: entry2 }) => entry2);
1673
- });
1763
+ const entries = await this.readAll();
1764
+ const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter(
1765
+ (entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
1766
+ );
1767
+ if (!query.text || query.text.trim().length === 0) {
1768
+ return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
1769
+ }
1770
+ const queryKeywords = keywords(query.text);
1771
+ return filtered.map((entry2) => ({ entry: entry2, score: relevanceScore(queryKeywords, entry2) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry: entry2 }) => entry2);
1674
1772
  }
1675
- async write(entry2) {
1676
- return this.synchronized(async () => {
1677
- const entries = await this.load();
1773
+ write(entry2) {
1774
+ return this.mutate((entries) => {
1678
1775
  const index = entries.findIndex((e) => e.key === entry2.key);
1679
1776
  const stamped = {
1680
1777
  ...entry2,
@@ -1683,35 +1780,11 @@ var LocalMemoryProvider = class {
1683
1780
  };
1684
1781
  if (index >= 0) entries[index] = stamped;
1685
1782
  else entries.push(stamped);
1686
- await this.saveAll(entries);
1783
+ return [entries, void 0];
1687
1784
  });
1688
1785
  }
1689
1786
  async count() {
1690
- return (await this.load()).length;
1691
- }
1692
- synchronized(operation) {
1693
- const result = this.queue.then(operation, operation);
1694
- this.queue = result.then(
1695
- () => void 0,
1696
- () => void 0
1697
- );
1698
- return result;
1699
- }
1700
- async load() {
1701
- try {
1702
- const raw = await readFile2(this.filePath, "utf8");
1703
- const parsed = JSON.parse(raw);
1704
- return Array.isArray(parsed) ? parsed : [];
1705
- } catch {
1706
- return [];
1707
- }
1708
- }
1709
- async saveAll(entries) {
1710
- const tmp = `${this.filePath}.tmp`;
1711
- await mkdir3(dirname3(this.filePath), { recursive: true });
1712
- await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
1713
- `, "utf8");
1714
- await rename2(tmp, this.filePath);
1787
+ return (await this.readAll()).length;
1715
1788
  }
1716
1789
  };
1717
1790
 
@@ -1981,7 +2054,7 @@ function registerTaskCommands(program) {
1981
2054
  const cancelled = await engine.cancel(taskId);
1982
2055
  console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
1983
2056
  });
1984
- task.command("retry <task-id>").description("Reset a failed/cancelled task to created; combine with --run to execute it again.").option("--run", "Run the task again immediately after resetting it.").action(async (taskId, options) => {
2057
+ task.command("retry <task-id>").description("Reset a failed, timed out, unknown or cancelled task to created; combine with --run to execute it again.").option("--run", "Run the task again immediately after resetting it.").action(async (taskId, options) => {
1985
2058
  const config = loadConfig();
1986
2059
  const engine = createTaskEngine(config);
1987
2060
  const reset = await engine.retry(taskId);
@@ -2004,6 +2077,15 @@ function printOutcome(task, options = {}) {
2004
2077
  if (task.context?.files) console.log(` files: ${task.context.files.join(", ")}`);
2005
2078
  console.log(` objective: ${task.objective}`);
2006
2079
  }
2080
+ if (options.verbose && task.attempt > 0) {
2081
+ console.log(` attempt: ${task.attempt}`);
2082
+ }
2083
+ if (task.lastError) console.log(` error: ${task.lastError}`);
2084
+ if (options.verbose && task.executions && task.executions.length > 0) {
2085
+ console.log(
2086
+ ` executions: ${task.executions.map((e) => `#${e.attempt}:${e.outcome}`).join(", ")}`
2087
+ );
2088
+ }
2007
2089
  const result = task.result;
2008
2090
  if (!result) return;
2009
2091
  if (result.summary) console.log(` summary: ${result.summary}`);
@@ -2228,84 +2310,39 @@ var ReportService = class {
2228
2310
  };
2229
2311
 
2230
2312
  // src/reports/store.ts
2231
- import { mkdir as mkdir4, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
2232
- import { dirname as dirname4 } from "path";
2233
- var FileReportStore = class {
2234
- filePath;
2235
- queue = Promise.resolve();
2236
- constructor(directory = `${process.cwd()}/.omninode`) {
2237
- this.filePath = `${directory}/reports.json`;
2313
+ var FileReportStore = class extends JsonFileStore {
2314
+ constructor(directory) {
2315
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "reports.json" });
2238
2316
  }
2239
2317
  async saveReports(reports) {
2240
- return this.synchronized(async () => {
2241
- const entries = await this.load();
2318
+ await this.mutate((entries) => {
2242
2319
  for (const report of reports) {
2243
- const index = entries.findIndex(
2244
- (e) => e.kind === "report" && e.report.id === report.id
2245
- );
2320
+ const index = entries.findIndex((e) => e.kind === "report" && e.report.id === report.id);
2246
2321
  const entry2 = { kind: "report", report };
2247
2322
  if (index >= 0) entries[index] = entry2;
2248
2323
  else entries.push(entry2);
2249
2324
  }
2250
- await this.write(entries);
2325
+ return [entries, void 0];
2251
2326
  });
2252
2327
  }
2253
2328
  async listReports(filter = {}) {
2254
- return this.synchronized(async () => {
2255
- const entries = await this.load();
2256
- return entries.filter((e) => e.kind === "report").map((e) => e.report).filter((report) => filter.taskId ? report.taskId === filter.taskId : true).filter((report) => filter.agent ? report.agent === filter.agent : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2257
- });
2329
+ return (await this.readAll()).filter((e) => e.kind === "report").map((e) => e.report).filter((report) => filter.taskId ? report.taskId === filter.taskId : true).filter((report) => filter.agent ? report.agent === filter.agent : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2258
2330
  }
2259
2331
  async saveCombined(combined) {
2260
- return this.synchronized(async () => {
2261
- const entries = await this.load();
2262
- const index = entries.findIndex(
2263
- (e) => e.kind === "combined" && e.combined.id === combined.id
2264
- );
2332
+ await this.mutate((entries) => {
2333
+ const index = entries.findIndex((e) => e.kind === "combined" && e.combined.id === combined.id);
2265
2334
  const entry2 = { kind: "combined", combined };
2266
2335
  if (index >= 0) entries[index] = entry2;
2267
2336
  else entries.push(entry2);
2268
- await this.write(entries);
2337
+ return [entries, void 0];
2269
2338
  });
2270
2339
  }
2271
2340
  async getCombined(id) {
2272
- return this.synchronized(async () => {
2273
- const entries = await this.load();
2274
- const found = entries.find((e) => e.kind === "combined" && e.combined.id === id);
2275
- return found?.kind === "combined" ? found.combined : void 0;
2276
- });
2341
+ const found = (await this.readAll()).find((e) => e.kind === "combined" && e.combined.id === id);
2342
+ return found?.kind === "combined" ? found.combined : void 0;
2277
2343
  }
2278
2344
  async listCombined(filter = {}) {
2279
- return this.synchronized(async () => {
2280
- const entries = await this.load();
2281
- return entries.filter((e) => e.kind === "combined").map((e) => e.combined).filter(
2282
- (combined) => filter.pipelineRunId ? combined.pipelineRunId === filter.pipelineRunId : true
2283
- ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2284
- });
2285
- }
2286
- synchronized(operation) {
2287
- const result = this.queue.then(operation, operation);
2288
- this.queue = result.then(
2289
- () => void 0,
2290
- () => void 0
2291
- );
2292
- return result;
2293
- }
2294
- async load() {
2295
- try {
2296
- const raw = await readFile3(this.filePath, "utf8");
2297
- const parsed = JSON.parse(raw);
2298
- return Array.isArray(parsed) ? parsed : [];
2299
- } catch {
2300
- return [];
2301
- }
2302
- }
2303
- async write(entries) {
2304
- const tmp = `${this.filePath}.tmp`;
2305
- await mkdir4(dirname4(this.filePath), { recursive: true });
2306
- await writeFile3(tmp, `${JSON.stringify(entries, null, 2)}
2307
- `, "utf8");
2308
- await rename3(tmp, this.filePath);
2345
+ return (await this.readAll()).filter((e) => e.kind === "combined").map((e) => e.combined).filter((combined) => filter.pipelineRunId ? combined.pipelineRunId === filter.pipelineRunId : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2309
2346
  }
2310
2347
  };
2311
2348
 
@@ -2789,7 +2826,7 @@ ${contribution}`);
2789
2826
  }
2790
2827
  const stepRun = {
2791
2828
  stepId: step.id,
2792
- status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
2829
+ status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
2793
2830
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
2794
2831
  ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
2795
2832
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
@@ -2800,8 +2837,10 @@ ${contribution}`);
2800
2837
  run2.reports = [...allReports];
2801
2838
  await this.options.store.save(run2);
2802
2839
  }
2803
- const failed = [...outcomes.values()].some((o) => o.status === "failed");
2804
- const finalStatus = failed ? "failed" : "completed";
2840
+ const allOutcomes = [...outcomes.values()];
2841
+ const failed = allOutcomes.some((o) => o.status === "failed");
2842
+ const partial = !failed && allOutcomes.some((o) => o.status === "partial");
2843
+ const finalStatus = failed ? "failed" : partial ? "partial" : "completed";
2805
2844
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
2806
2845
  let resultSummary;
2807
2846
  for (let i = def.steps.length - 1; i >= 0; i -= 1) {
@@ -2907,6 +2946,7 @@ ${contribution}`);
2907
2946
  )
2908
2947
  );
2909
2948
  const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
2949
+ const completedTasks = results.filter((r) => r.status === "completed");
2910
2950
  const failedTasks = results.filter((r) => r.status !== "completed");
2911
2951
  const lines = results.map((result, index) => {
2912
2952
  const agentName = agentNames[index] ?? "unknown";
@@ -2916,12 +2956,15 @@ ${contribution}`);
2916
2956
  const reports = results.flatMap((r) => r.result?.reports ?? []);
2917
2957
  const summary = lines.join("\n");
2918
2958
  if (failedTasks.length > 0) {
2919
- const firstError = failedTasks[0]?.result?.error ?? "unknown error";
2959
+ const firstError = failedTasks[0]?.result?.error ?? failedTasks[0]?.lastError ?? "unknown outcome";
2960
+ const states = failedTasks.map((r) => r.status).join(", ");
2920
2961
  return {
2921
- status: "failed",
2962
+ // Some agents succeeded → the step produced usable but incomplete
2963
+ // intelligence: partial, not failed (roadmap §9.3).
2964
+ status: completedTasks.length > 0 ? "partial" : "failed",
2922
2965
  summary,
2923
2966
  taskIds: created.map((t) => t.id),
2924
- error: `${failedTasks.length}/${results.length} task(s) failed \u2014 ${firstError}`,
2967
+ error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
2925
2968
  reports
2926
2969
  };
2927
2970
  }
@@ -2937,7 +2980,9 @@ ${contribution}`);
2937
2980
  const result = await this.options.tasks.run(task.id);
2938
2981
  if (result.status !== "completed") {
2939
2982
  return {
2940
- status: "failed",
2983
+ // Unknown/timed_out outcomes are not failures — the work may have
2984
+ // happened remotely; the step is incomplete, not broken.
2985
+ status: result.status === "unknown" || result.status === "timed_out" ? "partial" : "failed",
2941
2986
  summary: result.result?.summary ?? "",
2942
2987
  taskIds: [task.id],
2943
2988
  error: result.result?.error ?? `task ${task.id} did not complete`,
@@ -3053,114 +3098,44 @@ function createDefaultChatFn(providers) {
3053
3098
  }
3054
3099
 
3055
3100
  // src/pipelines/store.ts
3056
- import { mkdir as mkdir5, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
3057
- import { dirname as dirname5 } from "path";
3058
- var FilePipelineRunStore = class {
3059
- filePath;
3060
- queue = Promise.resolve();
3061
- constructor(directory = `${process.cwd()}/.omninode`) {
3062
- this.filePath = `${directory}/pipelines.json`;
3101
+ var FilePipelineRunStore = class extends JsonFileStore {
3102
+ constructor(directory) {
3103
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "pipelines.json" });
3063
3104
  }
3064
- async save(run2) {
3065
- return this.synchronized(async () => {
3066
- const runs = await this.load();
3105
+ save(run2) {
3106
+ return this.mutate((runs) => {
3067
3107
  const index = runs.findIndex((r) => r.id === run2.id);
3068
3108
  if (index >= 0) runs[index] = run2;
3069
3109
  else runs.push(run2);
3070
- await this.write(runs);
3110
+ return [runs, void 0];
3071
3111
  });
3072
3112
  }
3073
3113
  async get(id) {
3074
- return this.synchronized(async () => {
3075
- const runs = await this.load();
3076
- return runs.find((r) => r.id === id);
3077
- });
3114
+ return (await this.readAll()).find((r) => r.id === id);
3078
3115
  }
3079
3116
  async list(filter = {}) {
3080
- return this.synchronized(async () => {
3081
- const runs = await this.load();
3082
- return runs.filter((run2) => filter.pipelineId ? run2.pipelineId === filter.pipelineId : true).filter((run2) => filter.status ? run2.status === filter.status : true).sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? ""));
3083
- });
3084
- }
3085
- synchronized(operation) {
3086
- const result = this.queue.then(operation, operation);
3087
- this.queue = result.then(
3088
- () => void 0,
3089
- () => void 0
3090
- );
3091
- return result;
3092
- }
3093
- async load() {
3094
- try {
3095
- const raw = await readFile4(this.filePath, "utf8");
3096
- const parsed = JSON.parse(raw);
3097
- return Array.isArray(parsed) ? parsed : [];
3098
- } catch {
3099
- return [];
3100
- }
3101
- }
3102
- async write(runs) {
3103
- const tmp = `${this.filePath}.tmp`;
3104
- await mkdir5(dirname5(this.filePath), { recursive: true });
3105
- await writeFile4(tmp, `${JSON.stringify(runs, null, 2)}
3106
- `, "utf8");
3107
- await rename4(tmp, this.filePath);
3117
+ return (await this.readAll()).filter((run2) => filter.pipelineId ? run2.pipelineId === filter.pipelineId : true).filter((run2) => filter.status ? run2.status === filter.status : true).sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? ""));
3108
3118
  }
3109
3119
  };
3110
3120
 
3111
3121
  // src/planner/store.ts
3112
- import { mkdir as mkdir6, readFile as readFile5, rename as rename5, writeFile as writeFile5 } from "fs/promises";
3113
- import { dirname as dirname6 } from "path";
3114
- var FilePlanStore = class {
3115
- filePath;
3116
- queue = Promise.resolve();
3117
- constructor(directory = `${process.cwd()}/.omninode`) {
3118
- this.filePath = `${directory}/plans.json`;
3122
+ var FilePlanStore = class extends JsonFileStore {
3123
+ constructor(directory) {
3124
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "plans.json" });
3119
3125
  }
3120
- async save(plan) {
3121
- return this.synchronized(async () => {
3122
- const plans = await this.load();
3126
+ save(plan) {
3127
+ return this.mutate((plans) => {
3123
3128
  const index = plans.findIndex((p) => p.id === plan.id);
3124
3129
  if (index >= 0) plans[index] = plan;
3125
3130
  else plans.push(plan);
3126
- await this.write(plans);
3131
+ return [plans, void 0];
3127
3132
  });
3128
3133
  }
3129
3134
  async get(id) {
3130
- return this.synchronized(async () => {
3131
- const plans = await this.load();
3132
- return plans.find((p) => p.id === id);
3133
- });
3135
+ return (await this.readAll()).find((p) => p.id === id);
3134
3136
  }
3135
3137
  async list(filter = {}) {
3136
- return this.synchronized(async () => {
3137
- const plans = await this.load();
3138
- return plans.filter((plan) => filter.pipelineRunId ? plan.pipelineRunId === filter.pipelineRunId : true).filter((plan) => filter.taskId ? plan.taskId === filter.taskId : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
3139
- });
3140
- }
3141
- synchronized(operation) {
3142
- const result = this.queue.then(operation, operation);
3143
- this.queue = result.then(
3144
- () => void 0,
3145
- () => void 0
3146
- );
3147
- return result;
3148
- }
3149
- async load() {
3150
- try {
3151
- const raw = await readFile5(this.filePath, "utf8");
3152
- const parsed = JSON.parse(raw);
3153
- return Array.isArray(parsed) ? parsed : [];
3154
- } catch {
3155
- return [];
3156
- }
3157
- }
3158
- async write(plans) {
3159
- const tmp = `${this.filePath}.tmp`;
3160
- await mkdir6(dirname6(this.filePath), { recursive: true });
3161
- await writeFile5(tmp, `${JSON.stringify(plans, null, 2)}
3162
- `, "utf8");
3163
- await rename5(tmp, this.filePath);
3138
+ return (await this.readAll()).filter((plan) => filter.pipelineRunId ? plan.pipelineRunId === filter.pipelineRunId : true).filter((plan) => filter.taskId ? plan.taskId === filter.taskId : true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
3164
3139
  }
3165
3140
  };
3166
3141