@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
@@ -3,6 +3,11 @@ import type { ExplicitCueRecallEngine } from "./explicit-cue-recall.js";
3
3
 
4
4
  export interface EventOrderRecallOptions {
5
5
  engine: ExplicitCueRecallEngine | null | undefined;
6
+ // event-order reads a SINGLE LCM session key. Unlike the relevance-ranked
7
+ // sections, its evidence must not be merged across the #1505 fallback key set:
8
+ // `turn_index` is local to each LCM `session_id`, so interleaving keys would
9
+ // misstate chronology. The orchestrator reads the ordered key set via
10
+ // first-non-empty instead (see the event-order call site).
6
11
  sessionId?: string;
7
12
  query: string;
8
13
  maxChars: number;
@@ -42,6 +47,9 @@ export async function buildEventOrderRecallSection(
42
47
  ): Promise<string> {
43
48
  const budget = normalizePositiveInteger(options.maxChars);
44
49
  const maxItems = normalizePositiveInteger(options.maxItems ?? DEFAULT_MAX_ITEMS);
50
+ // event-order reads a SINGLE session key (`turn_index` is local to each LCM
51
+ // `session_id`, so chronology can't be merged across the #1505 fallback set —
52
+ // the orchestrator drives the ordered key set via first-non-empty instead).
45
53
  if (!options.engine || !options.sessionId || budget <= 0) {
46
54
  return "";
47
55
  }
@@ -1,4 +1,8 @@
1
1
  import { buildEvidencePack } from "./evidence-pack.js";
2
+ import {
3
+ gatherAcrossReadSessions,
4
+ resolveLcmReadSessionIds,
5
+ } from "./lcm-fallback-read.js";
2
6
 
3
7
  export interface ExplicitCueRecallEngine {
4
8
  expandContext(
@@ -29,6 +33,13 @@ export interface ExplicitCueRecallEngine {
29
33
  export interface ExplicitCueRecallOptions {
30
34
  engine: ExplicitCueRecallEngine | null | undefined;
31
35
  sessionId?: string;
36
+ /**
37
+ * Ordered, read-authorized LCM read key set (primary overlay → project/root
38
+ * fallbacks). When present, cue evidence is gathered across EVERY key into one
39
+ * shared accumulator and merged under this section's budget (#1505 codex P2).
40
+ * Falls back to `sessionId`.
41
+ */
42
+ sessionIds?: readonly (string | undefined)[];
32
43
  query: string;
33
44
  maxChars: number;
34
45
  maxItemChars?: number;
@@ -324,45 +335,75 @@ export async function buildExplicitCueRecallSection(
324
335
  }> = [];
325
336
  const seenTurns = new Set<string>();
326
337
 
327
- await collectTurnReferenceEvidence({
328
- engine,
329
- sessionId: options.sessionId,
330
- query,
331
- maxReferences,
332
- evidenceItems,
333
- seenTurns,
334
- });
335
-
336
- if (options.includeContentLexicalCues) {
337
- await collectNamedMeetingFactEvidence({
338
+ // #1505 codex P2: gather cue evidence across the ordered LCM read key set
339
+ // (primary overlay → project/root fallbacks) into ONE shared accumulator
340
+ // (`evidenceItems` / `seenTurns`), so a branch-scoped session's project/root
341
+ // fallback cues are RECOVERED instead of being skipped by the old
342
+ // first-non-empty short-circuit. `seenTurns` dedupes across keys by
343
+ // `session_id`+`turn_index`; the budget is applied exactly once in the
344
+ // `buildEvidencePack` call below. `gatherAcrossReadSessions` isolates a
345
+ // per-key read failure (a corrupt/locked fallback index must not discard the
346
+ // other keys' cues); single-key recall runs each collector directly, so a
347
+ // failure propagates exactly as before.
348
+ //
349
+ // Ordering: gather by evidence TYPE first (turn references → content cues →
350
+ // lexical cues), then by read-key priority within each type. This is the
351
+ // section's deliberate value order, and it carries across keys — so a fallback
352
+ // key's high-value turn references precede the primary key's lower-value
353
+ // lexical cues under a tight budget, while a single key is byte-for-byte the
354
+ // pre-#1505 insertion order. We intentionally do NOT score-sort the merged set:
355
+ // explicit-cue's highest-value cues (turn references / content cues) are
356
+ // deliberately UNSCORED while lexical search hits carry numeric scores, so a
357
+ // score-DESC sort would invert the priority and demote turn references below
358
+ // weak lexical hits (cursor[bot] / codex P2 on this PR).
359
+ const readSessionIds = resolveLcmReadSessionIds(options);
360
+ await gatherAcrossReadSessions(readSessionIds, (sessionId) =>
361
+ collectTurnReferenceEvidence({
338
362
  engine,
339
- sessionId: options.sessionId,
363
+ sessionId,
340
364
  query,
341
365
  maxReferences,
342
366
  evidenceItems,
343
367
  seenTurns,
344
- });
368
+ }),
369
+ );
345
370
 
346
- await collectFocusedTranscriptCueEvidence({
371
+ if (options.includeContentLexicalCues) {
372
+ await gatherAcrossReadSessions(readSessionIds, (sessionId) =>
373
+ collectNamedMeetingFactEvidence({
374
+ engine,
375
+ sessionId,
376
+ query,
377
+ maxReferences,
378
+ evidenceItems,
379
+ seenTurns,
380
+ }),
381
+ );
382
+
383
+ await gatherAcrossReadSessions(readSessionIds, (sessionId) =>
384
+ collectFocusedTranscriptCueEvidence({
385
+ engine,
386
+ sessionId,
387
+ query,
388
+ evidenceItems,
389
+ seenTurns,
390
+ }),
391
+ );
392
+ }
393
+
394
+ await gatherAcrossReadSessions(readSessionIds, (sessionId) =>
395
+ collectLexicalCueEvidence({
347
396
  engine,
348
- sessionId: options.sessionId,
397
+ sessionId,
349
398
  query,
399
+ maxReferences,
400
+ includeBenchmarkAnchorCues: options.includeBenchmarkAnchorCues,
401
+ includeContentLexicalCues: options.includeContentLexicalCues,
402
+ includeStructuredPlanCues: options.includeStructuredPlanCues,
350
403
  evidenceItems,
351
404
  seenTurns,
352
- });
353
- }
354
-
355
- await collectLexicalCueEvidence({
356
- engine,
357
- sessionId: options.sessionId,
358
- query,
359
- maxReferences,
360
- includeBenchmarkAnchorCues: options.includeBenchmarkAnchorCues,
361
- includeContentLexicalCues: options.includeContentLexicalCues,
362
- includeStructuredPlanCues: options.includeStructuredPlanCues,
363
- evidenceItems,
364
- seenTurns,
365
- });
405
+ }),
406
+ );
366
407
 
367
408
  const evidenceFocusQuery = buildEvidenceFocusQuery(query, {
368
409
  includeBenchmarkAnchorCues: options.includeBenchmarkAnchorCues,
@@ -4,10 +4,20 @@ import {
4
4
  type EvidencePackItem,
5
5
  } from "./evidence-pack.js";
6
6
  import type { ExplicitCueRecallEngine } from "./explicit-cue-recall.js";
7
+ import {
8
+ gatherAcrossReadSessions,
9
+ resolveLcmReadSessionIds,
10
+ } from "./lcm-fallback-read.js";
7
11
 
8
12
  export interface FocusedListRecallOptions {
9
13
  engine: ExplicitCueRecallEngine | null | undefined;
10
14
  sessionId?: string;
15
+ /**
16
+ * Ordered, read-authorized LCM read key set (primary overlay → project/root
17
+ * fallbacks). When present, evidence is gathered across EVERY key and merged
18
+ * under this section's budget (#1505 codex P2). Falls back to `sessionId`.
19
+ */
20
+ sessionIds?: readonly (string | undefined)[];
11
21
  query: string;
12
22
  maxChars: number;
13
23
  maxItemChars?: number;
@@ -52,7 +62,19 @@ export async function buildFocusedListRecallSection(
52
62
  return "";
53
63
  }
54
64
 
55
- const items = await collectFocusedListItems(options, intent);
65
+ // #1505 codex P2: gather candidates across the ordered LCM read key set
66
+ // (primary overlay → project/root fallbacks) and UNION them into the existing
67
+ // rank/dedupe/budget pass, so a stronger project-fallback candidate is not
68
+ // masked by a weak primary-key hit. `rankAndDedupeFocusedListItems` applies
69
+ // the section-appropriate dedupe and relevance rank; the budget is applied
70
+ // exactly once below. `gatherAcrossReadSessions` isolates a per-key read
71
+ // failure so a corrupt/locked fallback index can't discard the primary key's
72
+ // candidates; the single-key path runs exactly one collect and propagates a
73
+ // failure as before — byte-for-byte the pre-#1505 behavior.
74
+ const items: EvidencePackItem[] = [];
75
+ await gatherAcrossReadSessions(resolveLcmReadSessionIds(options), async (sessionId) => {
76
+ items.push(...(await collectFocusedListItems({ ...options, sessionId }, intent)));
77
+ });
56
78
  const ranked = rankAndDedupeFocusedListItems(items, options.query, intent)
57
79
  .slice(0, maxResults);
58
80
  if (ranked.length === 0) {
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Shared helper for reading LCM-backed recall sections across the ordered,
3
+ * read-authorized fallback key set (#1505 codex P2 "Merge LCM fallback reads
4
+ * instead of short-circuiting").
5
+ *
6
+ * Background: a branch-scoped session archives its LCM rows under whichever
7
+ * coding-overlay namespace was effective at write time, so its evidence can be
8
+ * split across the primary overlay key AND the project / root fallback keys.
9
+ * Normal QMD/file recall already searches the primary namespace PLUS
10
+ * `codingOverlay.readFallbacks` and MERGES the rows. The LCM read path must do
11
+ * the same: query EVERY authorized read key and merge the candidate evidence
12
+ * into each section's existing dedupe + rank + budget pass, instead of stopping
13
+ * at the first key that happens to yield a (possibly weak) hit.
14
+ *
15
+ * Each section already owns a section-appropriate dedupe (a `seen` set or a
16
+ * `rankAndDedupe…` step), so the fan-out only needs to resolve the ordered,
17
+ * deduped read-key set and UNION the per-key candidates into that existing
18
+ * pipeline — the budget is then applied exactly once to the union. Centralizing
19
+ * the key-set resolution here (rather than re-implementing per builder) follows
20
+ * CLAUDE.md rule 22 (scope resolution must be deduplicated).
21
+ */
22
+
23
+ /** A recall section's LCM read target: either a single key or an ordered set. */
24
+ export interface LcmReadSessionTarget {
25
+ /**
26
+ * The single LCM read `session_id` (pre-#1505 behavior). `undefined` means a
27
+ * sessionless, archive-wide read with no `session_id` filter.
28
+ */
29
+ sessionId?: string;
30
+ /**
31
+ * The ordered, read-authorized LCM read key set (primary overlay key first,
32
+ * then project / root fallbacks) the orchestrator derived from the same
33
+ * readable namespace set normal recall searches. When present and non-empty,
34
+ * it supersedes `sessionId`.
35
+ */
36
+ sessionIds?: readonly (string | undefined)[];
37
+ }
38
+
39
+ // `undefined` (a sessionless, archive-wide read) is a distinct, legitimate read
40
+ // target, so it needs a non-string sentinel in the dedupe set. A leading space
41
+ // keeps it disjoint from every real session key / namespaced LCM key (which are
42
+ // `[A-Za-z0-9._-]` plus the U+001F namespace sentinel, never leading-space).
43
+ const UNDEFINED_SESSION_SENTINEL = " <lcm-sessionless>";
44
+
45
+ /**
46
+ * Resolve the ordered, deduped set of LCM read `session_id`s a recall section
47
+ * must query.
48
+ *
49
+ * When `sessionIds` is provided (the #1505 fallback unification), it is used
50
+ * verbatim, deduped while preserving first-seen order so the caller queries
51
+ * keys in priority order (primary overlay → fallbacks) without re-querying an
52
+ * identical key (e.g. when two namespaces both collapse to the default store).
53
+ * Otherwise the section reads under the single `sessionId`, so the result is
54
+ * `[sessionId]` — byte-for-byte the pre-#1505 single-key behavior, including a
55
+ * single `undefined` for a sessionless archive-wide read.
56
+ */
57
+ export function resolveLcmReadSessionIds(
58
+ target: LcmReadSessionTarget,
59
+ ): Array<string | undefined> {
60
+ const source =
61
+ target.sessionIds && target.sessionIds.length > 0
62
+ ? target.sessionIds
63
+ : [target.sessionId];
64
+ const seen = new Set<string>();
65
+ const out: Array<string | undefined> = [];
66
+ for (const sessionId of source) {
67
+ const key = sessionId === undefined ? UNDEFINED_SESSION_SENTINEL : sessionId;
68
+ if (seen.has(key)) continue;
69
+ seen.add(key);
70
+ out.push(sessionId);
71
+ }
72
+ // Defensive: an all-empty `sessionIds` still collapses to the single-key path.
73
+ return out.length > 0 ? out : [target.sessionId];
74
+ }
75
+
76
+ /**
77
+ * Run a per-key LCM `gather` across the resolved read-key set with FAULT
78
+ * ISOLATION across keys (#1505 codex P2 review follow-up).
79
+ *
80
+ * A recall section that reads every key in a bare `for…await` loop loses the
81
+ * WHOLE section if any one key throws (e.g. a `SqliteError` from a corrupt or
82
+ * locked fallback index) — even when the primary overlay key already gathered
83
+ * evidence. The pre-#1505 first-non-empty read never had this problem: it
84
+ * returned the primary key's non-empty result without ever touching a failing
85
+ * fallback. This helper restores that resilience for the merged path: when more
86
+ * than one key is read, a per-key failure is contained so the other keys'
87
+ * evidence survives (best-effort recall — a total failure degrades to an empty
88
+ * section, which the orchestrator already treats as "no evidence").
89
+ *
90
+ * SINGLE-KEY is byte-for-byte the pre-#1505 behavior: the gather runs directly,
91
+ * so a failure PROPAGATES exactly as before (the caller / orchestrator catch
92
+ * still logs it). Fault isolation only engages once there is a fallback key that
93
+ * could fail independently of the primary.
94
+ */
95
+ export async function gatherAcrossReadSessions(
96
+ sessionIds: ReadonlyArray<string | undefined>,
97
+ gather: (sessionId: string | undefined) => Promise<void>,
98
+ ): Promise<void> {
99
+ if (sessionIds.length <= 1) {
100
+ for (const sessionId of sessionIds) {
101
+ await gather(sessionId);
102
+ }
103
+ return;
104
+ }
105
+ for (const sessionId of sessionIds) {
106
+ try {
107
+ await gather(sessionId);
108
+ } catch {
109
+ // One read key failed; keep the evidence already gathered from the other
110
+ // keys instead of discarding the whole section.
111
+ }
112
+ }
113
+ }
@@ -6,17 +6,22 @@ import type {
6
6
  SearchExecutionOptions,
7
7
  SearchQueryOptions,
8
8
  } from "../search/port.js";
9
+ import type { QmdCapabilities, QmdVersionStatus } from "../qmd.js";
9
10
  import type { PluginConfig, QmdSearchResult } from "../types.js";
10
11
 
11
12
  type CollectionState = "present" | "missing" | "unknown" | "skipped";
12
13
 
13
14
  class FakeBackend implements SearchBackend {
14
15
  updates = 0;
16
+ disposed = 0;
17
+ available = true;
15
18
  calls: Array<{
16
19
  method: string;
17
20
  collection: string | undefined;
18
21
  maxResults: number | undefined;
19
22
  }> = [];
23
+ availabilitySignals: Array<AbortSignal | undefined> = [];
24
+ probeCalls = 0;
20
25
  ensureSignals: Array<AbortSignal | undefined> = [];
21
26
  ensureCollections: Array<string | undefined> = [];
22
27
  checkSignals: Array<AbortSignal | undefined> = [];
@@ -29,6 +34,11 @@ class FakeBackend implements SearchBackend {
29
34
  check?: CollectionState;
30
35
  ensure?: CollectionState;
31
36
  } = {},
37
+ private readonly daemonMode = false,
38
+ private readonly diagnostics: {
39
+ debugStatus?: string;
40
+ versionStatus?: QmdVersionStatus;
41
+ } = {},
32
42
  ) {}
33
43
 
34
44
  private limitedResults(maxResults: number | undefined): QmdSearchResult[] {
@@ -38,15 +48,33 @@ class FakeBackend implements SearchBackend {
38
48
  }
39
49
 
40
50
  async probe(): Promise<boolean> {
41
- return true;
51
+ this.probeCalls += 1;
52
+ return this.available;
53
+ }
54
+
55
+ async checkAvailability(execution?: SearchExecutionOptions): Promise<boolean> {
56
+ this.availabilitySignals.push(execution?.signal);
57
+ return this.available;
42
58
  }
43
59
 
44
60
  isAvailable(): boolean {
45
- return true;
61
+ return this.available;
46
62
  }
47
63
 
48
64
  debugStatus(): string {
49
- return "fake";
65
+ return this.diagnostics.debugStatus ?? "fake";
66
+ }
67
+
68
+ isDaemonMode(): boolean {
69
+ return this.daemonMode;
70
+ }
71
+
72
+ getVersionStatus(): QmdVersionStatus | null {
73
+ return this.diagnostics.versionStatus ?? null;
74
+ }
75
+
76
+ async dispose(): Promise<void> {
77
+ this.disposed += 1;
50
78
  }
51
79
 
52
80
  async search(
@@ -154,6 +182,41 @@ function config(): PluginConfig {
154
182
  } as PluginConfig;
155
183
  }
156
184
 
185
+ function qmdCapabilities(enabled: boolean): QmdCapabilities {
186
+ return {
187
+ version: enabled ? "2.5.3" : null,
188
+ parsedVersion: enabled ? [2, 5, 3] : null,
189
+ stableSdk: enabled,
190
+ unifiedSearch: enabled,
191
+ getDocumentBody: enabled,
192
+ maintenanceApi: enabled,
193
+ legacySkillInstall: enabled,
194
+ intentHints: enabled,
195
+ explainTraces: enabled,
196
+ candidateLimit: enabled,
197
+ v2McpQueryTool: enabled,
198
+ structuredSearches: enabled,
199
+ queryRerankToggle: enabled,
200
+ chunkStrategy: enabled,
201
+ qmdBench: enabled,
202
+ perCollectionModels: enabled,
203
+ jsonLineNumbers: enabled,
204
+ editorLinks: enabled,
205
+ doctor: enabled,
206
+ versionedSkills: enabled,
207
+ absoluteSnippetLines: enabled,
208
+ fullQueryOutput: enabled,
209
+ forceCpu: enabled,
210
+ gpuBackendOverride: enabled,
211
+ embedParallelism: enabled,
212
+ modelEnvConsistency: enabled,
213
+ scopedEmbed: enabled,
214
+ safeStatusDeviceProbe: enabled,
215
+ mcpIndexSelection: enabled,
216
+ outputFormatFlag: enabled,
217
+ };
218
+ }
219
+
157
220
  test("updateNamespaces runs a global-update backend only once", async () => {
158
221
  const created: FakeBackend[] = [];
159
222
  const router = new NamespaceSearchRouter(
@@ -304,6 +367,198 @@ test("legacy default namespace root fail-opens missing guarded collections", asy
304
367
  assert.deepEqual(backend.ensureCollections, []);
305
368
  });
306
369
 
370
+ test("healthForNamespace checks namespace collection without auto-creating or caching state", async () => {
371
+ const created: FakeBackend[] = [];
372
+ const router = new NamespaceSearchRouter(
373
+ config(),
374
+ { storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
375
+ () => {
376
+ const backend = new FakeBackend(false, [], created.length === 0
377
+ ? { check: "missing" }
378
+ : { ensure: "present" });
379
+ created.push(backend);
380
+ return backend;
381
+ },
382
+ );
383
+
384
+ const health = await router.healthForNamespace("shared");
385
+
386
+ assert.equal(health.collectionState, "missing");
387
+ assert.equal(health.collection, "openclaw-engram--ns-736861726564");
388
+ assert.deepEqual(created[0]?.checkCollections, ["openclaw-engram--ns-736861726564"]);
389
+ assert.deepEqual(created[0]?.ensureCollections, []);
390
+ assert.equal(created[0]?.probeCalls, 0);
391
+ assert.equal(created[0]?.availabilitySignals.length, 1);
392
+ assert.equal(created[0]?.disposed, 1);
393
+
394
+ const ensured = await router.ensureNamespaceCollection("shared");
395
+
396
+ assert.equal(ensured, "present");
397
+ assert.equal(created.length, 2);
398
+ assert.deepEqual(created[1]?.ensureCollections, ["openclaw-engram--ns-736861726564"]);
399
+ });
400
+
401
+ test("healthForNamespace reports daemon mode from live cached namespace backend", async () => {
402
+ const created: FakeBackend[] = [];
403
+ const router = new NamespaceSearchRouter(
404
+ config(),
405
+ { storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
406
+ () => {
407
+ const backend = created.length === 0
408
+ ? new FakeBackend(false, [
409
+ {
410
+ path: "facts/a.md",
411
+ docid: "a",
412
+ score: 1,
413
+ snippet: "a",
414
+ },
415
+ ], { ensure: "present" }, true, {
416
+ debugStatus: "live-daemon",
417
+ versionStatus: {
418
+ installedVersion: "qmd 2.5.3",
419
+ supportedVersion: "2.5.3",
420
+ supported: true,
421
+ newerThanSupported: false,
422
+ upgradeAvailable: false,
423
+ capabilities: qmdCapabilities(true),
424
+ },
425
+ })
426
+ : new FakeBackend(false, [], { check: "present" }, false, {
427
+ debugStatus: "probe-unavailable",
428
+ versionStatus: {
429
+ installedVersion: null,
430
+ supportedVersion: "2.5.3",
431
+ supported: false,
432
+ newerThanSupported: false,
433
+ upgradeAvailable: false,
434
+ capabilities: qmdCapabilities(false),
435
+ },
436
+ });
437
+ if (created.length === 1) {
438
+ backend.available = false;
439
+ }
440
+ created.push(backend);
441
+ return backend;
442
+ },
443
+ );
444
+
445
+ await router.searchAcrossNamespaces({
446
+ query: "a",
447
+ namespaces: ["shared"],
448
+ maxResults: 1,
449
+ });
450
+ const health = await router.healthForNamespace("shared");
451
+
452
+ assert.equal(health.available, true);
453
+ assert.equal(health.daemonMode, true);
454
+ assert.equal(health.debugStatus, "live-daemon");
455
+ assert.equal(health.installedVersion, "qmd 2.5.3");
456
+ assert.equal(health.supportedVersion, "2.5.3");
457
+ assert.equal(health.supported, true);
458
+ assert.equal(health.upgradeAvailable, false);
459
+ assert.equal(health.doctorAvailable, true);
460
+ assert.equal(health.collectionState, "unknown");
461
+ assert.equal(created.length, 2);
462
+ assert.equal(created[0]?.disposed, 0);
463
+ assert.equal(created[1]?.disposed, 1);
464
+ assert.deepEqual(created[1]?.checkCollections, []);
465
+ });
466
+
467
+ test("healthForNamespace preserves cached missing collection state", async () => {
468
+ const created: FakeBackend[] = [];
469
+ const router = new NamespaceSearchRouter(
470
+ config(),
471
+ { storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
472
+ () => {
473
+ const backend = created.length === 0
474
+ ? new FakeBackend(false, [], { ensure: "missing" }, true)
475
+ : new FakeBackend(false, [], { check: "present" }, false);
476
+ created.push(backend);
477
+ return backend;
478
+ },
479
+ );
480
+
481
+ assert.deepEqual(
482
+ await router.searchAcrossNamespaces({
483
+ query: "a",
484
+ namespaces: ["shared"],
485
+ maxResults: 1,
486
+ }),
487
+ [],
488
+ );
489
+ const health = await router.healthForNamespace("shared");
490
+
491
+ assert.equal(health.available, true);
492
+ assert.equal(health.daemonMode, true);
493
+ assert.equal(health.collectionState, "missing");
494
+ assert.equal(created.length, 2);
495
+ assert.deepEqual(created[1]?.checkCollections, ["openclaw-engram--ns-736861726564"]);
496
+ });
497
+
498
+ test("healthForNamespace stops waiting when namespace availability probe aborts", async () => {
499
+ const backend = new class extends FakeBackend {
500
+ override async checkAvailability(execution?: SearchExecutionOptions): Promise<boolean> {
501
+ this.availabilitySignals.push(execution?.signal);
502
+ return await new Promise<boolean>(() => {});
503
+ }
504
+ }(false);
505
+ const router = new NamespaceSearchRouter(
506
+ config(),
507
+ { storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
508
+ () => backend,
509
+ );
510
+ const controller = new AbortController();
511
+ controller.abort();
512
+
513
+ const health = await router.healthForNamespace("shared", {
514
+ signal: controller.signal,
515
+ });
516
+
517
+ assert.equal(health.available, false);
518
+ assert.equal(health.collectionState, "unknown");
519
+ assert.equal(backend.probeCalls, 0);
520
+ assert.equal(backend.availabilitySignals[0], controller.signal);
521
+ assert.deepEqual(backend.checkCollections, []);
522
+ assert.deepEqual(backend.ensureCollections, []);
523
+ assert.equal(backend.disposed, 1);
524
+ });
525
+
526
+ test("ensureNamespaceCollection does not cache aborted namespace backend probes", async () => {
527
+ const created: FakeBackend[] = [];
528
+ const router = new NamespaceSearchRouter(
529
+ config(),
530
+ { storageFor: async (namespace: string) => ({ dir: `/tmp/remnic/${namespace}` }) },
531
+ () => {
532
+ const backend = created.length === 0
533
+ ? new class extends FakeBackend {
534
+ override async probe(): Promise<boolean> {
535
+ return await new Promise<boolean>(() => {});
536
+ }
537
+ }(false)
538
+ : new FakeBackend(false, [], { ensure: "present" });
539
+ created.push(backend);
540
+ return backend;
541
+ },
542
+ );
543
+ const controller = new AbortController();
544
+ controller.abort();
545
+
546
+ await assert.rejects(
547
+ () => router.ensureNamespaceCollection("shared", { signal: controller.signal }),
548
+ /operation aborted/,
549
+ );
550
+
551
+ assert.equal(created[0]?.disposed, 1);
552
+ assert.deepEqual(created[0]?.checkCollections, []);
553
+ assert.deepEqual(created[0]?.ensureCollections, []);
554
+
555
+ const ensured = await router.ensureNamespaceCollection("shared");
556
+
557
+ assert.equal(ensured, "present");
558
+ assert.equal(created.length, 2);
559
+ assert.deepEqual(created[1]?.ensureCollections, ["openclaw-engram--ns-736861726564"]);
560
+ });
561
+
307
562
  test("legacy default namespace root filters nested namespace search results", async () => {
308
563
  const router = new NamespaceSearchRouter(
309
564
  config(),