@remnic/core 9.3.665 → 9.3.667

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.
Files changed (150) hide show
  1. package/dist/access-audit.js +2 -2
  2. package/dist/access-cli.js +41 -40
  3. package/dist/access-cli.js.map +1 -1
  4. package/dist/access-http.d.ts +3 -2
  5. package/dist/access-http.js +25 -25
  6. package/dist/access-mcp.d.ts +3 -2
  7. package/dist/access-mcp.js +22 -22
  8. package/dist/access-schema.d.ts +36 -36
  9. package/dist/access-schema.js +3 -3
  10. package/dist/{access-service-D0SLB4MH.d.ts → access-service-BCuaiNHa.d.ts} +1 -1
  11. package/dist/access-service.d.ts +3 -2
  12. package/dist/access-service.js +21 -21
  13. package/dist/adapters/index.js +4 -4
  14. package/dist/adapters/registry.js +2 -2
  15. package/dist/bootstrap.d.ts +2 -1
  16. package/dist/briefing.js +4 -3
  17. package/dist/capabilities.d.ts +73 -0
  18. package/dist/capabilities.js +8 -0
  19. package/dist/capabilities.js.map +1 -0
  20. package/dist/causal-behavior.js +2 -2
  21. package/dist/causal-chain.js +2 -2
  22. package/dist/causal-consolidation.js +7 -6
  23. package/dist/causal-consolidation.js.map +1 -1
  24. package/dist/causal-retrieval.js +2 -2
  25. package/dist/causal-trajectory.js +1 -1
  26. package/dist/{chunk-ROHLEUTH.js → chunk-23EBQ27U.js} +5 -5
  27. package/dist/{chunk-YW52BQSU.js → chunk-2TCHDANJ.js} +2 -2
  28. package/dist/{chunk-IROWLAWG.js → chunk-46WUVFOD.js} +4 -4
  29. package/dist/{chunk-WH4SKYPX.js → chunk-4FJKKC2N.js} +107 -77
  30. package/dist/chunk-4FJKKC2N.js.map +1 -0
  31. package/dist/{chunk-7C4MPEPE.js → chunk-4T7P2HLJ.js} +3 -3
  32. package/dist/{chunk-7XH7VJN4.js → chunk-6T4LTI2F.js} +4 -4
  33. package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
  34. package/dist/{chunk-BZG2CWOQ.js → chunk-A5TEHAR4.js} +3 -3
  35. package/dist/{chunk-C7AF236A.js → chunk-AARDBQTA.js} +2 -2
  36. package/dist/{chunk-IHG6CC7T.js → chunk-BQJUPECT.js} +2 -2
  37. package/dist/{chunk-7OGJQP7T.js → chunk-CRO4LCQ6.js} +5 -5
  38. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  39. package/dist/{chunk-UXA5L2DZ.js → chunk-HQCGRSRU.js} +2 -2
  40. package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
  41. package/dist/{chunk-XW3W4PV4.js → chunk-JTPXSXHC.js} +2 -2
  42. package/dist/{chunk-OHJFJ4HI.js → chunk-KOXGLQS7.js} +2 -2
  43. package/dist/{chunk-2OPARZ4B.js → chunk-MPXYHC35.js} +26 -26
  44. package/dist/{chunk-6JBKHTQD.js → chunk-MR4PJ277.js} +2 -2
  45. package/dist/{chunk-EXXBA5OM.js → chunk-OI4BXFSB.js} +4 -4
  46. package/dist/{chunk-SQZ42MKH.js → chunk-OQH5XUH3.js} +6 -3
  47. package/dist/chunk-OQH5XUH3.js.map +1 -0
  48. package/dist/{chunk-2HEZXPYU.js → chunk-Q2LQZYQ7.js} +3 -3
  49. package/dist/{chunk-XRSIGVTS.js → chunk-QHWJG5C5.js} +8 -8
  50. package/dist/{chunk-T2AN3BSP.js → chunk-QZ7ODIVL.js} +2 -2
  51. package/dist/chunk-RI5XBIZ6.js +23 -0
  52. package/dist/chunk-RI5XBIZ6.js.map +1 -0
  53. package/dist/{chunk-D7IXTY5E.js → chunk-TJ7HH5LB.js} +2 -2
  54. package/dist/{chunk-V25ZAOSB.js → chunk-UOBLE67F.js} +4 -4
  55. package/dist/{chunk-JIX3ZL2J.js → chunk-UVUTV7CM.js} +15 -15
  56. package/dist/{chunk-VH6EIKVS.js → chunk-WKMCC4NQ.js} +35 -16
  57. package/dist/chunk-WKMCC4NQ.js.map +1 -0
  58. package/dist/{chunk-SSOMTUCA.js → chunk-WXGTC424.js} +1 -1
  59. package/dist/{chunk-KHGE6PMF.js → chunk-WXXLSZHA.js} +2 -2
  60. package/dist/{chunk-DSLUOQDY.js → chunk-XMWF6AU3.js} +2 -2
  61. package/dist/{chunk-DQY7NJ5L.js → chunk-XS2CWEHZ.js} +2 -2
  62. package/dist/{cli-BQRqR9N-.d.ts → cli-C98xlwYA.d.ts} +2 -2
  63. package/dist/cli.d.ts +4 -3
  64. package/dist/cli.js +42 -42
  65. package/dist/compounding/engine.js +4 -3
  66. package/dist/connectors/codex-materialize-runner.js +4 -3
  67. package/dist/connectors/index.js +4 -3
  68. package/dist/consolidation-provenance-check.js +2 -2
  69. package/dist/conversation-index/backend.js +2 -2
  70. package/dist/dashboard-runtime.js +2 -2
  71. package/dist/direct-answer-wiring.d.ts +13 -3
  72. package/dist/direct-answer-wiring.js +1 -1
  73. package/dist/entity-retrieval.js +4 -3
  74. package/dist/explicit-capture.d.ts +2 -1
  75. package/dist/index.d.ts +5 -4
  76. package/dist/index.js +66 -65
  77. package/dist/index.js.map +1 -1
  78. package/dist/lcm/engine.js +2 -2
  79. package/dist/lcm/index.js +4 -4
  80. package/dist/maintenance/memory-governance.js +4 -4
  81. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -3
  82. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  83. package/dist/mcp-memory-inspector-app.d.ts +3 -2
  84. package/dist/namespaces/migrate.js +11 -11
  85. package/dist/namespaces/search.js +7 -7
  86. package/dist/namespaces/storage.js +4 -3
  87. package/dist/operator-toolkit.js +15 -15
  88. package/dist/{orchestrator-Cg1UkvmO.d.ts → orchestrator-DyP9QYsh.d.ts} +16 -0
  89. package/dist/orchestrator.d.ts +2 -1
  90. package/dist/orchestrator.js +32 -31
  91. package/dist/recall-planner-llm.d.ts +2 -1
  92. package/dist/recall-planner-llm.js +3 -2
  93. package/dist/recall-planner-llm.js.map +1 -1
  94. package/dist/schemas.d.ts +64 -64
  95. package/dist/search/factory.js +6 -6
  96. package/dist/search/index.js +10 -10
  97. package/dist/search/lancedb-backend.js +1 -1
  98. package/dist/search/meilisearch-backend.js +1 -1
  99. package/dist/search/orama-backend.js +1 -1
  100. package/dist/semantic-consolidation.js +5 -4
  101. package/dist/semantic-rule-promotion.js +4 -3
  102. package/dist/semantic-rule-verifier.js +4 -3
  103. package/dist/shared-context/manager.d.ts +2 -2
  104. package/dist/storage.js +3 -2
  105. package/dist/transfer/backup.js +2 -2
  106. package/dist/transfer/capsule-export.js +2 -2
  107. package/dist/transfer/capsule-import.js +1 -1
  108. package/dist/transfer/types.d.ts +12 -12
  109. package/dist/verified-recall.js +4 -3
  110. package/package.json +1 -1
  111. package/src/capabilities.test.ts +97 -0
  112. package/src/capabilities.ts +86 -0
  113. package/src/direct-answer-wiring.test.ts +53 -2
  114. package/src/direct-answer-wiring.ts +18 -5
  115. package/src/orchestrator.ts +83 -22
  116. package/src/recall-planner-llm.test.ts +12 -11
  117. package/src/recall-planner-llm.ts +7 -1
  118. package/src/storage-fallback-category-dirs.test.ts +150 -1
  119. package/src/storage.ts +51 -14
  120. package/dist/chunk-SQZ42MKH.js.map +0 -1
  121. package/dist/chunk-VH6EIKVS.js.map +0 -1
  122. package/dist/chunk-WH4SKYPX.js.map +0 -1
  123. /package/dist/{chunk-ROHLEUTH.js.map → chunk-23EBQ27U.js.map} +0 -0
  124. /package/dist/{chunk-YW52BQSU.js.map → chunk-2TCHDANJ.js.map} +0 -0
  125. /package/dist/{chunk-IROWLAWG.js.map → chunk-46WUVFOD.js.map} +0 -0
  126. /package/dist/{chunk-7C4MPEPE.js.map → chunk-4T7P2HLJ.js.map} +0 -0
  127. /package/dist/{chunk-7XH7VJN4.js.map → chunk-6T4LTI2F.js.map} +0 -0
  128. /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
  129. /package/dist/{chunk-BZG2CWOQ.js.map → chunk-A5TEHAR4.js.map} +0 -0
  130. /package/dist/{chunk-C7AF236A.js.map → chunk-AARDBQTA.js.map} +0 -0
  131. /package/dist/{chunk-IHG6CC7T.js.map → chunk-BQJUPECT.js.map} +0 -0
  132. /package/dist/{chunk-7OGJQP7T.js.map → chunk-CRO4LCQ6.js.map} +0 -0
  133. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  134. /package/dist/{chunk-UXA5L2DZ.js.map → chunk-HQCGRSRU.js.map} +0 -0
  135. /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
  136. /package/dist/{chunk-XW3W4PV4.js.map → chunk-JTPXSXHC.js.map} +0 -0
  137. /package/dist/{chunk-OHJFJ4HI.js.map → chunk-KOXGLQS7.js.map} +0 -0
  138. /package/dist/{chunk-2OPARZ4B.js.map → chunk-MPXYHC35.js.map} +0 -0
  139. /package/dist/{chunk-6JBKHTQD.js.map → chunk-MR4PJ277.js.map} +0 -0
  140. /package/dist/{chunk-EXXBA5OM.js.map → chunk-OI4BXFSB.js.map} +0 -0
  141. /package/dist/{chunk-2HEZXPYU.js.map → chunk-Q2LQZYQ7.js.map} +0 -0
  142. /package/dist/{chunk-XRSIGVTS.js.map → chunk-QHWJG5C5.js.map} +0 -0
  143. /package/dist/{chunk-T2AN3BSP.js.map → chunk-QZ7ODIVL.js.map} +0 -0
  144. /package/dist/{chunk-D7IXTY5E.js.map → chunk-TJ7HH5LB.js.map} +0 -0
  145. /package/dist/{chunk-V25ZAOSB.js.map → chunk-UOBLE67F.js.map} +0 -0
  146. /package/dist/{chunk-JIX3ZL2J.js.map → chunk-UVUTV7CM.js.map} +0 -0
  147. /package/dist/{chunk-SSOMTUCA.js.map → chunk-WXGTC424.js.map} +0 -0
  148. /package/dist/{chunk-KHGE6PMF.js.map → chunk-WXXLSZHA.js.map} +0 -0
  149. /package/dist/{chunk-DSLUOQDY.js.map → chunk-XMWF6AU3.js.map} +0 -0
  150. /package/dist/{chunk-DQY7NJ5L.js.map → chunk-XS2CWEHZ.js.map} +0 -0
@@ -7,7 +7,7 @@ import {
7
7
  type DirectAnswerWiringInput,
8
8
  } from "./direct-answer-wiring.js";
9
9
  import { DEFAULT_TAXONOMY } from "./taxonomy/default-taxonomy.js";
10
- import type { MemoryFile, PluginConfig } from "./types.js";
10
+ import type { MemoryFile } from "./types.js";
11
11
  import type { TrustZoneName } from "./trust-zones.js";
12
12
 
13
13
  type WiringConfig = DirectAnswerWiringInput["config"];
@@ -94,7 +94,8 @@ test("tryDirectAnswer disabled-path does not call any source accessor", async ()
94
94
  const result = await tryDirectAnswer({
95
95
  query: "does not matter",
96
96
  namespace: "default",
97
- config: { ...BASE_CONFIG, recallDirectAnswerEnabled: false },
97
+ config: BASE_CONFIG,
98
+ enabled: false,
98
99
  sources,
99
100
  });
100
101
  assert.equal(result.eligible, false);
@@ -104,6 +105,42 @@ test("tryDirectAnswer disabled-path does not call any source accessor", async ()
104
105
  assert.deepEqual(sources.calls.importance, []);
105
106
  });
106
107
 
108
+ // ── Backward-compat (#1523): omit top-level `enabled`, fall back to config ──
109
+
110
+ test("tryDirectAnswer falls back to config.recallDirectAnswerEnabled when `enabled` is omitted (disabled)", async () => {
111
+ // Old input shape: config carries recallDirectAnswerEnabled: false and no
112
+ // top-level `enabled`. Must short-circuit as "disabled" (identical to the
113
+ // pre-#1523 behavior) rather than treating undefined as enabled.
114
+ const sources = makeMockSources({ memories: [makeMemory()] });
115
+ const result = await tryDirectAnswer({
116
+ query: "does not matter",
117
+ namespace: "default",
118
+ config: { ...BASE_CONFIG, recallDirectAnswerEnabled: false },
119
+ sources,
120
+ });
121
+ assert.equal(result.eligible, false);
122
+ assert.equal(result.reason, "disabled");
123
+ assert.equal(sources.calls.listCandidates, 0);
124
+ });
125
+
126
+ test("tryDirectAnswer falls back to config.recallDirectAnswerEnabled when `enabled` is omitted (enabled)", async () => {
127
+ // Old input shape with recallDirectAnswerEnabled: true and no `enabled` must
128
+ // NOT short-circuit — it proceeds to materialize candidates.
129
+ const sources = makeMockSources({
130
+ memories: [makeMemory({ tags: ["pnpm"], content: "remnic uses pnpm" })],
131
+ trustZones: { m1: "trusted" },
132
+ importance: { m1: 0.9 },
133
+ });
134
+ const result = await tryDirectAnswer({
135
+ query: "package manager remnic",
136
+ namespace: "default",
137
+ config: BASE_CONFIG, // recallDirectAnswerEnabled: true
138
+ sources,
139
+ });
140
+ assert.notEqual(result.reason, "disabled");
141
+ assert.equal(sources.calls.listCandidates, 1);
142
+ });
143
+
107
144
  // ── Empty-query short-circuit: no I/O ───────────────────────────────────────
108
145
 
109
146
  test("tryDirectAnswer skips all I/O when query normalizes to zero searchable tokens", async () => {
@@ -119,6 +156,7 @@ test("tryDirectAnswer skips all I/O when query normalizes to zero searchable tok
119
156
  query: "? !!! ",
120
157
  namespace: "default",
121
158
  config: BASE_CONFIG,
159
+ enabled: true,
122
160
  sources,
123
161
  });
124
162
  assert.equal(result.reason, "empty-query");
@@ -135,6 +173,7 @@ test("tryDirectAnswer with empty memory list returns no-candidates", async () =>
135
173
  query: "package manager remnic",
136
174
  namespace: "default",
137
175
  config: BASE_CONFIG,
176
+ enabled: true,
138
177
  sources,
139
178
  });
140
179
  assert.equal(result.reason, "no-candidates");
@@ -158,6 +197,7 @@ test("tryDirectAnswer skips importance resolution for non-trusted memories", asy
158
197
  query: "package manager remnic",
159
198
  namespace: "default",
160
199
  config: BASE_CONFIG,
200
+ enabled: true,
161
201
  sources,
162
202
  });
163
203
  assert.equal(result.eligible, false);
@@ -182,6 +222,7 @@ test("tryDirectAnswer skips importance for quarantine-zone memories", async () =
182
222
  query: "package manager remnic",
183
223
  namespace: "default",
184
224
  config: BASE_CONFIG,
225
+ enabled: true,
185
226
  sources,
186
227
  });
187
228
  assert.equal(result.eligible, false);
@@ -203,6 +244,7 @@ test("tryDirectAnswer skips importance when trust zone is missing (null)", async
203
244
  query: "package manager remnic",
204
245
  namespace: "default",
205
246
  config: BASE_CONFIG,
247
+ enabled: true,
206
248
  sources,
207
249
  });
208
250
  assert.equal(result.eligible, false);
@@ -229,6 +271,7 @@ test("tryDirectAnswer skips importance when taxonomy bucket is not eligible", as
229
271
  query: "package manager remnic",
230
272
  namespace: "default",
231
273
  config: BASE_CONFIG,
274
+ enabled: true,
232
275
  sources,
233
276
  });
234
277
  assert.equal(result.eligible, false);
@@ -254,6 +297,7 @@ test("tryDirectAnswer returns eligible for a single trusted user-confirmed decis
254
297
  query: "package manager remnic",
255
298
  namespace: "default",
256
299
  config: BASE_CONFIG,
300
+ enabled: true,
257
301
  sources,
258
302
  });
259
303
  assert.equal(result.eligible, true);
@@ -284,6 +328,7 @@ test("tryDirectAnswer defers to hybrid when two trusted candidates are within am
284
328
  query: "package manager remnic",
285
329
  namespace: "default",
286
330
  config: BASE_CONFIG,
331
+ enabled: true,
287
332
  sources,
288
333
  });
289
334
  assert.equal(result.eligible, false);
@@ -327,6 +372,7 @@ test("tryDirectAnswer throws AbortError when signal aborts mid-loop", async () =
327
372
  query: "package manager remnic",
328
373
  namespace: "default",
329
374
  config: BASE_CONFIG,
375
+ enabled: true,
330
376
  sources,
331
377
  abortSignal: controller.signal,
332
378
  }),
@@ -363,6 +409,7 @@ test("tryDirectAnswer throws when abort lands during trustZoneFor on the only me
363
409
  query: "package manager remnic",
364
410
  namespace: "default",
365
411
  config: BASE_CONFIG,
412
+ enabled: true,
366
413
  sources,
367
414
  abortSignal: controller.signal,
368
415
  }),
@@ -391,6 +438,7 @@ test("tryDirectAnswer throws when abort lands during trustZoneFor on the last of
391
438
  query: "package manager remnic",
392
439
  namespace: "default",
393
440
  config: BASE_CONFIG,
441
+ enabled: true,
394
442
  sources,
395
443
  abortSignal: controller.signal,
396
444
  }),
@@ -408,6 +456,7 @@ test("tryDirectAnswer throws when signal is already aborted before I/O", async (
408
456
  query: "anything",
409
457
  namespace: "default",
410
458
  config: BASE_CONFIG,
459
+ enabled: true,
411
460
  sources,
412
461
  abortSignal: controller.signal,
413
462
  }),
@@ -434,6 +483,7 @@ test("tryDirectAnswer passes the requested namespace to listCandidateMemories",
434
483
  query: "anything",
435
484
  namespace: "project-x",
436
485
  config: BASE_CONFIG,
486
+ enabled: true,
437
487
  sources,
438
488
  });
439
489
  assert.equal(observedNamespace, "project-x");
@@ -465,6 +515,7 @@ test("tryDirectAnswer forwards queryEntityRefs to the eligibility gate", async (
465
515
  query: "package manager remnic",
466
516
  namespace: "default",
467
517
  config: BASE_CONFIG,
518
+ enabled: true,
468
519
  sources,
469
520
  queryEntityRefs: ["remnic"],
470
521
  });
@@ -14,9 +14,10 @@
14
14
  *
15
15
  * Short-circuit contract:
16
16
  *
17
- * - When `config.recallDirectAnswerEnabled === false`, the function
18
- * returns the eligibility verdict with reason `"disabled"` without
19
- * touching any source accessor. This is the documented default.
17
+ * - When the resolved gate (`input.enabled` if supplied, else
18
+ * `config.recallDirectAnswerEnabled`) is `false`, the function returns the
19
+ * eligibility verdict with reason `"disabled"` without touching any source
20
+ * accessor. This is the documented default.
20
21
  * - When enabled, the wiring cheaply drops non-trusted-zone memories
21
22
  * and ineligible taxonomy buckets before computing importance, so
22
23
  * the eligibility module sees a pre-filtered candidate set. The
@@ -79,6 +80,15 @@ export interface DirectAnswerWiringInput {
79
80
  | "recallDirectAnswerAmbiguityMargin"
80
81
  | "recallDirectAnswerEligibleTaxonomyBuckets"
81
82
  >;
83
+ /**
84
+ * Direct-answer capability gate, resolved once at the recall-operation entry
85
+ * (issue #1523: `caps.recallDirectAnswer`). OPTIONAL and additive: when the
86
+ * caller supplies it, this module and the orchestrator agree on a single
87
+ * resolved gate value for the whole operation. When omitted, we fall back to
88
+ * `config.recallDirectAnswerEnabled` so existing callers on the old input
89
+ * shape keep identical behavior.
90
+ */
91
+ enabled?: boolean;
82
92
  sources: DirectAnswerSources;
83
93
  queryEntityRefs?: string[];
84
94
  abortSignal?: AbortSignal;
@@ -92,10 +102,13 @@ export interface DirectAnswerWiringInput {
92
102
  export async function tryDirectAnswer(
93
103
  input: DirectAnswerWiringInput,
94
104
  ): Promise<DirectAnswerResult> {
95
- const { query, namespace, config, sources, queryEntityRefs, abortSignal } = input;
105
+ const { query, namespace, config, enabled, sources, queryEntityRefs, abortSignal } = input;
96
106
 
97
107
  const eligibilityConfig: DirectAnswerConfig = {
98
- enabled: config.recallDirectAnswerEnabled,
108
+ // Prefer the resolved capability when supplied; fall back to the config
109
+ // flag so callers on the old input shape (config-only, no `enabled`) get
110
+ // identical gating (issue #1523 backward-compat).
111
+ enabled: enabled ?? config.recallDirectAnswerEnabled,
99
112
  tokenOverlapFloor: config.recallDirectAnswerTokenOverlapFloor,
100
113
  importanceFloor: config.recallDirectAnswerImportanceFloor,
101
114
  ambiguityMargin: config.recallDirectAnswerAmbiguityMargin,
@@ -245,6 +245,7 @@ import {
245
245
  type TrustZoneSearchResult,
246
246
  } from "./trust-zones.js";
247
247
  import { tryDirectAnswer, type DirectAnswerSources } from "./direct-answer-wiring.js";
248
+ import { resolveCapabilities, type CapabilitySet } from "./capabilities.js";
248
249
  import { DEFAULT_TAXONOMY } from "./taxonomy/index.js";
249
250
  import {
250
251
  searchHarmonicRetrieval,
@@ -1376,6 +1377,13 @@ export function resolveRecallModeDecision(options: RecallModeGraphOptions): Reca
1376
1377
  export async function resolveRecallModeDecisionAsync(
1377
1378
  options: RecallModeGraphOptions & {
1378
1379
  config: PluginConfig;
1380
+ /**
1381
+ * Recall-operation capability gates (issue #1523). OPTIONAL and additive:
1382
+ * the recall orchestrator passes a resolved set, but existing callers that
1383
+ * only pass `config` + planner flags stay backward-compatible — the LLM
1384
+ * planner gate falls back to `config.recallPlannerLlmEnabled` when omitted.
1385
+ */
1386
+ caps?: CapabilitySet;
1379
1387
  hints?: string[];
1380
1388
  llm?: FallbackLlmClient;
1381
1389
  signal?: AbortSignal;
@@ -1384,7 +1392,11 @@ export async function resolveRecallModeDecisionAsync(
1384
1392
  const heuristicDecision = resolveRecallModeDecision(options);
1385
1393
 
1386
1394
  // Planner globally off, or LLM planning not opted into → heuristic only.
1387
- if (!options.plannerEnabled || !options.config.recallPlannerLlmEnabled) {
1395
+ // Prefer the resolved capability when supplied; otherwise fall back to the
1396
+ // config flag so callers on the old option shape get identical gating.
1397
+ const plannerLlmEnabled =
1398
+ options.caps?.recallPlannerLlm ?? options.config.recallPlannerLlmEnabled;
1399
+ if (!options.plannerEnabled || !plannerLlmEnabled) {
1388
1400
  return heuristicDecision;
1389
1401
  }
1390
1402
 
@@ -1395,6 +1407,7 @@ export async function resolveRecallModeDecisionAsync(
1395
1407
  options.config,
1396
1408
  options.llm,
1397
1409
  options.signal,
1410
+ options.caps,
1398
1411
  );
1399
1412
 
1400
1413
  // Shadow mode: record what the LLM would have chosen but keep the heuristic
@@ -5738,6 +5751,10 @@ export class Orchestrator {
5738
5751
  sessionKey?: string,
5739
5752
  options: RecallInvocationOptions = {},
5740
5753
  ): Promise<string> {
5754
+ // Resolve the recall-operation capability gates ONCE, at the operation
5755
+ // entry, and thread the frozen set down (issue #1523). Never re-read the
5756
+ // migrated flags off `this.config` mid-operation.
5757
+ const caps = resolveCapabilities(this.config);
5741
5758
  const abortController = new AbortController();
5742
5759
  const onAbort = () => {
5743
5760
  abortController.abort();
@@ -5813,7 +5830,7 @@ export class Orchestrator {
5813
5830
  const recallPromise = this.recallInternal(prompt, sessionKey, {
5814
5831
  ...options,
5815
5832
  abortSignal: abortController.signal,
5816
- });
5833
+ }, caps);
5817
5834
  const RECALL_TIMEOUT_MS = this.config.recallOuterTimeoutMs ?? 75_000;
5818
5835
  if (RECALL_TIMEOUT_MS <= 0) {
5819
5836
  return await recallPromise;
@@ -5837,13 +5854,14 @@ export class Orchestrator {
5837
5854
  // Observation-mode direct-answer tier (issue #518 slice 3c).
5838
5855
  // Runs after the user's recall already succeeded, fire-and-forget,
5839
5856
  // so annotation latency can never delay the caller's response.
5840
- if (this.config.recallDirectAnswerEnabled && sessionKey) {
5857
+ if (caps.recallDirectAnswer && sessionKey) {
5841
5858
  try {
5842
5859
  this.enqueueDirectAnswerObservation(
5843
5860
  prompt,
5844
5861
  sessionKey,
5845
5862
  options.namespace?.trim() || undefined,
5846
5863
  options.principalOverride,
5864
+ caps,
5847
5865
  );
5848
5866
  } catch (err) {
5849
5867
  log.debug(`direct-answer observation setup failed: ${err}`);
@@ -5912,6 +5930,7 @@ export class Orchestrator {
5912
5930
  sessionKey: string,
5913
5931
  namespaceOverride: string | undefined,
5914
5932
  principalOverride: string | undefined,
5933
+ caps: CapabilitySet,
5915
5934
  ): void {
5916
5935
  const expectedSnapshot = this.lastRecall.get(sessionKey);
5917
5936
  if (expectedSnapshot === null) return;
@@ -5992,6 +6011,7 @@ export class Orchestrator {
5992
6011
  sessionKey,
5993
6012
  observationNamespaces,
5994
6013
  expectedIdentity,
6014
+ caps,
5995
6015
  undefined,
5996
6016
  );
5997
6017
  } catch (err) {
@@ -6007,6 +6027,7 @@ export class Orchestrator {
6007
6027
  expectedIdentity:
6008
6028
  | { writeNonce?: string; traceId?: string; recordedAt?: string }
6009
6029
  | undefined,
6030
+ caps: CapabilitySet,
6010
6031
  _parentAbortSignal?: AbortSignal,
6011
6032
  ): Promise<void> {
6012
6033
  const tierStart = Date.now();
@@ -6091,6 +6112,7 @@ export class Orchestrator {
6091
6112
  query: prompt,
6092
6113
  namespace: ns,
6093
6114
  config: this.config,
6115
+ enabled: caps.recallDirectAnswer,
6094
6116
  sources,
6095
6117
  });
6096
6118
  if (r.eligible && r.winner) {
@@ -7273,10 +7295,22 @@ export class Orchestrator {
7273
7295
  };
7274
7296
  }
7275
7297
 
7298
+ /**
7299
+ * Clock source for the shared post-retrieval assembly/enrichment budget. The
7300
+ * deadline is set from this value and every expiry check reads it back, so a
7301
+ * test can drive the budget deterministically instead of racing the few-ms
7302
+ * wall-clock window that made the "skips … after budget expires" tests flaky.
7303
+ * Production behavior is unchanged — it returns the wall clock.
7304
+ */
7305
+ protected recallAssemblyClockMs(): number {
7306
+ return Date.now();
7307
+ }
7308
+
7276
7309
  private async recallInternal(
7277
7310
  prompt: string,
7278
7311
  sessionKey?: string,
7279
7312
  options: RecallInvocationOptions = {},
7313
+ caps: CapabilitySet = resolveCapabilities(this.config),
7280
7314
  ): Promise<string> {
7281
7315
  const recallStart = Date.now();
7282
7316
  // Backend degradations observed by this recall's QMD searches (#1536):
@@ -7436,11 +7470,10 @@ export class Orchestrator {
7436
7470
  let identityInjectionTruncated = false;
7437
7471
  timings.queryPolicy = `${queryPolicy.promptShape}/${queryPolicy.retrievalBudgetMode}${queryPolicy.skipConversationRecall ? "/skip-conv" : ""}`;
7438
7472
  const recallModeDecisionOptions = {
7439
- plannerEnabled: this.config.recallPlannerEnabled,
7440
- graphRecallEnabled: this.config.graphRecallEnabled,
7473
+ plannerEnabled: caps.recallPlanner,
7474
+ graphRecallEnabled: caps.graphRecall,
7441
7475
  multiGraphMemoryEnabled: this.config.multiGraphMemoryEnabled,
7442
- graphExpandedIntentEnabled:
7443
- this.config.graphExpandedIntentEnabled === true,
7476
+ graphExpandedIntentEnabled: caps.graphExpandedIntent,
7444
7477
  prompt,
7445
7478
  };
7446
7479
  const requestedMode = options.mode;
@@ -7454,6 +7487,7 @@ export class Orchestrator {
7454
7487
  : await resolveRecallModeDecisionAsync({
7455
7488
  ...recallModeDecisionOptions,
7456
7489
  config: this.config,
7490
+ caps,
7457
7491
  signal: options.abortSignal,
7458
7492
  });
7459
7493
  if (
@@ -7779,7 +7813,7 @@ export class Orchestrator {
7779
7813
  promptLength: prompt.length,
7780
7814
  retrievalQueryHash,
7781
7815
  retrievalQueryLength: retrievalQuery.length,
7782
- plannerEnabled: this.config.recallPlannerEnabled,
7816
+ plannerEnabled: caps.recallPlanner,
7783
7817
  plannedMode: requestedMode ?? recallDecision.plannedMode,
7784
7818
  effectiveMode: recallMode,
7785
7819
  recallResultLimit,
@@ -7790,7 +7824,7 @@ export class Orchestrator {
7790
7824
  reason: graphDecisionReason,
7791
7825
  shadowMode: graphDecisionShadowMode,
7792
7826
  qmdAvailable,
7793
- graphRecallEnabled: this.config.graphRecallEnabled,
7827
+ graphRecallEnabled: caps.graphRecall,
7794
7828
  multiGraphMemoryEnabled: this.config.multiGraphMemoryEnabled,
7795
7829
  },
7796
7830
  });
@@ -10238,7 +10272,7 @@ export class Orchestrator {
10238
10272
 
10239
10273
  const enrichmentAssemblyDeadlineAtMs =
10240
10274
  enrichmentSectionDeadlineMs > 0
10241
- ? Date.now() + enrichmentSectionDeadlineMs
10275
+ ? this.recallAssemblyClockMs() + enrichmentSectionDeadlineMs
10242
10276
  : null;
10243
10277
 
10244
10278
  const awaitEnrichmentSection = async <T>(
@@ -10280,7 +10314,7 @@ export class Orchestrator {
10280
10314
  const timeoutMs =
10281
10315
  enrichmentAssemblyDeadlineAtMs === null
10282
10316
  ? null
10283
- : Math.max(0, enrichmentAssemblyDeadlineAtMs - Date.now());
10317
+ : Math.max(0, enrichmentAssemblyDeadlineAtMs - this.recallAssemblyClockMs());
10284
10318
  if (timeoutMs === 0) {
10285
10319
  const settledOutcome = promise.getSettledOutcome();
10286
10320
  if (settledOutcome) {
@@ -10326,7 +10360,7 @@ export class Orchestrator {
10326
10360
  const remainingEnrichmentAssemblyMs = (): number | null =>
10327
10361
  enrichmentAssemblyDeadlineAtMs === null
10328
10362
  ? null
10329
- : Math.max(0, enrichmentAssemblyDeadlineAtMs - Date.now());
10363
+ : Math.max(0, enrichmentAssemblyDeadlineAtMs - this.recallAssemblyClockMs());
10330
10364
 
10331
10365
  const awaitAssemblyStep = async <T>(
10332
10366
  name: string,
@@ -11012,7 +11046,7 @@ export class Orchestrator {
11012
11046
 
11013
11047
  const isFullModeGraphAssist =
11014
11048
  this.config.multiGraphMemoryEnabled &&
11015
- this.config.graphAssistInFullModeEnabled !== false &&
11049
+ caps.graphAssistInFullMode &&
11016
11050
  recallMode === "full" &&
11017
11051
  memoryResults.length >=
11018
11052
  Math.max(1, this.config.graphAssistMinSeedResults ?? 3);
@@ -11192,7 +11226,7 @@ export class Orchestrator {
11192
11226
  timeoutMs: this.config.rerankTimeoutMs,
11193
11227
  maxCandidates: this.config.rerankMaxCandidates,
11194
11228
  cache: this.rerankCache,
11195
- cacheEnabled: this.config.rerankCacheEnabled,
11229
+ cacheEnabled: caps.rerankCache,
11196
11230
  cacheTtlMs: this.config.rerankCacheTtlMs,
11197
11231
  });
11198
11232
  if (ranked && ranked.length > 0) {
@@ -11222,7 +11256,7 @@ export class Orchestrator {
11222
11256
  // flips the default once bench shows tie-or-win. Fail-open: any
11223
11257
  // lookup error leaves the original scores untouched rather than
11224
11258
  // breaking recall for the whole namespace.
11225
- if (this.config.recallMemoryWorthFilterEnabled && memoryResults.length > 0) {
11259
+ if (caps.recallMemoryWorthFilter && memoryResults.length > 0) {
11226
11260
  try {
11227
11261
  memoryResults = await this.applyMemoryWorthRerank(memoryResults, recallNamespaces);
11228
11262
  } catch (err) {
@@ -11260,7 +11294,7 @@ export class Orchestrator {
11260
11294
  memoryResults.length,
11261
11295
  );
11262
11296
  let confidenceGateRejected = false;
11263
- if (this.config.recallConfidenceGateEnabled && effectiveGateScore > 0) {
11297
+ if (caps.recallConfidenceGate && effectiveGateScore > 0) {
11264
11298
  if (effectiveGateScore < this.config.recallConfidenceGateThreshold) {
11265
11299
  log.debug(
11266
11300
  `recall: confidence gate rejected ${memoryResults.length} results (effective score ${effectiveGateScore.toFixed(3)} below ${this.config.recallConfidenceGateThreshold})`,
@@ -11278,6 +11312,7 @@ export class Orchestrator {
11278
11312
  memoryResults,
11279
11313
  recallResultLimit,
11280
11314
  retrievalQuery,
11315
+ caps,
11281
11316
  );
11282
11317
 
11283
11318
  // E-Mem-inspired memory reconstruction: fill gaps for referenced entities
@@ -11394,6 +11429,7 @@ export class Orchestrator {
11394
11429
  boostedScoped,
11395
11430
  recallResultLimit,
11396
11431
  retrievalQuery,
11432
+ caps,
11397
11433
  );
11398
11434
  },
11399
11435
  [] as QmdSearchResult[],
@@ -11431,6 +11467,7 @@ export class Orchestrator {
11431
11467
  recallNamespaces,
11432
11468
  recallResultLimit,
11433
11469
  recallMode,
11470
+ caps,
11434
11471
  queryAwarePrefilter,
11435
11472
  abortSignal: options.abortSignal,
11436
11473
  onDegradation: (degradation) => {
@@ -11551,6 +11588,7 @@ export class Orchestrator {
11551
11588
  boostedScoped,
11552
11589
  recallResultLimit,
11553
11590
  retrievalQuery,
11591
+ caps,
11554
11592
  );
11555
11593
  },
11556
11594
  [] as QmdSearchResult[],
@@ -11656,6 +11694,7 @@ export class Orchestrator {
11656
11694
  recallNamespaces,
11657
11695
  recallResultLimit,
11658
11696
  recallMode,
11697
+ caps,
11659
11698
  queryAwarePrefilter,
11660
11699
  abortSignal: options.abortSignal,
11661
11700
  onDegradation: (degradation) => {
@@ -11730,6 +11769,7 @@ export class Orchestrator {
11730
11769
  boostedRecent,
11731
11770
  recallResultLimit,
11732
11771
  retrievalQuery,
11772
+ caps,
11733
11773
  );
11734
11774
  },
11735
11775
  [] as QmdSearchResult[],
@@ -11768,6 +11808,7 @@ export class Orchestrator {
11768
11808
  recallNamespaces,
11769
11809
  recallResultLimit,
11770
11810
  recallMode,
11811
+ caps,
11771
11812
  queryAwarePrefilter,
11772
11813
  abortSignal: options.abortSignal,
11773
11814
  onDegradation: (degradation) => {
@@ -11815,6 +11856,7 @@ export class Orchestrator {
11815
11856
  recallNamespaces,
11816
11857
  recallResultLimit,
11817
11858
  recallMode,
11859
+ caps,
11818
11860
  queryAwarePrefilter,
11819
11861
  abortSignal: options.abortSignal,
11820
11862
  onDegradation: (degradation) => {
@@ -18361,6 +18403,11 @@ export class Orchestrator {
18361
18403
  results: QmdSearchResult[],
18362
18404
  limit: number,
18363
18405
  retrievalQuery?: string,
18406
+ // `caps` is additive AND last (issue #1523) so the positional call shape
18407
+ // stays backward-compatible: the recall pipeline threads a resolved set,
18408
+ // but callers that omit it (e.g. direct unit-test invocations) get an
18409
+ // equivalent set derived from the same config — behavior-preserving.
18410
+ caps: CapabilitySet = resolveCapabilities(this.config),
18364
18411
  ): QmdSearchResult[] {
18365
18412
  const safeLimit =
18366
18413
  typeof limit === "number" && Number.isFinite(limit)
@@ -18377,13 +18424,13 @@ export class Orchestrator {
18377
18424
  // facts/decisions before MMR picks the final section. No-op when the
18378
18425
  // flag is off or the query is not a problem-solving ask.
18379
18426
  const boosted =
18380
- this.config.recallReasoningTraceBoostEnabled && typeof retrievalQuery === "string"
18427
+ caps.recallReasoningTraceBoost && typeof retrievalQuery === "string"
18381
18428
  ? applyReasoningTraceBoost(results, {
18382
18429
  enabled: true,
18383
18430
  query: retrievalQuery,
18384
18431
  })
18385
18432
  : results;
18386
- const diversified = this.applyMmrToQmdResults(sectionId, boosted);
18433
+ const diversified = this.applyMmrToQmdResults(sectionId, boosted, caps);
18387
18434
  return diversified.slice(0, safeLimit);
18388
18435
  }
18389
18436
 
@@ -18398,8 +18445,11 @@ export class Orchestrator {
18398
18445
  private applyMmrToQmdResults(
18399
18446
  sectionId: string,
18400
18447
  results: QmdSearchResult[],
18448
+ // Additive `caps` (issue #1523); defaults to a config-derived set so direct
18449
+ // callers that omit it behave identically to the threaded recall path.
18450
+ caps: CapabilitySet = resolveCapabilities(this.config),
18401
18451
  ): QmdSearchResult[] {
18402
- if (this.config.recallMmrEnabled === false) return results;
18452
+ if (!caps.recallMmr) return results;
18403
18453
  if (!Array.isArray(results) || results.length < 2) return results;
18404
18454
 
18405
18455
  // Config is runtime API (see AGENTS.md §4): preserve `0` as a true zero
@@ -18698,6 +18748,13 @@ export class Orchestrator {
18698
18748
  recallNamespaces: string[];
18699
18749
  recallResultLimit: number;
18700
18750
  recallMode: RecallPlanMode;
18751
+ /**
18752
+ * Recall-operation capability gates resolved once at recall entry (#1523).
18753
+ * OPTIONAL and additive: the recall pipeline threads a resolved set, but
18754
+ * callers that omit it (e.g. direct unit-test invocations) get an
18755
+ * equivalent config-derived set — behavior-preserving.
18756
+ */
18757
+ caps?: CapabilitySet;
18701
18758
  queryAwarePrefilter?: QueryAwarePrefilter;
18702
18759
  abortSignal?: AbortSignal;
18703
18760
  /** Backend degradation observer — cold-tier QMD must report like hot (#1536). */
@@ -18717,6 +18774,9 @@ export class Orchestrator {
18717
18774
  /** Issue #681 — when true, bypass graphTraversalConfidenceFloor. */
18718
18775
  includeLowConfidence?: boolean;
18719
18776
  }): Promise<QmdSearchResult[]> {
18777
+ // Prefer the threaded set; fall back to a config-derived set so direct
18778
+ // callers (unit tests) behave identically to the recall pipeline (#1523).
18779
+ const caps = options.caps ?? resolveCapabilities(this.config);
18720
18780
  if (options.queryAwarePrefilter?.candidatePaths?.size === 0) {
18721
18781
  if (options.xrayPoolSizeSink) options.xrayPoolSizeSink.size = 0;
18722
18782
  return [];
@@ -18932,7 +18992,7 @@ export class Orchestrator {
18932
18992
  const isFullModeGraphAssist =
18933
18993
  this.config.qmdTierParityGraphEnabled &&
18934
18994
  this.config.multiGraphMemoryEnabled &&
18935
- this.config.graphAssistInFullModeEnabled !== false &&
18995
+ caps.graphAssistInFullMode &&
18936
18996
  options.recallMode === "full" &&
18937
18997
  results.length >= Math.max(1, this.config.graphAssistMinSeedResults ?? 3);
18938
18998
  const shouldRunGraphExpansion =
@@ -19028,7 +19088,7 @@ export class Orchestrator {
19028
19088
  timeoutMs: this.config.rerankTimeoutMs,
19029
19089
  maxCandidates: this.config.rerankMaxCandidates,
19030
19090
  cache: this.rerankCache,
19031
- cacheEnabled: this.config.rerankCacheEnabled,
19091
+ cacheEnabled: caps.rerankCache,
19032
19092
  cacheTtlMs: this.config.rerankCacheTtlMs,
19033
19093
  });
19034
19094
  if (ranked && ranked.length > 0) {
@@ -19054,7 +19114,7 @@ export class Orchestrator {
19054
19114
  // Memory Worth filter — must fire on the cold fallback path too, or the
19055
19115
  // feature flag produces divergent behavior by retrieval path (CLAUDE.md
19056
19116
  // rule 39). Fail-open on lookup errors.
19057
- if (this.config.recallMemoryWorthFilterEnabled && results.length > 0) {
19117
+ if (caps.recallMemoryWorthFilter && results.length > 0) {
19058
19118
  try {
19059
19119
  results = await this.applyMemoryWorthRerank(results, options.recallNamespaces);
19060
19120
  } catch (err) {
@@ -19079,6 +19139,7 @@ export class Orchestrator {
19079
19139
  results,
19080
19140
  options.recallResultLimit,
19081
19141
  options.prompt,
19142
+ caps,
19082
19143
  );
19083
19144
  }
19084
19145