@remnic/core 9.3.650 → 9.3.651

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 (154) hide show
  1. package/dist/access-cli.js +35 -34
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.js +15 -15
  4. package/dist/access-mcp.js +14 -14
  5. package/dist/access-schema.js +3 -3
  6. package/dist/access-service.js +12 -12
  7. package/dist/{auto-sync-54QQHOG5.js → auto-sync-5CJBJMPZ.js} +5 -5
  8. package/dist/briefing.js +3 -3
  9. package/dist/calibration.js +2 -2
  10. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
  11. package/dist/causal-consolidation.js +6 -6
  12. package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  13. package/dist/{chunk-OAZ5MFUB.js → chunk-3XGWCZ63.js} +45 -28
  14. package/dist/chunk-3XGWCZ63.js.map +1 -0
  15. package/dist/{chunk-QKE4LHNR.js → chunk-4HYSMH7D.js} +2 -2
  16. package/dist/{chunk-NMIOW7XG.js → chunk-4PTKFBST.js} +2 -2
  17. package/dist/{chunk-DDRNDPX4.js → chunk-4SKKVWLQ.js} +2 -2
  18. package/dist/chunk-5FOCXX5E.js +34 -0
  19. package/dist/chunk-5FOCXX5E.js.map +1 -0
  20. package/dist/{chunk-23RYLGYA.js → chunk-5WSDHTBO.js} +100 -111
  21. package/dist/chunk-5WSDHTBO.js.map +1 -0
  22. package/dist/{chunk-WPCCNSWO.js → chunk-6UKL6IXM.js} +4 -4
  23. package/dist/{chunk-DB5A3NHS.js → chunk-7LWRCOP7.js} +9 -2
  24. package/dist/chunk-7LWRCOP7.js.map +1 -0
  25. package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
  26. package/dist/{chunk-4BISW7RX.js → chunk-AJE7FJVE.js} +2 -2
  27. package/dist/{chunk-ZXWAQFDE.js → chunk-CFOCZPIQ.js} +2 -2
  28. package/dist/{chunk-NT5TINK5.js → chunk-DHGSZ3UD.js} +2 -2
  29. package/dist/{chunk-OTC2KOZ2.js → chunk-EHQLDFSH.js} +2 -2
  30. package/dist/{chunk-AMACWKM4.js → chunk-IJHLC5CH.js} +2 -2
  31. package/dist/{chunk-OR7R6M5Z.js → chunk-IVYSVAC6.js} +2 -2
  32. package/dist/{chunk-UMKPSD35.js → chunk-JF7SFXTG.js} +2 -2
  33. package/dist/{chunk-MCYT2RNT.js → chunk-KJDKZVF3.js} +3 -3
  34. package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
  35. package/dist/{chunk-PQFUUXWK.js → chunk-KWM33SPU.js} +2 -2
  36. package/dist/{chunk-A3BS64GV.js → chunk-LCC5EZTT.js} +4 -4
  37. package/dist/{chunk-ZT6R3WR3.js → chunk-LFTLXOFX.js} +4 -4
  38. package/dist/{chunk-3IJEQWQX.js → chunk-MF32AL7N.js} +4 -4
  39. package/dist/{chunk-D6WVJIS3.js → chunk-ORGWWNJG.js} +2 -2
  40. package/dist/{chunk-Z3PZRDLW.js → chunk-PRQXUSQV.js} +2 -2
  41. package/dist/{chunk-VWT3F4IV.js → chunk-PS3SYNHP.js} +12 -4
  42. package/dist/chunk-PS3SYNHP.js.map +1 -0
  43. package/dist/{chunk-IMWFHBG2.js → chunk-QWRC7GIO.js} +2 -2
  44. package/dist/{chunk-TUMH6EDV.js → chunk-RKN5J4RO.js} +26 -26
  45. package/dist/{chunk-TVOPSKOK.js → chunk-RSS2KWN6.js} +4 -4
  46. package/dist/{chunk-U3GQ33JC.js → chunk-SLTKP5WJ.js} +2 -2
  47. package/dist/{chunk-YAFSTKTH.js → chunk-SLYD3AH4.js} +10 -10
  48. package/dist/{chunk-6NKAQ74D.js → chunk-UU6MVCJ6.js} +1 -1
  49. package/dist/chunk-UU6MVCJ6.js.map +1 -0
  50. package/dist/{chunk-WEPMT6SC.js → chunk-V25ZAOSB.js} +5 -5
  51. package/dist/{chunk-UMTG2BN2.js → chunk-V4UDXYGG.js} +2 -2
  52. package/dist/{chunk-RRRCNIPK.js → chunk-WJK75OCH.js} +4 -4
  53. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  54. package/dist/{chunk-OZKZ2TRP.js → chunk-XBIACVCO.js} +9 -2
  55. package/dist/chunk-XBIACVCO.js.map +1 -0
  56. package/dist/{chunk-ALUZN7BE.js → chunk-XMN6MMTU.js} +2 -2
  57. package/dist/{chunk-A4BTPHIN.js → chunk-Y7NWBBHV.js} +6 -6
  58. package/dist/{chunk-M75TBFKQ.js → chunk-Z2OXSMZK.js} +2 -2
  59. package/dist/cli.js +30 -30
  60. package/dist/compounding/engine.js +3 -3
  61. package/dist/connectors/codex-materialize-runner.js +3 -3
  62. package/dist/connectors/index.js +3 -3
  63. package/dist/entity-retrieval.js +3 -3
  64. package/dist/event-order-recall.js +1 -1
  65. package/dist/explicit-cue-recall.d.ts +7 -0
  66. package/dist/explicit-cue-recall.js +2 -1
  67. package/dist/extraction-judge.js +3 -3
  68. package/dist/extraction.js +3 -3
  69. package/dist/fallback-llm.js +2 -2
  70. package/dist/focused-list-recall.d.ts +6 -0
  71. package/dist/focused-list-recall.js +2 -1
  72. package/dist/index.js +83 -82
  73. package/dist/index.js.map +1 -1
  74. package/dist/lcm/engine.js +2 -2
  75. package/dist/lcm/index.js +5 -5
  76. package/dist/lcm-fallback-read.d.ts +71 -0
  77. package/dist/lcm-fallback-read.js +10 -0
  78. package/dist/lcm-fallback-read.js.map +1 -0
  79. package/dist/maintenance/memory-governance.js +3 -3
  80. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  81. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  82. package/dist/namespaces/migrate.js +7 -7
  83. package/dist/namespaces/search.js +3 -3
  84. package/dist/namespaces/storage.js +3 -3
  85. package/dist/operator-toolkit.js +9 -9
  86. package/dist/orchestrator.js +29 -28
  87. package/dist/recall-planner-llm.js +2 -2
  88. package/dist/response-guidance-recall.d.ts +6 -0
  89. package/dist/response-guidance-recall.js +2 -1
  90. package/dist/schemas.d.ts +22 -22
  91. package/dist/search/factory.js +2 -2
  92. package/dist/search/index.js +4 -4
  93. package/dist/semantic-consolidation.js +4 -4
  94. package/dist/semantic-rule-promotion.js +3 -3
  95. package/dist/semantic-rule-verifier.js +3 -3
  96. package/dist/storage.js +2 -2
  97. package/dist/summarizer.js +3 -3
  98. package/dist/targeted-fact-recall.d.ts +6 -0
  99. package/dist/targeted-fact-recall.js +2 -1
  100. package/dist/transfer/backup.js +2 -2
  101. package/dist/transfer/capsule-export.js +2 -2
  102. package/dist/transfer/capsule-import.js +2 -2
  103. package/dist/transfer/import-sqlite.js +2 -2
  104. package/dist/transfer/types.d.ts +12 -12
  105. package/dist/verified-recall.js +3 -3
  106. package/package.json +1 -1
  107. package/src/event-order-recall.ts +8 -0
  108. package/src/explicit-cue-recall.ts +70 -29
  109. package/src/focused-list-recall.ts +23 -1
  110. package/src/lcm-fallback-read.ts +113 -0
  111. package/src/orchestrator.ts +168 -121
  112. package/src/response-guidance-recall.ts +21 -1
  113. package/src/targeted-fact-recall.ts +24 -3
  114. package/dist/chunk-23RYLGYA.js.map +0 -1
  115. package/dist/chunk-6NKAQ74D.js.map +0 -1
  116. package/dist/chunk-DB5A3NHS.js.map +0 -1
  117. package/dist/chunk-OAZ5MFUB.js.map +0 -1
  118. package/dist/chunk-OZKZ2TRP.js.map +0 -1
  119. package/dist/chunk-VWT3F4IV.js.map +0 -1
  120. /package/dist/{auto-sync-54QQHOG5.js.map → auto-sync-5CJBJMPZ.js.map} +0 -0
  121. /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
  122. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  123. /package/dist/{chunk-QKE4LHNR.js.map → chunk-4HYSMH7D.js.map} +0 -0
  124. /package/dist/{chunk-NMIOW7XG.js.map → chunk-4PTKFBST.js.map} +0 -0
  125. /package/dist/{chunk-DDRNDPX4.js.map → chunk-4SKKVWLQ.js.map} +0 -0
  126. /package/dist/{chunk-WPCCNSWO.js.map → chunk-6UKL6IXM.js.map} +0 -0
  127. /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
  128. /package/dist/{chunk-4BISW7RX.js.map → chunk-AJE7FJVE.js.map} +0 -0
  129. /package/dist/{chunk-ZXWAQFDE.js.map → chunk-CFOCZPIQ.js.map} +0 -0
  130. /package/dist/{chunk-NT5TINK5.js.map → chunk-DHGSZ3UD.js.map} +0 -0
  131. /package/dist/{chunk-OTC2KOZ2.js.map → chunk-EHQLDFSH.js.map} +0 -0
  132. /package/dist/{chunk-AMACWKM4.js.map → chunk-IJHLC5CH.js.map} +0 -0
  133. /package/dist/{chunk-OR7R6M5Z.js.map → chunk-IVYSVAC6.js.map} +0 -0
  134. /package/dist/{chunk-UMKPSD35.js.map → chunk-JF7SFXTG.js.map} +0 -0
  135. /package/dist/{chunk-MCYT2RNT.js.map → chunk-KJDKZVF3.js.map} +0 -0
  136. /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
  137. /package/dist/{chunk-PQFUUXWK.js.map → chunk-KWM33SPU.js.map} +0 -0
  138. /package/dist/{chunk-A3BS64GV.js.map → chunk-LCC5EZTT.js.map} +0 -0
  139. /package/dist/{chunk-ZT6R3WR3.js.map → chunk-LFTLXOFX.js.map} +0 -0
  140. /package/dist/{chunk-3IJEQWQX.js.map → chunk-MF32AL7N.js.map} +0 -0
  141. /package/dist/{chunk-D6WVJIS3.js.map → chunk-ORGWWNJG.js.map} +0 -0
  142. /package/dist/{chunk-Z3PZRDLW.js.map → chunk-PRQXUSQV.js.map} +0 -0
  143. /package/dist/{chunk-IMWFHBG2.js.map → chunk-QWRC7GIO.js.map} +0 -0
  144. /package/dist/{chunk-TUMH6EDV.js.map → chunk-RKN5J4RO.js.map} +0 -0
  145. /package/dist/{chunk-TVOPSKOK.js.map → chunk-RSS2KWN6.js.map} +0 -0
  146. /package/dist/{chunk-U3GQ33JC.js.map → chunk-SLTKP5WJ.js.map} +0 -0
  147. /package/dist/{chunk-YAFSTKTH.js.map → chunk-SLYD3AH4.js.map} +0 -0
  148. /package/dist/{chunk-WEPMT6SC.js.map → chunk-V25ZAOSB.js.map} +0 -0
  149. /package/dist/{chunk-UMTG2BN2.js.map → chunk-V4UDXYGG.js.map} +0 -0
  150. /package/dist/{chunk-RRRCNIPK.js.map → chunk-WJK75OCH.js.map} +0 -0
  151. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  152. /package/dist/{chunk-ALUZN7BE.js.map → chunk-XMN6MMTU.js.map} +0 -0
  153. /package/dist/{chunk-A4BTPHIN.js.map → chunk-Y7NWBBHV.js.map} +0 -0
  154. /package/dist/{chunk-M75TBFKQ.js.map → chunk-Z2OXSMZK.js.map} +0 -0
@@ -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
+ }
@@ -7243,16 +7243,24 @@ export class Orchestrator {
7243
7243
  );
7244
7244
  // Query an LCM-backed read across the ordered read key set and return the
7245
7245
  // 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).
7246
+ // overlay key is tried first; if a branch-scoped session has no rows under its
7247
+ // branch key, the project / root fallback keys are tried in order.
7248
+ //
7249
+ // #1505 codex P2 ("Merge LCM fallback reads instead of short-circuiting"): the
7250
+ // query-SCORED sections (explicit-cue, targeted-facts, focused-list,
7251
+ // response-guidance, event-order, structured message-parts) no longer use this
7252
+ // helper they MERGE candidates across EVERY authorized key under their single
7253
+ // budget (a weak primary-key hit must not mask stronger fallback evidence; the
7254
+ // section builders take `sessionIds`, structured-parts merges inline below).
7255
+ // This first-non-empty helper now serves ONLY the compressed-history section,
7256
+ // which is a per-session HOLISTIC DAG narrative with no per-item id to merge or
7257
+ // dedupe on — see its call site for the rationale.
7258
+ //
7259
+ // When the set is a single key (single-user / no-overlay / explicit-namespace),
7260
+ // this is exactly one call — unchanged. `lcmSessionId` is `string | undefined`:
7261
+ // a SESSIONLESS recall yields the single `undefined` key so the read runs ONE
7262
+ // archive-wide read with no `session_id` filter (pre-#1505 behavior). NEVER the
7263
+ // literal "default" session id (codex P2).
7256
7264
  const firstNonEmptyLcmRead = async <T>(
7257
7265
  read: (lcmSessionId: string | undefined) => Promise<T>,
7258
7266
  isEmpty: (value: T) => boolean,
@@ -9513,24 +9521,21 @@ export class Orchestrator {
9513
9521
  (recallMode as RecallPlanMode) !== "no_recall"
9514
9522
  ) {
9515
9523
  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
- );
9524
+ const explicitCueSection = await buildExplicitCueRecallSection({
9525
+ engine: this.lcmEngine,
9526
+ // #1495 thread 3 + #1505 fallback unification: read across the ordered
9527
+ // LCM read key set (primary overlay → coding fallbacks) so a
9528
+ // branch-scoped session finds its own explicit-cue evidence even when
9529
+ // archived at project/root scope (rule 39). #1505 codex P2: the builder
9530
+ // MERGES candidates across every key under its single budget instead of
9531
+ // short-circuiting on the first non-empty key.
9532
+ sessionIds: lcmReadSessionIds,
9533
+ query: retrievalQuery,
9534
+ maxChars: explicitCueMaxChars,
9535
+ maxReferences:
9536
+ this.getRecallSectionNumber("explicit-cue", "maxResults") ??
9537
+ this.config.explicitCueRecallMaxReferences,
9538
+ });
9534
9539
  if (explicitCueSection) {
9535
9540
  this.appendRecallSection(
9536
9541
  sectionBuckets,
@@ -9560,29 +9565,25 @@ export class Orchestrator {
9560
9565
  shouldRecallTargetedFactEvidence(retrievalQuery)
9561
9566
  ) {
9562
9567
  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
- );
9568
+ const targetedFactSection = await buildTargetedFactRecallSection({
9569
+ engine: this.lcmEngine,
9570
+ // #1495 + #1505 fallback unification: read across the ordered LCM read
9571
+ // key set so a branch-scoped session finds its own targeted-fact
9572
+ // evidence even when archived at project/root scope. #1505 codex P2: the
9573
+ // builder MERGES candidates across every key under its single budget.
9574
+ sessionIds: lcmReadSessionIds,
9575
+ query: retrievalQuery,
9576
+ maxChars: targetedFactMaxChars,
9577
+ maxSearchResults:
9578
+ this.getRecallSectionNumber("targeted-facts", "maxResults") ??
9579
+ this.config.targetedFactRecallMaxResults,
9580
+ maxScanWindowTurns:
9581
+ this.getRecallSectionNumber("targeted-facts", "maxTurns") ??
9582
+ this.config.targetedFactRecallScanWindowTurns,
9583
+ maxScanWindowTokens:
9584
+ this.getRecallSectionNumber("targeted-facts", "maxTokens") ??
9585
+ this.config.targetedFactRecallScanWindowTokens,
9586
+ });
9586
9587
  if (targetedFactSection) {
9587
9588
  this.appendRecallSection(
9588
9589
  sectionBuckets,
@@ -9613,29 +9614,26 @@ export class Orchestrator {
9613
9614
  shouldRecallFocusedListEvidence(retrievalQuery)
9614
9615
  ) {
9615
9616
  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
- );
9617
+ const focusedListSection = await buildFocusedListRecallSection({
9618
+ engine: this.lcmEngine,
9619
+ // #1495 thread 3 + #1505 fallback unification: read across the ordered
9620
+ // LCM read key set so a branch-scoped session reads its own
9621
+ // focused-list/count evidence even at project/root scope (rule 39).
9622
+ // #1505 codex P2: the builder MERGES candidates across every key under
9623
+ // its single budget.
9624
+ sessionIds: lcmReadSessionIds,
9625
+ query: retrievalQuery,
9626
+ maxChars: focusedListMaxChars,
9627
+ maxSearchResults:
9628
+ this.getRecallSectionNumber("focused-list", "maxResults") ??
9629
+ this.config.focusedListRecallMaxResults,
9630
+ maxScanWindowTurns:
9631
+ this.getRecallSectionNumber("focused-list", "maxTurns") ??
9632
+ this.config.focusedListRecallScanWindowTurns,
9633
+ maxScanWindowTokens:
9634
+ this.getRecallSectionNumber("focused-list", "maxTokens") ??
9635
+ this.config.focusedListRecallScanWindowTokens,
9636
+ });
9639
9637
  if (focusedListSection) {
9640
9638
  this.appendRecallSection(
9641
9639
  sectionBuckets,
@@ -9670,30 +9668,27 @@ export class Orchestrator {
9670
9668
  (responseGuidanceMatchesQuery || responseGuidanceForcedByPipeline)
9671
9669
  ) {
9672
9670
  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
- );
9671
+ const responseGuidanceSection = await buildResponseGuidanceRecallSection({
9672
+ engine: this.lcmEngine,
9673
+ // #1495 thread 3 + #1505 fallback unification: read across the ordered
9674
+ // LCM read key set so a branch-scoped session reads its own
9675
+ // response-guidance evidence even at project/root scope (rule 39).
9676
+ // #1505 codex P2: the builder MERGES candidates across every key under
9677
+ // its single budget.
9678
+ sessionIds: lcmReadSessionIds,
9679
+ query: retrievalQuery,
9680
+ maxChars: responseGuidanceMaxChars,
9681
+ maxSearchResults:
9682
+ this.getRecallSectionNumber("response-guidance", "maxResults") ??
9683
+ this.config.responseGuidanceRecallMaxResults,
9684
+ maxScanWindowTurns:
9685
+ this.getRecallSectionNumber("response-guidance", "maxTurns") ??
9686
+ this.config.responseGuidanceRecallScanWindowTurns,
9687
+ maxScanWindowTokens:
9688
+ this.getRecallSectionNumber("response-guidance", "maxTokens") ??
9689
+ this.config.responseGuidanceRecallScanWindowTokens,
9690
+ forceGeneric: responseGuidanceForcedByPipeline,
9691
+ });
9697
9692
  if (responseGuidanceSection) {
9698
9693
  this.appendRecallSection(
9699
9694
  sectionBuckets,
@@ -9722,13 +9717,22 @@ export class Orchestrator {
9722
9717
  shouldRecallEventOrderEvidence(retrievalQuery)
9723
9718
  ) {
9724
9719
  try {
9720
+ // #1495 thread 3 + #1505 fallback unification: read across the ordered LCM
9721
+ // read key set so a branch-scoped session reads its own chronological
9722
+ // event-order evidence even at project/root scope. UNLIKE the relevance-
9723
+ // ranked sections, event-order must NOT merge across keys: `turn_index` is
9724
+ // LOCAL to each LCM `session_id` (`observe` numbers turns per session via
9725
+ // `getMaxTurnIndex`), so interleaving two keys and sorting by `turn_index`
9726
+ // would place an older project-scope turn after a newer branch-scope turn
9727
+ // and misstate the chronology (#1505 codex P2). Like compressed-history,
9728
+ // event-order is an inherently per-session ORDERED artifact, so it takes
9729
+ // the highest-priority authorized key (primary overlay → project/root)
9730
+ // that actually has chronological evidence — each key's timeline is
9731
+ // internally consistent.
9725
9732
  const eventOrderSection = await firstNonEmptyLcmRead(
9726
9733
  (lcmSessionId) =>
9727
9734
  buildEventOrderRecallSection({
9728
9735
  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
9736
  sessionId: lcmSessionId,
9733
9737
  query: retrievalQuery,
9734
9738
  maxChars: eventOrderMaxChars,
@@ -9918,21 +9922,59 @@ export class Orchestrator {
9918
9922
  (recallMode as RecallPlanMode) !== "no_recall"
9919
9923
  ) {
9920
9924
  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
- [],
9925
+ // #1495 + #1505 fallback unification: read across the ordered LCM read
9926
+ // key set so a branch-scoped session reads its own structured
9927
+ // message-part evidence even when archived at project/root scope.
9928
+ // #1505 codex P2: structured matches are query-SCORED evidence, so MERGE
9929
+ // across EVERY key (primary overlay project/root fallbacks) instead of
9930
+ // short-circuiting on the first non-empty key a weak branch-key hit must
9931
+ // not mask stronger project-fallback parts. Keys are queried in priority
9932
+ // order; dedupe by session_id+turn_index+part_id keeps the primary key's
9933
+ // row on collision. `formatStructuredRecall` applies the single budget
9934
+ // below. A sessionless key (`undefined`) normalizes to "" → no matches
9935
+ // (structured parts are inherently per-session; pre-#1505 behavior, codex
9936
+ // P2).
9937
+ // FAULT ISOLATION (allSettled, not all): the pre-#1505 first-non-empty read
9938
+ // short-circuited, so a fallback key was often never queried and its latent
9939
+ // search failure never surfaced. Querying every key eagerly must NOT let one
9940
+ // key's failure (e.g. a SqliteError from a corrupt/locked fallback index)
9941
+ // reject the batch and discard the OTHER keys' parts — or, since this and
9942
+ // the compressed-history read below share one try block, silently drop the
9943
+ // compressed-history section a healthy primary key would still produce. So
9944
+ // read each key independently and keep the fulfilled batches.
9945
+ const structuredSettled = await Promise.allSettled(
9946
+ lcmReadSessionIds.map((lcmSessionId) =>
9947
+ this.lcmEngine!.searchStructuredParts(lcmSessionId ?? "", retrievalQuery),
9948
+ ),
9935
9949
  );
9950
+ for (const settled of structuredSettled) {
9951
+ if (settled.status === "rejected") {
9952
+ log.debug(
9953
+ `LCM structured-parts read failed for one key: ${settled.reason}`,
9954
+ );
9955
+ }
9956
+ }
9957
+ const seenStructuredParts = new Set<string>();
9958
+ const structuredMatches = structuredSettled
9959
+ .flatMap((settled) => (settled.status === "fulfilled" ? settled.value : []))
9960
+ .filter((match) => {
9961
+ const key = `${match.session_id} ${match.turn_index} ${match.part_id}`;
9962
+ if (seenStructuredParts.has(key)) return false;
9963
+ seenStructuredParts.add(key);
9964
+ return true;
9965
+ })
9966
+ // Restore the archive's per-key ordering (score DESC, then turn DESC)
9967
+ // across the MERGED set so the strongest parts win the shared budget in
9968
+ // `formatStructuredRecall` — otherwise weak primary-key parts could crowd
9969
+ // out stronger fallback parts. Stable sort: a single key is already in
9970
+ // this order, so it stays byte-for-byte the pre-#1505 behavior.
9971
+ // `?? 0` is defensive: `LcmStructuredRecallMatch.score` is always a
9972
+ // number here, but a bare `b.score - a.score` would yield NaN (falsy)
9973
+ // for any future unscored match and silently fall through to turn order.
9974
+ .sort(
9975
+ (a, b) =>
9976
+ (b.score ?? 0) - (a.score ?? 0) || b.turn_index - a.turn_index,
9977
+ );
9936
9978
  const structuredSection = this.lcmEngine.formatStructuredRecall(
9937
9979
  structuredMatches,
9938
9980
  Math.ceil(this.config.recallBudgetChars * 0.08),
@@ -9955,15 +9997,20 @@ export class Orchestrator {
9955
9997
  }
9956
9998
  }
9957
9999
  }
10000
+ // #1495 + #1505 fallback unification: read across the ordered LCM read key
10001
+ // set so a branch-scoped session reads its own compressed-history evidence
10002
+ // even at project/root scope. UNLIKE the query-scored sections above, the
10003
+ // compressed history is a per-session HOLISTIC DAG narrative, not a set of
10004
+ // independently-rankable evidence items — concatenating two sessions'
10005
+ // summaries would double-count the conversation and blow the budget, and
10006
+ // there is no per-item id to dedupe on. So this section deliberately keeps
10007
+ // first-non-empty semantics (#1505 codex P2 scope: "merge the query-matched
10008
+ // sections"): the highest-priority authorized key (primary overlay →
10009
+ // project/root) that actually has a compressed history wins. A sessionless
10010
+ // key (`undefined`) normalizes to empty → no section (pre-#1505 behavior).
9958
10011
  const lcmSection = await firstNonEmptyLcmRead(
9959
10012
  (lcmSessionId) =>
9960
10013
  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
10014
  lcmSessionId ?? "",
9968
10015
  this.config.recallBudgetChars,
9969
10016
  ),