@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.
- package/dist/access-audit.js +2 -2
- package/dist/access-cli.js +41 -40
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +3 -2
- package/dist/access-http.js +25 -25
- package/dist/access-mcp.d.ts +3 -2
- package/dist/access-mcp.js +22 -22
- package/dist/access-schema.d.ts +36 -36
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-D0SLB4MH.d.ts → access-service-BCuaiNHa.d.ts} +1 -1
- package/dist/access-service.d.ts +3 -2
- package/dist/access-service.js +21 -21
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/bootstrap.d.ts +2 -1
- package/dist/briefing.js +4 -3
- package/dist/capabilities.d.ts +73 -0
- package/dist/capabilities.js +8 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +7 -6
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-ROHLEUTH.js → chunk-23EBQ27U.js} +5 -5
- package/dist/{chunk-YW52BQSU.js → chunk-2TCHDANJ.js} +2 -2
- package/dist/{chunk-IROWLAWG.js → chunk-46WUVFOD.js} +4 -4
- package/dist/{chunk-WH4SKYPX.js → chunk-4FJKKC2N.js} +107 -77
- package/dist/chunk-4FJKKC2N.js.map +1 -0
- package/dist/{chunk-7C4MPEPE.js → chunk-4T7P2HLJ.js} +3 -3
- package/dist/{chunk-7XH7VJN4.js → chunk-6T4LTI2F.js} +4 -4
- package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
- package/dist/{chunk-BZG2CWOQ.js → chunk-A5TEHAR4.js} +3 -3
- package/dist/{chunk-C7AF236A.js → chunk-AARDBQTA.js} +2 -2
- package/dist/{chunk-IHG6CC7T.js → chunk-BQJUPECT.js} +2 -2
- package/dist/{chunk-7OGJQP7T.js → chunk-CRO4LCQ6.js} +5 -5
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-UXA5L2DZ.js → chunk-HQCGRSRU.js} +2 -2
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-XW3W4PV4.js → chunk-JTPXSXHC.js} +2 -2
- package/dist/{chunk-OHJFJ4HI.js → chunk-KOXGLQS7.js} +2 -2
- package/dist/{chunk-2OPARZ4B.js → chunk-MPXYHC35.js} +26 -26
- package/dist/{chunk-6JBKHTQD.js → chunk-MR4PJ277.js} +2 -2
- package/dist/{chunk-EXXBA5OM.js → chunk-OI4BXFSB.js} +4 -4
- package/dist/{chunk-SQZ42MKH.js → chunk-OQH5XUH3.js} +6 -3
- package/dist/chunk-OQH5XUH3.js.map +1 -0
- package/dist/{chunk-2HEZXPYU.js → chunk-Q2LQZYQ7.js} +3 -3
- package/dist/{chunk-XRSIGVTS.js → chunk-QHWJG5C5.js} +8 -8
- package/dist/{chunk-T2AN3BSP.js → chunk-QZ7ODIVL.js} +2 -2
- package/dist/chunk-RI5XBIZ6.js +23 -0
- package/dist/chunk-RI5XBIZ6.js.map +1 -0
- package/dist/{chunk-D7IXTY5E.js → chunk-TJ7HH5LB.js} +2 -2
- package/dist/{chunk-V25ZAOSB.js → chunk-UOBLE67F.js} +4 -4
- package/dist/{chunk-JIX3ZL2J.js → chunk-UVUTV7CM.js} +15 -15
- package/dist/{chunk-VH6EIKVS.js → chunk-WKMCC4NQ.js} +35 -16
- package/dist/chunk-WKMCC4NQ.js.map +1 -0
- package/dist/{chunk-SSOMTUCA.js → chunk-WXGTC424.js} +1 -1
- package/dist/{chunk-KHGE6PMF.js → chunk-WXXLSZHA.js} +2 -2
- package/dist/{chunk-DSLUOQDY.js → chunk-XMWF6AU3.js} +2 -2
- package/dist/{chunk-DQY7NJ5L.js → chunk-XS2CWEHZ.js} +2 -2
- package/dist/{cli-BQRqR9N-.d.ts → cli-C98xlwYA.d.ts} +2 -2
- package/dist/cli.d.ts +4 -3
- package/dist/cli.js +42 -42
- package/dist/compounding/engine.js +4 -3
- package/dist/connectors/codex-materialize-runner.js +4 -3
- package/dist/connectors/index.js +4 -3
- package/dist/consolidation-provenance-check.js +2 -2
- package/dist/conversation-index/backend.js +2 -2
- package/dist/dashboard-runtime.js +2 -2
- package/dist/direct-answer-wiring.d.ts +13 -3
- package/dist/direct-answer-wiring.js +1 -1
- package/dist/entity-retrieval.js +4 -3
- package/dist/explicit-capture.d.ts +2 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +66 -65
- package/dist/index.js.map +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +4 -4
- package/dist/maintenance/memory-governance.js +4 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -3
- package/dist/maintenance/rebuild-memory-projection.js +5 -5
- package/dist/mcp-memory-inspector-app.d.ts +3 -2
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.js +4 -3
- package/dist/operator-toolkit.js +15 -15
- package/dist/{orchestrator-Cg1UkvmO.d.ts → orchestrator-DyP9QYsh.d.ts} +16 -0
- package/dist/orchestrator.d.ts +2 -1
- package/dist/orchestrator.js +32 -31
- package/dist/recall-planner-llm.d.ts +2 -1
- package/dist/recall-planner-llm.js +3 -2
- package/dist/recall-planner-llm.js.map +1 -1
- package/dist/schemas.d.ts +64 -64
- package/dist/search/factory.js +6 -6
- package/dist/search/index.js +10 -10
- package/dist/search/lancedb-backend.js +1 -1
- package/dist/search/meilisearch-backend.js +1 -1
- package/dist/search/orama-backend.js +1 -1
- package/dist/semantic-consolidation.js +5 -4
- package/dist/semantic-rule-promotion.js +4 -3
- package/dist/semantic-rule-verifier.js +4 -3
- package/dist/shared-context/manager.d.ts +2 -2
- package/dist/storage.js +3 -2
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +4 -3
- package/package.json +1 -1
- package/src/capabilities.test.ts +97 -0
- package/src/capabilities.ts +86 -0
- package/src/direct-answer-wiring.test.ts +53 -2
- package/src/direct-answer-wiring.ts +18 -5
- package/src/orchestrator.ts +83 -22
- package/src/recall-planner-llm.test.ts +12 -11
- package/src/recall-planner-llm.ts +7 -1
- package/src/storage-fallback-category-dirs.test.ts +150 -1
- package/src/storage.ts +51 -14
- package/dist/chunk-SQZ42MKH.js.map +0 -1
- package/dist/chunk-VH6EIKVS.js.map +0 -1
- package/dist/chunk-WH4SKYPX.js.map +0 -1
- /package/dist/{chunk-ROHLEUTH.js.map → chunk-23EBQ27U.js.map} +0 -0
- /package/dist/{chunk-YW52BQSU.js.map → chunk-2TCHDANJ.js.map} +0 -0
- /package/dist/{chunk-IROWLAWG.js.map → chunk-46WUVFOD.js.map} +0 -0
- /package/dist/{chunk-7C4MPEPE.js.map → chunk-4T7P2HLJ.js.map} +0 -0
- /package/dist/{chunk-7XH7VJN4.js.map → chunk-6T4LTI2F.js.map} +0 -0
- /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
- /package/dist/{chunk-BZG2CWOQ.js.map → chunk-A5TEHAR4.js.map} +0 -0
- /package/dist/{chunk-C7AF236A.js.map → chunk-AARDBQTA.js.map} +0 -0
- /package/dist/{chunk-IHG6CC7T.js.map → chunk-BQJUPECT.js.map} +0 -0
- /package/dist/{chunk-7OGJQP7T.js.map → chunk-CRO4LCQ6.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-UXA5L2DZ.js.map → chunk-HQCGRSRU.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-XW3W4PV4.js.map → chunk-JTPXSXHC.js.map} +0 -0
- /package/dist/{chunk-OHJFJ4HI.js.map → chunk-KOXGLQS7.js.map} +0 -0
- /package/dist/{chunk-2OPARZ4B.js.map → chunk-MPXYHC35.js.map} +0 -0
- /package/dist/{chunk-6JBKHTQD.js.map → chunk-MR4PJ277.js.map} +0 -0
- /package/dist/{chunk-EXXBA5OM.js.map → chunk-OI4BXFSB.js.map} +0 -0
- /package/dist/{chunk-2HEZXPYU.js.map → chunk-Q2LQZYQ7.js.map} +0 -0
- /package/dist/{chunk-XRSIGVTS.js.map → chunk-QHWJG5C5.js.map} +0 -0
- /package/dist/{chunk-T2AN3BSP.js.map → chunk-QZ7ODIVL.js.map} +0 -0
- /package/dist/{chunk-D7IXTY5E.js.map → chunk-TJ7HH5LB.js.map} +0 -0
- /package/dist/{chunk-V25ZAOSB.js.map → chunk-UOBLE67F.js.map} +0 -0
- /package/dist/{chunk-JIX3ZL2J.js.map → chunk-UVUTV7CM.js.map} +0 -0
- /package/dist/{chunk-SSOMTUCA.js.map → chunk-WXGTC424.js.map} +0 -0
- /package/dist/{chunk-KHGE6PMF.js.map → chunk-WXXLSZHA.js.map} +0 -0
- /package/dist/{chunk-DSLUOQDY.js.map → chunk-XMWF6AU3.js.map} +0 -0
- /package/dist/{chunk-DQY7NJ5L.js.map → chunk-XS2CWEHZ.js.map} +0 -0
|
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
|
|
4
4
|
import { parseConfig } from "./config.js";
|
|
5
|
+
import { resolveCapabilities } from "./capabilities.js";
|
|
5
6
|
import {
|
|
6
7
|
planRecallModeLLM,
|
|
7
8
|
resolveRecallPlannerLlmOptions,
|
|
@@ -40,7 +41,7 @@ test("returns heuristic without calling the LLM when recallPlannerLlmEnabled is
|
|
|
40
41
|
const captured: Array<Record<string, unknown>> = [];
|
|
41
42
|
const llm = stubLlm({ capturedOptions: captured, result: { mode: "no_recall" } });
|
|
42
43
|
|
|
43
|
-
const result = await planRecallModeLLM("what did we decide about auth?", undefined, config, llm);
|
|
44
|
+
const result = await planRecallModeLLM("what did we decide about auth?", undefined, config, llm, undefined, resolveCapabilities(config));
|
|
44
45
|
|
|
45
46
|
assert.equal(captured.length, 0, "LLM must not be contacted when disabled");
|
|
46
47
|
assert.equal(result.source, "heuristic");
|
|
@@ -54,7 +55,7 @@ test("uses the LLM classification when enabled", async () => {
|
|
|
54
55
|
const config = parseConfig({ recallPlannerLlmEnabled: true });
|
|
55
56
|
const llm = stubLlm({ result: { mode: "graph_mode", reason: "asks for root cause" }, modelUsed: "anthropic/claude" });
|
|
56
57
|
|
|
57
|
-
const result = await planRecallModeLLM("restart the gateway", undefined, config, llm);
|
|
58
|
+
const result = await planRecallModeLLM("restart the gateway", undefined, config, llm, undefined, resolveCapabilities(config));
|
|
58
59
|
|
|
59
60
|
assert.equal(result.source, "llm");
|
|
60
61
|
assert.equal(result.mode, "graph_mode");
|
|
@@ -74,7 +75,7 @@ test("forwards taskModelChain AND recallPlannerModel in gateway mode (provider-a
|
|
|
74
75
|
const captured: Array<Record<string, unknown>> = [];
|
|
75
76
|
const llm = stubLlm({ capturedOptions: captured, result: { mode: "minimal" } });
|
|
76
77
|
|
|
77
|
-
await planRecallModeLLM("check status", undefined, config, llm);
|
|
78
|
+
await planRecallModeLLM("check status", undefined, config, llm, undefined, resolveCapabilities(config));
|
|
78
79
|
|
|
79
80
|
assert.equal(captured.length, 1);
|
|
80
81
|
// recallPlannerModel is tried first (prepended), taskModelChain is the fallback chain.
|
|
@@ -97,7 +98,7 @@ test("plugin mode passes only the explicit model, no gateway chain", async () =>
|
|
|
97
98
|
const captured: Array<Record<string, unknown>> = [];
|
|
98
99
|
const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
|
|
99
100
|
|
|
100
|
-
await planRecallModeLLM("summarize the project", undefined, config, llm);
|
|
101
|
+
await planRecallModeLLM("summarize the project", undefined, config, llm, undefined, resolveCapabilities(config));
|
|
101
102
|
|
|
102
103
|
assert.equal(captured.length, 1);
|
|
103
104
|
assert.equal(captured[0]?.model, "openai/gpt-5.5");
|
|
@@ -109,7 +110,7 @@ test("falls back to heuristic when the LLM throws", async () => {
|
|
|
109
110
|
const config = parseConfig({ recallPlannerLlmEnabled: true });
|
|
110
111
|
const llm = stubLlm({ throwError: "boom" });
|
|
111
112
|
|
|
112
|
-
const result = await planRecallModeLLM("what happened during the outage?", undefined, config, llm);
|
|
113
|
+
const result = await planRecallModeLLM("what happened during the outage?", undefined, config, llm, undefined, resolveCapabilities(config));
|
|
113
114
|
|
|
114
115
|
assert.equal(result.source, "heuristic-fallback");
|
|
115
116
|
assert.equal(result.fallbackUsed, true);
|
|
@@ -123,7 +124,7 @@ test("falls back to heuristic when the LLM returns no parseable result", async (
|
|
|
123
124
|
const config = parseConfig({ recallPlannerLlmEnabled: true });
|
|
124
125
|
const llm = stubLlm({ result: null });
|
|
125
126
|
|
|
126
|
-
const result = await planRecallModeLLM("how did we get here?", undefined, config, llm);
|
|
127
|
+
const result = await planRecallModeLLM("how did we get here?", undefined, config, llm, undefined, resolveCapabilities(config));
|
|
127
128
|
|
|
128
129
|
assert.equal(result.source, "heuristic-fallback");
|
|
129
130
|
assert.equal(result.fallbackUsed, true);
|
|
@@ -139,7 +140,7 @@ test("falls back without a network attempt when the chain is empty and the model
|
|
|
139
140
|
const captured: Array<Record<string, unknown>> = [];
|
|
140
141
|
const llm = stubLlm({ available: false, capturedOptions: captured, result: { mode: "full" } });
|
|
141
142
|
|
|
142
|
-
const result = await planRecallModeLLM("anything", undefined, config, llm);
|
|
143
|
+
const result = await planRecallModeLLM("anything", undefined, config, llm, undefined, resolveCapabilities(config));
|
|
143
144
|
|
|
144
145
|
assert.equal(captured.length, 0, "no network attempt when nothing is routable");
|
|
145
146
|
assert.equal(result.source, "heuristic-fallback");
|
|
@@ -155,7 +156,7 @@ test("attempts the call (and falls back) when a provider-qualified model overrid
|
|
|
155
156
|
const captured: Array<Record<string, unknown>> = [];
|
|
156
157
|
const llm = stubLlm({ available: false, capturedOptions: captured, result: null });
|
|
157
158
|
|
|
158
|
-
const result = await planRecallModeLLM("anything", undefined, config, llm);
|
|
159
|
+
const result = await planRecallModeLLM("anything", undefined, config, llm, undefined, resolveCapabilities(config));
|
|
159
160
|
|
|
160
161
|
assert.equal(captured.length, 1, "qualified model override → still attempt the call");
|
|
161
162
|
assert.equal(captured[0]?.model, "openai/gpt-5.5");
|
|
@@ -170,7 +171,7 @@ test("an already-aborted recall short-circuits to the heuristic without an LLM c
|
|
|
170
171
|
const ac = new AbortController();
|
|
171
172
|
ac.abort();
|
|
172
173
|
|
|
173
|
-
const result = await planRecallModeLLM("what did we decide?", undefined, config, llm, ac.signal);
|
|
174
|
+
const result = await planRecallModeLLM("what did we decide?", undefined, config, llm, ac.signal, resolveCapabilities(config));
|
|
174
175
|
|
|
175
176
|
assert.equal(captured.length, 0, "no LLM call when the recall is already aborted");
|
|
176
177
|
assert.equal(result.source, "heuristic-fallback");
|
|
@@ -184,7 +185,7 @@ test("forwards the abort signal into the LLM call (cancellation contract)", asyn
|
|
|
184
185
|
const llm = stubLlm({ capturedOptions: captured, result: { mode: "minimal" } });
|
|
185
186
|
const ac = new AbortController();
|
|
186
187
|
|
|
187
|
-
await planRecallModeLLM("check status", undefined, config, llm, ac.signal);
|
|
188
|
+
await planRecallModeLLM("check status", undefined, config, llm, ac.signal, resolveCapabilities(config));
|
|
188
189
|
|
|
189
190
|
assert.equal(captured.length, 1);
|
|
190
191
|
assert.equal(captured[0]?.signal, ac.signal, "recall abort signal must reach FallbackLlmClient");
|
|
@@ -195,7 +196,7 @@ test("empty prompts skip the LLM entirely", async () => {
|
|
|
195
196
|
const captured: Array<Record<string, unknown>> = [];
|
|
196
197
|
const llm = stubLlm({ capturedOptions: captured, result: { mode: "full" } });
|
|
197
198
|
|
|
198
|
-
const result = await planRecallModeLLM(" ", undefined, config, llm);
|
|
199
|
+
const result = await planRecallModeLLM(" ", undefined, config, llm, undefined, resolveCapabilities(config));
|
|
199
200
|
|
|
200
201
|
assert.equal(captured.length, 0);
|
|
201
202
|
assert.equal(result.mode, "no_recall"); // heuristic returns no_recall for empty
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
import type { PluginConfig, RecallPlanMode } from "./types.js";
|
|
4
|
+
import type { CapabilitySet } from "./capabilities.js";
|
|
4
5
|
import { planRecallMode } from "./intent.js";
|
|
5
6
|
import {
|
|
6
7
|
FallbackLlmClient,
|
|
@@ -189,10 +190,15 @@ export async function planRecallModeLLM(
|
|
|
189
190
|
config: PluginConfig,
|
|
190
191
|
llm?: FallbackLlmClient,
|
|
191
192
|
signal?: AbortSignal,
|
|
193
|
+
caps?: CapabilitySet,
|
|
192
194
|
): Promise<RecallPlannerLlmResult> {
|
|
193
195
|
const heuristicMode = planRecallMode(prompt);
|
|
194
196
|
|
|
195
|
-
|
|
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;
|
|
201
|
+
if (!plannerLlmEnabled) {
|
|
196
202
|
return heuristicResult(heuristicMode, "heuristic", "llm-disabled", 0, false);
|
|
197
203
|
}
|
|
198
204
|
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import assert from "node:assert/strict";
|
|
30
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
30
|
+
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
31
31
|
import os from "node:os";
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
import test from "node:test";
|
|
@@ -356,6 +356,155 @@ test("question-queue items written by writeQuestion() do NOT leak into fallback
|
|
|
356
356
|
* directly with a config object that explicitly disables QMD, proving the disk
|
|
357
357
|
* scan a QMD-disabled deployment relies on returns every category.
|
|
358
358
|
*/
|
|
359
|
+
/**
|
|
360
|
+
* Symlink containment (same walker-hardening class as PR #1563 / issue #1546).
|
|
361
|
+
*
|
|
362
|
+
* `collectActiveMemoryPaths()` walks the RECALL_FALLBACK_DIRS category dirs for
|
|
363
|
+
* the QMD-unavailable filesystem recall fallback. A category dir symlinked
|
|
364
|
+
* outside `memoryDir` (e.g. `decisions/` -> an external directory) must NOT be
|
|
365
|
+
* followed — otherwise out-of-store files leak into recall results. The walker
|
|
366
|
+
* now `lstat`s each dir (skipping symlinks/non-dirs), realpaths it, and asserts
|
|
367
|
+
* it stays inside the memory root; per entry it skips symlinks and asserts the
|
|
368
|
+
* file resolves inside the root before including it.
|
|
369
|
+
*/
|
|
370
|
+
test("category dir symlinked outside memoryDir does NOT leak files into fallback recall", async (t) => {
|
|
371
|
+
if (process.platform === "win32") {
|
|
372
|
+
t.skip("directory symlink setup is platform-specific");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const baseDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-symlink-dir-"));
|
|
376
|
+
const outsideDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-symlink-out-"));
|
|
377
|
+
const storage = new StorageManager(baseDir);
|
|
378
|
+
StorageManager.clearAllStaticCaches();
|
|
379
|
+
storage.invalidateAllMemoriesCacheForDir();
|
|
380
|
+
try {
|
|
381
|
+
// A genuine in-store memory that MUST still be recalled.
|
|
382
|
+
await seedMemory(baseDir, "facts", "real-fact", "fact", "A genuine in-store fact.");
|
|
383
|
+
|
|
384
|
+
// An out-of-store memory reachable only via a symlinked category dir.
|
|
385
|
+
await writeFile(
|
|
386
|
+
path.join(outsideDir, "leaked.md"),
|
|
387
|
+
memoryFile("leaked-secret", "decision", "Out-of-store file must never be recalled."),
|
|
388
|
+
"utf-8",
|
|
389
|
+
);
|
|
390
|
+
// `decisions/` is a RECALL_FALLBACK_DIRS category dir; point it outside the store.
|
|
391
|
+
await symlink(outsideDir, path.join(baseDir, "decisions"), "dir");
|
|
392
|
+
|
|
393
|
+
storage.invalidateAllMemoriesCacheForDir();
|
|
394
|
+
const memories = await storage.readAllMemories();
|
|
395
|
+
const foundIds = new Set(memories.map((m) => m.frontmatter.id));
|
|
396
|
+
|
|
397
|
+
assert.ok(foundIds.has("real-fact"), "the genuine in-store fact must still be recalled");
|
|
398
|
+
assert.ok(
|
|
399
|
+
!foundIds.has("leaked-secret"),
|
|
400
|
+
"a symlinked-out category dir must not leak files into recall",
|
|
401
|
+
);
|
|
402
|
+
assert.equal(memories.length, 1, "only the in-store memory should be returned");
|
|
403
|
+
} finally {
|
|
404
|
+
StorageManager.clearAllStaticCaches();
|
|
405
|
+
await rm(baseDir, { recursive: true, force: true });
|
|
406
|
+
await rm(outsideDir, { recursive: true, force: true });
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("symlinked entry nested inside a category dir does NOT leak into fallback recall", async (t) => {
|
|
411
|
+
if (process.platform === "win32") {
|
|
412
|
+
t.skip("directory symlink setup is platform-specific");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const baseDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-symlink-entry-"));
|
|
416
|
+
const outsideDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-symlink-entry-out-"));
|
|
417
|
+
const storage = new StorageManager(baseDir);
|
|
418
|
+
StorageManager.clearAllStaticCaches();
|
|
419
|
+
storage.invalidateAllMemoriesCacheForDir();
|
|
420
|
+
try {
|
|
421
|
+
// A genuine in-store memory in the same category dir that holds the symlink.
|
|
422
|
+
await seedMemory(baseDir, "facts", "real-fact", "fact", "A genuine in-store fact.");
|
|
423
|
+
|
|
424
|
+
await writeFile(
|
|
425
|
+
path.join(outsideDir, "leaked.md"),
|
|
426
|
+
memoryFile("nested-leak", "fact", "Out-of-store nested file must never be recalled."),
|
|
427
|
+
"utf-8",
|
|
428
|
+
);
|
|
429
|
+
// A symlinked *entry* inside facts/ that escapes the store.
|
|
430
|
+
await symlink(outsideDir, path.join(baseDir, "facts", "escape"), "dir");
|
|
431
|
+
|
|
432
|
+
storage.invalidateAllMemoriesCacheForDir();
|
|
433
|
+
const memories = await storage.readAllMemories();
|
|
434
|
+
const foundIds = new Set(memories.map((m) => m.frontmatter.id));
|
|
435
|
+
|
|
436
|
+
assert.ok(foundIds.has("real-fact"), "the genuine in-store fact must still be recalled");
|
|
437
|
+
assert.ok(
|
|
438
|
+
!foundIds.has("nested-leak"),
|
|
439
|
+
"a symlinked entry escaping the store must not leak files into recall",
|
|
440
|
+
);
|
|
441
|
+
assert.equal(memories.length, 1, "only the in-store memory should be returned");
|
|
442
|
+
} finally {
|
|
443
|
+
StorageManager.clearAllStaticCaches();
|
|
444
|
+
await rm(baseDir, { recursive: true, force: true });
|
|
445
|
+
await rm(outsideDir, { recursive: true, force: true });
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Regression for Cursor Bugbot "Poisoned md skips sibling subdirs".
|
|
451
|
+
*
|
|
452
|
+
* Per-entry containment/realpath failures must be isolated so they cannot drop
|
|
453
|
+
* sibling `.md` files or, crucially, the nested-subdir recursion. This asserts
|
|
454
|
+
* that a category dir which also contains a symlinked-out entry still recurses
|
|
455
|
+
* into its real nested subdirectory and returns every in-store memory, while
|
|
456
|
+
* the out-of-store target stays excluded.
|
|
457
|
+
*/
|
|
458
|
+
test("a guarded/skipped sibling entry does NOT drop nested in-store memories", async (t) => {
|
|
459
|
+
if (process.platform === "win32") {
|
|
460
|
+
t.skip("directory symlink setup is platform-specific");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const baseDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-sibling-"));
|
|
464
|
+
const outsideDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-sibling-out-"));
|
|
465
|
+
const storage = new StorageManager(baseDir);
|
|
466
|
+
StorageManager.clearAllStaticCaches();
|
|
467
|
+
storage.invalidateAllMemoriesCacheForDir();
|
|
468
|
+
try {
|
|
469
|
+
const factsDir = path.join(baseDir, "facts");
|
|
470
|
+
await mkdir(factsDir, { recursive: true });
|
|
471
|
+
// A top-level in-store memory and a nested in-store memory in the SAME
|
|
472
|
+
// category dir that also holds the symlinked-out sibling.
|
|
473
|
+
await writeFile(
|
|
474
|
+
path.join(factsDir, "top.md"),
|
|
475
|
+
memoryFile("top-fact", "fact", "Top-level in-store fact."),
|
|
476
|
+
"utf-8",
|
|
477
|
+
);
|
|
478
|
+
await seedMemory(baseDir, "facts", "deep-fact", "fact", "Deeply nested in-store fact.", {
|
|
479
|
+
nested: true,
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
await writeFile(
|
|
483
|
+
path.join(outsideDir, "leaked.md"),
|
|
484
|
+
memoryFile("leaked-secret", "fact", "Out-of-store file must never be recalled."),
|
|
485
|
+
"utf-8",
|
|
486
|
+
);
|
|
487
|
+
// A symlinked-out sibling entry alongside the real files/subdir.
|
|
488
|
+
await symlink(outsideDir, path.join(factsDir, "escape"), "dir");
|
|
489
|
+
|
|
490
|
+
storage.invalidateAllMemoriesCacheForDir();
|
|
491
|
+
const memories = await storage.readAllMemories();
|
|
492
|
+
const foundIds = new Set(memories.map((m) => m.frontmatter.id));
|
|
493
|
+
|
|
494
|
+
assert.ok(foundIds.has("top-fact"), "the top-level in-store fact must be recalled");
|
|
495
|
+
assert.ok(
|
|
496
|
+
foundIds.has("deep-fact"),
|
|
497
|
+
"the nested in-store fact must still be recalled past a skipped sibling",
|
|
498
|
+
);
|
|
499
|
+
assert.ok(!foundIds.has("leaked-secret"), "the symlinked-out file must not leak");
|
|
500
|
+
assert.equal(memories.length, 2, "exactly the two in-store memories should be returned");
|
|
501
|
+
} finally {
|
|
502
|
+
StorageManager.clearAllStaticCaches();
|
|
503
|
+
await rm(baseDir, { recursive: true, force: true });
|
|
504
|
+
await rm(outsideDir, { recursive: true, force: true });
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
359
508
|
test("QMD-disabled deployment: disk-scan collector returns all categories", async () => {
|
|
360
509
|
const baseDir = await mkdtemp(path.join(os.tmpdir(), "engram-1497-qmd-off-"));
|
|
361
510
|
// entitySchemas is the only other constructor arg; QMD is never constructed by
|
package/src/storage.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { access, readdir, readFile, stat, writeFile, mkdir, unlink, rename, appendFile, open } from "node:fs/promises";
|
|
2
|
-
import { appendFileSync, createReadStream, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { access, lstat, readdir, readFile, realpath, stat, writeFile, mkdir, unlink, rename, appendFile, open } from "node:fs/promises";
|
|
2
|
+
import { appendFileSync, createReadStream, mkdirSync, readFileSync, statSync, type Dirent } from "node:fs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { log } from "./logger.js";
|
|
6
6
|
import { isErrnoCode } from "./utils/errno.js";
|
|
7
7
|
import { RECALL_FALLBACK_DIRS, getCategoryDir, categoryDirName } from "./utils/category-dir.js";
|
|
8
|
+
import { assertPathInsideRoot } from "./utils/path-containment.js";
|
|
8
9
|
import { getCachedEntities, invalidateAllForDir, setCachedEntities } from "./memory-cache.js";
|
|
9
10
|
import { rotateMarkdownFileToArchive } from "./hygiene.js";
|
|
10
11
|
import { sanitizeMemoryContent } from "./sanitize.js";
|
|
@@ -4082,23 +4083,59 @@ export class StorageManager {
|
|
|
4082
4083
|
private async collectActiveMemoryPaths(): Promise<string[]> {
|
|
4083
4084
|
const filePaths: string[] = [];
|
|
4084
4085
|
|
|
4086
|
+
// Resolve the memory root once for containment checks below. A category dir
|
|
4087
|
+
// symlinked outside memoryDir (e.g. decisions/ -> an external dir) must NOT
|
|
4088
|
+
// pull out-of-store files into the QMD-unavailable recall fallback (info
|
|
4089
|
+
// leak). Same walker-hardening pattern as document-scanner.ts / cli.ts /
|
|
4090
|
+
// consolidation-provenance-check.ts; reuses the shared containment helper.
|
|
4091
|
+
let memoryRootReal: string;
|
|
4092
|
+
try {
|
|
4093
|
+
memoryRootReal = await realpath(this.baseDir);
|
|
4094
|
+
} catch {
|
|
4095
|
+
return filePaths;
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4085
4098
|
const collectPaths = async (dir: string) => {
|
|
4099
|
+
// Directory-level guard, isolated from per-entry handling: skip symlinked
|
|
4100
|
+
// or non-directory category dirs and assert the resolved dir stays inside
|
|
4101
|
+
// the memory root before reading. A failure here means the whole subtree
|
|
4102
|
+
// does not exist or escaped the store — fail closed by skipping it.
|
|
4103
|
+
let entries: Dirent[];
|
|
4086
4104
|
try {
|
|
4087
|
-
const
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4105
|
+
const dirStat = await lstat(dir);
|
|
4106
|
+
if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) return;
|
|
4107
|
+
assertPathInsideRoot(memoryRootReal, await realpath(dir), dir);
|
|
4108
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
4109
|
+
} catch {
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
const subdirs: string[] = [];
|
|
4114
|
+
for (const entry of entries) {
|
|
4115
|
+
// Never follow symlinked entries out of the store.
|
|
4116
|
+
if (entry.isSymbolicLink()) continue;
|
|
4117
|
+
const fullPath = path.join(dir, entry.name);
|
|
4118
|
+
if (entry.isDirectory()) {
|
|
4119
|
+
subdirs.push(fullPath);
|
|
4120
|
+
} else if (entry.name.endsWith(".md")) {
|
|
4121
|
+
// Isolate per-entry failures in their own try/catch: a containment or
|
|
4122
|
+
// realpath failure on ONE .md entry must not drop sibling files or,
|
|
4123
|
+
// crucially, the deferred subdir recursion below (Cursor Bugbot:
|
|
4124
|
+
// "Poisoned md skips sibling subdirs"). Mirrors the per-file try/catch
|
|
4125
|
+
// in search/document-scanner.ts scanDir and
|
|
4126
|
+
// consolidation-provenance-check.ts walkMarkdownFiles.
|
|
4127
|
+
try {
|
|
4128
|
+
assertPathInsideRoot(memoryRootReal, await realpath(fullPath), fullPath);
|
|
4094
4129
|
filePaths.push(fullPath);
|
|
4130
|
+
} catch {
|
|
4131
|
+
// Skip just this entry (symlink/containment/realpath failure).
|
|
4095
4132
|
}
|
|
4096
4133
|
}
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4134
|
+
}
|
|
4135
|
+
// Recurse into real subdirectories regardless of any single poisoned entry
|
|
4136
|
+
// above, so valid nested in-store memories are never dropped.
|
|
4137
|
+
for (const subdir of subdirs) {
|
|
4138
|
+
await collectPaths(subdir);
|
|
4102
4139
|
}
|
|
4103
4140
|
};
|
|
4104
4141
|
|
|
@@ -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 `config.recallDirectAnswerEnabled === false`, the function\n * returns the eligibility verdict with reason `\"disabled\"` without\n * touching any source 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 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, sources, queryEntityRefs, abortSignal } = input;\n\n const eligibilityConfig: DirectAnswerConfig = {\n 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"]}
|