@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/index.js CHANGED
@@ -1,3 +1,12 @@
1
+ // src/types/task.ts
2
+ var TERMINAL_TASK_STATUSES = [
3
+ "completed",
4
+ "failed",
5
+ "timed_out",
6
+ "cancelled",
7
+ "partially_completed"
8
+ ];
9
+
1
10
  // src/errors/index.ts
2
11
  var OmniNodeError = class extends Error {
3
12
  code;
@@ -804,7 +813,19 @@ var ProcessAgent = class {
804
813
  new AgentError(
805
814
  "AGENT_TIMEOUT",
806
815
  `Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
807
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 4e3), stderr: stderr.slice(0, 2e3) } }
816
+ {
817
+ details: {
818
+ operation: "agent.run",
819
+ agent: this.config.name,
820
+ durationMs: timeoutMs,
821
+ // The process was started and may have performed side
822
+ // effects before being killed — the outcome is unknown,
823
+ // not failed (roadmap §6.3).
824
+ dispatched: true,
825
+ stdout: stdout.slice(0, 4e3),
826
+ stderr: stderr.slice(0, 2e3)
827
+ }
828
+ }
808
829
  )
809
830
  );
810
831
  return;
@@ -987,40 +1008,54 @@ function buildRoleRegistry(config) {
987
1008
  return registry;
988
1009
  }
989
1010
 
990
- // src/tasks/store.ts
1011
+ // src/persistence/json-file-store.ts
991
1012
  import { mkdir, readFile, rename, writeFile } from "fs/promises";
992
1013
  import { dirname } from "path";
993
- var FileTaskStore = class {
1014
+ var JsonFileStore = class {
994
1015
  filePath;
995
- /**
996
- * Read-modify-write operations are serialized: parallel pipeline tasks
997
- * would otherwise race on the JSON file (last writer wins).
998
- */
1016
+ schemaVersion;
1017
+ onCorrupt;
1018
+ log;
999
1019
  queue = Promise.resolve();
1000
- constructor(directory = `${process.cwd()}/.omninode`) {
1001
- this.filePath = `${directory}/tasks.json`;
1020
+ constructor(options) {
1021
+ this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
1022
+ this.schemaVersion = options.schemaVersion ?? 1;
1023
+ this.onCorrupt = options.onCorrupt;
1024
+ this.log = logger.child({ component: "persistence", file: options.fileName });
1002
1025
  }
1003
- async save(task) {
1004
- return this.synchronized(async () => {
1005
- const tasks = await this.load();
1006
- const index = tasks.findIndex((t) => t.id === task.id);
1007
- if (index >= 0) tasks[index] = task;
1008
- else tasks.push(task);
1009
- await this.write(tasks);
1010
- });
1026
+ get path() {
1027
+ return this.filePath;
1011
1028
  }
1012
- async get(id) {
1013
- return this.synchronized(async () => {
1014
- const tasks = await this.load();
1015
- return tasks.find((t) => t.id === id);
1016
- });
1029
+ /** All items, or [] when missing/corrupt. */
1030
+ async readAll() {
1031
+ let raw;
1032
+ try {
1033
+ raw = await readFile(this.filePath, "utf8");
1034
+ } catch {
1035
+ return [];
1036
+ }
1037
+ try {
1038
+ const parsed = JSON.parse(raw);
1039
+ if (Array.isArray(parsed)) return parsed;
1040
+ if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.items)) {
1041
+ return parsed.items;
1042
+ }
1043
+ throw new Error("persisted file has an unrecognized shape");
1044
+ } catch (error) {
1045
+ this.log.warn(`Treating ${this.filePath} as empty \u2014 it could not be parsed.`);
1046
+ this.onCorrupt?.(this.filePath, error);
1047
+ return [];
1048
+ }
1017
1049
  }
1018
- async list(filter = {}) {
1019
- return this.synchronized(async () => {
1020
- const tasks = await this.load();
1021
- 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));
1022
- });
1050
+ async writeAll(items) {
1051
+ const document = { schemaVersion: this.schemaVersion, items };
1052
+ const tmp = `${this.filePath}.tmp`;
1053
+ await mkdir(dirname(this.filePath), { recursive: true });
1054
+ await writeFile(tmp, `${JSON.stringify(document, null, 2)}
1055
+ `, "utf8");
1056
+ await rename(tmp, this.filePath);
1023
1057
  }
1058
+ /** Serializes an async operation against this file. */
1024
1059
  synchronized(operation) {
1025
1060
  const result = this.queue.then(operation, operation);
1026
1061
  this.queue = result.then(
@@ -1029,21 +1064,37 @@ var FileTaskStore = class {
1029
1064
  );
1030
1065
  return result;
1031
1066
  }
1032
- async load() {
1033
- try {
1034
- const raw = await readFile(this.filePath, "utf8");
1035
- const parsed = JSON.parse(raw);
1036
- return Array.isArray(parsed) ? parsed : [];
1037
- } catch {
1038
- return [];
1039
- }
1067
+ /** Read-modify-write helper: applies `mutate` to the current items. */
1068
+ async mutate(mutate) {
1069
+ return this.synchronized(async () => {
1070
+ const items = await this.readAll();
1071
+ const [next, result] = await mutate(items);
1072
+ await this.writeAll(next);
1073
+ return result;
1074
+ });
1040
1075
  }
1041
- async write(tasks) {
1042
- const tmp = `${this.filePath}.tmp`;
1043
- await mkdir(dirname(this.filePath), { recursive: true });
1044
- await writeFile(tmp, `${JSON.stringify(tasks, null, 2)}
1045
- `, "utf8");
1046
- await rename(tmp, this.filePath);
1076
+ };
1077
+
1078
+ // src/tasks/store.ts
1079
+ var FileTaskStore = class extends JsonFileStore {
1080
+ constructor(directory) {
1081
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "tasks.json" });
1082
+ }
1083
+ save(task) {
1084
+ return this.mutate((tasks) => {
1085
+ const index = tasks.findIndex((t) => t.id === task.id);
1086
+ if (index >= 0) tasks[index] = task;
1087
+ else tasks.push(task);
1088
+ return [tasks, void 0];
1089
+ });
1090
+ }
1091
+ async get(id) {
1092
+ const tasks = await this.readAll();
1093
+ return tasks.find((t) => t.id === id);
1094
+ }
1095
+ async list(filter = {}) {
1096
+ const tasks = await this.readAll();
1097
+ 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));
1047
1098
  }
1048
1099
  };
1049
1100
 
@@ -1052,12 +1103,18 @@ import { randomBytes } from "crypto";
1052
1103
  var ALLOWED_TRANSITIONS = {
1053
1104
  created: ["queued", "running", "cancelled"],
1054
1105
  queued: ["running", "cancelled"],
1055
- running: ["completed", "failed", "cancelled"],
1106
+ running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
1056
1107
  waiting: ["running", "cancelled"],
1057
1108
  completed: [],
1058
- failed: [],
1059
- cancelled: []
1109
+ // failed → running is the only re-entry: bounded automatic retry (§6.4).
1110
+ failed: ["running"],
1111
+ timed_out: [],
1112
+ cancelled: [],
1113
+ unknown: [],
1114
+ partially_completed: []
1060
1115
  };
1116
+ var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
1117
+ var EXECUTION_HISTORY_LIMIT = 10;
1061
1118
  var TaskEngine = class {
1062
1119
  constructor(options) {
1063
1120
  this.options = options;
@@ -1089,6 +1146,7 @@ var TaskEngine = class {
1089
1146
  id: generateTaskId(),
1090
1147
  objective: input.objective.trim(),
1091
1148
  status: "created",
1149
+ attempt: 0,
1092
1150
  createdAt: now,
1093
1151
  updatedAt: now,
1094
1152
  ...input.project !== void 0 ? { project: input.project } : {},
@@ -1105,8 +1163,10 @@ var TaskEngine = class {
1105
1163
  const task = await this.mustGet(id);
1106
1164
  return this.transition(task, "queued");
1107
1165
  }
1108
- /** Runs the task through its assigned agent and records the result. */
1109
- async run(id) {
1166
+ /** Runs the task through its assigned agent and records the result.
1167
+ * `maxAttempts` enables bounded automatic retries of *retryable* errors
1168
+ * with linear backoff; unknown/timed_out outcomes are never auto-retried. */
1169
+ async run(id, options = {}) {
1110
1170
  const task = await this.mustGet(id);
1111
1171
  if (task.status !== "created" && task.status !== "queued") {
1112
1172
  throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
@@ -1119,49 +1179,84 @@ var TaskEngine = class {
1119
1179
  throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
1120
1180
  }
1121
1181
  const role = task.role ? this.options.roles.get(task.role) : void 0;
1122
- const running = await this.transition(task, "running");
1123
1182
  this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1124
- let background = running.context?.background;
1183
+ let background = task.context?.background;
1125
1184
  if (this.options.memory) {
1126
1185
  const memoryContext = await this.options.memory.gatherContext({
1127
- objective: running.objective,
1128
- ...running.role ? { role: running.role } : {},
1129
- ...running.project ? { project: running.project } : {}
1186
+ objective: task.objective,
1187
+ ...task.role ? { role: task.role } : {},
1188
+ ...task.project ? { project: task.project } : {}
1130
1189
  });
1131
1190
  if (memoryContext.length > 0) {
1132
1191
  background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
1133
1192
  }
1134
1193
  }
1135
- let output;
1136
- try {
1137
- output = await agent.run({
1138
- taskId: running.id,
1139
- objective: running.objective,
1140
- ...role ? { role } : {},
1141
- ...background !== void 0 ? { context: background } : {}
1142
- });
1143
- } catch (error) {
1144
- const message = error instanceof Error ? error.message : String(error);
1145
- this.log.error(`Task ${task.id} failed: ${message}`);
1146
- return this.transition(running, "failed", {
1147
- result: { error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }
1194
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
1195
+ const backoffMs = options.retryBackoffMs ?? 0;
1196
+ let current = task;
1197
+ for (let i = 0; i < maxAttempts; i += 1) {
1198
+ const attempt = current.attempt + 1;
1199
+ const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1200
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1201
+ current = await this.transition(current, "running", { attempt });
1202
+ let output;
1203
+ let thrown;
1204
+ try {
1205
+ output = await agent.run({
1206
+ taskId: current.id,
1207
+ objective: current.objective,
1208
+ ...role ? { role } : {},
1209
+ ...background !== void 0 ? { context: background } : {}
1210
+ });
1211
+ } catch (error) {
1212
+ thrown = error;
1213
+ }
1214
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1215
+ if (thrown !== void 0 || output === void 0) {
1216
+ const outcome = classifyThrown(thrown);
1217
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
1218
+ const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
1219
+ current = await this.transition(current, outcome, {
1220
+ attempt,
1221
+ lastError: message,
1222
+ execution: {
1223
+ executionId,
1224
+ attempt,
1225
+ startedAt,
1226
+ finishedAt,
1227
+ outcome,
1228
+ error: message
1229
+ }
1230
+ });
1231
+ this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
1232
+ if (retryable) {
1233
+ if (backoffMs > 0) await sleep(backoffMs * attempt);
1234
+ continue;
1235
+ }
1236
+ if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
1237
+ return current;
1238
+ }
1239
+ const result = {
1240
+ ...output.summary !== void 0 ? { summary: output.summary } : {},
1241
+ ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1242
+ // Keep the verbatim output (bounded) for report extraction and audit.
1243
+ ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1244
+ ...output.error !== void 0 ? { error: output.error } : {},
1245
+ finishedAt
1246
+ };
1247
+ const finalStatus = output.status === "completed" ? "completed" : "failed";
1248
+ current = await this.transition(current, finalStatus, {
1249
+ attempt,
1250
+ result,
1251
+ execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
1148
1252
  });
1253
+ this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
1254
+ break;
1149
1255
  }
1150
- const result = {
1151
- ...output.summary !== void 0 ? { summary: output.summary } : {},
1152
- ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
1153
- // Keep the verbatim output (bounded) for report extraction and audit.
1154
- ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
1155
- ...output.error !== void 0 ? { error: output.error } : {},
1156
- finishedAt: (/* @__PURE__ */ new Date()).toISOString()
1157
- };
1158
- const finalStatus = output.status === "completed" ? "completed" : "failed";
1159
- const finished = await this.transition(running, finalStatus, { result });
1160
- this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
1161
1256
  if (this.options.memory) {
1162
- await this.options.memory.recordTaskOutcome(finished);
1257
+ await this.options.memory.recordTaskOutcome(current);
1163
1258
  }
1164
- return finished;
1259
+ return current;
1165
1260
  }
1166
1261
  async cancel(id) {
1167
1262
  const task = await this.mustGet(id);
@@ -1174,13 +1269,14 @@ var TaskEngine = class {
1174
1269
  }
1175
1270
  return this.transition(task, "cancelled");
1176
1271
  }
1177
- /** Error recovery (§27 Phase 9): reset a failed/cancelled task so it can run again. */
1272
+ /** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
1178
1273
  async retry(id) {
1179
1274
  const task = await this.mustGet(id);
1180
- if (task.status !== "failed" && task.status !== "cancelled") {
1275
+ const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
1276
+ if (!retryableStates.includes(task.status)) {
1181
1277
  throw new TaskError(
1182
1278
  "TASK_INVALID",
1183
- `Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1279
+ `Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1184
1280
  );
1185
1281
  }
1186
1282
  const { result: _discarded, ...rest } = task;
@@ -1212,10 +1308,14 @@ var TaskEngine = class {
1212
1308
  if (!allowed.includes(to)) {
1213
1309
  throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
1214
1310
  }
1311
+ const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
1215
1312
  const updated = {
1216
1313
  ...task,
1217
1314
  status: to,
1218
1315
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1316
+ ...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
1317
+ ...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
1318
+ ...executions !== void 0 ? { executions } : {},
1219
1319
  ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
1220
1320
  };
1221
1321
  await this.options.store.save(updated);
@@ -1236,6 +1336,19 @@ var TaskEngine = class {
1236
1336
  function generateTaskId() {
1237
1337
  return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1238
1338
  }
1339
+ function errorCode(error) {
1340
+ return error?.code ?? "";
1341
+ }
1342
+ function classifyThrown(error) {
1343
+ if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
1344
+ const dispatched = error.details?.dispatched;
1345
+ return dispatched === false ? "timed_out" : "unknown";
1346
+ }
1347
+ return "failed";
1348
+ }
1349
+ function sleep(ms) {
1350
+ return new Promise((resolve) => setTimeout(resolve, ms));
1351
+ }
1239
1352
 
1240
1353
  // src/audit/index.ts
1241
1354
  import { appendFile, mkdir as mkdir2 } from "fs/promises";
@@ -1260,10 +1373,10 @@ var FileAuditLog = class {
1260
1373
  }
1261
1374
  /** Recent events, oldest first; used by the CLI. */
1262
1375
  async recent(limit = 20) {
1263
- const { readFile: readFile6 } = await import("fs/promises");
1376
+ const { readFile: readFile2 } = await import("fs/promises");
1264
1377
  let raw;
1265
1378
  try {
1266
- raw = await readFile6(this.filePath, "utf8");
1379
+ raw = await readFile2(this.filePath, "utf8");
1267
1380
  } catch {
1268
1381
  return [];
1269
1382
  }
@@ -1279,8 +1392,6 @@ var FileAuditLog = class {
1279
1392
  };
1280
1393
 
1281
1394
  // src/memory/local.ts
1282
- import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
1283
- import { dirname as dirname3 } from "path";
1284
1395
  var STOPWORDS = /* @__PURE__ */ new Set([
1285
1396
  "the",
1286
1397
  "and",
@@ -1337,29 +1448,24 @@ function relevanceScore(queryKeywords, entry) {
1337
1448
  }
1338
1449
  return overlap * 2 + tagMatches * 1.5;
1339
1450
  }
1340
- var LocalMemoryProvider = class {
1451
+ var LocalMemoryProvider = class extends JsonFileStore {
1341
1452
  name = "local";
1342
- filePath;
1343
- queue = Promise.resolve();
1344
- constructor(directory = `${process.cwd()}/.omninode`) {
1345
- this.filePath = `${directory}/memory.json`;
1453
+ constructor(directory) {
1454
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
1346
1455
  }
1347
1456
  async query(query) {
1348
- return this.synchronized(async () => {
1349
- const entries = await this.load();
1350
- const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter(
1351
- (entry) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry.tags ?? []).includes(tag)) : true
1352
- );
1353
- if (!query.text || query.text.trim().length === 0) {
1354
- return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
1355
- }
1356
- const queryKeywords = keywords(query.text);
1357
- return filtered.map((entry) => ({ entry, score: relevanceScore(queryKeywords, entry) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry }) => entry);
1358
- });
1457
+ const entries = await this.readAll();
1458
+ const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter(
1459
+ (entry) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry.tags ?? []).includes(tag)) : true
1460
+ );
1461
+ if (!query.text || query.text.trim().length === 0) {
1462
+ return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
1463
+ }
1464
+ const queryKeywords = keywords(query.text);
1465
+ return filtered.map((entry) => ({ entry, score: relevanceScore(queryKeywords, entry) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry }) => entry);
1359
1466
  }
1360
- async write(entry) {
1361
- return this.synchronized(async () => {
1362
- const entries = await this.load();
1467
+ write(entry) {
1468
+ return this.mutate((entries) => {
1363
1469
  const index = entries.findIndex((e) => e.key === entry.key);
1364
1470
  const stamped = {
1365
1471
  ...entry,
@@ -1368,35 +1474,11 @@ var LocalMemoryProvider = class {
1368
1474
  };
1369
1475
  if (index >= 0) entries[index] = stamped;
1370
1476
  else entries.push(stamped);
1371
- await this.saveAll(entries);
1477
+ return [entries, void 0];
1372
1478
  });
1373
1479
  }
1374
1480
  async count() {
1375
- return (await this.load()).length;
1376
- }
1377
- synchronized(operation) {
1378
- const result = this.queue.then(operation, operation);
1379
- this.queue = result.then(
1380
- () => void 0,
1381
- () => void 0
1382
- );
1383
- return result;
1384
- }
1385
- async load() {
1386
- try {
1387
- const raw = await readFile2(this.filePath, "utf8");
1388
- const parsed = JSON.parse(raw);
1389
- return Array.isArray(parsed) ? parsed : [];
1390
- } catch {
1391
- return [];
1392
- }
1393
- }
1394
- async saveAll(entries) {
1395
- const tmp = `${this.filePath}.tmp`;
1396
- await mkdir3(dirname3(this.filePath), { recursive: true });
1397
- await writeFile2(tmp, `${JSON.stringify(entries, null, 2)}
1398
- `, "utf8");
1399
- await rename2(tmp, this.filePath);
1481
+ return (await this.readAll()).length;
1400
1482
  }
1401
1483
  };
1402
1484
 
@@ -1975,7 +2057,7 @@ ${contribution}`);
1975
2057
  }
1976
2058
  const stepRun = {
1977
2059
  stepId: step.id,
1978
- status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
2060
+ status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
1979
2061
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
1980
2062
  ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
1981
2063
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
@@ -1986,8 +2068,10 @@ ${contribution}`);
1986
2068
  run.reports = [...allReports];
1987
2069
  await this.options.store.save(run);
1988
2070
  }
1989
- const failed = [...outcomes.values()].some((o) => o.status === "failed");
1990
- const finalStatus = failed ? "failed" : "completed";
2071
+ const allOutcomes = [...outcomes.values()];
2072
+ const failed = allOutcomes.some((o) => o.status === "failed");
2073
+ const partial = !failed && allOutcomes.some((o) => o.status === "partial");
2074
+ const finalStatus = failed ? "failed" : partial ? "partial" : "completed";
1991
2075
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
1992
2076
  let resultSummary;
1993
2077
  for (let i = def.steps.length - 1; i >= 0; i -= 1) {
@@ -2093,6 +2177,7 @@ ${contribution}`);
2093
2177
  )
2094
2178
  );
2095
2179
  const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
2180
+ const completedTasks = results.filter((r) => r.status === "completed");
2096
2181
  const failedTasks = results.filter((r) => r.status !== "completed");
2097
2182
  const lines = results.map((result, index) => {
2098
2183
  const agentName = agentNames[index] ?? "unknown";
@@ -2102,12 +2187,15 @@ ${contribution}`);
2102
2187
  const reports = results.flatMap((r) => r.result?.reports ?? []);
2103
2188
  const summary = lines.join("\n");
2104
2189
  if (failedTasks.length > 0) {
2105
- const firstError = failedTasks[0]?.result?.error ?? "unknown error";
2190
+ const firstError = failedTasks[0]?.result?.error ?? failedTasks[0]?.lastError ?? "unknown outcome";
2191
+ const states = failedTasks.map((r) => r.status).join(", ");
2106
2192
  return {
2107
- status: "failed",
2193
+ // Some agents succeeded → the step produced usable but incomplete
2194
+ // intelligence: partial, not failed (roadmap §9.3).
2195
+ status: completedTasks.length > 0 ? "partial" : "failed",
2108
2196
  summary,
2109
2197
  taskIds: created.map((t) => t.id),
2110
- error: `${failedTasks.length}/${results.length} task(s) failed \u2014 ${firstError}`,
2198
+ error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
2111
2199
  reports
2112
2200
  };
2113
2201
  }
@@ -2123,7 +2211,9 @@ ${contribution}`);
2123
2211
  const result = await this.options.tasks.run(task.id);
2124
2212
  if (result.status !== "completed") {
2125
2213
  return {
2126
- status: "failed",
2214
+ // Unknown/timed_out outcomes are not failures — the work may have
2215
+ // happened remotely; the step is incomplete, not broken.
2216
+ status: result.status === "unknown" || result.status === "timed_out" ? "partial" : "failed",
2127
2217
  summary: result.result?.summary ?? "",
2128
2218
  taskIds: [task.id],
2129
2219
  error: result.result?.error ?? `task ${task.id} did not complete`,
@@ -2239,114 +2329,44 @@ function createDefaultChatFn(providers) {
2239
2329
  }
2240
2330
 
2241
2331
  // src/pipelines/store.ts
2242
- import { mkdir as mkdir4, readFile as readFile3, rename as rename3, writeFile as writeFile3 } from "fs/promises";
2243
- import { dirname as dirname4 } from "path";
2244
- var FilePipelineRunStore = class {
2245
- filePath;
2246
- queue = Promise.resolve();
2247
- constructor(directory = `${process.cwd()}/.omninode`) {
2248
- this.filePath = `${directory}/pipelines.json`;
2332
+ var FilePipelineRunStore = class extends JsonFileStore {
2333
+ constructor(directory) {
2334
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "pipelines.json" });
2249
2335
  }
2250
- async save(run) {
2251
- return this.synchronized(async () => {
2252
- const runs = await this.load();
2336
+ save(run) {
2337
+ return this.mutate((runs) => {
2253
2338
  const index = runs.findIndex((r) => r.id === run.id);
2254
2339
  if (index >= 0) runs[index] = run;
2255
2340
  else runs.push(run);
2256
- await this.write(runs);
2341
+ return [runs, void 0];
2257
2342
  });
2258
2343
  }
2259
2344
  async get(id) {
2260
- return this.synchronized(async () => {
2261
- const runs = await this.load();
2262
- return runs.find((r) => r.id === id);
2263
- });
2345
+ return (await this.readAll()).find((r) => r.id === id);
2264
2346
  }
2265
2347
  async list(filter = {}) {
2266
- return this.synchronized(async () => {
2267
- const runs = await this.load();
2268
- return runs.filter((run) => filter.pipelineId ? run.pipelineId === filter.pipelineId : true).filter((run) => filter.status ? run.status === filter.status : true).sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? ""));
2269
- });
2270
- }
2271
- synchronized(operation) {
2272
- const result = this.queue.then(operation, operation);
2273
- this.queue = result.then(
2274
- () => void 0,
2275
- () => void 0
2276
- );
2277
- return result;
2278
- }
2279
- async load() {
2280
- try {
2281
- const raw = await readFile3(this.filePath, "utf8");
2282
- const parsed = JSON.parse(raw);
2283
- return Array.isArray(parsed) ? parsed : [];
2284
- } catch {
2285
- return [];
2286
- }
2287
- }
2288
- async write(runs) {
2289
- const tmp = `${this.filePath}.tmp`;
2290
- await mkdir4(dirname4(this.filePath), { recursive: true });
2291
- await writeFile3(tmp, `${JSON.stringify(runs, null, 2)}
2292
- `, "utf8");
2293
- await rename3(tmp, this.filePath);
2348
+ return (await this.readAll()).filter((run) => filter.pipelineId ? run.pipelineId === filter.pipelineId : true).filter((run) => filter.status ? run.status === filter.status : true).sort((a, b) => (a.startedAt ?? "").localeCompare(b.startedAt ?? ""));
2294
2349
  }
2295
2350
  };
2296
2351
 
2297
2352
  // src/planner/store.ts
2298
- import { mkdir as mkdir5, readFile as readFile4, rename as rename4, writeFile as writeFile4 } from "fs/promises";
2299
- import { dirname as dirname5 } from "path";
2300
- var FilePlanStore = class {
2301
- filePath;
2302
- queue = Promise.resolve();
2303
- constructor(directory = `${process.cwd()}/.omninode`) {
2304
- this.filePath = `${directory}/plans.json`;
2353
+ var FilePlanStore = class extends JsonFileStore {
2354
+ constructor(directory) {
2355
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "plans.json" });
2305
2356
  }
2306
- async save(plan) {
2307
- return this.synchronized(async () => {
2308
- const plans = await this.load();
2357
+ save(plan) {
2358
+ return this.mutate((plans) => {
2309
2359
  const index = plans.findIndex((p) => p.id === plan.id);
2310
2360
  if (index >= 0) plans[index] = plan;
2311
2361
  else plans.push(plan);
2312
- await this.write(plans);
2362
+ return [plans, void 0];
2313
2363
  });
2314
2364
  }
2315
2365
  async get(id) {
2316
- return this.synchronized(async () => {
2317
- const plans = await this.load();
2318
- return plans.find((p) => p.id === id);
2319
- });
2366
+ return (await this.readAll()).find((p) => p.id === id);
2320
2367
  }
2321
2368
  async list(filter = {}) {
2322
- return this.synchronized(async () => {
2323
- const plans = await this.load();
2324
- 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));
2325
- });
2326
- }
2327
- synchronized(operation) {
2328
- const result = this.queue.then(operation, operation);
2329
- this.queue = result.then(
2330
- () => void 0,
2331
- () => void 0
2332
- );
2333
- return result;
2334
- }
2335
- async load() {
2336
- try {
2337
- const raw = await readFile4(this.filePath, "utf8");
2338
- const parsed = JSON.parse(raw);
2339
- return Array.isArray(parsed) ? parsed : [];
2340
- } catch {
2341
- return [];
2342
- }
2343
- }
2344
- async write(plans) {
2345
- const tmp = `${this.filePath}.tmp`;
2346
- await mkdir5(dirname5(this.filePath), { recursive: true });
2347
- await writeFile4(tmp, `${JSON.stringify(plans, null, 2)}
2348
- `, "utf8");
2349
- await rename4(tmp, this.filePath);
2369
+ 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));
2350
2370
  }
2351
2371
  };
2352
2372
 
@@ -2582,84 +2602,39 @@ var ReportService = class {
2582
2602
  };
2583
2603
 
2584
2604
  // src/reports/store.ts
2585
- import { mkdir as mkdir6, readFile as readFile5, rename as rename5, writeFile as writeFile5 } from "fs/promises";
2586
- import { dirname as dirname6 } from "path";
2587
- var FileReportStore = class {
2588
- filePath;
2589
- queue = Promise.resolve();
2590
- constructor(directory = `${process.cwd()}/.omninode`) {
2591
- this.filePath = `${directory}/reports.json`;
2605
+ var FileReportStore = class extends JsonFileStore {
2606
+ constructor(directory) {
2607
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "reports.json" });
2592
2608
  }
2593
2609
  async saveReports(reports) {
2594
- return this.synchronized(async () => {
2595
- const entries = await this.load();
2610
+ await this.mutate((entries) => {
2596
2611
  for (const report of reports) {
2597
- const index = entries.findIndex(
2598
- (e) => e.kind === "report" && e.report.id === report.id
2599
- );
2612
+ const index = entries.findIndex((e) => e.kind === "report" && e.report.id === report.id);
2600
2613
  const entry = { kind: "report", report };
2601
2614
  if (index >= 0) entries[index] = entry;
2602
2615
  else entries.push(entry);
2603
2616
  }
2604
- await this.write(entries);
2617
+ return [entries, void 0];
2605
2618
  });
2606
2619
  }
2607
2620
  async listReports(filter = {}) {
2608
- return this.synchronized(async () => {
2609
- const entries = await this.load();
2610
- 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));
2611
- });
2621
+ 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));
2612
2622
  }
2613
2623
  async saveCombined(combined) {
2614
- return this.synchronized(async () => {
2615
- const entries = await this.load();
2616
- const index = entries.findIndex(
2617
- (e) => e.kind === "combined" && e.combined.id === combined.id
2618
- );
2624
+ await this.mutate((entries) => {
2625
+ const index = entries.findIndex((e) => e.kind === "combined" && e.combined.id === combined.id);
2619
2626
  const entry = { kind: "combined", combined };
2620
2627
  if (index >= 0) entries[index] = entry;
2621
2628
  else entries.push(entry);
2622
- await this.write(entries);
2629
+ return [entries, void 0];
2623
2630
  });
2624
2631
  }
2625
2632
  async getCombined(id) {
2626
- return this.synchronized(async () => {
2627
- const entries = await this.load();
2628
- const found = entries.find((e) => e.kind === "combined" && e.combined.id === id);
2629
- return found?.kind === "combined" ? found.combined : void 0;
2630
- });
2633
+ const found = (await this.readAll()).find((e) => e.kind === "combined" && e.combined.id === id);
2634
+ return found?.kind === "combined" ? found.combined : void 0;
2631
2635
  }
2632
2636
  async listCombined(filter = {}) {
2633
- return this.synchronized(async () => {
2634
- const entries = await this.load();
2635
- return entries.filter((e) => e.kind === "combined").map((e) => e.combined).filter(
2636
- (combined) => filter.pipelineRunId ? combined.pipelineRunId === filter.pipelineRunId : true
2637
- ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2638
- });
2639
- }
2640
- synchronized(operation) {
2641
- const result = this.queue.then(operation, operation);
2642
- this.queue = result.then(
2643
- () => void 0,
2644
- () => void 0
2645
- );
2646
- return result;
2647
- }
2648
- async load() {
2649
- try {
2650
- const raw = await readFile5(this.filePath, "utf8");
2651
- const parsed = JSON.parse(raw);
2652
- return Array.isArray(parsed) ? parsed : [];
2653
- } catch {
2654
- return [];
2655
- }
2656
- }
2657
- async write(entries) {
2658
- const tmp = `${this.filePath}.tmp`;
2659
- await mkdir6(dirname6(this.filePath), { recursive: true });
2660
- await writeFile5(tmp, `${JSON.stringify(entries, null, 2)}
2661
- `, "utf8");
2662
- await rename5(tmp, this.filePath);
2637
+ 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));
2663
2638
  }
2664
2639
  };
2665
2640
 
@@ -2691,8 +2666,26 @@ function buildPipelineEngine(config, log = logger) {
2691
2666
  });
2692
2667
  }
2693
2668
 
2669
+ // src/persistence/index.ts
2670
+ var ProjectStores = class {
2671
+ tasks;
2672
+ pipelineRuns;
2673
+ reports;
2674
+ plans;
2675
+ memory;
2676
+ audit;
2677
+ constructor(directory = `${process.cwd()}/.omninode`) {
2678
+ this.tasks = new FileTaskStore(directory);
2679
+ this.pipelineRuns = new FilePipelineRunStore(directory);
2680
+ this.reports = new FileReportStore(directory);
2681
+ this.plans = new FilePlanStore(directory);
2682
+ this.memory = new LocalMemoryProvider(directory);
2683
+ this.audit = new FileAuditLog(directory);
2684
+ }
2685
+ };
2686
+
2694
2687
  // src/version.ts
2695
- var OMNINODE_VERSION = "1.0.0";
2688
+ var OMNINODE_VERSION = "2.0.0-alpha.2";
2696
2689
  export {
2697
2690
  AgentError,
2698
2691
  AgentRegistry,
@@ -2700,12 +2693,16 @@ export {
2700
2693
  ConsoleLogSink,
2701
2694
  FileAuditLog,
2702
2695
  FilePipelineRunStore,
2696
+ FilePlanStore,
2703
2697
  FileReportStore,
2704
2698
  FileTaskStore,
2699
+ HeuristicPlanner,
2700
+ JsonFileStore,
2705
2701
  LocalMemoryProvider,
2706
2702
  Logger,
2707
2703
  MemoryError,
2708
2704
  MemoryService,
2705
+ ModelPlanner,
2709
2706
  ModelRegistry,
2710
2707
  OMNINODE_VERSION,
2711
2708
  OPENAI_COMPATIBLE_TYPES,
@@ -2716,11 +2713,13 @@ export {
2716
2713
  PipelineError,
2717
2714
  PlannerError,
2718
2715
  ProcessAgent,
2716
+ ProjectStores,
2719
2717
  ProtocolError,
2720
2718
  ProviderError,
2721
2719
  RememberaMemoryProvider,
2722
2720
  ReportService,
2723
2721
  RoleRegistry,
2722
+ TERMINAL_TASK_STATUSES,
2724
2723
  TaskEngine,
2725
2724
  TaskError,
2726
2725
  agentConfigSchema,
@@ -2728,7 +2727,10 @@ export {
2728
2727
  appConfigSchema,
2729
2728
  authHeaders,
2730
2729
  buildAgentRegistry,
2730
+ buildChildEnv,
2731
2731
  buildPipelineEngine,
2732
+ buildPlanner,
2733
+ buildPlannerContext,
2732
2734
  buildRoleRegistry,
2733
2735
  composeTaskText,
2734
2736
  createAgent,
@@ -2744,6 +2746,7 @@ export {
2744
2746
  keywords,
2745
2747
  loadConfig,
2746
2748
  logger,
2749
+ parsePlanJson,
2747
2750
  pipelineConfigSchema,
2748
2751
  pipelineStepConfigSchema,
2749
2752
  plannerConfigSchema,