@remnic/core 9.3.649 → 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 (176) hide show
  1. package/dist/access-cli.js +36 -35
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.d.ts +2 -2
  4. package/dist/access-http.js +16 -16
  5. package/dist/access-mcp.d.ts +2 -2
  6. package/dist/access-mcp.js +15 -15
  7. package/dist/access-schema.js +3 -3
  8. package/dist/{access-service-DFXIlGvZ.d.ts → access-service-DIZRHQ7Q.d.ts} +255 -2
  9. package/dist/access-service.d.ts +2 -2
  10. package/dist/access-service.js +13 -13
  11. package/dist/{auto-sync-54QQHOG5.js → auto-sync-5CJBJMPZ.js} +5 -5
  12. package/dist/bootstrap.d.ts +1 -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-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  18. package/dist/{chunk-OAZ5MFUB.js → chunk-3XGWCZ63.js} +45 -28
  19. package/dist/chunk-3XGWCZ63.js.map +1 -0
  20. package/dist/{chunk-QKE4LHNR.js → chunk-4HYSMH7D.js} +2 -2
  21. package/dist/{chunk-NMIOW7XG.js → chunk-4PTKFBST.js} +2 -2
  22. package/dist/{chunk-DDRNDPX4.js → chunk-4SKKVWLQ.js} +2 -2
  23. package/dist/chunk-5FOCXX5E.js +34 -0
  24. package/dist/chunk-5FOCXX5E.js.map +1 -0
  25. package/dist/{chunk-XUGVP7ZU.js → chunk-5WSDHTBO.js} +166 -47
  26. package/dist/chunk-5WSDHTBO.js.map +1 -0
  27. package/dist/{chunk-WPCCNSWO.js → chunk-6UKL6IXM.js} +4 -4
  28. package/dist/{chunk-DB5A3NHS.js → chunk-7LWRCOP7.js} +9 -2
  29. package/dist/chunk-7LWRCOP7.js.map +1 -0
  30. package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
  31. package/dist/{chunk-4BISW7RX.js → chunk-AJE7FJVE.js} +2 -2
  32. package/dist/{chunk-ZXWAQFDE.js → chunk-CFOCZPIQ.js} +2 -2
  33. package/dist/{chunk-NT5TINK5.js → chunk-DHGSZ3UD.js} +2 -2
  34. package/dist/{chunk-OTC2KOZ2.js → chunk-EHQLDFSH.js} +2 -2
  35. package/dist/{chunk-AMACWKM4.js → chunk-IJHLC5CH.js} +2 -2
  36. package/dist/{chunk-OR7R6M5Z.js → chunk-IVYSVAC6.js} +2 -2
  37. package/dist/{chunk-UMKPSD35.js → chunk-JF7SFXTG.js} +2 -2
  38. package/dist/{chunk-MCYT2RNT.js → chunk-KJDKZVF3.js} +3 -3
  39. package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
  40. package/dist/{chunk-PQFUUXWK.js → chunk-KWM33SPU.js} +2 -2
  41. package/dist/{chunk-A3BS64GV.js → chunk-LCC5EZTT.js} +4 -4
  42. package/dist/{chunk-ZT6R3WR3.js → chunk-LFTLXOFX.js} +4 -4
  43. package/dist/{chunk-CNRZ6WJU.js → chunk-MF32AL7N.js} +5 -5
  44. package/dist/{chunk-6GIKAUTN.js → chunk-MMJANTJX.js} +33 -2
  45. package/dist/{chunk-6GIKAUTN.js.map → chunk-MMJANTJX.js.map} +1 -1
  46. package/dist/{chunk-D6WVJIS3.js → chunk-ORGWWNJG.js} +2 -2
  47. package/dist/{chunk-Z3PZRDLW.js → chunk-PRQXUSQV.js} +2 -2
  48. package/dist/{chunk-VWT3F4IV.js → chunk-PS3SYNHP.js} +12 -4
  49. package/dist/chunk-PS3SYNHP.js.map +1 -0
  50. package/dist/{chunk-IMWFHBG2.js → chunk-QWRC7GIO.js} +2 -2
  51. package/dist/{chunk-FQYFMIKG.js → chunk-RKN5J4RO.js} +26 -26
  52. package/dist/{chunk-FUXV6HSO.js → chunk-RSS2KWN6.js} +5 -5
  53. package/dist/{chunk-U3GQ33JC.js → chunk-SLTKP5WJ.js} +2 -2
  54. package/dist/{chunk-5ETA6OAS.js → chunk-SLYD3AH4.js} +617 -89
  55. package/dist/chunk-SLYD3AH4.js.map +1 -0
  56. package/dist/{chunk-6NKAQ74D.js → chunk-UU6MVCJ6.js} +1 -1
  57. package/dist/chunk-UU6MVCJ6.js.map +1 -0
  58. package/dist/{chunk-WEPMT6SC.js → chunk-V25ZAOSB.js} +5 -5
  59. package/dist/{chunk-UMTG2BN2.js → chunk-V4UDXYGG.js} +2 -2
  60. package/dist/{chunk-RRRCNIPK.js → chunk-WJK75OCH.js} +4 -4
  61. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  62. package/dist/{chunk-OZKZ2TRP.js → chunk-XBIACVCO.js} +9 -2
  63. package/dist/chunk-XBIACVCO.js.map +1 -0
  64. package/dist/{chunk-ALUZN7BE.js → chunk-XMN6MMTU.js} +2 -2
  65. package/dist/{chunk-A4BTPHIN.js → chunk-Y7NWBBHV.js} +6 -6
  66. package/dist/{chunk-M75TBFKQ.js → chunk-Z2OXSMZK.js} +2 -2
  67. package/dist/{cli-DrL2Nv4j.d.ts → cli-BG4ybtJr.d.ts} +2 -2
  68. package/dist/cli.d.ts +3 -3
  69. package/dist/cli.js +31 -31
  70. package/dist/compounding/engine.js +3 -3
  71. package/dist/connectors/codex-materialize-runner.js +3 -3
  72. package/dist/connectors/index.js +3 -3
  73. package/dist/entity-retrieval.js +3 -3
  74. package/dist/event-order-recall.js +1 -1
  75. package/dist/explicit-capture.d.ts +1 -1
  76. package/dist/explicit-cue-recall.d.ts +7 -0
  77. package/dist/explicit-cue-recall.js +2 -1
  78. package/dist/extraction-judge.js +3 -3
  79. package/dist/extraction.js +3 -3
  80. package/dist/fallback-llm.js +2 -2
  81. package/dist/focused-list-recall.d.ts +6 -0
  82. package/dist/focused-list-recall.js +2 -1
  83. package/dist/index.d.ts +4 -4
  84. package/dist/index.js +84 -83
  85. package/dist/index.js.map +1 -1
  86. package/dist/lcm/engine.js +2 -2
  87. package/dist/lcm/index.js +5 -5
  88. package/dist/lcm-fallback-read.d.ts +71 -0
  89. package/dist/lcm-fallback-read.js +10 -0
  90. package/dist/lcm-fallback-read.js.map +1 -0
  91. package/dist/maintenance/memory-governance.js +3 -3
  92. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  93. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  94. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  95. package/dist/namespaces/migrate.js +7 -7
  96. package/dist/namespaces/search.js +3 -3
  97. package/dist/namespaces/storage.js +3 -3
  98. package/dist/operator-toolkit.js +9 -9
  99. package/dist/{orchestrator-DEQW9j0Z.d.ts → orchestrator-CX-oqwJq.d.ts} +58 -0
  100. package/dist/orchestrator.d.ts +1 -1
  101. package/dist/orchestrator.js +30 -29
  102. package/dist/recall-planner-llm.js +2 -2
  103. package/dist/response-guidance-recall.d.ts +6 -0
  104. package/dist/response-guidance-recall.js +2 -1
  105. package/dist/schemas.d.ts +22 -22
  106. package/dist/search/factory.js +2 -2
  107. package/dist/search/index.js +4 -4
  108. package/dist/semantic-consolidation.js +4 -4
  109. package/dist/semantic-rule-promotion.js +3 -3
  110. package/dist/semantic-rule-verifier.js +3 -3
  111. package/dist/storage.js +2 -2
  112. package/dist/summarizer.js +3 -3
  113. package/dist/targeted-fact-recall.d.ts +6 -0
  114. package/dist/targeted-fact-recall.js +2 -1
  115. package/dist/transfer/backup.js +2 -2
  116. package/dist/transfer/capsule-export.js +2 -2
  117. package/dist/transfer/capsule-import.js +2 -2
  118. package/dist/transfer/import-sqlite.js +2 -2
  119. package/dist/transfer/types.d.ts +12 -12
  120. package/dist/verified-recall.js +3 -3
  121. package/package.json +1 -1
  122. package/src/access-service-lcm-forgery.test.ts +410 -0
  123. package/src/access-service-observe-lcm-parity.test.ts +1397 -0
  124. package/src/access-service-observe-scope.test.ts +599 -0
  125. package/src/access-service-raw-excerpt-read-gate.test.ts +443 -0
  126. package/src/access-service.ts +1270 -113
  127. package/src/coding/coding-namespace.test.ts +44 -0
  128. package/src/coding/coding-namespace.ts +163 -0
  129. package/src/event-order-recall.ts +8 -0
  130. package/src/explicit-cue-recall.ts +70 -29
  131. package/src/focused-list-recall.ts +23 -1
  132. package/src/lcm-fallback-read.ts +113 -0
  133. package/src/orchestrator.ts +331 -26
  134. package/src/response-guidance-recall.ts +21 -1
  135. package/src/targeted-fact-recall.ts +24 -3
  136. package/dist/chunk-5ETA6OAS.js.map +0 -1
  137. package/dist/chunk-6NKAQ74D.js.map +0 -1
  138. package/dist/chunk-DB5A3NHS.js.map +0 -1
  139. package/dist/chunk-OAZ5MFUB.js.map +0 -1
  140. package/dist/chunk-OZKZ2TRP.js.map +0 -1
  141. package/dist/chunk-VWT3F4IV.js.map +0 -1
  142. package/dist/chunk-XUGVP7ZU.js.map +0 -1
  143. /package/dist/{auto-sync-54QQHOG5.js.map → auto-sync-5CJBJMPZ.js.map} +0 -0
  144. /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
  145. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  146. /package/dist/{chunk-QKE4LHNR.js.map → chunk-4HYSMH7D.js.map} +0 -0
  147. /package/dist/{chunk-NMIOW7XG.js.map → chunk-4PTKFBST.js.map} +0 -0
  148. /package/dist/{chunk-DDRNDPX4.js.map → chunk-4SKKVWLQ.js.map} +0 -0
  149. /package/dist/{chunk-WPCCNSWO.js.map → chunk-6UKL6IXM.js.map} +0 -0
  150. /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
  151. /package/dist/{chunk-4BISW7RX.js.map → chunk-AJE7FJVE.js.map} +0 -0
  152. /package/dist/{chunk-ZXWAQFDE.js.map → chunk-CFOCZPIQ.js.map} +0 -0
  153. /package/dist/{chunk-NT5TINK5.js.map → chunk-DHGSZ3UD.js.map} +0 -0
  154. /package/dist/{chunk-OTC2KOZ2.js.map → chunk-EHQLDFSH.js.map} +0 -0
  155. /package/dist/{chunk-AMACWKM4.js.map → chunk-IJHLC5CH.js.map} +0 -0
  156. /package/dist/{chunk-OR7R6M5Z.js.map → chunk-IVYSVAC6.js.map} +0 -0
  157. /package/dist/{chunk-UMKPSD35.js.map → chunk-JF7SFXTG.js.map} +0 -0
  158. /package/dist/{chunk-MCYT2RNT.js.map → chunk-KJDKZVF3.js.map} +0 -0
  159. /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
  160. /package/dist/{chunk-PQFUUXWK.js.map → chunk-KWM33SPU.js.map} +0 -0
  161. /package/dist/{chunk-A3BS64GV.js.map → chunk-LCC5EZTT.js.map} +0 -0
  162. /package/dist/{chunk-ZT6R3WR3.js.map → chunk-LFTLXOFX.js.map} +0 -0
  163. /package/dist/{chunk-CNRZ6WJU.js.map → chunk-MF32AL7N.js.map} +0 -0
  164. /package/dist/{chunk-D6WVJIS3.js.map → chunk-ORGWWNJG.js.map} +0 -0
  165. /package/dist/{chunk-Z3PZRDLW.js.map → chunk-PRQXUSQV.js.map} +0 -0
  166. /package/dist/{chunk-IMWFHBG2.js.map → chunk-QWRC7GIO.js.map} +0 -0
  167. /package/dist/{chunk-FQYFMIKG.js.map → chunk-RKN5J4RO.js.map} +0 -0
  168. /package/dist/{chunk-FUXV6HSO.js.map → chunk-RSS2KWN6.js.map} +0 -0
  169. /package/dist/{chunk-U3GQ33JC.js.map → chunk-SLTKP5WJ.js.map} +0 -0
  170. /package/dist/{chunk-WEPMT6SC.js.map → chunk-V25ZAOSB.js.map} +0 -0
  171. /package/dist/{chunk-UMTG2BN2.js.map → chunk-V4UDXYGG.js.map} +0 -0
  172. /package/dist/{chunk-RRRCNIPK.js.map → chunk-WJK75OCH.js.map} +0 -0
  173. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  174. /package/dist/{chunk-ALUZN7BE.js.map → chunk-XMN6MMTU.js.map} +0 -0
  175. /package/dist/{chunk-A4BTPHIN.js.map → chunk-Y7NWBBHV.js.map} +0 -0
  176. /package/dist/{chunk-M75TBFKQ.js.map → chunk-Z2OXSMZK.js.map} +0 -0
@@ -307,6 +307,7 @@ import {
307
307
  } from "./namespaces/principal.js";
308
308
  import {
309
309
  combineNamespaces,
310
+ lcmReadSessionIdsForNamespaces,
310
311
  resolveCodingNamespaceOverlay,
311
312
  } from "./coding/coding-namespace.js";
312
313
  import type { CodingContext } from "./types.js";
@@ -2011,6 +2012,66 @@ export class Orchestrator {
2011
2012
  return this.applyCodingNamespaceOverlay(sessionKey, base);
2012
2013
  }
2013
2014
 
2015
+ /**
2016
+ * Effective namespace a same-session LCM/structured-history READER must use
2017
+ * to find what the access `observe` surface WROTE (#1495 thread 2).
2018
+ *
2019
+ * This MUST mirror the `observe` scope plan's write-namespace resolution, NOT
2020
+ * `resolveSelfNamespace`: when no coding overlay applies, `observe` archives
2021
+ * under `config.defaultNamespace` (an unqualified observed turn is NOT moved
2022
+ * to the principal self namespace — identical to
2023
+ * `resolveCodingScopedWriteNamespace`/`memory_store`, rule 39). Only when a
2024
+ * coding overlay actually changes the namespace does the writer (and so the
2025
+ * reader) use the overlaid `project-*` namespace. Returning the self base for
2026
+ * the no-overlay case would prefix the read key with a namespace the writer
2027
+ * never used, so the reader would miss its own evidence.
2028
+ *
2029
+ * Honours the access-surface `principalOverride` (#1505 thread 2, codex): when
2030
+ * a recall supplies an authenticated principal NOT encoded in the raw
2031
+ * `sessionKey`, `observe` archived LCM under THAT principal's base namespace.
2032
+ * Deriving the base from `resolvePrincipal(sessionKey)` alone could fall back
2033
+ * to `default`, so principal `alice` observing `sess-1` would write under
2034
+ * `alice` but READ under `default`. Threading the override here keeps the read
2035
+ * base identical to the write base.
2036
+ *
2037
+ * READ-AUTHORIZATION gate (#1505 round 3, codex P2 "Gate LCM recall keys by
2038
+ * readable namespaces"): the overlay LCM read key is a `<principal>-project-*`
2039
+ * sub-namespace of the principal SELF base. The normal recall namespace set
2040
+ * below only substitutes the coding overlay when the principal SELF base is
2041
+ * actually in the readable recall set (`recallNamespacesForPrincipal` — gated
2042
+ * by `defaultRecallNamespaces.includes("self")` AND `canReadNamespace`). If a
2043
+ * principal can WRITE but not READ its self namespace (or `defaultRecall-
2044
+ * Namespaces` omits `self`), QMD/file recall never touches those overlay rows,
2045
+ * so neither may the LCM read key. When the self base is NOT readable, fall
2046
+ * back to the default store — exactly what an unqualified, unauthorized recall
2047
+ * resolves to — rather than injecting overlay rows the rest of recall excludes
2048
+ * (rule 42 read/write parity; rule 48 least-privilege).
2049
+ */
2050
+ private lcmReadNamespaceForSession(
2051
+ sessionKey?: string,
2052
+ principalOverride?: string,
2053
+ ): string {
2054
+ const principal =
2055
+ typeof principalOverride === "string" && principalOverride.length > 0
2056
+ ? principalOverride
2057
+ : this.resolvePrincipal(sessionKey);
2058
+ const base = defaultNamespaceForPrincipal(principal, this.config);
2059
+ const overlaid = this.applyCodingNamespaceOverlay(sessionKey, base);
2060
+ // No overlay → collapse to the default store so the LCM key is the raw
2061
+ // sessionKey, exactly what an unqualified observe archived under.
2062
+ if (overlaid === base) return this.config.defaultNamespace;
2063
+ // Overlay applied. Only honour it when the principal SELF base is in the
2064
+ // readable recall set (same gate the recall namespace set uses to
2065
+ // substitute the overlay). Otherwise the overlay rows are unauthorized for
2066
+ // this reader — fall back to the default store so the LCM read matches
2067
+ // what QMD/file recall would surface.
2068
+ const selfReadableInRecall = recallNamespacesForPrincipal(
2069
+ principal,
2070
+ this.config,
2071
+ ).includes(base);
2072
+ return selfReadableInRecall ? overlaid : this.config.defaultNamespace;
2073
+ }
2074
+
2014
2075
  /**
2015
2076
  * Attach a coding-agent context to a session (issue #569). Called by the
2016
2077
  * Claude Code / Codex / Cursor connectors at session start after
@@ -6890,6 +6951,10 @@ export class Orchestrator {
6890
6951
  .digest("hex")
6891
6952
  .slice(0, 16);
6892
6953
  const sectionBuckets = new Map<string, string[]>();
6954
+ // The effective LCM read session_id SET is computed below from
6955
+ // `recallNamespaces` (the SAME read-authorized namespace set normal QMD/file
6956
+ // recall searches, incl. coding `readFallbacks`). See the
6957
+ // `lcmReadSessionIds` derivation after `recallNamespaces` is built.
6893
6958
  const queryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
6894
6959
  cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
6895
6960
  cronRecallNormalizedQueryMaxChars:
@@ -7119,6 +7184,94 @@ export class Orchestrator {
7119
7184
  } else {
7120
7185
  recallNamespaces = readableRecallNamespaces;
7121
7186
  }
7187
+ // Effective LCM read NAMESPACE SET (#1505 thread "Include coding fallback
7188
+ // namespaces in LCM reads"). `observe` archives LCM / structured history
7189
+ // under `${effectiveNamespace}:${sessionKey}` for whichever namespace was
7190
+ // effective at write time. A branch-scoped session whose evidence was
7191
+ // archived at project / root scope must still surface it, exactly as normal
7192
+ // QMD/file recall does — QMD/file recall searches the primary overlay key AND
7193
+ // `codingOverlay.readFallbacks` (project / root), NOT just the primary
7194
+ // overlay key. The prior single `lcmReadSessionId` only targeted the primary
7195
+ // overlay, so branch-scoped sessions missed fallback LCM evidence.
7196
+ //
7197
+ // READ-AUTHORIZATION (preserved from the prior round's
7198
+ // `lcmReadNamespaceForSession` gate; rule 39 / 42 / 48): the coding-overlay
7199
+ // namespace AND its fallbacks are `<principal>-project-*` sub-namespaces of
7200
+ // the principal SELF base, authorized transitively by that base. They are
7201
+ // included ONLY when the principal self base is in the readable recall set
7202
+ // (`readableRecallNamespaces` — gated by `defaultRecallNamespaces.includes
7203
+ // ("self")` AND `canReadNamespace`). When the self base is NOT readable (e.g.
7204
+ // a write-only / self-omitted principal), the overlay rows are unauthorized
7205
+ // for this reader, so the LCM read collapses to the default store — exactly
7206
+ // what an unqualified, unauthorized recall resolves to — and NEVER searches a
7207
+ // `<principal>-project-*` key (no cross-tenant read leak). This mirrors what
7208
+ // the rest of recall surfaces for such a principal (its readable
7209
+ // shared/policy namespaces have no per-session LCM key, so they contribute
7210
+ // nothing here). `recallNamespaces` itself appends fallbacks unconditionally
7211
+ // for QMD/file recall; the LCM read keys apply the stricter, self-base gate
7212
+ // so the prior round's authorization invariant is preserved.
7213
+ const codingOverlaySelfReadable =
7214
+ codingOverlay !== null &&
7215
+ readableRecallNamespaces.includes(principalSelfNamespace);
7216
+ let lcmReadNamespaces: string[];
7217
+ if (namespaceOverride) {
7218
+ // Explicit namespace already read-authorized above (canReadNamespace gate).
7219
+ lcmReadNamespaces = [namespaceOverride];
7220
+ } else if (codingOverlay && codingSelfNamespace && codingOverlaySelfReadable) {
7221
+ // Self base readable → overlay rows authorized. Read the primary overlay
7222
+ // key first, then each coding read fallback (project → root), combined with
7223
+ // the principal base for isolation — the SAME ordered set QMD/file recall
7224
+ // searches for this authorized coding session.
7225
+ const fallbackNs = codingOverlay.readFallbacks.map((fallback) =>
7226
+ combineNamespaces(principalSelfNamespace, fallback),
7227
+ );
7228
+ lcmReadNamespaces = [codingSelfNamespace, ...fallbackNs];
7229
+ } else {
7230
+ // No overlay, OR overlay present but self base unreadable → collapse to the
7231
+ // default store (raw sessionKey), exactly as the prior round did. No
7232
+ // `<principal>-project-*` overlay key is searched.
7233
+ lcmReadNamespaces = [this.config.defaultNamespace];
7234
+ }
7235
+ // Map the ordered, read-authorized namespace set → ordered, deduped LCM read
7236
+ // session_id set. Single-user / no-overlay recall passes a single-namespace
7237
+ // set that collapses to the raw `sessionKey`, so this is `[sessionKey]` —
7238
+ // byte-for-byte the pre-#1495 single-key behavior.
7239
+ const lcmReadSessionIds = lcmReadSessionIdsForNamespaces(
7240
+ lcmReadNamespaces,
7241
+ sessionKey,
7242
+ this.config.defaultNamespace,
7243
+ );
7244
+ // Query an LCM-backed read across the ordered read key set and return the
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 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).
7264
+ const firstNonEmptyLcmRead = async <T>(
7265
+ read: (lcmSessionId: string | undefined) => Promise<T>,
7266
+ isEmpty: (value: T) => boolean,
7267
+ empty: T,
7268
+ ): Promise<T> => {
7269
+ for (const lcmSessionId of lcmReadSessionIds) {
7270
+ const value = await read(lcmSessionId);
7271
+ if (!isEmpty(value)) return value;
7272
+ }
7273
+ return empty;
7274
+ };
7122
7275
  const qmdAvailable = this.qmd.isAvailable();
7123
7276
  let graphDecisionStatus: IntentDebugSnapshot["graphDecision"]["status"] =
7124
7277
  recallDecision.plannedMode === "graph_mode" ? "skipped" : "not_requested";
@@ -9370,7 +9523,13 @@ export class Orchestrator {
9370
9523
  try {
9371
9524
  const explicitCueSection = await buildExplicitCueRecallSection({
9372
9525
  engine: this.lcmEngine,
9373
- sessionId: sessionKey,
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,
9374
9533
  query: retrievalQuery,
9375
9534
  maxChars: explicitCueMaxChars,
9376
9535
  maxReferences:
@@ -9408,7 +9567,11 @@ export class Orchestrator {
9408
9567
  try {
9409
9568
  const targetedFactSection = await buildTargetedFactRecallSection({
9410
9569
  engine: this.lcmEngine,
9411
- sessionId: sessionKey,
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,
9412
9575
  query: retrievalQuery,
9413
9576
  maxChars: targetedFactMaxChars,
9414
9577
  maxSearchResults:
@@ -9453,7 +9616,12 @@ export class Orchestrator {
9453
9616
  try {
9454
9617
  const focusedListSection = await buildFocusedListRecallSection({
9455
9618
  engine: this.lcmEngine,
9456
- sessionId: sessionKey,
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,
9457
9625
  query: retrievalQuery,
9458
9626
  maxChars: focusedListMaxChars,
9459
9627
  maxSearchResults:
@@ -9502,7 +9670,12 @@ export class Orchestrator {
9502
9670
  try {
9503
9671
  const responseGuidanceSection = await buildResponseGuidanceRecallSection({
9504
9672
  engine: this.lcmEngine,
9505
- sessionId: sessionKey,
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,
9506
9679
  query: retrievalQuery,
9507
9680
  maxChars: responseGuidanceMaxChars,
9508
9681
  maxSearchResults:
@@ -9544,21 +9717,38 @@ export class Orchestrator {
9544
9717
  shouldRecallEventOrderEvidence(retrievalQuery)
9545
9718
  ) {
9546
9719
  try {
9547
- const eventOrderSection = await buildEventOrderRecallSection({
9548
- engine: this.lcmEngine,
9549
- sessionId: sessionKey,
9550
- query: retrievalQuery,
9551
- maxChars: eventOrderMaxChars,
9552
- maxItems:
9553
- this.getRecallSectionNumber("event-order", "maxResults") ??
9554
- this.config.eventOrderRecallMaxResults,
9555
- maxScanWindowTurns:
9556
- this.getRecallSectionNumber("event-order", "maxTurns") ??
9557
- this.config.eventOrderRecallScanWindowTurns,
9558
- maxScanWindowTokens:
9559
- this.getRecallSectionNumber("event-order", "maxTokens") ??
9560
- this.config.eventOrderRecallScanWindowTokens,
9561
- });
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.
9732
+ const eventOrderSection = await firstNonEmptyLcmRead(
9733
+ (lcmSessionId) =>
9734
+ buildEventOrderRecallSection({
9735
+ engine: this.lcmEngine,
9736
+ sessionId: lcmSessionId,
9737
+ query: retrievalQuery,
9738
+ maxChars: eventOrderMaxChars,
9739
+ maxItems:
9740
+ this.getRecallSectionNumber("event-order", "maxResults") ??
9741
+ this.config.eventOrderRecallMaxResults,
9742
+ maxScanWindowTurns:
9743
+ this.getRecallSectionNumber("event-order", "maxTurns") ??
9744
+ this.config.eventOrderRecallScanWindowTurns,
9745
+ maxScanWindowTokens:
9746
+ this.getRecallSectionNumber("event-order", "maxTokens") ??
9747
+ this.config.eventOrderRecallScanWindowTokens,
9748
+ }),
9749
+ (s) => !s,
9750
+ "",
9751
+ );
9562
9752
  if (eventOrderSection) {
9563
9753
  this.appendRecallSection(
9564
9754
  sectionBuckets,
@@ -9732,10 +9922,59 @@ export class Orchestrator {
9732
9922
  (recallMode as RecallPlanMode) !== "no_recall"
9733
9923
  ) {
9734
9924
  try {
9735
- const structuredMatches = await this.lcmEngine.searchStructuredParts(
9736
- sessionKey ?? "default",
9737
- retrievalQuery,
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
+ ),
9738
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
+ );
9739
9978
  const structuredSection = this.lcmEngine.formatStructuredRecall(
9740
9979
  structuredMatches,
9741
9980
  Math.ceil(this.config.recallBudgetChars * 0.08),
@@ -9758,9 +9997,25 @@ export class Orchestrator {
9758
9997
  }
9759
9998
  }
9760
9999
  }
9761
- const lcmSection = await this.lcmEngine.assembleRecall(
9762
- sessionKey ?? "default",
9763
- this.config.recallBudgetChars,
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).
10011
+ const lcmSection = await firstNonEmptyLcmRead(
10012
+ (lcmSessionId) =>
10013
+ this.lcmEngine!.assembleRecall(
10014
+ lcmSessionId ?? "",
10015
+ this.config.recallBudgetChars,
10016
+ ),
10017
+ (s) => !s,
10018
+ "",
9764
10019
  );
9765
10020
  if (lcmSection) {
9766
10021
  this.appendRecallSection(
@@ -11235,6 +11490,28 @@ export class Orchestrator {
11235
11490
  deadlineMs?: number;
11236
11491
  archiveLcm?: boolean;
11237
11492
  abortSignal?: AbortSignal;
11493
+ /**
11494
+ * Pin extraction writes to this namespace instead of deriving one from
11495
+ * `defaultNamespaceForPrincipal(resolvePrincipal(sessionKey))` + the
11496
+ * coding overlay (#1495). The access `observe` surface resolves a single
11497
+ * effective scope plan and passes its `writeNamespace` here so the
11498
+ * extracted memories land in the SAME namespace as LCM archival,
11499
+ * objective-state snapshots, and project-scoped recall — without relying
11500
+ * on re-deriving the namespace from a namespace-prefixed session key.
11501
+ * Same hook bulk-import uses (#460).
11502
+ */
11503
+ writeNamespaceOverride?: string;
11504
+ /**
11505
+ * Pin the provenance PRINCIPAL instead of deriving it from
11506
+ * `resolvePrincipal(turn.sessionKey)` (#1495 thread 1). The access
11507
+ * `observe` surface authenticates the caller at the transport layer and
11508
+ * passes its resolved principal here so extracted-memory provenance uses
11509
+ * the SAME identity the surface authorized — independent of storage
11510
+ * routing (`writeNamespaceOverride`) and of whatever `resolvePrincipal`
11511
+ * would parse from the raw session key. Mirrors the recall path's
11512
+ * `principalOverride` (issue #570 PR 4).
11513
+ */
11514
+ principalOverride?: string;
11238
11515
  } = {},
11239
11516
  ): Promise<void> {
11240
11517
  if (!Array.isArray(turns) || turns.length === 0) return;
@@ -11321,6 +11598,8 @@ export class Orchestrator {
11321
11598
  bufferKey,
11322
11599
  extractionDeadlineMs: options.deadlineMs,
11323
11600
  abortSignal: options.abortSignal,
11601
+ writeNamespaceOverride: options.writeNamespaceOverride,
11602
+ principalOverride: options.principalOverride,
11324
11603
  onTaskSettled: (err) => (err ? reject(err) : resolve()),
11325
11604
  }).catch(reject);
11326
11605
  }),
@@ -11717,6 +11996,12 @@ export class Orchestrator {
11717
11996
  * regardless of user-configured principal routing rules.
11718
11997
  */
11719
11998
  writeNamespaceOverride?: string;
11999
+ /**
12000
+ * Pin the provenance principal (#1495 thread 1). Forwarded to
12001
+ * `runExtraction` so access `observe` can record provenance under the
12002
+ * authenticated principal instead of `resolvePrincipal(sessionKey)`.
12003
+ */
12004
+ principalOverride?: string;
11720
12005
  } = {},
11721
12006
  ): Promise<void> {
11722
12007
  const bufferKey = options.bufferKey ?? turnsToExtract[0]?.sessionKey ?? "default";
@@ -11790,6 +12075,7 @@ export class Orchestrator {
11790
12075
  abortSignal: options.abortSignal,
11791
12076
  failOnExtractionFailure: options.failOnExtractionFailure === true,
11792
12077
  writeNamespaceOverride: options.writeNamespaceOverride,
12078
+ principalOverride: options.principalOverride,
11793
12079
  });
11794
12080
  settleTask(undefined, result);
11795
12081
  } catch (err) {
@@ -11946,6 +12232,14 @@ export class Orchestrator {
11946
12232
  * for provenance; only the storage target is overridden.
11947
12233
  */
11948
12234
  writeNamespaceOverride?: string;
12235
+ /**
12236
+ * Pin the provenance principal instead of deriving it from
12237
+ * `resolvePrincipal(sessionKey)` (#1495 thread 1). When set, this is the
12238
+ * identity an access surface already authenticated; used so observed-turn
12239
+ * provenance is correct even though `turn.sessionKey` is the ORIGINAL
12240
+ * (un-prefixed) key and storage is pinned via `writeNamespaceOverride`.
12241
+ */
12242
+ principalOverride?: string;
11949
12243
  } = {},
11950
12244
  ): Promise<ExtractionRunResult> {
11951
12245
  log.debug(`running extraction on ${turns.length} turns`);
@@ -12039,7 +12333,18 @@ export class Orchestrator {
12039
12333
  };
12040
12334
  }
12041
12335
 
12042
- const principal = resolvePrincipal(sessionKey, this.config);
12336
+ // Provenance principal honours the access-surface override (#1495 thread 1,
12337
+ // mirroring the recall path's `principalOverride`, issue #570 PR 4). Access
12338
+ // surfaces that authenticated the caller at the transport layer pass their
12339
+ // resolved principal so provenance uses the SAME identity the surface
12340
+ // authorized, instead of `resolvePrincipal(sessionKey)` — which on a
12341
+ // namespace-prefixed key would collapse to `default`. The ORIGINAL,
12342
+ // un-prefixed session key still drives threading.
12343
+ const principal =
12344
+ typeof options.principalOverride === "string" &&
12345
+ options.principalOverride.length > 0
12346
+ ? options.principalOverride
12347
+ : resolvePrincipal(sessionKey, this.config);
12043
12348
  // Write path — overlay the coding-agent namespace (issue #569) when the
12044
12349
  // session has a codingContext and `codingMode.projectScope` is true.
12045
12350
  // Explicit `writeNamespaceOverride` from callers still wins, matching
@@ -1,9 +1,19 @@
1
1
  import { buildEvidencePack, type EvidencePackItem } from "./evidence-pack.js";
2
2
  import type { ExplicitCueRecallEngine } from "./explicit-cue-recall.js";
3
+ import {
4
+ gatherAcrossReadSessions,
5
+ resolveLcmReadSessionIds,
6
+ } from "./lcm-fallback-read.js";
3
7
 
4
8
  export interface ResponseGuidanceRecallOptions {
5
9
  engine: ExplicitCueRecallEngine | null | undefined;
6
10
  sessionId?: string;
11
+ /**
12
+ * Ordered, read-authorized LCM read key set (primary overlay → project/root
13
+ * fallbacks). When present, evidence is gathered across EVERY key and merged
14
+ * under this section's budget (#1505 codex P2). Falls back to `sessionId`.
15
+ */
16
+ sessionIds?: readonly (string | undefined)[];
7
17
  query: string;
8
18
  maxChars: number;
9
19
  maxItemChars?: number;
@@ -157,7 +167,17 @@ export async function buildResponseGuidanceRecallSection(
157
167
  ) {
158
168
  return "";
159
169
  }
160
- const items = await collectGuidanceItems(options, intents);
170
+ // #1505 codex P2: gather candidates across the ordered LCM read key set
171
+ // (primary overlay → project/root fallbacks) and UNION them into the existing
172
+ // rank/dedupe/budget pass, so stronger project-fallback guidance is not masked
173
+ // by a weak primary-key hit. `gatherAcrossReadSessions` isolates a per-key read
174
+ // failure so a corrupt/locked fallback index can't discard the primary key's
175
+ // guidance; the single-key path collects exactly once and propagates a failure
176
+ // as before — byte-for-byte the pre-#1505 behavior.
177
+ const items: EvidencePackItem[] = [];
178
+ await gatherAcrossReadSessions(resolveLcmReadSessionIds(options), async (sessionId) => {
179
+ items.push(...(await collectGuidanceItems({ ...options, sessionId }, intents)));
180
+ });
161
181
  const ranked = rankAndDedupeGuidanceItems(items, options.query, intents)
162
182
  .slice(0, maxResults);
163
183
  if (ranked.length === 0) {
@@ -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 TargetedFactRecallOptions {
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;
@@ -43,10 +53,21 @@ export async function buildTargetedFactRecallSection(
43
53
  return "";
44
54
  }
45
55
 
46
- const searchItems = await collectTargetedFactSearchItems(options);
47
- const scannedItems = await collectTargetedFactScanItems(options);
56
+ // #1505 codex P2: gather candidates across the ordered LCM read key set
57
+ // (primary overlay project/root fallbacks) and UNION them into the existing
58
+ // rank/dedupe/budget pass, so stronger project-fallback evidence is not masked
59
+ // by a weak primary-key hit. `gatherAcrossReadSessions` isolates a per-key read
60
+ // failure so a corrupt/locked fallback index can't discard the primary key's
61
+ // evidence; the single-key path collects exactly one search+scan pair and
62
+ // propagates a failure as before — byte-for-byte the pre-#1505 behavior.
63
+ const collected: EvidencePackItem[] = [];
64
+ await gatherAcrossReadSessions(resolveLcmReadSessionIds(options), async (sessionId) => {
65
+ const scoped = { ...options, sessionId };
66
+ collected.push(...(await collectTargetedFactSearchItems(scoped)));
67
+ collected.push(...(await collectTargetedFactScanItems(scoped)));
68
+ });
48
69
  const ranked = rankAndDedupeTargetedFactItems(
49
- [...searchItems, ...scannedItems],
70
+ collected,
50
71
  options.query,
51
72
  ).slice(0, maxResults);
52
73