@remnic/core 9.3.665 → 9.3.666

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 (149) 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-DsS-TatL.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-7C4MPEPE.js → chunk-4T7P2HLJ.js} +3 -3
  30. package/dist/{chunk-7XH7VJN4.js → chunk-6T4LTI2F.js} +4 -4
  31. package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
  32. package/dist/{chunk-BZG2CWOQ.js → chunk-A5TEHAR4.js} +3 -3
  33. package/dist/{chunk-C7AF236A.js → chunk-AARDBQTA.js} +2 -2
  34. package/dist/{chunk-IHG6CC7T.js → chunk-BQJUPECT.js} +2 -2
  35. package/dist/{chunk-7OGJQP7T.js → chunk-CRO4LCQ6.js} +5 -5
  36. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  37. package/dist/{chunk-WH4SKYPX.js → chunk-GZ6QAYSH.js} +94 -74
  38. package/dist/chunk-GZ6QAYSH.js.map +1 -0
  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-BypxcNqq.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-DZqPVoMI.d.ts} +8 -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 +42 -42
  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/verified-recall.js +4 -3
  109. package/package.json +1 -1
  110. package/src/capabilities.test.ts +97 -0
  111. package/src/capabilities.ts +86 -0
  112. package/src/direct-answer-wiring.test.ts +53 -2
  113. package/src/direct-answer-wiring.ts +18 -5
  114. package/src/orchestrator.ts +69 -19
  115. package/src/recall-planner-llm.test.ts +12 -11
  116. package/src/recall-planner-llm.ts +7 -1
  117. package/src/storage-fallback-category-dirs.test.ts +150 -1
  118. package/src/storage.ts +51 -14
  119. package/dist/chunk-SQZ42MKH.js.map +0 -1
  120. package/dist/chunk-VH6EIKVS.js.map +0 -1
  121. package/dist/chunk-WH4SKYPX.js.map +0 -1
  122. /package/dist/{chunk-ROHLEUTH.js.map → chunk-23EBQ27U.js.map} +0 -0
  123. /package/dist/{chunk-YW52BQSU.js.map → chunk-2TCHDANJ.js.map} +0 -0
  124. /package/dist/{chunk-IROWLAWG.js.map → chunk-46WUVFOD.js.map} +0 -0
  125. /package/dist/{chunk-7C4MPEPE.js.map → chunk-4T7P2HLJ.js.map} +0 -0
  126. /package/dist/{chunk-7XH7VJN4.js.map → chunk-6T4LTI2F.js.map} +0 -0
  127. /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
  128. /package/dist/{chunk-BZG2CWOQ.js.map → chunk-A5TEHAR4.js.map} +0 -0
  129. /package/dist/{chunk-C7AF236A.js.map → chunk-AARDBQTA.js.map} +0 -0
  130. /package/dist/{chunk-IHG6CC7T.js.map → chunk-BQJUPECT.js.map} +0 -0
  131. /package/dist/{chunk-7OGJQP7T.js.map → chunk-CRO4LCQ6.js.map} +0 -0
  132. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  133. /package/dist/{chunk-UXA5L2DZ.js.map → chunk-HQCGRSRU.js.map} +0 -0
  134. /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
  135. /package/dist/{chunk-XW3W4PV4.js.map → chunk-JTPXSXHC.js.map} +0 -0
  136. /package/dist/{chunk-OHJFJ4HI.js.map → chunk-KOXGLQS7.js.map} +0 -0
  137. /package/dist/{chunk-2OPARZ4B.js.map → chunk-MPXYHC35.js.map} +0 -0
  138. /package/dist/{chunk-6JBKHTQD.js.map → chunk-MR4PJ277.js.map} +0 -0
  139. /package/dist/{chunk-EXXBA5OM.js.map → chunk-OI4BXFSB.js.map} +0 -0
  140. /package/dist/{chunk-2HEZXPYU.js.map → chunk-Q2LQZYQ7.js.map} +0 -0
  141. /package/dist/{chunk-XRSIGVTS.js.map → chunk-QHWJG5C5.js.map} +0 -0
  142. /package/dist/{chunk-T2AN3BSP.js.map → chunk-QZ7ODIVL.js.map} +0 -0
  143. /package/dist/{chunk-D7IXTY5E.js.map → chunk-TJ7HH5LB.js.map} +0 -0
  144. /package/dist/{chunk-V25ZAOSB.js.map → chunk-UOBLE67F.js.map} +0 -0
  145. /package/dist/{chunk-JIX3ZL2J.js.map → chunk-UVUTV7CM.js.map} +0 -0
  146. /package/dist/{chunk-SSOMTUCA.js.map → chunk-WXGTC424.js.map} +0 -0
  147. /package/dist/{chunk-KHGE6PMF.js.map → chunk-WXXLSZHA.js.map} +0 -0
  148. /package/dist/{chunk-DSLUOQDY.js.map → chunk-XMWF6AU3.js.map} +0 -0
  149. /package/dist/{chunk-DQY7NJ5L.js.map → chunk-XS2CWEHZ.js.map} +0 -0
@@ -5,11 +5,11 @@ import {
5
5
  import "../chunk-WEHSQBFR.js";
6
6
  import "../chunk-X7Y7WX73.js";
7
7
  import "../chunk-J4EB7DNW.js";
8
+ import "../chunk-UI3NYK34.js";
9
+ import "../chunk-GCGJW34D.js";
8
10
  import "../chunk-BJMBJZ2Y.js";
9
11
  import "../chunk-UKJAGEXH.js";
10
12
  import "../chunk-FP2373TW.js";
11
- import "../chunk-UI3NYK34.js";
12
- import "../chunk-GCGJW34D.js";
13
13
  import "../chunk-A6XUJE5D.js";
14
14
  import "../chunk-PZ5AY32C.js";
15
15
  export {
@@ -4,10 +4,10 @@ import {
4
4
  import "../chunk-VF4XKTX3.js";
5
5
  import "../chunk-WEHSQBFR.js";
6
6
  import "../chunk-X7Y7WX73.js";
7
+ import "../chunk-GCGJW34D.js";
7
8
  import "../chunk-BJMBJZ2Y.js";
8
9
  import "../chunk-UKJAGEXH.js";
9
10
  import "../chunk-FP2373TW.js";
10
- import "../chunk-GCGJW34D.js";
11
11
  import "../chunk-A6XUJE5D.js";
12
12
  import "../chunk-VS2IYZRU.js";
13
13
  import "../chunk-PZ5AY32C.js";
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  compareVerifiedEpisodeResults,
3
3
  searchVerifiedEpisodes
4
- } from "./chunk-YW52BQSU.js";
4
+ } from "./chunk-2TCHDANJ.js";
5
5
  import "./chunk-HQ6NIBL6.js";
6
- import "./chunk-VH6EIKVS.js";
6
+ import "./chunk-WKMCC4NQ.js";
7
7
  import "./chunk-M7XQSUBB.js";
8
8
  import "./chunk-5UZXUTVO.js";
9
+ import "./chunk-5GPPACXK.js";
9
10
  import "./chunk-J6A3CX5N.js";
10
11
  import "./chunk-AZBV4RRY.js";
11
12
  import "./chunk-AWJ2FHCF.js";
@@ -17,8 +18,8 @@ import "./chunk-6KYMPV2O.js";
17
18
  import "./chunk-DM2T26WE.js";
18
19
  import "./chunk-LDXUBPMO.js";
19
20
  import "./chunk-FVQJYWH7.js";
20
- import "./chunk-G7D6GZ5J.js";
21
21
  import "./chunk-VF4XKTX3.js";
22
+ import "./chunk-G7D6GZ5J.js";
22
23
  import "./chunk-4DJQYKMN.js";
23
24
  import "./chunk-ZBJMUXZH.js";
24
25
  import "./chunk-2ODBA7MQ.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.665",
3
+ "version": "9.3.666",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,97 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseConfig } from "./config.js";
5
+ import { resolveCapabilities, type CapabilitySet } from "./capabilities.js";
6
+
7
+ /**
8
+ * Characterization tests for the recall-operation CapabilitySet (issue #1523).
9
+ *
10
+ * These guard against composition drift: every capability field must project
11
+ * from its `<field>Enabled` config flag, so a future edit to
12
+ * `resolveCapabilities` that accidentally maps a field to the wrong flag (the
13
+ * rule-39 gate-divergence class, moved up one layer) fails loudly here.
14
+ */
15
+
16
+ /**
17
+ * Map of CapabilitySet field → the PluginConfig flag it projects from.
18
+ * Kept explicit (rather than derived by string concat) so the two graph flags
19
+ * with non-`<field>Enabled` names are covered too.
20
+ */
21
+ const FIELD_TO_FLAG: Record<keyof CapabilitySet, string> = {
22
+ rerankCache: "rerankCacheEnabled",
23
+ recallDirectAnswer: "recallDirectAnswerEnabled",
24
+ recallMemoryWorthFilter: "recallMemoryWorthFilterEnabled",
25
+ recallMmr: "recallMmrEnabled",
26
+ recallReasoningTraceBoost: "recallReasoningTraceBoostEnabled",
27
+ recallPlannerLlm: "recallPlannerLlmEnabled",
28
+ recallPlanner: "recallPlannerEnabled",
29
+ recallConfidenceGate: "recallConfidenceGateEnabled",
30
+ graphRecall: "graphRecallEnabled",
31
+ graphAssistInFullMode: "graphAssistInFullModeEnabled",
32
+ graphExpandedIntent: "graphExpandedIntentEnabled",
33
+ };
34
+
35
+ const FIELDS = Object.keys(FIELD_TO_FLAG) as Array<keyof CapabilitySet>;
36
+
37
+ test("resolveCapabilities projects every field from its <field>Enabled flag (true variant)", () => {
38
+ // Build a config where every migrated flag is explicitly true.
39
+ const overrides: Record<string, boolean> = {};
40
+ for (const flag of Object.values(FIELD_TO_FLAG)) overrides[flag] = true;
41
+ const config = parseConfig(overrides);
42
+ const caps = resolveCapabilities(config);
43
+
44
+ for (const field of FIELDS) {
45
+ const flag = FIELD_TO_FLAG[field];
46
+ assert.equal(
47
+ caps[field],
48
+ (config as unknown as Record<string, boolean>)[flag],
49
+ `caps.${field} must equal config.${flag} (true variant)`,
50
+ );
51
+ assert.equal(caps[field], true, `caps.${field} should be true here`);
52
+ }
53
+ });
54
+
55
+ test("resolveCapabilities projects every field from its <field>Enabled flag (false variant)", () => {
56
+ const overrides: Record<string, boolean> = {};
57
+ for (const flag of Object.values(FIELD_TO_FLAG)) overrides[flag] = false;
58
+ const config = parseConfig(overrides);
59
+ const caps = resolveCapabilities(config);
60
+
61
+ for (const field of FIELDS) {
62
+ const flag = FIELD_TO_FLAG[field];
63
+ // The two optional graph flags carry default-when-undefined semantics, but
64
+ // when explicitly set to a concrete boolean the projection must match it.
65
+ assert.equal(
66
+ caps[field],
67
+ (config as unknown as Record<string, boolean>)[flag],
68
+ `caps.${field} must equal config.${flag} (false variant)`,
69
+ );
70
+ assert.equal(caps[field], false, `caps.${field} should be false here`);
71
+ }
72
+ });
73
+
74
+ test("resolveCapabilities preserves optional-flag defaults when the flag is undefined", () => {
75
+ // parseConfig with no overrides exercises the documented defaults. The two
76
+ // optional graph flags encode asymmetric defaults on purpose:
77
+ // graphAssistInFullModeEnabled → default-ON (`!== false`)
78
+ // graphExpandedIntentEnabled → default-OFF (`=== true`)
79
+ const config = parseConfig({});
80
+ const caps = resolveCapabilities(config);
81
+
82
+ assert.equal(
83
+ caps.graphAssistInFullMode,
84
+ config.graphAssistInFullModeEnabled !== false,
85
+ "graphAssistInFullMode must be default-on unless explicitly false",
86
+ );
87
+ assert.equal(
88
+ caps.graphExpandedIntent,
89
+ config.graphExpandedIntentEnabled === true,
90
+ "graphExpandedIntent must be default-off unless explicitly true",
91
+ );
92
+ });
93
+
94
+ test("resolveCapabilities returns a frozen object", () => {
95
+ const caps = resolveCapabilities(parseConfig({}));
96
+ assert.equal(Object.isFrozen(caps), true, "CapabilitySet must be frozen");
97
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * CapabilitySet — recall-operation feature gates resolved once, then threaded.
3
+ *
4
+ * Issue #1523 (Phase 1 of epic #1520). Root cause this addresses: 161+
5
+ * scattered `config.<flag>Enabled` reads mean each gate is re-derived at every
6
+ * call site, and reviews keep finding parallel code paths where one branch
7
+ * checks a gate the other forgot (CLAUDE.md rule 39 — the "gate divergence"
8
+ * defect class). The fix is to resolve a frozen capability projection ONCE at
9
+ * the top of the recall operation and pass it down explicitly.
10
+ *
11
+ * Scope of THIS module (first migration PR): only the recall-operation-scoped
12
+ * flags below. Flags that are also read in graph construction, writes, CLI, or
13
+ * the summarizer are deliberately deferred to a follow-up so we never leave a
14
+ * single flag half-migrated (some sites on `caps.`, some on `config.`).
15
+ *
16
+ * Field naming: each field is the config flag name with the trailing `Enabled`
17
+ * removed (`recallMmrEnabled` → `recallMmr`).
18
+ *
19
+ * This is plumbing, not a feature — there is deliberately NO `enabled` gate for
20
+ * the CapabilitySet itself (rule 30 governs behavior changes; resolving and
21
+ * threading a capability projection must stay behavior-preserving).
22
+ */
23
+
24
+ import type { PluginConfig } from "./types.js";
25
+
26
+ /**
27
+ * Frozen projection of recall-operation feature gates.
28
+ *
29
+ * Every field is `readonly boolean`. The composition that maps a config flag to
30
+ * a capability (including default-when-undefined semantics for optional flags)
31
+ * lives ONLY in {@link resolveCapabilities} — call sites must read the
32
+ * capability, never re-derive it from raw config.
33
+ */
34
+ export interface CapabilitySet {
35
+ /** `rerankCacheEnabled` — cache reranker scores across recall passes. */
36
+ readonly rerankCache: boolean;
37
+ /** `recallDirectAnswerEnabled` — observation-mode direct-answer tier. */
38
+ readonly recallDirectAnswer: boolean;
39
+ /** `recallMemoryWorthFilterEnabled` — Memory-Worth score reweighting. */
40
+ readonly recallMemoryWorthFilter: boolean;
41
+ /** `recallMmrEnabled` — maximal-marginal-relevance diversification. */
42
+ readonly recallMmr: boolean;
43
+ /** `recallReasoningTraceBoostEnabled` — boost reasoning-trace memories. */
44
+ readonly recallReasoningTraceBoost: boolean;
45
+ /** `recallPlannerLlmEnabled` — LLM-backed recall-mode planner. */
46
+ readonly recallPlannerLlm: boolean;
47
+ /** `recallPlannerEnabled` — recall-mode planner (heuristic + optional LLM). */
48
+ readonly recallPlanner: boolean;
49
+ /** `recallConfidenceGateEnabled` — Synapse-style confidence gate. */
50
+ readonly recallConfidenceGate: boolean;
51
+ /** `graphRecallEnabled` — graph-mode recall tier (gates planner graph mode). */
52
+ readonly graphRecall: boolean;
53
+ /** `graphAssistInFullModeEnabled` — graph-assist overlay in full mode. */
54
+ readonly graphAssistInFullMode: boolean;
55
+ /** `graphExpandedIntentEnabled` — promote broad-intent asks to graph mode. */
56
+ readonly graphExpandedIntent: boolean;
57
+ }
58
+
59
+ /**
60
+ * Resolve the recall-operation {@link CapabilitySet} from parsed config.
61
+ *
62
+ * Call this ONCE per recall operation (at the `recall()` / `recallInternal`
63
+ * entry) and thread the result down. Composition lives here and only here.
64
+ *
65
+ * Session toggles are intentionally not a parameter yet: `session-toggles.ts`
66
+ * is agent-scoped (per session/agent enable-disable of the whole plugin), not
67
+ * flag-scoped — none of the flags projected here have a per-session override,
68
+ * so there is nothing for a toggle argument to compose at this layer.
69
+ */
70
+ export function resolveCapabilities(config: PluginConfig): CapabilitySet {
71
+ return Object.freeze({
72
+ rerankCache: config.rerankCacheEnabled,
73
+ recallDirectAnswer: config.recallDirectAnswerEnabled,
74
+ recallMemoryWorthFilter: config.recallMemoryWorthFilterEnabled,
75
+ recallMmr: config.recallMmrEnabled,
76
+ recallReasoningTraceBoost: config.recallReasoningTraceBoostEnabled,
77
+ recallPlannerLlm: config.recallPlannerLlmEnabled,
78
+ recallPlanner: config.recallPlannerEnabled,
79
+ recallConfidenceGate: config.recallConfidenceGateEnabled,
80
+ graphRecall: config.graphRecallEnabled,
81
+ // Optional flags: preserve the exact default-when-undefined semantics the
82
+ // migrated call sites used (`!== false` = default-on, `=== true` = default-off).
83
+ graphAssistInFullMode: config.graphAssistInFullModeEnabled !== false,
84
+ graphExpandedIntent: config.graphExpandedIntentEnabled === true,
85
+ });
86
+ }
@@ -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,