@remnic/core 9.3.650 → 9.3.652

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 (193) hide show
  1. package/dist/access-cli.js +39 -38
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.d.ts +3 -2
  4. package/dist/access-http.js +19 -19
  5. package/dist/access-mcp.d.ts +3 -2
  6. package/dist/access-mcp.js +18 -18
  7. package/dist/access-schema.js +3 -3
  8. package/dist/{access-service-DIZRHQ7Q.d.ts → access-service-CdJFd3_b.d.ts} +23 -2
  9. package/dist/access-service.d.ts +3 -2
  10. package/dist/access-service.js +16 -16
  11. package/dist/{auto-sync-54QQHOG5.js → auto-sync-5CJBJMPZ.js} +5 -5
  12. package/dist/bootstrap.d.ts +2 -1
  13. package/dist/briefing.js +3 -3
  14. package/dist/calibration.js +2 -2
  15. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
  16. package/dist/causal-consolidation.js +6 -6
  17. package/dist/{chunk-QT4THOLT.js → chunk-2DGQLOOM.js} +1 -1
  18. package/dist/chunk-2DGQLOOM.js.map +1 -0
  19. package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  20. package/dist/{chunk-OAZ5MFUB.js → chunk-3XGWCZ63.js} +45 -28
  21. package/dist/chunk-3XGWCZ63.js.map +1 -0
  22. package/dist/{chunk-QKE4LHNR.js → chunk-4HYSMH7D.js} +2 -2
  23. package/dist/{chunk-DDRNDPX4.js → chunk-4SKKVWLQ.js} +2 -2
  24. package/dist/chunk-5FOCXX5E.js +34 -0
  25. package/dist/chunk-5FOCXX5E.js.map +1 -0
  26. package/dist/{chunk-YAFSTKTH.js → chunk-5V3TAB7D.js} +184 -12
  27. package/dist/chunk-5V3TAB7D.js.map +1 -0
  28. package/dist/{chunk-DB5A3NHS.js → chunk-7LWRCOP7.js} +9 -2
  29. package/dist/chunk-7LWRCOP7.js.map +1 -0
  30. package/dist/{chunk-FOVPSMGI.js → chunk-7WEB3FLJ.js} +2 -2
  31. package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
  32. package/dist/{chunk-4BISW7RX.js → chunk-AJE7FJVE.js} +2 -2
  33. package/dist/{chunk-ZXWAQFDE.js → chunk-CFOCZPIQ.js} +2 -2
  34. package/dist/{chunk-NT5TINK5.js → chunk-DHGSZ3UD.js} +2 -2
  35. package/dist/{chunk-OTC2KOZ2.js → chunk-EHQLDFSH.js} +2 -2
  36. package/dist/{chunk-RRRCNIPK.js → chunk-GI45G4BK.js} +4 -4
  37. package/dist/{chunk-AMACWKM4.js → chunk-IJHLC5CH.js} +2 -2
  38. package/dist/{chunk-OR7R6M5Z.js → chunk-IVYSVAC6.js} +2 -2
  39. package/dist/{chunk-76QTEJ2Q.js → chunk-JBHXMCYN.js} +2 -2
  40. package/dist/{chunk-UMKPSD35.js → chunk-JF7SFXTG.js} +2 -2
  41. package/dist/{chunk-NMIOW7XG.js → chunk-JVRPJ7D4.js} +126 -26
  42. package/dist/chunk-JVRPJ7D4.js.map +1 -0
  43. package/dist/{chunk-TQUWNX7C.js → chunk-JX2RINDR.js} +2 -2
  44. package/dist/{chunk-MCYT2RNT.js → chunk-KJDKZVF3.js} +3 -3
  45. package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
  46. package/dist/{chunk-PQFUUXWK.js → chunk-KWM33SPU.js} +2 -2
  47. package/dist/{chunk-A3BS64GV.js → chunk-LCC5EZTT.js} +4 -4
  48. package/dist/{chunk-TVOPSKOK.js → chunk-MGGNV3H2.js} +4 -4
  49. package/dist/{chunk-D6WVJIS3.js → chunk-ORGWWNJG.js} +2 -2
  50. package/dist/{chunk-Z3PZRDLW.js → chunk-PRQXUSQV.js} +2 -2
  51. package/dist/{chunk-VWT3F4IV.js → chunk-PS3SYNHP.js} +12 -4
  52. package/dist/chunk-PS3SYNHP.js.map +1 -0
  53. package/dist/{chunk-I4COC5XW.js → chunk-PYWNNF2I.js} +47 -9
  54. package/dist/chunk-PYWNNF2I.js.map +1 -0
  55. package/dist/{chunk-IMWFHBG2.js → chunk-QWRC7GIO.js} +2 -2
  56. package/dist/{chunk-U3GQ33JC.js → chunk-SLTKP5WJ.js} +2 -2
  57. package/dist/{chunk-23RYLGYA.js → chunk-TCX4WLKK.js} +104 -112
  58. package/dist/chunk-TCX4WLKK.js.map +1 -0
  59. package/dist/{chunk-6NKAQ74D.js → chunk-UU6MVCJ6.js} +1 -1
  60. package/dist/chunk-UU6MVCJ6.js.map +1 -0
  61. package/dist/{chunk-WEPMT6SC.js → chunk-V25ZAOSB.js} +5 -5
  62. package/dist/{chunk-UMTG2BN2.js → chunk-V4UDXYGG.js} +2 -2
  63. package/dist/{chunk-TUMH6EDV.js → chunk-WSFNYPAT.js} +26 -26
  64. package/dist/{chunk-ZT6R3WR3.js → chunk-WTI35CVJ.js} +4 -4
  65. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  66. package/dist/{chunk-OZKZ2TRP.js → chunk-XBIACVCO.js} +9 -2
  67. package/dist/chunk-XBIACVCO.js.map +1 -0
  68. package/dist/{chunk-ALUZN7BE.js → chunk-XMN6MMTU.js} +2 -2
  69. package/dist/{chunk-A4BTPHIN.js → chunk-Y7NWBBHV.js} +6 -6
  70. package/dist/{chunk-WPCCNSWO.js → chunk-YM3LR4LS.js} +7 -7
  71. package/dist/{chunk-3IJEQWQX.js → chunk-YOVKPOMD.js} +4 -4
  72. package/dist/{chunk-M75TBFKQ.js → chunk-Z2OXSMZK.js} +2 -2
  73. package/dist/{cli-BG4ybtJr.d.ts → cli-DDo7Qgs-.d.ts} +2 -2
  74. package/dist/cli.d.ts +4 -3
  75. package/dist/cli.js +34 -34
  76. package/dist/compounding/engine.js +3 -3
  77. package/dist/connectors/codex-materialize-runner.js +3 -3
  78. package/dist/connectors/index.js +3 -3
  79. package/dist/entity-retrieval.js +3 -3
  80. package/dist/event-order-recall.js +1 -1
  81. package/dist/explicit-capture.d.ts +2 -1
  82. package/dist/explicit-cue-recall.d.ts +7 -0
  83. package/dist/explicit-cue-recall.js +2 -1
  84. package/dist/extraction-judge.js +3 -3
  85. package/dist/extraction.js +3 -3
  86. package/dist/fallback-llm.js +2 -2
  87. package/dist/focused-list-recall.d.ts +6 -0
  88. package/dist/focused-list-recall.js +2 -1
  89. package/dist/index.d.ts +5 -4
  90. package/dist/index.js +87 -86
  91. package/dist/index.js.map +1 -1
  92. package/dist/lcm/engine.js +2 -2
  93. package/dist/lcm/index.js +5 -5
  94. package/dist/lcm-fallback-read.d.ts +71 -0
  95. package/dist/lcm-fallback-read.js +10 -0
  96. package/dist/lcm-fallback-read.js.map +1 -0
  97. package/dist/maintenance/memory-governance.js +3 -3
  98. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  99. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  100. package/dist/mcp-memory-inspector-app.d.ts +3 -2
  101. package/dist/namespaces/migrate.js +11 -11
  102. package/dist/namespaces/search.d.ts +18 -1
  103. package/dist/namespaces/search.js +7 -7
  104. package/dist/namespaces/storage.js +3 -3
  105. package/dist/operator-toolkit.js +13 -13
  106. package/dist/{orchestrator-CX-oqwJq.d.ts → orchestrator-8fTZsa0y.d.ts} +2 -0
  107. package/dist/orchestrator.d.ts +2 -1
  108. package/dist/orchestrator.js +33 -32
  109. package/dist/qmd.d.ts +2 -1
  110. package/dist/qmd.js +2 -2
  111. package/dist/recall-planner-llm.js +2 -2
  112. package/dist/response-guidance-recall.d.ts +6 -0
  113. package/dist/response-guidance-recall.js +2 -1
  114. package/dist/search/factory.js +6 -6
  115. package/dist/search/index.js +8 -8
  116. package/dist/search/lancedb-backend.js +2 -2
  117. package/dist/search/meilisearch-backend.js +2 -2
  118. package/dist/search/orama-backend.js +2 -2
  119. package/dist/search/port.d.ts +6 -0
  120. package/dist/search/port.js +1 -1
  121. package/dist/semantic-consolidation.js +4 -4
  122. package/dist/semantic-rule-promotion.js +3 -3
  123. package/dist/semantic-rule-verifier.js +3 -3
  124. package/dist/storage.js +2 -2
  125. package/dist/summarizer.js +3 -3
  126. package/dist/targeted-fact-recall.d.ts +6 -0
  127. package/dist/targeted-fact-recall.js +2 -1
  128. package/dist/transfer/backup.js +2 -2
  129. package/dist/transfer/capsule-export.js +2 -2
  130. package/dist/transfer/capsule-import.js +2 -2
  131. package/dist/transfer/import-sqlite.js +2 -2
  132. package/dist/verified-recall.js +3 -3
  133. package/package.json +1 -1
  134. package/src/access-service-health.test.ts +402 -0
  135. package/src/access-service.ts +274 -2
  136. package/src/event-order-recall.ts +8 -0
  137. package/src/explicit-cue-recall.ts +70 -29
  138. package/src/focused-list-recall.ts +23 -1
  139. package/src/lcm-fallback-read.ts +113 -0
  140. package/src/namespaces/search.test.ts +258 -3
  141. package/src/namespaces/search.ts +184 -30
  142. package/src/orchestrator.ts +179 -122
  143. package/src/qmd.test.ts +102 -0
  144. package/src/qmd.ts +54 -7
  145. package/src/response-guidance-recall.ts +21 -1
  146. package/src/search/port.ts +6 -0
  147. package/src/targeted-fact-recall.ts +24 -3
  148. package/dist/chunk-23RYLGYA.js.map +0 -1
  149. package/dist/chunk-6NKAQ74D.js.map +0 -1
  150. package/dist/chunk-DB5A3NHS.js.map +0 -1
  151. package/dist/chunk-I4COC5XW.js.map +0 -1
  152. package/dist/chunk-NMIOW7XG.js.map +0 -1
  153. package/dist/chunk-OAZ5MFUB.js.map +0 -1
  154. package/dist/chunk-OZKZ2TRP.js.map +0 -1
  155. package/dist/chunk-QT4THOLT.js.map +0 -1
  156. package/dist/chunk-VWT3F4IV.js.map +0 -1
  157. package/dist/chunk-YAFSTKTH.js.map +0 -1
  158. /package/dist/{auto-sync-54QQHOG5.js.map → auto-sync-5CJBJMPZ.js.map} +0 -0
  159. /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
  160. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  161. /package/dist/{chunk-QKE4LHNR.js.map → chunk-4HYSMH7D.js.map} +0 -0
  162. /package/dist/{chunk-DDRNDPX4.js.map → chunk-4SKKVWLQ.js.map} +0 -0
  163. /package/dist/{chunk-FOVPSMGI.js.map → chunk-7WEB3FLJ.js.map} +0 -0
  164. /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
  165. /package/dist/{chunk-4BISW7RX.js.map → chunk-AJE7FJVE.js.map} +0 -0
  166. /package/dist/{chunk-ZXWAQFDE.js.map → chunk-CFOCZPIQ.js.map} +0 -0
  167. /package/dist/{chunk-NT5TINK5.js.map → chunk-DHGSZ3UD.js.map} +0 -0
  168. /package/dist/{chunk-OTC2KOZ2.js.map → chunk-EHQLDFSH.js.map} +0 -0
  169. /package/dist/{chunk-RRRCNIPK.js.map → chunk-GI45G4BK.js.map} +0 -0
  170. /package/dist/{chunk-AMACWKM4.js.map → chunk-IJHLC5CH.js.map} +0 -0
  171. /package/dist/{chunk-OR7R6M5Z.js.map → chunk-IVYSVAC6.js.map} +0 -0
  172. /package/dist/{chunk-76QTEJ2Q.js.map → chunk-JBHXMCYN.js.map} +0 -0
  173. /package/dist/{chunk-UMKPSD35.js.map → chunk-JF7SFXTG.js.map} +0 -0
  174. /package/dist/{chunk-TQUWNX7C.js.map → chunk-JX2RINDR.js.map} +0 -0
  175. /package/dist/{chunk-MCYT2RNT.js.map → chunk-KJDKZVF3.js.map} +0 -0
  176. /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
  177. /package/dist/{chunk-PQFUUXWK.js.map → chunk-KWM33SPU.js.map} +0 -0
  178. /package/dist/{chunk-A3BS64GV.js.map → chunk-LCC5EZTT.js.map} +0 -0
  179. /package/dist/{chunk-TVOPSKOK.js.map → chunk-MGGNV3H2.js.map} +0 -0
  180. /package/dist/{chunk-D6WVJIS3.js.map → chunk-ORGWWNJG.js.map} +0 -0
  181. /package/dist/{chunk-Z3PZRDLW.js.map → chunk-PRQXUSQV.js.map} +0 -0
  182. /package/dist/{chunk-IMWFHBG2.js.map → chunk-QWRC7GIO.js.map} +0 -0
  183. /package/dist/{chunk-U3GQ33JC.js.map → chunk-SLTKP5WJ.js.map} +0 -0
  184. /package/dist/{chunk-WEPMT6SC.js.map → chunk-V25ZAOSB.js.map} +0 -0
  185. /package/dist/{chunk-UMTG2BN2.js.map → chunk-V4UDXYGG.js.map} +0 -0
  186. /package/dist/{chunk-TUMH6EDV.js.map → chunk-WSFNYPAT.js.map} +0 -0
  187. /package/dist/{chunk-ZT6R3WR3.js.map → chunk-WTI35CVJ.js.map} +0 -0
  188. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  189. /package/dist/{chunk-ALUZN7BE.js.map → chunk-XMN6MMTU.js.map} +0 -0
  190. /package/dist/{chunk-A4BTPHIN.js.map → chunk-Y7NWBBHV.js.map} +0 -0
  191. /package/dist/{chunk-WPCCNSWO.js.map → chunk-YM3LR4LS.js.map} +0 -0
  192. /package/dist/{chunk-3IJEQWQX.js.map → chunk-YOVKPOMD.js.map} +0 -0
  193. /package/dist/{chunk-M75TBFKQ.js.map → chunk-Z2OXSMZK.js.map} +0 -0
@@ -44,12 +44,32 @@ type NamespaceBackendRecord = {
44
44
  filtersNestedNamespaces: boolean;
45
45
  };
46
46
 
47
- type CollectionState = "present" | "missing" | "unknown" | "skipped";
47
+ export type CollectionState = "present" | "missing" | "unknown" | "skipped";
48
+
49
+ export interface NamespaceSearchHealth {
50
+ collection: string;
51
+ memoryDir: string;
52
+ available: boolean;
53
+ collectionState: CollectionState;
54
+ debugStatus: string;
55
+ installedVersion: string | null;
56
+ supportedVersion: string | null;
57
+ supported: boolean | null;
58
+ upgradeAvailable: boolean | null;
59
+ doctorAvailable: boolean | null;
60
+ daemonMode: boolean | null;
61
+ }
48
62
 
49
63
  type NamespaceScopedSearchConfig = PluginConfig & {
50
64
  hostEmbeddingProviderScope?: string;
51
65
  };
52
66
 
67
+ type BackendRecordOptions = {
68
+ autoCreateCollection: boolean;
69
+ abortAsUnavailable: boolean;
70
+ failOpenMissingGuardedCollection: boolean;
71
+ };
72
+
53
73
  export class NamespaceSearchRouter {
54
74
  private readonly cache = new Map<string, Promise<NamespaceBackendRecord>>();
55
75
 
@@ -184,6 +204,67 @@ export class NamespaceSearchRouter {
184
204
  return record.collectionState;
185
205
  }
186
206
 
207
+ async healthForNamespace(
208
+ namespace: string,
209
+ execution?: SearchExecutionOptions,
210
+ ): Promise<NamespaceSearchHealth> {
211
+ const key = namespace.trim() || this.config.defaultNamespace;
212
+ const record = await this.createBackendRecordFor(
213
+ key,
214
+ execution,
215
+ {
216
+ autoCreateCollection: false,
217
+ abortAsUnavailable: true,
218
+ failOpenMissingGuardedCollection: false,
219
+ },
220
+ );
221
+ try {
222
+ const liveRecord = await this.liveCachedRecordForHealth(key, record, execution);
223
+ const diagnosticBackend = liveRecord?.backend ?? record.backend;
224
+ const versionStatus =
225
+ "getVersionStatus" in diagnosticBackend &&
226
+ typeof diagnosticBackend.getVersionStatus === "function"
227
+ ? diagnosticBackend.getVersionStatus()
228
+ : null;
229
+ const daemonMode = daemonModeForBackend(diagnosticBackend);
230
+ const collectionState =
231
+ liveRecord?.collectionState === "missing"
232
+ ? "missing"
233
+ : record.collectionState;
234
+
235
+ return {
236
+ collection: record.collection,
237
+ memoryDir: record.memoryDir,
238
+ available: liveRecord?.available ?? record.available,
239
+ collectionState,
240
+ debugStatus: diagnosticBackend.debugStatus(),
241
+ installedVersion: versionStatus?.installedVersion ?? null,
242
+ supportedVersion: versionStatus?.supportedVersion ?? null,
243
+ supported: versionStatus?.supported ?? null,
244
+ upgradeAvailable: versionStatus?.upgradeAvailable ?? null,
245
+ doctorAvailable: versionStatus?.capabilities?.doctor ?? null,
246
+ daemonMode,
247
+ };
248
+ } finally {
249
+ const dispose = (record.backend as { dispose?: () => void | Promise<void> }).dispose;
250
+ await dispose?.call(record.backend);
251
+ }
252
+ }
253
+
254
+ private async liveCachedRecordForHealth(
255
+ key: string,
256
+ disposableRecord: NamespaceBackendRecord,
257
+ execution?: SearchExecutionOptions,
258
+ ): Promise<NamespaceBackendRecord | null> {
259
+ const pending = this.cache.get(key);
260
+ if (!pending) return null;
261
+ const cachedRecord = await awaitWithAbort(pending, execution?.signal).catch(() => null);
262
+ if (!cachedRecord) return null;
263
+ if (cachedRecord.collection !== disposableRecord.collection) return null;
264
+ if (cachedRecord.memoryDir !== disposableRecord.memoryDir) return null;
265
+ return cachedRecord;
266
+ }
267
+
187
268
  /** Clear cached backend records so the next access re-probes availability. */
188
269
  clearCache(): void {
189
270
  this.cache.clear();
@@ -211,31 +292,69 @@ export class NamespaceSearchRouter {
211
292
  const existing = this.cache.get(key);
212
293
  if (existing) return await existing;
213
294
 
214
- const pending = (async (): Promise<NamespaceBackendRecord> => {
215
- const storage = await this.storageRouter.storageFor(key);
216
- const useLegacyDefaultCollection =
217
- key === this.config.defaultNamespace && storage.dir === this.config.memoryDir;
218
- const filtersNestedNamespaces =
219
- this.config.namespacesEnabled === true && useLegacyDefaultCollection;
220
- const rootHostEmbeddingScope =
221
- (this.config as NamespaceScopedSearchConfig).hostEmbeddingProviderScope ??
222
- this.config.memoryDir;
223
- const scopedConfig: NamespaceScopedSearchConfig = {
224
- ...this.config,
225
- memoryDir: storage.dir,
226
- hostEmbeddingProviderScope: rootHostEmbeddingScope,
227
- qmdCollection: namespaceCollectionName(this.config.qmdCollection, key, {
228
- defaultNamespace: this.config.defaultNamespace,
229
- useLegacyDefaultCollection,
230
- }),
231
- };
295
+ const pending = this.createBackendRecordFor(key, execution, {
296
+ autoCreateCollection: true,
297
+ abortAsUnavailable: false,
298
+ failOpenMissingGuardedCollection: true,
299
+ }).catch((error) => {
300
+ this.cache.delete(key);
301
+ throw error;
302
+ });
303
+
304
+ this.cache.set(key, pending);
305
+ return await pending;
306
+ }
307
+
308
+ private async createBackendRecordFor(
309
+ namespace: string,
310
+ execution: SearchExecutionOptions | undefined,
311
+ options: BackendRecordOptions,
312
+ ): Promise<NamespaceBackendRecord> {
313
+ const key = namespace.trim() || this.config.defaultNamespace;
314
+ const storage = await this.storageRouter.storageFor(key);
315
+ const useLegacyDefaultCollection =
316
+ key === this.config.defaultNamespace && storage.dir === this.config.memoryDir;
317
+ const filtersNestedNamespaces =
318
+ this.config.namespacesEnabled === true && useLegacyDefaultCollection;
319
+ const rootHostEmbeddingScope =
320
+ (this.config as NamespaceScopedSearchConfig).hostEmbeddingProviderScope ??
321
+ this.config.memoryDir;
322
+ const scopedConfig: NamespaceScopedSearchConfig = {
323
+ ...this.config,
324
+ memoryDir: storage.dir,
325
+ hostEmbeddingProviderScope: rootHostEmbeddingScope,
326
+ qmdCollection: namespaceCollectionName(this.config.qmdCollection, key, {
327
+ defaultNamespace: this.config.defaultNamespace,
328
+ useLegacyDefaultCollection,
329
+ }),
330
+ };
232
331
 
233
- const backend = this.createBackend(scopedConfig);
234
- const available = await backend.probe().catch(() => false);
332
+ const backend = this.createBackend(scopedConfig);
333
+ try {
334
+ const availabilityProbe =
335
+ options.autoCreateCollection || typeof backend.checkAvailability !== "function"
336
+ ? backend.probe()
337
+ : backend.checkAvailability({ signal: execution?.signal });
338
+ const available = await awaitWithAbort(availabilityProbe, execution?.signal).catch((error) => {
339
+ if (error instanceof NamespaceSearchAbortError && !options.abortAsUnavailable) {
340
+ throw error;
341
+ }
342
+ return false;
343
+ });
235
344
  const collectionState = available
236
- ? await this.collectionStateForBackend(backend, storage.dir, scopedConfig.qmdCollection, {
237
- skipAutoCreate: filtersNestedNamespaces,
238
- execution,
345
+ ? await awaitWithAbort(
346
+ this.collectionStateForBackend(backend, storage.dir, scopedConfig.qmdCollection, {
347
+ autoCreate: options.autoCreateCollection,
348
+ failOpenMissingGuardedCollection: options.failOpenMissingGuardedCollection,
349
+ skipAutoCreate: filtersNestedNamespaces,
350
+ execution,
351
+ }),
352
+ execution?.signal,
353
+ ).catch((error) => {
354
+ if (error instanceof NamespaceSearchAbortError && !options.abortAsUnavailable) {
355
+ throw error;
356
+ }
357
+ return "unknown" as const;
239
358
  })
240
359
  : "unknown";
241
360
  return {
@@ -246,10 +365,13 @@ export class NamespaceSearchRouter {
246
365
  collectionState,
247
366
  filtersNestedNamespaces,
248
367
  };
249
- })();
250
-
251
- this.cache.set(key, pending);
252
- return await pending;
368
+ } catch (error) {
369
+ const dispose = (backend as { dispose?: () => void | Promise<void> }).dispose;
370
+ if (dispose) {
371
+ await Promise.resolve(dispose.call(backend)).catch(() => {});
372
+ }
373
+ throw error;
374
+ }
253
375
  }
254
376
 
255
377
  private async collectionStateForBackend(
@@ -257,21 +379,47 @@ export class NamespaceSearchRouter {
257
379
  memoryDir: string,
258
380
  collection: string,
259
381
  options: {
382
+ autoCreate: boolean;
383
+ failOpenMissingGuardedCollection: boolean;
260
384
  skipAutoCreate: boolean;
261
385
  execution?: SearchExecutionOptions;
262
386
  },
263
387
  ): Promise<CollectionState> {
264
- if (options.skipAutoCreate) {
388
+ if (!options.autoCreate || options.skipAutoCreate) {
265
389
  if (!backend.checkCollection) return "unknown";
266
390
  const collectionState = await backend
267
391
  .checkCollection(collection, options.execution)
268
392
  .catch(() => "unknown" as const);
269
- return collectionState === "missing" ? "unknown" : collectionState;
393
+ return options.failOpenMissingGuardedCollection && collectionState === "missing"
394
+ ? "unknown"
395
+ : collectionState;
270
396
  }
271
397
  return await backend.ensureCollection(memoryDir, collection, options.execution).catch(() => "unknown" as const);
272
398
  }
273
399
  }
274
400
 
401
+ class NamespaceSearchAbortError extends Error {
402
+ constructor() {
403
+ super("operation aborted");
404
+ }
405
+ }
406
+
407
+ function awaitWithAbort<T>(operation: Promise<T>, signal?: AbortSignal): Promise<T> {
408
+ if (!signal) return operation;
409
+ if (signal.aborted) return Promise.reject(new NamespaceSearchAbortError());
410
+
411
+ return new Promise<T>((resolve, reject) => {
412
+ const onAbort = () => {
413
+ signal.removeEventListener("abort", onAbort);
414
+ reject(new NamespaceSearchAbortError());
415
+ };
416
+ signal.addEventListener("abort", onAbort, { once: true });
417
+ operation.then(resolve, reject).finally(() => {
418
+ signal.removeEventListener("abort", onAbort);
419
+ });
420
+ });
421
+ }
422
+
275
423
  function filterNamespaceSubtreeResults(
276
424
  record: NamespaceBackendRecord,
277
425
  results: QmdSearchResult[],
@@ -294,6 +442,12 @@ function backendSearchLimit(
294
442
  );
295
443
  }
296
444
 
445
+ function daemonModeForBackend(backend: SearchBackend): boolean | null {
446
+ return "isDaemonMode" in backend && typeof backend.isDaemonMode === "function"
447
+ ? backend.isDaemonMode() === true
448
+ : null;
449
+ }
450
+
297
451
  function pathIsInsideNamespaceSubtree(
298
452
  memoryDir: string,
299
453
  collection: string,
@@ -311,7 +311,10 @@ import {
311
311
  resolveCodingNamespaceOverlay,
312
312
  } from "./coding/coding-namespace.js";
313
313
  import type { CodingContext } from "./types.js";
314
- import { NamespaceSearchRouter } from "./namespaces/search.js";
314
+ import {
315
+ NamespaceSearchRouter,
316
+ type NamespaceSearchHealth,
317
+ } from "./namespaces/search.js";
315
318
  import { SharedContextManager } from "./shared-context/manager.js";
316
319
  import {
317
320
  CompoundingEngine,
@@ -2324,6 +2327,13 @@ export class Orchestrator {
2324
2327
  });
2325
2328
  }
2326
2329
 
2330
+ async searchHealthForNamespace(
2331
+ namespace: string,
2332
+ execution?: SearchExecutionOptions,
2333
+ ): Promise<NamespaceSearchHealth> {
2334
+ return await this.namespaceSearchRouter.healthForNamespace(namespace, execution);
2335
+ }
2336
+
2327
2337
  private isSearchAvailableForNamespaceRouting(): boolean {
2328
2338
  if (this.config.namespacesEnabled) return true;
2329
2339
  return this.qmd.isAvailable();
@@ -7243,16 +7253,24 @@ export class Orchestrator {
7243
7253
  );
7244
7254
  // Query an LCM-backed read across the ordered read key set and return the
7245
7255
  // FIRST non-empty result (#1505 fallback-namespace unification). The primary
7246
- // overlay key is tried first; if a branch-scoped session has no rows under
7247
- // its branch key, the project / root fallback keys are tried in order. Each
7248
- // builder applies its own per-session budget/limit, so taking the first hit
7249
- // (rather than concatenating across keys) preserves existing budgets while
7250
- // recovering fallback evidence. When the set is a single key (single-user /
7251
- // no-overlay / explicit-namespace), this is exactly one call — unchanged.
7252
- // `lcmSessionId` is `string | undefined`: a SESSIONLESS recall yields the
7253
- // single `undefined` key so the LCM builders run ONE archive-wide read with
7254
- // no `session_id` filter (pre-#1505 behavior; the builders accept an optional
7255
- // `sessionId`). NEVER the literal "default" session id (codex P2).
7256
+ // overlay key is tried first; if a branch-scoped session has no rows under its
7257
+ // branch key, the project / root fallback keys are tried in order.
7258
+ //
7259
+ // #1505 codex P2 ("Merge LCM fallback reads instead of short-circuiting"): the
7260
+ // query-SCORED sections (explicit-cue, targeted-facts, focused-list,
7261
+ // response-guidance, event-order, structured message-parts) no longer use this
7262
+ // helper they MERGE candidates across EVERY authorized key under their single
7263
+ // budget (a weak primary-key hit must not mask stronger fallback evidence; the
7264
+ // section builders take `sessionIds`, structured-parts merges inline below).
7265
+ // This first-non-empty helper now serves ONLY the compressed-history section,
7266
+ // which is a per-session HOLISTIC DAG narrative with no per-item id to merge or
7267
+ // dedupe on — see its call site for the rationale.
7268
+ //
7269
+ // When the set is a single key (single-user / no-overlay / explicit-namespace),
7270
+ // this is exactly one call — unchanged. `lcmSessionId` is `string | undefined`:
7271
+ // a SESSIONLESS recall yields the single `undefined` key so the read runs ONE
7272
+ // archive-wide read with no `session_id` filter (pre-#1505 behavior). NEVER the
7273
+ // literal "default" session id (codex P2).
7256
7274
  const firstNonEmptyLcmRead = async <T>(
7257
7275
  read: (lcmSessionId: string | undefined) => Promise<T>,
7258
7276
  isEmpty: (value: T) => boolean,
@@ -9513,24 +9531,21 @@ export class Orchestrator {
9513
9531
  (recallMode as RecallPlanMode) !== "no_recall"
9514
9532
  ) {
9515
9533
  try {
9516
- const explicitCueSection = await firstNonEmptyLcmRead(
9517
- (lcmSessionId) =>
9518
- buildExplicitCueRecallSection({
9519
- engine: this.lcmEngine,
9520
- // #1495 thread 3 + #1505 fallback unification: read across the
9521
- // ordered LCM read key set (primary overlay coding fallbacks)
9522
- // so a branch-scoped session finds its own explicit-cue evidence
9523
- // even when archived at project/root scope (rule 39).
9524
- sessionId: lcmSessionId,
9525
- query: retrievalQuery,
9526
- maxChars: explicitCueMaxChars,
9527
- maxReferences:
9528
- this.getRecallSectionNumber("explicit-cue", "maxResults") ??
9529
- this.config.explicitCueRecallMaxReferences,
9530
- }),
9531
- (s) => !s,
9532
- "",
9533
- );
9534
+ const explicitCueSection = await buildExplicitCueRecallSection({
9535
+ engine: this.lcmEngine,
9536
+ // #1495 thread 3 + #1505 fallback unification: read across the ordered
9537
+ // LCM read key set (primary overlay → coding fallbacks) so a
9538
+ // branch-scoped session finds its own explicit-cue evidence even when
9539
+ // archived at project/root scope (rule 39). #1505 codex P2: the builder
9540
+ // MERGES candidates across every key under its single budget instead of
9541
+ // short-circuiting on the first non-empty key.
9542
+ sessionIds: lcmReadSessionIds,
9543
+ query: retrievalQuery,
9544
+ maxChars: explicitCueMaxChars,
9545
+ maxReferences:
9546
+ this.getRecallSectionNumber("explicit-cue", "maxResults") ??
9547
+ this.config.explicitCueRecallMaxReferences,
9548
+ });
9534
9549
  if (explicitCueSection) {
9535
9550
  this.appendRecallSection(
9536
9551
  sectionBuckets,
@@ -9560,29 +9575,25 @@ export class Orchestrator {
9560
9575
  shouldRecallTargetedFactEvidence(retrievalQuery)
9561
9576
  ) {
9562
9577
  try {
9563
- const targetedFactSection = await firstNonEmptyLcmRead(
9564
- (lcmSessionId) =>
9565
- buildTargetedFactRecallSection({
9566
- engine: this.lcmEngine,
9567
- // #1495 + #1505 fallback unification: read across the ordered LCM
9568
- // read key set so a branch-scoped session finds its own
9569
- // targeted-fact evidence even when archived at project/root scope.
9570
- sessionId: lcmSessionId,
9571
- query: retrievalQuery,
9572
- maxChars: targetedFactMaxChars,
9573
- maxSearchResults:
9574
- this.getRecallSectionNumber("targeted-facts", "maxResults") ??
9575
- this.config.targetedFactRecallMaxResults,
9576
- maxScanWindowTurns:
9577
- this.getRecallSectionNumber("targeted-facts", "maxTurns") ??
9578
- this.config.targetedFactRecallScanWindowTurns,
9579
- maxScanWindowTokens:
9580
- this.getRecallSectionNumber("targeted-facts", "maxTokens") ??
9581
- this.config.targetedFactRecallScanWindowTokens,
9582
- }),
9583
- (s) => !s,
9584
- "",
9585
- );
9578
+ const targetedFactSection = await buildTargetedFactRecallSection({
9579
+ engine: this.lcmEngine,
9580
+ // #1495 + #1505 fallback unification: read across the ordered LCM read
9581
+ // key set so a branch-scoped session finds its own targeted-fact
9582
+ // evidence even when archived at project/root scope. #1505 codex P2: the
9583
+ // builder MERGES candidates across every key under its single budget.
9584
+ sessionIds: lcmReadSessionIds,
9585
+ query: retrievalQuery,
9586
+ maxChars: targetedFactMaxChars,
9587
+ maxSearchResults:
9588
+ this.getRecallSectionNumber("targeted-facts", "maxResults") ??
9589
+ this.config.targetedFactRecallMaxResults,
9590
+ maxScanWindowTurns:
9591
+ this.getRecallSectionNumber("targeted-facts", "maxTurns") ??
9592
+ this.config.targetedFactRecallScanWindowTurns,
9593
+ maxScanWindowTokens:
9594
+ this.getRecallSectionNumber("targeted-facts", "maxTokens") ??
9595
+ this.config.targetedFactRecallScanWindowTokens,
9596
+ });
9586
9597
  if (targetedFactSection) {
9587
9598
  this.appendRecallSection(
9588
9599
  sectionBuckets,
@@ -9613,29 +9624,26 @@ export class Orchestrator {
9613
9624
  shouldRecallFocusedListEvidence(retrievalQuery)
9614
9625
  ) {
9615
9626
  try {
9616
- const focusedListSection = await firstNonEmptyLcmRead(
9617
- (lcmSessionId) =>
9618
- buildFocusedListRecallSection({
9619
- engine: this.lcmEngine,
9620
- // #1495 thread 3 + #1505 fallback unification: read across the
9621
- // ordered LCM read key set so a branch-scoped session reads its own
9622
- // focused-list/count evidence even at project/root scope (rule 39).
9623
- sessionId: lcmSessionId,
9624
- query: retrievalQuery,
9625
- maxChars: focusedListMaxChars,
9626
- maxSearchResults:
9627
- this.getRecallSectionNumber("focused-list", "maxResults") ??
9628
- this.config.focusedListRecallMaxResults,
9629
- maxScanWindowTurns:
9630
- this.getRecallSectionNumber("focused-list", "maxTurns") ??
9631
- this.config.focusedListRecallScanWindowTurns,
9632
- maxScanWindowTokens:
9633
- this.getRecallSectionNumber("focused-list", "maxTokens") ??
9634
- this.config.focusedListRecallScanWindowTokens,
9635
- }),
9636
- (s) => !s,
9637
- "",
9638
- );
9627
+ const focusedListSection = await buildFocusedListRecallSection({
9628
+ engine: this.lcmEngine,
9629
+ // #1495 thread 3 + #1505 fallback unification: read across the ordered
9630
+ // LCM read key set so a branch-scoped session reads its own
9631
+ // focused-list/count evidence even at project/root scope (rule 39).
9632
+ // #1505 codex P2: the builder MERGES candidates across every key under
9633
+ // its single budget.
9634
+ sessionIds: lcmReadSessionIds,
9635
+ query: retrievalQuery,
9636
+ maxChars: focusedListMaxChars,
9637
+ maxSearchResults:
9638
+ this.getRecallSectionNumber("focused-list", "maxResults") ??
9639
+ this.config.focusedListRecallMaxResults,
9640
+ maxScanWindowTurns:
9641
+ this.getRecallSectionNumber("focused-list", "maxTurns") ??
9642
+ this.config.focusedListRecallScanWindowTurns,
9643
+ maxScanWindowTokens:
9644
+ this.getRecallSectionNumber("focused-list", "maxTokens") ??
9645
+ this.config.focusedListRecallScanWindowTokens,
9646
+ });
9639
9647
  if (focusedListSection) {
9640
9648
  this.appendRecallSection(
9641
9649
  sectionBuckets,
@@ -9670,30 +9678,27 @@ export class Orchestrator {
9670
9678
  (responseGuidanceMatchesQuery || responseGuidanceForcedByPipeline)
9671
9679
  ) {
9672
9680
  try {
9673
- const responseGuidanceSection = await firstNonEmptyLcmRead(
9674
- (lcmSessionId) =>
9675
- buildResponseGuidanceRecallSection({
9676
- engine: this.lcmEngine,
9677
- // #1495 thread 3 + #1505 fallback unification: read across the
9678
- // ordered LCM read key set so a branch-scoped session reads its own
9679
- // response-guidance evidence even at project/root scope (rule 39).
9680
- sessionId: lcmSessionId,
9681
- query: retrievalQuery,
9682
- maxChars: responseGuidanceMaxChars,
9683
- maxSearchResults:
9684
- this.getRecallSectionNumber("response-guidance", "maxResults") ??
9685
- this.config.responseGuidanceRecallMaxResults,
9686
- maxScanWindowTurns:
9687
- this.getRecallSectionNumber("response-guidance", "maxTurns") ??
9688
- this.config.responseGuidanceRecallScanWindowTurns,
9689
- maxScanWindowTokens:
9690
- this.getRecallSectionNumber("response-guidance", "maxTokens") ??
9691
- this.config.responseGuidanceRecallScanWindowTokens,
9692
- forceGeneric: responseGuidanceForcedByPipeline,
9693
- }),
9694
- (s) => !s,
9695
- "",
9696
- );
9681
+ const responseGuidanceSection = await buildResponseGuidanceRecallSection({
9682
+ engine: this.lcmEngine,
9683
+ // #1495 thread 3 + #1505 fallback unification: read across the ordered
9684
+ // LCM read key set so a branch-scoped session reads its own
9685
+ // response-guidance evidence even at project/root scope (rule 39).
9686
+ // #1505 codex P2: the builder MERGES candidates across every key under
9687
+ // its single budget.
9688
+ sessionIds: lcmReadSessionIds,
9689
+ query: retrievalQuery,
9690
+ maxChars: responseGuidanceMaxChars,
9691
+ maxSearchResults:
9692
+ this.getRecallSectionNumber("response-guidance", "maxResults") ??
9693
+ this.config.responseGuidanceRecallMaxResults,
9694
+ maxScanWindowTurns:
9695
+ this.getRecallSectionNumber("response-guidance", "maxTurns") ??
9696
+ this.config.responseGuidanceRecallScanWindowTurns,
9697
+ maxScanWindowTokens:
9698
+ this.getRecallSectionNumber("response-guidance", "maxTokens") ??
9699
+ this.config.responseGuidanceRecallScanWindowTokens,
9700
+ forceGeneric: responseGuidanceForcedByPipeline,
9701
+ });
9697
9702
  if (responseGuidanceSection) {
9698
9703
  this.appendRecallSection(
9699
9704
  sectionBuckets,
@@ -9722,13 +9727,22 @@ export class Orchestrator {
9722
9727
  shouldRecallEventOrderEvidence(retrievalQuery)
9723
9728
  ) {
9724
9729
  try {
9730
+ // #1495 thread 3 + #1505 fallback unification: read across the ordered LCM
9731
+ // read key set so a branch-scoped session reads its own chronological
9732
+ // event-order evidence even at project/root scope. UNLIKE the relevance-
9733
+ // ranked sections, event-order must NOT merge across keys: `turn_index` is
9734
+ // LOCAL to each LCM `session_id` (`observe` numbers turns per session via
9735
+ // `getMaxTurnIndex`), so interleaving two keys and sorting by `turn_index`
9736
+ // would place an older project-scope turn after a newer branch-scope turn
9737
+ // and misstate the chronology (#1505 codex P2). Like compressed-history,
9738
+ // event-order is an inherently per-session ORDERED artifact, so it takes
9739
+ // the highest-priority authorized key (primary overlay → project/root)
9740
+ // that actually has chronological evidence — each key's timeline is
9741
+ // internally consistent.
9725
9742
  const eventOrderSection = await firstNonEmptyLcmRead(
9726
9743
  (lcmSessionId) =>
9727
9744
  buildEventOrderRecallSection({
9728
9745
  engine: this.lcmEngine,
9729
- // #1495 thread 3 + #1505 fallback unification: read across the
9730
- // ordered LCM read key set so a branch-scoped session reads its own
9731
- // chronological event-order evidence even at project/root scope.
9732
9746
  sessionId: lcmSessionId,
9733
9747
  query: retrievalQuery,
9734
9748
  maxChars: eventOrderMaxChars,
@@ -9918,21 +9932,59 @@ export class Orchestrator {
9918
9932
  (recallMode as RecallPlanMode) !== "no_recall"
9919
9933
  ) {
9920
9934
  try {
9921
- const structuredMatches = await firstNonEmptyLcmRead(
9922
- (lcmSessionId) =>
9923
- this.lcmEngine!.searchStructuredParts(
9924
- // #1495 + #1505 fallback unification: read across the ordered LCM
9925
- // read key set so a branch-scoped session reads its own structured
9926
- // message-part evidence even when archived at project/root scope.
9927
- // Structured parts are inherently per-session (the DAG is keyed by
9928
- // session_id), so a SESSIONLESS read (`undefined`) normalizes to
9929
- // empty no section, the correct pre-#1505 behavior (codex P2).
9930
- lcmSessionId ?? "",
9931
- retrievalQuery,
9932
- ),
9933
- (matches) => matches.length === 0,
9934
- [],
9935
+ // #1495 + #1505 fallback unification: read across the ordered LCM read
9936
+ // key set so a branch-scoped session reads its own structured
9937
+ // message-part evidence even when archived at project/root scope.
9938
+ // #1505 codex P2: structured matches are query-SCORED evidence, so MERGE
9939
+ // across EVERY key (primary overlay project/root fallbacks) instead of
9940
+ // short-circuiting on the first non-empty key a weak branch-key hit must
9941
+ // not mask stronger project-fallback parts. Keys are queried in priority
9942
+ // order; dedupe by session_id+turn_index+part_id keeps the primary key's
9943
+ // row on collision. `formatStructuredRecall` applies the single budget
9944
+ // below. A sessionless key (`undefined`) normalizes to "" → no matches
9945
+ // (structured parts are inherently per-session; pre-#1505 behavior, codex
9946
+ // P2).
9947
+ // FAULT ISOLATION (allSettled, not all): the pre-#1505 first-non-empty read
9948
+ // short-circuited, so a fallback key was often never queried and its latent
9949
+ // search failure never surfaced. Querying every key eagerly must NOT let one
9950
+ // key's failure (e.g. a SqliteError from a corrupt/locked fallback index)
9951
+ // reject the batch and discard the OTHER keys' parts — or, since this and
9952
+ // the compressed-history read below share one try block, silently drop the
9953
+ // compressed-history section a healthy primary key would still produce. So
9954
+ // read each key independently and keep the fulfilled batches.
9955
+ const structuredSettled = await Promise.allSettled(
9956
+ lcmReadSessionIds.map((lcmSessionId) =>
9957
+ this.lcmEngine!.searchStructuredParts(lcmSessionId ?? "", retrievalQuery),
9958
+ ),
9935
9959
  );
9960
+ for (const settled of structuredSettled) {
9961
+ if (settled.status === "rejected") {
9962
+ log.debug(
9963
+ `LCM structured-parts read failed for one key: ${settled.reason}`,
9964
+ );
9965
+ }
9966
+ }
9967
+ const seenStructuredParts = new Set<string>();
9968
+ const structuredMatches = structuredSettled
9969
+ .flatMap((settled) => (settled.status === "fulfilled" ? settled.value : []))
9970
+ .filter((match) => {
9971
+ const key = `${match.session_id} ${match.turn_index} ${match.part_id}`;
9972
+ if (seenStructuredParts.has(key)) return false;
9973
+ seenStructuredParts.add(key);
9974
+ return true;
9975
+ })
9976
+ // Restore the archive's per-key ordering (score DESC, then turn DESC)
9977
+ // across the MERGED set so the strongest parts win the shared budget in
9978
+ // `formatStructuredRecall` — otherwise weak primary-key parts could crowd
9979
+ // out stronger fallback parts. Stable sort: a single key is already in
9980
+ // this order, so it stays byte-for-byte the pre-#1505 behavior.
9981
+ // `?? 0` is defensive: `LcmStructuredRecallMatch.score` is always a
9982
+ // number here, but a bare `b.score - a.score` would yield NaN (falsy)
9983
+ // for any future unscored match and silently fall through to turn order.
9984
+ .sort(
9985
+ (a, b) =>
9986
+ (b.score ?? 0) - (a.score ?? 0) || b.turn_index - a.turn_index,
9987
+ );
9936
9988
  const structuredSection = this.lcmEngine.formatStructuredRecall(
9937
9989
  structuredMatches,
9938
9990
  Math.ceil(this.config.recallBudgetChars * 0.08),
@@ -9955,15 +10007,20 @@ export class Orchestrator {
9955
10007
  }
9956
10008
  }
9957
10009
  }
10010
+ // #1495 + #1505 fallback unification: read across the ordered LCM read key
10011
+ // set so a branch-scoped session reads its own compressed-history evidence
10012
+ // even at project/root scope. UNLIKE the query-scored sections above, the
10013
+ // compressed history is a per-session HOLISTIC DAG narrative, not a set of
10014
+ // independently-rankable evidence items — concatenating two sessions'
10015
+ // summaries would double-count the conversation and blow the budget, and
10016
+ // there is no per-item id to dedupe on. So this section deliberately keeps
10017
+ // first-non-empty semantics (#1505 codex P2 scope: "merge the query-matched
10018
+ // sections"): the highest-priority authorized key (primary overlay →
10019
+ // project/root) that actually has a compressed history wins. A sessionless
10020
+ // key (`undefined`) normalizes to empty → no section (pre-#1505 behavior).
9958
10021
  const lcmSection = await firstNonEmptyLcmRead(
9959
10022
  (lcmSessionId) =>
9960
10023
  this.lcmEngine!.assembleRecall(
9961
- // #1495 + #1505 fallback unification: read across the ordered LCM
9962
- // read key set so a branch-scoped session reads its own
9963
- // compressed-history evidence even at project/root scope.
9964
- // Compressed history is inherently per-session (a per-session DAG),
9965
- // so a SESSIONLESS read (`undefined`) normalizes to empty → no
9966
- // section, the correct pre-#1505 behavior (codex P2).
9967
10024
  lcmSessionId ?? "",
9968
10025
  this.config.recallBudgetChars,
9969
10026
  ),