@remnic/core 9.69.70 → 9.69.72

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 (74) hide show
  1. package/dist/access-admin-ops-surface.js +2 -2
  2. package/dist/access-authorization-probe.js +3 -3
  3. package/dist/access-boundary.js +2 -2
  4. package/dist/access-cli.js +17 -17
  5. package/dist/access-extraction-force-flush.js +2 -2
  6. package/dist/access-http-briefing.js +3 -3
  7. package/dist/access-http-query.js +2 -2
  8. package/dist/access-http.js +14 -14
  9. package/dist/access-identity-continuity-surface.js +2 -2
  10. package/dist/access-lcm-surface.js +2 -2
  11. package/dist/access-mcp.js +9 -9
  12. package/dist/access-namespace-preflight.js +2 -2
  13. package/dist/access-observe-write-surface.js +2 -2
  14. package/dist/access-operations-batch.js +3 -3
  15. package/dist/access-operations.js +8 -8
  16. package/dist/access-recall-concurrency.js +2 -2
  17. package/dist/access-recall-response.js +2 -2
  18. package/dist/access-recall-surface.js +2 -2
  19. package/dist/access-service.js +2 -2
  20. package/dist/access-surface-catalog.js +4 -4
  21. package/dist/{chunk-WIQDQQPW.js → chunk-2XGPN5TL.js} +2 -2
  22. package/dist/{chunk-GXHUFKAG.js → chunk-2ZU6CYTJ.js} +2 -2
  23. package/dist/{chunk-AJBWVDRV.js → chunk-3X5OPDPF.js} +3 -3
  24. package/dist/chunk-3X5OPDPF.js.map +1 -0
  25. package/dist/{chunk-YLZPCYCB.js → chunk-5I63ZRIH.js} +2 -2
  26. package/dist/{chunk-PT7D26KT.js → chunk-7FL25VH3.js} +3 -3
  27. package/dist/{chunk-6LQ7GDTI.js → chunk-ADWCQWK3.js} +2 -2
  28. package/dist/{chunk-2R72MVH7.js → chunk-BIKX37D2.js} +15 -9
  29. package/dist/{chunk-2R72MVH7.js.map → chunk-BIKX37D2.js.map} +1 -1
  30. package/dist/{chunk-T5TAT2XL.js → chunk-HW4DKAAA.js} +2 -2
  31. package/dist/{chunk-66NK4G3A.js → chunk-ICMXLHSC.js} +2 -2
  32. package/dist/{chunk-KTYBS2UG.js → chunk-KNPWG3VI.js} +4 -4
  33. package/dist/{chunk-GULV5LFB.js → chunk-ONEUTVGJ.js} +2 -2
  34. package/dist/{chunk-IKDP2PN5.js → chunk-QVWNNN64.js} +4 -4
  35. package/dist/{chunk-EGXTKSSA.js → chunk-RFVF5PLN.js} +2 -2
  36. package/dist/{chunk-4FLIMX4B.js → chunk-VDKZTNG5.js} +2 -2
  37. package/dist/{chunk-JHAFEGED.js → chunk-W7PY7IIB.js} +8 -8
  38. package/dist/{chunk-NGN2VAKB.js → chunk-YDMN7TUM.js} +2 -2
  39. package/dist/{chunk-SQIEPKIU.js → chunk-YEHKKZ5T.js} +2 -2
  40. package/dist/cli.js +15 -15
  41. package/dist/deep-recall-seeds.d.ts +3 -1
  42. package/dist/deep-recall-seeds.d.ts.map +1 -1
  43. package/dist/deep-recall-seeds.js +1 -1
  44. package/dist/external-wiki-access.js +3 -3
  45. package/dist/index.js +17 -17
  46. package/dist/memory-browse-operations.js +3 -3
  47. package/dist/orchestrator.js +17 -17
  48. package/dist/recall-navigation-operations.js +3 -3
  49. package/dist/recall-navigation.js +2 -2
  50. package/dist/support-passport/index.js +5 -5
  51. package/dist/support-passport/private-file.d.ts.map +1 -1
  52. package/package.json +2 -2
  53. package/src/deep-recall-seeds.test.ts +35 -0
  54. package/src/deep-recall-seeds.ts +5 -4
  55. package/src/deep-recall-surface.test.ts +5 -8
  56. package/src/support-passport/grant-service.test.ts +45 -20
  57. package/src/support-passport/private-file.test.ts +10 -5
  58. package/src/support-passport/private-file.ts +14 -7
  59. package/dist/chunk-AJBWVDRV.js.map +0 -1
  60. /package/dist/{chunk-WIQDQQPW.js.map → chunk-2XGPN5TL.js.map} +0 -0
  61. /package/dist/{chunk-GXHUFKAG.js.map → chunk-2ZU6CYTJ.js.map} +0 -0
  62. /package/dist/{chunk-YLZPCYCB.js.map → chunk-5I63ZRIH.js.map} +0 -0
  63. /package/dist/{chunk-PT7D26KT.js.map → chunk-7FL25VH3.js.map} +0 -0
  64. /package/dist/{chunk-6LQ7GDTI.js.map → chunk-ADWCQWK3.js.map} +0 -0
  65. /package/dist/{chunk-T5TAT2XL.js.map → chunk-HW4DKAAA.js.map} +0 -0
  66. /package/dist/{chunk-66NK4G3A.js.map → chunk-ICMXLHSC.js.map} +0 -0
  67. /package/dist/{chunk-KTYBS2UG.js.map → chunk-KNPWG3VI.js.map} +0 -0
  68. /package/dist/{chunk-GULV5LFB.js.map → chunk-ONEUTVGJ.js.map} +0 -0
  69. /package/dist/{chunk-IKDP2PN5.js.map → chunk-QVWNNN64.js.map} +0 -0
  70. /package/dist/{chunk-EGXTKSSA.js.map → chunk-RFVF5PLN.js.map} +0 -0
  71. /package/dist/{chunk-4FLIMX4B.js.map → chunk-VDKZTNG5.js.map} +0 -0
  72. /package/dist/{chunk-JHAFEGED.js.map → chunk-W7PY7IIB.js.map} +0 -0
  73. /package/dist/{chunk-NGN2VAKB.js.map → chunk-YDMN7TUM.js.map} +0 -0
  74. /package/dist/{chunk-SQIEPKIU.js.map → chunk-YEHKKZ5T.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.69.70",
3
+ "version": "9.69.72",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3317,7 +3317,7 @@
3317
3317
  "core"
3318
3318
  ],
3319
3319
  "peerDependencies": {
3320
- "@remnic/coding-graph": "^9.69.70"
3320
+ "@remnic/coding-graph": "^9.69.72"
3321
3321
  },
3322
3322
  "peerDependenciesMeta": {
3323
3323
  "@remnic/coding-graph": {
@@ -51,3 +51,38 @@ test("seed search forwards the cancellation signal and the capped limit to the r
51
51
  assert.equal(captured?.execution?.signal, controller.signal, "the transport signal reaches the search router");
52
52
  assert.equal(resolverCalls, 2, "each returned hit is resolved per hit");
53
53
  });
54
+
55
+ test("resolve-miss drops the hit instead of admitting the bare docid (issue #3087)", async () => {
56
+ const resolvedPaths: string[] = [];
57
+ const router: DeepRecallSeedRouter = {
58
+ async searchAcrossNamespaces() {
59
+ return [
60
+ { path: "entities/company-x.md", docid: "foreign-doc-42", score: 0.7 },
61
+ { path: "facts/2026-09-12/fact-good.md", docid: "doc-good", score: 0.6 },
62
+ { path: "", docid: "doc-nopath", score: 0.5 },
63
+ ];
64
+ },
65
+ };
66
+ const searchSeed = createDeepRecallSeedSearch({
67
+ namespace: "ns_3087",
68
+ storage: { dir: "/synthetic/ns_3087", readMemoryByPath: async () => null },
69
+ router,
70
+ resolver: {
71
+ async readQmdResultMemory(resultPath) {
72
+ resolvedPaths.push(resultPath);
73
+ return resultPath === "facts/2026-09-12/fact-good.md"
74
+ ? { frontmatter: { id: "fact-good-123" } }
75
+ : null;
76
+ },
77
+ },
78
+ });
79
+
80
+ const seeds = await searchSeed("company x", 3);
81
+
82
+ assert.deepEqual(
83
+ seeds,
84
+ [{ memoryId: "fact-good-123", score: 0.6 }],
85
+ "a hit the resolver cannot resolve is dropped, never admitted by its bare docid",
86
+ );
87
+ assert.ok(!resolvedPaths.includes(""), "an empty path is skipped before the resolver");
88
+ });
@@ -15,7 +15,9 @@
15
15
  * and the two forms it cannot decode (`qmd:///`, a leading `/`) are stripped
16
16
  * for one retry. A hit with no path is dropped: its namespace membership
17
17
  * cannot be verified, and a bare docid from a foreign collection must never
18
- * enter the working set as a resolved memory.
18
+ * enter the working set as a resolved memory. A hit with a path that still
19
+ * fails to resolve is dropped for the same reason: the resolver miss leaves
20
+ * the docid as the only identity, and a docid is not a memory id.
19
21
  *
20
22
  * An unavailable namespace backend (or a missing collection) contributes an
21
23
  * empty result set and reports itself only through `execution.onDegradation`,
@@ -127,9 +129,8 @@ export function createDeepRecallSeedSearch(deps: {
127
129
  const seeds: DeepRecallSeedHit[] = [];
128
130
  for (const hit of hits) {
129
131
  if (typeof hit.path !== "string" || hit.path.length === 0) continue;
130
- const memoryId =
131
- (await resolveSeedMemoryId(deps.resolver, deps.storage, deps.namespace, hit.path)) ?? hit.docid;
132
- if (typeof memoryId !== "string" || memoryId.length === 0) continue;
132
+ const memoryId = await resolveSeedMemoryId(deps.resolver, deps.storage, deps.namespace, hit.path);
133
+ if (memoryId === null) continue;
133
134
  seeds.push({
134
135
  memoryId,
135
136
  score: typeof hit.score === "number" && Number.isFinite(hit.score) ? hit.score : 0,
@@ -870,8 +870,8 @@ test("deep recall seeds resolve collection-qualified paths when namespaces are d
870
870
  path: rawPath,
871
871
  score: 0.5 + index / 10,
872
872
  })),
873
- // A foreign-collection hit stays unresolvable: it keeps its docid and
874
- // still seeds the working set, but never hydrates into the graph.
873
+ // A foreign-collection hit is unresolvable: skip it. Admitting the
874
+ // bare docid would violate the foreign-collection guard (#3087).
875
875
  { docid: "hash-doc-foreign", path: "foreign-coll/facts/2026-08-01/fact-1.md", score: 0.9 },
876
876
  ],
877
877
  };
@@ -885,15 +885,12 @@ test("deep recall seeds resolve collection-qualified paths when namespaces are d
885
885
 
886
886
  assert.deepEqual(
887
887
  seeds.map((seed) => seed.memoryId),
888
- [
889
- ...rawPaths.map(() => "mem-seed-1"),
890
- "hash-doc-foreign",
891
- ],
892
- "every collection-qualified raw path form must resolve to the frontmatter id; only a foreign collection keeps its docid",
888
+ rawPaths.map(() => "mem-seed-1"),
889
+ "every collection-qualified raw path form must resolve to the frontmatter id; a foreign collection is dropped",
893
890
  );
894
891
  assert.deepEqual(
895
892
  seeds.map((seed) => seed.score),
896
- [0.5, 0.6, 0.7, 0.8, 0.9],
893
+ [0.5, 0.6, 0.7, 0.8],
897
894
  "resolution must not disturb the hit scores",
898
895
  );
899
896
  } finally {
@@ -29,6 +29,7 @@ import {
29
29
  readPrivateFileNoFollow,
30
30
  removePrivateFilesNoFollow,
31
31
  requirePrivateFileDescriptorRoot,
32
+ withPrivateDirectoryNoFollow,
32
33
  writePrivateFileAtomicallyNoFollow,
33
34
  } from "./private-file.js";
34
35
 
@@ -2865,15 +2866,7 @@ test("the grant store rejects a symlinked grant directory", async () => {
2865
2866
  }
2866
2867
  });
2867
2868
 
2868
- test("private file operations select supported directory access strategies", () => {
2869
- assert.throws(
2870
- () =>
2871
- requirePrivateFileDescriptorRoot(
2872
- "win32",
2873
- "private file directory cannot be pinned",
2874
- ),
2875
- /private file directory cannot be pinned/,
2876
- );
2869
+ test("private file operations select supported directory access strategies", async () => {
2877
2870
  assert.equal(
2878
2871
  requirePrivateFileDescriptorRoot("linux", "unreachable"),
2879
2872
  "/proc/self/fd",
@@ -2882,6 +2875,28 @@ test("private file operations select supported directory access strategies", ()
2882
2875
  requirePrivateFileDescriptorRoot("darwin", "unreachable"),
2883
2876
  "/dev/fd",
2884
2877
  );
2878
+ assert.throws(
2879
+ () => requirePrivateFileDescriptorRoot("sunos", "unreachable"),
2880
+ /unreachable/,
2881
+ );
2882
+
2883
+ // win32 has no POSIX directory-fd root; pinning degrades to the resolved
2884
+ // stable path while the no-follow ancestor walk and containment asserts stay.
2885
+ const root = await mkdtemp(path.join(tmpdir(), "remnic-support-private-win32-"));
2886
+ try {
2887
+ const target = path.join(root, "state", "memory");
2888
+ await mkdir(target, { recursive: true });
2889
+ const pinned = await withPrivateDirectoryNoFollow(
2890
+ root,
2891
+ target,
2892
+ "private file directory cannot be pinned",
2893
+ async (pinnedDirectory) => pinnedDirectory,
2894
+ "win32",
2895
+ );
2896
+ assert.equal(pinned, path.resolve(target));
2897
+ } finally {
2898
+ await rm(root, { recursive: true, force: true });
2899
+ }
2885
2900
  });
2886
2901
 
2887
2902
  test("private directory creation syncs every verified parent entry", async () => {
@@ -3105,25 +3120,35 @@ test("the grant store rejects a symlink alias in the configured memory root", as
3105
3120
  }
3106
3121
  });
3107
3122
 
3108
- test("Windows private-file operations fail before mutation", async () => {
3123
+ test("Windows private-file operations pin without a POSIX descriptor root", async () => {
3109
3124
  const root = await mkdtemp(path.join(tmpdir(), "remnic-support-private-win32-"));
3110
3125
  try {
3111
3126
  const directory = path.join(root, "state", "support-passport", "grants");
3112
- const filePath = path.join(directory, "grant.json");
3113
3127
  const errorMessage = "private Windows file operation failed";
3128
+ await ensurePrivateDirectoryNoFollow(root, directory, errorMessage, undefined, true, "win32");
3129
+ assert.equal((await lstat(directory)).isDirectory(), true);
3130
+
3131
+ const liveParent = path.join(root, "live");
3132
+ const parkedParent = path.join(root, "parked");
3133
+ const outside = path.join(root, "outside");
3134
+ await mkdir(liveParent);
3135
+ await mkdir(path.join(outside, "memory"), { recursive: true });
3136
+ await renameSync(liveParent, parkedParent);
3137
+ await symlink(outside, liveParent, "dir");
3138
+
3114
3139
  await assert.rejects(
3115
- ensurePrivateDirectoryNoFollow(root, directory, errorMessage, undefined, true, "win32"),
3116
- new RegExp(errorMessage)
3117
- );
3118
- assert.equal(
3119
- await lstat(directory).then(
3120
- () => true,
3121
- () => false
3140
+ ensurePrivateDirectoryNoFollow(
3141
+ root,
3142
+ path.join(liveParent, "memory"),
3143
+ errorMessage,
3144
+ undefined,
3145
+ true,
3146
+ "win32",
3122
3147
  ),
3123
- false
3148
+ new RegExp(errorMessage),
3124
3149
  );
3125
3150
  assert.equal(
3126
- await lstat(filePath).then(
3151
+ await lstat(path.join(outside, "memory", "state")).then(
3127
3152
  () => true,
3128
3153
  () => false
3129
3154
  ),
@@ -30,16 +30,21 @@ test("private append cannot escape its pinned directory", async (context) => {
30
30
  await assert.rejects(readFile(outsidePath, "utf8"), { code: "ENOENT" });
31
31
  });
32
32
 
33
- test("private append fails before mutation when descriptor pinning is unavailable", async (context) => {
33
+ test("private append writes through a path-pinned directory on win32", async (context) => {
34
34
  const root = await mkdtemp(path.join(os.tmpdir(), "remnic-private-append-win32-"));
35
35
  context.after(async () => await rm(root, { recursive: true, force: true }));
36
36
  const privateDirectory = path.join(root, "private");
37
+ await mkdir(privateDirectory);
37
38
  const auditPath = path.join(privateDirectory, "audit.jsonl");
38
- await assert.rejects(
39
- ensurePrivateDirectoryNoFollow(root, privateDirectory, "private path escaped", undefined, true, "win32"),
40
- /private path escaped/
39
+ await appendPrivateFileNoFollow(
40
+ privateDirectory,
41
+ auditPath,
42
+ "private\n",
43
+ "private path escaped",
44
+ root,
45
+ "win32",
41
46
  );
42
- await assert.rejects(readFile(auditPath, "utf8"), { code: "ENOENT" });
47
+ assert.equal(await readFile(auditPath, "utf8"), "private\n");
43
48
  });
44
49
 
45
50
  test("private append rejects a renamed target after opening it", async (context) => {
@@ -104,6 +104,7 @@ export async function resolvePrivateDirectoryPath(
104
104
  errorMessage: string,
105
105
  platform: NodeJS.Platform = process.platform
106
106
  ): Promise<string> {
107
+ if (pinsDirectoriesByPath(platform)) return path.resolve(directory);
107
108
  const descriptorRoot = requirePrivateFileDescriptorRoot(platform, errorMessage);
108
109
  const pinnedPath = path.join(descriptorRoot, String(handle.fd));
109
110
  const metadata = await stat(pinnedPath);
@@ -125,6 +126,10 @@ export function requirePrivateFileDescriptorRoot(platform: NodeJS.Platform, erro
125
126
  throw new Error(errorMessage);
126
127
  }
127
128
 
129
+ function pinsDirectoriesByPath(platform: NodeJS.Platform): boolean {
130
+ return platform === "win32";
131
+ }
132
+
128
133
  async function openStableDirectoryFromRoot(
129
134
  trustedRoot: string,
130
135
  directory: string,
@@ -140,7 +145,7 @@ async function openStableDirectoryFromRoot(
140
145
  const root = path.resolve(trustedRoot);
141
146
  const target = path.resolve(directory);
142
147
  assertContainedPath(root, target, errorMessage);
143
- requirePrivateFileDescriptorRoot(platform, errorMessage);
148
+ if (!pinsDirectoriesByPath(platform)) requirePrivateFileDescriptorRoot(platform, errorMessage);
144
149
  const relative = path.relative(root, target);
145
150
  const components = relative === "" ? [] : relative.split(path.sep);
146
151
  const handles: FileHandle[] = [];
@@ -149,7 +154,7 @@ async function openStableDirectoryFromRoot(
149
154
  let current = await openDirectoryNoFollow(root, errorMessage);
150
155
  handles.push(current.handle);
151
156
  for (const component of components) {
152
- const pinnedParent = await resolvePrivateDirectoryPath(currentPath, current.handle, current.opened, errorMessage);
157
+ const pinnedParent = await resolvePrivateDirectoryPath(currentPath, current.handle, current.opened, errorMessage, platform);
153
158
  const child = await openDirectoryNoFollow(path.join(pinnedParent, component), errorMessage);
154
159
  handles.push(child.handle);
155
160
  assertStableDirectory(current.before, current.opened, await lstat(currentPath), errorMessage);
@@ -186,7 +191,7 @@ export async function ensurePrivateDirectoryNoFollow(
186
191
  const target = path.resolve(directory);
187
192
  assertContainedPath(root, target, errorMessage);
188
193
  const relative = path.relative(root, target);
189
- requirePrivateFileDescriptorRoot(platform, errorMessage);
194
+ if (!pinsDirectoriesByPath(platform)) requirePrivateFileDescriptorRoot(platform, errorMessage);
190
195
  const components = relative === "" ? [] : relative.split(path.sep);
191
196
  const handles: FileHandle[] = [];
192
197
  let currentPath = root;
@@ -194,7 +199,7 @@ export async function ensurePrivateDirectoryNoFollow(
194
199
  let current = await openDirectoryNoFollow(root, errorMessage);
195
200
  handles.push(current.handle);
196
201
  for (const component of components) {
197
- const pinnedParent = await resolvePrivateDirectoryPath(currentPath, current.handle, current.opened, errorMessage);
202
+ const pinnedParent = await resolvePrivateDirectoryPath(currentPath, current.handle, current.opened, errorMessage, platform);
198
203
  const childPath = path.join(pinnedParent, component);
199
204
  let created = false;
200
205
  try {
@@ -393,10 +398,12 @@ export async function appendPrivateFileInPinnedDirectory(
393
398
  errorMessage: string,
394
399
  platform: NodeJS.Platform = process.platform
395
400
  ): Promise<void> {
396
- const descriptorRoot = requirePrivateFileDescriptorRoot(platform, errorMessage);
397
401
  const resolvedDirectory = path.resolve(pinnedDirectory);
398
- const descriptor = path.relative(descriptorRoot, resolvedDirectory);
399
- if (!/^\d+$/.test(descriptor)) throw new Error(errorMessage);
402
+ if (!pinsDirectoriesByPath(platform)) {
403
+ const descriptorRoot = requirePrivateFileDescriptorRoot(platform, errorMessage);
404
+ const descriptor = path.relative(descriptorRoot, resolvedDirectory);
405
+ if (!/^\d+$/.test(descriptor)) throw new Error(errorMessage);
406
+ }
400
407
  const before = await stat(resolvedDirectory);
401
408
  if (!before.isDirectory()) throw new Error(errorMessage);
402
409
  let directoryHandle: FileHandle | undefined;
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/deep-recall-seeds.ts"],"sourcesContent":["/**\n * Deep-recall seed retrieval (issue #2332 review).\n *\n * Seeds route through the SAME namespace search router the rest of the read\n * path uses, so a non-default caller searches its own suffixed collection\n * instead of the base (default-namespace) collection — read and write resolve\n * through one resolver, never two (§30).\n *\n * Each hit is resolved to a real memory id through the shared QMD result\n * resolver (`QmdResultResolver.readQmdResultMemory`), per hit and bounded by\n * the requested limit, instead of pre-scanning the whole namespace corpus to\n * build a path index. With namespaces disabled the fanout queries the QMD\n * backend directly, so hits keep the indexer's raw path forms; the resolver\n * decodes `qmd://<collection>/` URIs and `<collection>/`-prefixed relatives,\n * and the two forms it cannot decode (`qmd:///`, a leading `/`) are stripped\n * for one retry. A hit with no path is dropped: its namespace membership\n * cannot be verified, and a bare docid from a foreign collection must never\n * enter the working set as a resolved memory.\n *\n * An unavailable namespace backend (or a missing collection) contributes an\n * empty result set and reports itself only through `execution.onDegradation`,\n * so the observer is REQUIRED here: without it a dead index is indistinguishable\n * from a healthy empty one and deep recall would answer `ok: true` with zero\n * entries instead of the advertised `backend_unavailable` failure.\n */\n\nimport type { DeepRecallSeedHit } from \"./deep-recall.js\";\nimport type { SearchDegradation } from \"./search/port.js\";\n\n/** Namespace-scoped search entrypoint (satisfied by `Orchestrator.searchAcrossNamespaces`). */\nexport interface DeepRecallSeedRouter {\n searchAcrossNamespaces(options: {\n query: string;\n namespaces: string[];\n maxResults?: number;\n execution?: {\n signal?: AbortSignal;\n onDegradation?: (degradation: SearchDegradation) => void;\n };\n }): Promise<ReadonlyArray<{ path?: string; docid?: string; score?: number }>>;\n}\n\n/** Namespace-scoped storage read (satisfied by `StorageManager`). */\nexport interface DeepRecallSeedStorage {\n readonly dir: string;\n readMemoryByPath(filePath: string): Promise<{ frontmatter: { id?: string } } | null>;\n}\n\n/** Full QMD hit -> memory resolver (satisfied by `QmdResultResolver#readQmdResultMemory`). */\nexport interface DeepRecallSeedResultResolver {\n readQmdResultMemory(\n resultPath: string,\n fallbackStorage: DeepRecallSeedStorage,\n recallNamespaces?: readonly string[],\n preferredNamespace?: string,\n ): Promise<{ frontmatter: { id?: string } } | null>;\n}\n\n/**\n * Strip the collection-qualified forms the indexer can emit but\n * `readQmdResultMemory` cannot decode: the empty-hostname `qmd:///` URI and\n * the root-relative `/` path both reach its fallback probe still prefixed and\n * miss. Returns `null` when the path carries no such prefix — including a\n * decodable `qmd://<collection>/` or `<collection>/` form, which the resolver\n * owns, and a foreign collection prefix, which must stay a miss.\n */\nfunction collectionPrefixStrippedPath(resultPath: string): string | null {\n const trimmed = resultPath.trim();\n if (trimmed.startsWith(\"qmd:///\")) {\n const relative = trimmed.slice(\"qmd:///\".length).replace(/^\\/+/, \"\");\n return relative.length > 0 ? relative : null;\n }\n if (trimmed.startsWith(\"/\")) {\n const relative = trimmed.replace(/^\\/+/, \"\");\n return relative.length > 0 ? relative : null;\n }\n return null;\n}\n\nasync function resolveSeedMemoryId(\n resolver: DeepRecallSeedResultResolver,\n storage: DeepRecallSeedStorage,\n namespace: string,\n resultPath: string | undefined,\n): Promise<string | null> {\n if (typeof resultPath !== \"string\" || resultPath.length === 0) return null;\n const attempts = [resultPath];\n const stripped = collectionPrefixStrippedPath(resultPath);\n if (stripped !== null) attempts.push(stripped);\n for (const attempt of attempts) {\n const memory = await resolver.readQmdResultMemory(attempt, storage, [namespace]);\n const id = memory?.frontmatter?.id;\n if (typeof id === \"string\" && id.length > 0) return id;\n }\n return null;\n}\n\nexport function createDeepRecallSeedSearch(deps: {\n namespace: string;\n storage: DeepRecallSeedStorage;\n router: DeepRecallSeedRouter;\n resolver: DeepRecallSeedResultResolver;\n /** Transport cancellation (issue #2915); forwarded to the search router. */\n signal?: AbortSignal;\n}): (query: string, limit: number) => Promise<DeepRecallSeedHit[]> {\n return async (query, limit) => {\n if (limit <= 0) return [];\n const unavailable: SearchDegradation[] = [];\n const hits = await deps.router.searchAcrossNamespaces({\n query,\n namespaces: [deps.namespace],\n maxResults: limit,\n execution: {\n ...(deps.signal ? { signal: deps.signal } : {}),\n onDegradation: (degradation) => {\n if (degradation.code === \"backend_unavailable\") unavailable.push(degradation);\n },\n },\n });\n // The requested namespace has no usable index: a seed-search failure, which\n // is the one condition deep recall reports as `backend_unavailable`.\n if (unavailable.length > 0) {\n throw new Error(\n `deep recall seed search unavailable: ${unavailable.map((d) => d.detail ?? d.code).join(\"; \")}`,\n );\n }\n const seeds: DeepRecallSeedHit[] = [];\n for (const hit of hits) {\n if (typeof hit.path !== \"string\" || hit.path.length === 0) continue;\n const memoryId =\n (await resolveSeedMemoryId(deps.resolver, deps.storage, deps.namespace, hit.path)) ?? hit.docid;\n if (typeof memoryId !== \"string\" || memoryId.length === 0) continue;\n seeds.push({\n memoryId,\n score: typeof hit.score === \"number\" && Number.isFinite(hit.score) ? hit.score : 0,\n });\n }\n return seeds;\n };\n}\n"],"mappings":";AAkEA,SAAS,6BAA6B,YAAmC;AACvE,QAAM,UAAU,WAAW,KAAK;AAChC,MAAI,QAAQ,WAAW,SAAS,GAAG;AACjC,UAAM,WAAW,QAAQ,MAAM,UAAU,MAAM,EAAE,QAAQ,QAAQ,EAAE;AACnE,WAAO,SAAS,SAAS,IAAI,WAAW;AAAA,EAC1C;AACA,MAAI,QAAQ,WAAW,GAAG,GAAG;AAC3B,UAAM,WAAW,QAAQ,QAAQ,QAAQ,EAAE;AAC3C,WAAO,SAAS,SAAS,IAAI,WAAW;AAAA,EAC1C;AACA,SAAO;AACT;AAEA,eAAe,oBACb,UACA,SACA,WACA,YACwB;AACxB,MAAI,OAAO,eAAe,YAAY,WAAW,WAAW,EAAG,QAAO;AACtE,QAAM,WAAW,CAAC,UAAU;AAC5B,QAAM,WAAW,6BAA6B,UAAU;AACxD,MAAI,aAAa,KAAM,UAAS,KAAK,QAAQ;AAC7C,aAAW,WAAW,UAAU;AAC9B,UAAM,SAAS,MAAM,SAAS,oBAAoB,SAAS,SAAS,CAAC,SAAS,CAAC;AAC/E,UAAM,KAAK,QAAQ,aAAa;AAChC,QAAI,OAAO,OAAO,YAAY,GAAG,SAAS,EAAG,QAAO;AAAA,EACtD;AACA,SAAO;AACT;AAEO,SAAS,2BAA2B,MAOwB;AACjE,SAAO,OAAO,OAAO,UAAU;AAC7B,QAAI,SAAS,EAAG,QAAO,CAAC;AACxB,UAAM,cAAmC,CAAC;AAC1C,UAAM,OAAO,MAAM,KAAK,OAAO,uBAAuB;AAAA,MACpD;AAAA,MACA,YAAY,CAAC,KAAK,SAAS;AAAA,MAC3B,YAAY;AAAA,MACZ,WAAW;AAAA,QACT,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,QAC7C,eAAe,CAAC,gBAAgB;AAC9B,cAAI,YAAY,SAAS,sBAAuB,aAAY,KAAK,WAAW;AAAA,QAC9E;AAAA,MACF;AAAA,IACF,CAAC;AAGD,QAAI,YAAY,SAAS,GAAG;AAC1B,YAAM,IAAI;AAAA,QACR,wCAAwC,YAAY,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;AAAA,MAC/F;AAAA,IACF;AACA,UAAM,QAA6B,CAAC;AACpC,eAAW,OAAO,MAAM;AACtB,UAAI,OAAO,IAAI,SAAS,YAAY,IAAI,KAAK,WAAW,EAAG;AAC3D,YAAM,WACH,MAAM,oBAAoB,KAAK,UAAU,KAAK,SAAS,KAAK,WAAW,IAAI,IAAI,KAAM,IAAI;AAC5F,UAAI,OAAO,aAAa,YAAY,SAAS,WAAW,EAAG;AAC3D,YAAM,KAAK;AAAA,QACT;AAAA,QACA,OAAO,OAAO,IAAI,UAAU,YAAY,OAAO,SAAS,IAAI,KAAK,IAAI,IAAI,QAAQ;AAAA,MACnF,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AACF;","names":[]}