@hilbras/omninode 1.0.0 → 2.0.0-alpha.10

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;
@@ -116,6 +125,7 @@ var providerConfigSchema = z.object({
116
125
  type: z.enum(["openai-compatible", "omnihilbras", "openrouter", "local", "custom"]),
117
126
  base_url: z.string().url(),
118
127
  api_key_env_var: z.string().min(1).optional(),
128
+ timeout_ms: z.number().int().positive().optional(),
119
129
  headers: z.record(z.string()).optional(),
120
130
  enabled: z.boolean().optional(),
121
131
  metadata: z.record(z.unknown()).optional()
@@ -131,6 +141,11 @@ var agentConfigSchema = z.object({
131
141
  input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
132
142
  timeout_ms: z.number().int().positive().optional(),
133
143
  inherit_env: z.boolean().optional(),
144
+ env_policy: z.enum(["inherit", "allowlist", "denylist", "explicit"]).optional(),
145
+ env_allowlist: z.array(z.string()).optional(),
146
+ env_denylist: z.array(z.string()).optional(),
147
+ max_output_bytes: z.number().int().positive().optional(),
148
+ allow_external_cwd: z.boolean().optional(),
134
149
  metadata: z.record(z.unknown()).optional()
135
150
  }).strict();
136
151
  var roleConfigSchema = z.object({
@@ -175,7 +190,9 @@ var projectConfigSchema = z.object({
175
190
  memory: z.object({
176
191
  provider: z.string().min(1).default("local"),
177
192
  base_url: z.string().url().optional(),
178
- api_key_env_var: z.string().min(1).optional()
193
+ api_key_env_var: z.string().min(1).optional(),
194
+ /** When true, memory failures fail tasks instead of being tolerated (§12). */
195
+ required: z.boolean().optional()
179
196
  }).strict().optional()
180
197
  }).strict();
181
198
  var appConfigSchema = z.object({
@@ -257,7 +274,8 @@ function toAppConfig(yaml) {
257
274
  memory: {
258
275
  provider: yaml.project.memory.provider,
259
276
  baseUrl: yaml.project.memory.base_url,
260
- apiKeyEnvVar: yaml.project.memory.api_key_env_var
277
+ apiKeyEnvVar: yaml.project.memory.api_key_env_var,
278
+ required: yaml.project.memory.required
261
279
  }
262
280
  } : {}
263
281
  },
@@ -288,6 +306,7 @@ function toProviderConfig(raw) {
288
306
  type: raw.type,
289
307
  baseUrl: raw.base_url,
290
308
  apiKeyEnvVar: raw.api_key_env_var,
309
+ timeoutMs: raw.timeout_ms,
291
310
  headers: raw.headers,
292
311
  enabled: raw.enabled,
293
312
  metadata: raw.metadata
@@ -305,6 +324,11 @@ function toAgentConfig(raw) {
305
324
  inputMode: raw.input_mode,
306
325
  timeoutMs: raw.timeout_ms,
307
326
  inheritEnv: raw.inherit_env,
327
+ envPolicy: raw.env_policy,
328
+ envAllowlist: raw.env_allowlist,
329
+ envDenylist: raw.env_denylist,
330
+ maxOutputBytes: raw.max_output_bytes,
331
+ allowExternalCwd: raw.allow_external_cwd,
308
332
  metadata: raw.metadata
309
333
  };
310
334
  }
@@ -375,57 +399,200 @@ function authHeaders(config, env = process.env) {
375
399
  }
376
400
 
377
401
  // src/providers/http.ts
402
+ var HttpResponseError = class extends Error {
403
+ constructor(message, status, cause) {
404
+ super(message, { cause });
405
+ this.status = status;
406
+ this.name = "HttpResponseError";
407
+ }
408
+ status;
409
+ };
378
410
  var DEFAULT_TIMEOUT_MS = 3e4;
379
411
  async function requestJson(url, options = {}) {
380
412
  const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
381
- let response;
382
- try {
383
- response = await fetch(url, {
384
- method,
385
- headers: {
386
- accept: "application/json",
387
- ...body !== void 0 ? { "content-type": "application/json" } : {},
388
- ...headers
389
- },
390
- ...body !== void 0 ? { body: JSON.stringify(body) } : {},
391
- signal: AbortSignal.timeout(timeoutMs)
392
- });
393
- } catch (error) {
394
- const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
395
- throw new ProviderError("PROVIDER_UNAVAILABLE", `Request to ${url} failed: ${reason}.`, {
396
- cause: error
397
- });
398
- }
413
+ const response = await fetch(url, {
414
+ method,
415
+ headers: {
416
+ accept: "application/json",
417
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
418
+ ...headers
419
+ },
420
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
421
+ signal: AbortSignal.timeout(timeoutMs)
422
+ });
399
423
  let parsed;
400
424
  try {
401
425
  parsed = await response.json();
402
426
  } catch (error) {
403
- throw new ProviderError("PROVIDER_UNAVAILABLE", `Response from ${url} was not valid JSON.`, {
404
- cause: error,
405
- details: { status: response.status }
406
- });
427
+ throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
407
428
  }
408
429
  return { status: response.status, body: parsed };
409
430
  }
410
431
 
432
+ // src/providers/errors.ts
433
+ function kindToCode(kind) {
434
+ switch (kind) {
435
+ case "AUTHENTICATION_ERROR":
436
+ return "PROVIDER_AUTH_FAILED";
437
+ case "MODEL_NOT_FOUND":
438
+ return "MODEL_NOT_FOUND";
439
+ default:
440
+ return "PROVIDER_UNAVAILABLE";
441
+ }
442
+ }
443
+ function isRetryable(kind) {
444
+ return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
445
+ }
446
+ function kindFromStatus(status) {
447
+ if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
448
+ if (status === 429) return "RATE_LIMIT_ERROR";
449
+ if (status === 404) return "MODEL_NOT_FOUND";
450
+ if (status === 408 || status === 504) return "TIMEOUT";
451
+ if (status >= 500) return "SERVER_ERROR";
452
+ if (status >= 400) return "INVALID_REQUEST";
453
+ return "UNKNOWN_ERROR";
454
+ }
455
+ function extractMessage(body) {
456
+ if (body === null || typeof body !== "object") return void 0;
457
+ const root = body;
458
+ const error = root.error ?? root;
459
+ const message = error.message ?? error.detail ?? root.message;
460
+ return typeof message === "string" ? message : void 0;
461
+ }
462
+ function parseRetryAfter(value, now = Date.now()) {
463
+ if (!value) return void 0;
464
+ const seconds = Number(value);
465
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
466
+ const date = Date.parse(value);
467
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
468
+ }
469
+ function isTimeoutError(error) {
470
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
471
+ }
472
+ function isNetworkError(error) {
473
+ return error instanceof TypeError || typeof error?.cause === "object" && error instanceof Error && !error.code;
474
+ }
475
+ function fallbackMessage(kind, context, status) {
476
+ const { provider, operation } = context;
477
+ switch (kind) {
478
+ case "AUTHENTICATION_ERROR":
479
+ return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
480
+ case "RATE_LIMIT_ERROR":
481
+ return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
482
+ case "MODEL_NOT_FOUND":
483
+ return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
484
+ case "TIMEOUT":
485
+ return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
486
+ case "SERVER_ERROR":
487
+ return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
488
+ case "INVALID_REQUEST":
489
+ return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
490
+ default:
491
+ return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
492
+ }
493
+ }
494
+ function providerHttpError(context) {
495
+ const status = context.status ?? 0;
496
+ const kind = kindFromStatus(status);
497
+ const detail = extractMessage(context.body);
498
+ const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
499
+ return new ProviderError(
500
+ context.code ?? kindToCode(kind),
501
+ detail ?? fallbackMessage(kind, context, status),
502
+ {
503
+ cause: context.cause,
504
+ details: {
505
+ kind,
506
+ provider: context.provider,
507
+ operation: context.operation,
508
+ status,
509
+ retryable: isRetryable(kind),
510
+ ...retryAfterMs !== void 0 ? { retryAfterMs } : {},
511
+ ...detail ? { providerMessage: detail } : {}
512
+ }
513
+ }
514
+ );
515
+ }
516
+ function providerTransportError(context) {
517
+ const cause = context.cause;
518
+ const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
519
+ const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
520
+ return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
521
+ cause: context.cause,
522
+ details: {
523
+ kind,
524
+ provider: context.provider,
525
+ operation: context.operation,
526
+ retryable: isRetryable(kind)
527
+ }
528
+ });
529
+ }
530
+
411
531
  // src/providers/openai-compatible/index.ts
532
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
412
533
  var OpenAICompatibleProvider = class {
413
534
  config;
535
+ providerId;
536
+ authentication;
414
537
  log;
415
- constructor(config, log = logger) {
538
+ /** Discovered models, so getModel() answers without another round-trip. */
539
+ discovered;
540
+ env;
541
+ constructor(config, log = logger, env = process.env) {
416
542
  this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
543
+ this.env = env;
544
+ this.providerId = config.providerId ?? config.name;
545
+ this.authentication = {
546
+ method: config.apiKeyEnvVar ? "bearer" : "none",
547
+ ...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
548
+ configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
549
+ };
417
550
  this.log = log.child({ provider: config.name });
418
551
  }
419
- async listModels() {
420
- const { status, body } = await requestJson(`${this.config.baseUrl}/models`, {
421
- headers: authHeaders(this.config)
422
- });
423
- if (status === 401 || status === 403) {
424
- throw authFailed(this.config, status);
552
+ /** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
553
+ get requestTimeoutMs() {
554
+ return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
555
+ }
556
+ /** Authenticate + discover (roadmap §10, Model Discovery). */
557
+ async connect() {
558
+ authHeaders(this.config, this.env);
559
+ const models = await this.listModels();
560
+ this.discovered = models;
561
+ return models;
562
+ }
563
+ async getModel(modelId) {
564
+ const models = this.discovered ?? await this.connect();
565
+ return models.find((m) => m.id === modelId);
566
+ }
567
+ /** Raw /models body (protected so adapters can capture gateway-level metadata). */
568
+ async rawModelsResponse() {
569
+ const response = await this.modelsRequest();
570
+ return response.body;
571
+ }
572
+ async modelsRequest() {
573
+ let response;
574
+ try {
575
+ response = await requestJson(`${this.config.baseUrl}/models`, {
576
+ headers: authHeaders(this.config, this.env),
577
+ timeoutMs: this.requestTimeoutMs
578
+ });
579
+ } catch (error) {
580
+ if (error instanceof ProviderError) throw error;
581
+ throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
425
582
  }
583
+ const { status, body } = response;
426
584
  if (status !== 200) {
427
- throw unavailable(this.config, `Model discovery returned HTTP ${status}.`, status);
585
+ throw providerHttpError({
586
+ provider: this.config.name,
587
+ operation: "listModels",
588
+ status,
589
+ body
590
+ });
428
591
  }
592
+ return response;
593
+ }
594
+ async listModels() {
595
+ const body = await this.rawModelsResponse();
429
596
  const entries = extractModelEntries(body, this.config);
430
597
  const models = [];
431
598
  let skipped = 0;
@@ -434,22 +601,44 @@ var OpenAICompatibleProvider = class {
434
601
  skipped += 1;
435
602
  continue;
436
603
  }
437
- models.push({
438
- provider: this.config.name,
439
- id: entry.id,
440
- displayName: entry.id,
441
- status: "available",
442
- metadata: {
443
- ...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
444
- ...typeof entry.created === "number" ? { createdAt: entry.created } : {}
445
- }
446
- });
604
+ models.push(this.toModelInfo(entry));
447
605
  }
448
606
  if (skipped > 0) {
449
607
  this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
450
608
  }
451
609
  return models;
452
610
  }
611
+ /** Maps a raw model entry onto the normalized metadata shape (§10). */
612
+ toModelInfo(entry) {
613
+ const parameters = Array.isArray(entry.supported_parameters) ? entry.supported_parameters.filter((p) => typeof p === "string") : void 0;
614
+ const contextWindow = typeof entry.context_length === "number" ? entry.context_length : typeof entry.context_window === "number" ? entry.context_window : void 0;
615
+ const inputTypes = normalizeModalities(entry.input_modalities);
616
+ const outputTypes = normalizeModalities(entry.output_modalities);
617
+ return {
618
+ provider: this.config.name,
619
+ id: entry.id,
620
+ displayName: entry.id,
621
+ ...typeof entry.name === "string" ? { name: entry.name } : {},
622
+ status: "available",
623
+ ...contextWindow !== void 0 ? { contextWindow } : {},
624
+ ...inputTypes !== void 0 ? { inputTypes } : {},
625
+ ...outputTypes !== void 0 ? { outputTypes } : {},
626
+ ...parameters !== void 0 ? {
627
+ supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
628
+ supportsStructuredOutput: parameters.includes("response_format"),
629
+ supportsStreaming: parameters.includes("stream")
630
+ } : {},
631
+ metadata: {
632
+ ...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
633
+ ...typeof entry.created === "number" ? { createdAt: entry.created } : {},
634
+ ...parameters !== void 0 ? { supportedParameters: parameters } : {}
635
+ }
636
+ };
637
+ }
638
+ /** Streaming where the gateway supports it (§11). */
639
+ stream(request, onChunk) {
640
+ return streamChatCompletion(this.config, request, onChunk, this.env);
641
+ }
453
642
  async healthCheck() {
454
643
  const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
455
644
  try {
@@ -479,27 +668,35 @@ var OpenAICompatibleProvider = class {
479
668
  }
480
669
  }
481
670
  async chat(request) {
482
- const { status, body } = await requestJson(`${this.config.baseUrl}/chat/completions`, {
483
- method: "POST",
484
- headers: authHeaders(this.config),
485
- body: {
486
- model: request.model,
487
- messages: request.messages,
488
- ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
489
- ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
490
- ...request.stop !== void 0 ? { stop: request.stop } : {}
491
- }
492
- });
493
- if (status === 401 || status === 403) throw authFailed(this.config, status);
494
- if (status === 404) {
495
- throw new ProviderError(
496
- "MODEL_NOT_FOUND",
497
- `Model "${request.model}" or the chat endpoint was not found on provider "${this.config.name}" (HTTP 404).`,
498
- { details: { status } }
499
- );
671
+ let response;
672
+ try {
673
+ response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
674
+ method: "POST",
675
+ headers: authHeaders(this.config, this.env),
676
+ timeoutMs: this.requestTimeoutMs,
677
+ body: {
678
+ model: request.model,
679
+ messages: request.messages,
680
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
681
+ ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
682
+ ...request.stop !== void 0 ? { stop: request.stop } : {}
683
+ }
684
+ });
685
+ } catch (error) {
686
+ if (error instanceof ProviderError) throw error;
687
+ throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
500
688
  }
689
+ const { status, body } = response;
501
690
  if (status !== 200) {
502
- throw unavailable(this.config, `Chat completion returned HTTP ${status}.`, status);
691
+ throw providerHttpError({
692
+ provider: this.config.name,
693
+ operation: "chat",
694
+ status,
695
+ body,
696
+ ...status === 404 ? {
697
+ code: "MODEL_NOT_FOUND"
698
+ } : {}
699
+ });
503
700
  }
504
701
  const payload = body;
505
702
  const choices = Array.isArray(payload.choices) ? payload.choices : [];
@@ -521,6 +718,81 @@ var OpenAICompatibleProvider = class {
521
718
  };
522
719
  }
523
720
  };
721
+ async function streamChatCompletion(config, request, onChunk, env = process.env) {
722
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
723
+ const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
724
+ let response;
725
+ try {
726
+ response = await fetch(`${baseUrl}/chat/completions`, {
727
+ method: "POST",
728
+ headers: {
729
+ accept: "text/event-stream",
730
+ "content-type": "application/json",
731
+ ...authHeaders(config, env)
732
+ },
733
+ body: JSON.stringify({
734
+ model: request.model,
735
+ messages: request.messages,
736
+ stream: true,
737
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
738
+ ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
739
+ }),
740
+ signal: AbortSignal.timeout(timeoutMs)
741
+ });
742
+ } catch (error) {
743
+ throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
744
+ }
745
+ if (!response.ok) {
746
+ const body = await response.json().catch(() => void 0);
747
+ throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
748
+ }
749
+ if (!response.body) {
750
+ throw providerHttpError({
751
+ provider: config.name,
752
+ operation: "stream",
753
+ status: 500,
754
+ body: { error: { message: "streaming is not supported by this endpoint" } }
755
+ });
756
+ }
757
+ let content = "";
758
+ let model = request.model;
759
+ let finishReason;
760
+ const decoder = new TextDecoder();
761
+ let buffer = "";
762
+ for await (const piece of response.body) {
763
+ buffer += decoder.decode(piece, { stream: true });
764
+ let newline = buffer.indexOf("\n");
765
+ while (newline >= 0) {
766
+ const line = buffer.slice(0, newline).trim();
767
+ buffer = buffer.slice(newline + 1);
768
+ newline = buffer.indexOf("\n");
769
+ if (line.length === 0 || line.startsWith(":")) continue;
770
+ const data = line.startsWith("data:") ? line.slice(5).trim() : line;
771
+ if (data === "[DONE]") continue;
772
+ let event;
773
+ try {
774
+ event = JSON.parse(data);
775
+ } catch {
776
+ continue;
777
+ }
778
+ if (typeof event.model === "string") model = event.model;
779
+ const choices = Array.isArray(event.choices) ? event.choices : [];
780
+ const first = choices[0];
781
+ const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
782
+ if (delta.length > 0) content += delta;
783
+ if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
784
+ onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
785
+ }
786
+ }
787
+ return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
788
+ }
789
+ function normalizeModalities(value) {
790
+ const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
791
+ if (!list) return void 0;
792
+ const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
793
+ const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
794
+ return modalities.length > 0 ? modalities : void 0;
795
+ }
524
796
  function normalizeBaseUrl(baseUrl) {
525
797
  return baseUrl.replace(/\/+$/, "");
526
798
  }
@@ -535,14 +807,6 @@ function extractModelEntries(body, config) {
535
807
  { details: { endpoint: "models" } }
536
808
  );
537
809
  }
538
- function authFailed(config, status) {
539
- const envHint = config.apiKeyEnvVar ? ` Check that $${config.apiKeyEnvVar} is set and valid.` : " The provider requires authentication; configure api_key_env_var for it.";
540
- return new ProviderError(
541
- "PROVIDER_AUTH_FAILED",
542
- `Authentication with provider "${config.name}" failed (HTTP ${status}).${envHint}`,
543
- { details: { status } }
544
- );
545
- }
546
810
  function unavailable(config, message, status) {
547
811
  return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
548
812
  details: { status }
@@ -555,11 +819,33 @@ function inferHealth(error) {
555
819
 
556
820
  // src/providers/omnihilbras/index.ts
557
821
  var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
822
+ gateway;
823
+ async listModels() {
824
+ const raw = await this.rawModelsResponse();
825
+ await this.extractGatewayMetadata(raw);
826
+ const models = await super.listModels();
827
+ return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
828
+ }
558
829
  /**
559
830
  * Run the full connect flow. Throws on authentication or transport failure
560
831
  * (use healthCheck() for a non-throwing probe).
561
832
  */
562
- async connect(registry) {
833
+ /**
834
+ * Gateway metadata reported alongside the model list (gateway version, tier,
835
+ * region…), kept separate from per-model metadata (§11 — provider metadata).
836
+ */
837
+ gatewayMetadata() {
838
+ return this.gateway;
839
+ }
840
+ async extractGatewayMetadata(body) {
841
+ if (body !== null && typeof body === "object") {
842
+ const gateway = body.gateway;
843
+ if (gateway !== null && typeof gateway === "object") {
844
+ this.gateway = gateway;
845
+ }
846
+ }
847
+ }
848
+ async connectAndRegister(registry) {
563
849
  const apiKey = resolveApiKey(this.config);
564
850
  const discovered = await this.listModels();
565
851
  const seen = /* @__PURE__ */ new Set();
@@ -664,19 +950,431 @@ var ModelRegistry = class {
664
950
 
665
951
  // src/agents/process/index.ts
666
952
  import { spawn } from "child_process";
667
- var DEFAULT_TIMEOUT_MS2 = 6e5;
668
- var KILL_GRACE_MS = 5e3;
953
+
954
+ // src/agent-protocol/version.ts
955
+ var PROTOCOL_V2 = "omninode-agent-protocol/2";
956
+ var SUPPORTED_PROTOCOL_VERSIONS = [
957
+ "omninode-agent-protocol/1",
958
+ PROTOCOL_V2
959
+ ];
960
+ var MAX_MESSAGE_BYTES = 1048576;
961
+ function isSupportedProtocol(protocol) {
962
+ return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
963
+ }
964
+
965
+ // src/agent-protocol/types.ts
966
+ function newMessageId(prefix = "msg") {
967
+ const random = Math.random().toString(36).slice(2, 10);
968
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
969
+ }
970
+ function envelope(type, payload, ids = {}) {
971
+ return {
972
+ protocol: PROTOCOL_V2,
973
+ messageId: newMessageId(),
974
+ ...ids.requestId !== void 0 ? { requestId: ids.requestId } : {},
975
+ ...ids.taskId !== void 0 ? { taskId: ids.taskId } : {},
976
+ ...ids.agentId !== void 0 ? { agentId: ids.agentId } : {},
977
+ ...ids.pipelineId !== void 0 ? { pipelineId: ids.pipelineId } : {},
978
+ ...ids.executionId !== void 0 ? { executionId: ids.executionId } : {},
979
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
980
+ type,
981
+ payload,
982
+ ...ids.metadata !== void 0 ? { metadata: ids.metadata } : {}
983
+ };
984
+ }
985
+
986
+ // src/agent-protocol/codec.ts
987
+ var KNOWN_TYPES = /* @__PURE__ */ new Set([
988
+ // node → agent
989
+ "TASK",
990
+ "HELLO_ACK",
991
+ "CONTEXT",
992
+ "INSTRUCTION",
993
+ "RESPONSE",
994
+ "CANCEL",
995
+ // agent → node
996
+ "HELLO",
997
+ "TASK_REQUEST",
998
+ "TASK_ACCEPTED",
999
+ "TASK_STARTED",
1000
+ "STATUS",
1001
+ "QUESTION",
1002
+ "REPORT",
1003
+ "ARTIFACT",
1004
+ "COMPLETION",
1005
+ "ERROR",
1006
+ "CANCELLED"
1007
+ ]);
1008
+ var PAYLOAD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["TASK", "REPORT", "COMPLETION", "ERROR", "QUESTION", "ARTIFACT", "HELLO"]);
1009
+ function validateEnvelope(value, line) {
1010
+ const sample = line?.slice(0, 200);
1011
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1012
+ return { ok: false, error: { code: "NOT_AN_OBJECT", message: "message must be a JSON object", ...sample ? { sample } : {} } };
1013
+ }
1014
+ const raw = value;
1015
+ const protocol = raw.protocol;
1016
+ if (protocol === void 0) {
1017
+ const type = raw.type;
1018
+ if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
1019
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(type)}`, ...sample ? { sample } : {} } };
1020
+ }
1021
+ return {
1022
+ ok: true,
1023
+ legacy: true,
1024
+ envelope: {
1025
+ protocol: "omninode-agent-protocol/1",
1026
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "legacy",
1027
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1028
+ type,
1029
+ payload: raw.payload
1030
+ }
1031
+ };
1032
+ }
1033
+ if (typeof protocol !== "string" || !isSupportedProtocol(protocol)) {
1034
+ return {
1035
+ ok: false,
1036
+ error: { code: "UNSUPPORTED_VERSION", message: `unsupported protocol "${String(protocol)}"`, ...sample ? { sample } : {} }
1037
+ };
1038
+ }
1039
+ if (raw.type === void 0) {
1040
+ return { ok: false, error: { code: "MISSING_FIELD", message: "message is missing 'type'", ...sample ? { sample } : {} } };
1041
+ }
1042
+ if (typeof raw.type !== "string" || !KNOWN_TYPES.has(raw.type)) {
1043
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(raw.type)}`, ...sample ? { sample } : {} } };
1044
+ }
1045
+ if (raw.timestamp !== void 0 && typeof raw.timestamp !== "string") {
1046
+ return { ok: false, error: { code: "MISSING_FIELD", message: "'timestamp' must be a string", ...sample ? { sample } : {} } };
1047
+ }
1048
+ if (PAYLOAD_REQUIRED_TYPES.has(raw.type) && (raw.payload === void 0 || raw.payload === null)) {
1049
+ return { ok: false, error: { code: "INVALID_PAYLOAD", message: `${raw.type} requires a payload`, ...sample ? { sample } : {} } };
1050
+ }
1051
+ return {
1052
+ ok: true,
1053
+ legacy: protocol !== PROTOCOL_V2,
1054
+ envelope: {
1055
+ protocol,
1056
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "unknown",
1057
+ ...typeof raw.requestId === "string" ? { requestId: raw.requestId } : {},
1058
+ ...typeof raw.taskId === "string" ? { taskId: raw.taskId } : {},
1059
+ ...typeof raw.agentId === "string" ? { agentId: raw.agentId } : {},
1060
+ ...typeof raw.pipelineId === "string" ? { pipelineId: raw.pipelineId } : {},
1061
+ ...typeof raw.executionId === "string" ? { executionId: raw.executionId } : {},
1062
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
1063
+ type: raw.type,
1064
+ payload: raw.payload,
1065
+ ...raw.metadata !== null && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}
1066
+ }
1067
+ };
1068
+ }
1069
+ function decodeMessage(line, maxBytes = MAX_MESSAGE_BYTES) {
1070
+ const trimmed = line.trim();
1071
+ if (trimmed.length === 0) {
1072
+ return { ok: false, error: { code: "INVALID_JSON", message: "empty line" } };
1073
+ }
1074
+ if (trimmed.length > maxBytes) {
1075
+ return {
1076
+ ok: false,
1077
+ error: { code: "OVERSIZED", message: `message of ${trimmed.length} bytes exceeds the ${maxBytes} byte limit`, sample: trimmed.slice(0, 200) }
1078
+ };
1079
+ }
1080
+ let value;
1081
+ try {
1082
+ value = JSON.parse(trimmed);
1083
+ } catch (error) {
1084
+ return {
1085
+ ok: false,
1086
+ error: { code: "INVALID_JSON", message: error instanceof Error ? error.message : "invalid JSON", sample: trimmed.slice(0, 200) }
1087
+ };
1088
+ }
1089
+ return validateEnvelope(value, trimmed);
1090
+ }
1091
+ function encodeMessage(envelope2, maxBytes = MAX_MESSAGE_BYTES) {
1092
+ const line = JSON.stringify(envelope2);
1093
+ if (line.length > maxBytes) {
1094
+ throw new Error(`outbound protocol message of ${line.length} bytes exceeds the ${maxBytes} byte limit`);
1095
+ }
1096
+ return line;
1097
+ }
1098
+ var ProtocolDecoder = class {
1099
+ buffer = "";
1100
+ maxBytes;
1101
+ constructor(maxBytes = MAX_MESSAGE_BYTES) {
1102
+ this.maxBytes = maxBytes;
1103
+ }
1104
+ push(chunk) {
1105
+ this.buffer += chunk;
1106
+ const results = [];
1107
+ let index = this.buffer.indexOf("\n");
1108
+ while (index >= 0) {
1109
+ const line = this.buffer.slice(0, index);
1110
+ this.buffer = this.buffer.slice(index + 1);
1111
+ if (line.trim().length > 0) results.push(decodeMessage(line, this.maxBytes));
1112
+ index = this.buffer.indexOf("\n");
1113
+ }
1114
+ if (this.buffer.length > this.maxBytes) {
1115
+ results.push({
1116
+ ok: false,
1117
+ error: { code: "OVERSIZED", message: `line exceeded ${this.maxBytes} bytes without a newline`, sample: this.buffer.slice(0, 200) }
1118
+ });
1119
+ this.buffer = "";
1120
+ }
1121
+ return results;
1122
+ }
1123
+ /** Flush any trailing line when the process exits. */
1124
+ flush() {
1125
+ if (this.buffer.trim().length === 0) {
1126
+ this.buffer = "";
1127
+ return [];
1128
+ }
1129
+ const result = decodeMessage(this.buffer, this.maxBytes);
1130
+ this.buffer = "";
1131
+ return [result];
1132
+ }
1133
+ };
1134
+
1135
+ // src/agent-protocol/handshake.ts
1136
+ function parseHello(envelopeValue) {
1137
+ if (envelopeValue.type !== "HELLO") {
1138
+ return { ok: false, reason: `expected HELLO, received ${envelopeValue.type}` };
1139
+ }
1140
+ const payload = envelopeValue.payload;
1141
+ if (payload === null || typeof payload !== "object") {
1142
+ return { ok: false, reason: "HELLO payload must be an object" };
1143
+ }
1144
+ const p = payload;
1145
+ if (typeof p.name !== "string" || p.name.length === 0) {
1146
+ return { ok: false, reason: "HELLO payload is missing 'name'" };
1147
+ }
1148
+ return {
1149
+ ok: true,
1150
+ descriptor: {
1151
+ protocol: envelopeValue.protocol,
1152
+ name: p.name,
1153
+ ...typeof p.version === "string" ? { version: p.version } : {},
1154
+ capabilities: Array.isArray(p.capabilities) ? p.capabilities.filter((c) => typeof c === "string") : [],
1155
+ supports: Array.isArray(p.supports) ? p.supports.filter((s) => typeof s === "string") : []
1156
+ }
1157
+ };
1158
+ }
1159
+ function buildHello(descriptor) {
1160
+ return envelope("HELLO", descriptor, { agentId: descriptor.name });
1161
+ }
1162
+ function buildHelloAck(descriptor) {
1163
+ return envelope("HELLO_ACK", {
1164
+ ok: true,
1165
+ protocol: PROTOCOL_V2,
1166
+ agent: descriptor.name,
1167
+ ...descriptor.version !== void 0 ? { agentVersion: descriptor.version } : {},
1168
+ supportedProtocols: [PROTOCOL_V2]
1169
+ }, { agentId: descriptor.name });
1170
+ }
1171
+
1172
+ // src/agent-protocol/session.ts
1173
+ var ProtocolSession = class {
1174
+ decoder;
1175
+ ids;
1176
+ responder;
1177
+ violations = [];
1178
+ questions = [];
1179
+ artifacts = [];
1180
+ reports = [];
1181
+ descriptor;
1182
+ completionSummary;
1183
+ errorMessage;
1184
+ constructor(ids, options = {}) {
1185
+ this.ids = ids;
1186
+ this.responder = options.responder;
1187
+ this.decoder = new ProtocolDecoder(options.maxBytes);
1188
+ }
1189
+ /** The v2 messages OmniNode sends to open a task (written to the agent's stdin). */
1190
+ taskMessages(objective, role, context, instruction) {
1191
+ const ids = this.ids;
1192
+ const messages = [envelope("TASK", { taskId: ids.taskId, objective }, ids)];
1193
+ if (role !== void 0) messages.push(envelope("CONTEXT", { role }, ids));
1194
+ if (context !== void 0 && context.length > 0) messages.push(envelope("CONTEXT", { body: context }, ids));
1195
+ if (instruction !== void 0 && instruction.length > 0) messages.push(envelope("INSTRUCTION", { text: instruction }, ids));
1196
+ return messages.map((m) => encodeMessage(m));
1197
+ }
1198
+ helloAck(descriptor) {
1199
+ return encodeMessage(buildHelloAck(descriptor));
1200
+ }
1201
+ /** Consumes decoded stdout, answering questions as they arrive. */
1202
+ async handle(decoded) {
1203
+ if (!decoded.ok) {
1204
+ this.violations.push(decoded.error);
1205
+ return;
1206
+ }
1207
+ const message = decoded.envelope;
1208
+ if (message.protocol === PROTOCOL_V2) this.sawV2 = true;
1209
+ if (message.type === "HELLO") {
1210
+ const hello = parseHello(message);
1211
+ if (hello.ok) this.descriptor = hello.descriptor;
1212
+ else this.violations.push({ code: "INVALID_PAYLOAD", message: `bad HELLO: ${hello.reason}` });
1213
+ return;
1214
+ }
1215
+ if (message.type === "QUESTION") {
1216
+ await this.answerQuestion(message);
1217
+ return;
1218
+ }
1219
+ if (message.type === "REPORT") {
1220
+ this.reports.push(message.payload);
1221
+ return;
1222
+ }
1223
+ if (message.type === "ARTIFACT") {
1224
+ this.artifacts.push(message.payload);
1225
+ return;
1226
+ }
1227
+ if (message.type === "COMPLETION") {
1228
+ const payload = message.payload;
1229
+ this.completionSummary = typeof payload?.summary === "string" ? payload.summary : "";
1230
+ return;
1231
+ }
1232
+ if (message.type === "ERROR") {
1233
+ const payload = message.payload;
1234
+ const detail = typeof payload?.message === "string" ? payload.message : "agent reported an error";
1235
+ const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
1236
+ this.errorMessage = `Agent error${code}: ${detail}`;
1237
+ return;
1238
+ }
1239
+ if (message.type === "TASK_ACCEPTED" || message.type === "TASK_STARTED" || message.type === "STATUS") {
1240
+ return;
1241
+ }
1242
+ this.violations.push({
1243
+ code: "UNKNOWN_TYPE",
1244
+ message: `unexpected message type ${message.type} from agent`
1245
+ });
1246
+ }
1247
+ async answerQuestion(message) {
1248
+ const payload = message.payload ?? {};
1249
+ const questionId = typeof payload.questionId === "string" ? payload.questionId : message.messageId;
1250
+ const question = typeof payload.question === "string" ? payload.question : "";
1251
+ const expected = typeof payload.expected === "string" ? payload.expected : "text";
1252
+ let answer;
1253
+ if (this.responder) {
1254
+ try {
1255
+ answer = await this.responder({ questionId, question, expected });
1256
+ } catch (error) {
1257
+ this.violations.push({
1258
+ code: "INVALID_PAYLOAD",
1259
+ message: `question responder failed: ${error instanceof Error ? error.message : String(error)}`
1260
+ });
1261
+ }
1262
+ }
1263
+ this.questions.push({ questionId, question, ...answer !== void 0 ? { answer } : {} });
1264
+ this.pendingAnswers.push(
1265
+ encodeMessage(
1266
+ envelope("RESPONSE", { questionId, ...answer !== void 0 ? { body: answer } : {} }, {
1267
+ ...this.ids,
1268
+ requestId: message.requestId ?? this.ids.requestId
1269
+ })
1270
+ )
1271
+ );
1272
+ }
1273
+ pendingAnswers = [];
1274
+ /** True once any inbound message carried the v2 protocol field. */
1275
+ sawV2 = false;
1276
+ };
1277
+ async function decodeStream(session, chunk) {
1278
+ const decoded = session.decoder.push(chunk);
1279
+ for (const result of decoded) {
1280
+ await session.handle(result);
1281
+ }
1282
+ return session.pendingAnswers.splice(0);
1283
+ }
1284
+ function requireCompletion(session) {
1285
+ if (session.errorMessage !== void 0) {
1286
+ throw new ProtocolError(session.errorMessage);
1287
+ }
1288
+ }
1289
+
1290
+ // src/agents/cwd.ts
1291
+ import { statSync } from "fs";
1292
+ import { isAbsolute, relative, resolve } from "path";
1293
+ function validateWorkingDirectory(cwd, options = {}) {
1294
+ if (!cwd) return void 0;
1295
+ const normalized = isAbsolute(cwd) ? resolve(cwd) : resolve(process.cwd(), cwd);
1296
+ let stats;
1297
+ try {
1298
+ stats = statSync(normalized);
1299
+ } catch (error) {
1300
+ throw new AgentError(
1301
+ "AGENT_NOT_FOUND",
1302
+ `Working directory "${normalized}" does not exist or is not accessible.`,
1303
+ { cause: error, details: { cwd: normalized } }
1304
+ );
1305
+ }
1306
+ if (!stats.isDirectory()) {
1307
+ throw new AgentError("AGENT_NOT_FOUND", `Working directory "${normalized}" is not a directory.`, {
1308
+ details: { cwd: normalized }
1309
+ });
1310
+ }
1311
+ try {
1312
+ statSync(resolve(normalized, "."));
1313
+ } catch (error) {
1314
+ throw new AgentError(
1315
+ "AGENT_NOT_FOUND",
1316
+ `Working directory "${normalized}" is not accessible.`,
1317
+ { cause: error }
1318
+ );
1319
+ }
1320
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
1321
+ const rel = relative(projectRoot, normalized);
1322
+ const outsideProject = rel.startsWith("..") || isAbsolute(rel);
1323
+ if (outsideProject && options.allowExternal !== true) {
1324
+ throw new AgentError(
1325
+ "AGENT_NOT_FOUND",
1326
+ `Working directory "${normalized}" is outside the project root "${projectRoot}". Set allow_external_cwd: true on the agent to permit this.`,
1327
+ { details: { cwd: normalized, projectRoot } }
1328
+ );
1329
+ }
1330
+ return { cwd: normalized, outsideProject };
1331
+ }
1332
+
1333
+ // src/agents/env.ts
1334
+ var BASELINE_KEYS = ["PATH", "HOME"];
1335
+ function resolveEnvPolicy(config) {
1336
+ if (config.envPolicy !== void 0) return config.envPolicy;
1337
+ if (config.inheritEnv === false) return "explicit";
1338
+ return "inherit";
1339
+ }
669
1340
  function buildChildEnv(config) {
670
1341
  const configured = config.env ?? {};
671
- if (config.inheritEnv !== false) {
1342
+ const policy = resolveEnvPolicy(config);
1343
+ if (policy === "inherit") {
672
1344
  return { ...process.env, ...configured };
673
1345
  }
674
- return {
675
- PATH: process.env.PATH ?? "",
676
- HOME: process.env.HOME ?? "",
677
- ...configured
678
- };
1346
+ const base = {};
1347
+ for (const key of BASELINE_KEYS) {
1348
+ const value = process.env[key];
1349
+ if (value !== void 0) base[key] = value;
1350
+ }
1351
+ switch (policy) {
1352
+ case "explicit":
1353
+ return { ...base, ...configured };
1354
+ case "allowlist": {
1355
+ const allowed = { ...base };
1356
+ for (const key of config.envAllowlist ?? []) {
1357
+ const value = process.env[key];
1358
+ if (value !== void 0) allowed[key] = value;
1359
+ }
1360
+ return { ...allowed, ...configured };
1361
+ }
1362
+ case "denylist": {
1363
+ const denied = new Set((config.envDenylist ?? []).map((k) => k.toUpperCase()));
1364
+ const filtered = {};
1365
+ for (const [key, value] of Object.entries(process.env)) {
1366
+ if (denied.has(key.toUpperCase())) continue;
1367
+ filtered[key] = value;
1368
+ }
1369
+ return { ...filtered, ...configured };
1370
+ }
1371
+ }
679
1372
  }
1373
+
1374
+ // src/agents/process/index.ts
1375
+ var DEFAULT_TIMEOUT_MS2 = 6e5;
1376
+ var KILL_GRACE_MS = 5e3;
1377
+ var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
680
1378
  function composeTaskText(input) {
681
1379
  const sections = [`# Objective
682
1380
  ${input.objective}`];
@@ -697,18 +1395,19 @@ ${input.instruction}`);
697
1395
  return sections.join("\n\n");
698
1396
  }
699
1397
  function buildProtocolMessages(input, agentName) {
700
- const meta = {
1398
+ const session = new ProtocolSession({
1399
+ requestId: `req-${input.taskId}`,
701
1400
  taskId: input.taskId,
702
- agent: agentName,
703
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
704
- };
705
- const messages = [
706
- { type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
707
- ];
708
- if (input.role) messages.push({ type: "ROLE", payload: input.role });
709
- if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
710
- if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
711
- return messages.map((message) => JSON.stringify({ meta, message }));
1401
+ agentId: agentName,
1402
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1403
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1404
+ });
1405
+ return session.taskMessages(
1406
+ input.objective,
1407
+ input.role,
1408
+ input.context,
1409
+ input.instruction
1410
+ );
712
1411
  }
713
1412
  var ProcessAgent = class {
714
1413
  config;
@@ -737,40 +1436,73 @@ var ProcessAgent = class {
737
1436
  }
738
1437
  const mode = this.config.inputMode ?? "stdin";
739
1438
  const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
740
- return new Promise((resolve, reject) => {
1439
+ return new Promise((resolve2, reject) => {
1440
+ let cwd;
1441
+ try {
1442
+ cwd = validateWorkingDirectory(this.config.cwd, {
1443
+ allowExternal: this.config.allowExternalCwd
1444
+ })?.cwd;
1445
+ } catch (error) {
1446
+ reject(error);
1447
+ return;
1448
+ }
741
1449
  const child = spawn(
742
1450
  command,
743
1451
  [...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
744
1452
  {
745
- cwd: this.config.cwd,
1453
+ ...cwd !== void 0 ? { cwd } : {},
746
1454
  env: buildChildEnv(this.config),
747
- stdio: ["pipe", "pipe", "pipe"]
1455
+ stdio: ["pipe", "pipe", "pipe"],
1456
+ // Own process group so the whole tree can be terminated together
1457
+ // (roadmap §8: no orphaned children).
1458
+ ...process.platform === "win32" ? {} : { detached: true }
748
1459
  }
749
1460
  );
750
1461
  this.running.set(input.taskId, child);
1462
+ GLOBAL_RUNNING.add(child);
1463
+ installSignalForwarding();
751
1464
  let stdout = "";
752
1465
  let stderr = "";
753
1466
  let timedOut = false;
1467
+ let outputLimitExceeded = false;
754
1468
  let settled = false;
1469
+ const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
755
1470
  const timer = setTimeout(() => {
756
1471
  timedOut = true;
757
- this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
758
- child.kill("SIGTERM");
759
- setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
1472
+ this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process tree.`);
1473
+ killTree(child, "SIGTERM");
1474
+ setTimeout(() => killTree(child, "SIGKILL"), KILL_GRACE_MS).unref();
760
1475
  }, timeoutMs);
761
1476
  const settle = (finish) => {
762
1477
  if (settled) return;
763
1478
  settled = true;
764
1479
  clearTimeout(timer);
765
1480
  this.running.delete(input.taskId);
1481
+ GLOBAL_RUNNING.delete(child);
766
1482
  finish();
767
1483
  };
768
- child.stdout?.on("data", (chunk) => {
769
- stdout += chunk.toString();
770
- });
771
- child.stderr?.on("data", (chunk) => {
772
- stderr += chunk.toString();
773
- });
1484
+ const collect = (into, chunk) => {
1485
+ if (outputLimitExceeded) return;
1486
+ const text = sanitizeChunk(chunk);
1487
+ if (into === "stdout") {
1488
+ if (stdout.length + text.length > maxOutputBytes) {
1489
+ stdout = (stdout + text).slice(0, maxOutputBytes);
1490
+ outputLimitExceeded = true;
1491
+ this.log.warn(`Agent "${this.config.name}" exceeded the ${maxOutputBytes} byte output limit \u2014 killing it.`);
1492
+ killTree(child, "SIGKILL");
1493
+ return;
1494
+ }
1495
+ stdout += text;
1496
+ } else {
1497
+ if (stderr.length + text.length > maxOutputBytes) {
1498
+ stderr = (stderr + text).slice(0, maxOutputBytes);
1499
+ return;
1500
+ }
1501
+ stderr += text;
1502
+ }
1503
+ };
1504
+ child.stdout?.on("data", (chunk) => collect("stdout", chunk));
1505
+ child.stderr?.on("data", (chunk) => collect("stderr", chunk));
774
1506
  child.stdin?.on("error", () => {
775
1507
  });
776
1508
  if (mode !== "arg") {
@@ -804,7 +1536,19 @@ var ProcessAgent = class {
804
1536
  new AgentError(
805
1537
  "AGENT_TIMEOUT",
806
1538
  `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) } }
1539
+ {
1540
+ details: {
1541
+ operation: "agent.run",
1542
+ agent: this.config.name,
1543
+ durationMs: timeoutMs,
1544
+ // The process was started and may have performed side
1545
+ // effects before being killed — the outcome is unknown,
1546
+ // not failed (roadmap §6.3).
1547
+ dispatched: true,
1548
+ stdout: stdout.slice(0, 4e3),
1549
+ stderr: stderr.slice(0, 2e3)
1550
+ }
1551
+ }
808
1552
  )
809
1553
  );
810
1554
  return;
@@ -812,7 +1556,7 @@ var ProcessAgent = class {
812
1556
  const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
813
1557
  if (mode === "protocol") {
814
1558
  try {
815
- resolve(
1559
+ resolve2(
816
1560
  interpretProtocolRun({
817
1561
  input,
818
1562
  agentName: this.config.name,
@@ -827,14 +1571,14 @@ var ProcessAgent = class {
827
1571
  return;
828
1572
  }
829
1573
  const trimmed = stdout.trim();
830
- resolve({
1574
+ resolve2({
831
1575
  taskId: input.taskId,
832
- status: code === 0 ? "completed" : "failed",
1576
+ status: code === 0 && !outputLimitExceeded ? "completed" : "failed",
833
1577
  summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
834
1578
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
835
1579
  exitCode: code ?? void 0,
836
1580
  logs,
837
- error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
1581
+ ...outputLimitExceeded ? { error: `Agent exceeded the ${maxOutputBytes} byte output limit and was terminated.` } : code === 0 ? {} : { error: describeExit(code, signal) }
838
1582
  });
839
1583
  });
840
1584
  });
@@ -843,67 +1587,117 @@ var ProcessAgent = class {
843
1587
  /** Ask the agent to stop current work (SIGTERM). */
844
1588
  async cancel(taskId) {
845
1589
  const child = this.running.get(taskId);
846
- if (child) child.kill("SIGTERM");
1590
+ if (child) killTree(child, "SIGTERM");
847
1591
  }
848
1592
  runningTaskIds() {
849
1593
  return [...this.running.keys()];
850
1594
  }
851
1595
  };
852
- function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
853
- const reports = [];
854
- let summary;
855
- let error;
856
- let sawProtocolMessage = false;
857
- for (const line of stdout.split("\n")) {
858
- const trimmed2 = line.trim();
859
- if (trimmed2.length === 0) continue;
860
- const message = parseAgentMessage(trimmed2);
861
- if (!message) continue;
862
- sawProtocolMessage = true;
863
- if (message.type === "REPORT") {
864
- const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
865
- if (report) reports.push(report);
866
- } else if (message.type === "COMPLETION") {
867
- const payload = message.payload;
868
- if (typeof payload?.summary === "string") summary = payload.summary;
869
- } else if (message.type === "ERROR") {
870
- const payload = message.payload;
871
- const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
872
- const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
873
- error = `Agent error${code}: ${detail}`;
874
- }
1596
+ function interpretProtocolRun({
1597
+ input,
1598
+ agentName,
1599
+ stdout,
1600
+ exitCode,
1601
+ logs,
1602
+ responder
1603
+ }) {
1604
+ const session = new ProtocolSession(
1605
+ {
1606
+ requestId: `req-${input.taskId}`,
1607
+ taskId: input.taskId,
1608
+ agentId: agentName,
1609
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1610
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1611
+ },
1612
+ { ...responder !== void 0 ? { responder } : {} }
1613
+ );
1614
+ for (const result of session.decoder.push(stdout)) {
1615
+ session.handle(result);
875
1616
  }
876
- if (!sawProtocolMessage && exitCode === 0) {
877
- throw new ProtocolError(
878
- `Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
879
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
880
- );
1617
+ for (const result of session.decoder.flush()) {
1618
+ session.handle(result);
881
1619
  }
1620
+ const reports = session.reports.flatMap((payload) => Array.isArray(payload) ? payload : [payload]).map((payload, index) => normalizeReport(payload, input.taskId, agentName, index)).filter((report) => report !== void 0);
882
1621
  const trimmed = stdout.trim();
1622
+ const protocolBlock = {
1623
+ protocol: session.descriptor?.protocol ?? PROTOCOL_V2,
1624
+ legacy: !session.sawV2 && session.descriptor === void 0,
1625
+ ...session.descriptor !== void 0 ? { descriptor: session.descriptor } : {},
1626
+ questions: session.questions,
1627
+ violations: session.violations.map((v) => `${v.code}: ${v.message}`)
1628
+ };
1629
+ if (session.errorMessage !== void 0) {
1630
+ return {
1631
+ taskId: input.taskId,
1632
+ status: "failed",
1633
+ error: session.errorMessage,
1634
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1635
+ exitCode: exitCode ?? void 0,
1636
+ logs,
1637
+ protocol: protocolBlock,
1638
+ ...reports.length > 0 ? { reports } : {}
1639
+ };
1640
+ }
1641
+ if (session.descriptor === void 0 && session.violations.length > 0 && session.reports.length === 0 && session.completionSummary === void 0) {
1642
+ return {
1643
+ taskId: input.taskId,
1644
+ status: "failed",
1645
+ error: `agent produced no valid protocol messages: ${protocolBlock.violations.join("; ")}`,
1646
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1647
+ exitCode: exitCode ?? void 0,
1648
+ logs,
1649
+ protocol: protocolBlock
1650
+ };
1651
+ }
1652
+ const completed = exitCode === 0 && session.completionSummary !== void 0;
883
1653
  return {
884
1654
  taskId: input.taskId,
885
- status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
886
- summary: summary ?? reports[0]?.summary,
1655
+ status: completed || exitCode === 0 ? completed ? "completed" : "completed" : "failed",
1656
+ summary: session.completionSummary ?? reports[0]?.summary,
887
1657
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
888
1658
  exitCode: exitCode ?? void 0,
889
1659
  logs,
890
- reports: reports.length > 0 ? reports : void 0,
891
- error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
1660
+ protocol: protocolBlock,
1661
+ ...reports.length > 0 ? { reports } : {},
1662
+ ...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
892
1663
  };
893
1664
  }
894
- function parseAgentMessage(line) {
895
- try {
896
- const parsed = JSON.parse(line);
897
- if (typeof parsed.type === "string") {
898
- return { type: parsed.type, payload: parsed.payload };
899
- }
900
- const inner = parsed.message;
901
- if (inner && typeof inner.type === "string") {
902
- return { type: inner.type, payload: inner.payload };
1665
+ var GLOBAL_RUNNING = /* @__PURE__ */ new Set();
1666
+ var signalForwardingInstalled = false;
1667
+ function installSignalForwarding() {
1668
+ if (signalForwardingInstalled) return;
1669
+ signalForwardingInstalled = true;
1670
+ for (const signal of ["SIGINT", "SIGTERM"]) {
1671
+ process.on(signal, () => {
1672
+ for (const child of GLOBAL_RUNNING) {
1673
+ killTree(child, "SIGTERM");
1674
+ }
1675
+ });
1676
+ }
1677
+ }
1678
+ function killTree(child, signal) {
1679
+ if (child.pid === void 0) return;
1680
+ if (process.platform !== "win32") {
1681
+ try {
1682
+ process.kill(-child.pid, signal);
1683
+ return;
1684
+ } catch {
903
1685
  }
1686
+ }
1687
+ try {
1688
+ child.kill(signal);
904
1689
  } catch {
905
1690
  }
906
- return void 0;
1691
+ }
1692
+ function describeExit(code, signal) {
1693
+ if (signal !== null) {
1694
+ return signal === "SIGKILL" ? "Process was killed (SIGKILL) \u2014 usually an external kill or OOM." : `Process terminated by signal ${signal}.`;
1695
+ }
1696
+ if (code === null) return "Process terminated without an exit code.";
1697
+ return `Process exited with code ${code}.`;
1698
+ }
1699
+ function sanitizeChunk(chunk) {
1700
+ return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
907
1701
  }
908
1702
  function normalizeReport(payload, taskId, agentName, index) {
909
1703
  if (payload === null || typeof payload !== "object") return void 0;
@@ -924,6 +1718,40 @@ function firstLine(text, maxLength = 200) {
924
1718
  return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
925
1719
  }
926
1720
 
1721
+ // src/agents/adapters.ts
1722
+ var AgentAdapterRegistry = class {
1723
+ adapters = /* @__PURE__ */ new Map();
1724
+ register(adapter) {
1725
+ this.adapters.set(adapter.integration, adapter);
1726
+ }
1727
+ get(integration) {
1728
+ return this.adapters.get(integration);
1729
+ }
1730
+ list() {
1731
+ return [...this.adapters.values()];
1732
+ }
1733
+ create(config) {
1734
+ const adapter = this.get(config.integration);
1735
+ if (!adapter) {
1736
+ throw new AgentError(
1737
+ "AGENT_NOT_FOUND",
1738
+ `No agent adapter registered for integration "${config.integration}". Registered: ${[...this.adapters.keys()].join(", ") || "(none)"}.`
1739
+ );
1740
+ }
1741
+ return adapter.create(config);
1742
+ }
1743
+ };
1744
+ var ProcessAgentAdapter = class {
1745
+ constructor(factory) {
1746
+ this.factory = factory;
1747
+ }
1748
+ factory;
1749
+ integration = "process";
1750
+ create(config) {
1751
+ return this.factory(config);
1752
+ }
1753
+ };
1754
+
927
1755
  // src/agents/registry.ts
928
1756
  var AgentRegistry = class {
929
1757
  agents = /* @__PURE__ */ new Map();
@@ -939,14 +1767,15 @@ var AgentRegistry = class {
939
1767
  };
940
1768
 
941
1769
  // src/agents/factory.ts
942
- function createAgent(config) {
943
- if (config.integration === "process") {
944
- return new ProcessAgent(config);
945
- }
1770
+ var defaultAgentAdapters = new AgentAdapterRegistry();
1771
+ defaultAgentAdapters.register(new ProcessAgentAdapter((config) => new ProcessAgent(config)));
1772
+ function createAgent(config, adapters = defaultAgentAdapters) {
1773
+ const registered = adapters.get(config.integration);
1774
+ if (registered) return registered.create(config);
946
1775
  if (config.integration === "native") {
947
1776
  throw new OmniNodeError(
948
1777
  "NOT_IMPLEMENTED",
949
- `Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
1778
+ `No native adapter is registered for "${config.name}". Register an IAgentAdapter (createAgent(config, adapters)) or use input over stdin/stdout (type "cli").`
950
1779
  );
951
1780
  }
952
1781
  throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
@@ -987,40 +1816,54 @@ function buildRoleRegistry(config) {
987
1816
  return registry;
988
1817
  }
989
1818
 
990
- // src/tasks/store.ts
1819
+ // src/persistence/json-file-store.ts
991
1820
  import { mkdir, readFile, rename, writeFile } from "fs/promises";
992
1821
  import { dirname } from "path";
993
- var FileTaskStore = class {
1822
+ var JsonFileStore = class {
994
1823
  filePath;
995
- /**
996
- * Read-modify-write operations are serialized: parallel pipeline tasks
997
- * would otherwise race on the JSON file (last writer wins).
998
- */
1824
+ schemaVersion;
1825
+ onCorrupt;
1826
+ log;
999
1827
  queue = Promise.resolve();
1000
- constructor(directory = `${process.cwd()}/.omninode`) {
1001
- this.filePath = `${directory}/tasks.json`;
1828
+ constructor(options) {
1829
+ this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
1830
+ this.schemaVersion = options.schemaVersion ?? 1;
1831
+ this.onCorrupt = options.onCorrupt;
1832
+ this.log = logger.child({ component: "persistence", file: options.fileName });
1002
1833
  }
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
- });
1834
+ get path() {
1835
+ return this.filePath;
1011
1836
  }
1012
- async get(id) {
1013
- return this.synchronized(async () => {
1014
- const tasks = await this.load();
1015
- return tasks.find((t) => t.id === id);
1016
- });
1837
+ /** All items, or [] when missing/corrupt. */
1838
+ async readAll() {
1839
+ let raw;
1840
+ try {
1841
+ raw = await readFile(this.filePath, "utf8");
1842
+ } catch {
1843
+ return [];
1844
+ }
1845
+ try {
1846
+ const parsed = JSON.parse(raw);
1847
+ if (Array.isArray(parsed)) return parsed;
1848
+ if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.items)) {
1849
+ return parsed.items;
1850
+ }
1851
+ throw new Error("persisted file has an unrecognized shape");
1852
+ } catch (error) {
1853
+ this.log.warn(`Treating ${this.filePath} as empty \u2014 it could not be parsed.`);
1854
+ this.onCorrupt?.(this.filePath, error);
1855
+ return [];
1856
+ }
1017
1857
  }
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
- });
1858
+ async writeAll(items) {
1859
+ const document = { schemaVersion: this.schemaVersion, items };
1860
+ const tmp = `${this.filePath}.tmp`;
1861
+ await mkdir(dirname(this.filePath), { recursive: true });
1862
+ await writeFile(tmp, `${JSON.stringify(document, null, 2)}
1863
+ `, "utf8");
1864
+ await rename(tmp, this.filePath);
1023
1865
  }
1866
+ /** Serializes an async operation against this file. */
1024
1867
  synchronized(operation) {
1025
1868
  const result = this.queue.then(operation, operation);
1026
1869
  this.queue = result.then(
@@ -1029,21 +1872,37 @@ var FileTaskStore = class {
1029
1872
  );
1030
1873
  return result;
1031
1874
  }
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
- }
1875
+ /** Read-modify-write helper: applies `mutate` to the current items. */
1876
+ async mutate(mutate) {
1877
+ return this.synchronized(async () => {
1878
+ const items = await this.readAll();
1879
+ const [next, result] = await mutate(items);
1880
+ await this.writeAll(next);
1881
+ return result;
1882
+ });
1883
+ }
1884
+ };
1885
+
1886
+ // src/tasks/store.ts
1887
+ var FileTaskStore = class extends JsonFileStore {
1888
+ constructor(directory) {
1889
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "tasks.json" });
1890
+ }
1891
+ save(task) {
1892
+ return this.mutate((tasks) => {
1893
+ const index = tasks.findIndex((t) => t.id === task.id);
1894
+ if (index >= 0) tasks[index] = task;
1895
+ else tasks.push(task);
1896
+ return [tasks, void 0];
1897
+ });
1040
1898
  }
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);
1899
+ async get(id) {
1900
+ const tasks = await this.readAll();
1901
+ return tasks.find((t) => t.id === id);
1902
+ }
1903
+ async list(filter = {}) {
1904
+ const tasks = await this.readAll();
1905
+ 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
1906
  }
1048
1907
  };
1049
1908
 
@@ -1052,12 +1911,18 @@ import { randomBytes } from "crypto";
1052
1911
  var ALLOWED_TRANSITIONS = {
1053
1912
  created: ["queued", "running", "cancelled"],
1054
1913
  queued: ["running", "cancelled"],
1055
- running: ["completed", "failed", "cancelled"],
1914
+ running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
1056
1915
  waiting: ["running", "cancelled"],
1057
1916
  completed: [],
1058
- failed: [],
1059
- cancelled: []
1917
+ // failed → running is the only re-entry: bounded automatic retry (§6.4).
1918
+ failed: ["running"],
1919
+ timed_out: [],
1920
+ cancelled: [],
1921
+ unknown: [],
1922
+ partially_completed: []
1060
1923
  };
1924
+ var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
1925
+ var EXECUTION_HISTORY_LIMIT = 10;
1061
1926
  var TaskEngine = class {
1062
1927
  constructor(options) {
1063
1928
  this.options = options;
@@ -1089,6 +1954,7 @@ var TaskEngine = class {
1089
1954
  id: generateTaskId(),
1090
1955
  objective: input.objective.trim(),
1091
1956
  status: "created",
1957
+ attempt: 0,
1092
1958
  createdAt: now,
1093
1959
  updatedAt: now,
1094
1960
  ...input.project !== void 0 ? { project: input.project } : {},
@@ -1105,8 +1971,10 @@ var TaskEngine = class {
1105
1971
  const task = await this.mustGet(id);
1106
1972
  return this.transition(task, "queued");
1107
1973
  }
1108
- /** Runs the task through its assigned agent and records the result. */
1109
- async run(id) {
1974
+ /** Runs the task through its assigned agent and records the result.
1975
+ * `maxAttempts` enables bounded automatic retries of *retryable* errors
1976
+ * with linear backoff; unknown/timed_out outcomes are never auto-retried. */
1977
+ async run(id, options = {}) {
1110
1978
  const task = await this.mustGet(id);
1111
1979
  if (task.status !== "created" && task.status !== "queued") {
1112
1980
  throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
@@ -1119,49 +1987,89 @@ var TaskEngine = class {
1119
1987
  throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
1120
1988
  }
1121
1989
  const role = task.role ? this.options.roles.get(task.role) : void 0;
1122
- const running = await this.transition(task, "running");
1123
1990
  this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
1124
- let background = running.context?.background;
1991
+ let background = task.context?.background;
1125
1992
  if (this.options.memory) {
1126
1993
  const memoryContext = await this.options.memory.gatherContext({
1127
- objective: running.objective,
1128
- ...running.role ? { role: running.role } : {},
1129
- ...running.project ? { project: running.project } : {}
1994
+ objective: task.objective,
1995
+ ...task.role ? { role: task.role } : {},
1996
+ ...task.project ? { project: task.project } : {}
1130
1997
  });
1131
1998
  if (memoryContext.length > 0) {
1132
1999
  background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
1133
2000
  }
1134
2001
  }
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() }
2002
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
2003
+ const backoffMs = options.retryBackoffMs ?? 0;
2004
+ let current = task;
2005
+ for (let i = 0; i < maxAttempts; i += 1) {
2006
+ const attempt = current.attempt + 1;
2007
+ const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
2008
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2009
+ current = await this.transition(current, "running", { attempt });
2010
+ let output;
2011
+ let thrown;
2012
+ try {
2013
+ output = await agent.run({
2014
+ taskId: current.id,
2015
+ objective: current.objective,
2016
+ ...role ? { role } : {},
2017
+ ...background !== void 0 ? { context: background } : {}
2018
+ });
2019
+ } catch (error) {
2020
+ thrown = error;
2021
+ }
2022
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2023
+ if (thrown !== void 0 || output === void 0) {
2024
+ const outcome = classifyThrown(thrown);
2025
+ const message = thrown instanceof Error ? thrown.message : String(thrown);
2026
+ const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
2027
+ current = await this.transition(current, outcome, {
2028
+ attempt,
2029
+ lastError: message,
2030
+ execution: {
2031
+ executionId,
2032
+ attempt,
2033
+ startedAt,
2034
+ finishedAt,
2035
+ outcome,
2036
+ error: message
2037
+ }
2038
+ });
2039
+ this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
2040
+ if (retryable) {
2041
+ if (backoffMs > 0) await sleep(backoffMs * attempt);
2042
+ continue;
2043
+ }
2044
+ if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
2045
+ return current;
2046
+ }
2047
+ const currentState = await this.options.store.get(current.id);
2048
+ if (currentState?.status === "cancelled") {
2049
+ current = currentState;
2050
+ break;
2051
+ }
2052
+ const result = {
2053
+ ...output.summary !== void 0 ? { summary: output.summary } : {},
2054
+ ...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
2055
+ // Keep the verbatim output (bounded) for report extraction and audit.
2056
+ ...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
2057
+ ...output.error !== void 0 ? { error: output.error } : {},
2058
+ finishedAt
2059
+ };
2060
+ const finalStatus = output.status === "completed" ? "completed" : "failed";
2061
+ current = await this.transition(current, finalStatus, {
2062
+ attempt,
2063
+ result,
2064
+ execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
1148
2065
  });
2066
+ this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
2067
+ break;
1149
2068
  }
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
2069
  if (this.options.memory) {
1162
- await this.options.memory.recordTaskOutcome(finished);
2070
+ await this.options.memory.recordTaskOutcome(current);
1163
2071
  }
1164
- return finished;
2072
+ return current;
1165
2073
  }
1166
2074
  async cancel(id) {
1167
2075
  const task = await this.mustGet(id);
@@ -1174,13 +2082,14 @@ var TaskEngine = class {
1174
2082
  }
1175
2083
  return this.transition(task, "cancelled");
1176
2084
  }
1177
- /** Error recovery (§27 Phase 9): reset a failed/cancelled task so it can run again. */
2085
+ /** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
1178
2086
  async retry(id) {
1179
2087
  const task = await this.mustGet(id);
1180
- if (task.status !== "failed" && task.status !== "cancelled") {
2088
+ const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
2089
+ if (!retryableStates.includes(task.status)) {
1181
2090
  throw new TaskError(
1182
2091
  "TASK_INVALID",
1183
- `Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
2092
+ `Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
1184
2093
  );
1185
2094
  }
1186
2095
  const { result: _discarded, ...rest } = task;
@@ -1212,10 +2121,14 @@ var TaskEngine = class {
1212
2121
  if (!allowed.includes(to)) {
1213
2122
  throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
1214
2123
  }
2124
+ const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
1215
2125
  const updated = {
1216
2126
  ...task,
1217
2127
  status: to,
1218
2128
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2129
+ ...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
2130
+ ...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
2131
+ ...executions !== void 0 ? { executions } : {},
1219
2132
  ...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
1220
2133
  };
1221
2134
  await this.options.store.save(updated);
@@ -1236,6 +2149,19 @@ var TaskEngine = class {
1236
2149
  function generateTaskId() {
1237
2150
  return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
1238
2151
  }
2152
+ function errorCode(error) {
2153
+ return error?.code ?? "";
2154
+ }
2155
+ function classifyThrown(error) {
2156
+ if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
2157
+ const dispatched = error.details?.dispatched;
2158
+ return dispatched === false ? "timed_out" : "unknown";
2159
+ }
2160
+ return "failed";
2161
+ }
2162
+ function sleep(ms) {
2163
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2164
+ }
1239
2165
 
1240
2166
  // src/audit/index.ts
1241
2167
  import { appendFile, mkdir as mkdir2 } from "fs/promises";
@@ -1260,10 +2186,10 @@ var FileAuditLog = class {
1260
2186
  }
1261
2187
  /** Recent events, oldest first; used by the CLI. */
1262
2188
  async recent(limit = 20) {
1263
- const { readFile: readFile6 } = await import("fs/promises");
2189
+ const { readFile: readFile2 } = await import("fs/promises");
1264
2190
  let raw;
1265
2191
  try {
1266
- raw = await readFile6(this.filePath, "utf8");
2192
+ raw = await readFile2(this.filePath, "utf8");
1267
2193
  } catch {
1268
2194
  return [];
1269
2195
  }
@@ -1278,9 +2204,24 @@ var FileAuditLog = class {
1278
2204
  }
1279
2205
  };
1280
2206
 
2207
+ // src/memory/base.ts
2208
+ function defaultSearch(provider, text, options = {}) {
2209
+ return provider.retrieve({
2210
+ text,
2211
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
2212
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
2213
+ });
2214
+ }
2215
+ function defaultMetadata(name, options = {}) {
2216
+ return {
2217
+ name,
2218
+ capabilities: options.capabilities ?? ["retrieve", "store", "search"],
2219
+ relevanceRanking: options.relevanceRanking ?? false,
2220
+ ...options.backend !== void 0 ? { backend: options.backend } : {}
2221
+ };
2222
+ }
2223
+
1281
2224
  // 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
2225
  var STOPWORDS = /* @__PURE__ */ new Set([
1285
2226
  "the",
1286
2227
  "and",
@@ -1337,29 +2278,42 @@ function relevanceScore(queryKeywords, entry) {
1337
2278
  }
1338
2279
  return overlap * 2 + tagMatches * 1.5;
1339
2280
  }
1340
- var LocalMemoryProvider = class {
2281
+ var LocalMemoryProvider = class extends JsonFileStore {
1341
2282
  name = "local";
1342
- filePath;
1343
- queue = Promise.resolve();
1344
- constructor(directory = `${process.cwd()}/.omninode`) {
1345
- this.filePath = `${directory}/memory.json`;
1346
- }
1347
- 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);
2283
+ constructor(directory) {
2284
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
2285
+ }
2286
+ metadata() {
2287
+ return defaultMetadata(this.name, {
2288
+ capabilities: ["retrieve", "store", "search", "categories", "tags"],
2289
+ relevanceRanking: true,
2290
+ backend: { kind: "json-file", deterministicScoring: true }
1358
2291
  });
1359
2292
  }
1360
- async write(entry) {
1361
- return this.synchronized(async () => {
1362
- const entries = await this.load();
2293
+ search(text, options = {}) {
2294
+ return defaultSearch(this, text, options);
2295
+ }
2296
+ /** @deprecated v1 alias of {@link retrieve}. */
2297
+ query(query) {
2298
+ return this.retrieve(query);
2299
+ }
2300
+ /** @deprecated v1 alias of {@link store}. */
2301
+ write(entry) {
2302
+ return this.store(entry);
2303
+ }
2304
+ async retrieve(query) {
2305
+ const entries = await this.readAll();
2306
+ const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter((entry) => query.category ? entry.category === query.category : true).filter(
2307
+ (entry) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry.tags ?? []).includes(tag)) : true
2308
+ );
2309
+ if (!query.text || query.text.trim().length === 0) {
2310
+ return filtered.sort((a, b) => (b.updatedAt ?? b.createdAt ?? "").localeCompare(a.updatedAt ?? a.createdAt ?? "")).slice(0, query.limit ?? 10);
2311
+ }
2312
+ const queryKeywords = keywords(query.text);
2313
+ 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);
2314
+ }
2315
+ store(entry) {
2316
+ return this.mutate((entries) => {
1363
2317
  const index = entries.findIndex((e) => e.key === entry.key);
1364
2318
  const stamped = {
1365
2319
  ...entry,
@@ -1368,35 +2322,11 @@ var LocalMemoryProvider = class {
1368
2322
  };
1369
2323
  if (index >= 0) entries[index] = stamped;
1370
2324
  else entries.push(stamped);
1371
- await this.saveAll(entries);
2325
+ return [entries, void 0];
1372
2326
  });
1373
2327
  }
1374
2328
  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);
2329
+ return (await this.readAll()).length;
1400
2330
  }
1401
2331
  };
1402
2332
 
@@ -1413,11 +2343,40 @@ var RememberaMemoryProvider = class {
1413
2343
  this.env = config.env ?? process.env;
1414
2344
  this.log = log.child({ component: "remembera" });
1415
2345
  }
2346
+ get authenticationConfigured() {
2347
+ return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
2348
+ }
1416
2349
  headers() {
1417
2350
  const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
1418
2351
  return key ? { authorization: `Bearer ${key}` } : {};
1419
2352
  }
1420
- async query(query) {
2353
+ /** Search delegates to the gateway's relevance query. */
2354
+ search(text, options = {}) {
2355
+ return defaultSearch(
2356
+ this,
2357
+ text,
2358
+ {
2359
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
2360
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
2361
+ }
2362
+ );
2363
+ }
2364
+ metadata() {
2365
+ return defaultMetadata(this.name, {
2366
+ capabilities: ["retrieve", "store", "search", "categories", "tags"],
2367
+ relevanceRanking: true,
2368
+ backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
2369
+ });
2370
+ }
2371
+ /** @deprecated v1 alias of {@link retrieve}. */
2372
+ query(query) {
2373
+ return this.retrieve(query);
2374
+ }
2375
+ /** @deprecated v1 alias of {@link store}. */
2376
+ write(entry) {
2377
+ return this.store(entry);
2378
+ }
2379
+ async retrieve(query) {
1421
2380
  const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
1422
2381
  if (status !== 200) {
1423
2382
  throw new MemoryError(`Remembera query returned HTTP ${status}.`);
@@ -1428,7 +2387,7 @@ var RememberaMemoryProvider = class {
1428
2387
  }
1429
2388
  return entries.map((entry) => this.toEntry(entry)).filter((entry) => entry !== void 0);
1430
2389
  }
1431
- async write(entry) {
2390
+ async store(entry) {
1432
2391
  const { status } = await this.request(`${this.baseUrl}/api/memory`, entry);
1433
2392
  if (status !== 200 && status !== 201) {
1434
2393
  throw new MemoryError(`Remembera write returned HTTP ${status}.`);
@@ -1463,6 +2422,7 @@ var RememberaMemoryProvider = class {
1463
2422
  content: entry.content,
1464
2423
  scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
1465
2424
  ...Array.isArray(entry.tags) ? { tags: entry.tags.filter((t) => typeof t === "string") } : {},
2425
+ ...typeof entry.category === "string" ? { category: entry.category } : {},
1466
2426
  ...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
1467
2427
  ...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {}
1468
2428
  };
@@ -1493,57 +2453,54 @@ function createMemoryProvider(config) {
1493
2453
  }
1494
2454
 
1495
2455
  // src/memory/service.ts
2456
+ var DEFAULT_ENTRY_LIMIT = 8;
1496
2457
  var MAX_CONTEXT_CHARS = 4e3;
1497
2458
  var MemoryService = class {
1498
- constructor(provider, log = logger) {
2459
+ constructor(provider, log = logger, options = {}) {
1499
2460
  this.provider = provider;
1500
2461
  this.log = log.child({ component: "memory", provider: provider.name });
2462
+ this.required = options.required ?? false;
2463
+ this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
1501
2464
  }
1502
2465
  provider;
1503
2466
  log;
2467
+ required;
2468
+ maxContextChars;
1504
2469
  get providerRef() {
1505
2470
  return this.provider;
1506
2471
  }
1507
2472
  get providerName() {
1508
2473
  return this.provider.name;
1509
2474
  }
1510
- /** Relevant context for a task objective; empty string when nothing matches. */
1511
- async gatherContext(query) {
2475
+ get isRequired() {
2476
+ return this.required;
2477
+ }
2478
+ /** Structured, budgeted context for a task (§12 — avoid loading all memory). */
2479
+ async contextFor(query) {
2480
+ let entries;
1512
2481
  try {
1513
- const entries = await this.provider.query({
2482
+ entries = await this.provider.retrieve({
1514
2483
  text: query.objective,
1515
2484
  ...query.role ? { tags: [query.role] } : {},
1516
- limit: 8
2485
+ limit: query.limit ?? DEFAULT_ENTRY_LIMIT
1517
2486
  });
1518
- if (entries.length === 0) return "";
1519
- const byScope = /* @__PURE__ */ new Map();
1520
- for (const entry of entries) {
1521
- byScope.set(entry.scope, [...byScope.get(entry.scope) ?? [], entry]);
1522
- }
1523
- const parts = [];
1524
- for (const scope of ["project", "task", "role", "knowledge"]) {
1525
- const scopeEntries = byScope.get(scope) ?? [];
1526
- if (scopeEntries.length === 0) continue;
1527
- const lines = scopeEntries.map((entry) => `- ${entry.content}`).join("\n");
1528
- parts.push(`### Relevant memory (${scope})
1529
- ${lines}`);
1530
- }
1531
- const context = parts.join("\n\n");
1532
- return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
1533
2487
  } catch (error) {
1534
- this.log.warn(
1535
- `Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
1536
- );
1537
- return "";
2488
+ return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
1538
2489
  }
2490
+ return this.format(entries);
2491
+ }
2492
+ /** Prompt-ready context text (v1 convenience wrapper). */
2493
+ async gatherContext(query) {
2494
+ return (await this.contextFor(query)).text;
1539
2495
  }
1540
- /** Records a finished task's outcome — best-effort. */
2496
+ /** Records a finished task's outcome — task history plus "known problems" (§12). */
1541
2497
  async recordTaskOutcome(task) {
1542
2498
  const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
1543
2499
  try {
1544
- await this.provider.write({
2500
+ await this.provider.store({
1545
2501
  key: `task:${task.id}`,
1546
2502
  scope: "task",
2503
+ category: "tasks",
1547
2504
  content: `Objective: ${task.objective}
1548
2505
  Status: ${task.status}
1549
2506
  Outcome: ${outcome}`,
@@ -1554,18 +2511,53 @@ Outcome: ${outcome}`,
1554
2511
  (report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
1555
2512
  );
1556
2513
  for (const { report, finding } of significant) {
1557
- await this.provider.write({
2514
+ await this.provider.store({
1558
2515
  key: `finding:${report.id}:${finding.id}`,
1559
2516
  scope: "project",
2517
+ category: "problems",
1560
2518
  content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
1561
2519
  tags: ["known-problem", report.agent]
1562
2520
  });
1563
2521
  }
1564
2522
  } catch (error) {
1565
- this.log.warn(
1566
- `Memory write failed (continuing): ${error instanceof Error ? error.message : String(error)}`
2523
+ this.handleFailure("store", error, void 0);
2524
+ }
2525
+ }
2526
+ format(entries) {
2527
+ if (entries.length === 0) return { text: "", entries, truncated: false };
2528
+ const byScope = /* @__PURE__ */ new Map();
2529
+ for (const entry of entries) {
2530
+ byScope.set(entry.scope, [...byScope.get(entry.scope) ?? [], entry]);
2531
+ }
2532
+ const parts = [];
2533
+ for (const scope of ["project", "task", "role", "knowledge"]) {
2534
+ const scopeEntries = byScope.get(scope) ?? [];
2535
+ if (scopeEntries.length === 0) continue;
2536
+ const lines = scopeEntries.map((entry) => {
2537
+ const category = entry.category ? `[${entry.category}] ` : "";
2538
+ return `- ${category}${entry.content}`;
2539
+ }).join("\n");
2540
+ parts.push(`### Relevant memory (${scope})
2541
+ ${lines}`);
2542
+ }
2543
+ let text = parts.join("\n\n");
2544
+ const truncated = text.length > this.maxContextChars;
2545
+ if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
2546
+ return { text, entries, truncated };
2547
+ }
2548
+ /**
2549
+ * Failure policy (§12): continue unless memory is explicitly required.
2550
+ */
2551
+ handleFailure(operation, error, fallback) {
2552
+ const message = error instanceof Error ? error.message : String(error);
2553
+ if (this.required) {
2554
+ throw new MemoryError(
2555
+ `Memory is marked required, but ${operation} failed: ${message}`,
2556
+ { cause: error }
1567
2557
  );
1568
2558
  }
2559
+ this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
2560
+ return fallback ?? { text: "", entries: [], truncated: false };
1569
2561
  }
1570
2562
  };
1571
2563
 
@@ -1577,7 +2569,11 @@ function createTaskEngine(config, log = logger, directory) {
1577
2569
  agents,
1578
2570
  roles,
1579
2571
  store: new FileTaskStore(directory),
1580
- ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
2572
+ ...config.project.memory !== void 0 ? {
2573
+ memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
2574
+ required: config.project.memory.required
2575
+ })
2576
+ } : {},
1581
2577
  audit: new FileAuditLog(),
1582
2578
  log
1583
2579
  });
@@ -1816,9 +2812,23 @@ var PipelineEngine = class {
1816
2812
  }
1817
2813
  options;
1818
2814
  log;
2815
+ /** Runs executing in THIS process — cancellation can terminate them directly. */
2816
+ activeRuns = /* @__PURE__ */ new Map();
1819
2817
  get store() {
1820
2818
  return this.options.store;
1821
2819
  }
2820
+ /**
2821
+ * Saves the run while preserving fields that may be set by another process
2822
+ * (e.g. a cancellation request) — an in-memory snapshot must never clobber
2823
+ * externally requested cancellation (§9.2).
2824
+ */
2825
+ async persist(run) {
2826
+ const current = await this.options.store.get(run.id);
2827
+ await this.options.store.save({
2828
+ ...run,
2829
+ ...current?.cancellationRequested === true ? { cancellationRequested: true, cancellationRequestedAt: current.cancellationRequestedAt } : {}
2830
+ });
2831
+ }
1822
2832
  /** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
1823
2833
  validate(def) {
1824
2834
  const ids = /* @__PURE__ */ new Set();
@@ -1918,18 +2928,44 @@ var PipelineEngine = class {
1918
2928
  );
1919
2929
  }
1920
2930
  }
2931
+ if (step.model) {
2932
+ const separator = step.model.indexOf(":");
2933
+ if (separator <= 0 || separator === step.model.length - 1) {
2934
+ throw new PipelineError(
2935
+ "PIPELINE_INVALID",
2936
+ `Step "${step.id}" has an invalid model reference "${step.model}" \u2014 use "provider:model-id".`
2937
+ );
2938
+ }
2939
+ const providerName = step.model.slice(0, separator);
2940
+ if (!this.options.providers.some((p) => p.name === providerName)) {
2941
+ throw new PipelineError(
2942
+ "PIPELINE_INVALID",
2943
+ `Step "${step.id}" uses model "${step.model}" but provider "${providerName}" is not configured.`
2944
+ );
2945
+ }
2946
+ }
1921
2947
  }
1922
2948
  const now = (/* @__PURE__ */ new Date()).toISOString();
1923
2949
  const run = {
1924
2950
  id: `run-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
1925
2951
  pipelineId: def.id,
2952
+ attempt: 1,
1926
2953
  objective,
1927
2954
  status: "running",
1928
2955
  startedAt: now,
1929
2956
  stepRuns: []
1930
2957
  };
1931
2958
  await this.options.store.save(run);
1932
- this.log.info(`Running pipeline ${def.id} as ${run.id}.`);
2959
+ const liveTaskIds = /* @__PURE__ */ new Set();
2960
+ this.activeRuns.set(run.id, {
2961
+ taskIds: liveTaskIds,
2962
+ cancelTasks: async () => {
2963
+ for (const taskId of liveTaskIds) {
2964
+ await this.options.tasks.cancel(taskId).catch(() => void 0);
2965
+ }
2966
+ }
2967
+ });
2968
+ this.log.info(`Running pipeline ${def.id} as ${run.id} (attempt ${run.attempt}).`);
1933
2969
  await this.options.audit?.record({
1934
2970
  at: (/* @__PURE__ */ new Date()).toISOString(),
1935
2971
  action: "pipeline.run.started",
@@ -1941,7 +2977,22 @@ var PipelineEngine = class {
1941
2977
  const pending = new Set(def.steps.map((s) => s.id));
1942
2978
  const contextParts = [];
1943
2979
  const allReports = [];
2980
+ let cancelled = false;
1944
2981
  while (pending.size > 0) {
2982
+ const persisted = await this.options.store.get(run.id);
2983
+ if (persisted?.cancellationRequested === true) {
2984
+ cancelled = true;
2985
+ run.cancellationRequested = true;
2986
+ run.cancellationRequestedAt = persisted.cancellationRequestedAt;
2987
+ await this.activeRuns.get(run.id)?.cancelTasks();
2988
+ for (const step of def.steps.filter((s) => pending.has(s.id))) {
2989
+ const stepRun = { stepId: step.id, status: "cancelled" };
2990
+ run.stepRuns.push(stepRun);
2991
+ options.onStep?.(stepRun);
2992
+ }
2993
+ pending.clear();
2994
+ break;
2995
+ }
1945
2996
  const ready = def.steps.filter(
1946
2997
  (step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
1947
2998
  );
@@ -1958,7 +3009,8 @@ var PipelineEngine = class {
1958
3009
  ...role ? { role } : {},
1959
3010
  combinedContext: contextParts.join("\n\n"),
1960
3011
  reports: [...allReports],
1961
- pipelineRunId: run.id
3012
+ pipelineRunId: run.id,
3013
+ runId: run.id
1962
3014
  };
1963
3015
  return this.executeStep(step, ctx);
1964
3016
  })
@@ -1975,7 +3027,9 @@ ${contribution}`);
1975
3027
  }
1976
3028
  const stepRun = {
1977
3029
  stepId: step.id,
1978
- status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : "completed",
3030
+ attempt: outcome.attempt ?? 1,
3031
+ ...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
3032
+ status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
1979
3033
  ...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
1980
3034
  ...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
1981
3035
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
@@ -1984,10 +3038,12 @@ ${contribution}`);
1984
3038
  options.onStep?.(stepRun);
1985
3039
  });
1986
3040
  run.reports = [...allReports];
1987
- await this.options.store.save(run);
3041
+ await this.persist(run);
1988
3042
  }
1989
- const failed = [...outcomes.values()].some((o) => o.status === "failed");
1990
- const finalStatus = failed ? "failed" : "completed";
3043
+ const allOutcomes = [...outcomes.values()];
3044
+ const failed = !cancelled && allOutcomes.some((o) => o.status === "failed");
3045
+ const partial = !cancelled && !failed && allOutcomes.some((o) => o.status === "partial");
3046
+ const finalStatus = cancelled ? "cancelled" : failed ? "failed" : partial ? "partial" : "completed";
1991
3047
  const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
1992
3048
  let resultSummary;
1993
3049
  for (let i = def.steps.length - 1; i >= 0; i -= 1) {
@@ -2015,16 +3071,19 @@ ${contribution}`);
2015
3071
  run.reports = collected;
2016
3072
  }
2017
3073
  }
3074
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
3075
+ this.activeRuns.delete(run.id);
2018
3076
  const finished = {
2019
3077
  ...run,
2020
3078
  status: finalStatus,
2021
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
3079
+ finishedAt,
3080
+ attempts: [...run.attempts ?? [], { attempt: run.attempt, startedAt: run.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
2022
3081
  ...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
2023
3082
  ...combinedReportId !== void 0 ? { combinedReportId } : {},
2024
3083
  ...planId !== void 0 ? { planId } : {},
2025
3084
  ...resultSummary !== void 0 ? { resultSummary } : {}
2026
3085
  };
2027
- await this.options.store.save(finished);
3086
+ await this.persist(finished);
2028
3087
  this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
2029
3088
  await this.options.audit?.record({
2030
3089
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2034,6 +3093,23 @@ ${contribution}`);
2034
3093
  });
2035
3094
  return finished;
2036
3095
  }
3096
+ /** First-class cancellation (§9.2): requests cancellation and terminates
3097
+ * tasks that are running in this process immediately. */
3098
+ async cancel(runId) {
3099
+ const run = await this.options.store.get(runId);
3100
+ if (!run) {
3101
+ throw new PipelineError("PIPELINE_NOT_FOUND", `Pipeline run "${runId}" does not exist.`);
3102
+ }
3103
+ await this.options.store.save({
3104
+ ...run,
3105
+ cancellationRequested: true,
3106
+ cancellationRequestedAt: (/* @__PURE__ */ new Date()).toISOString()
3107
+ });
3108
+ const active = this.activeRuns.get(runId);
3109
+ if (active) {
3110
+ await active.cancelTasks();
3111
+ }
3112
+ }
2037
3113
  async get(runId) {
2038
3114
  return this.options.store.get(runId);
2039
3115
  }
@@ -2057,9 +3133,16 @@ ${contribution}`);
2057
3133
  this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
2058
3134
  }
2059
3135
  last = await this.executeStepOnce(step, ctx);
2060
- if (last.status !== "failed") return last;
3136
+ if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
2061
3137
  }
2062
- return last;
3138
+ return { ...last, attempt: retries + 1 };
3139
+ }
3140
+ /** Registers tasks created for a step as live, so cancellation can terminate them (§9.2). */
3141
+ trackLiveTasks(runId, taskIds) {
3142
+ if (!runId) return;
3143
+ const active = this.activeRuns.get(runId);
3144
+ if (!active) return;
3145
+ for (const id of taskIds) active.taskIds.add(id);
2063
3146
  }
2064
3147
  async executeStepOnce(step, ctx) {
2065
3148
  switch (step.kind) {
@@ -2092,7 +3175,10 @@ ${contribution}`);
2092
3175
  })
2093
3176
  )
2094
3177
  );
3178
+ this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
2095
3179
  const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
3180
+ const executionIds = results.map((r) => r.executions?.at(-1)?.executionId).filter((id) => id !== void 0);
3181
+ const completedTasks = results.filter((r) => r.status === "completed");
2096
3182
  const failedTasks = results.filter((r) => r.status !== "completed");
2097
3183
  const lines = results.map((result, index) => {
2098
3184
  const agentName = agentNames[index] ?? "unknown";
@@ -2102,16 +3188,25 @@ ${contribution}`);
2102
3188
  const reports = results.flatMap((r) => r.result?.reports ?? []);
2103
3189
  const summary = lines.join("\n");
2104
3190
  if (failedTasks.length > 0) {
2105
- const firstError = failedTasks[0]?.result?.error ?? "unknown error";
3191
+ const firstError = failedTasks[0]?.result?.error ?? failedTasks[0]?.lastError ?? "unknown outcome";
3192
+ const states = failedTasks.map((r) => r.status).join(", ");
2106
3193
  return {
2107
- status: "failed",
3194
+ // Some agents succeeded → the step produced usable but incomplete
3195
+ // intelligence: partial, not failed (roadmap §9.3).
3196
+ status: completedTasks.length > 0 ? "partial" : "failed",
2108
3197
  summary,
2109
3198
  taskIds: created.map((t) => t.id),
2110
- error: `${failedTasks.length}/${results.length} task(s) failed \u2014 ${firstError}`,
3199
+ error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
2111
3200
  reports
2112
3201
  };
2113
3202
  }
2114
- return { status: "completed", summary, taskIds: created.map((t) => t.id), reports };
3203
+ return {
3204
+ status: "completed",
3205
+ summary,
3206
+ taskIds: created.map((t) => t.id),
3207
+ ...executionIds.length > 0 ? { executionIds } : {},
3208
+ reports
3209
+ };
2115
3210
  }
2116
3211
  async runSingle(step, ctx, agentName) {
2117
3212
  const task = await this.options.tasks.create({
@@ -2120,10 +3215,13 @@ ${contribution}`);
2120
3215
  ...ctx.role ? { role: ctx.role.id } : {},
2121
3216
  ...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
2122
3217
  });
3218
+ this.trackLiveTasks(ctx.runId, [task.id]);
2123
3219
  const result = await this.options.tasks.run(task.id);
2124
3220
  if (result.status !== "completed") {
2125
3221
  return {
2126
- status: "failed",
3222
+ // Unknown/timed_out outcomes are not failures — the work may have
3223
+ // happened remotely; the step is incomplete, not broken.
3224
+ status: result.status === "unknown" || result.status === "timed_out" ? "partial" : "failed",
2127
3225
  summary: result.result?.summary ?? "",
2128
3226
  taskIds: [task.id],
2129
3227
  error: result.result?.error ?? `task ${task.id} did not complete`,
@@ -2239,114 +3337,44 @@ function createDefaultChatFn(providers) {
2239
3337
  }
2240
3338
 
2241
3339
  // 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`;
3340
+ var FilePipelineRunStore = class extends JsonFileStore {
3341
+ constructor(directory) {
3342
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "pipelines.json" });
2249
3343
  }
2250
- async save(run) {
2251
- return this.synchronized(async () => {
2252
- const runs = await this.load();
3344
+ save(run) {
3345
+ return this.mutate((runs) => {
2253
3346
  const index = runs.findIndex((r) => r.id === run.id);
2254
3347
  if (index >= 0) runs[index] = run;
2255
3348
  else runs.push(run);
2256
- await this.write(runs);
3349
+ return [runs, void 0];
2257
3350
  });
2258
3351
  }
2259
3352
  async get(id) {
2260
- return this.synchronized(async () => {
2261
- const runs = await this.load();
2262
- return runs.find((r) => r.id === id);
2263
- });
3353
+ return (await this.readAll()).find((r) => r.id === id);
2264
3354
  }
2265
3355
  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);
3356
+ 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
3357
  }
2295
3358
  };
2296
3359
 
2297
3360
  // 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`;
3361
+ var FilePlanStore = class extends JsonFileStore {
3362
+ constructor(directory) {
3363
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "plans.json" });
2305
3364
  }
2306
- async save(plan) {
2307
- return this.synchronized(async () => {
2308
- const plans = await this.load();
3365
+ save(plan) {
3366
+ return this.mutate((plans) => {
2309
3367
  const index = plans.findIndex((p) => p.id === plan.id);
2310
3368
  if (index >= 0) plans[index] = plan;
2311
3369
  else plans.push(plan);
2312
- await this.write(plans);
3370
+ return [plans, void 0];
2313
3371
  });
2314
3372
  }
2315
3373
  async get(id) {
2316
- return this.synchronized(async () => {
2317
- const plans = await this.load();
2318
- return plans.find((p) => p.id === id);
2319
- });
3374
+ return (await this.readAll()).find((p) => p.id === id);
2320
3375
  }
2321
3376
  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);
3377
+ 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
3378
  }
2351
3379
  };
2352
3380
 
@@ -2373,6 +3401,94 @@ function buildPlanner(plannerConfig, chat, log = logger) {
2373
3401
 
2374
3402
  // src/reports/aggregate.ts
2375
3403
  import { randomBytes as randomBytes5 } from "crypto";
3404
+
3405
+ // src/reports/schema.ts
3406
+ import { z as z3 } from "zod";
3407
+ var evidenceSchema = z3.union([
3408
+ z3.string().transform((excerpt) => ({ kind: "observation", excerpt })),
3409
+ z3.object({
3410
+ kind: z3.enum(["file", "line", "url", "command", "observation", "artifact"]),
3411
+ ref: z3.string().optional(),
3412
+ line: z3.number().int().optional(),
3413
+ excerpt: z3.string().optional(),
3414
+ agent: z3.string().optional()
3415
+ })
3416
+ ]);
3417
+ var findingSchema = z3.object({
3418
+ id: z3.string().min(1),
3419
+ title: z3.string().min(1),
3420
+ detail: z3.string().default(""),
3421
+ description: z3.string().optional(),
3422
+ severity: z3.enum(["info", "low", "medium", "high", "critical"]).optional(),
3423
+ confidence: z3.enum(["low", "medium", "high"]).optional(),
3424
+ severityBySource: z3.record(z3.enum(["info", "low", "medium", "high", "critical"])).optional(),
3425
+ confidenceBySource: z3.record(z3.enum(["low", "medium", "high"])).optional(),
3426
+ recommendations: z3.array(z3.string()).optional(),
3427
+ evidence: z3.array(evidenceSchema).optional(),
3428
+ location: z3.string().optional(),
3429
+ tags: z3.array(z3.string()).optional(),
3430
+ source: z3.object({ agent: z3.string().optional(), reportId: z3.string().optional(), pipelineRunId: z3.string().optional() }).optional(),
3431
+ recommendation: z3.string().optional()
3432
+ });
3433
+ var artifactSchema = z3.object({
3434
+ kind: z3.enum(["file", "patch", "log", "screenshot", "document", "data"]),
3435
+ ref: z3.string().min(1),
3436
+ description: z3.string().optional(),
3437
+ mediaType: z3.string().optional(),
3438
+ bytes: z3.number().int().optional()
3439
+ });
3440
+ var reportSchema = z3.object({
3441
+ id: z3.string().min(1),
3442
+ taskId: z3.string().min(1),
3443
+ agent: z3.string().min(1),
3444
+ agentId: z3.string().optional(),
3445
+ pipelineRunId: z3.string().optional(),
3446
+ model: z3.string().optional(),
3447
+ summary: z3.string(),
3448
+ findings: z3.array(findingSchema),
3449
+ recommendations: z3.array(z3.string()),
3450
+ confidence: z3.enum(["low", "medium", "high"]).optional(),
3451
+ evidence: z3.array(evidenceSchema).optional(),
3452
+ artifacts: z3.array(artifactSchema).optional(),
3453
+ createdAt: z3.string().min(1),
3454
+ metadata: z3.record(z3.unknown()).optional()
3455
+ });
3456
+ function validateReport(value) {
3457
+ const parsed = reportSchema.safeParse(value);
3458
+ if (!parsed.success) {
3459
+ const issues = parsed.error.issues.map((issue) => ({
3460
+ path: issue.path.join(".") || "(root)",
3461
+ message: issue.message
3462
+ }));
3463
+ return {
3464
+ valid: false,
3465
+ issues,
3466
+ reason: `report failed schema validation (${issues.length} issue(s))`
3467
+ };
3468
+ }
3469
+ const report = parsed.data;
3470
+ report.findings = report.findings.map((finding) => ({
3471
+ ...finding,
3472
+ description: finding.description ?? finding.detail,
3473
+ ...finding.source === void 0 ? { source: { agent: report.agent, reportId: report.id, ...report.pipelineRunId !== void 0 ? { pipelineRunId: report.pipelineRunId } : {} } } : {}
3474
+ }));
3475
+ return { valid: true, report };
3476
+ }
3477
+ function normalizeEvidence(input) {
3478
+ if (input === void 0) return void 0;
3479
+ return input.map(
3480
+ (item) => typeof item === "string" ? { kind: "observation", excerpt: item } : item
3481
+ );
3482
+ }
3483
+ function normalizeFinding(finding) {
3484
+ return {
3485
+ ...finding,
3486
+ description: finding.description ?? finding.detail,
3487
+ evidence: normalizeEvidence(finding.evidence)
3488
+ };
3489
+ }
3490
+
3491
+ // src/reports/aggregate.ts
2376
3492
  var SEVERITY_RANK2 = {
2377
3493
  info: 0,
2378
3494
  low: 1,
@@ -2394,7 +3510,12 @@ function aggregateReports(reports, options = {}) {
2394
3510
  }
2395
3511
  const sources = [...new Set(reports.map((r) => r.agent))].sort();
2396
3512
  const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
2397
- const summary = `${reports.length} report(s) from ${sources.length > 0 ? sources.join(", ") : "no agents"}: ${findingGroups.length} unique finding group(s) (${totalRawFindings} raw), ${recommendationGroups.length} recommendation group(s).`;
3513
+ const conflicts = detectConflicts(findingGroups);
3514
+ const agreements = findingGroups.filter(
3515
+ (group) => group.sources.length > 1 && group.severity !== void 0 && new Set(Object.values(group.severityBySource ?? {})).size === 1
3516
+ );
3517
+ const disputed = conflicts.length;
3518
+ const summary = `${reports.length} report(s) from ${sources.length > 0 ? sources.join(", ") : "no agents"}: ${findingGroups.length} unique finding group(s) (${totalRawFindings} raw), ${agreements.length} agreement(s), ${disputed} conflict(s), ${recommendationGroups.length} recommendation group(s).`;
2398
3519
  return {
2399
3520
  id: `combined-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
2400
3521
  title: options.objective !== void 0 ? truncate2(options.objective, 120) : "Combined report",
@@ -2403,10 +3524,57 @@ function aggregateReports(reports, options = {}) {
2403
3524
  taskIds: options.taskIds ?? [],
2404
3525
  summary,
2405
3526
  findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
3527
+ agreements,
3528
+ conflicts,
2406
3529
  recommendations: recommendationGroups,
2407
3530
  sources,
2408
3531
  reportIds: reports.map((r) => r.id),
2409
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
3532
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3533
+ metadata: {
3534
+ agents: sources.length,
3535
+ reports: reports.length,
3536
+ rawFindings: totalRawFindings,
3537
+ findingGroups: findingGroups.length,
3538
+ agreements: agreements.length,
3539
+ conflicts: conflicts.length
3540
+ }
3541
+ };
3542
+ }
3543
+ function rankConfidence(confidence) {
3544
+ return confidence ? { low: 1, medium: 2, high: 3 }[confidence] : 0;
3545
+ }
3546
+ function detectConflicts(groups) {
3547
+ const conflicts = [];
3548
+ for (const group of groups) {
3549
+ if (group.sources.length < 2) continue;
3550
+ const severities = new Set(Object.values(group.severityBySource ?? {}));
3551
+ if (severities.size > 1) {
3552
+ conflicts.push({
3553
+ type: "severity",
3554
+ findingTitle: group.title,
3555
+ positions: group.sources.map((source) => position(source, group)),
3556
+ note: `Agents disagree on severity (${[...severities].sort((a, b) => SEVERITY_RANK2[a] - SEVERITY_RANK2[b]).join(", ")}); the highest severity is shown, every position is preserved.`
3557
+ });
3558
+ }
3559
+ const recommendations = (group.recommendations ?? []).filter((r) => r.length > 0);
3560
+ if (recommendations.length > 1) {
3561
+ conflicts.push({
3562
+ type: "recommendation",
3563
+ findingTitle: group.title,
3564
+ positions: group.sources.map((source) => position(source, group)),
3565
+ note: `Agents proposed ${recommendations.length} different fixes; none was discarded.`
3566
+ });
3567
+ }
3568
+ }
3569
+ return conflicts;
3570
+ }
3571
+ function position(source, group) {
3572
+ return {
3573
+ agent: source.agent,
3574
+ reportId: source.reportId,
3575
+ ...group.severityBySource?.[source.agent] !== void 0 ? { severity: group.severityBySource[source.agent] } : {},
3576
+ ...group.confidenceBySource?.[source.agent] !== void 0 ? { confidence: group.confidenceBySource[source.agent] } : {},
3577
+ ...group.recommendations?.[0] !== void 0 ? { recommendation: group.recommendations[0] } : {}
2410
3578
  };
2411
3579
  }
2412
3580
  function rank(finding) {
@@ -2419,10 +3587,22 @@ function mergeFinding(groups, finding, report, threshold) {
2419
3587
  group.occurrences += 1;
2420
3588
  group.details.push(finding.detail);
2421
3589
  group.sources.push(source);
2422
- group.evidence.push(...finding.evidence ?? []);
3590
+ group.evidence.push(...normalizeEvidence(finding.evidence) ?? []);
2423
3591
  if (finding.severity && (!group.severity || SEVERITY_RANK2[finding.severity] > SEVERITY_RANK2[group.severity])) {
2424
3592
  group.severity = finding.severity;
2425
3593
  }
3594
+ if (finding.confidence && rankConfidence(finding.confidence) > rankConfidence(group.confidence)) {
3595
+ group.confidence = finding.confidence;
3596
+ }
3597
+ if (finding.severity) {
3598
+ group.severityBySource = { ...group.severityBySource, [report.agent]: finding.severity };
3599
+ }
3600
+ if (finding.confidence) {
3601
+ group.confidenceBySource = { ...group.confidenceBySource, [report.agent]: finding.confidence };
3602
+ }
3603
+ if (finding.recommendation && !group.recommendations?.includes(finding.recommendation)) {
3604
+ group.recommendations = [...group.recommendations ?? [], finding.recommendation];
3605
+ }
2426
3606
  return;
2427
3607
  }
2428
3608
  groups.push({
@@ -2431,7 +3611,11 @@ function mergeFinding(groups, finding, report, threshold) {
2431
3611
  sources: [source],
2432
3612
  occurrences: 1,
2433
3613
  ...finding.severity ? { severity: finding.severity } : {},
2434
- evidence: [...finding.evidence ?? []]
3614
+ ...finding.confidence ? { confidence: finding.confidence } : {},
3615
+ ...finding.severity ? { severityBySource: { [report.agent]: finding.severity } } : {},
3616
+ ...finding.confidence ? { confidenceBySource: { [report.agent]: finding.confidence } } : {},
3617
+ ...finding.recommendation ? { recommendations: [finding.recommendation] } : {},
3618
+ evidence: [...normalizeEvidence(finding.evidence) ?? []]
2435
3619
  });
2436
3620
  }
2437
3621
  function mergeRecommendation(groups, text, report, threshold) {
@@ -2513,6 +3697,7 @@ function extractReportFromText(raw, meta) {
2513
3697
  id: `${meta.taskId}-f-${index + 1}`,
2514
3698
  title: truncate3(text, 120),
2515
3699
  detail: text,
3700
+ description: text,
2516
3701
  severity: guessSeverity(text)
2517
3702
  }));
2518
3703
  return {
@@ -2523,7 +3708,8 @@ function extractReportFromText(raw, meta) {
2523
3708
  summary,
2524
3709
  findings,
2525
3710
  recommendations: recommendationTexts.map((t) => truncate3(t, 200)),
2526
- metadata: evidenceTexts.length > 0 ? { evidence: evidenceTexts } : void 0,
3711
+ ...evidenceTexts.length > 0 ? { evidence: evidenceTexts.map((excerpt) => ({ kind: "observation", excerpt })) } : {},
3712
+ metadata: evidenceTexts.length > 0 ? { evidenceCount: evidenceTexts.length } : void 0,
2527
3713
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2528
3714
  };
2529
3715
  }
@@ -2539,27 +3725,46 @@ var ReportService = class {
2539
3725
  }
2540
3726
  store;
2541
3727
  log;
3728
+ /** Rejected reports from the last collection (§13 — malformed never enters aggregation). */
3729
+ lastDiagnostics = { collected: 0, accepted: 0, rejected: [] };
2542
3730
  get storeRef() {
2543
3731
  return this.store;
2544
3732
  }
2545
- /** Normalizes finished tasks into reports (§17 — normalizer). */
3733
+ /** Normalizes finished tasks into reports (§17 — normalizer), validating each
3734
+ * one so malformed reports never reach aggregation (§13). */
2546
3735
  async collectFromTasks(tasks) {
2547
- const reports = [];
3736
+ const candidates = [];
2548
3737
  for (const task of tasks) {
2549
3738
  const structured = task.result?.reports ?? [];
2550
3739
  if (structured.length > 0) {
2551
- reports.push(...structured);
3740
+ candidates.push(...structured);
2552
3741
  continue;
2553
3742
  }
2554
3743
  const text = task.result?.rawOutput ?? task.result?.summary ?? "";
2555
3744
  if (text.trim().length === 0) continue;
2556
- reports.push(
3745
+ candidates.push(
2557
3746
  extractReportFromText(text, {
2558
3747
  taskId: task.id,
2559
3748
  agent: task.agent ?? "unknown"
2560
3749
  })
2561
3750
  );
2562
3751
  }
3752
+ const reports = [];
3753
+ const rejected = [];
3754
+ for (const candidate of candidates) {
3755
+ const result = validateReport(candidate);
3756
+ if (result.valid) {
3757
+ reports.push(result.report);
3758
+ } else {
3759
+ rejected.push({
3760
+ key: candidate?.id ?? "(unknown)",
3761
+ reason: result.reason,
3762
+ issues: result.issues.map((issue) => `${issue.path}: ${issue.message}`)
3763
+ });
3764
+ this.log.warn(`Rejected malformed report: ${result.reason}`);
3765
+ }
3766
+ }
3767
+ this.lastDiagnostics = { collected: candidates.length, accepted: reports.length, rejected };
2563
3768
  return reports;
2564
3769
  }
2565
3770
  async saveReports(reports) {
@@ -2582,84 +3787,39 @@ var ReportService = class {
2582
3787
  };
2583
3788
 
2584
3789
  // 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`;
3790
+ var FileReportStore = class extends JsonFileStore {
3791
+ constructor(directory) {
3792
+ super({ ...directory !== void 0 ? { directory } : {}, fileName: "reports.json" });
2592
3793
  }
2593
3794
  async saveReports(reports) {
2594
- return this.synchronized(async () => {
2595
- const entries = await this.load();
3795
+ await this.mutate((entries) => {
2596
3796
  for (const report of reports) {
2597
- const index = entries.findIndex(
2598
- (e) => e.kind === "report" && e.report.id === report.id
2599
- );
3797
+ const index = entries.findIndex((e) => e.kind === "report" && e.report.id === report.id);
2600
3798
  const entry = { kind: "report", report };
2601
3799
  if (index >= 0) entries[index] = entry;
2602
3800
  else entries.push(entry);
2603
3801
  }
2604
- await this.write(entries);
3802
+ return [entries, void 0];
2605
3803
  });
2606
3804
  }
2607
3805
  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
- });
3806
+ 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
3807
  }
2613
3808
  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
- );
3809
+ await this.mutate((entries) => {
3810
+ const index = entries.findIndex((e) => e.kind === "combined" && e.combined.id === combined.id);
2619
3811
  const entry = { kind: "combined", combined };
2620
3812
  if (index >= 0) entries[index] = entry;
2621
3813
  else entries.push(entry);
2622
- await this.write(entries);
3814
+ return [entries, void 0];
2623
3815
  });
2624
3816
  }
2625
3817
  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
- });
3818
+ const found = (await this.readAll()).find((e) => e.kind === "combined" && e.combined.id === id);
3819
+ return found?.kind === "combined" ? found.combined : void 0;
2631
3820
  }
2632
3821
  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);
3822
+ 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
3823
  }
2664
3824
  };
2665
3825
 
@@ -2672,7 +3832,11 @@ function buildPipelineEngine(config, log = logger) {
2672
3832
  agents,
2673
3833
  roles,
2674
3834
  store: new FileTaskStore(),
2675
- ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
3835
+ ...config.project.memory !== void 0 ? {
3836
+ memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
3837
+ required: config.project.memory.required
3838
+ })
3839
+ } : {},
2676
3840
  audit: new FileAuditLog(),
2677
3841
  log
2678
3842
  });
@@ -2691,36 +3855,69 @@ function buildPipelineEngine(config, log = logger) {
2691
3855
  });
2692
3856
  }
2693
3857
 
3858
+ // src/persistence/index.ts
3859
+ var ProjectStores = class {
3860
+ tasks;
3861
+ pipelineRuns;
3862
+ reports;
3863
+ plans;
3864
+ memory;
3865
+ audit;
3866
+ constructor(directory = `${process.cwd()}/.omninode`) {
3867
+ this.tasks = new FileTaskStore(directory);
3868
+ this.pipelineRuns = new FilePipelineRunStore(directory);
3869
+ this.reports = new FileReportStore(directory);
3870
+ this.plans = new FilePlanStore(directory);
3871
+ this.memory = new LocalMemoryProvider(directory);
3872
+ this.audit = new FileAuditLog(directory);
3873
+ }
3874
+ };
3875
+
2694
3876
  // src/version.ts
2695
- var OMNINODE_VERSION = "1.0.0";
3877
+ var OMNINODE_VERSION = "2.0.0-alpha.10";
2696
3878
  export {
3879
+ AgentAdapterRegistry,
2697
3880
  AgentError,
2698
3881
  AgentRegistry,
2699
3882
  ConfigError,
2700
3883
  ConsoleLogSink,
3884
+ DEFAULT_MAX_OUTPUT_BYTES,
2701
3885
  FileAuditLog,
2702
3886
  FilePipelineRunStore,
3887
+ FilePlanStore,
2703
3888
  FileReportStore,
2704
3889
  FileTaskStore,
3890
+ HeuristicPlanner,
3891
+ JsonFileStore,
2705
3892
  LocalMemoryProvider,
2706
3893
  Logger,
3894
+ MAX_CONTEXT_CHARS,
3895
+ MAX_MESSAGE_BYTES,
2707
3896
  MemoryError,
2708
3897
  MemoryService,
3898
+ ModelPlanner,
2709
3899
  ModelRegistry,
2710
3900
  OMNINODE_VERSION,
2711
3901
  OPENAI_COMPATIBLE_TYPES,
2712
3902
  OmniHilbrasProvider,
2713
3903
  OmniNodeError,
2714
3904
  OpenAICompatibleProvider,
3905
+ PROTOCOL_V2,
2715
3906
  PipelineEngine,
2716
3907
  PipelineError,
2717
3908
  PlannerError,
2718
3909
  ProcessAgent,
3910
+ ProcessAgentAdapter,
3911
+ ProjectStores,
3912
+ ProtocolDecoder,
2719
3913
  ProtocolError,
3914
+ ProtocolSession,
2720
3915
  ProviderError,
2721
3916
  RememberaMemoryProvider,
2722
3917
  ReportService,
2723
3918
  RoleRegistry,
3919
+ SUPPORTED_PROTOCOL_VERSIONS,
3920
+ TERMINAL_TASK_STATUSES,
2724
3921
  TaskEngine,
2725
3922
  TaskError,
2726
3923
  agentConfigSchema,
@@ -2728,7 +3925,12 @@ export {
2728
3925
  appConfigSchema,
2729
3926
  authHeaders,
2730
3927
  buildAgentRegistry,
3928
+ buildChildEnv,
3929
+ buildHello,
3930
+ buildHelloAck,
2731
3931
  buildPipelineEngine,
3932
+ buildPlanner,
3933
+ buildPlannerContext,
2732
3934
  buildRoleRegistry,
2733
3935
  composeTaskText,
2734
3936
  createAgent,
@@ -2736,23 +3938,52 @@ export {
2736
3938
  createMemoryProvider,
2737
3939
  createProvider,
2738
3940
  createTaskEngine,
3941
+ decodeMessage,
3942
+ decodeStream,
3943
+ defaultMetadata,
2739
3944
  defaultProjectConfigYaml,
3945
+ defaultSearch,
3946
+ describeExit,
3947
+ detectConflicts,
3948
+ encodeMessage,
3949
+ envelope,
2740
3950
  expandEnvRefs,
3951
+ extractMessage,
2741
3952
  extractReportFromText,
2742
3953
  findConfigFile,
3954
+ isNetworkError,
2743
3955
  isOmniNodeError,
3956
+ isRetryable,
3957
+ isSupportedProtocol,
3958
+ isTimeoutError,
2744
3959
  keywords,
3960
+ killTree,
3961
+ kindFromStatus,
2745
3962
  loadConfig,
2746
3963
  logger,
3964
+ newMessageId,
3965
+ normalizeEvidence,
3966
+ normalizeFinding,
3967
+ parseHello,
3968
+ parsePlanJson,
3969
+ parseRetryAfter,
2747
3970
  pipelineConfigSchema,
2748
3971
  pipelineStepConfigSchema,
2749
3972
  plannerConfigSchema,
2750
3973
  projectConfigSchema,
2751
3974
  providerConfigSchema,
3975
+ providerHttpError,
3976
+ providerTransportError,
2752
3977
  registryKey,
2753
3978
  requestJson,
3979
+ requireCompletion,
2754
3980
  resolveApiKey,
3981
+ resolveEnvPolicy,
2755
3982
  roleConfigSchema,
2756
- similar
3983
+ sanitizeChunk,
3984
+ similar,
3985
+ validateEnvelope,
3986
+ validateReport,
3987
+ validateWorkingDirectory
2757
3988
  };
2758
3989
  //# sourceMappingURL=index.js.map