@remnic/core 9.3.715 → 9.3.716

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 (38) hide show
  1. package/dist/access-boundary.d.ts +2 -2
  2. package/dist/access-cli.js +2 -2
  3. package/dist/access-http.d.ts +2 -2
  4. package/dist/access-mcp.d.ts +2 -2
  5. package/dist/access-operations.d.ts +2 -2
  6. package/dist/{access-service-GgnA--CE.d.ts → access-service-BW8cm_uU.d.ts} +1 -1
  7. package/dist/access-service.d.ts +2 -2
  8. package/dist/access-surface-catalog.d.ts +2 -2
  9. package/dist/bootstrap.d.ts +1 -1
  10. package/dist/{chunk-OQH5XUH3.js → chunk-Q6IP2ARR.js} +2 -5
  11. package/dist/chunk-Q6IP2ARR.js.map +1 -0
  12. package/dist/{chunk-YUJL27ET.js → chunk-SQ4LU27I.js} +5 -5
  13. package/dist/chunk-SQ4LU27I.js.map +1 -0
  14. package/dist/{cli-Do1YtfnH.d.ts → cli-Dpivy32W.d.ts} +2 -2
  15. package/dist/cli.d.ts +3 -3
  16. package/dist/direct-answer-wiring.d.ts +9 -9
  17. package/dist/direct-answer-wiring.js +1 -1
  18. package/dist/explicit-capture.d.ts +1 -1
  19. package/dist/index.d.ts +4 -4
  20. package/dist/index.js +2 -2
  21. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  22. package/dist/{orchestrator-BOaVstFG.d.ts → orchestrator-vygo2YIM.d.ts} +4 -5
  23. package/dist/orchestrator.d.ts +1 -1
  24. package/dist/orchestrator.js +2 -2
  25. package/dist/recall-planner-llm.d.ts +1 -1
  26. package/dist/recall-planner-llm.js +2 -2
  27. package/dist/recall-planner-llm.js.map +1 -1
  28. package/dist/schemas.d.ts +22 -22
  29. package/dist/transfer/types.d.ts +12 -12
  30. package/package.json +2 -2
  31. package/src/capabilities.test.ts +28 -0
  32. package/src/direct-answer-wiring.test.ts +20 -13
  33. package/src/direct-answer-wiring.ts +9 -13
  34. package/src/orchestrator.ts +7 -10
  35. package/src/recall-planner-llm.test.ts +30 -11
  36. package/src/recall-planner-llm.ts +4 -5
  37. package/dist/chunk-OQH5XUH3.js.map +0 -1
  38. package/dist/chunk-YUJL27ET.js.map +0 -1
@@ -313,13 +313,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
313
313
  peerProfiles: boolean;
314
314
  }>;
315
315
  }, "strip", z.ZodTypeAny, {
316
- schemaVersion: string;
317
316
  includes: {
318
317
  procedural: boolean;
319
318
  taxonomy: boolean;
320
319
  identityAnchors: boolean;
321
320
  peerProfiles: boolean;
322
321
  };
322
+ schemaVersion: string;
323
323
  id: string;
324
324
  version: string;
325
325
  description: string;
@@ -334,13 +334,13 @@ declare const CapsuleBlockSchema: z.ZodObject<{
334
334
  directAnswerEnabled: boolean;
335
335
  };
336
336
  }, {
337
- schemaVersion: string;
338
337
  includes: {
339
338
  procedural: boolean;
340
339
  taxonomy: boolean;
341
340
  identityAnchors: boolean;
342
341
  peerProfiles: boolean;
343
342
  };
343
+ schemaVersion: string;
344
344
  id: string;
345
345
  version: string;
346
346
  description: string;
@@ -464,13 +464,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
464
464
  peerProfiles: boolean;
465
465
  }>;
466
466
  }, "strip", z.ZodTypeAny, {
467
- schemaVersion: string;
468
467
  includes: {
469
468
  procedural: boolean;
470
469
  taxonomy: boolean;
471
470
  identityAnchors: boolean;
472
471
  peerProfiles: boolean;
473
472
  };
473
+ schemaVersion: string;
474
474
  id: string;
475
475
  version: string;
476
476
  description: string;
@@ -485,13 +485,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
485
485
  directAnswerEnabled: boolean;
486
486
  };
487
487
  }, {
488
- schemaVersion: string;
489
488
  includes: {
490
489
  procedural: boolean;
491
490
  taxonomy: boolean;
492
491
  identityAnchors: boolean;
493
492
  peerProfiles: boolean;
494
493
  };
494
+ schemaVersion: string;
495
495
  id: string;
496
496
  version: string;
497
497
  description: string;
@@ -518,13 +518,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
518
518
  pluginVersion: string;
519
519
  includesTranscripts: boolean;
520
520
  capsule: {
521
- schemaVersion: string;
522
521
  includes: {
523
522
  procedural: boolean;
524
523
  taxonomy: boolean;
525
524
  identityAnchors: boolean;
526
525
  peerProfiles: boolean;
527
526
  };
527
+ schemaVersion: string;
528
528
  id: string;
529
529
  version: string;
530
530
  description: string;
@@ -551,13 +551,13 @@ declare const ExportManifestV2Schema: z.ZodObject<{
551
551
  pluginVersion: string;
552
552
  includesTranscripts: boolean;
553
553
  capsule: {
554
- schemaVersion: string;
555
554
  includes: {
556
555
  procedural: boolean;
557
556
  taxonomy: boolean;
558
557
  identityAnchors: boolean;
559
558
  peerProfiles: boolean;
560
559
  };
560
+ schemaVersion: string;
561
561
  id: string;
562
562
  version: string;
563
563
  description: string;
@@ -683,13 +683,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
683
683
  peerProfiles: boolean;
684
684
  }>;
685
685
  }, "strip", z.ZodTypeAny, {
686
- schemaVersion: string;
687
686
  includes: {
688
687
  procedural: boolean;
689
688
  taxonomy: boolean;
690
689
  identityAnchors: boolean;
691
690
  peerProfiles: boolean;
692
691
  };
692
+ schemaVersion: string;
693
693
  id: string;
694
694
  version: string;
695
695
  description: string;
@@ -704,13 +704,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
704
704
  directAnswerEnabled: boolean;
705
705
  };
706
706
  }, {
707
- schemaVersion: string;
708
707
  includes: {
709
708
  procedural: boolean;
710
709
  taxonomy: boolean;
711
710
  identityAnchors: boolean;
712
711
  peerProfiles: boolean;
713
712
  };
713
+ schemaVersion: string;
714
714
  id: string;
715
715
  version: string;
716
716
  description: string;
@@ -737,13 +737,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
737
737
  pluginVersion: string;
738
738
  includesTranscripts: boolean;
739
739
  capsule: {
740
- schemaVersion: string;
741
740
  includes: {
742
741
  procedural: boolean;
743
742
  taxonomy: boolean;
744
743
  identityAnchors: boolean;
745
744
  peerProfiles: boolean;
746
745
  };
746
+ schemaVersion: string;
747
747
  id: string;
748
748
  version: string;
749
749
  description: string;
@@ -770,13 +770,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
770
770
  pluginVersion: string;
771
771
  includesTranscripts: boolean;
772
772
  capsule: {
773
- schemaVersion: string;
774
773
  includes: {
775
774
  procedural: boolean;
776
775
  taxonomy: boolean;
777
776
  identityAnchors: boolean;
778
777
  peerProfiles: boolean;
779
778
  };
779
+ schemaVersion: string;
780
780
  id: string;
781
781
  version: string;
782
782
  description: string;
@@ -819,13 +819,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
819
819
  pluginVersion: string;
820
820
  includesTranscripts: boolean;
821
821
  capsule: {
822
- schemaVersion: string;
823
822
  includes: {
824
823
  procedural: boolean;
825
824
  taxonomy: boolean;
826
825
  identityAnchors: boolean;
827
826
  peerProfiles: boolean;
828
827
  };
828
+ schemaVersion: string;
829
829
  id: string;
830
830
  version: string;
831
831
  description: string;
@@ -858,13 +858,13 @@ declare const ExportBundleV2Schema: z.ZodObject<{
858
858
  pluginVersion: string;
859
859
  includesTranscripts: boolean;
860
860
  capsule: {
861
- schemaVersion: string;
862
861
  includes: {
863
862
  procedural: boolean;
864
863
  taxonomy: boolean;
865
864
  identityAnchors: boolean;
866
865
  peerProfiles: boolean;
867
866
  };
867
+ schemaVersion: string;
868
868
  id: string;
869
869
  version: string;
870
870
  description: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.715",
3
+ "version": "9.3.716",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -2966,7 +2966,7 @@
2966
2966
  "core"
2967
2967
  ],
2968
2968
  "peerDependencies": {
2969
- "@remnic/coding-graph": "^9.3.715"
2969
+ "@remnic/coding-graph": "^9.3.716"
2970
2970
  },
2971
2971
  "peerDependenciesMeta": {
2972
2972
  "@remnic/coding-graph": {
@@ -96,6 +96,34 @@ test("resolveCapabilities returns a frozen object", () => {
96
96
  assert.equal(Object.isFrozen(caps), true, "CapabilitySet must be frozen");
97
97
  });
98
98
 
99
+ test("resolveCapabilities is deterministic — same config always yields identical gate values (one-resolution-per-op #1523)", () => {
100
+ // The one-resolution-per-op contract (issue #1523): resolve ONCE at the
101
+ // operation entry, thread the frozen result down. This test proves the
102
+ // resolution is deterministic — calling it twice with the same config
103
+ // produces identical values for every gate, so there is never a reason to
104
+ // re-resolve mid-operation.
105
+ const config = parseConfig({
106
+ rerankCacheEnabled: true,
107
+ recallDirectAnswerEnabled: false,
108
+ recallMmrEnabled: true,
109
+ recallPlannerLlmEnabled: true,
110
+ graphRecallEnabled: true,
111
+ });
112
+ const first = resolveCapabilities(config);
113
+ const second = resolveCapabilities(config);
114
+ for (const field of FIELDS) {
115
+ assert.equal(
116
+ first[field],
117
+ second[field],
118
+ `field ${field} must be deterministic across resolutions`,
119
+ );
120
+ }
121
+ // Both must be frozen — a mid-operation mutation would break the contract.
122
+ assert.equal(Object.isFrozen(first), true);
123
+ assert.equal(Object.isFrozen(second), true);
124
+ });
125
+
126
+
99
127
  // ---------------------------------------------------------------------------
100
128
  // GraphConstructionCapabilitySet — gate-parity tests (issue #1566 Cluster A).
101
129
  //
@@ -13,7 +13,6 @@ import type { TrustZoneName } from "./trust-zones.js";
13
13
  type WiringConfig = DirectAnswerWiringInput["config"];
14
14
 
15
15
  const BASE_CONFIG: WiringConfig = {
16
- recallDirectAnswerEnabled: true,
17
16
  recallDirectAnswerTokenOverlapFloor: 0.5,
18
17
  recallDirectAnswerImportanceFloor: 0.7,
19
18
  recallDirectAnswerAmbiguityMargin: 0.15,
@@ -105,17 +104,24 @@ test("tryDirectAnswer disabled-path does not call any source accessor", async ()
105
104
  assert.deepEqual(sources.calls.importance, []);
106
105
  });
107
106
 
108
- // ── Backward-compat (#1523): omit top-level `enabled`, fall back to config ──
107
+ // ── One-resolution-per-op (#1523): `enabled` is the sole gate ──────────────
109
108
 
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()] });
109
+ test("tryDirectAnswer disabled when `enabled: false` even with matching candidates (caps wins over any config-derived value)", async () => {
110
+ // Issue #1523 contract: the resolved capability is the sole gate.
111
+ // `enabled` comes from `resolveCapabilities(config).recallDirectAnswer`,
112
+ // resolved ONCE at the recall entry. This test proves the gate value is
113
+ // authoritative when it says disabled, the function short-circuits
114
+ // regardless of candidate quality.
115
+ const sources = makeMockSources({
116
+ memories: [makeMemory({ tags: ["pnpm"], content: "remnic uses pnpm" })],
117
+ trustZones: { m1: "trusted" },
118
+ importance: { m1: 0.9 },
119
+ });
115
120
  const result = await tryDirectAnswer({
116
- query: "does not matter",
121
+ query: "package manager remnic",
117
122
  namespace: "default",
118
- config: { ...BASE_CONFIG, recallDirectAnswerEnabled: false },
123
+ config: BASE_CONFIG,
124
+ enabled: false,
119
125
  sources,
120
126
  });
121
127
  assert.equal(result.eligible, false);
@@ -123,9 +129,9 @@ test("tryDirectAnswer falls back to config.recallDirectAnswerEnabled when `enabl
123
129
  assert.equal(sources.calls.listCandidates, 0);
124
130
  });
125
131
 
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.
132
+ test("tryDirectAnswer proceeds when `enabled: true` and candidates match", async () => {
133
+ // Same contract, positive case: when the resolved capability says enabled,
134
+ // the function proceeds to materialize candidates.
129
135
  const sources = makeMockSources({
130
136
  memories: [makeMemory({ tags: ["pnpm"], content: "remnic uses pnpm" })],
131
137
  trustZones: { m1: "trusted" },
@@ -134,7 +140,8 @@ test("tryDirectAnswer falls back to config.recallDirectAnswerEnabled when `enabl
134
140
  const result = await tryDirectAnswer({
135
141
  query: "package manager remnic",
136
142
  namespace: "default",
137
- config: BASE_CONFIG, // recallDirectAnswerEnabled: true
143
+ config: BASE_CONFIG,
144
+ enabled: true,
138
145
  sources,
139
146
  });
140
147
  assert.notEqual(result.reason, "disabled");
@@ -14,8 +14,9 @@
14
14
  *
15
15
  * Short-circuit contract:
16
16
  *
17
- * - When the resolved gate (`input.enabled` if supplied, else
18
- * `config.recallDirectAnswerEnabled`) is `false`, the function returns the
17
+ * - When the resolved gate (`input.enabled`, projected from
18
+ * `caps.recallDirectAnswer` at the recall-operation entry) is `false`, the
19
+ * function returns the
19
20
  * eligibility verdict with reason `"disabled"` without touching any source
20
21
  * accessor. This is the documented default.
21
22
  * - When enabled, the wiring cheaply drops non-trusted-zone memories
@@ -74,7 +75,6 @@ export interface DirectAnswerWiringInput {
74
75
  namespace: string;
75
76
  config: Pick<
76
77
  PluginConfig,
77
- | "recallDirectAnswerEnabled"
78
78
  | "recallDirectAnswerTokenOverlapFloor"
79
79
  | "recallDirectAnswerImportanceFloor"
80
80
  | "recallDirectAnswerAmbiguityMargin"
@@ -82,13 +82,12 @@ export interface DirectAnswerWiringInput {
82
82
  >;
83
83
  /**
84
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.
85
+ * via `resolveCapabilities(config).recallDirectAnswer` (issue #1523). This
86
+ * is the sole gate the function no longer reads the config flag directly.
87
+ * Callers MUST pass the resolved capability so the whole operation agrees on
88
+ * a single gate value.
90
89
  */
91
- enabled?: boolean;
90
+ enabled: boolean;
92
91
  sources: DirectAnswerSources;
93
92
  queryEntityRefs?: string[];
94
93
  abortSignal?: AbortSignal;
@@ -105,10 +104,7 @@ export async function tryDirectAnswer(
105
104
  const { query, namespace, config, enabled, sources, queryEntityRefs, abortSignal } = input;
106
105
 
107
106
  const eligibilityConfig: DirectAnswerConfig = {
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,
107
+ enabled,
112
108
  tokenOverlapFloor: config.recallDirectAnswerTokenOverlapFloor,
113
109
  importanceFloor: config.recallDirectAnswerImportanceFloor,
114
110
  ambiguityMargin: config.recallDirectAnswerAmbiguityMargin,
@@ -1396,12 +1396,11 @@ export async function resolveRecallModeDecisionAsync(
1396
1396
  options: RecallModeGraphOptions & {
1397
1397
  config: PluginConfig;
1398
1398
  /**
1399
- * Recall-operation capability gates (issue #1523). OPTIONAL and additive:
1400
- * the recall orchestrator passes a resolved set, but existing callers that
1401
- * only pass `config` + planner flags stay backward-compatible the LLM
1402
- * planner gate falls back to `config.recallPlannerLlmEnabled` when omitted.
1399
+ * Recall-operation capability gates (issue #1523). REQUIRED: the recall
1400
+ * orchestrator always passes a resolved set the LLM planner gate reads
1401
+ * `caps.recallPlannerLlm`, never re-derives from config.
1403
1402
  */
1404
- caps?: CapabilitySet;
1403
+ caps: CapabilitySet;
1405
1404
  hints?: string[];
1406
1405
  llm?: FallbackLlmClient;
1407
1406
  signal?: AbortSignal;
@@ -1410,10 +1409,8 @@ export async function resolveRecallModeDecisionAsync(
1410
1409
  const heuristicDecision = resolveRecallModeDecision(options);
1411
1410
 
1412
1411
  // Planner globally off, or LLM planning not opted into → heuristic only.
1413
- // Prefer the resolved capability when supplied; otherwise fall back to the
1414
- // config flag so callers on the old option shape get identical gating.
1415
- const plannerLlmEnabled =
1416
- options.caps?.recallPlannerLlm ?? options.config.recallPlannerLlmEnabled;
1412
+ // Read the resolved capability (issue #1523) never re-derive from config.
1413
+ const plannerLlmEnabled = options.caps.recallPlannerLlm;
1417
1414
  if (!options.plannerEnabled || !plannerLlmEnabled) {
1418
1415
  return heuristicDecision;
1419
1416
  }
@@ -1423,9 +1420,9 @@ export async function resolveRecallModeDecisionAsync(
1423
1420
  options.prompt,
1424
1421
  options.hints,
1425
1422
  options.config,
1423
+ options.caps,
1426
1424
  options.llm,
1427
1425
  options.signal,
1428
- options.caps,
1429
1426
  );
1430
1427
 
1431
1428
  // Shadow mode: record what the LLM would have chosen but keep the heuristic
@@ -41,7 +41,7 @@ test("returns heuristic without calling the LLM when recallPlannerLlmEnabled is
41
41
  const captured: Array<Record<string, unknown>> = [];
42
42
  const llm = stubLlm({ capturedOptions: captured, result: { mode: "no_recall" } });
43
43
 
44
- const result = await planRecallModeLLM("what did we decide about auth?", undefined, config, llm, undefined, resolveCapabilities(config));
44
+ const result = await planRecallModeLLM("what did we decide about auth?", undefined, config, resolveCapabilities(config), llm, undefined);
45
45
 
46
46
  assert.equal(captured.length, 0, "LLM must not be contacted when disabled");
47
47
  assert.equal(result.source, "heuristic");
@@ -55,7 +55,7 @@ test("uses the LLM classification when enabled", async () => {
55
55
  const config = parseConfig({ recallPlannerLlmEnabled: true });
56
56
  const llm = stubLlm({ result: { mode: "graph_mode", reason: "asks for root cause" }, modelUsed: "anthropic/claude" });
57
57
 
58
- const result = await planRecallModeLLM("restart the gateway", undefined, config, llm, undefined, resolveCapabilities(config));
58
+ const result = await planRecallModeLLM("restart the gateway", undefined, config, resolveCapabilities(config), llm, undefined);
59
59
 
60
60
  assert.equal(result.source, "llm");
61
61
  assert.equal(result.mode, "graph_mode");
@@ -75,7 +75,7 @@ test("forwards taskModelChain AND recallPlannerModel in gateway mode (provider-a
75
75
  const captured: Array<Record<string, unknown>> = [];
76
76
  const llm = stubLlm({ capturedOptions: captured, result: { mode: "minimal" } });
77
77
 
78
- await planRecallModeLLM("check status", undefined, config, llm, undefined, resolveCapabilities(config));
78
+ await planRecallModeLLM("check status", undefined, config, resolveCapabilities(config), llm, undefined);
79
79
 
80
80
  assert.equal(captured.length, 1);
81
81
  // recallPlannerModel is tried first (prepended), taskModelChain is the fallback chain.
@@ -98,7 +98,7 @@ test("plugin mode passes only the explicit model, no gateway chain", async () =>
98
98
  const captured: Array<Record<string, unknown>> = [];
99
99
  const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
100
100
 
101
- await planRecallModeLLM("summarize the project", undefined, config, llm, undefined, resolveCapabilities(config));
101
+ await planRecallModeLLM("summarize the project", undefined, config, resolveCapabilities(config), llm, undefined);
102
102
 
103
103
  assert.equal(captured.length, 1);
104
104
  assert.equal(captured[0]?.model, "openai/gpt-5.5");
@@ -110,7 +110,7 @@ test("falls back to heuristic when the LLM throws", async () => {
110
110
  const config = parseConfig({ recallPlannerLlmEnabled: true });
111
111
  const llm = stubLlm({ throwError: "boom" });
112
112
 
113
- const result = await planRecallModeLLM("what happened during the outage?", undefined, config, llm, undefined, resolveCapabilities(config));
113
+ const result = await planRecallModeLLM("what happened during the outage?", undefined, config, resolveCapabilities(config), llm, undefined);
114
114
 
115
115
  assert.equal(result.source, "heuristic-fallback");
116
116
  assert.equal(result.fallbackUsed, true);
@@ -124,7 +124,7 @@ test("falls back to heuristic when the LLM returns no parseable result", async (
124
124
  const config = parseConfig({ recallPlannerLlmEnabled: true });
125
125
  const llm = stubLlm({ result: null });
126
126
 
127
- const result = await planRecallModeLLM("how did we get here?", undefined, config, llm, undefined, resolveCapabilities(config));
127
+ const result = await planRecallModeLLM("how did we get here?", undefined, config, resolveCapabilities(config), llm, undefined);
128
128
 
129
129
  assert.equal(result.source, "heuristic-fallback");
130
130
  assert.equal(result.fallbackUsed, true);
@@ -140,7 +140,7 @@ test("falls back without a network attempt when the chain is empty and the model
140
140
  const captured: Array<Record<string, unknown>> = [];
141
141
  const llm = stubLlm({ available: false, capturedOptions: captured, result: { mode: "full" } });
142
142
 
143
- const result = await planRecallModeLLM("anything", undefined, config, llm, undefined, resolveCapabilities(config));
143
+ const result = await planRecallModeLLM("anything", undefined, config, resolveCapabilities(config), llm, undefined);
144
144
 
145
145
  assert.equal(captured.length, 0, "no network attempt when nothing is routable");
146
146
  assert.equal(result.source, "heuristic-fallback");
@@ -156,7 +156,7 @@ test("attempts the call (and falls back) when a provider-qualified model overrid
156
156
  const captured: Array<Record<string, unknown>> = [];
157
157
  const llm = stubLlm({ available: false, capturedOptions: captured, result: null });
158
158
 
159
- const result = await planRecallModeLLM("anything", undefined, config, llm, undefined, resolveCapabilities(config));
159
+ const result = await planRecallModeLLM("anything", undefined, config, resolveCapabilities(config), llm, undefined);
160
160
 
161
161
  assert.equal(captured.length, 1, "qualified model override → still attempt the call");
162
162
  assert.equal(captured[0]?.model, "openai/gpt-5.5");
@@ -171,7 +171,7 @@ test("an already-aborted recall short-circuits to the heuristic without an LLM c
171
171
  const ac = new AbortController();
172
172
  ac.abort();
173
173
 
174
- const result = await planRecallModeLLM("what did we decide?", undefined, config, llm, ac.signal, resolveCapabilities(config));
174
+ const result = await planRecallModeLLM("what did we decide?", undefined, config, resolveCapabilities(config), llm, ac.signal);
175
175
 
176
176
  assert.equal(captured.length, 0, "no LLM call when the recall is already aborted");
177
177
  assert.equal(result.source, "heuristic-fallback");
@@ -185,7 +185,7 @@ test("forwards the abort signal into the LLM call (cancellation contract)", asyn
185
185
  const llm = stubLlm({ capturedOptions: captured, result: { mode: "minimal" } });
186
186
  const ac = new AbortController();
187
187
 
188
- await planRecallModeLLM("check status", undefined, config, llm, ac.signal, resolveCapabilities(config));
188
+ await planRecallModeLLM("check status", undefined, config, resolveCapabilities(config), llm, ac.signal);
189
189
 
190
190
  assert.equal(captured.length, 1);
191
191
  assert.equal(captured[0]?.signal, ac.signal, "recall abort signal must reach FallbackLlmClient");
@@ -196,7 +196,7 @@ test("empty prompts skip the LLM entirely", async () => {
196
196
  const captured: Array<Record<string, unknown>> = [];
197
197
  const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
198
198
 
199
- const result = await planRecallModeLLM(" ", undefined, config, llm, undefined, resolveCapabilities(config));
199
+ const result = await planRecallModeLLM(" ", undefined, config, resolveCapabilities(config), llm, undefined);
200
200
 
201
201
  assert.equal(captured.length, 0);
202
202
  assert.equal(result.mode, "no_recall"); // heuristic returns no_recall for empty
@@ -223,3 +223,22 @@ test("resolveRecallPlannerLlmOptions drops bare model names but keeps provider-q
223
223
  );
224
224
  assert.equal(qualified.model, "anthropic/claude-haiku-4-5");
225
225
  });
226
+
227
+ test("caps.recallPlannerLlm is authoritative — disabled caps wins over enabled config flag (one-resolution-per-op #1523)", async () => {
228
+ // Issue #1523 contract: the capability resolved at the operation entry is
229
+ // THE gate. Construct a deliberate mismatch: config says enabled, but the
230
+ // resolved CapabilitySet says disabled. The function must NOT call the LLM.
231
+ const config = parseConfig({ recallPlannerLlmEnabled: true });
232
+ const captured: Array<Record<string, unknown>> = [];
233
+ const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
234
+
235
+ // Manually construct caps with recallPlannerLlm: false (simulating a
236
+ // resolution that disagrees with the raw config — e.g. session toggle off).
237
+ const caps = { ...resolveCapabilities(config), recallPlannerLlm: false } as any;
238
+
239
+ const result = await planRecallModeLLM("what did we decide?", undefined, config, caps, llm, undefined);
240
+
241
+ assert.equal(captured.length, 0, "LLM must not be contacted when caps says disabled, even if config says enabled");
242
+ assert.equal(result.source, "heuristic");
243
+ assert.equal(result.reason, "llm-disabled");
244
+ });
@@ -188,16 +188,15 @@ export async function planRecallModeLLM(
188
188
  prompt: string,
189
189
  hints: string[] | undefined,
190
190
  config: PluginConfig,
191
+ caps: CapabilitySet,
191
192
  llm?: FallbackLlmClient,
192
193
  signal?: AbortSignal,
193
- caps?: CapabilitySet,
194
194
  ): Promise<RecallPlannerLlmResult> {
195
195
  const heuristicMode = planRecallMode(prompt);
196
196
 
197
- // `caps` is OPTIONAL and additive (issue #1523). Prefer the resolved
198
- // capability when supplied; fall back to the config flag so existing callers
199
- // that pass only `config` keep identical gating.
200
- const plannerLlmEnabled = caps?.recallPlannerLlm ?? config.recallPlannerLlmEnabled;
197
+ // The recall-operation entry resolved this gate once (issue #1523); read the
198
+ // capability, never re-derive it from raw config here.
199
+ const plannerLlmEnabled = caps.recallPlannerLlm;
201
200
  if (!plannerLlmEnabled) {
202
201
  return heuristicResult(heuristicMode, "heuristic", "llm-disabled", 0, false);
203
202
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/taxonomy/resolver.ts","../src/direct-answer-wiring.ts"],"sourcesContent":["/**\n * Resolver decision tree for the MECE taxonomy.\n *\n * Given extracted content and its MemoryCategory, determines which\n * taxonomy category the knowledge should be filed under.\n */\n\nimport type { MemoryCategory } from \"../types.js\";\nimport type { ResolverDecision, Taxonomy, TaxonomyCategory } from \"./types.js\";\n\nconst DEFAULT_CATEGORY_ID = \"facts\";\n\n/**\n * Resolve a piece of content to a taxonomy category.\n *\n * Algorithm:\n * 1. Find all taxonomy categories whose `memoryCategories` include\n * the given `memoryCategory`.\n * 2. If exactly one match, return it with confidence 1.0.\n * 3. If multiple matches, prefer the strongest exact-token keyword\n * overlap from filing rules. Use priority only as a tie-breaker.\n * If no filing rules match, choose the explicit/default fallback\n * category instead of letting low-priority generic terms win.\n * 4. If no match, fall back to the \"facts\" category (or first\n * category if \"facts\" is absent) with low confidence.\n * 5. Always populate `alternatives` with other plausible categories.\n */\nexport function resolveCategory(\n content: string,\n memoryCategory: MemoryCategory,\n taxonomy: Taxonomy,\n): ResolverDecision {\n const contentTokens = tokenizeKeywordText(content);\n\n // Step 1: find matching categories\n const matches = taxonomy.categories.filter((cat) =>\n cat.memoryCategories.includes(memoryCategory),\n );\n\n if (matches.length === 0) {\n // No taxonomy category accepts this MemoryCategory — fall back\n const fallback =\n taxonomy.categories.find((c) => c.id === DEFAULT_CATEGORY_ID) ??\n taxonomy.categories[0];\n if (!fallback) {\n return {\n categoryId: DEFAULT_CATEGORY_ID,\n confidence: 0,\n reason: \"Taxonomy is empty; using default category\",\n alternatives: [],\n };\n }\n const alternatives = taxonomy.categories\n .filter((c) => c.id !== fallback.id)\n .map((c) => ({\n categoryId: c.id,\n reason: c.description,\n }));\n return {\n categoryId: fallback.id,\n confidence: 0.3,\n reason: `No taxonomy category maps to MemoryCategory \"${memoryCategory}\"; falling back to \"${fallback.name}\"`,\n alternatives,\n };\n }\n\n if (matches.length === 1) {\n const match = matches[0]!;\n const alternatives = taxonomy.categories\n .filter((c) => c.id !== match.id)\n .map((c) => ({\n categoryId: c.id,\n reason: c.description,\n }));\n return {\n categoryId: match.id,\n confidence: 1.0,\n reason: `Unique match: MemoryCategory \"${memoryCategory}\" maps to \"${match.name}\"`,\n alternatives,\n };\n }\n\n // Multiple matches — use filing rule keyword heuristics + priority\n const scored = matches.map((cat) => ({\n cat,\n keywordScore: computeKeywordScoreForTokens(contentTokens, cat),\n }));\n\n // Sort by keyword score descending, then priority ascending (lower wins)\n scored.sort((a, b) => {\n if (b.keywordScore !== a.keywordScore) return b.keywordScore - a.keywordScore;\n return a.cat.priority - b.cat.priority;\n });\n\n const topScored = scored[0]!;\n const best = topScored.keywordScore > 0\n ? topScored\n : {\n cat: selectFallbackCategory(matches) ?? topScored.cat,\n keywordScore: 0,\n };\n const runnerUp = scored[1];\n\n // Confidence is higher when keyword match clearly differentiates\n const confidence =\n best.keywordScore > 0 && (!runnerUp || best.keywordScore > runnerUp.keywordScore)\n ? 0.9\n : 0.7;\n\n const alternatives = taxonomy.categories\n .filter((c) => c.id !== best.cat.id)\n .map((c) => ({\n categoryId: c.id,\n reason: c.description,\n }));\n\n const reason =\n best.keywordScore > 0\n ? `Filing rules for \"${best.cat.name}\" matched content keywords (priority ${best.cat.priority})`\n : `No filing rules matched content keywords; using fallback category \"${best.cat.name}\"`;\n\n return {\n categoryId: best.cat.id,\n confidence,\n reason,\n alternatives,\n };\n}\n\n/**\n * Compute a simple keyword overlap score between content and\n * a category's filing rules + description.\n */\nfunction computeKeywordScoreForTokens(contentTokens: Set<string>, cat: TaxonomyCategory): number {\n let score = 0;\n const ruleText = [...cat.filingRules, cat.description]\n .join(\" \")\n .toLowerCase();\n\n const keywords = tokenizeKeywordText(ruleText);\n\n for (const kw of keywords) {\n if (contentTokens.has(kw)) {\n score += 1;\n }\n }\n return score;\n}\n\nfunction tokenizeKeywordText(value: string): Set<string> {\n return new Set(\n value\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .map((word) => word.trim())\n .filter((word) => word.length >= 3 && !TAXONOMY_KEYWORD_STOPWORDS.has(word)),\n );\n}\n\nfunction selectFallbackCategory(\n categories: readonly TaxonomyCategory[],\n): TaxonomyCategory | undefined {\n return categories.find((cat) => cat.id === DEFAULT_CATEGORY_ID) ??\n categories.find((cat) => {\n const text = `${cat.id} ${cat.name} ${cat.description} ${cat.filingRules.join(\" \")}`.toLowerCase();\n return /\\b(general|fallback)\\b/.test(text);\n });\n}\n\nconst TAXONOMY_KEYWORD_STOPWORDS = new Set([\n \"about\",\n \"all\",\n \"and\",\n \"are\",\n \"for\",\n \"from\",\n \"has\",\n \"into\",\n \"not\",\n \"the\",\n \"this\",\n \"was\",\n \"with\",\n]);\n","/**\n * Direct-answer wiring (issue #518 slice 3).\n *\n * Binds the pure eligibility decision (`direct-answer.ts`) to the data\n * sources needed to build candidates: storage, trust-zones, taxonomy,\n * and importance scoring. Kept as a separate module so that:\n *\n * - The eligibility layer stays pure and unit-testable without stores.\n * - Each caller injects its own source accessors. The orchestrator\n * binding is a follow-on slice; tests here use mock sources.\n * - The wiring is safe to ship alone — nothing calls `tryDirectAnswer`\n * yet, so enabling this module's presence does not change recall\n * behavior. The next slice adds exactly one call site before QMD.\n *\n * Short-circuit contract:\n *\n * - When the resolved gate (`input.enabled` if supplied, else\n * `config.recallDirectAnswerEnabled`) is `false`, the function returns the\n * eligibility verdict with reason `\"disabled\"` without touching any source\n * accessor. This is the documented default.\n * - When enabled, the wiring cheaply drops non-trusted-zone memories\n * and ineligible taxonomy buckets before computing importance, so\n * the eligibility module sees a pre-filtered candidate set. The\n * eligibility module still performs the same checks itself — this\n * module is purely an I/O and prefiltering layer.\n */\n\nimport type { MemoryFile, PluginConfig } from \"./types.js\";\nimport type { TrustZoneName } from \"./trust-zones.js\";\nimport type { Taxonomy } from \"./taxonomy/types.js\";\nimport { resolveCategory } from \"./taxonomy/resolver.js\";\nimport { normalizeRecallTokens } from \"./recall-tokenization.js\";\nimport { throwIfAborted } from \"./abort-error.js\";\nimport {\n isDirectAnswerEligible,\n type DirectAnswerCandidate,\n type DirectAnswerConfig,\n type DirectAnswerResult,\n} from \"./direct-answer.js\";\n\n/**\n * Caller-provided accessors for candidate sourcing. Decouples the\n * wiring from any specific storage / trust-zone / importance backend.\n */\nexport interface DirectAnswerSources {\n /**\n * List memories eligible to be considered for direct-answer.\n * Callers are expected to return only active, non-superseded memories\n * in the requested namespace; the wiring will cheaply re-filter on\n * trust zone and taxonomy bucket and hand the rest to the eligibility\n * module, which applies the full gate ladder.\n */\n listCandidateMemories(options: {\n namespace: string;\n abortSignal?: AbortSignal;\n }): Promise<MemoryFile[]>;\n /**\n * Resolve the trust-zone record for a memory. Returns `null` when\n * the memory has no trust-zone record (treated as not trusted).\n */\n trustZoneFor(memoryId: string): Promise<TrustZoneName | null>;\n /**\n * Resolve a calibrated importance score in [0, 1] for a memory.\n */\n importanceFor(memory: MemoryFile): number;\n /**\n * Taxonomy used to classify memories into direct-answer buckets.\n */\n taxonomy: Taxonomy;\n}\n\nexport interface DirectAnswerWiringInput {\n query: string;\n namespace: string;\n config: Pick<\n PluginConfig,\n | \"recallDirectAnswerEnabled\"\n | \"recallDirectAnswerTokenOverlapFloor\"\n | \"recallDirectAnswerImportanceFloor\"\n | \"recallDirectAnswerAmbiguityMargin\"\n | \"recallDirectAnswerEligibleTaxonomyBuckets\"\n >;\n /**\n * Direct-answer capability gate, resolved once at the recall-operation entry\n * (issue #1523: `caps.recallDirectAnswer`). OPTIONAL and additive: when the\n * caller supplies it, this module and the orchestrator agree on a single\n * resolved gate value for the whole operation. When omitted, we fall back to\n * `config.recallDirectAnswerEnabled` so existing callers on the old input\n * shape keep identical behavior.\n */\n enabled?: boolean;\n sources: DirectAnswerSources;\n queryEntityRefs?: string[];\n abortSignal?: AbortSignal;\n}\n\n/**\n * Attempt direct-answer resolution. Returns the eligibility verdict\n * produced by `isDirectAnswerEligible` with candidates materialized\n * from the caller-supplied sources.\n */\nexport async function tryDirectAnswer(\n input: DirectAnswerWiringInput,\n): Promise<DirectAnswerResult> {\n const { query, namespace, config, enabled, sources, queryEntityRefs, abortSignal } = input;\n\n const eligibilityConfig: DirectAnswerConfig = {\n // Prefer the resolved capability when supplied; fall back to the config\n // flag so callers on the old input shape (config-only, no `enabled`) get\n // identical gating (issue #1523 backward-compat).\n enabled: enabled ?? config.recallDirectAnswerEnabled,\n tokenOverlapFloor: config.recallDirectAnswerTokenOverlapFloor,\n importanceFloor: config.recallDirectAnswerImportanceFloor,\n ambiguityMargin: config.recallDirectAnswerAmbiguityMargin,\n eligibleTaxonomyBuckets: config.recallDirectAnswerEligibleTaxonomyBuckets,\n };\n\n // Short-circuit disabled case before touching any I/O.\n if (!eligibilityConfig.enabled) {\n return isDirectAnswerEligible({\n query,\n candidates: [],\n config: eligibilityConfig,\n queryEntityRefs,\n });\n }\n\n // Short-circuit empty-query case before any I/O. isDirectAnswerEligible\n // deterministically returns reason \"empty-query\" when the query\n // normalizes to zero searchable tokens; there's no point materializing\n // candidates just to reach the same verdict, and doing so would\n // surface avoidable upstream errors for requests that should exit\n // immediately.\n if (normalizeRecallTokens(query).length === 0) {\n return isDirectAnswerEligible({\n query,\n candidates: [],\n config: eligibilityConfig,\n queryEntityRefs,\n });\n }\n\n throwIfAborted(abortSignal, \"direct-answer wiring aborted\");\n const memories = await sources.listCandidateMemories({ namespace, abortSignal });\n throwIfAborted(abortSignal, \"direct-answer wiring aborted\");\n const candidates: DirectAnswerCandidate[] = [];\n\n for (const memory of memories) {\n // Throw rather than returning a partial verdict — a mid-loop abort\n // that left competing candidates unprocessed could otherwise surface\n // a spurious \"eligible\" result that the ambiguity gate never got a\n // chance to reject. The check repeats after every await so an\n // abort that lands during the in-flight I/O on the final memory\n // (after which no further iteration would exist) still stops us.\n throwIfAborted(abortSignal, \"direct-answer wiring aborted\");\n\n const trustZone = await sources.trustZoneFor(memory.frontmatter.id);\n throwIfAborted(abortSignal, \"direct-answer wiring aborted\");\n\n // Cheap pre-filter: non-trusted memories can't qualify, so skip\n // taxonomy and importance resolution for them.\n if (trustZone !== \"trusted\") continue;\n\n const decision = resolveCategory(\n memory.content,\n memory.frontmatter.category,\n sources.taxonomy,\n );\n const taxonomyBucket = decision.categoryId;\n if (!eligibilityConfig.eligibleTaxonomyBuckets.includes(taxonomyBucket)) continue;\n\n const importanceScore = sources.importanceFor(memory);\n\n candidates.push({\n memory,\n trustZone,\n taxonomyBucket,\n importanceScore,\n });\n }\n\n // Final check — if abort landed during the trust-zone await for the\n // last memory, the loop condition no longer fires. Guard before we\n // hand candidates to the eligibility gate.\n throwIfAborted(abortSignal, \"direct-answer wiring aborted\");\n\n return isDirectAnswerEligible({\n query,\n candidates,\n config: eligibilityConfig,\n queryEntityRefs,\n });\n}\n"],"mappings":"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","names":["alternatives"]}