@hilbras/omninode 2.0.0-alpha.1 → 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/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.1 — v2 Phase 1: Architecture & API Stabilization** (the v2
21
+ **v2.0.0-alpha.2 — v2 Phase 2: Execution Reliability** (after Phase 1: architecture & API stabilization) (the v2
22
22
  roadmap is [docs/ROADMAP_V2.md](docs/ROADMAP_V2.md); the v1 plan is complete:
23
23
  the
24
24
  full loop from §28 runs in one command —
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.1";
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;
@@ -1397,12 +1409,18 @@ import { randomBytes } from "crypto";
1397
1409
  var ALLOWED_TRANSITIONS = {
1398
1410
  created: ["queued", "running", "cancelled"],
1399
1411
  queued: ["running", "cancelled"],
1400
- running: ["completed", "failed", "cancelled"],
1412
+ running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
1401
1413
  waiting: ["running", "cancelled"],
1402
1414
  completed: [],
1403
- failed: [],
1404
- 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: []
1405
1421
  };
1422
+ var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
1423
+ var EXECUTION_HISTORY_LIMIT = 10;
1406
1424
  var TaskEngine = class {
1407
1425
  constructor(options) {
1408
1426
  this.options = options;
@@ -1434,6 +1452,7 @@ var TaskEngine = class {
1434
1452
  id: generateTaskId(),
1435
1453
  objective: input.objective.trim(),
1436
1454
  status: "created",
1455
+ attempt: 0,
1437
1456
  createdAt: now,
1438
1457
  updatedAt: now,
1439
1458
  ...input.project !== void 0 ? { project: input.project } : {},
@@ -1450,8 +1469,10 @@ var TaskEngine = class {
1450
1469
  const task = await this.mustGet(id);
1451
1470
  return this.transition(task, "queued");
1452
1471
  }
1453
- /** Runs the task through its assigned agent and records the result. */
1454
- 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 = {}) {
1455
1476
  const task = await this.mustGet(id);
1456
1477
  if (task.status !== "created" && task.status !== "queued") {
1457
1478
  throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
@@ -1464,49 +1485,84 @@ var TaskEngine = class {
1464
1485
  throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
1465
1486
  }
1466
1487
  const role = task.role ? this.options.roles.get(task.role) : void 0;
1467
- const running = await this.transition(task, "running");
1468
1488
  this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1469
- let background = running.context?.background;
1489
+ let background = task.context?.background;
1470
1490
  if (this.options.memory) {
1471
1491
  const memoryContext = await this.options.memory.gatherContext({
1472
- objective: running.objective,
1473
- ...running.role ? { role: running.role } : {},
1474
- ...running.project ? { project: running.project } : {}
1492
+ objective: task.objective,
1493
+ ...task.role ? { role: task.role } : {},
1494
+ ...task.project ? { project: task.project } : {}
1475
1495
  });
1476
1496
  if (memoryContext.length > 0) {
1477
1497
  background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
1478
1498
  }
1479
1499
  }
1480
- let output;
1481
- try {
1482
- output = await agent.run({
1483
- taskId: running.id,
1484
- objective: running.objective,
1485
- ...role ? { role } : {},
1486
- ...background !== void 0 ? { context: background } : {}
1487
- });
1488
- } catch (error) {
1489
- const message = error instanceof Error ? error.message : String(error);
1490
- this.log.error(`Task ${task.id} failed: ${message}`);
1491
- return this.transition(running, "failed", {
1492
- 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 }
1493
1558
  });
1559
+ this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
1560
+ break;
1494
1561
  }
1495
- const result = {
1496
- ...output.summary !== void 0 ? { summary: output.summary } : {},
1497
- ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1498
- // Keep the verbatim output (bounded) for report extraction and audit.
1499
- ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1500
- ...output.error !== void 0 ? { error: output.error } : {},
1501
- finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1502
- };
1503
- const finalStatus = output.status === "completed" ? "completed" : "failed";
1504
- const finished = await this.transition(running, finalStatus, { result });
1505
- this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
1506
1562
  if (this.options.memory) {
1507
- await this.options.memory.recordTaskOutcome(finished);
1563
+ await this.options.memory.recordTaskOutcome(current);
1508
1564
  }
1509
- return finished;
1565
+ return current;
1510
1566
  }
1511
1567
  async cancel(id) {
1512
1568
  const task = await this.mustGet(id);
@@ -1519,13 +1575,14 @@ var TaskEngine = class {
1519
1575
  }
1520
1576
  return this.transition(task, "cancelled");
1521
1577
  }
1522
- /** 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. */
1523
1579
  async retry(id) {
1524
1580
  const task = await this.mustGet(id);
1525
- if (task.status !== "failed" && task.status !== "cancelled") {
1581
+ const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
1582
+ if (!retryableStates.includes(task.status)) {
1526
1583
  throw new TaskError(
1527
1584
  "TASK_INVALID",
1528
- `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}".`
1529
1586
  );
1530
1587
  }
1531
1588
  const { result: _discarded, ...rest } = task;
@@ -1557,10 +1614,14 @@ var TaskEngine = class {
1557
1614
  if (!allowed.includes(to)) {
1558
1615
  throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
1559
1616
  }
1617
+ const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
1560
1618
  const updated = {
1561
1619
  ...task,
1562
1620
  status: to,
1563
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 } : {},
1564
1625
  ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
1565
1626
  };
1566
1627
  await this.options.store.save(updated);
@@ -1581,6 +1642,19 @@ var TaskEngine = class {
1581
1642
  function generateTaskId() {
1582
1643
  return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1583
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
+ }
1584
1658
 
1585
1659
  // src/audit/index.ts
1586
1660
  import { appendFile, mkdir as mkdir2 } from "fs/promises";
@@ -1980,7 +2054,7 @@ function registerTaskCommands(program) {
1980
2054
  const cancelled = await engine.cancel(taskId);
1981
2055
  console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
1982
2056
  });
1983
- 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) => {
1984
2058
  const config = loadConfig();
1985
2059
  const engine = createTaskEngine(config);
1986
2060
  const reset = await engine.retry(taskId);
@@ -2003,6 +2077,15 @@ function printOutcome(task, options = {}) {
2003
2077
  if (task.context?.files) console.log(` files: ${task.context.files.join(", ")}`);
2004
2078
  console.log(` objective: ${task.objective}`);
2005
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
+ }
2006
2089
  const result = task.result;
2007
2090
  if (!result) return;
2008
2091
  if (result.summary) console.log(` summary: ${result.summary}`);
@@ -2743,7 +2826,7 @@ ${contribution}`);
2743
2826
  }
2744
2827
  const stepRun = {
2745
2828
  stepId: step.id,
2746
- 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",
2747
2830
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
2748
2831
  ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
2749
2832
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
@@ -2754,8 +2837,10 @@ ${contribution}`);
2754
2837
  run2.reports = [...allReports];
2755
2838
  await this.options.store.save(run2);
2756
2839
  }
2757
- const failed = [...outcomes.values()].some((o) => o.status === "failed");
2758
- 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";
2759
2844
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
2760
2845
  let resultSummary;
2761
2846
  for (let i = def.steps.length - 1; i >= 0; i -= 1) {
@@ -2861,6 +2946,7 @@ ${contribution}`);
2861
2946
  )
2862
2947
  );
2863
2948
  const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
2949
+ const completedTasks = results.filter((r) => r.status === "completed");
2864
2950
  const failedTasks = results.filter((r) => r.status !== "completed");
2865
2951
  const lines = results.map((result, index) => {
2866
2952
  const agentName = agentNames[index] ?? "unknown";
@@ -2870,12 +2956,15 @@ ${contribution}`);
2870
2956
  const reports = results.flatMap((r) => r.result?.reports ?? []);
2871
2957
  const summary = lines.join("\n");
2872
2958
  if (failedTasks.length > 0) {
2873
- 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(", ");
2874
2961
  return {
2875
- 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",
2876
2965
  summary,
2877
2966
  taskIds: created.map((t) => t.id),
2878
- error: `${failedTasks.length}/${results.length} task(s) failed \u2014 ${firstError}`,
2967
+ error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
2879
2968
  reports
2880
2969
  };
2881
2970
  }
@@ -2891,7 +2980,9 @@ ${contribution}`);
2891
2980
  const result = await this.options.tasks.run(task.id);
2892
2981
  if (result.status !== "completed") {
2893
2982
  return {
2894
- 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",
2895
2986
  summary: result.result?.summary ?? "",
2896
2987
  taskIds: [task.id],
2897
2988
  error: result.result?.error ?? `task ${task.id} did not complete`,