@hilbras/omninode 2.0.0-alpha.6 → 2.0.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,9 +18,9 @@ existing intelligence; it does not try to become another model.
18
18
 
19
19
  ## Status
20
20
 
21
- **v2.0.0-alpha.6 — v2 Phase 6: Provider Infrastructure v2**
21
+ **v2.0.0-alpha.8 — v2 Phase 8: Remembera Integration v2**
22
22
  (the reliability & interoperability line: architecture → execution →
23
- protocol → adapters → pipelines → providers) (the v2
23
+ protocol → adapters → pipelines → providers → OmniHilbras → memory) (the v2
24
24
  roadmap is [docs/ROADMAP_V2.md](docs/ROADMAP_V2.md); the v1 plan is complete:
25
25
  the
26
26
  full loop from §28 runs in one command —
@@ -31,6 +31,18 @@ for the plan and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design).
31
31
 
32
32
  ### What's new in v2 (so far)
33
33
 
34
+ - **Phase 8 — Remembera / Memory v2**: the memory contract is formalized
35
+ (`retrieve` / `store` / `search` / `metadata` with v1 `query` / `write`
36
+ aliases kept working), memory gains structured **categories** (decisions,
37
+ problems, conventions, tasks…), retrieval is **task-oriented and budgeted**
38
+ (never "load all memory"), and `memory.required: true` makes backend
39
+ failures fatal instead of tolerated. Details: [MEMORY.md](docs/MEMORY.md).
40
+ - **Phase 7 — OmniHilbras Integration v2**: the dedicated adapter now
41
+ captures gateway-level metadata (version, tier, region) and attaches it
42
+ to discovered models, honors provider-level `timeout_ms`, and supports
43
+ streaming completions (`stream()` over SSE) — while a guard test proves
44
+ the core references OmniHilbras *only* through the provider layer, and a
45
+ full pipeline runs with no OmniHilbras configured at all.
34
46
  - **Phase 6 — Provider Infrastructure v2**: standardized provider contract
35
47
  (`providerId`, secret-free `authentication`, declared `capabilities`,
36
48
  `connect()` / `getModel()` discovery), richer model metadata (context
@@ -327,7 +339,7 @@ required for the core engine.
327
339
  - [Architecture](docs/ARCHITECTURE.md) — module map, workflow, local state
328
340
  - [Security model](docs/SECURITY.md) — secrets, process execution, env isolation, audit log
329
341
  - [API stability policy](docs/API_STABILITY.md) and [public API inventory](docs/API.md)
330
- - [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
342
+ - [Agent Protocol v2 spec](docs/PROTOCOL.md) · [Providers](docs/PROVIDERS.md) · [Memory](docs/MEMORY.md) · [Deprecations](docs/DEPRECATIONS.md) · [Changelog](CHANGELOG.md) · [v2 roadmap](docs/ROADMAP_V2.md)
331
343
  - [Example project](examples/demo/) — a runnable end-to-end demo (`./demo.sh`)
332
344
 
333
345
  Prefer a container? `docker build -t omninode .` then
package/dist/cli/index.js CHANGED
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/version.ts
9
- var OMNINODE_VERSION = "2.0.0-alpha.6";
9
+ var OMNINODE_VERSION = "2.0.0-alpha.8";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -68,6 +68,7 @@ var providerConfigSchema = z.object({
68
68
  type: z.enum(["openai-compatible", "omnihilbras", "openrouter", "local", "custom"]),
69
69
  base_url: z.string().url(),
70
70
  api_key_env_var: z.string().min(1).optional(),
71
+ timeout_ms: z.number().int().positive().optional(),
71
72
  headers: z.record(z.string()).optional(),
72
73
  enabled: z.boolean().optional(),
73
74
  metadata: z.record(z.unknown()).optional()
@@ -132,7 +133,9 @@ var projectConfigSchema = z.object({
132
133
  memory: z.object({
133
134
  provider: z.string().min(1).default("local"),
134
135
  base_url: z.string().url().optional(),
135
- api_key_env_var: z.string().min(1).optional()
136
+ api_key_env_var: z.string().min(1).optional(),
137
+ /** When true, memory failures fail tasks instead of being tolerated (§12). */
138
+ required: z.boolean().optional()
136
139
  }).strict().optional()
137
140
  }).strict();
138
141
  var appConfigSchema = z.object({
@@ -214,7 +217,8 @@ function toAppConfig(yaml) {
214
217
  memory: {
215
218
  provider: yaml.project.memory.provider,
216
219
  baseUrl: yaml.project.memory.base_url,
217
- apiKeyEnvVar: yaml.project.memory.api_key_env_var
220
+ apiKeyEnvVar: yaml.project.memory.api_key_env_var,
221
+ required: yaml.project.memory.required
218
222
  }
219
223
  } : {}
220
224
  },
@@ -245,6 +249,7 @@ function toProviderConfig(raw) {
245
249
  type: raw.type,
246
250
  baseUrl: raw.base_url,
247
251
  apiKeyEnvVar: raw.api_key_env_var,
252
+ timeoutMs: raw.timeout_ms,
248
253
  headers: raw.headers,
249
254
  enabled: raw.enabled,
250
255
  metadata: raw.metadata
@@ -546,6 +551,7 @@ function providerTransportError(context) {
546
551
  }
547
552
 
548
553
  // src/providers/openai-compatible/index.ts
554
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
549
555
  var OpenAICompatibleProvider = class {
550
556
  config;
551
557
  providerId;
@@ -565,6 +571,10 @@ var OpenAICompatibleProvider = class {
565
571
  };
566
572
  this.log = log.child({ provider: config.name });
567
573
  }
574
+ /** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
575
+ get requestTimeoutMs() {
576
+ return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
577
+ }
568
578
  /** Authenticate + discover (roadmap §10, Model Discovery). */
569
579
  async connect() {
570
580
  authHeaders(this.config, this.env);
@@ -576,11 +586,17 @@ var OpenAICompatibleProvider = class {
576
586
  const models = this.discovered ?? await this.connect();
577
587
  return models.find((m) => m.id === modelId);
578
588
  }
579
- async listModels() {
589
+ /** Raw /models body (protected so adapters can capture gateway-level metadata). */
590
+ async rawModelsResponse() {
591
+ const response = await this.modelsRequest();
592
+ return response.body;
593
+ }
594
+ async modelsRequest() {
580
595
  let response;
581
596
  try {
582
597
  response = await requestJson(`${this.config.baseUrl}/models`, {
583
- headers: authHeaders(this.config, this.env)
598
+ headers: authHeaders(this.config, this.env),
599
+ timeoutMs: this.requestTimeoutMs
584
600
  });
585
601
  } catch (error) {
586
602
  if (error instanceof ProviderError) throw error;
@@ -595,6 +611,10 @@ var OpenAICompatibleProvider = class {
595
611
  body
596
612
  });
597
613
  }
614
+ return response;
615
+ }
616
+ async listModels() {
617
+ const body = await this.rawModelsResponse();
598
618
  const entries = extractModelEntries(body, this.config);
599
619
  const models = [];
600
620
  let skipped = 0;
@@ -637,6 +657,10 @@ var OpenAICompatibleProvider = class {
637
657
  }
638
658
  };
639
659
  }
660
+ /** Streaming where the gateway supports it (§11). */
661
+ stream(request, onChunk) {
662
+ return streamChatCompletion(this.config, request, onChunk, this.env);
663
+ }
640
664
  async healthCheck() {
641
665
  const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
642
666
  try {
@@ -671,6 +695,7 @@ var OpenAICompatibleProvider = class {
671
695
  response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
672
696
  method: "POST",
673
697
  headers: authHeaders(this.config, this.env),
698
+ timeoutMs: this.requestTimeoutMs,
674
699
  body: {
675
700
  model: request.model,
676
701
  messages: request.messages,
@@ -715,6 +740,74 @@ var OpenAICompatibleProvider = class {
715
740
  };
716
741
  }
717
742
  };
743
+ async function streamChatCompletion(config, request, onChunk, env = process.env) {
744
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
745
+ const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
746
+ let response;
747
+ try {
748
+ response = await fetch(`${baseUrl}/chat/completions`, {
749
+ method: "POST",
750
+ headers: {
751
+ accept: "text/event-stream",
752
+ "content-type": "application/json",
753
+ ...authHeaders(config, env)
754
+ },
755
+ body: JSON.stringify({
756
+ model: request.model,
757
+ messages: request.messages,
758
+ stream: true,
759
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
760
+ ...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
761
+ }),
762
+ signal: AbortSignal.timeout(timeoutMs)
763
+ });
764
+ } catch (error) {
765
+ throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
766
+ }
767
+ if (!response.ok) {
768
+ const body = await response.json().catch(() => void 0);
769
+ throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
770
+ }
771
+ if (!response.body) {
772
+ throw providerHttpError({
773
+ provider: config.name,
774
+ operation: "stream",
775
+ status: 500,
776
+ body: { error: { message: "streaming is not supported by this endpoint" } }
777
+ });
778
+ }
779
+ let content = "";
780
+ let model = request.model;
781
+ let finishReason;
782
+ const decoder = new TextDecoder();
783
+ let buffer = "";
784
+ for await (const piece of response.body) {
785
+ buffer += decoder.decode(piece, { stream: true });
786
+ let newline = buffer.indexOf("\n");
787
+ while (newline >= 0) {
788
+ const line = buffer.slice(0, newline).trim();
789
+ buffer = buffer.slice(newline + 1);
790
+ newline = buffer.indexOf("\n");
791
+ if (line.length === 0 || line.startsWith(":")) continue;
792
+ const data = line.startsWith("data:") ? line.slice(5).trim() : line;
793
+ if (data === "[DONE]") continue;
794
+ let event;
795
+ try {
796
+ event = JSON.parse(data);
797
+ } catch {
798
+ continue;
799
+ }
800
+ if (typeof event.model === "string") model = event.model;
801
+ const choices = Array.isArray(event.choices) ? event.choices : [];
802
+ const first = choices[0];
803
+ const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
804
+ if (delta.length > 0) content += delta;
805
+ if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
806
+ onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
807
+ }
808
+ }
809
+ return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
810
+ }
718
811
  function normalizeModalities(value) {
719
812
  const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
720
813
  if (!list) return void 0;
@@ -748,10 +841,32 @@ function inferHealth(error) {
748
841
 
749
842
  // src/providers/omnihilbras/index.ts
750
843
  var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
844
+ gateway;
845
+ async listModels() {
846
+ const raw = await this.rawModelsResponse();
847
+ await this.extractGatewayMetadata(raw);
848
+ const models = await super.listModels();
849
+ return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
850
+ }
751
851
  /**
752
852
  * Run the full connect flow. Throws on authentication or transport failure
753
853
  * (use healthCheck() for a non-throwing probe).
754
854
  */
855
+ /**
856
+ * Gateway metadata reported alongside the model list (gateway version, tier,
857
+ * region…), kept separate from per-model metadata (§11 — provider metadata).
858
+ */
859
+ gatewayMetadata() {
860
+ return this.gateway;
861
+ }
862
+ async extractGatewayMetadata(body) {
863
+ if (body !== null && typeof body === "object") {
864
+ const gateway = body.gateway;
865
+ if (gateway !== null && typeof gateway === "object") {
866
+ this.gateway = gateway;
867
+ }
868
+ }
869
+ }
755
870
  async connectAndRegister(registry) {
756
871
  const apiKey = resolveApiKey(this.config);
757
872
  const discovered = await this.listModels();
@@ -2377,6 +2492,23 @@ var FileAuditLog = class {
2377
2492
  }
2378
2493
  };
2379
2494
 
2495
+ // src/memory/base.ts
2496
+ function defaultSearch(provider, text, options = {}) {
2497
+ return provider.retrieve({
2498
+ text,
2499
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
2500
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
2501
+ });
2502
+ }
2503
+ function defaultMetadata(name, options = {}) {
2504
+ return {
2505
+ name,
2506
+ capabilities: options.capabilities ?? ["retrieve", "store", "search"],
2507
+ relevanceRanking: options.relevanceRanking ?? false,
2508
+ ...options.backend !== void 0 ? { backend: options.backend } : {}
2509
+ };
2510
+ }
2511
+
2380
2512
  // src/memory/local.ts
2381
2513
  var STOPWORDS = /* @__PURE__ */ new Set([
2382
2514
  "the",
@@ -2439,9 +2571,27 @@ var LocalMemoryProvider = class extends JsonFileStore {
2439
2571
  constructor(directory) {
2440
2572
  super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
2441
2573
  }
2442
- async query(query) {
2574
+ metadata() {
2575
+ return defaultMetadata(this.name, {
2576
+ capabilities: ["retrieve", "store", "search", "categories", "tags"],
2577
+ relevanceRanking: true,
2578
+ backend: { kind: "json-file", deterministicScoring: true }
2579
+ });
2580
+ }
2581
+ search(text, options = {}) {
2582
+ return defaultSearch(this, text, options);
2583
+ }
2584
+ /** @deprecated v1 alias of {@link retrieve}. */
2585
+ query(query) {
2586
+ return this.retrieve(query);
2587
+ }
2588
+ /** @deprecated v1 alias of {@link store}. */
2589
+ write(entry2) {
2590
+ return this.store(entry2);
2591
+ }
2592
+ async retrieve(query) {
2443
2593
  const entries = await this.readAll();
2444
- const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter(
2594
+ const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter((entry2) => query.category ? entry2.category === query.category : true).filter(
2445
2595
  (entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
2446
2596
  );
2447
2597
  if (!query.text || query.text.trim().length === 0) {
@@ -2450,7 +2600,7 @@ var LocalMemoryProvider = class extends JsonFileStore {
2450
2600
  const queryKeywords = keywords(query.text);
2451
2601
  return filtered.map((entry2) => ({ entry: entry2, score: relevanceScore(queryKeywords, entry2) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry: entry2 }) => entry2);
2452
2602
  }
2453
- write(entry2) {
2603
+ store(entry2) {
2454
2604
  return this.mutate((entries) => {
2455
2605
  const index = entries.findIndex((e) => e.key === entry2.key);
2456
2606
  const stamped = {
@@ -2481,11 +2631,40 @@ var RememberaMemoryProvider = class {
2481
2631
  this.env = config.env ?? process.env;
2482
2632
  this.log = log.child({ component: "remembera" });
2483
2633
  }
2634
+ get authenticationConfigured() {
2635
+ return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
2636
+ }
2484
2637
  headers() {
2485
2638
  const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
2486
2639
  return key ? { authorization: `Bearer ${key}` } : {};
2487
2640
  }
2488
- async query(query) {
2641
+ /** Search delegates to the gateway's relevance query. */
2642
+ search(text, options = {}) {
2643
+ return defaultSearch(
2644
+ this,
2645
+ text,
2646
+ {
2647
+ ...options.scope !== void 0 ? { scope: options.scope } : {},
2648
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
2649
+ }
2650
+ );
2651
+ }
2652
+ metadata() {
2653
+ return defaultMetadata(this.name, {
2654
+ capabilities: ["retrieve", "store", "search", "categories", "tags"],
2655
+ relevanceRanking: true,
2656
+ backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
2657
+ });
2658
+ }
2659
+ /** @deprecated v1 alias of {@link retrieve}. */
2660
+ query(query) {
2661
+ return this.retrieve(query);
2662
+ }
2663
+ /** @deprecated v1 alias of {@link store}. */
2664
+ write(entry2) {
2665
+ return this.store(entry2);
2666
+ }
2667
+ async retrieve(query) {
2489
2668
  const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
2490
2669
  if (status !== 200) {
2491
2670
  throw new MemoryError(`Remembera query returned HTTP ${status}.`);
@@ -2496,7 +2675,7 @@ var RememberaMemoryProvider = class {
2496
2675
  }
2497
2676
  return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
2498
2677
  }
2499
- async write(entry2) {
2678
+ async store(entry2) {
2500
2679
  const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
2501
2680
  if (status !== 200 && status !== 201) {
2502
2681
  throw new MemoryError(`Remembera write returned HTTP ${status}.`);
@@ -2531,6 +2710,7 @@ var RememberaMemoryProvider = class {
2531
2710
  content: entry2.content,
2532
2711
  scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
2533
2712
  ...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
2713
+ ...typeof entry2.category === "string" ? { category: entry2.category } : {},
2534
2714
  ...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
2535
2715
  ...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
2536
2716
  };
@@ -2561,57 +2741,54 @@ function createMemoryProvider(config) {
2561
2741
  }
2562
2742
 
2563
2743
  // src/memory/service.ts
2744
+ var DEFAULT_ENTRY_LIMIT = 8;
2564
2745
  var MAX_CONTEXT_CHARS = 4e3;
2565
2746
  var MemoryService = class {
2566
- constructor(provider, log = logger) {
2747
+ constructor(provider, log = logger, options = {}) {
2567
2748
  this.provider = provider;
2568
2749
  this.log = log.child({ component: "memory", provider: provider.name });
2750
+ this.required = options.required ?? false;
2751
+ this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
2569
2752
  }
2570
2753
  provider;
2571
2754
  log;
2755
+ required;
2756
+ maxContextChars;
2572
2757
  get providerRef() {
2573
2758
  return this.provider;
2574
2759
  }
2575
2760
  get providerName() {
2576
2761
  return this.provider.name;
2577
2762
  }
2578
- /** Relevant context for a task objective; empty string when nothing matches. */
2579
- async gatherContext(query) {
2763
+ get isRequired() {
2764
+ return this.required;
2765
+ }
2766
+ /** Structured, budgeted context for a task (§12 — avoid loading all memory). */
2767
+ async contextFor(query) {
2768
+ let entries;
2580
2769
  try {
2581
- const entries = await this.provider.query({
2770
+ entries = await this.provider.retrieve({
2582
2771
  text: query.objective,
2583
2772
  ...query.role ? { tags: [query.role] } : {},
2584
- limit: 8
2773
+ limit: query.limit ?? DEFAULT_ENTRY_LIMIT
2585
2774
  });
2586
- if (entries.length === 0) return "";
2587
- const byScope = /* @__PURE__ */ new Map();
2588
- for (const entry2 of entries) {
2589
- byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
2590
- }
2591
- const parts = [];
2592
- for (const scope of ["project", "task", "role", "knowledge"]) {
2593
- const scopeEntries = byScope.get(scope) ?? [];
2594
- if (scopeEntries.length === 0) continue;
2595
- const lines = scopeEntries.map((entry2) => `- ${entry2.content}`).join("\n");
2596
- parts.push(`### Relevant memory (${scope})
2597
- ${lines}`);
2598
- }
2599
- const context = parts.join("\n\n");
2600
- return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
2601
2775
  } catch (error) {
2602
- this.log.warn(
2603
- `Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
2604
- );
2605
- return "";
2776
+ return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
2606
2777
  }
2778
+ return this.format(entries);
2607
2779
  }
2608
- /** Records a finished task's outcome — best-effort. */
2780
+ /** Prompt-ready context text (v1 convenience wrapper). */
2781
+ async gatherContext(query) {
2782
+ return (await this.contextFor(query)).text;
2783
+ }
2784
+ /** Records a finished task's outcome — task history plus "known problems" (§12). */
2609
2785
  async recordTaskOutcome(task) {
2610
2786
  const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
2611
2787
  try {
2612
- await this.provider.write({
2788
+ await this.provider.store({
2613
2789
  key: `task:${task.id}`,
2614
2790
  scope: "task",
2791
+ category: "tasks",
2615
2792
  content: `Objective: ${task.objective}
2616
2793
  Status: ${task.status}
2617
2794
  Outcome: ${outcome}`,
@@ -2622,18 +2799,53 @@ Outcome: ${outcome}`,
2622
2799
  (report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
2623
2800
  );
2624
2801
  for (const { report, finding } of significant) {
2625
- await this.provider.write({
2802
+ await this.provider.store({
2626
2803
  key: `finding:${report.id}:${finding.id}`,
2627
2804
  scope: "project",
2805
+ category: "problems",
2628
2806
  content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
2629
2807
  tags: ["known-problem", report.agent]
2630
2808
  });
2631
2809
  }
2632
2810
  } catch (error) {
2633
- this.log.warn(
2634
- `Memory write failed (continuing): ${error instanceof Error ? error.message : String(error)}`
2811
+ this.handleFailure("store", error, void 0);
2812
+ }
2813
+ }
2814
+ format(entries) {
2815
+ if (entries.length === 0) return { text: "", entries, truncated: false };
2816
+ const byScope = /* @__PURE__ */ new Map();
2817
+ for (const entry2 of entries) {
2818
+ byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
2819
+ }
2820
+ const parts = [];
2821
+ for (const scope of ["project", "task", "role", "knowledge"]) {
2822
+ const scopeEntries = byScope.get(scope) ?? [];
2823
+ if (scopeEntries.length === 0) continue;
2824
+ const lines = scopeEntries.map((entry2) => {
2825
+ const category = entry2.category ? `[${entry2.category}] ` : "";
2826
+ return `- ${category}${entry2.content}`;
2827
+ }).join("\n");
2828
+ parts.push(`### Relevant memory (${scope})
2829
+ ${lines}`);
2830
+ }
2831
+ let text = parts.join("\n\n");
2832
+ const truncated = text.length > this.maxContextChars;
2833
+ if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
2834
+ return { text, entries, truncated };
2835
+ }
2836
+ /**
2837
+ * Failure policy (§12): continue unless memory is explicitly required.
2838
+ */
2839
+ handleFailure(operation, error, fallback) {
2840
+ const message = error instanceof Error ? error.message : String(error);
2841
+ if (this.required) {
2842
+ throw new MemoryError(
2843
+ `Memory is marked required, but ${operation} failed: ${message}`,
2844
+ { cause: error }
2635
2845
  );
2636
2846
  }
2847
+ this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
2848
+ return fallback ?? { text: "", entries: [], truncated: false };
2637
2849
  }
2638
2850
  };
2639
2851
 
@@ -2667,7 +2879,11 @@ function createTaskEngine(config, log = logger, directory) {
2667
2879
  agents,
2668
2880
  roles,
2669
2881
  store: new FileTaskStore(directory),
2670
- ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
2882
+ ...config.project.memory !== void 0 ? {
2883
+ memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
2884
+ required: config.project.memory.required
2885
+ })
2886
+ } : {},
2671
2887
  audit: new FileAuditLog(),
2672
2888
  log
2673
2889
  });
@@ -3943,7 +4159,11 @@ function buildPipelineEngine(config, log = logger) {
3943
4159
  agents,
3944
4160
  roles,
3945
4161
  store: new FileTaskStore(),
3946
- ...config.project.memory !== void 0 ? { memory: new MemoryService(createMemoryProvider(config.project.memory), log) } : {},
4162
+ ...config.project.memory !== void 0 ? {
4163
+ memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
4164
+ required: config.project.memory.required
4165
+ })
4166
+ } : {},
3947
4167
  audit: new FileAuditLog(),
3948
4168
  log
3949
4169
  });