@remnic/core 9.3.751 → 9.3.752

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 (319) hide show
  1. package/dist/access-boundary.d.ts +5 -5
  2. package/dist/access-boundary.js +29 -29
  3. package/dist/access-cli.js +88 -88
  4. package/dist/access-http.d.ts +5 -5
  5. package/dist/access-http.js +35 -35
  6. package/dist/access-mcp.d.ts +5 -5
  7. package/dist/access-mcp.js +34 -34
  8. package/dist/access-operations-batch.js +31 -31
  9. package/dist/access-operations.d.ts +5 -5
  10. package/dist/access-operations.js +33 -33
  11. package/dist/access-recall-surface.d.ts +363 -0
  12. package/dist/access-recall-surface.js +128 -0
  13. package/dist/access-schema.js +7 -7
  14. package/dist/{access-service-DmAEOJdJ.d.ts → access-service-C2cOHQvg.d.ts} +7 -26
  15. package/dist/access-service.d.ts +5 -5
  16. package/dist/access-service.js +28 -28
  17. package/dist/access-surface-catalog.d.ts +5 -5
  18. package/dist/action-confidence.d.ts +1 -1
  19. package/dist/active-memory-bridge.d.ts +1 -1
  20. package/dist/active-recall.d.ts +1 -1
  21. package/dist/active-recall.js +1 -1
  22. package/dist/behavior-learner.d.ts +1 -1
  23. package/dist/behavior-signals.d.ts +1 -1
  24. package/dist/bootstrap.d.ts +4 -4
  25. package/dist/briefing.d.ts +1 -1
  26. package/dist/briefing.js +4 -4
  27. package/dist/buffer-surprise-report.d.ts +1 -1
  28. package/dist/buffer.d.ts +1 -1
  29. package/dist/buffer.js +2 -2
  30. package/dist/calibration.d.ts +1 -1
  31. package/dist/capabilities.d.ts +1 -1
  32. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-CZJSLEFG.js} +3 -3
  33. package/dist/{catalog-BpOcwHdE.d.ts → catalog-D_FvzTM0.d.ts} +1 -1
  34. package/dist/causal-behavior.d.ts +1 -1
  35. package/dist/causal-behavior.js +2 -2
  36. package/dist/causal-chain.js +2 -2
  37. package/dist/causal-consolidation.d.ts +1 -1
  38. package/dist/causal-consolidation.js +9 -9
  39. package/dist/causal-retrieval.js +2 -2
  40. package/dist/causal-trajectory-graph.d.ts +1 -1
  41. package/dist/causal-trajectory.js +1 -1
  42. package/dist/{chunk-2NLLXCJG.js → chunk-2BFLDH5F.js} +2 -2
  43. package/dist/{chunk-UU32EAIY.js → chunk-2DPCAW2T.js} +5 -5
  44. package/dist/{chunk-5PZCDUJ6.js → chunk-2U4E34Y3.js} +4 -4
  45. package/dist/{chunk-JRZV3767.js → chunk-33NX5BLR.js} +4 -4
  46. package/dist/{chunk-X7Y7WX73.js → chunk-437S3G37.js} +2 -2
  47. package/dist/{chunk-HAZVVOYT.js → chunk-5DXUE7GB.js} +2 -2
  48. package/dist/{chunk-4DLFJJOQ.js → chunk-5JAT4ZJD.js} +4 -4
  49. package/dist/{chunk-RS25QOKZ.js → chunk-64SE4X5G.js} +2 -2
  50. package/dist/{chunk-QRRBXD24.js → chunk-67JRXV6Z.js} +7 -7
  51. package/dist/{chunk-WA7FECSG.js → chunk-6ROCH5JV.js} +7 -7
  52. package/dist/{chunk-5QANTTY2.js → chunk-BIAMJUUJ.js} +4 -4
  53. package/dist/{chunk-IKHKHADN.js → chunk-C4LNKQ3M.js} +5 -5
  54. package/dist/{chunk-B35L45HZ.js → chunk-DC5OBZS3.js} +2 -2
  55. package/dist/{chunk-ORAK3LNZ.js → chunk-ER3CCWK2.js} +2 -2
  56. package/dist/{chunk-SD34EK4Z.js → chunk-FEELYTRT.js} +5 -5
  57. package/dist/{chunk-KQAFEZQX.js → chunk-FLVL4L3L.js} +2 -2
  58. package/dist/{chunk-WIWPSQYU.js → chunk-FXRFFYWD.js} +4 -4
  59. package/dist/{chunk-W3M34XT4.js → chunk-GECEETQ3.js} +4 -4
  60. package/dist/{chunk-WWSXVOGY.js → chunk-GFGYHLON.js} +2 -2
  61. package/dist/{chunk-BJMBJZ2Y.js → chunk-GWTFLF2P.js} +59 -59
  62. package/dist/chunk-GWTFLF2P.js.map +1 -0
  63. package/dist/{chunk-WOX2UW4K.js → chunk-IBPEVGP5.js} +2 -2
  64. package/dist/{chunk-JZ7P26GF.js → chunk-IQWFWSCP.js} +4 -4
  65. package/dist/{chunk-6SXNQFHB.js → chunk-J2JB6BHT.js} +8 -8
  66. package/dist/{chunk-K2JM4DZZ.js → chunk-JHZ3LEM7.js} +2 -2
  67. package/dist/{chunk-YGGXUNS4.js → chunk-JNKSFIAY.js} +45 -45
  68. package/dist/{chunk-CGZWGVQF.js → chunk-K5CWCJKJ.js} +6 -6
  69. package/dist/{chunk-TPLE6SNY.js → chunk-KFNG72W7.js} +7 -7
  70. package/dist/{chunk-4NWIGAIC.js → chunk-KT2VQLP7.js} +8 -8
  71. package/dist/{chunk-6FNZ3NMS.js → chunk-L3VSTJ2V.js} +2 -2
  72. package/dist/{chunk-ARV3AUOM.js → chunk-M5QKGHCR.js} +2 -2
  73. package/dist/{chunk-E4PFDD3N.js → chunk-MEPWWSTD.js} +4 -4
  74. package/dist/{chunk-V5RVMULT.js → chunk-MLDAD27I.js} +7 -7
  75. package/dist/{chunk-K2R3DEEH.js → chunk-MTN2G4DT.js} +2 -2
  76. package/dist/{chunk-DBSMLRTO.js → chunk-MW7UUXTA.js} +5 -5
  77. package/dist/{chunk-OSMBC3JU.js → chunk-OKOF7ITI.js} +6 -6
  78. package/dist/{chunk-DBRVU5PR.js → chunk-OVMLS7TN.js} +3 -3
  79. package/dist/{chunk-NNL4LJAD.js → chunk-QMMCMV2L.js} +5 -5
  80. package/dist/{chunk-RNWWAS5V.js → chunk-QRM3K3XT.js} +5 -5
  81. package/dist/{chunk-HM6UJPPO.js → chunk-R7TGDLV3.js} +4 -4
  82. package/dist/{chunk-EEKDRAWX.js → chunk-RBYV3WSB.js} +4 -4
  83. package/dist/{chunk-LOPZ2DWR.js → chunk-RRRZ3CLN.js} +4 -4
  84. package/dist/{chunk-7TWA7DKP.js → chunk-S5UYWK3O.js} +6 -6
  85. package/dist/{chunk-QOGXUNWL.js → chunk-SC5AEZLU.js} +9 -9
  86. package/dist/{chunk-SNEXVX3U.js → chunk-UFNQKZV7.js} +15 -15
  87. package/dist/{chunk-W3BFT5OJ.js → chunk-V4ZPLQLC.js} +2 -2
  88. package/dist/{chunk-SQNED75R.js → chunk-V5UV7BOA.js} +1 -1
  89. package/dist/{chunk-RWZB6MAY.js → chunk-VQL22SHO.js} +891 -1002
  90. package/dist/chunk-VQL22SHO.js.map +1 -0
  91. package/dist/{chunk-UETBEHBA.js → chunk-VX2RYM2K.js} +2232 -1942
  92. package/dist/chunk-VX2RYM2K.js.map +1 -0
  93. package/dist/{chunk-K5LLP4CY.js → chunk-XKAOT4ZB.js} +4 -4
  94. package/dist/{chunk-ZBVQ4ZM4.js → chunk-YGNQG4Q6.js} +8 -8
  95. package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
  96. package/dist/{chunk-HYI2GP3F.js → chunk-ZGGI3COG.js} +9 -9
  97. package/dist/{cli-lmf5RGyQ.d.ts → cli-DFUU9WYu.d.ts} +3 -3
  98. package/dist/cli.d.ts +6 -6
  99. package/dist/cli.js +60 -60
  100. package/dist/compounding/engine.d.ts +1 -1
  101. package/dist/compounding/engine.js +6 -6
  102. package/dist/compounding/preference-consolidator.d.ts +1 -1
  103. package/dist/compression-optimizer.d.ts +1 -1
  104. package/dist/config.d.ts +1 -1
  105. package/dist/config.js +1 -1
  106. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  107. package/dist/connectors/codex-materialize-runner.js +5 -5
  108. package/dist/connectors/codex-materialize.d.ts +1 -1
  109. package/dist/connectors/index.d.ts +1 -1
  110. package/dist/connectors/index.js +7 -7
  111. package/dist/consolidation-provenance-check.d.ts +1 -1
  112. package/dist/consolidation-undo.d.ts +1 -1
  113. package/dist/contradiction/index.d.ts +1 -1
  114. package/dist/contradiction/index.js +2 -2
  115. package/dist/{contradiction-scan-3FJYWS3G.js → contradiction-scan-KO4WWCS4.js} +3 -3
  116. package/dist/contradiction-scan-KO4WWCS4.js.map +1 -0
  117. package/dist/conversation-index/backend.d.ts +1 -1
  118. package/dist/conversation-index/chunker.d.ts +1 -1
  119. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  120. package/dist/conversation-index/indexer.d.ts +1 -1
  121. package/dist/conversation-index/search.d.ts +1 -1
  122. package/dist/dashboard-runtime.js +2 -2
  123. package/dist/day-summary.d.ts +1 -1
  124. package/dist/day-summary.js +2 -2
  125. package/dist/delinearize.d.ts +1 -1
  126. package/dist/direct-answer-wiring.d.ts +1 -1
  127. package/dist/direct-answer.d.ts +1 -1
  128. package/dist/embedding-fallback.d.ts +1 -1
  129. package/dist/embedding-fallback.js +2 -2
  130. package/dist/enrichment/index.d.ts +1 -1
  131. package/dist/entity-retrieval.d.ts +1 -1
  132. package/dist/entity-retrieval.js +5 -5
  133. package/dist/entity-schema.d.ts +1 -1
  134. package/dist/explicit-capture.d.ts +4 -4
  135. package/dist/explicit-capture.js +2 -2
  136. package/dist/extraction-faithfulness.d.ts +1 -1
  137. package/dist/extraction-judge-telemetry.d.ts +1 -1
  138. package/dist/extraction-judge-training.d.ts +1 -1
  139. package/dist/extraction-judge.d.ts +1 -1
  140. package/dist/extraction.d.ts +1 -1
  141. package/dist/extraction.js +4 -4
  142. package/dist/fallback-llm.d.ts +1 -1
  143. package/dist/graph-dashboard-diff.d.ts +1 -1
  144. package/dist/graph-dashboard-key.d.ts +1 -1
  145. package/dist/graph-dashboard-parser.d.ts +1 -1
  146. package/dist/graph-edge-reinforcement.d.ts +1 -1
  147. package/dist/graph-snapshot.d.ts +1 -1
  148. package/dist/graph.d.ts +1 -1
  149. package/dist/identity-continuity.d.ts +1 -1
  150. package/dist/importance.d.ts +1 -1
  151. package/dist/index.d.ts +9 -9
  152. package/dist/index.js +142 -142
  153. package/dist/intent.d.ts +1 -1
  154. package/dist/lcm/engine.d.ts +1 -1
  155. package/dist/lcm/engine.js +3 -3
  156. package/dist/lcm/index.d.ts +1 -1
  157. package/dist/lcm/index.js +8 -8
  158. package/dist/lcm/tools.d.ts +1 -1
  159. package/dist/lifecycle.d.ts +1 -1
  160. package/dist/live-connectors-runner.d.ts +1 -1
  161. package/dist/local-llm.d.ts +1 -1
  162. package/dist/local-llm.js +2 -2
  163. package/dist/local-model-endpoint.d.ts +1 -1
  164. package/dist/maintenance/memory-governance.d.ts +1 -1
  165. package/dist/maintenance/memory-governance.js +4 -4
  166. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
  167. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  168. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  169. package/dist/memory-action-policy.d.ts +1 -1
  170. package/dist/memory-cache.d.ts +1 -1
  171. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  172. package/dist/memory-projection-store.d.ts +1 -1
  173. package/dist/memory-provenance.d.ts +1 -1
  174. package/dist/memory-worth-outcomes.d.ts +1 -1
  175. package/dist/models-json.d.ts +1 -1
  176. package/dist/namespaces/migrate.d.ts +2 -2
  177. package/dist/namespaces/migrate.js +10 -10
  178. package/dist/namespaces/principal.d.ts +1 -1
  179. package/dist/namespaces/search.d.ts +1 -1
  180. package/dist/namespaces/search.js +4 -4
  181. package/dist/namespaces/storage.d.ts +2 -2
  182. package/dist/namespaces/storage.js +6 -6
  183. package/dist/native-knowledge.d.ts +1 -1
  184. package/dist/operator-toolkit.d.ts +1 -1
  185. package/dist/operator-toolkit.js +17 -17
  186. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  187. package/dist/orchestration/compression-guideline-coordinator.js +2 -2
  188. package/dist/orchestration/maintenance.d.ts +2 -2
  189. package/dist/orchestration/maintenance.js +10 -10
  190. package/dist/{orchestrator-D7bb8ZGp.d.ts → orchestrator-BhxMxtQJ.d.ts} +27 -106
  191. package/dist/orchestrator.d.ts +4 -4
  192. package/dist/orchestrator.js +86 -86
  193. package/dist/patterns-cli.d.ts +1 -1
  194. package/dist/policy-runtime.d.ts +1 -1
  195. package/dist/provenance.d.ts +1 -1
  196. package/dist/qmd-recall-cache.d.ts +1 -1
  197. package/dist/qmd.d.ts +1 -1
  198. package/dist/recall-disclosure-escalation.d.ts +1 -1
  199. package/dist/recall-explain-renderer.d.ts +1 -1
  200. package/dist/recall-planner-llm.d.ts +1 -1
  201. package/dist/recall-state.d.ts +1 -1
  202. package/dist/recall-tag-filter.d.ts +1 -1
  203. package/dist/recall-xray-cli.d.ts +1 -1
  204. package/dist/recall-xray-renderer.d.ts +1 -1
  205. package/dist/recall-xray.d.ts +1 -1
  206. package/dist/resolve-auth-token.d.ts +1 -1
  207. package/dist/resume-bundles.js +1 -1
  208. package/dist/retrieval-agents.d.ts +1 -1
  209. package/dist/retrieval-tiers.d.ts +1 -1
  210. package/dist/routing/engine.d.ts +1 -1
  211. package/dist/routing/store.d.ts +1 -1
  212. package/dist/routing/store.js +2 -2
  213. package/dist/schemas.d.ts +22 -22
  214. package/dist/search/embed-helper.d.ts +1 -1
  215. package/dist/search/embed-helper.js +2 -2
  216. package/dist/search/factory.d.ts +1 -1
  217. package/dist/search/factory.js +3 -3
  218. package/dist/search/index.d.ts +1 -1
  219. package/dist/search/index.js +3 -3
  220. package/dist/search/lancedb-backend.d.ts +1 -1
  221. package/dist/search/meilisearch-backend.d.ts +1 -1
  222. package/dist/search/noop-backend.d.ts +1 -1
  223. package/dist/search/orama-backend.d.ts +1 -1
  224. package/dist/search/port.d.ts +1 -1
  225. package/dist/search/remote-backend.d.ts +1 -1
  226. package/dist/secure-store/index.js +2 -2
  227. package/dist/{semantic-consolidation-CTfbVwCA.d.ts → semantic-consolidation-BrsoFeMP.d.ts} +1 -1
  228. package/dist/semantic-consolidation.d.ts +2 -2
  229. package/dist/semantic-consolidation.js +7 -7
  230. package/dist/semantic-rule-promotion.js +4 -4
  231. package/dist/semantic-rule-verifier.d.ts +1 -1
  232. package/dist/semantic-rule-verifier.js +4 -4
  233. package/dist/session-observer-bands.d.ts +1 -1
  234. package/dist/session-observer-state.d.ts +1 -1
  235. package/dist/shared-context/manager.d.ts +1 -1
  236. package/dist/shared-context/manager.js +2 -2
  237. package/dist/signal.d.ts +1 -1
  238. package/dist/storage.d.ts +1 -1
  239. package/dist/storage.js +3 -3
  240. package/dist/summarizer.d.ts +1 -1
  241. package/dist/summarizer.js +3 -3
  242. package/dist/summary-snapshot.d.ts +1 -1
  243. package/dist/temporal-supersession.d.ts +1 -1
  244. package/dist/temporal-validity.d.ts +1 -1
  245. package/dist/threading.d.ts +1 -1
  246. package/dist/tier-migration.d.ts +1 -1
  247. package/dist/tier-routing.d.ts +1 -1
  248. package/dist/topics.d.ts +1 -1
  249. package/dist/transcript.d.ts +1 -1
  250. package/dist/transfer/backup.js +3 -3
  251. package/dist/transfer/capsule-export.js +3 -3
  252. package/dist/transfer/capsule-import.js +3 -3
  253. package/dist/transfer/types.d.ts +12 -12
  254. package/dist/trust-score-stage.d.ts +1 -1
  255. package/dist/trust-score.d.ts +1 -1
  256. package/dist/{types-D4NYDtXI.d.ts → types-BfI4uJeb.d.ts} +1 -1
  257. package/dist/types.d.ts +1 -1
  258. package/dist/utility-runtime.d.ts +1 -1
  259. package/dist/verified-recall.js +4 -4
  260. package/package.json +2 -2
  261. package/src/access-recall-surface.ts +1552 -0
  262. package/src/access-service.ts +29 -1219
  263. package/src/orchestration/recall-entry.ts +290 -0
  264. package/src/orchestration/session-context.ts +407 -0
  265. package/src/orchestrator.ts +125 -462
  266. package/dist/chunk-BJMBJZ2Y.js.map +0 -1
  267. package/dist/chunk-RWZB6MAY.js.map +0 -1
  268. package/dist/chunk-UETBEHBA.js.map +0 -1
  269. /package/dist/{capsule-crypto-7FJQINUR.js.map → access-recall-surface.js.map} +0 -0
  270. /package/dist/{contradiction-scan-3FJYWS3G.js.map → capsule-crypto-CZJSLEFG.js.map} +0 -0
  271. /package/dist/{chunk-2NLLXCJG.js.map → chunk-2BFLDH5F.js.map} +0 -0
  272. /package/dist/{chunk-UU32EAIY.js.map → chunk-2DPCAW2T.js.map} +0 -0
  273. /package/dist/{chunk-5PZCDUJ6.js.map → chunk-2U4E34Y3.js.map} +0 -0
  274. /package/dist/{chunk-JRZV3767.js.map → chunk-33NX5BLR.js.map} +0 -0
  275. /package/dist/{chunk-X7Y7WX73.js.map → chunk-437S3G37.js.map} +0 -0
  276. /package/dist/{chunk-HAZVVOYT.js.map → chunk-5DXUE7GB.js.map} +0 -0
  277. /package/dist/{chunk-4DLFJJOQ.js.map → chunk-5JAT4ZJD.js.map} +0 -0
  278. /package/dist/{chunk-RS25QOKZ.js.map → chunk-64SE4X5G.js.map} +0 -0
  279. /package/dist/{chunk-QRRBXD24.js.map → chunk-67JRXV6Z.js.map} +0 -0
  280. /package/dist/{chunk-WA7FECSG.js.map → chunk-6ROCH5JV.js.map} +0 -0
  281. /package/dist/{chunk-5QANTTY2.js.map → chunk-BIAMJUUJ.js.map} +0 -0
  282. /package/dist/{chunk-IKHKHADN.js.map → chunk-C4LNKQ3M.js.map} +0 -0
  283. /package/dist/{chunk-B35L45HZ.js.map → chunk-DC5OBZS3.js.map} +0 -0
  284. /package/dist/{chunk-ORAK3LNZ.js.map → chunk-ER3CCWK2.js.map} +0 -0
  285. /package/dist/{chunk-SD34EK4Z.js.map → chunk-FEELYTRT.js.map} +0 -0
  286. /package/dist/{chunk-KQAFEZQX.js.map → chunk-FLVL4L3L.js.map} +0 -0
  287. /package/dist/{chunk-WIWPSQYU.js.map → chunk-FXRFFYWD.js.map} +0 -0
  288. /package/dist/{chunk-W3M34XT4.js.map → chunk-GECEETQ3.js.map} +0 -0
  289. /package/dist/{chunk-WWSXVOGY.js.map → chunk-GFGYHLON.js.map} +0 -0
  290. /package/dist/{chunk-WOX2UW4K.js.map → chunk-IBPEVGP5.js.map} +0 -0
  291. /package/dist/{chunk-JZ7P26GF.js.map → chunk-IQWFWSCP.js.map} +0 -0
  292. /package/dist/{chunk-6SXNQFHB.js.map → chunk-J2JB6BHT.js.map} +0 -0
  293. /package/dist/{chunk-K2JM4DZZ.js.map → chunk-JHZ3LEM7.js.map} +0 -0
  294. /package/dist/{chunk-YGGXUNS4.js.map → chunk-JNKSFIAY.js.map} +0 -0
  295. /package/dist/{chunk-CGZWGVQF.js.map → chunk-K5CWCJKJ.js.map} +0 -0
  296. /package/dist/{chunk-TPLE6SNY.js.map → chunk-KFNG72W7.js.map} +0 -0
  297. /package/dist/{chunk-4NWIGAIC.js.map → chunk-KT2VQLP7.js.map} +0 -0
  298. /package/dist/{chunk-6FNZ3NMS.js.map → chunk-L3VSTJ2V.js.map} +0 -0
  299. /package/dist/{chunk-ARV3AUOM.js.map → chunk-M5QKGHCR.js.map} +0 -0
  300. /package/dist/{chunk-E4PFDD3N.js.map → chunk-MEPWWSTD.js.map} +0 -0
  301. /package/dist/{chunk-V5RVMULT.js.map → chunk-MLDAD27I.js.map} +0 -0
  302. /package/dist/{chunk-K2R3DEEH.js.map → chunk-MTN2G4DT.js.map} +0 -0
  303. /package/dist/{chunk-DBSMLRTO.js.map → chunk-MW7UUXTA.js.map} +0 -0
  304. /package/dist/{chunk-OSMBC3JU.js.map → chunk-OKOF7ITI.js.map} +0 -0
  305. /package/dist/{chunk-DBRVU5PR.js.map → chunk-OVMLS7TN.js.map} +0 -0
  306. /package/dist/{chunk-NNL4LJAD.js.map → chunk-QMMCMV2L.js.map} +0 -0
  307. /package/dist/{chunk-RNWWAS5V.js.map → chunk-QRM3K3XT.js.map} +0 -0
  308. /package/dist/{chunk-HM6UJPPO.js.map → chunk-R7TGDLV3.js.map} +0 -0
  309. /package/dist/{chunk-EEKDRAWX.js.map → chunk-RBYV3WSB.js.map} +0 -0
  310. /package/dist/{chunk-LOPZ2DWR.js.map → chunk-RRRZ3CLN.js.map} +0 -0
  311. /package/dist/{chunk-7TWA7DKP.js.map → chunk-S5UYWK3O.js.map} +0 -0
  312. /package/dist/{chunk-QOGXUNWL.js.map → chunk-SC5AEZLU.js.map} +0 -0
  313. /package/dist/{chunk-SNEXVX3U.js.map → chunk-UFNQKZV7.js.map} +0 -0
  314. /package/dist/{chunk-W3BFT5OJ.js.map → chunk-V4ZPLQLC.js.map} +0 -0
  315. /package/dist/{chunk-SQNED75R.js.map → chunk-V5UV7BOA.js.map} +0 -0
  316. /package/dist/{chunk-K5LLP4CY.js.map → chunk-XKAOT4ZB.js.map} +0 -0
  317. /package/dist/{chunk-ZBVQ4ZM4.js.map → chunk-YGNQG4Q6.js.map} +0 -0
  318. /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
  319. /package/dist/{chunk-HYI2GP3F.js.map → chunk-ZGGI3COG.js.map} +0 -0
@@ -0,0 +1,1552 @@
1
+ /**
2
+ * Access recall read-surface (extracted from access-service.ts; god-file
3
+ * decomposition follow-through of #1526, same playbook: verbatim move +
4
+ * live selfDeps wiring so Object.create(prototype) fakes and instance
5
+ * stubs keep working).
6
+ *
7
+ * Owns the recall read pipeline of the access layer:
8
+ * - executeRecall (budget/audit-gated recall + serialization)
9
+ * - recallXray (X-ray capture surface, #570)
10
+ * - serializeRecallResults / buildRecallResponseFromXraySnapshot
11
+ * - fetchRawExcerpts (raw-excerpt read gate)
12
+ */
13
+
14
+ import { createHash } from "node:crypto";
15
+ import * as nodePath from "node:path";
16
+ import { AccessAuditAdapter, type AccessAuditResult } from "./access-audit.js";
17
+ import { resolveNamespaceCapabilities } from "./capabilities.js";
18
+ import { resolveCodingNamespaceOverlay } from "./coding/coding-namespace.js";
19
+ import { type BudgetDecision, CrossNamespaceBudget } from "./cross-namespace-budget.js";
20
+ import { normalizeProjectionTags } from "./memory-projection-format.js";
21
+ import { namespaceIdentityFromToken } from "./namespaces/identity.js";
22
+ import { canReadNamespace, defaultNamespaceForPrincipal, recallNamespacesForPrincipal, resolvePrincipal } from "./namespaces/principal.js";
23
+ import { expandScopeProfileReadNamespaces, resolveScopeProfilePlan } from "./namespaces/scope-profiles.js";
24
+ import type { Orchestrator, RecallInvocationOptions } from "./orchestrator.js";
25
+ import { decideDisclosureEscalation } from "./recall-disclosure-escalation.js";
26
+ import type { LastRecallSnapshot } from "./recall-state.js";
27
+ import { type TagMatchMode, applyTagFilter, normalizeTags, parseTagMatch } from "./recall-tag-filter.js";
28
+ import { type RecallXraySnapshot, estimateRecallTokens } from "./recall-xray.js";
29
+ import { SecureStoreLockedError } from "./secure-store/index.js";
30
+ import { StorageManager } from "./index.js";
31
+ import { DEFAULT_RECALL_DISCLOSURE, type MemoryFile, type RecallDisclosure, type RecallPlanMode, isRecallDisclosure } from "./types.js";
32
+ import {
33
+ EngramAccessInputError,
34
+ shapeMemorySummary,
35
+ type EngramAccessMemorySummary,
36
+ type EngramAccessRecallRequest,
37
+ type EngramAccessRecallResponse,
38
+ type EngramAccessSetCodingContextRequest,
39
+ } from "./access-service.js";
40
+ // Canonical copies (access-service.ts still carries private duplicates for
41
+ // its remaining callers; dedupe tracked separately).
42
+ import { qmdCollectionPathParts, qmdResultPathCandidates } from "./orchestration/qmd-result-resolver.js";
43
+
44
+ export interface AccessRecallSurfaceDeps {
45
+ readonly auditAdapter: AccessAuditAdapter | null;
46
+ readonly budget: CrossNamespaceBudget;
47
+ buildRecallDebug(
48
+ snapshot: LastRecallSnapshot | null,
49
+ namespace: string,
50
+ includeDebug: boolean,
51
+ sessionKey?: string,
52
+ ): Promise<EngramAccessRecallResponse["debug"] | undefined>;
53
+ buildRecallResponseFromXraySnapshot(options: {
54
+ query: string;
55
+ sessionKey?: string;
56
+ snapshot: RecallXraySnapshot;
57
+ disclosure: RecallDisclosure;
58
+ startedAt: number;
59
+ requestedMode?: RecallPlanMode | "auto";
60
+ normalizedMode?: RecallPlanMode;
61
+ /**
62
+ * Read-authorization-gated namespace for the raw-excerpt LCM lookup (#1505
63
+ * thread 2f7). Threaded through to `serializeRecallResults` so the
64
+ * `includeRecall` x-ray path honours the SAME read gate as normal recall and
65
+ * never attaches overlay transcript rows the gate excludes.
66
+ */
67
+ rawExcerptNamespace?: string;
68
+ /**
69
+ * Ordered, read-authorized LCM read session_id SET (#1505 fallback
70
+ * unification). Threaded through to `serializeRecallResults` so the x-ray raw
71
+ * disclosure path also finds excerpts archived at the coding read fallbacks.
72
+ */
73
+ rawExcerptSessionIds?: string[];
74
+ /**
75
+ * Force NO raw excerpts (#1505 thread NBHWz). Set when the IMPLICIT
76
+ * raw-excerpt read gate found NO readable LCM namespace, so the x-ray
77
+ * includeRecall path degrades to empty excerpts rather than falling back to
78
+ * the write/overlay namespace the read gate excludes.
79
+ */
80
+ rawExcerptsSuppressed?: boolean;
81
+ }): Promise<EngramAccessRecallResponse>;
82
+ fetchRawExcerpts(
83
+ disclosure: RecallDisclosure,
84
+ context: {
85
+ query: string;
86
+ sessionKey?: string;
87
+ namespace?: string;
88
+ /**
89
+ * Pre-resolved, ordered, read-authorized LCM read session_id SET (#1505
90
+ * fallback unification). When supplied, raw disclosure queries each key in
91
+ * order (primary coding overlay → read fallbacks) and merges rows, exactly
92
+ * as the orchestrator recall path and `lcmSearch` do, so a branch-scoped
93
+ * session finds excerpts archived at project/root scope. Already
94
+ * read-gated by `resolveLcmReadSessionIds`, so an unauthorized
95
+ * `<principal>-project-*` key is never present. Falls back to the legacy
96
+ * single `namespace`-prefixed key when absent (sessionless / legacy
97
+ * callers).
98
+ */
99
+ lcmSessionIds?: string[];
100
+ } | null,
101
+ ): Promise<EngramAccessMemorySummary["rawExcerpts"] | null>;
102
+ maybeAttachCodingContext(
103
+ sessionKey: string | undefined,
104
+ options: { cwd?: string; projectTag?: string },
105
+ ): Promise<void>;
106
+ normalizeRecallMode(mode?: RecallPlanMode | "auto"): RecallPlanMode | undefined;
107
+ readonly orchestrator: Orchestrator;
108
+ resolveLcmReadSessionIds(
109
+ explicitNamespace: string | undefined,
110
+ resolvedNamespace: string,
111
+ sessionKey: string,
112
+ authenticatedPrincipal: string | undefined,
113
+ ): string[];
114
+ resolveNamespace(namespace?: string): string;
115
+ resolveRawExcerptReadNamespace(
116
+ explicitNamespace: string | undefined,
117
+ sessionKey: string | undefined,
118
+ authenticatedPrincipal: string | undefined,
119
+ ): string | undefined;
120
+ resolveRecallNamespace(
121
+ namespace: string | undefined,
122
+ sessionKey: string | undefined,
123
+ authenticatedPrincipal?: string,
124
+ ): string | undefined;
125
+ resolveRequestPrincipal(sessionKey: string | undefined, authenticatedPrincipal?: string): string | undefined;
126
+ serializeMemorySummary(
127
+ memory: MemoryFile,
128
+ baseDir: string,
129
+ disclosure?: RecallDisclosure,
130
+ rawExcerpts?: EngramAccessMemorySummary["rawExcerpts"],
131
+ ): EngramAccessMemorySummary;
132
+ serializeRecallResults(
133
+ snapshot: LastRecallSnapshot | null,
134
+ disclosure: RecallDisclosure,
135
+ rawContext?:
136
+ | {
137
+ query: string;
138
+ sessionKey?: string;
139
+ /**
140
+ * Read-authorization-gated namespace for the raw-excerpt LCM lookup
141
+ * (#1505 thread 2f7). When the caller supplies it, the raw lookup uses
142
+ * THIS namespace prefix instead of `snapshot.namespace` (the
143
+ * write/overlay namespace), so raw disclosure honours the SAME read
144
+ * gate as normal recall + `lcmSearch`. Omitted ⇒ falls back to the
145
+ * snapshot namespace (single-store / sessionless callers, unchanged).
146
+ */
147
+ rawExcerptNamespace?: string;
148
+ /**
149
+ * Ordered, read-authorized LCM read session_id SET (#1505 fallback
150
+ * unification). When supplied, raw disclosure queries each key (primary
151
+ * coding overlay → read fallbacks) and merges rows so a branch-scoped
152
+ * session finds excerpts archived at project/root scope. Already
153
+ * read-gated, so no unauthorized overlay key is present. Omitted ⇒ the
154
+ * legacy single `rawExcerptNamespace`-prefixed key (unchanged).
155
+ */
156
+ rawExcerptSessionIds?: string[];
157
+ /**
158
+ * Force NO raw excerpts even when `disclosure === "raw"` (#1505 thread
159
+ * NBHWz). Set by callers when the IMPLICIT raw-excerpt read gate found
160
+ * NO readable LCM namespace (a restrictive `default` READ policy with
161
+ * no readable overlay/self namespace). The lookup must NOT fall back to
162
+ * `snapshot.namespace` (the write/overlay namespace the read gate
163
+ * excludes) — it returns empty excerpts so raw recall degrades
164
+ * gracefully instead of leaking unreadable rows or throwing.
165
+ */
166
+ rawExcerptsSuppressed?: boolean;
167
+ }
168
+ | null,
169
+ ): Promise<EngramAccessMemorySummary[]>;
170
+ setCodingContext(request: EngramAccessSetCodingContextRequest): void;
171
+ storageForAbsoluteRecallPath(
172
+ memoryPath: string,
173
+ primaryNamespace: string,
174
+ recallNamespaces?: readonly string[],
175
+ ): Promise<{ storage: StorageManager; dir: string } | null>;
176
+ xrayQueue: Promise<void>;
177
+ }
178
+
179
+ export class AccessRecallSurface {
180
+ constructor(
181
+ private readonly deps: AccessRecallSurfaceDeps,
182
+ ) {}
183
+
184
+ async buildRecallResponseFromXraySnapshot(options: {
185
+ query: string;
186
+ sessionKey?: string;
187
+ snapshot: RecallXraySnapshot;
188
+ disclosure: RecallDisclosure;
189
+ startedAt: number;
190
+ requestedMode?: RecallPlanMode | "auto";
191
+ normalizedMode?: RecallPlanMode;
192
+ /**
193
+ * Read-authorization-gated namespace for the raw-excerpt LCM lookup (#1505
194
+ * thread 2f7). Threaded through to `serializeRecallResults` so the
195
+ * `includeRecall` x-ray path honours the SAME read gate as normal recall and
196
+ * never attaches overlay transcript rows the gate excludes.
197
+ */
198
+ rawExcerptNamespace?: string;
199
+ /**
200
+ * Ordered, read-authorized LCM read session_id SET (#1505 fallback
201
+ * unification). Threaded through to `serializeRecallResults` so the x-ray raw
202
+ * disclosure path also finds excerpts archived at the coding read fallbacks.
203
+ */
204
+ rawExcerptSessionIds?: string[];
205
+ /**
206
+ * Force NO raw excerpts (#1505 thread NBHWz). Set when the IMPLICIT
207
+ * raw-excerpt read gate found NO readable LCM namespace, so the x-ray
208
+ * includeRecall path degrades to empty excerpts rather than falling back to
209
+ * the write/overlay namespace the read gate excludes.
210
+ */
211
+ rawExcerptsSuppressed?: boolean;
212
+ }): Promise<EngramAccessRecallResponse> {
213
+ const memoryIds = options.snapshot.results.map((result) => result.memoryId);
214
+ const resultPaths = options.snapshot.results.map((result) => result.path);
215
+ const namespace = options.snapshot.namespace
216
+ ? this.deps.resolveNamespace(options.snapshot.namespace)
217
+ : this.deps.orchestrator.config.defaultNamespace;
218
+ const sourcesUsed = Array.from(
219
+ new Set(options.snapshot.results.map((result) => result.servedBy)),
220
+ );
221
+ const snapshotForSerialization: LastRecallSnapshot = {
222
+ sessionKey: options.sessionKey ?? "",
223
+ recordedAt: new Date(options.snapshot.capturedAt).toISOString(),
224
+ queryHash: createHash("sha256").update(options.query).digest("hex"),
225
+ queryLen: options.query.length,
226
+ memoryIds,
227
+ namespace,
228
+ recallNamespaces: [namespace],
229
+ traceId: options.snapshot.traceId,
230
+ plannerMode: options.normalizedMode,
231
+ requestedMode:
232
+ options.requestedMode && options.requestedMode !== "auto"
233
+ ? options.requestedMode
234
+ : undefined,
235
+ sourcesUsed,
236
+ budgetsApplied: {
237
+ appliedTopK: memoryIds.length,
238
+ recallBudgetChars: options.snapshot.budget.chars,
239
+ maxMemoryTokens: this.deps.orchestrator.config.maxMemoryTokens,
240
+ finalContextChars: options.snapshot.budget.used,
241
+ },
242
+ latencyMs: Date.now() - options.startedAt,
243
+ resultPaths,
244
+ };
245
+ const results = await this.deps.serializeRecallResults(
246
+ snapshotForSerialization,
247
+ options.disclosure,
248
+ {
249
+ query: options.query,
250
+ ...(options.sessionKey ? { sessionKey: options.sessionKey } : {}),
251
+ ...(options.rawExcerptNamespace
252
+ ? { rawExcerptNamespace: options.rawExcerptNamespace }
253
+ : {}),
254
+ ...(options.rawExcerptSessionIds !== undefined
255
+ ? { rawExcerptSessionIds: options.rawExcerptSessionIds }
256
+ : {}),
257
+ ...(options.rawExcerptsSuppressed
258
+ ? { rawExcerptsSuppressed: options.rawExcerptsSuppressed }
259
+ : {}),
260
+ },
261
+ );
262
+ const context = results
263
+ .map((result) => {
264
+ const content =
265
+ typeof result.content === "string" && result.content.length > 0
266
+ ? result.content
267
+ : "";
268
+ return content || result.preview;
269
+ })
270
+ .filter((text) => text.length > 0)
271
+ .join("\n\n");
272
+
273
+ return {
274
+ query: options.query,
275
+ ...(options.sessionKey ? { sessionKey: options.sessionKey } : {}),
276
+ namespace,
277
+ context,
278
+ count: memoryIds.length,
279
+ memoryIds,
280
+ results,
281
+ recordedAt: snapshotForSerialization.recordedAt,
282
+ traceId: options.snapshot.traceId,
283
+ plannerMode: options.normalizedMode,
284
+ fallbackUsed: sourcesUsed.some((source) => source !== "hybrid"),
285
+ sourcesUsed,
286
+ disclosure: options.disclosure,
287
+ budgetsApplied: snapshotForSerialization.budgetsApplied,
288
+ latencyMs: snapshotForSerialization.latencyMs,
289
+ };
290
+ }
291
+
292
+ async serializeRecallResults(
293
+ snapshot: LastRecallSnapshot | null,
294
+ disclosure: RecallDisclosure,
295
+ rawContext:
296
+ | {
297
+ query: string;
298
+ sessionKey?: string;
299
+ /**
300
+ * Read-authorization-gated namespace for the raw-excerpt LCM lookup
301
+ * (#1505 thread 2f7). When the caller supplies it, the raw lookup uses
302
+ * THIS namespace prefix instead of `snapshot.namespace` (the
303
+ * write/overlay namespace), so raw disclosure honours the SAME read
304
+ * gate as normal recall + `lcmSearch`. Omitted ⇒ falls back to the
305
+ * snapshot namespace (single-store / sessionless callers, unchanged).
306
+ */
307
+ rawExcerptNamespace?: string;
308
+ /**
309
+ * Ordered, read-authorized LCM read session_id SET (#1505 fallback
310
+ * unification). When supplied, raw disclosure queries each key (primary
311
+ * coding overlay → read fallbacks) and merges rows so a branch-scoped
312
+ * session finds excerpts archived at project/root scope. Already
313
+ * read-gated, so no unauthorized overlay key is present. Omitted ⇒ the
314
+ * legacy single `rawExcerptNamespace`-prefixed key (unchanged).
315
+ */
316
+ rawExcerptSessionIds?: string[];
317
+ /**
318
+ * Force NO raw excerpts even when `disclosure === "raw"` (#1505 thread
319
+ * NBHWz). Set by callers when the IMPLICIT raw-excerpt read gate found
320
+ * NO readable LCM namespace (a restrictive `default` READ policy with
321
+ * no readable overlay/self namespace). The lookup must NOT fall back to
322
+ * `snapshot.namespace` (the write/overlay namespace the read gate
323
+ * excludes) — it returns empty excerpts so raw recall degrades
324
+ * gracefully instead of leaking unreadable rows or throwing.
325
+ */
326
+ rawExcerptsSuppressed?: boolean;
327
+ }
328
+ | null = null,
329
+ ): Promise<EngramAccessMemorySummary[]> {
330
+ if (!snapshot) return [];
331
+ const namespace = snapshot.namespace ? this.deps.resolveNamespace(snapshot.namespace) : this.deps.orchestrator.config.defaultNamespace;
332
+ const storage = await this.deps.orchestrator.getStorage(namespace);
333
+ const storageDir = storage.dir;
334
+ const recallNamespaces = Array.from(
335
+ new Set(
336
+ [
337
+ namespace,
338
+ ...(Array.isArray(snapshot.recallNamespaces)
339
+ ? snapshot.recallNamespaces.map((ns) => this.deps.resolveNamespace(ns))
340
+ : []),
341
+ this.deps.orchestrator.config.defaultNamespace,
342
+ this.deps.orchestrator.config.sharedNamespace,
343
+ ...(this.deps.orchestrator.config.namespacePolicies ?? []).map((p) => p.name),
344
+ ].filter((ns): ns is string => typeof ns === "string" && ns.length > 0),
345
+ ),
346
+ );
347
+ const results: EngramAccessMemorySummary[] = [];
348
+ const seen = new Set<string>();
349
+ const collectionNamespaceFromPrefix = (collectionPrefix: string): string | null => {
350
+ const baseCollection = this.deps.orchestrator.config.qmdCollection;
351
+ if (collectionPrefix === baseCollection) return this.deps.orchestrator.config.defaultNamespace;
352
+ const namespaceSuffix = collectionPrefix.startsWith(`${baseCollection}--`)
353
+ ? collectionPrefix.slice(baseCollection.length + 2)
354
+ : "";
355
+ if (!namespaceSuffix) return null;
356
+
357
+ const decoded = namespaceIdentityFromToken(namespaceSuffix);
358
+ if (decoded !== null) return decoded || this.deps.orchestrator.config.defaultNamespace;
359
+ if (namespaceSuffix.startsWith("ns--")) {
360
+ const legacyNamespace = namespaceSuffix.slice("ns--".length).trim();
361
+ return legacyNamespace || null;
362
+ }
363
+ return null;
364
+ };
365
+ const readResultPath = async (
366
+ memoryPath: string,
367
+ ): Promise<{ memory: MemoryFile; baseDir: string } | null> => {
368
+ const parts = qmdCollectionPathParts(memoryPath);
369
+ const coldCollection =
370
+ this.deps.orchestrator.config.qmdColdCollection ?? "openclaw-engram-cold";
371
+ if (parts && parts.collection === coldCollection) {
372
+ const storages: Array<{ storage: StorageManager; dir: string }> = [];
373
+ const seenStorageDirs = new Set<string>();
374
+ const addStorage = (candidateStorage: StorageManager): void => {
375
+ const candidateDir = nodePath.resolve(candidateStorage.dir);
376
+ if (seenStorageDirs.has(candidateDir)) return;
377
+ seenStorageDirs.add(candidateDir);
378
+ storages.push({ storage: candidateStorage, dir: candidateDir });
379
+ };
380
+ addStorage(storage);
381
+ for (const recallNamespace of recallNamespaces) {
382
+ try {
383
+ addStorage(await this.deps.orchestrator.getStorage(recallNamespace));
384
+ } catch {
385
+ continue;
386
+ }
387
+ }
388
+ for (const candidateStorage of storages) {
389
+ try {
390
+ const coldRoot = nodePath.join(candidateStorage.dir, "cold");
391
+ for (const candidatePath of qmdResultPathCandidates(
392
+ coldRoot,
393
+ parts.relativePath,
394
+ )) {
395
+ const memory =
396
+ await candidateStorage.storage.readMemoryByPath(candidatePath);
397
+ if (memory) return { memory, baseDir: candidateStorage.dir };
398
+ }
399
+ } catch (err) {
400
+ if (err instanceof SecureStoreLockedError) throw err;
401
+ continue;
402
+ }
403
+ }
404
+ return null;
405
+ }
406
+
407
+ const collectionNamespace = parts
408
+ ? collectionNamespaceFromPrefix(parts.collection)
409
+ : null;
410
+
411
+ if (parts && collectionNamespace) {
412
+ try {
413
+ const collectionStorage =
414
+ await this.deps.orchestrator.getStorage(collectionNamespace);
415
+ for (const candidate of qmdResultPathCandidates(
416
+ collectionStorage.dir,
417
+ parts.relativePath,
418
+ )) {
419
+ const memory = await collectionStorage.readMemoryByPath(candidate);
420
+ if (memory) return { memory, baseDir: collectionStorage.dir };
421
+ }
422
+ return null;
423
+ } catch (err) {
424
+ if (err instanceof SecureStoreLockedError) throw err;
425
+ return null;
426
+ }
427
+ }
428
+
429
+ if (nodePath.isAbsolute(memoryPath)) {
430
+ const ownerStorage = await this.deps.storageForAbsoluteRecallPath(
431
+ memoryPath,
432
+ namespace,
433
+ recallNamespaces,
434
+ );
435
+ if (!ownerStorage) return null;
436
+ for (const candidate of qmdResultPathCandidates(
437
+ ownerStorage.dir,
438
+ memoryPath,
439
+ )) {
440
+ const memory = await ownerStorage.storage.readMemoryByPath(candidate);
441
+ if (memory) return { memory, baseDir: ownerStorage.dir };
442
+ }
443
+ return null;
444
+ }
445
+
446
+ for (const candidate of qmdResultPathCandidates(storageDir, memoryPath)) {
447
+ const memory = await storage.readMemoryByPath(candidate);
448
+ if (memory) return { memory, baseDir: storageDir };
449
+ }
450
+ return null;
451
+ };
452
+
453
+ // Pre-fetch raw excerpts once when `disclosure === "raw"` so we don't
454
+ // hit the LCM archive per-result (issue #677 PR 2/4). Excerpts are
455
+ // attached to the first result; per-result attribution is reserved
456
+ // for a future PR if/when the LCM index can be joined to memory ids.
457
+ // Coerce `null` (non-raw disclosure) to `undefined` so the optional
458
+ // serializer field is never explicitly `null`.
459
+ // Namespace for the LCM `${namespace}:${sessionKey}` prefix: prefer the
460
+ // caller-supplied READ-AUTHORIZATION-GATED `rawExcerptNamespace` (#1505
461
+ // thread 2f7) so raw disclosure honours the same read gate as normal recall
462
+ // + `lcmSearch` and never attaches `<principal>-project-*` overlay rows the
463
+ // gate excludes. Fall back to the snapshot's resolved namespace only when no
464
+ // gated namespace was threaded (sessionless / legacy callers) — unchanged.
465
+ const rawExcerptsResult =
466
+ rawContext?.rawExcerptsSuppressed === true
467
+ ? // Implicit raw recall with NO readable LCM namespace (#1505 thread
468
+ // NBHWz): emit empty excerpts rather than falling back to the
469
+ // write/overlay `namespace` the read gate excludes.
470
+ []
471
+ : await this.deps.fetchRawExcerpts(
472
+ disclosure,
473
+ rawContext
474
+ ? {
475
+ query: rawContext.query,
476
+ ...(rawContext.sessionKey
477
+ ? { sessionKey: rawContext.sessionKey }
478
+ : {}),
479
+ namespace: rawContext.rawExcerptNamespace ?? namespace,
480
+ ...(rawContext.rawExcerptSessionIds !== undefined
481
+ ? { lcmSessionIds: rawContext.rawExcerptSessionIds }
482
+ : {}),
483
+ }
484
+ : null,
485
+ );
486
+ const rawExcerpts = rawExcerptsResult ?? undefined;
487
+
488
+ for (const memoryPath of snapshot.resultPaths ?? []) {
489
+ if (!memoryPath || seen.has(memoryPath)) continue;
490
+ const resolved = await readResultPath(memoryPath);
491
+ if (!resolved) continue;
492
+ const { memory, baseDir } = resolved;
493
+ seen.add(memoryPath);
494
+ results.push(
495
+ this.deps.serializeMemorySummary(
496
+ memory,
497
+ baseDir,
498
+ disclosure,
499
+ // Attach the (possibly empty) raw excerpts to the first raw
500
+ // result; subsequent results do not duplicate the array.
501
+ results.length === 0 ? rawExcerpts : undefined,
502
+ ),
503
+ );
504
+ }
505
+
506
+ if (results.length > 0) return results;
507
+
508
+ for (const memoryId of snapshot.memoryIds) {
509
+ const memory = await storage.getMemoryById(memoryId);
510
+ if (!memory || seen.has(memory.path)) continue;
511
+ seen.add(memory.path);
512
+ results.push(
513
+ this.deps.serializeMemorySummary(
514
+ memory,
515
+ storageDir,
516
+ disclosure,
517
+ results.length === 0 ? rawExcerpts : undefined,
518
+ ),
519
+ );
520
+ }
521
+ return results;
522
+ }
523
+
524
+ /**
525
+ * Fetch raw transcript excerpts from the LCM archive for `disclosure ===
526
+ * "raw"` recalls (issue #677 PR 2/4). Returns `null` for non-raw recall
527
+ * depths, an empty array when LCM is disabled / not initialized / has no
528
+ * matches, and an array of LCM-side excerpts otherwise. Errors are
529
+ * swallowed and treated as "no excerpts" so a failing LCM never breaks
530
+ * the recall response.
531
+ *
532
+ * Namespace handling: LCM archival prefixes non-default-namespace
533
+ * sessions with `${namespace}:${sessionKey}` (see `observe()` around
534
+ * line 2498), so the lookup must mirror that prefix or raw recalls in
535
+ * non-default namespaces miss their own excerpts.
536
+ */
537
+ async fetchRawExcerpts(
538
+ disclosure: RecallDisclosure,
539
+ context: {
540
+ query: string;
541
+ sessionKey?: string;
542
+ namespace?: string;
543
+ /**
544
+ * Pre-resolved, ordered, read-authorized LCM read session_id SET (#1505
545
+ * fallback unification). When supplied, raw disclosure queries each key in
546
+ * order (primary coding overlay → read fallbacks) and merges rows, exactly
547
+ * as the orchestrator recall path and `lcmSearch` do, so a branch-scoped
548
+ * session finds excerpts archived at project/root scope. Already
549
+ * read-gated by `resolveLcmReadSessionIds`, so an unauthorized
550
+ * `<principal>-project-*` key is never present. Falls back to the legacy
551
+ * single `namespace`-prefixed key when absent (sessionless / legacy
552
+ * callers).
553
+ */
554
+ lcmSessionIds?: string[];
555
+ } | null,
556
+ ): Promise<EngramAccessMemorySummary["rawExcerpts"] | null> {
557
+ if (disclosure !== "raw") return null;
558
+ if (!context || !context.query) return [];
559
+ // Privacy guard: raw disclosure must be session-scoped. Without a
560
+ // sessionKey, `lcm.searchContextFull(query, n, undefined)` searches
561
+ // across every archived session in the LCM store and would return
562
+ // excerpts from unrelated sessions (potentially crossing namespaces
563
+ // via their `${namespace}:${sessionKey}` prefix encoding). Treat a
564
+ // missing sessionKey as "no excerpts" — callers asking for raw
565
+ // disclosure outside a session get an empty list, not a leak.
566
+ if (!context.sessionKey) return [];
567
+ const lcm = this.deps.orchestrator.lcmEngine;
568
+ if (!lcm || !lcm.enabled) return [];
569
+ try {
570
+ const legacyKey =
571
+ context.namespace &&
572
+ context.namespace !== this.deps.orchestrator.config.defaultNamespace
573
+ ? `${context.namespace}:${context.sessionKey}`
574
+ : context.sessionKey;
575
+ const lcmSessionIds =
576
+ context.lcmSessionIds !== undefined
577
+ ? context.lcmSessionIds
578
+ : [legacyKey];
579
+ // Cap the excerpt fanout so recall responses stay bounded. Five matches
580
+ // is enough to anchor the model in the raw transcript without ballooning
581
+ // token spend; raw is meant as the escape hatch, not the default. The cap
582
+ // is applied across the MERGED result set so adding fallback keys never
583
+ // inflates the excerpt budget.
584
+ const limit = 5;
585
+ const seenRows = new Set<string>();
586
+ const excerpts: NonNullable<EngramAccessMemorySummary["rawExcerpts"]> = [];
587
+ const settledRows = await Promise.allSettled(
588
+ lcmSessionIds.map(async (lcmSessionKey) =>
589
+ lcm.searchContextFull(context.query, limit, lcmSessionKey),
590
+ ),
591
+ );
592
+ for (const result of settledRows) {
593
+ if (excerpts.length >= limit) break;
594
+ if (result.status !== "fulfilled") continue;
595
+ for (const r of result.value) {
596
+ const dedupeKey = `${r.session_id} ${r.turn_index}`;
597
+ if (seenRows.has(dedupeKey)) continue;
598
+ seenRows.add(dedupeKey);
599
+ excerpts.push({
600
+ turnIndex: r.turn_index,
601
+ role: r.role,
602
+ content: r.content,
603
+ sessionId: r.session_id,
604
+ });
605
+ if (excerpts.length >= limit) break;
606
+ }
607
+ }
608
+ return excerpts;
609
+ } catch {
610
+ // CLAUDE.md rule 13: never let an external subsystem (LCM/SQLite)
611
+ // crash the primary recall flow.
612
+ return [];
613
+ }
614
+ }
615
+
616
+ async executeRecall(
617
+ request: EngramAccessRecallRequest,
618
+ ): Promise<{
619
+ response: EngramAccessRecallResponse;
620
+ budgetRecordPrincipal: string | null;
621
+ }> {
622
+ const query = request.query;
623
+ // Disclosure depth (issue #677). Default to `"chunk"` when omitted so
624
+ // pre-#677 callers see unchanged behavior. Reject explicitly invalid
625
+ // string values per CLAUDE.md rule 51 (do not silently fall back).
626
+ const callerProvidedDisclosure =
627
+ request.disclosure !== undefined && request.disclosure !== null;
628
+ const requestedDisclosure: RecallDisclosure = (() => {
629
+ if (!callerProvidedDisclosure) {
630
+ return DEFAULT_RECALL_DISCLOSURE;
631
+ }
632
+ if (!isRecallDisclosure(request.disclosure)) {
633
+ throw new EngramAccessInputError(
634
+ `disclosure must be one of: chunk, section, raw (got: ${String(request.disclosure)})`,
635
+ );
636
+ }
637
+ return request.disclosure;
638
+ })();
639
+ // Attach any coding context shipped with the recall request BEFORE
640
+ // namespace resolution so the overlay applies to this recall (issue #569).
641
+ if (request.codingContext !== undefined && request.sessionKey) {
642
+ this.deps.setCodingContext({
643
+ sessionKey: request.sessionKey,
644
+ codingContext: request.codingContext,
645
+ });
646
+ }
647
+ // Auto-resolve coding context from cwd/projectTag when no explicit
648
+ // codingContext was supplied (issue #569 wiring). This allows Claude
649
+ // Code hooks and OpenClaw connectors to get project-scoped memory
650
+ // transparently.
651
+ if (request.codingContext === undefined && request.sessionKey) {
652
+ await this.deps.maybeAttachCodingContext(request.sessionKey, {
653
+ cwd: request.cwd,
654
+ projectTag: request.projectTag,
655
+ });
656
+ }
657
+ const authenticatedPrincipal = request.authenticatedPrincipal?.trim();
658
+ const namespaceOverride = this.deps.resolveRecallNamespace(
659
+ request.namespace,
660
+ request.sessionKey,
661
+ authenticatedPrincipal,
662
+ );
663
+ const namespace = namespaceOverride ?? this.deps.orchestrator.config.defaultNamespace;
664
+ // Normalize mode early so that no_recall / invalid modes skip budget
665
+ // accounting (Codex P1: budget recorded before mode validation).
666
+ const mode = this.deps.normalizeRecallMode(request.mode);
667
+ const maybePrincipal = this.deps.resolveRequestPrincipal(request.sessionKey, authenticatedPrincipal);
668
+ if (resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces && !maybePrincipal) {
669
+ throw new EngramAccessInputError(
670
+ "authentication required: namespaces are enabled and no principal was supplied",
671
+ );
672
+ }
673
+ const principal = maybePrincipal ?? "default";
674
+ const principalNamespace = defaultNamespaceForPrincipal(principal, this.deps.orchestrator.config);
675
+ const profileCodingContext =
676
+ request.sessionKey && typeof this.deps.orchestrator.getCodingContextForSession === "function"
677
+ ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey)
678
+ : null;
679
+ const profileCodingOverlay =
680
+ !namespaceOverride &&
681
+ profileCodingContext &&
682
+ resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces &&
683
+ this.deps.orchestrator.config.codingMode?.projectScope
684
+ ? resolveCodingNamespaceOverlay(
685
+ profileCodingContext,
686
+ this.deps.orchestrator.config.codingMode,
687
+ this.deps.orchestrator.config.defaultNamespace,
688
+ )
689
+ : null;
690
+ const profilePlan = namespaceOverride
691
+ ? null
692
+ : resolveScopeProfilePlan({
693
+ config: this.deps.orchestrator.config,
694
+ principal,
695
+ codingContext: profileCodingContext,
696
+ codingOverlay: profileCodingOverlay,
697
+ });
698
+ // Skip budget checks for modes that never perform a cross-namespace read.
699
+ const modeSkipsBudget = mode === "no_recall";
700
+ // Derive the full set of namespaces the orchestrator will actually search.
701
+ // When no explicit override is provided, `recallNamespacesForPrincipal()` may
702
+ // expand to shared / policy-default namespaces. Budget must be checked
703
+ // against every cross-namespace entry in the effective set so that omitting
704
+ // `namespace` cannot bypass the limiter (Cursor/Codex review feedback).
705
+ //
706
+ const legacyRecallNamespaces = Array.isArray(this.deps.orchestrator.config.defaultRecallNamespaces)
707
+ ? recallNamespacesForPrincipal(principal, this.deps.orchestrator.config)
708
+ : [];
709
+ const effectiveNamespaces = namespaceOverride
710
+ ? [namespaceOverride]
711
+ : profilePlan
712
+ ? expandScopeProfileReadNamespaces({
713
+ profilePlan,
714
+ principalSelfNamespace: profilePlan.baseNamespace,
715
+ config: this.deps.orchestrator.config,
716
+ principal,
717
+ codingOverlay: profileCodingOverlay,
718
+ legacyRecallNamespaces,
719
+ })
720
+ : legacyRecallNamespaces;
721
+ const budgetPrincipalNamespace = profilePlan?.baseNamespace ?? principalNamespace;
722
+ let budgetDecision: BudgetDecision;
723
+ let recordBudgetAfterSuccess = false;
724
+ if (modeSkipsBudget) {
725
+ budgetDecision = {
726
+ allowed: true as const,
727
+ reason: "allowed-same-namespace" as const,
728
+ count: 0,
729
+ limit: {
730
+ softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
731
+ hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
732
+ windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 60_000,
733
+ },
734
+ };
735
+ } else {
736
+ // Peek at every effective namespace to determine whether ANY would be
737
+ // cross-namespace WITHOUT recording side effects (Cursor review:
738
+ // multi-count bug). Record a single budget event only when at least
739
+ // one effective namespace differs from the principal's self namespace,
740
+ // and only after recall succeeds so retried transient failures do not
741
+ // consume budget multiple times before a successful response can be
742
+ // cached behind the request idempotency key.
743
+ let anyCrossNamespace = false;
744
+ let denied: BudgetDecision | null = null;
745
+ let crossNamespaceDecision: BudgetDecision | null = null;
746
+ for (const ns of effectiveNamespaces) {
747
+ const peek = this.deps.budget.peek({
748
+ principal,
749
+ principalNamespace: budgetPrincipalNamespace,
750
+ queryNamespace: ns,
751
+ });
752
+ if (peek.reason !== "allowed-same-namespace") {
753
+ anyCrossNamespace = true;
754
+ crossNamespaceDecision ??= peek;
755
+ }
756
+ if (!peek.allowed) {
757
+ denied = peek;
758
+ break;
759
+ }
760
+ }
761
+ if (denied) {
762
+ // The peek projected a denial — deny without recording so the
763
+ // bucket is not inflated by rejected attempts.
764
+ budgetDecision = denied;
765
+ } else if (anyCrossNamespace) {
766
+ budgetDecision = crossNamespaceDecision ?? {
767
+ allowed: true as const,
768
+ reason: "allowed-under-soft" as const,
769
+ count: 0,
770
+ limit: {
771
+ softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
772
+ hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
773
+ windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 60_000,
774
+ },
775
+ };
776
+ recordBudgetAfterSuccess = true;
777
+ } else {
778
+ budgetDecision = {
779
+ allowed: true as const,
780
+ reason: "allowed-same-namespace" as const,
781
+ count: 0,
782
+ limit: {
783
+ softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
784
+ hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
785
+ windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 60_000,
786
+ },
787
+ };
788
+ }
789
+ if (!budgetDecision.allowed) {
790
+ throw new EngramAccessInputError(
791
+ `recall denied: cross-namespace budget exceeded (${budgetDecision.count}/${budgetDecision.limit.hardLimit} in ${budgetDecision.limit.windowMs}ms window)`,
792
+ );
793
+ }
794
+ // Prune expired principal buckets to prevent unbounded Map growth from
795
+ // high-cardinality / transient principals (Codex P2 review feedback).
796
+ this.deps.budget.gc();
797
+ }
798
+ const topK = Number.isFinite(request.topK) ? Math.max(0, Math.floor(request.topK ?? 0)) : undefined;
799
+ // Issue #680 — historical recall pin. Validate at the input
800
+ // boundary so a malformed `asOf` is rejected with a structured
801
+ // 400 instead of silently flooring at NaN inside the orchestrator
802
+ // (CLAUDE.md rule 51, gotcha #51). Empty / undefined is fine —
803
+ // means "no pin".
804
+ let asOf: string | undefined;
805
+ if (request.asOf !== undefined && request.asOf !== null) {
806
+ if (typeof request.asOf !== "string" || request.asOf.trim().length === 0) {
807
+ throw new EngramAccessInputError(
808
+ "asOf must be a non-empty ISO 8601 timestamp string",
809
+ );
810
+ }
811
+ const parsed = Date.parse(request.asOf);
812
+ if (!Number.isFinite(parsed)) {
813
+ throw new EngramAccessInputError(
814
+ `asOf must be a parseable ISO 8601 timestamp (got: "${request.asOf}")`,
815
+ );
816
+ }
817
+ asOf = request.asOf;
818
+ }
819
+ const recallOptions: RecallInvocationOptions = {
820
+ namespace: namespaceOverride,
821
+ topK,
822
+ mode,
823
+ ...(authenticatedPrincipal ? { principalOverride: authenticatedPrincipal } : {}),
824
+ ...(asOf !== undefined ? { asOf } : {}),
825
+ ...(request.includeLowConfidence === true ? { includeLowConfidence: true } : {}),
826
+ };
827
+ const startedAt = Date.now();
828
+ const context = await this.deps.orchestrator.recall(query, request.sessionKey, recallOptions);
829
+ const snapshot = request.sessionKey
830
+ ? this.deps.orchestrator.lastRecall.get(request.sessionKey)
831
+ : null;
832
+ const effectiveNamespace = snapshot?.namespace
833
+ ? this.deps.resolveNamespace(snapshot.namespace)
834
+ : namespace;
835
+ // Auto-escalation policy (issue #677 PR 4/4). When the operator
836
+ // configured `recallDisclosureEscalation: "auto"` AND the caller
837
+ // did not explicitly choose a disclosure level AND recall produced
838
+ // a low-confidence result set (proxied by fill ratio: results
839
+ // returned / topK requested), we escalate the default `chunk`
840
+ // shape to `section` so the LLM gets richer context to compensate
841
+ // for ambiguous retrieval. Manual mode and explicit caller
842
+ // disclosure both bypass the policy. Documented in
843
+ // `recall-disclosure-escalation.ts` and unit-tested there.
844
+ // Confidence-proxy denominator: priority order is
845
+ // 1. `snapshot.budgetsApplied.appliedTopK` (ALWAYS wins) — this is
846
+ // the limit the orchestrator actually applied after planner /
847
+ // minimal-mode / section-cap narrowing. Codex P1 rounds 2+3
848
+ // on #705 emphasize that even a caller's explicit `request.topK`
849
+ // is wrong when the orchestrator caps below it (e.g. topK=50
850
+ // but appliedTopK=3 makes a 2-hit recall actually 0.67, not
851
+ // 0.04).
852
+ // 2. The caller's explicit `topK` when the snapshot lacks
853
+ // `budgetsApplied` (early-return paths, error cases).
854
+ // 3. Config `qmdMaxResults` as a last-resort fallback.
855
+ // Floor at observed-results so the ratio stays in [0, 1] even if
856
+ // any of the signals drifts below the actual hit count.
857
+ const resultsReturned = snapshot?.memoryIds?.length ?? 0;
858
+ const appliedTopK = snapshot?.budgetsApplied?.appliedTopK;
859
+ const configMaxResults =
860
+ typeof this.deps.orchestrator.config.qmdMaxResults === "number" &&
861
+ Number.isFinite(this.deps.orchestrator.config.qmdMaxResults) &&
862
+ this.deps.orchestrator.config.qmdMaxResults > 0
863
+ ? this.deps.orchestrator.config.qmdMaxResults
864
+ : 0;
865
+ const topKDenominator =
866
+ typeof appliedTopK === "number" &&
867
+ Number.isFinite(appliedTopK) &&
868
+ appliedTopK > 0
869
+ ? Math.max(appliedTopK, resultsReturned)
870
+ : typeof topK === "number" && topK > 0
871
+ ? Math.max(topK, resultsReturned)
872
+ : Math.max(configMaxResults, resultsReturned, 1);
873
+ // When the recall produced no snapshot (sessionless / namespace
874
+ // mismatch / early-return path), there is no confidence signal to
875
+ // base escalation on. Pass `undefined` so the helper takes its
876
+ // `no-top-k-confidence` branch instead of computing 0/N=0 and
877
+ // forcing auto-escalation on every sessionless caller (Codex P2
878
+ // review on PR #705).
879
+ const topKConfidence =
880
+ snapshot && topKDenominator > 0
881
+ ? Math.min(1, resultsReturned / topKDenominator)
882
+ : undefined;
883
+ const escalationDecision = decideDisclosureEscalation({
884
+ mode: this.deps.orchestrator.config.recallDisclosureEscalation,
885
+ threshold: this.deps.orchestrator.config.recallDisclosureEscalationThreshold,
886
+ originalDisclosure: requestedDisclosure,
887
+ callerProvidedDisclosure,
888
+ topKConfidence,
889
+ });
890
+ const disclosure = escalationDecision.effective;
891
+ // Gate the raw-excerpt LCM read with the SAME read-authorization namespace
892
+ // `lcmSearch` + the in-prompt LCM sections use (#1505 thread 2f7), so
893
+ // `disclosure: "raw"` never attaches `<principal>-project-*` overlay rows
894
+ // when the principal can WRITE but not READ its self base (or
895
+ // `defaultRecallNamespaces` omits `self`). Computed ONLY for raw disclosure:
896
+ // it is the sole consumer, and resolving the overlay on every chunk/section
897
+ // recall would be wasted work — keeping non-raw recall byte-for-byte
898
+ // unchanged.
899
+ // Trim the sessionKey to match what `orchestrator.recall(...)` already does
900
+ // (`request.sessionKey?.trim() || undefined`) and what the x-ray raw-excerpt
901
+ // path uses (cursor "Raw excerpt key not trimmed"). A whitespace-padded key
902
+ // otherwise drives recall under one identity but resolves the raw-excerpt
903
+ // overlay namespace + LCM `session_id` under a DIFFERENT (untrimmed) prefix,
904
+ // so excerpts are gated/queried inconsistently with recall and the x-ray path.
905
+ const trimmedSessionKey = request.sessionKey?.trim() || undefined;
906
+ const rawExcerptNamespace =
907
+ disclosure === "raw"
908
+ ? this.deps.resolveRawExcerptReadNamespace(
909
+ request.namespace,
910
+ trimmedSessionKey,
911
+ authenticatedPrincipal,
912
+ )
913
+ : undefined;
914
+ // `undefined` for an IMPLICIT raw recall means NO readable LCM namespace
915
+ // exists (restrictive `default` READ policy, no readable overlay/self) —
916
+ // suppress excerpts rather than fall back to the write/overlay namespace the
917
+ // read gate excludes (#1505 thread NBHWz). An EXPLICIT namespace always
918
+ // resolves (or throws) above, so suppression only applies to the implicit
919
+ // path.
920
+ const hasExplicitNamespace =
921
+ typeof request.namespace === "string" &&
922
+ request.namespace.trim().length > 0;
923
+ const rawExcerptsSuppressed =
924
+ disclosure === "raw" &&
925
+ !hasExplicitNamespace &&
926
+ rawExcerptNamespace === undefined;
927
+ // Ordered, read-authorized LCM read key SET (#1505 fallback unification) so
928
+ // raw disclosure finds excerpts a branch-scoped session archived at
929
+ // project/root scope — exactly as recall + `lcmSearch` do. Only with a
930
+ // concrete sessionKey; already read-gated.
931
+ const rawExcerptSessionIds =
932
+ disclosure === "raw" && rawExcerptNamespace && trimmedSessionKey
933
+ ? this.deps.resolveLcmReadSessionIds(
934
+ request.namespace,
935
+ rawExcerptNamespace,
936
+ trimmedSessionKey,
937
+ authenticatedPrincipal,
938
+ )
939
+ : undefined;
940
+ let results = await this.deps.serializeRecallResults(snapshot, disclosure, {
941
+ query,
942
+ sessionKey: trimmedSessionKey,
943
+ ...(rawExcerptNamespace ? { rawExcerptNamespace } : {}),
944
+ ...(rawExcerptSessionIds !== undefined ? { rawExcerptSessionIds } : {}),
945
+ ...(rawExcerptsSuppressed ? { rawExcerptsSuppressed } : {}),
946
+ });
947
+
948
+ // Tag filter (issue #689). Applied post-recall, post-serialization so
949
+ // the actual frontmatter tags are already loaded onto each result. When
950
+ // `tags` is absent or empty the filter is a no-op; an invalid `tagMatch`
951
+ // throws via `parseTagMatch` (CLAUDE.md rule 51).
952
+ const filterTags = normalizeTags(request.tags);
953
+ let tagMatchMode: TagMatchMode | undefined;
954
+ try {
955
+ tagMatchMode = parseTagMatch(request.tagMatch);
956
+ } catch (err) {
957
+ throw new EngramAccessInputError(
958
+ err instanceof Error ? err.message : String(err),
959
+ );
960
+ }
961
+ let effectiveContext = context;
962
+ if (filterTags && filterTags.length > 0) {
963
+ const beforeIds = results.map((r) => r.id);
964
+ const { results: admitted } = applyTagFilter(results, {
965
+ tags: filterTags,
966
+ tagMatch: tagMatchMode,
967
+ });
968
+ results = admitted;
969
+ // Codex P1: `context` was generated by orchestrator.recall(...)
970
+ // BEFORE the tag filter ran, so it can contain memories that don't
971
+ // match the requested tags. Surfaces consuming `context` (the
972
+ // prompt-injection string) would leak excluded content into the
973
+ // LLM. When the filter actually drops any result, rebuild context
974
+ // from the admitted set so excluded content is unreachable through
975
+ // any field of the response. The rebuilt context concatenates each
976
+ // admitted result's available text (full content at section/raw
977
+ // disclosure, otherwise the preview) — a different wire format
978
+ // than the orchestrator's native context, but a strict subset
979
+ // safe to inject.
980
+ const admittedIds = new Set(results.map((r) => r.id));
981
+ const droppedAny = beforeIds.some((id) => !admittedIds.has(id));
982
+ if (droppedAny) {
983
+ effectiveContext = results
984
+ .map((r) => {
985
+ const content =
986
+ typeof (r as { content?: unknown }).content === "string"
987
+ ? ((r as { content?: string }).content ?? "")
988
+ : "";
989
+ const preview =
990
+ typeof (r as { preview?: unknown }).preview === "string"
991
+ ? ((r as { preview?: string }).preview ?? "")
992
+ : "";
993
+ return content || preview;
994
+ })
995
+ .filter((s) => s.length > 0)
996
+ .join("\n\n");
997
+ }
998
+ }
999
+ const filteredMemoryIds = filterTags && filterTags.length > 0
1000
+ ? results.map((r) => r.id)
1001
+ : (snapshot?.memoryIds ?? []);
1002
+ const debug = await this.deps.buildRecallDebug(
1003
+ snapshot,
1004
+ effectiveNamespace,
1005
+ request.includeDebug === true,
1006
+ request.sessionKey,
1007
+ );
1008
+
1009
+ // Fire-and-forget audit recording. Must never block or crash recall.
1010
+ let auditAnomalies: AccessAuditResult["anomalies"] | undefined;
1011
+ if (this.deps.auditAdapter) {
1012
+ try {
1013
+ const resolvedAgentId = principal ?? "__anonymous__";
1014
+ const auditEntry = {
1015
+ ts: new Date().toISOString(),
1016
+ sessionKey: request.sessionKey ?? "",
1017
+ agentId: resolvedAgentId,
1018
+ trigger: "access-surface",
1019
+ queryText: query,
1020
+ candidateMemoryIds: snapshot?.memoryIds ?? [],
1021
+ // Audit must reflect what was actually injected, not what
1022
+ // recall produced before the tag filter. Using `context`
1023
+ // (pre-filter) overstates injectedChars and can leak content
1024
+ // from excluded memories into the audit summary (cursor
1025
+ // Medium on PR #712).
1026
+ summary: effectiveContext.slice(0, 200) || null,
1027
+ injectedChars: effectiveContext.length,
1028
+ toggleState: "enabled" as const,
1029
+ latencyMs: Date.now() - startedAt,
1030
+ plannerMode: snapshot?.plannerMode ?? mode,
1031
+ requestedMode: mode,
1032
+ fallbackUsed: snapshot?.fallbackUsed ?? false,
1033
+ };
1034
+ const auditResult = await this.deps.auditAdapter.record(
1035
+ resolvedAgentId || "__anonymous__",
1036
+ auditEntry,
1037
+ );
1038
+ auditAnomalies = auditResult.anomalies;
1039
+ } catch {
1040
+ // Audit failures must never crash the recall path.
1041
+ }
1042
+ }
1043
+
1044
+ return {
1045
+ response: {
1046
+ query,
1047
+ sessionKey: request.sessionKey,
1048
+ namespace: effectiveNamespace,
1049
+ context: effectiveContext,
1050
+ count: filterTags && filterTags.length > 0
1051
+ ? results.length
1052
+ : (snapshot?.memoryIds.length ?? results.length),
1053
+ memoryIds: filteredMemoryIds,
1054
+ results,
1055
+ recordedAt: snapshot?.recordedAt,
1056
+ traceId: snapshot?.traceId,
1057
+ plannerMode: snapshot?.plannerMode ?? mode,
1058
+ fallbackUsed: snapshot?.fallbackUsed ?? false,
1059
+ sourcesUsed: snapshot?.sourcesUsed ?? [],
1060
+ disclosure,
1061
+ budgetsApplied: snapshot?.budgetsApplied,
1062
+ auditAnomalies,
1063
+ budgetWarning: budgetDecision.reason === "warn-over-soft" ? budgetDecision : undefined,
1064
+ latencyMs: snapshot?.latencyMs ?? (Date.now() - startedAt),
1065
+ debug,
1066
+ },
1067
+ budgetRecordPrincipal: recordBudgetAfterSuccess ? principal : null,
1068
+ };
1069
+ }
1070
+
1071
+ /**
1072
+ * Recall X-ray (issue #570). Runs a recall with `xrayCapture: true`
1073
+ * and returns the resulting snapshot as structured JSON so every
1074
+ * surface (CLI / HTTP / MCP) gets the same payload. Namespace scope
1075
+ * is enforced before the recall fires (CLAUDE.md rule 42 — read and
1076
+ * write paths must resolve through the same namespace layer) so an
1077
+ * unauthorized principal cannot capture an x-ray for a namespace it
1078
+ * cannot read.
1079
+ */
1080
+ async recallXray(request: {
1081
+ query: string;
1082
+ sessionKey?: string;
1083
+ namespace?: string;
1084
+ budget?: number;
1085
+ authenticatedPrincipal?: string;
1086
+ /**
1087
+ * Disclosure depth used to shape per-result payload (issue #677
1088
+ * PR 3/4). When set, each X-ray result is decorated with the
1089
+ * matching `disclosure` field and `estimatedTokens` computed from
1090
+ * the actual rendered content at that depth, so the renderer's
1091
+ * "Token spend by disclosure" summary reflects real spend rather
1092
+ * than staying empty when no caller wires the depth knob through.
1093
+ */
1094
+ disclosure?: RecallDisclosure;
1095
+ /**
1096
+ * Free-form recall tag filter (issue #689). Mirrors the field on
1097
+ * `EngramAccessRecallRequest`. When non-empty, the captured X-ray
1098
+ * snapshot's `results` are filtered down to memories whose
1099
+ * frontmatter tags satisfy `tagMatch` ("any" by default), and a
1100
+ * `tag-filter` entry is appended to `filters`.
1101
+ */
1102
+ tags?: string[];
1103
+ /** Match mode for `tags`. See `EngramAccessRecallRequest.tagMatch`. */
1104
+ tagMatch?: "any" | "all";
1105
+ /** Recall planner mode override. Mirrors `EngramAccessRecallRequest.mode`. */
1106
+ mode?: RecallPlanMode | "auto";
1107
+ /**
1108
+ * User-aware context scopes active for this recall. Forwarded into
1109
+ * provenance construction so boundary scopes are evaluated against
1110
+ * the caller's real context instead of an empty-context default.
1111
+ */
1112
+ currentContextScopes?: readonly unknown[];
1113
+ /**
1114
+ * Internal inspector affordance: include a recall-shaped response
1115
+ * derived from the same X-ray snapshot. Left off by default so the
1116
+ * regular X-ray API/CLI/MCP surfaces keep their existing payload shape.
1117
+ */
1118
+ includeRecall?: boolean;
1119
+ }): Promise<{
1120
+ snapshotFound: boolean;
1121
+ snapshot?: RecallXraySnapshot;
1122
+ recall?: EngramAccessRecallResponse;
1123
+ }> {
1124
+ const query = typeof request.query === "string" ? request.query : "";
1125
+ if (query.trim().length === 0) {
1126
+ // Match the CLI contract (CLAUDE.md rule 51): reject empty
1127
+ // input with an explicit error rather than silently producing
1128
+ // an empty snapshot.
1129
+ throw new Error("recallXray: query is required and must be non-empty");
1130
+ }
1131
+ // Validate disclosure UP FRONT — before recall executes, before
1132
+ // the xray queue mutex is acquired, before namespace resolution.
1133
+ // A bad value should fail fast rather than after we've burned
1134
+ // cycles on an irreversible recall (Cursor Medium review on PR
1135
+ // #699).
1136
+ if (
1137
+ request.disclosure !== undefined &&
1138
+ !isRecallDisclosure(request.disclosure)
1139
+ ) {
1140
+ throw new EngramAccessInputError(
1141
+ `recallXray: disclosure must be one of: chunk, section, raw (got: ${String(request.disclosure)})`,
1142
+ );
1143
+ }
1144
+
1145
+ const namespacesEnabled = resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces;
1146
+ const requestedNamespace = request.namespace?.trim()
1147
+ ? this.deps.resolveNamespace(request.namespace)
1148
+ : undefined;
1149
+ const authenticatedPrincipal = request.authenticatedPrincipal?.trim();
1150
+ const principal =
1151
+ authenticatedPrincipal
1152
+ || resolvePrincipal(request.sessionKey, this.deps.orchestrator.config);
1153
+
1154
+ if (requestedNamespace) {
1155
+ if (
1156
+ !canReadNamespace(
1157
+ principal,
1158
+ requestedNamespace,
1159
+ this.deps.orchestrator.config,
1160
+ )
1161
+ ) {
1162
+ return { snapshotFound: false };
1163
+ }
1164
+ } else if (
1165
+ namespacesEnabled
1166
+ && !authenticatedPrincipal
1167
+ && !request.sessionKey?.trim()
1168
+ ) {
1169
+ // Namespaces enabled but no identity supplied — reject rather
1170
+ // than scanning the global namespace (CLAUDE.md rule 48:
1171
+ // least-privileged default).
1172
+ return { snapshotFound: false };
1173
+ }
1174
+
1175
+ // Optional `--budget` override must be a positive integer. Invalid
1176
+ // values throw rather than silently defaulting (CLAUDE.md rule 51).
1177
+ let budgetOverride: number | undefined;
1178
+ if (request.budget !== undefined && request.budget !== null) {
1179
+ const parsed =
1180
+ typeof request.budget === "number"
1181
+ ? request.budget
1182
+ : Number(request.budget);
1183
+ if (
1184
+ !Number.isFinite(parsed)
1185
+ || parsed <= 0
1186
+ || !Number.isInteger(parsed)
1187
+ ) {
1188
+ throw new Error(
1189
+ `recallXray: budget expects a positive integer; got ${JSON.stringify(request.budget)}`,
1190
+ );
1191
+ }
1192
+ budgetOverride = parsed;
1193
+ }
1194
+ const mode = this.deps.normalizeRecallMode(request.mode);
1195
+ const disclosure = request.disclosure ?? DEFAULT_RECALL_DISCLOSURE;
1196
+
1197
+ // Serialize x-ray invocations behind a per-service mutex so the
1198
+ // per-process `getLastXraySnapshot()` slot cannot be clobbered by
1199
+ // a concurrent capturing call before this caller reads it back.
1200
+ // Budget and principal are now threaded through
1201
+ // `RecallInvocationOptions`, so global config mutation is gone
1202
+ // (CLAUDE.md rule 47: no shared mutable state across async
1203
+ // boundaries). The mutex stays only for the snapshot-slot
1204
+ // ordering guarantee.
1205
+ const previousQueue = this.deps.xrayQueue;
1206
+ let release: () => void = () => {};
1207
+ this.deps.xrayQueue = new Promise<void>((resolve) => {
1208
+ release = resolve;
1209
+ });
1210
+ await previousQueue;
1211
+ const recallStartedAt = Date.now();
1212
+
1213
+ const recallSessionKey = request.sessionKey?.trim() || undefined;
1214
+ let xrayResponse: {
1215
+ snapshotFound: boolean;
1216
+ snapshot?: RecallXraySnapshot;
1217
+ } = { snapshotFound: false };
1218
+
1219
+ try {
1220
+ // Clear any prior snapshot so a capture failure surfaces as
1221
+ // `{snapshotFound: false}` rather than returning stale data
1222
+ // from an earlier call on the same orchestrator.
1223
+ this.deps.orchestrator.clearLastXraySnapshot();
1224
+ await this.deps.orchestrator.recall(query, recallSessionKey, {
1225
+ xrayCapture: true,
1226
+ ...(requestedNamespace ? { namespace: requestedNamespace } : {}),
1227
+ ...(budgetOverride !== undefined
1228
+ ? { budgetCharsOverride: budgetOverride }
1229
+ : {}),
1230
+ ...(mode !== undefined ? { mode } : {}),
1231
+ // When the caller supplies an authenticated principal, forward
1232
+ // it via the dedicated override channel so orchestrator-side
1233
+ // ACL decisions use the SAME principal the access-surface
1234
+ // pre-check above authorized. Threading an
1235
+ // `authenticatedPrincipal` through `sessionKey` would be wrong:
1236
+ // `resolvePrincipal(sessionKey)` only maps configured raw
1237
+ // session keys and otherwise collapses to `"default"`, which
1238
+ // in namespace-enabled deployments produces false denials /
1239
+ // wrong-scope serving despite the pre-check passing
1240
+ // (CLAUDE.md rule 42).
1241
+ ...(authenticatedPrincipal
1242
+ ? { principalOverride: authenticatedPrincipal }
1243
+ : {}),
1244
+ ...(request.currentContextScopes !== undefined
1245
+ ? { currentContextScopes: request.currentContextScopes }
1246
+ : {}),
1247
+ });
1248
+
1249
+ const rawSnapshot = this.deps.orchestrator.getLastXraySnapshot();
1250
+ // Re-check namespace after capture: the recall may have served
1251
+ // from a different namespace than the caller requested. Drop
1252
+ // the snapshot rather than leak cross-tenant data (CLAUDE.md
1253
+ // rules 42 + 47). The comparison is strict so a snapshot whose
1254
+ // namespace is `undefined` cannot bypass the scope the caller
1255
+ // asked for.
1256
+ const namespaceMismatch =
1257
+ requestedNamespace !== undefined &&
1258
+ rawSnapshot?.namespace !== requestedNamespace;
1259
+ if (!rawSnapshot) {
1260
+ xrayResponse = { snapshotFound: false };
1261
+ } else if (namespaceMismatch) {
1262
+ xrayResponse = { snapshotFound: false };
1263
+ } else {
1264
+ // Tag filter (issue #689). Mirrors `recall()` semantics — applied
1265
+ // post-capture by reading each result's frontmatter tags and
1266
+ // dropping non-matching results. Filter activity surfaces as a
1267
+ // `tag-filter` entry in `snapshot.filters` so X-ray consumers can
1268
+ // see the "considered → admitted" delta.
1269
+ let snapshot = rawSnapshot;
1270
+ const xrayFilterTags = normalizeTags(request.tags);
1271
+ let xrayTagMatch: TagMatchMode | undefined;
1272
+ try {
1273
+ xrayTagMatch = parseTagMatch(request.tagMatch);
1274
+ } catch (err) {
1275
+ throw new EngramAccessInputError(
1276
+ err instanceof Error ? err.message : String(err),
1277
+ );
1278
+ }
1279
+ if (xrayFilterTags && xrayFilterTags.length > 0) {
1280
+ const namespace = snapshot.namespace
1281
+ ? this.deps.resolveNamespace(snapshot.namespace)
1282
+ : this.deps.orchestrator.config.defaultNamespace;
1283
+ const tagsByIndex = await Promise.all(
1284
+ snapshot.results.map(async (result) => {
1285
+ try {
1286
+ const storage = await this.deps.orchestrator.getStorage(namespace);
1287
+ const memory = await storage.readMemoryByPath(result.path);
1288
+ const t = memory?.frontmatter?.tags;
1289
+ // Normalize identically to the recall path
1290
+ // (`normalizeProjectionTags`): trim and drop empty strings
1291
+ // so X-ray tag matching stays consistent with the recall
1292
+ // surface. Without this, a frontmatter tag like " draft "
1293
+ // would match in recall but not in X-ray (cursor review).
1294
+ return Array.isArray(t) ? normalizeProjectionTags(t) : [];
1295
+ } catch {
1296
+ return [];
1297
+ }
1298
+ }),
1299
+ );
1300
+ const tagged = snapshot.results.map((result, index) => ({
1301
+ result,
1302
+ tags: tagsByIndex[index] ?? [],
1303
+ }));
1304
+ const { results: admittedTagged, trace } = applyTagFilter(tagged, {
1305
+ tags: xrayFilterTags,
1306
+ tagMatch: xrayTagMatch,
1307
+ });
1308
+ const admittedResults = admittedTagged.map((entry) => entry.result);
1309
+ const filters = trace ? [...snapshot.filters, trace] : snapshot.filters;
1310
+ snapshot = { ...snapshot, results: admittedResults, filters };
1311
+ }
1312
+ // Decorate per-result disclosure + token estimate when the caller
1313
+ // wired a depth knob (issue #677 PR 3/4 — codex review on #699
1314
+ // flagged that the renderer's per-disclosure summary stays empty
1315
+ // until callers populate these fields). Estimate tokens from
1316
+ // the actual rendered payload at the requested depth so the
1317
+ // summary reflects real spend; chunk uses the preview, section
1318
+ // and raw use full content. Best-effort only — a missing
1319
+ // memory or read failure is silently dropped (CLAUDE.md rule 13).
1320
+ if (request.disclosure !== undefined) {
1321
+ // Disclosure already validated up front; pin to the narrowed
1322
+ // type here. Re-validation inside the queue would be dead code.
1323
+ const disclosure: RecallDisclosure = request.disclosure;
1324
+ const namespace = snapshot.namespace
1325
+ ? this.deps.resolveNamespace(snapshot.namespace)
1326
+ : this.deps.orchestrator.config.defaultNamespace;
1327
+ // Pre-fetch raw excerpts ONCE so the first raw-disclosure
1328
+ // result's token estimate includes the LCM-side excerpt spend
1329
+ // that `shapeMemorySummary` actually attaches in the recall
1330
+ // response. Without this, raw recalls systematically
1331
+ // undercounted spend on the first result (Cursor Medium review
1332
+ // on PR #699). Excerpts are scoped to the same session +
1333
+ // namespace as the recall.
1334
+ // Trim sessionKey to match what `orchestrator.recall(...)`
1335
+ // already does (`request.sessionKey?.trim() || undefined`),
1336
+ // otherwise a whitespace-padded key drives recall under one
1337
+ // identity but probes LCM under a different prefix and
1338
+ // misses stored excerpts (Cursor Low review on PR #699).
1339
+ const trimmedSessionKey = request.sessionKey?.trim() || undefined;
1340
+ // Read-authorization-gated namespace for the raw-excerpt LCM lookup
1341
+ // (#1505 thread 2f7). NOT `snapshot.namespace` (the write/overlay
1342
+ // namespace), which would attach `<principal>-project-*` overlay rows
1343
+ // when the principal can WRITE but not READ its self base. Mirrors the
1344
+ // recall + `lcmSearch` read gate. Resolved ONLY for raw disclosure (its
1345
+ // sole consumer); the `namespace` above is still used for the
1346
+ // memory-FILE reads below (a separate, snapshot-scoped read), so non-raw
1347
+ // x-ray decoration stays byte-for-byte unchanged.
1348
+ const rawExcerptNamespace =
1349
+ disclosure === "raw"
1350
+ ? this.deps.resolveRawExcerptReadNamespace(
1351
+ request.namespace,
1352
+ trimmedSessionKey,
1353
+ authenticatedPrincipal,
1354
+ )
1355
+ : namespace;
1356
+ // `undefined` for an IMPLICIT raw recall means NO readable LCM namespace
1357
+ // exists (restrictive `default` READ policy, no readable overlay/self)
1358
+ // — suppress excerpts rather than fall back to the write/overlay
1359
+ // namespace the read gate excludes (#1505 thread NBHWz).
1360
+ const xrayHasExplicitNamespace =
1361
+ typeof request.namespace === "string" &&
1362
+ request.namespace.trim().length > 0;
1363
+ const rawExcerptsSuppressed =
1364
+ disclosure === "raw" &&
1365
+ !xrayHasExplicitNamespace &&
1366
+ rawExcerptNamespace === undefined;
1367
+ // Ordered, read-authorized LCM read key SET (#1505 fallback
1368
+ // unification) so raw disclosure finds excerpts a branch-scoped session
1369
+ // archived at project/root scope — exactly as recall + `lcmSearch` do.
1370
+ // Only meaningful with a concrete sessionKey + a readable namespace;
1371
+ // already read-gated so no unauthorized overlay key is present.
1372
+ const rawExcerptSessionIds =
1373
+ disclosure === "raw" && trimmedSessionKey && rawExcerptNamespace
1374
+ ? this.deps.resolveLcmReadSessionIds(
1375
+ request.namespace,
1376
+ rawExcerptNamespace,
1377
+ trimmedSessionKey,
1378
+ authenticatedPrincipal,
1379
+ )
1380
+ : undefined;
1381
+ const rawExcerpts =
1382
+ disclosure === "raw" && !rawExcerptsSuppressed
1383
+ ? await this.deps.fetchRawExcerpts(disclosure, {
1384
+ query,
1385
+ ...(trimmedSessionKey ? { sessionKey: trimmedSessionKey } : {}),
1386
+ ...(rawExcerptNamespace
1387
+ ? { namespace: rawExcerptNamespace }
1388
+ : {}),
1389
+ ...(rawExcerptSessionIds !== undefined
1390
+ ? { lcmSessionIds: rawExcerptSessionIds }
1391
+ : {}),
1392
+ })
1393
+ : disclosure === "raw"
1394
+ ? []
1395
+ : null;
1396
+ const rawExcerptText =
1397
+ rawExcerpts && rawExcerpts.length > 0
1398
+ ? rawExcerpts.map((e) => e.content).join("\n")
1399
+ : "";
1400
+ // Pre-load every memory in parallel so we can:
1401
+ // (a) re-attribute raw excerpts to the *first readable* result
1402
+ // rather than always to index 0 (Cursor Low review on PR
1403
+ // #699: a missing/unreadable result[0] orphaned the excerpt
1404
+ // budget); and
1405
+ // (b) include the metadata fields `shapeMemorySummary` actually
1406
+ // emits at every depth (id, path, category, status, created,
1407
+ // updated, tags, entityRef) in the token estimate, so the
1408
+ // summary reflects real spend rather than only payload-body
1409
+ // spend (Cursor Low review on PR #699).
1410
+ const memoryByIndex = await Promise.all(
1411
+ snapshot.results.map(async (result) => {
1412
+ try {
1413
+ const storage = await this.deps.orchestrator.getStorage(namespace);
1414
+ return await storage.readMemoryByPath(result.path);
1415
+ } catch {
1416
+ return null;
1417
+ }
1418
+ }),
1419
+ );
1420
+ const firstReadableIndex = memoryByIndex.findIndex((m) => m !== null);
1421
+ const baseDir =
1422
+ (await this.deps.orchestrator.getStorage(namespace)).dir;
1423
+ const decorated = snapshot.results.map((result, index) => {
1424
+ const memory = memoryByIndex[index];
1425
+ if (!memory) {
1426
+ // Unreadable result: attach the disclosure tag anyway so
1427
+ // the per-disclosure summary classifies it correctly,
1428
+ // but skip the token estimate since we don't have the
1429
+ // content to measure. Without the disclosure tag the
1430
+ // result silently flows into the `unspecified` bucket
1431
+ // even though the caller explicitly requested a depth
1432
+ // (Cursor Low review on PR #699).
1433
+ return { ...result, disclosure };
1434
+ }
1435
+ // Build a representative shaped summary so the estimate
1436
+ // counts every field `shapeMemorySummary` actually emits.
1437
+ // The serialized JSON form is a close-enough proxy for the
1438
+ // wire payload size.
1439
+ const shaped = shapeMemorySummary(
1440
+ memory,
1441
+ baseDir,
1442
+ disclosure,
1443
+ disclosure === "raw" &&
1444
+ index === firstReadableIndex &&
1445
+ rawExcerpts &&
1446
+ rawExcerpts.length > 0
1447
+ ? rawExcerpts
1448
+ : undefined,
1449
+ );
1450
+ return {
1451
+ ...result,
1452
+ disclosure,
1453
+ estimatedTokens: estimateRecallTokens(JSON.stringify(shaped)),
1454
+ };
1455
+ });
1456
+ // Edge case: every result was unreadable but rawExcerpts
1457
+ // still has content — credit that spend to result[0] rather
1458
+ // than dropping it on the floor. Without this, the raw row
1459
+ // in the per-disclosure summary under-reports spend whenever
1460
+ // every memory file is missing/unreadable.
1461
+ if (
1462
+ disclosure === "raw" &&
1463
+ firstReadableIndex === -1 &&
1464
+ rawExcerptText.length > 0 &&
1465
+ decorated.length > 0
1466
+ ) {
1467
+ decorated[0] = {
1468
+ ...decorated[0]!,
1469
+ disclosure,
1470
+ estimatedTokens: estimateRecallTokens(rawExcerptText),
1471
+ };
1472
+ }
1473
+ const decoratedSnapshot = { ...snapshot, results: decorated };
1474
+ xrayResponse = {
1475
+ snapshotFound: true,
1476
+ snapshot: decoratedSnapshot,
1477
+ };
1478
+ } else {
1479
+ xrayResponse = {
1480
+ snapshotFound: true,
1481
+ snapshot,
1482
+ };
1483
+ }
1484
+ }
1485
+ } finally {
1486
+ release();
1487
+ }
1488
+
1489
+ if (
1490
+ request.includeRecall === true &&
1491
+ xrayResponse.snapshotFound === true &&
1492
+ xrayResponse.snapshot
1493
+ ) {
1494
+ // Same read-authorization-gated raw-excerpt namespace the recall path uses
1495
+ // (#1505 thread 2f7), so the includeRecall x-ray path can't leak overlay
1496
+ // transcript rows via raw disclosure. Resolved ONLY for raw disclosure (the
1497
+ // sole consumer) so non-raw x-ray recall stays byte-for-byte unchanged. The
1498
+ // ordered LCM read key SET (#1505 fallback unification) adds the coding read
1499
+ // fallbacks so a branch-scoped session also finds excerpts at project/root
1500
+ // scope.
1501
+ const xrayRawExcerptNamespace =
1502
+ disclosure === "raw"
1503
+ ? this.deps.resolveRawExcerptReadNamespace(
1504
+ request.namespace,
1505
+ recallSessionKey,
1506
+ authenticatedPrincipal,
1507
+ )
1508
+ : undefined;
1509
+ // `undefined` for an IMPLICIT raw recall means NO readable LCM namespace
1510
+ // exists — suppress excerpts rather than fall back to the write/overlay
1511
+ // namespace the read gate excludes (#1505 thread NBHWz).
1512
+ const xrayHasExplicitNamespace =
1513
+ typeof request.namespace === "string" &&
1514
+ request.namespace.trim().length > 0;
1515
+ const xrayRawExcerptsSuppressed =
1516
+ disclosure === "raw" &&
1517
+ !xrayHasExplicitNamespace &&
1518
+ xrayRawExcerptNamespace === undefined;
1519
+ const xrayRawExcerptSessionIds =
1520
+ disclosure === "raw" && xrayRawExcerptNamespace && recallSessionKey
1521
+ ? this.deps.resolveLcmReadSessionIds(
1522
+ request.namespace,
1523
+ xrayRawExcerptNamespace,
1524
+ recallSessionKey,
1525
+ authenticatedPrincipal,
1526
+ )
1527
+ : undefined;
1528
+ return {
1529
+ ...xrayResponse,
1530
+ recall: await this.deps.buildRecallResponseFromXraySnapshot({
1531
+ query,
1532
+ sessionKey: recallSessionKey,
1533
+ snapshot: xrayResponse.snapshot,
1534
+ disclosure,
1535
+ startedAt: recallStartedAt,
1536
+ requestedMode: request.mode,
1537
+ normalizedMode: mode,
1538
+ ...(xrayRawExcerptNamespace
1539
+ ? { rawExcerptNamespace: xrayRawExcerptNamespace }
1540
+ : {}),
1541
+ ...(xrayRawExcerptSessionIds !== undefined
1542
+ ? { rawExcerptSessionIds: xrayRawExcerptSessionIds }
1543
+ : {}),
1544
+ ...(xrayRawExcerptsSuppressed
1545
+ ? { rawExcerptsSuppressed: xrayRawExcerptsSuppressed }
1546
+ : {}),
1547
+ }),
1548
+ };
1549
+ }
1550
+ return xrayResponse;
1551
+ }
1552
+ }