@remnic/core 9.3.751 → 9.3.753

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 (300) hide show
  1. package/dist/access-admin-ops-surface.d.ts +194 -0
  2. package/dist/access-admin-ops-surface.js +128 -0
  3. package/dist/access-boundary.d.ts +5 -5
  4. package/dist/access-boundary.js +30 -30
  5. package/dist/access-cli.js +92 -92
  6. package/dist/access-http.d.ts +5 -5
  7. package/dist/access-http.js +38 -38
  8. package/dist/access-mcp.d.ts +5 -5
  9. package/dist/access-mcp.js +35 -35
  10. package/dist/access-operations-batch.js +32 -32
  11. package/dist/access-operations.d.ts +11 -11
  12. package/dist/access-operations.js +34 -34
  13. package/dist/access-recall-surface.d.ts +363 -0
  14. package/dist/access-recall-surface.js +128 -0
  15. package/dist/access-schema.d.ts +116 -116
  16. package/dist/access-schema.js +8 -8
  17. package/dist/{access-service-DmAEOJdJ.d.ts → access-service-MitduesD.d.ts} +75 -118
  18. package/dist/access-service.d.ts +5 -5
  19. package/dist/access-service.js +33 -29
  20. package/dist/access-surface-catalog.d.ts +5 -5
  21. package/dist/action-confidence.d.ts +1 -1
  22. package/dist/active-memory-bridge.d.ts +1 -1
  23. package/dist/active-recall.d.ts +1 -1
  24. package/dist/active-recall.js +6 -6
  25. package/dist/adapters/index.js +4 -4
  26. package/dist/adapters/registry.js +2 -2
  27. package/dist/behavior-learner.d.ts +1 -1
  28. package/dist/behavior-signals.d.ts +1 -1
  29. package/dist/bootstrap.d.ts +4 -4
  30. package/dist/briefing.d.ts +3 -3
  31. package/dist/briefing.js +10 -10
  32. package/dist/buffer-surprise-report.d.ts +1 -1
  33. package/dist/buffer.d.ts +3 -3
  34. package/dist/calibration.d.ts +1 -1
  35. package/dist/capabilities.d.ts +1 -1
  36. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-CZJSLEFG.js} +3 -3
  37. package/dist/capsule-crypto-CZJSLEFG.js.map +1 -0
  38. package/dist/{catalog-BpOcwHdE.d.ts → catalog-BEmNVRN0.d.ts} +1 -1
  39. package/dist/causal-behavior.d.ts +1 -1
  40. package/dist/causal-behavior.js +2 -2
  41. package/dist/causal-chain.js +2 -2
  42. package/dist/causal-consolidation.d.ts +1 -1
  43. package/dist/causal-consolidation.js +15 -15
  44. package/dist/causal-retrieval.js +2 -2
  45. package/dist/causal-trajectory-graph.d.ts +1 -1
  46. package/dist/causal-trajectory.js +1 -1
  47. package/dist/{chunk-6SXNQFHB.js → chunk-243P7YYV.js} +7 -7
  48. package/dist/{chunk-2NLLXCJG.js → chunk-2BFLDH5F.js} +2 -2
  49. package/dist/{chunk-ZBVQ4ZM4.js → chunk-34THARMH.js} +17 -17
  50. package/dist/{chunk-SNEXVX3U.js → chunk-3HGZX26P.js} +21 -21
  51. package/dist/{chunk-5PZCDUJ6.js → chunk-3ZIKDH6C.js} +4 -4
  52. package/dist/{chunk-X7Y7WX73.js → chunk-437S3G37.js} +2 -2
  53. package/dist/{chunk-RS25QOKZ.js → chunk-64SE4X5G.js} +2 -2
  54. package/dist/{chunk-JBPKEARU.js → chunk-AU7Q3LSC.js} +4 -4
  55. package/dist/{chunk-6FNZ3NMS.js → chunk-AXBRVBT6.js} +2 -2
  56. package/dist/{chunk-W3BFT5OJ.js → chunk-BWOEYBRB.js} +2 -2
  57. package/dist/{chunk-UU32EAIY.js → chunk-C3IHRWIC.js} +7 -7
  58. package/dist/{chunk-DBRVU5PR.js → chunk-DKIZ4BUQ.js} +14 -14
  59. package/dist/{chunk-EEKDRAWX.js → chunk-DS4QM6I7.js} +4 -4
  60. package/dist/{chunk-WOX2UW4K.js → chunk-E3MRYDUQ.js} +2 -2
  61. package/dist/{chunk-K2R3DEEH.js → chunk-E6LENGYV.js} +2 -2
  62. package/dist/{chunk-CGZWGVQF.js → chunk-EEO4YWQ7.js} +9 -9
  63. package/dist/{chunk-ORAK3LNZ.js → chunk-ER3CCWK2.js} +2 -2
  64. package/dist/{chunk-TPLE6SNY.js → chunk-GWGWBK5Y.js} +7 -7
  65. package/dist/{chunk-BJMBJZ2Y.js → chunk-GWTFLF2P.js} +59 -59
  66. package/dist/chunk-GWTFLF2P.js.map +1 -0
  67. package/dist/{chunk-IKHKHADN.js → chunk-ITLJATQI.js} +5 -5
  68. package/dist/{chunk-E4PFDD3N.js → chunk-JES2O5Q6.js} +4 -4
  69. package/dist/{chunk-QOGXUNWL.js → chunk-JGPURIEH.js} +7 -7
  70. package/dist/{chunk-YGGXUNS4.js → chunk-KY4WOZ3P.js} +55 -55
  71. package/dist/{chunk-WA7FECSG.js → chunk-KYHGLDFU.js} +7 -7
  72. package/dist/{chunk-V5RVMULT.js → chunk-LZ73Y56P.js} +10 -10
  73. package/dist/{chunk-ARV3AUOM.js → chunk-M5QKGHCR.js} +2 -2
  74. package/dist/{chunk-SD34EK4Z.js → chunk-N3VE7OCY.js} +4 -4
  75. package/dist/{chunk-KQAFEZQX.js → chunk-OUXPVWTZ.js} +6 -6
  76. package/dist/{chunk-NNL4LJAD.js → chunk-OZMBT7N3.js} +5 -5
  77. package/dist/{chunk-RWZB6MAY.js → chunk-PN5AMCK2.js} +1065 -1176
  78. package/dist/chunk-PN5AMCK2.js.map +1 -0
  79. package/dist/{chunk-WWSXVOGY.js → chunk-QIRZI3C3.js} +2 -2
  80. package/dist/{chunk-B35L45HZ.js → chunk-TVISHYL5.js} +2 -2
  81. package/dist/{chunk-SQNED75R.js → chunk-V5UV7BOA.js} +1 -1
  82. package/dist/{chunk-7TWA7DKP.js → chunk-VVSV2QVY.js} +2 -2
  83. package/dist/{chunk-4NWIGAIC.js → chunk-XOPLMSVV.js} +2 -2
  84. package/dist/{chunk-K2JM4DZZ.js → chunk-YNBEHUZH.js} +2 -2
  85. package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
  86. package/dist/{chunk-UETBEHBA.js → chunk-ZECLLIMX.js} +3442 -3085
  87. package/dist/chunk-ZECLLIMX.js.map +1 -0
  88. package/dist/{chunk-QRRBXD24.js → chunk-ZIFPFKB2.js} +7 -7
  89. package/dist/{chunk-HYI2GP3F.js → chunk-ZMHDSIEX.js} +7 -7
  90. package/dist/{cli-lmf5RGyQ.d.ts → cli-DqonYBZ0.d.ts} +3 -3
  91. package/dist/cli.d.ts +6 -6
  92. package/dist/cli.js +64 -64
  93. package/dist/compounding/engine.d.ts +3 -3
  94. package/dist/compounding/engine.js +11 -11
  95. package/dist/compounding/preference-consolidator.d.ts +1 -1
  96. package/dist/compression-optimizer.d.ts +1 -1
  97. package/dist/config.d.ts +1 -1
  98. package/dist/config.js +3 -3
  99. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  100. package/dist/connectors/codex-materialize-runner.js +11 -11
  101. package/dist/connectors/codex-materialize.d.ts +1 -1
  102. package/dist/connectors/index.d.ts +1 -1
  103. package/dist/connectors/index.js +12 -12
  104. package/dist/consolidation-provenance-check.d.ts +1 -1
  105. package/dist/consolidation-undo.d.ts +1 -1
  106. package/dist/contradiction/index.d.ts +1 -1
  107. package/dist/contradiction/index.js +2 -2
  108. package/dist/{contradiction-scan-3FJYWS3G.js → contradiction-scan-2SODRNUF.js} +3 -3
  109. package/dist/contradiction-scan-2SODRNUF.js.map +1 -0
  110. package/dist/conversation-index/backend.d.ts +1 -1
  111. package/dist/conversation-index/chunker.d.ts +1 -1
  112. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  113. package/dist/conversation-index/indexer.d.ts +1 -1
  114. package/dist/conversation-index/search.d.ts +1 -1
  115. package/dist/dashboard-runtime.js +2 -2
  116. package/dist/day-summary.d.ts +1 -1
  117. package/dist/delinearize.d.ts +1 -1
  118. package/dist/direct-answer-wiring.d.ts +1 -1
  119. package/dist/direct-answer.d.ts +1 -1
  120. package/dist/embedding-fallback.d.ts +1 -1
  121. package/dist/enrichment/index.d.ts +1 -1
  122. package/dist/entity-retrieval.d.ts +1 -1
  123. package/dist/entity-retrieval.js +11 -11
  124. package/dist/entity-schema.d.ts +1 -1
  125. package/dist/explicit-capture.d.ts +6 -6
  126. package/dist/extraction-faithfulness.d.ts +1 -1
  127. package/dist/extraction-judge-telemetry.d.ts +1 -1
  128. package/dist/extraction-judge-training.d.ts +1 -1
  129. package/dist/extraction-judge.d.ts +1 -1
  130. package/dist/extraction.d.ts +1 -1
  131. package/dist/extraction.js +3 -3
  132. package/dist/fallback-llm.d.ts +1 -1
  133. package/dist/{first-start-migration-4I3VDSYS.js → first-start-migration-DY6YCRC2.js} +2 -2
  134. package/dist/{forget-WYLR4ZFV.js → forget-OEE5OELK.js} +2 -2
  135. package/dist/graph-dashboard-diff.d.ts +1 -1
  136. package/dist/graph-dashboard-key.d.ts +1 -1
  137. package/dist/graph-dashboard-parser.d.ts +1 -1
  138. package/dist/graph-edge-reinforcement.d.ts +1 -1
  139. package/dist/graph-snapshot.d.ts +1 -1
  140. package/dist/graph.d.ts +1 -1
  141. package/dist/identity-continuity.d.ts +1 -1
  142. package/dist/importance.d.ts +1 -1
  143. package/dist/index.d.ts +1284 -1284
  144. package/dist/index.js +148 -148
  145. package/dist/intent.d.ts +1 -1
  146. package/dist/lcm/engine.d.ts +1 -1
  147. package/dist/lcm/engine.js +4 -4
  148. package/dist/lcm/index.d.ts +1 -1
  149. package/dist/lcm/index.js +8 -8
  150. package/dist/lcm/tools.d.ts +1 -1
  151. package/dist/lifecycle.d.ts +1 -1
  152. package/dist/live-connectors-runner.d.ts +1 -1
  153. package/dist/local-llm.d.ts +1 -1
  154. package/dist/local-model-endpoint.d.ts +1 -1
  155. package/dist/maintenance/memory-governance.d.ts +1 -1
  156. package/dist/maintenance/memory-governance.js +10 -10
  157. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
  158. package/dist/maintenance/rebuild-memory-projection.js +11 -11
  159. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  160. package/dist/memory-action-policy.d.ts +1 -1
  161. package/dist/memory-cache.d.ts +1 -1
  162. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  163. package/dist/memory-projection-store.d.ts +1 -1
  164. package/dist/memory-provenance.d.ts +1 -1
  165. package/dist/memory-worth-outcomes.d.ts +1 -1
  166. package/dist/models-json.d.ts +1 -1
  167. package/dist/namespaces/migrate.d.ts +2 -2
  168. package/dist/namespaces/migrate.js +13 -13
  169. package/dist/namespaces/principal.d.ts +1 -1
  170. package/dist/namespaces/search.d.ts +1 -1
  171. package/dist/namespaces/search.js +1 -1
  172. package/dist/namespaces/storage.d.ts +2 -2
  173. package/dist/namespaces/storage.js +12 -12
  174. package/dist/native-knowledge.d.ts +1 -1
  175. package/dist/operator-toolkit.d.ts +1 -1
  176. package/dist/operator-toolkit.js +19 -19
  177. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  178. package/dist/orchestration/maintenance.d.ts +2 -2
  179. package/dist/orchestration/maintenance.js +15 -15
  180. package/dist/{orchestrator-D7bb8ZGp.d.ts → orchestrator-D7ZGOQLX.d.ts} +101 -180
  181. package/dist/orchestrator.d.ts +4 -4
  182. package/dist/orchestrator.js +92 -92
  183. package/dist/patterns-cli.d.ts +1 -1
  184. package/dist/policy-runtime.d.ts +1 -1
  185. package/dist/provenance.d.ts +1 -1
  186. package/dist/qmd-recall-cache.d.ts +1 -1
  187. package/dist/qmd.d.ts +1 -1
  188. package/dist/recall-disclosure-escalation.d.ts +1 -1
  189. package/dist/recall-explain-renderer.d.ts +1 -1
  190. package/dist/recall-planner-llm.d.ts +1 -1
  191. package/dist/recall-state.d.ts +1 -1
  192. package/dist/recall-tag-filter.d.ts +1 -1
  193. package/dist/recall-xray-cli.d.ts +1 -1
  194. package/dist/recall-xray-renderer.d.ts +1 -1
  195. package/dist/recall-xray.d.ts +1 -1
  196. package/dist/resolve-auth-token.d.ts +1 -1
  197. package/dist/resume-bundles.js +3 -3
  198. package/dist/retrieval-agents.d.ts +1 -1
  199. package/dist/retrieval-tiers.d.ts +1 -1
  200. package/dist/routing/engine.d.ts +1 -1
  201. package/dist/routing/store.d.ts +1 -1
  202. package/dist/schemas.d.ts +100 -100
  203. package/dist/search/embed-helper.d.ts +1 -1
  204. package/dist/search/factory.d.ts +1 -1
  205. package/dist/search/factory.js +1 -1
  206. package/dist/search/index.d.ts +1 -1
  207. package/dist/search/index.js +1 -1
  208. package/dist/search/lancedb-backend.d.ts +1 -1
  209. package/dist/search/meilisearch-backend.d.ts +1 -1
  210. package/dist/search/noop-backend.d.ts +1 -1
  211. package/dist/search/orama-backend.d.ts +1 -1
  212. package/dist/search/port.d.ts +1 -1
  213. package/dist/search/remote-backend.d.ts +1 -1
  214. package/dist/secure-store/index.js +2 -2
  215. package/dist/{semantic-consolidation-CTfbVwCA.d.ts → semantic-consolidation-7Sndms2M.d.ts} +1 -1
  216. package/dist/semantic-consolidation.d.ts +2 -2
  217. package/dist/semantic-consolidation.js +13 -13
  218. package/dist/semantic-rule-promotion.js +10 -10
  219. package/dist/semantic-rule-verifier.d.ts +1 -1
  220. package/dist/semantic-rule-verifier.js +10 -10
  221. package/dist/session-observer-bands.d.ts +1 -1
  222. package/dist/session-observer-state.d.ts +1 -1
  223. package/dist/shared-context/manager.d.ts +9 -9
  224. package/dist/signal.d.ts +1 -1
  225. package/dist/storage.d.ts +1 -1
  226. package/dist/storage.js +9 -9
  227. package/dist/summarizer.d.ts +1 -1
  228. package/dist/summarizer.js +2 -2
  229. package/dist/summary-snapshot.d.ts +1 -1
  230. package/dist/temporal-supersession.d.ts +1 -1
  231. package/dist/temporal-validity.d.ts +1 -1
  232. package/dist/threading.d.ts +1 -1
  233. package/dist/tier-migration.d.ts +1 -1
  234. package/dist/tier-routing.d.ts +1 -1
  235. package/dist/{tier-stats-5XF4UBRQ.js → tier-stats-TG6GUZ6N.js} +4 -4
  236. package/dist/topics.d.ts +1 -1
  237. package/dist/transcript.d.ts +1 -1
  238. package/dist/transfer/autodetect.js +1 -1
  239. package/dist/transfer/backup.js +3 -3
  240. package/dist/transfer/capsule-export.js +4 -4
  241. package/dist/transfer/capsule-import.js +3 -3
  242. package/dist/transfer/types.d.ts +110 -110
  243. package/dist/trust-score-stage.d.ts +1 -1
  244. package/dist/trust-score.d.ts +1 -1
  245. package/dist/{types-D4NYDtXI.d.ts → types-DTEFxFuL.d.ts} +1 -1
  246. package/dist/types.d.ts +1 -1
  247. package/dist/utility-runtime.d.ts +1 -1
  248. package/dist/verified-recall.js +10 -10
  249. package/package.json +2 -2
  250. package/src/access-admin-ops-surface.ts +828 -0
  251. package/src/access-recall-surface.ts +1552 -0
  252. package/src/access-service.ts +70 -1917
  253. package/src/orchestration/recall-entry.ts +290 -0
  254. package/src/orchestration/session-context.ts +407 -0
  255. package/src/orchestrator.ts +125 -462
  256. package/dist/chunk-BJMBJZ2Y.js.map +0 -1
  257. package/dist/chunk-RWZB6MAY.js.map +0 -1
  258. package/dist/chunk-UETBEHBA.js.map +0 -1
  259. /package/dist/{capsule-crypto-7FJQINUR.js.map → access-admin-ops-surface.js.map} +0 -0
  260. /package/dist/{contradiction-scan-3FJYWS3G.js.map → access-recall-surface.js.map} +0 -0
  261. /package/dist/{chunk-6SXNQFHB.js.map → chunk-243P7YYV.js.map} +0 -0
  262. /package/dist/{chunk-2NLLXCJG.js.map → chunk-2BFLDH5F.js.map} +0 -0
  263. /package/dist/{chunk-ZBVQ4ZM4.js.map → chunk-34THARMH.js.map} +0 -0
  264. /package/dist/{chunk-SNEXVX3U.js.map → chunk-3HGZX26P.js.map} +0 -0
  265. /package/dist/{chunk-5PZCDUJ6.js.map → chunk-3ZIKDH6C.js.map} +0 -0
  266. /package/dist/{chunk-X7Y7WX73.js.map → chunk-437S3G37.js.map} +0 -0
  267. /package/dist/{chunk-RS25QOKZ.js.map → chunk-64SE4X5G.js.map} +0 -0
  268. /package/dist/{chunk-JBPKEARU.js.map → chunk-AU7Q3LSC.js.map} +0 -0
  269. /package/dist/{chunk-6FNZ3NMS.js.map → chunk-AXBRVBT6.js.map} +0 -0
  270. /package/dist/{chunk-W3BFT5OJ.js.map → chunk-BWOEYBRB.js.map} +0 -0
  271. /package/dist/{chunk-UU32EAIY.js.map → chunk-C3IHRWIC.js.map} +0 -0
  272. /package/dist/{chunk-DBRVU5PR.js.map → chunk-DKIZ4BUQ.js.map} +0 -0
  273. /package/dist/{chunk-EEKDRAWX.js.map → chunk-DS4QM6I7.js.map} +0 -0
  274. /package/dist/{chunk-WOX2UW4K.js.map → chunk-E3MRYDUQ.js.map} +0 -0
  275. /package/dist/{chunk-K2R3DEEH.js.map → chunk-E6LENGYV.js.map} +0 -0
  276. /package/dist/{chunk-CGZWGVQF.js.map → chunk-EEO4YWQ7.js.map} +0 -0
  277. /package/dist/{chunk-ORAK3LNZ.js.map → chunk-ER3CCWK2.js.map} +0 -0
  278. /package/dist/{chunk-TPLE6SNY.js.map → chunk-GWGWBK5Y.js.map} +0 -0
  279. /package/dist/{chunk-IKHKHADN.js.map → chunk-ITLJATQI.js.map} +0 -0
  280. /package/dist/{chunk-E4PFDD3N.js.map → chunk-JES2O5Q6.js.map} +0 -0
  281. /package/dist/{chunk-QOGXUNWL.js.map → chunk-JGPURIEH.js.map} +0 -0
  282. /package/dist/{chunk-YGGXUNS4.js.map → chunk-KY4WOZ3P.js.map} +0 -0
  283. /package/dist/{chunk-WA7FECSG.js.map → chunk-KYHGLDFU.js.map} +0 -0
  284. /package/dist/{chunk-V5RVMULT.js.map → chunk-LZ73Y56P.js.map} +0 -0
  285. /package/dist/{chunk-ARV3AUOM.js.map → chunk-M5QKGHCR.js.map} +0 -0
  286. /package/dist/{chunk-SD34EK4Z.js.map → chunk-N3VE7OCY.js.map} +0 -0
  287. /package/dist/{chunk-KQAFEZQX.js.map → chunk-OUXPVWTZ.js.map} +0 -0
  288. /package/dist/{chunk-NNL4LJAD.js.map → chunk-OZMBT7N3.js.map} +0 -0
  289. /package/dist/{chunk-WWSXVOGY.js.map → chunk-QIRZI3C3.js.map} +0 -0
  290. /package/dist/{chunk-B35L45HZ.js.map → chunk-TVISHYL5.js.map} +0 -0
  291. /package/dist/{chunk-SQNED75R.js.map → chunk-V5UV7BOA.js.map} +0 -0
  292. /package/dist/{chunk-7TWA7DKP.js.map → chunk-VVSV2QVY.js.map} +0 -0
  293. /package/dist/{chunk-4NWIGAIC.js.map → chunk-XOPLMSVV.js.map} +0 -0
  294. /package/dist/{chunk-K2JM4DZZ.js.map → chunk-YNBEHUZH.js.map} +0 -0
  295. /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
  296. /package/dist/{chunk-QRRBXD24.js.map → chunk-ZIFPFKB2.js.map} +0 -0
  297. /package/dist/{chunk-HYI2GP3F.js.map → chunk-ZMHDSIEX.js.map} +0 -0
  298. /package/dist/{first-start-migration-4I3VDSYS.js.map → first-start-migration-DY6YCRC2.js.map} +0 -0
  299. /package/dist/{forget-WYLR4ZFV.js.map → forget-OEE5OELK.js.map} +0 -0
  300. /package/dist/{tier-stats-5XF4UBRQ.js.map → tier-stats-TG6GUZ6N.js.map} +0 -0
@@ -251,6 +251,9 @@ import {
251
251
  import { formatProfileTraceAscii } from "./profiling.js";
252
252
  import { resolveAccessSetupCapabilities, resolveGraphConstructionCapabilities, resolveIndexingCapabilities } from "./capabilities.js";
253
253
  import { resolveRecallEnhancementCapabilities } from "./capabilities.js";
254
+ import { AccessAdminOpsSurface } from "./access-admin-ops-surface.js";
255
+ import { AccessRecallSurface } from "./access-recall-surface.js";
256
+ import { selfDeps } from "./orchestration/self-deps.js";
254
257
 
255
258
  export class EngramAccessInputError extends Error {}
256
259
 
@@ -300,12 +303,12 @@ function qmdResultPathCandidates(
300
303
  return [...candidates];
301
304
  }
302
305
 
303
- type AccessProfilingReportRequest = {
306
+ export type AccessProfilingReportRequest = {
304
307
  format?: string;
305
308
  limit?: number;
306
309
  };
307
310
 
308
- type AccessProfilingReportResponse = {
311
+ export type AccessProfilingReportResponse = {
309
312
  enabled: boolean;
310
313
  format?: "ascii" | "json";
311
314
  report?: string;
@@ -858,7 +861,7 @@ export interface EngramAccessOfflineSyncApplyResponse extends OfflineSyncApplyCh
858
861
  export type EngramAccessActionConfidenceRequest = ActionConfidenceInput;
859
862
  export type EngramAccessActionConfidenceResponse = ActionConfidenceResult;
860
863
 
861
- async function buildProjectedGovernanceProposedActions(
864
+ export async function buildProjectedGovernanceProposedActions(
862
865
  storage: Awaited<ReturnType<Orchestrator["getStorage"]>>,
863
866
  projected: NonNullable<Awaited<ReturnType<Awaited<ReturnType<Orchestrator["getStorage"]>>["getProjectedGovernanceRecord"]>>>,
864
867
  ): Promise<Awaited<ReturnType<typeof readMemoryGovernanceRunArtifact>>["appliedActions"]> {
@@ -877,7 +880,7 @@ async function buildProjectedGovernanceProposedActions(
877
880
  return buildProposedActions(reviewQueue, memories);
878
881
  }
879
882
 
880
- function hasGroupedGovernanceActions(
883
+ export function hasGroupedGovernanceActions(
881
884
  grouped?: Awaited<ReturnType<typeof readMemoryGovernanceRunArtifact>>["transitionReport"]["proposed"],
882
885
  ): boolean {
883
886
  if (!grouped) return false;
@@ -1305,6 +1308,30 @@ export class EngramAccessService {
1305
1308
  private readonly budget: CrossNamespaceBudget;
1306
1309
  private readonly auditAdapter: AccessAuditAdapter | null;
1307
1310
 
1311
+ /** AccessAdminOpsSurface (access-service decomposition). Lazy; selfDeps live wiring. */
1312
+ private _accessAdminOpsSurface: AccessAdminOpsSurface | undefined;
1313
+
1314
+ private get accessAdminOpsSurface(): AccessAdminOpsSurface {
1315
+ if (!this._accessAdminOpsSurface) {
1316
+ this._accessAdminOpsSurface = new AccessAdminOpsSurface(
1317
+ selfDeps<ConstructorParameters<typeof AccessAdminOpsSurface>[0]>(this),
1318
+ );
1319
+ }
1320
+ return this._accessAdminOpsSurface;
1321
+ }
1322
+
1323
+ /** AccessRecallSurface (access-service decomposition). Lazy; selfDeps live wiring. */
1324
+ private _accessRecallSurface: AccessRecallSurface | undefined;
1325
+
1326
+ private get accessRecallSurface(): AccessRecallSurface {
1327
+ if (!this._accessRecallSurface) {
1328
+ this._accessRecallSurface = new AccessRecallSurface(
1329
+ selfDeps<ConstructorParameters<typeof AccessRecallSurface>[0]>(this),
1330
+ );
1331
+ }
1332
+ return this._accessRecallSurface;
1333
+ }
1334
+
1308
1335
  constructor(private readonly orchestrator: Orchestrator) {
1309
1336
  this.idempotency = new AccessIdempotencyStore(orchestrator.config.memoryDir);
1310
1337
  const accessCaps = resolveAccessSetupCapabilities(orchestrator.config); // #1566 Cluster B
@@ -2097,83 +2124,9 @@ export class EngramAccessService {
2097
2124
  */
2098
2125
  rawExcerptsSuppressed?: boolean;
2099
2126
  }): Promise<EngramAccessRecallResponse> {
2100
- const memoryIds = options.snapshot.results.map((result) => result.memoryId);
2101
- const resultPaths = options.snapshot.results.map((result) => result.path);
2102
- const namespace = options.snapshot.namespace
2103
- ? this.resolveNamespace(options.snapshot.namespace)
2104
- : this.orchestrator.config.defaultNamespace;
2105
- const sourcesUsed = Array.from(
2106
- new Set(options.snapshot.results.map((result) => result.servedBy)),
2107
- );
2108
- const snapshotForSerialization: LastRecallSnapshot = {
2109
- sessionKey: options.sessionKey ?? "",
2110
- recordedAt: new Date(options.snapshot.capturedAt).toISOString(),
2111
- queryHash: createHash("sha256").update(options.query).digest("hex"),
2112
- queryLen: options.query.length,
2113
- memoryIds,
2114
- namespace,
2115
- recallNamespaces: [namespace],
2116
- traceId: options.snapshot.traceId,
2117
- plannerMode: options.normalizedMode,
2118
- requestedMode:
2119
- options.requestedMode && options.requestedMode !== "auto"
2120
- ? options.requestedMode
2121
- : undefined,
2122
- sourcesUsed,
2123
- budgetsApplied: {
2124
- appliedTopK: memoryIds.length,
2125
- recallBudgetChars: options.snapshot.budget.chars,
2126
- maxMemoryTokens: this.orchestrator.config.maxMemoryTokens,
2127
- finalContextChars: options.snapshot.budget.used,
2128
- },
2129
- latencyMs: Date.now() - options.startedAt,
2130
- resultPaths,
2131
- };
2132
- const results = await this.serializeRecallResults(
2133
- snapshotForSerialization,
2134
- options.disclosure,
2135
- {
2136
- query: options.query,
2137
- ...(options.sessionKey ? { sessionKey: options.sessionKey } : {}),
2138
- ...(options.rawExcerptNamespace
2139
- ? { rawExcerptNamespace: options.rawExcerptNamespace }
2140
- : {}),
2141
- ...(options.rawExcerptSessionIds !== undefined
2142
- ? { rawExcerptSessionIds: options.rawExcerptSessionIds }
2143
- : {}),
2144
- ...(options.rawExcerptsSuppressed
2145
- ? { rawExcerptsSuppressed: options.rawExcerptsSuppressed }
2146
- : {}),
2147
- },
2127
+ return this.accessRecallSurface.buildRecallResponseFromXraySnapshot(
2128
+ options,
2148
2129
  );
2149
- const context = results
2150
- .map((result) => {
2151
- const content =
2152
- typeof result.content === "string" && result.content.length > 0
2153
- ? result.content
2154
- : "";
2155
- return content || result.preview;
2156
- })
2157
- .filter((text) => text.length > 0)
2158
- .join("\n\n");
2159
-
2160
- return {
2161
- query: options.query,
2162
- ...(options.sessionKey ? { sessionKey: options.sessionKey } : {}),
2163
- namespace,
2164
- context,
2165
- count: memoryIds.length,
2166
- memoryIds,
2167
- results,
2168
- recordedAt: snapshotForSerialization.recordedAt,
2169
- traceId: options.snapshot.traceId,
2170
- plannerMode: options.normalizedMode,
2171
- fallbackUsed: sourcesUsed.some((source) => source !== "hybrid"),
2172
- sourcesUsed,
2173
- disclosure: options.disclosure,
2174
- budgetsApplied: snapshotForSerialization.budgetsApplied,
2175
- latencyMs: snapshotForSerialization.latencyMs,
2176
- };
2177
2130
  }
2178
2131
 
2179
2132
  private async serializeRecallResults(
@@ -2214,198 +2167,11 @@ export class EngramAccessService {
2214
2167
  }
2215
2168
  | null = null,
2216
2169
  ): Promise<EngramAccessMemorySummary[]> {
2217
- if (!snapshot) return [];
2218
- const namespace = snapshot.namespace ? this.resolveNamespace(snapshot.namespace) : this.orchestrator.config.defaultNamespace;
2219
- const storage = await this.orchestrator.getStorage(namespace);
2220
- const storageDir = storage.dir;
2221
- const recallNamespaces = Array.from(
2222
- new Set(
2223
- [
2224
- namespace,
2225
- ...(Array.isArray(snapshot.recallNamespaces)
2226
- ? snapshot.recallNamespaces.map((ns) => this.resolveNamespace(ns))
2227
- : []),
2228
- this.orchestrator.config.defaultNamespace,
2229
- this.orchestrator.config.sharedNamespace,
2230
- ...(this.orchestrator.config.namespacePolicies ?? []).map((p) => p.name),
2231
- ].filter((ns): ns is string => typeof ns === "string" && ns.length > 0),
2232
- ),
2170
+ return this.accessRecallSurface.serializeRecallResults(
2171
+ snapshot,
2172
+ disclosure,
2173
+ rawContext,
2233
2174
  );
2234
- const results: EngramAccessMemorySummary[] = [];
2235
- const seen = new Set<string>();
2236
- const collectionNamespaceFromPrefix = (collectionPrefix: string): string | null => {
2237
- const baseCollection = this.orchestrator.config.qmdCollection;
2238
- if (collectionPrefix === baseCollection) return this.orchestrator.config.defaultNamespace;
2239
- const namespaceSuffix = collectionPrefix.startsWith(`${baseCollection}--`)
2240
- ? collectionPrefix.slice(baseCollection.length + 2)
2241
- : "";
2242
- if (!namespaceSuffix) return null;
2243
-
2244
- const decoded = namespaceIdentityFromToken(namespaceSuffix);
2245
- if (decoded !== null) return decoded || this.orchestrator.config.defaultNamespace;
2246
- if (namespaceSuffix.startsWith("ns--")) {
2247
- const legacyNamespace = namespaceSuffix.slice("ns--".length).trim();
2248
- return legacyNamespace || null;
2249
- }
2250
- return null;
2251
- };
2252
- const readResultPath = async (
2253
- memoryPath: string,
2254
- ): Promise<{ memory: MemoryFile; baseDir: string } | null> => {
2255
- const parts = qmdCollectionPathParts(memoryPath);
2256
- const coldCollection =
2257
- this.orchestrator.config.qmdColdCollection ?? "openclaw-engram-cold";
2258
- if (parts && parts.collection === coldCollection) {
2259
- const storages: Array<{ storage: StorageManager; dir: string }> = [];
2260
- const seenStorageDirs = new Set<string>();
2261
- const addStorage = (candidateStorage: StorageManager): void => {
2262
- const candidateDir = nodePath.resolve(candidateStorage.dir);
2263
- if (seenStorageDirs.has(candidateDir)) return;
2264
- seenStorageDirs.add(candidateDir);
2265
- storages.push({ storage: candidateStorage, dir: candidateDir });
2266
- };
2267
- addStorage(storage);
2268
- for (const recallNamespace of recallNamespaces) {
2269
- try {
2270
- addStorage(await this.orchestrator.getStorage(recallNamespace));
2271
- } catch {
2272
- continue;
2273
- }
2274
- }
2275
- for (const candidateStorage of storages) {
2276
- try {
2277
- const coldRoot = nodePath.join(candidateStorage.dir, "cold");
2278
- for (const candidatePath of qmdResultPathCandidates(
2279
- coldRoot,
2280
- parts.relativePath,
2281
- )) {
2282
- const memory =
2283
- await candidateStorage.storage.readMemoryByPath(candidatePath);
2284
- if (memory) return { memory, baseDir: candidateStorage.dir };
2285
- }
2286
- } catch (err) {
2287
- if (err instanceof SecureStoreLockedError) throw err;
2288
- continue;
2289
- }
2290
- }
2291
- return null;
2292
- }
2293
-
2294
- const collectionNamespace = parts
2295
- ? collectionNamespaceFromPrefix(parts.collection)
2296
- : null;
2297
-
2298
- if (parts && collectionNamespace) {
2299
- try {
2300
- const collectionStorage =
2301
- await this.orchestrator.getStorage(collectionNamespace);
2302
- for (const candidate of qmdResultPathCandidates(
2303
- collectionStorage.dir,
2304
- parts.relativePath,
2305
- )) {
2306
- const memory = await collectionStorage.readMemoryByPath(candidate);
2307
- if (memory) return { memory, baseDir: collectionStorage.dir };
2308
- }
2309
- return null;
2310
- } catch (err) {
2311
- if (err instanceof SecureStoreLockedError) throw err;
2312
- return null;
2313
- }
2314
- }
2315
-
2316
- if (nodePath.isAbsolute(memoryPath)) {
2317
- const ownerStorage = await this.storageForAbsoluteRecallPath(
2318
- memoryPath,
2319
- namespace,
2320
- recallNamespaces,
2321
- );
2322
- if (!ownerStorage) return null;
2323
- for (const candidate of qmdResultPathCandidates(
2324
- ownerStorage.dir,
2325
- memoryPath,
2326
- )) {
2327
- const memory = await ownerStorage.storage.readMemoryByPath(candidate);
2328
- if (memory) return { memory, baseDir: ownerStorage.dir };
2329
- }
2330
- return null;
2331
- }
2332
-
2333
- for (const candidate of qmdResultPathCandidates(storageDir, memoryPath)) {
2334
- const memory = await storage.readMemoryByPath(candidate);
2335
- if (memory) return { memory, baseDir: storageDir };
2336
- }
2337
- return null;
2338
- };
2339
-
2340
- // Pre-fetch raw excerpts once when `disclosure === "raw"` so we don't
2341
- // hit the LCM archive per-result (issue #677 PR 2/4). Excerpts are
2342
- // attached to the first result; per-result attribution is reserved
2343
- // for a future PR if/when the LCM index can be joined to memory ids.
2344
- // Coerce `null` (non-raw disclosure) to `undefined` so the optional
2345
- // serializer field is never explicitly `null`.
2346
- // Namespace for the LCM `${namespace}:${sessionKey}` prefix: prefer the
2347
- // caller-supplied READ-AUTHORIZATION-GATED `rawExcerptNamespace` (#1505
2348
- // thread 2f7) so raw disclosure honours the same read gate as normal recall
2349
- // + `lcmSearch` and never attaches `<principal>-project-*` overlay rows the
2350
- // gate excludes. Fall back to the snapshot's resolved namespace only when no
2351
- // gated namespace was threaded (sessionless / legacy callers) — unchanged.
2352
- const rawExcerptsResult =
2353
- rawContext?.rawExcerptsSuppressed === true
2354
- ? // Implicit raw recall with NO readable LCM namespace (#1505 thread
2355
- // NBHWz): emit empty excerpts rather than falling back to the
2356
- // write/overlay `namespace` the read gate excludes.
2357
- []
2358
- : await this.fetchRawExcerpts(
2359
- disclosure,
2360
- rawContext
2361
- ? {
2362
- query: rawContext.query,
2363
- ...(rawContext.sessionKey
2364
- ? { sessionKey: rawContext.sessionKey }
2365
- : {}),
2366
- namespace: rawContext.rawExcerptNamespace ?? namespace,
2367
- ...(rawContext.rawExcerptSessionIds !== undefined
2368
- ? { lcmSessionIds: rawContext.rawExcerptSessionIds }
2369
- : {}),
2370
- }
2371
- : null,
2372
- );
2373
- const rawExcerpts = rawExcerptsResult ?? undefined;
2374
-
2375
- for (const memoryPath of snapshot.resultPaths ?? []) {
2376
- if (!memoryPath || seen.has(memoryPath)) continue;
2377
- const resolved = await readResultPath(memoryPath);
2378
- if (!resolved) continue;
2379
- const { memory, baseDir } = resolved;
2380
- seen.add(memoryPath);
2381
- results.push(
2382
- this.serializeMemorySummary(
2383
- memory,
2384
- baseDir,
2385
- disclosure,
2386
- // Attach the (possibly empty) raw excerpts to the first raw
2387
- // result; subsequent results do not duplicate the array.
2388
- results.length === 0 ? rawExcerpts : undefined,
2389
- ),
2390
- );
2391
- }
2392
-
2393
- if (results.length > 0) return results;
2394
-
2395
- for (const memoryId of snapshot.memoryIds) {
2396
- const memory = await storage.getMemoryById(memoryId);
2397
- if (!memory || seen.has(memory.path)) continue;
2398
- seen.add(memory.path);
2399
- results.push(
2400
- this.serializeMemorySummary(
2401
- memory,
2402
- storageDir,
2403
- disclosure,
2404
- results.length === 0 ? rawExcerpts : undefined,
2405
- ),
2406
- );
2407
- }
2408
- return results;
2409
2175
  }
2410
2176
 
2411
2177
  private async storageForAbsoluteRecallPath(
@@ -2460,19 +2226,6 @@ export class EngramAccessService {
2460
2226
  return matches[0] ?? null;
2461
2227
  }
2462
2228
 
2463
- /**
2464
- * Fetch raw transcript excerpts from the LCM archive for `disclosure ===
2465
- * "raw"` recalls (issue #677 PR 2/4). Returns `null` for non-raw recall
2466
- * depths, an empty array when LCM is disabled / not initialized / has no
2467
- * matches, and an array of LCM-side excerpts otherwise. Errors are
2468
- * swallowed and treated as "no excerpts" so a failing LCM never breaks
2469
- * the recall response.
2470
- *
2471
- * Namespace handling: LCM archival prefixes non-default-namespace
2472
- * sessions with `${namespace}:${sessionKey}` (see `observe()` around
2473
- * line 2498), so the lookup must mirror that prefix or raw recalls in
2474
- * non-default namespaces miss their own excerpts.
2475
- */
2476
2229
  private async fetchRawExcerpts(
2477
2230
  disclosure: RecallDisclosure,
2478
2231
  context: {
@@ -2493,63 +2246,10 @@ export class EngramAccessService {
2493
2246
  lcmSessionIds?: string[];
2494
2247
  } | null,
2495
2248
  ): Promise<EngramAccessMemorySummary["rawExcerpts"] | null> {
2496
- if (disclosure !== "raw") return null;
2497
- if (!context || !context.query) return [];
2498
- // Privacy guard: raw disclosure must be session-scoped. Without a
2499
- // sessionKey, `lcm.searchContextFull(query, n, undefined)` searches
2500
- // across every archived session in the LCM store and would return
2501
- // excerpts from unrelated sessions (potentially crossing namespaces
2502
- // via their `${namespace}:${sessionKey}` prefix encoding). Treat a
2503
- // missing sessionKey as "no excerpts" — callers asking for raw
2504
- // disclosure outside a session get an empty list, not a leak.
2505
- if (!context.sessionKey) return [];
2506
- const lcm = this.orchestrator.lcmEngine;
2507
- if (!lcm || !lcm.enabled) return [];
2508
- try {
2509
- const legacyKey =
2510
- context.namespace &&
2511
- context.namespace !== this.orchestrator.config.defaultNamespace
2512
- ? `${context.namespace}:${context.sessionKey}`
2513
- : context.sessionKey;
2514
- const lcmSessionIds =
2515
- context.lcmSessionIds !== undefined
2516
- ? context.lcmSessionIds
2517
- : [legacyKey];
2518
- // Cap the excerpt fanout so recall responses stay bounded. Five matches
2519
- // is enough to anchor the model in the raw transcript without ballooning
2520
- // token spend; raw is meant as the escape hatch, not the default. The cap
2521
- // is applied across the MERGED result set so adding fallback keys never
2522
- // inflates the excerpt budget.
2523
- const limit = 5;
2524
- const seenRows = new Set<string>();
2525
- const excerpts: NonNullable<EngramAccessMemorySummary["rawExcerpts"]> = [];
2526
- const settledRows = await Promise.allSettled(
2527
- lcmSessionIds.map(async (lcmSessionKey) =>
2528
- lcm.searchContextFull(context.query, limit, lcmSessionKey),
2529
- ),
2530
- );
2531
- for (const result of settledRows) {
2532
- if (excerpts.length >= limit) break;
2533
- if (result.status !== "fulfilled") continue;
2534
- for (const r of result.value) {
2535
- const dedupeKey = `${r.session_id} ${r.turn_index}`;
2536
- if (seenRows.has(dedupeKey)) continue;
2537
- seenRows.add(dedupeKey);
2538
- excerpts.push({
2539
- turnIndex: r.turn_index,
2540
- role: r.role,
2541
- content: r.content,
2542
- sessionId: r.session_id,
2543
- });
2544
- if (excerpts.length >= limit) break;
2545
- }
2546
- }
2547
- return excerpts;
2548
- } catch {
2549
- // CLAUDE.md rule 13: never let an external subsystem (LCM/SQLite)
2550
- // crash the primary recall flow.
2551
- return [];
2552
- }
2249
+ return this.accessRecallSurface.fetchRawExcerpts(
2250
+ disclosure,
2251
+ context,
2252
+ );
2553
2253
  }
2554
2254
 
2555
2255
  private async handleIdempotentWrite<T extends { idempotencyReplay?: boolean }>(options: {
@@ -3267,453 +2967,9 @@ export class EngramAccessService {
3267
2967
  response: EngramAccessRecallResponse;
3268
2968
  budgetRecordPrincipal: string | null;
3269
2969
  }> {
3270
- const query = request.query;
3271
- // Disclosure depth (issue #677). Default to `"chunk"` when omitted so
3272
- // pre-#677 callers see unchanged behavior. Reject explicitly invalid
3273
- // string values per CLAUDE.md rule 51 (do not silently fall back).
3274
- const callerProvidedDisclosure =
3275
- request.disclosure !== undefined && request.disclosure !== null;
3276
- const requestedDisclosure: RecallDisclosure = (() => {
3277
- if (!callerProvidedDisclosure) {
3278
- return DEFAULT_RECALL_DISCLOSURE;
3279
- }
3280
- if (!isRecallDisclosure(request.disclosure)) {
3281
- throw new EngramAccessInputError(
3282
- `disclosure must be one of: chunk, section, raw (got: ${String(request.disclosure)})`,
3283
- );
3284
- }
3285
- return request.disclosure;
3286
- })();
3287
- // Attach any coding context shipped with the recall request BEFORE
3288
- // namespace resolution so the overlay applies to this recall (issue #569).
3289
- if (request.codingContext !== undefined && request.sessionKey) {
3290
- this.setCodingContext({
3291
- sessionKey: request.sessionKey,
3292
- codingContext: request.codingContext,
3293
- });
3294
- }
3295
- // Auto-resolve coding context from cwd/projectTag when no explicit
3296
- // codingContext was supplied (issue #569 wiring). This allows Claude
3297
- // Code hooks and OpenClaw connectors to get project-scoped memory
3298
- // transparently.
3299
- if (request.codingContext === undefined && request.sessionKey) {
3300
- await this.maybeAttachCodingContext(request.sessionKey, {
3301
- cwd: request.cwd,
3302
- projectTag: request.projectTag,
3303
- });
3304
- }
3305
- const authenticatedPrincipal = request.authenticatedPrincipal?.trim();
3306
- const namespaceOverride = this.resolveRecallNamespace(
3307
- request.namespace,
3308
- request.sessionKey,
3309
- authenticatedPrincipal,
2970
+ return this.accessRecallSurface.executeRecall(
2971
+ request,
3310
2972
  );
3311
- const namespace = namespaceOverride ?? this.orchestrator.config.defaultNamespace;
3312
- // Normalize mode early so that no_recall / invalid modes skip budget
3313
- // accounting (Codex P1: budget recorded before mode validation).
3314
- const mode = this.normalizeRecallMode(request.mode);
3315
- const maybePrincipal = this.resolveRequestPrincipal(request.sessionKey, authenticatedPrincipal);
3316
- if (resolveNamespaceCapabilities(this.orchestrator.config).namespaces && !maybePrincipal) {
3317
- throw new EngramAccessInputError(
3318
- "authentication required: namespaces are enabled and no principal was supplied",
3319
- );
3320
- }
3321
- const principal = maybePrincipal ?? "default";
3322
- const principalNamespace = defaultNamespaceForPrincipal(principal, this.orchestrator.config);
3323
- const profileCodingContext =
3324
- request.sessionKey && typeof this.orchestrator.getCodingContextForSession === "function"
3325
- ? this.orchestrator.getCodingContextForSession(request.sessionKey)
3326
- : null;
3327
- const profileCodingOverlay =
3328
- !namespaceOverride &&
3329
- profileCodingContext &&
3330
- resolveNamespaceCapabilities(this.orchestrator.config).namespaces &&
3331
- this.orchestrator.config.codingMode?.projectScope
3332
- ? resolveCodingNamespaceOverlay(
3333
- profileCodingContext,
3334
- this.orchestrator.config.codingMode,
3335
- this.orchestrator.config.defaultNamespace,
3336
- )
3337
- : null;
3338
- const profilePlan = namespaceOverride
3339
- ? null
3340
- : resolveScopeProfilePlan({
3341
- config: this.orchestrator.config,
3342
- principal,
3343
- codingContext: profileCodingContext,
3344
- codingOverlay: profileCodingOverlay,
3345
- });
3346
- // Skip budget checks for modes that never perform a cross-namespace read.
3347
- const modeSkipsBudget = mode === "no_recall";
3348
- // Derive the full set of namespaces the orchestrator will actually search.
3349
- // When no explicit override is provided, `recallNamespacesForPrincipal()` may
3350
- // expand to shared / policy-default namespaces. Budget must be checked
3351
- // against every cross-namespace entry in the effective set so that omitting
3352
- // `namespace` cannot bypass the limiter (Cursor/Codex review feedback).
3353
- //
3354
- const legacyRecallNamespaces = Array.isArray(this.orchestrator.config.defaultRecallNamespaces)
3355
- ? recallNamespacesForPrincipal(principal, this.orchestrator.config)
3356
- : [];
3357
- const effectiveNamespaces = namespaceOverride
3358
- ? [namespaceOverride]
3359
- : profilePlan
3360
- ? expandScopeProfileReadNamespaces({
3361
- profilePlan,
3362
- principalSelfNamespace: profilePlan.baseNamespace,
3363
- config: this.orchestrator.config,
3364
- principal,
3365
- codingOverlay: profileCodingOverlay,
3366
- legacyRecallNamespaces,
3367
- })
3368
- : legacyRecallNamespaces;
3369
- const budgetPrincipalNamespace = profilePlan?.baseNamespace ?? principalNamespace;
3370
- let budgetDecision: BudgetDecision;
3371
- let recordBudgetAfterSuccess = false;
3372
- if (modeSkipsBudget) {
3373
- budgetDecision = {
3374
- allowed: true as const,
3375
- reason: "allowed-same-namespace" as const,
3376
- count: 0,
3377
- limit: {
3378
- softLimit: this.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
3379
- hardLimit: this.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
3380
- windowMs: this.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 60_000,
3381
- },
3382
- };
3383
- } else {
3384
- // Peek at every effective namespace to determine whether ANY would be
3385
- // cross-namespace WITHOUT recording side effects (Cursor review:
3386
- // multi-count bug). Record a single budget event only when at least
3387
- // one effective namespace differs from the principal's self namespace,
3388
- // and only after recall succeeds so retried transient failures do not
3389
- // consume budget multiple times before a successful response can be
3390
- // cached behind the request idempotency key.
3391
- let anyCrossNamespace = false;
3392
- let denied: BudgetDecision | null = null;
3393
- let crossNamespaceDecision: BudgetDecision | null = null;
3394
- for (const ns of effectiveNamespaces) {
3395
- const peek = this.budget.peek({
3396
- principal,
3397
- principalNamespace: budgetPrincipalNamespace,
3398
- queryNamespace: ns,
3399
- });
3400
- if (peek.reason !== "allowed-same-namespace") {
3401
- anyCrossNamespace = true;
3402
- crossNamespaceDecision ??= peek;
3403
- }
3404
- if (!peek.allowed) {
3405
- denied = peek;
3406
- break;
3407
- }
3408
- }
3409
- if (denied) {
3410
- // The peek projected a denial — deny without recording so the
3411
- // bucket is not inflated by rejected attempts.
3412
- budgetDecision = denied;
3413
- } else if (anyCrossNamespace) {
3414
- budgetDecision = crossNamespaceDecision ?? {
3415
- allowed: true as const,
3416
- reason: "allowed-under-soft" as const,
3417
- count: 0,
3418
- limit: {
3419
- softLimit: this.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
3420
- hardLimit: this.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
3421
- windowMs: this.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 60_000,
3422
- },
3423
- };
3424
- recordBudgetAfterSuccess = true;
3425
- } else {
3426
- budgetDecision = {
3427
- allowed: true as const,
3428
- reason: "allowed-same-namespace" as const,
3429
- count: 0,
3430
- limit: {
3431
- softLimit: this.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
3432
- hardLimit: this.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
3433
- windowMs: this.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 60_000,
3434
- },
3435
- };
3436
- }
3437
- if (!budgetDecision.allowed) {
3438
- throw new EngramAccessInputError(
3439
- `recall denied: cross-namespace budget exceeded (${budgetDecision.count}/${budgetDecision.limit.hardLimit} in ${budgetDecision.limit.windowMs}ms window)`,
3440
- );
3441
- }
3442
- // Prune expired principal buckets to prevent unbounded Map growth from
3443
- // high-cardinality / transient principals (Codex P2 review feedback).
3444
- this.budget.gc();
3445
- }
3446
- const topK = Number.isFinite(request.topK) ? Math.max(0, Math.floor(request.topK ?? 0)) : undefined;
3447
- // Issue #680 — historical recall pin. Validate at the input
3448
- // boundary so a malformed `asOf` is rejected with a structured
3449
- // 400 instead of silently flooring at NaN inside the orchestrator
3450
- // (CLAUDE.md rule 51, gotcha #51). Empty / undefined is fine —
3451
- // means "no pin".
3452
- let asOf: string | undefined;
3453
- if (request.asOf !== undefined && request.asOf !== null) {
3454
- if (typeof request.asOf !== "string" || request.asOf.trim().length === 0) {
3455
- throw new EngramAccessInputError(
3456
- "asOf must be a non-empty ISO 8601 timestamp string",
3457
- );
3458
- }
3459
- const parsed = Date.parse(request.asOf);
3460
- if (!Number.isFinite(parsed)) {
3461
- throw new EngramAccessInputError(
3462
- `asOf must be a parseable ISO 8601 timestamp (got: "${request.asOf}")`,
3463
- );
3464
- }
3465
- asOf = request.asOf;
3466
- }
3467
- const recallOptions: RecallInvocationOptions = {
3468
- namespace: namespaceOverride,
3469
- topK,
3470
- mode,
3471
- ...(authenticatedPrincipal ? { principalOverride: authenticatedPrincipal } : {}),
3472
- ...(asOf !== undefined ? { asOf } : {}),
3473
- ...(request.includeLowConfidence === true ? { includeLowConfidence: true } : {}),
3474
- };
3475
- const startedAt = Date.now();
3476
- const context = await this.orchestrator.recall(query, request.sessionKey, recallOptions);
3477
- const snapshot = request.sessionKey
3478
- ? this.orchestrator.lastRecall.get(request.sessionKey)
3479
- : null;
3480
- const effectiveNamespace = snapshot?.namespace
3481
- ? this.resolveNamespace(snapshot.namespace)
3482
- : namespace;
3483
- // Auto-escalation policy (issue #677 PR 4/4). When the operator
3484
- // configured `recallDisclosureEscalation: "auto"` AND the caller
3485
- // did not explicitly choose a disclosure level AND recall produced
3486
- // a low-confidence result set (proxied by fill ratio: results
3487
- // returned / topK requested), we escalate the default `chunk`
3488
- // shape to `section` so the LLM gets richer context to compensate
3489
- // for ambiguous retrieval. Manual mode and explicit caller
3490
- // disclosure both bypass the policy. Documented in
3491
- // `recall-disclosure-escalation.ts` and unit-tested there.
3492
- // Confidence-proxy denominator: priority order is
3493
- // 1. `snapshot.budgetsApplied.appliedTopK` (ALWAYS wins) — this is
3494
- // the limit the orchestrator actually applied after planner /
3495
- // minimal-mode / section-cap narrowing. Codex P1 rounds 2+3
3496
- // on #705 emphasize that even a caller's explicit `request.topK`
3497
- // is wrong when the orchestrator caps below it (e.g. topK=50
3498
- // but appliedTopK=3 makes a 2-hit recall actually 0.67, not
3499
- // 0.04).
3500
- // 2. The caller's explicit `topK` when the snapshot lacks
3501
- // `budgetsApplied` (early-return paths, error cases).
3502
- // 3. Config `qmdMaxResults` as a last-resort fallback.
3503
- // Floor at observed-results so the ratio stays in [0, 1] even if
3504
- // any of the signals drifts below the actual hit count.
3505
- const resultsReturned = snapshot?.memoryIds?.length ?? 0;
3506
- const appliedTopK = snapshot?.budgetsApplied?.appliedTopK;
3507
- const configMaxResults =
3508
- typeof this.orchestrator.config.qmdMaxResults === "number" &&
3509
- Number.isFinite(this.orchestrator.config.qmdMaxResults) &&
3510
- this.orchestrator.config.qmdMaxResults > 0
3511
- ? this.orchestrator.config.qmdMaxResults
3512
- : 0;
3513
- const topKDenominator =
3514
- typeof appliedTopK === "number" &&
3515
- Number.isFinite(appliedTopK) &&
3516
- appliedTopK > 0
3517
- ? Math.max(appliedTopK, resultsReturned)
3518
- : typeof topK === "number" && topK > 0
3519
- ? Math.max(topK, resultsReturned)
3520
- : Math.max(configMaxResults, resultsReturned, 1);
3521
- // When the recall produced no snapshot (sessionless / namespace
3522
- // mismatch / early-return path), there is no confidence signal to
3523
- // base escalation on. Pass `undefined` so the helper takes its
3524
- // `no-top-k-confidence` branch instead of computing 0/N=0 and
3525
- // forcing auto-escalation on every sessionless caller (Codex P2
3526
- // review on PR #705).
3527
- const topKConfidence =
3528
- snapshot && topKDenominator > 0
3529
- ? Math.min(1, resultsReturned / topKDenominator)
3530
- : undefined;
3531
- const escalationDecision = decideDisclosureEscalation({
3532
- mode: this.orchestrator.config.recallDisclosureEscalation,
3533
- threshold: this.orchestrator.config.recallDisclosureEscalationThreshold,
3534
- originalDisclosure: requestedDisclosure,
3535
- callerProvidedDisclosure,
3536
- topKConfidence,
3537
- });
3538
- const disclosure = escalationDecision.effective;
3539
- // Gate the raw-excerpt LCM read with the SAME read-authorization namespace
3540
- // `lcmSearch` + the in-prompt LCM sections use (#1505 thread 2f7), so
3541
- // `disclosure: "raw"` never attaches `<principal>-project-*` overlay rows
3542
- // when the principal can WRITE but not READ its self base (or
3543
- // `defaultRecallNamespaces` omits `self`). Computed ONLY for raw disclosure:
3544
- // it is the sole consumer, and resolving the overlay on every chunk/section
3545
- // recall would be wasted work — keeping non-raw recall byte-for-byte
3546
- // unchanged.
3547
- // Trim the sessionKey to match what `orchestrator.recall(...)` already does
3548
- // (`request.sessionKey?.trim() || undefined`) and what the x-ray raw-excerpt
3549
- // path uses (cursor "Raw excerpt key not trimmed"). A whitespace-padded key
3550
- // otherwise drives recall under one identity but resolves the raw-excerpt
3551
- // overlay namespace + LCM `session_id` under a DIFFERENT (untrimmed) prefix,
3552
- // so excerpts are gated/queried inconsistently with recall and the x-ray path.
3553
- const trimmedSessionKey = request.sessionKey?.trim() || undefined;
3554
- const rawExcerptNamespace =
3555
- disclosure === "raw"
3556
- ? this.resolveRawExcerptReadNamespace(
3557
- request.namespace,
3558
- trimmedSessionKey,
3559
- authenticatedPrincipal,
3560
- )
3561
- : undefined;
3562
- // `undefined` for an IMPLICIT raw recall means NO readable LCM namespace
3563
- // exists (restrictive `default` READ policy, no readable overlay/self) —
3564
- // suppress excerpts rather than fall back to the write/overlay namespace the
3565
- // read gate excludes (#1505 thread NBHWz). An EXPLICIT namespace always
3566
- // resolves (or throws) above, so suppression only applies to the implicit
3567
- // path.
3568
- const hasExplicitNamespace =
3569
- typeof request.namespace === "string" &&
3570
- request.namespace.trim().length > 0;
3571
- const rawExcerptsSuppressed =
3572
- disclosure === "raw" &&
3573
- !hasExplicitNamespace &&
3574
- rawExcerptNamespace === undefined;
3575
- // Ordered, read-authorized LCM read key SET (#1505 fallback unification) so
3576
- // raw disclosure finds excerpts a branch-scoped session archived at
3577
- // project/root scope — exactly as recall + `lcmSearch` do. Only with a
3578
- // concrete sessionKey; already read-gated.
3579
- const rawExcerptSessionIds =
3580
- disclosure === "raw" && rawExcerptNamespace && trimmedSessionKey
3581
- ? this.resolveLcmReadSessionIds(
3582
- request.namespace,
3583
- rawExcerptNamespace,
3584
- trimmedSessionKey,
3585
- authenticatedPrincipal,
3586
- )
3587
- : undefined;
3588
- let results = await this.serializeRecallResults(snapshot, disclosure, {
3589
- query,
3590
- sessionKey: trimmedSessionKey,
3591
- ...(rawExcerptNamespace ? { rawExcerptNamespace } : {}),
3592
- ...(rawExcerptSessionIds !== undefined ? { rawExcerptSessionIds } : {}),
3593
- ...(rawExcerptsSuppressed ? { rawExcerptsSuppressed } : {}),
3594
- });
3595
-
3596
- // Tag filter (issue #689). Applied post-recall, post-serialization so
3597
- // the actual frontmatter tags are already loaded onto each result. When
3598
- // `tags` is absent or empty the filter is a no-op; an invalid `tagMatch`
3599
- // throws via `parseTagMatch` (CLAUDE.md rule 51).
3600
- const filterTags = normalizeTags(request.tags);
3601
- let tagMatchMode: TagMatchMode | undefined;
3602
- try {
3603
- tagMatchMode = parseTagMatch(request.tagMatch);
3604
- } catch (err) {
3605
- throw new EngramAccessInputError(
3606
- err instanceof Error ? err.message : String(err),
3607
- );
3608
- }
3609
- let effectiveContext = context;
3610
- if (filterTags && filterTags.length > 0) {
3611
- const beforeIds = results.map((r) => r.id);
3612
- const { results: admitted } = applyTagFilter(results, {
3613
- tags: filterTags,
3614
- tagMatch: tagMatchMode,
3615
- });
3616
- results = admitted;
3617
- // Codex P1: `context` was generated by orchestrator.recall(...)
3618
- // BEFORE the tag filter ran, so it can contain memories that don't
3619
- // match the requested tags. Surfaces consuming `context` (the
3620
- // prompt-injection string) would leak excluded content into the
3621
- // LLM. When the filter actually drops any result, rebuild context
3622
- // from the admitted set so excluded content is unreachable through
3623
- // any field of the response. The rebuilt context concatenates each
3624
- // admitted result's available text (full content at section/raw
3625
- // disclosure, otherwise the preview) — a different wire format
3626
- // than the orchestrator's native context, but a strict subset
3627
- // safe to inject.
3628
- const admittedIds = new Set(results.map((r) => r.id));
3629
- const droppedAny = beforeIds.some((id) => !admittedIds.has(id));
3630
- if (droppedAny) {
3631
- effectiveContext = results
3632
- .map((r) => {
3633
- const content =
3634
- typeof (r as { content?: unknown }).content === "string"
3635
- ? ((r as { content?: string }).content ?? "")
3636
- : "";
3637
- const preview =
3638
- typeof (r as { preview?: unknown }).preview === "string"
3639
- ? ((r as { preview?: string }).preview ?? "")
3640
- : "";
3641
- return content || preview;
3642
- })
3643
- .filter((s) => s.length > 0)
3644
- .join("\n\n");
3645
- }
3646
- }
3647
- const filteredMemoryIds = filterTags && filterTags.length > 0
3648
- ? results.map((r) => r.id)
3649
- : (snapshot?.memoryIds ?? []);
3650
- const debug = await this.buildRecallDebug(
3651
- snapshot,
3652
- effectiveNamespace,
3653
- request.includeDebug === true,
3654
- request.sessionKey,
3655
- );
3656
-
3657
- // Fire-and-forget audit recording. Must never block or crash recall.
3658
- let auditAnomalies: AccessAuditResult["anomalies"] | undefined;
3659
- if (this.auditAdapter) {
3660
- try {
3661
- const resolvedAgentId = principal ?? "__anonymous__";
3662
- const auditEntry = {
3663
- ts: new Date().toISOString(),
3664
- sessionKey: request.sessionKey ?? "",
3665
- agentId: resolvedAgentId,
3666
- trigger: "access-surface",
3667
- queryText: query,
3668
- candidateMemoryIds: snapshot?.memoryIds ?? [],
3669
- // Audit must reflect what was actually injected, not what
3670
- // recall produced before the tag filter. Using `context`
3671
- // (pre-filter) overstates injectedChars and can leak content
3672
- // from excluded memories into the audit summary (cursor
3673
- // Medium on PR #712).
3674
- summary: effectiveContext.slice(0, 200) || null,
3675
- injectedChars: effectiveContext.length,
3676
- toggleState: "enabled" as const,
3677
- latencyMs: Date.now() - startedAt,
3678
- plannerMode: snapshot?.plannerMode ?? mode,
3679
- requestedMode: mode,
3680
- fallbackUsed: snapshot?.fallbackUsed ?? false,
3681
- };
3682
- const auditResult = await this.auditAdapter.record(
3683
- resolvedAgentId || "__anonymous__",
3684
- auditEntry,
3685
- );
3686
- auditAnomalies = auditResult.anomalies;
3687
- } catch {
3688
- // Audit failures must never crash the recall path.
3689
- }
3690
- }
3691
-
3692
- return {
3693
- response: {
3694
- query,
3695
- sessionKey: request.sessionKey,
3696
- namespace: effectiveNamespace,
3697
- context: effectiveContext,
3698
- count: filterTags && filterTags.length > 0
3699
- ? results.length
3700
- : (snapshot?.memoryIds.length ?? results.length),
3701
- memoryIds: filteredMemoryIds,
3702
- results,
3703
- recordedAt: snapshot?.recordedAt,
3704
- traceId: snapshot?.traceId,
3705
- plannerMode: snapshot?.plannerMode ?? mode,
3706
- fallbackUsed: snapshot?.fallbackUsed ?? false,
3707
- sourcesUsed: snapshot?.sourcesUsed ?? [],
3708
- disclosure,
3709
- budgetsApplied: snapshot?.budgetsApplied,
3710
- auditAnomalies,
3711
- budgetWarning: budgetDecision.reason === "warn-over-soft" ? budgetDecision : undefined,
3712
- latencyMs: snapshot?.latencyMs ?? (Date.now() - startedAt),
3713
- debug,
3714
- },
3715
- budgetRecordPrincipal: recordBudgetAfterSuccess ? principal : null,
3716
- };
3717
2973
  }
3718
2974
 
3719
2975
  async recallExplain(
@@ -3814,15 +3070,6 @@ export class EngramAccessService {
3814
3070
  return toRecallExplainJson(snapshot);
3815
3071
  }
3816
3072
 
3817
- /**
3818
- * Recall X-ray (issue #570). Runs a recall with `xrayCapture: true`
3819
- * and returns the resulting snapshot as structured JSON so every
3820
- * surface (CLI / HTTP / MCP) gets the same payload. Namespace scope
3821
- * is enforced before the recall fires (CLAUDE.md rule 42 — read and
3822
- * write paths must resolve through the same namespace layer) so an
3823
- * unauthorized principal cannot capture an x-ray for a namespace it
3824
- * cannot read.
3825
- */
3826
3073
  async recallXray(request: {
3827
3074
  query: string;
3828
3075
  sessionKey?: string;
@@ -3867,433 +3114,9 @@ export class EngramAccessService {
3867
3114
  snapshot?: RecallXraySnapshot;
3868
3115
  recall?: EngramAccessRecallResponse;
3869
3116
  }> {
3870
- const query = typeof request.query === "string" ? request.query : "";
3871
- if (query.trim().length === 0) {
3872
- // Match the CLI contract (CLAUDE.md rule 51): reject empty
3873
- // input with an explicit error rather than silently producing
3874
- // an empty snapshot.
3875
- throw new Error("recallXray: query is required and must be non-empty");
3876
- }
3877
- // Validate disclosure UP FRONT — before recall executes, before
3878
- // the xray queue mutex is acquired, before namespace resolution.
3879
- // A bad value should fail fast rather than after we've burned
3880
- // cycles on an irreversible recall (Cursor Medium review on PR
3881
- // #699).
3882
- if (
3883
- request.disclosure !== undefined &&
3884
- !isRecallDisclosure(request.disclosure)
3885
- ) {
3886
- throw new EngramAccessInputError(
3887
- `recallXray: disclosure must be one of: chunk, section, raw (got: ${String(request.disclosure)})`,
3888
- );
3889
- }
3890
-
3891
- const namespacesEnabled = resolveNamespaceCapabilities(this.orchestrator.config).namespaces;
3892
- const requestedNamespace = request.namespace?.trim()
3893
- ? this.resolveNamespace(request.namespace)
3894
- : undefined;
3895
- const authenticatedPrincipal = request.authenticatedPrincipal?.trim();
3896
- const principal =
3897
- authenticatedPrincipal
3898
- || resolvePrincipal(request.sessionKey, this.orchestrator.config);
3899
-
3900
- if (requestedNamespace) {
3901
- if (
3902
- !canReadNamespace(
3903
- principal,
3904
- requestedNamespace,
3905
- this.orchestrator.config,
3906
- )
3907
- ) {
3908
- return { snapshotFound: false };
3909
- }
3910
- } else if (
3911
- namespacesEnabled
3912
- && !authenticatedPrincipal
3913
- && !request.sessionKey?.trim()
3914
- ) {
3915
- // Namespaces enabled but no identity supplied — reject rather
3916
- // than scanning the global namespace (CLAUDE.md rule 48:
3917
- // least-privileged default).
3918
- return { snapshotFound: false };
3919
- }
3920
-
3921
- // Optional `--budget` override must be a positive integer. Invalid
3922
- // values throw rather than silently defaulting (CLAUDE.md rule 51).
3923
- let budgetOverride: number | undefined;
3924
- if (request.budget !== undefined && request.budget !== null) {
3925
- const parsed =
3926
- typeof request.budget === "number"
3927
- ? request.budget
3928
- : Number(request.budget);
3929
- if (
3930
- !Number.isFinite(parsed)
3931
- || parsed <= 0
3932
- || !Number.isInteger(parsed)
3933
- ) {
3934
- throw new Error(
3935
- `recallXray: budget expects a positive integer; got ${JSON.stringify(request.budget)}`,
3936
- );
3937
- }
3938
- budgetOverride = parsed;
3939
- }
3940
- const mode = this.normalizeRecallMode(request.mode);
3941
- const disclosure = request.disclosure ?? DEFAULT_RECALL_DISCLOSURE;
3942
-
3943
- // Serialize x-ray invocations behind a per-service mutex so the
3944
- // per-process `getLastXraySnapshot()` slot cannot be clobbered by
3945
- // a concurrent capturing call before this caller reads it back.
3946
- // Budget and principal are now threaded through
3947
- // `RecallInvocationOptions`, so global config mutation is gone
3948
- // (CLAUDE.md rule 47: no shared mutable state across async
3949
- // boundaries). The mutex stays only for the snapshot-slot
3950
- // ordering guarantee.
3951
- const previousQueue = this.xrayQueue;
3952
- let release: () => void = () => {};
3953
- this.xrayQueue = new Promise<void>((resolve) => {
3954
- release = resolve;
3955
- });
3956
- await previousQueue;
3957
- const recallStartedAt = Date.now();
3958
-
3959
- const recallSessionKey = request.sessionKey?.trim() || undefined;
3960
- let xrayResponse: {
3961
- snapshotFound: boolean;
3962
- snapshot?: RecallXraySnapshot;
3963
- } = { snapshotFound: false };
3964
-
3965
- try {
3966
- // Clear any prior snapshot so a capture failure surfaces as
3967
- // `{snapshotFound: false}` rather than returning stale data
3968
- // from an earlier call on the same orchestrator.
3969
- this.orchestrator.clearLastXraySnapshot();
3970
- await this.orchestrator.recall(query, recallSessionKey, {
3971
- xrayCapture: true,
3972
- ...(requestedNamespace ? { namespace: requestedNamespace } : {}),
3973
- ...(budgetOverride !== undefined
3974
- ? { budgetCharsOverride: budgetOverride }
3975
- : {}),
3976
- ...(mode !== undefined ? { mode } : {}),
3977
- // When the caller supplies an authenticated principal, forward
3978
- // it via the dedicated override channel so orchestrator-side
3979
- // ACL decisions use the SAME principal the access-surface
3980
- // pre-check above authorized. Threading an
3981
- // `authenticatedPrincipal` through `sessionKey` would be wrong:
3982
- // `resolvePrincipal(sessionKey)` only maps configured raw
3983
- // session keys and otherwise collapses to `"default"`, which
3984
- // in namespace-enabled deployments produces false denials /
3985
- // wrong-scope serving despite the pre-check passing
3986
- // (CLAUDE.md rule 42).
3987
- ...(authenticatedPrincipal
3988
- ? { principalOverride: authenticatedPrincipal }
3989
- : {}),
3990
- ...(request.currentContextScopes !== undefined
3991
- ? { currentContextScopes: request.currentContextScopes }
3992
- : {}),
3993
- });
3994
-
3995
- const rawSnapshot = this.orchestrator.getLastXraySnapshot();
3996
- // Re-check namespace after capture: the recall may have served
3997
- // from a different namespace than the caller requested. Drop
3998
- // the snapshot rather than leak cross-tenant data (CLAUDE.md
3999
- // rules 42 + 47). The comparison is strict so a snapshot whose
4000
- // namespace is `undefined` cannot bypass the scope the caller
4001
- // asked for.
4002
- const namespaceMismatch =
4003
- requestedNamespace !== undefined &&
4004
- rawSnapshot?.namespace !== requestedNamespace;
4005
- if (!rawSnapshot) {
4006
- xrayResponse = { snapshotFound: false };
4007
- } else if (namespaceMismatch) {
4008
- xrayResponse = { snapshotFound: false };
4009
- } else {
4010
- // Tag filter (issue #689). Mirrors `recall()` semantics — applied
4011
- // post-capture by reading each result's frontmatter tags and
4012
- // dropping non-matching results. Filter activity surfaces as a
4013
- // `tag-filter` entry in `snapshot.filters` so X-ray consumers can
4014
- // see the "considered → admitted" delta.
4015
- let snapshot = rawSnapshot;
4016
- const xrayFilterTags = normalizeTags(request.tags);
4017
- let xrayTagMatch: TagMatchMode | undefined;
4018
- try {
4019
- xrayTagMatch = parseTagMatch(request.tagMatch);
4020
- } catch (err) {
4021
- throw new EngramAccessInputError(
4022
- err instanceof Error ? err.message : String(err),
4023
- );
4024
- }
4025
- if (xrayFilterTags && xrayFilterTags.length > 0) {
4026
- const namespace = snapshot.namespace
4027
- ? this.resolveNamespace(snapshot.namespace)
4028
- : this.orchestrator.config.defaultNamespace;
4029
- const tagsByIndex = await Promise.all(
4030
- snapshot.results.map(async (result) => {
4031
- try {
4032
- const storage = await this.orchestrator.getStorage(namespace);
4033
- const memory = await storage.readMemoryByPath(result.path);
4034
- const t = memory?.frontmatter?.tags;
4035
- // Normalize identically to the recall path
4036
- // (`normalizeProjectionTags`): trim and drop empty strings
4037
- // so X-ray tag matching stays consistent with the recall
4038
- // surface. Without this, a frontmatter tag like " draft "
4039
- // would match in recall but not in X-ray (cursor review).
4040
- return Array.isArray(t) ? normalizeProjectionTags(t) : [];
4041
- } catch {
4042
- return [];
4043
- }
4044
- }),
4045
- );
4046
- const tagged = snapshot.results.map((result, index) => ({
4047
- result,
4048
- tags: tagsByIndex[index] ?? [],
4049
- }));
4050
- const { results: admittedTagged, trace } = applyTagFilter(tagged, {
4051
- tags: xrayFilterTags,
4052
- tagMatch: xrayTagMatch,
4053
- });
4054
- const admittedResults = admittedTagged.map((entry) => entry.result);
4055
- const filters = trace ? [...snapshot.filters, trace] : snapshot.filters;
4056
- snapshot = { ...snapshot, results: admittedResults, filters };
4057
- }
4058
- // Decorate per-result disclosure + token estimate when the caller
4059
- // wired a depth knob (issue #677 PR 3/4 — codex review on #699
4060
- // flagged that the renderer's per-disclosure summary stays empty
4061
- // until callers populate these fields). Estimate tokens from
4062
- // the actual rendered payload at the requested depth so the
4063
- // summary reflects real spend; chunk uses the preview, section
4064
- // and raw use full content. Best-effort only — a missing
4065
- // memory or read failure is silently dropped (CLAUDE.md rule 13).
4066
- if (request.disclosure !== undefined) {
4067
- // Disclosure already validated up front; pin to the narrowed
4068
- // type here. Re-validation inside the queue would be dead code.
4069
- const disclosure: RecallDisclosure = request.disclosure;
4070
- const namespace = snapshot.namespace
4071
- ? this.resolveNamespace(snapshot.namespace)
4072
- : this.orchestrator.config.defaultNamespace;
4073
- // Pre-fetch raw excerpts ONCE so the first raw-disclosure
4074
- // result's token estimate includes the LCM-side excerpt spend
4075
- // that `shapeMemorySummary` actually attaches in the recall
4076
- // response. Without this, raw recalls systematically
4077
- // undercounted spend on the first result (Cursor Medium review
4078
- // on PR #699). Excerpts are scoped to the same session +
4079
- // namespace as the recall.
4080
- // Trim sessionKey to match what `orchestrator.recall(...)`
4081
- // already does (`request.sessionKey?.trim() || undefined`),
4082
- // otherwise a whitespace-padded key drives recall under one
4083
- // identity but probes LCM under a different prefix and
4084
- // misses stored excerpts (Cursor Low review on PR #699).
4085
- const trimmedSessionKey = request.sessionKey?.trim() || undefined;
4086
- // Read-authorization-gated namespace for the raw-excerpt LCM lookup
4087
- // (#1505 thread 2f7). NOT `snapshot.namespace` (the write/overlay
4088
- // namespace), which would attach `<principal>-project-*` overlay rows
4089
- // when the principal can WRITE but not READ its self base. Mirrors the
4090
- // recall + `lcmSearch` read gate. Resolved ONLY for raw disclosure (its
4091
- // sole consumer); the `namespace` above is still used for the
4092
- // memory-FILE reads below (a separate, snapshot-scoped read), so non-raw
4093
- // x-ray decoration stays byte-for-byte unchanged.
4094
- const rawExcerptNamespace =
4095
- disclosure === "raw"
4096
- ? this.resolveRawExcerptReadNamespace(
4097
- request.namespace,
4098
- trimmedSessionKey,
4099
- authenticatedPrincipal,
4100
- )
4101
- : namespace;
4102
- // `undefined` for an IMPLICIT raw recall means NO readable LCM namespace
4103
- // exists (restrictive `default` READ policy, no readable overlay/self)
4104
- // — suppress excerpts rather than fall back to the write/overlay
4105
- // namespace the read gate excludes (#1505 thread NBHWz).
4106
- const xrayHasExplicitNamespace =
4107
- typeof request.namespace === "string" &&
4108
- request.namespace.trim().length > 0;
4109
- const rawExcerptsSuppressed =
4110
- disclosure === "raw" &&
4111
- !xrayHasExplicitNamespace &&
4112
- rawExcerptNamespace === undefined;
4113
- // Ordered, read-authorized LCM read key SET (#1505 fallback
4114
- // unification) so raw disclosure finds excerpts a branch-scoped session
4115
- // archived at project/root scope — exactly as recall + `lcmSearch` do.
4116
- // Only meaningful with a concrete sessionKey + a readable namespace;
4117
- // already read-gated so no unauthorized overlay key is present.
4118
- const rawExcerptSessionIds =
4119
- disclosure === "raw" && trimmedSessionKey && rawExcerptNamespace
4120
- ? this.resolveLcmReadSessionIds(
4121
- request.namespace,
4122
- rawExcerptNamespace,
4123
- trimmedSessionKey,
4124
- authenticatedPrincipal,
4125
- )
4126
- : undefined;
4127
- const rawExcerpts =
4128
- disclosure === "raw" && !rawExcerptsSuppressed
4129
- ? await this.fetchRawExcerpts(disclosure, {
4130
- query,
4131
- ...(trimmedSessionKey ? { sessionKey: trimmedSessionKey } : {}),
4132
- ...(rawExcerptNamespace
4133
- ? { namespace: rawExcerptNamespace }
4134
- : {}),
4135
- ...(rawExcerptSessionIds !== undefined
4136
- ? { lcmSessionIds: rawExcerptSessionIds }
4137
- : {}),
4138
- })
4139
- : disclosure === "raw"
4140
- ? []
4141
- : null;
4142
- const rawExcerptText =
4143
- rawExcerpts && rawExcerpts.length > 0
4144
- ? rawExcerpts.map((e) => e.content).join("\n")
4145
- : "";
4146
- // Pre-load every memory in parallel so we can:
4147
- // (a) re-attribute raw excerpts to the *first readable* result
4148
- // rather than always to index 0 (Cursor Low review on PR
4149
- // #699: a missing/unreadable result[0] orphaned the excerpt
4150
- // budget); and
4151
- // (b) include the metadata fields `shapeMemorySummary` actually
4152
- // emits at every depth (id, path, category, status, created,
4153
- // updated, tags, entityRef) in the token estimate, so the
4154
- // summary reflects real spend rather than only payload-body
4155
- // spend (Cursor Low review on PR #699).
4156
- const memoryByIndex = await Promise.all(
4157
- snapshot.results.map(async (result) => {
4158
- try {
4159
- const storage = await this.orchestrator.getStorage(namespace);
4160
- return await storage.readMemoryByPath(result.path);
4161
- } catch {
4162
- return null;
4163
- }
4164
- }),
4165
- );
4166
- const firstReadableIndex = memoryByIndex.findIndex((m) => m !== null);
4167
- const baseDir =
4168
- (await this.orchestrator.getStorage(namespace)).dir;
4169
- const decorated = snapshot.results.map((result, index) => {
4170
- const memory = memoryByIndex[index];
4171
- if (!memory) {
4172
- // Unreadable result: attach the disclosure tag anyway so
4173
- // the per-disclosure summary classifies it correctly,
4174
- // but skip the token estimate since we don't have the
4175
- // content to measure. Without the disclosure tag the
4176
- // result silently flows into the `unspecified` bucket
4177
- // even though the caller explicitly requested a depth
4178
- // (Cursor Low review on PR #699).
4179
- return { ...result, disclosure };
4180
- }
4181
- // Build a representative shaped summary so the estimate
4182
- // counts every field `shapeMemorySummary` actually emits.
4183
- // The serialized JSON form is a close-enough proxy for the
4184
- // wire payload size.
4185
- const shaped = shapeMemorySummary(
4186
- memory,
4187
- baseDir,
4188
- disclosure,
4189
- disclosure === "raw" &&
4190
- index === firstReadableIndex &&
4191
- rawExcerpts &&
4192
- rawExcerpts.length > 0
4193
- ? rawExcerpts
4194
- : undefined,
4195
- );
4196
- return {
4197
- ...result,
4198
- disclosure,
4199
- estimatedTokens: estimateRecallTokens(JSON.stringify(shaped)),
4200
- };
4201
- });
4202
- // Edge case: every result was unreadable but rawExcerpts
4203
- // still has content — credit that spend to result[0] rather
4204
- // than dropping it on the floor. Without this, the raw row
4205
- // in the per-disclosure summary under-reports spend whenever
4206
- // every memory file is missing/unreadable.
4207
- if (
4208
- disclosure === "raw" &&
4209
- firstReadableIndex === -1 &&
4210
- rawExcerptText.length > 0 &&
4211
- decorated.length > 0
4212
- ) {
4213
- decorated[0] = {
4214
- ...decorated[0]!,
4215
- disclosure,
4216
- estimatedTokens: estimateRecallTokens(rawExcerptText),
4217
- };
4218
- }
4219
- const decoratedSnapshot = { ...snapshot, results: decorated };
4220
- xrayResponse = {
4221
- snapshotFound: true,
4222
- snapshot: decoratedSnapshot,
4223
- };
4224
- } else {
4225
- xrayResponse = {
4226
- snapshotFound: true,
4227
- snapshot,
4228
- };
4229
- }
4230
- }
4231
- } finally {
4232
- release();
4233
- }
4234
-
4235
- if (
4236
- request.includeRecall === true &&
4237
- xrayResponse.snapshotFound === true &&
4238
- xrayResponse.snapshot
4239
- ) {
4240
- // Same read-authorization-gated raw-excerpt namespace the recall path uses
4241
- // (#1505 thread 2f7), so the includeRecall x-ray path can't leak overlay
4242
- // transcript rows via raw disclosure. Resolved ONLY for raw disclosure (the
4243
- // sole consumer) so non-raw x-ray recall stays byte-for-byte unchanged. The
4244
- // ordered LCM read key SET (#1505 fallback unification) adds the coding read
4245
- // fallbacks so a branch-scoped session also finds excerpts at project/root
4246
- // scope.
4247
- const xrayRawExcerptNamespace =
4248
- disclosure === "raw"
4249
- ? this.resolveRawExcerptReadNamespace(
4250
- request.namespace,
4251
- recallSessionKey,
4252
- authenticatedPrincipal,
4253
- )
4254
- : undefined;
4255
- // `undefined` for an IMPLICIT raw recall means NO readable LCM namespace
4256
- // exists — suppress excerpts rather than fall back to the write/overlay
4257
- // namespace the read gate excludes (#1505 thread NBHWz).
4258
- const xrayHasExplicitNamespace =
4259
- typeof request.namespace === "string" &&
4260
- request.namespace.trim().length > 0;
4261
- const xrayRawExcerptsSuppressed =
4262
- disclosure === "raw" &&
4263
- !xrayHasExplicitNamespace &&
4264
- xrayRawExcerptNamespace === undefined;
4265
- const xrayRawExcerptSessionIds =
4266
- disclosure === "raw" && xrayRawExcerptNamespace && recallSessionKey
4267
- ? this.resolveLcmReadSessionIds(
4268
- request.namespace,
4269
- xrayRawExcerptNamespace,
4270
- recallSessionKey,
4271
- authenticatedPrincipal,
4272
- )
4273
- : undefined;
4274
- return {
4275
- ...xrayResponse,
4276
- recall: await this.buildRecallResponseFromXraySnapshot({
4277
- query,
4278
- sessionKey: recallSessionKey,
4279
- snapshot: xrayResponse.snapshot,
4280
- disclosure,
4281
- startedAt: recallStartedAt,
4282
- requestedMode: request.mode,
4283
- normalizedMode: mode,
4284
- ...(xrayRawExcerptNamespace
4285
- ? { rawExcerptNamespace: xrayRawExcerptNamespace }
4286
- : {}),
4287
- ...(xrayRawExcerptSessionIds !== undefined
4288
- ? { rawExcerptSessionIds: xrayRawExcerptSessionIds }
4289
- : {}),
4290
- ...(xrayRawExcerptsSuppressed
4291
- ? { rawExcerptsSuppressed: xrayRawExcerptsSuppressed }
4292
- : {}),
4293
- }),
4294
- };
4295
- }
4296
- return xrayResponse;
3117
+ return this.accessRecallSurface.recallXray(
3118
+ request,
3119
+ );
4297
3120
  }
4298
3121
  // Sequence lock for `recallXray` — see comment inside the method.
4299
3122
  // Lives on the instance so every x-ray call on the same service
@@ -5025,97 +3848,11 @@ export class EngramAccessService {
5025
3848
  }
5026
3849
 
5027
3850
  async reviewQueue(runId?: string, namespace?: string, principal?: string): Promise<EngramAccessReviewQueueResponse> {
5028
- const resolvedNamespace = this.resolveReadableNamespace(namespace, principal);
5029
- const storage = await this.orchestrator.getStorage(resolvedNamespace);
5030
- const projected = await storage.getProjectedGovernanceRecord();
5031
- if (projected && (!runId || projected.runId === runId.trim())) {
5032
- const projectedAppliedActions = projected.appliedActionRows.map((row) => ({
5033
- action: row.action,
5034
- memoryId: row.memoryId,
5035
- reasonCode: row.reasonCode,
5036
- beforeStatus: row.beforeStatus,
5037
- afterStatus: row.afterStatus,
5038
- originalPath: row.originalPath,
5039
- currentPath: row.currentPath,
5040
- })) as Awaited<
5041
- ReturnType<typeof readMemoryGovernanceRunArtifact>
5042
- >["appliedActions"];
5043
- const projectedProposedActions = await buildProjectedGovernanceProposedActions(storage, projected);
5044
- const projectedArtifact = await (async () => {
5045
- try {
5046
- return await readMemoryGovernanceRunArtifact(storage.dir, projected.runId);
5047
- } catch {
5048
- return null;
5049
- }
5050
- })();
5051
- const metrics = projected.metrics as Awaited<ReturnType<typeof readMemoryGovernanceRunArtifact>>["metrics"];
5052
- const fallbackTransitionReport = {
5053
- proposed: groupActionsByStatus(projectedProposedActions),
5054
- applied: groupActionsByStatus(projectedAppliedActions),
5055
- };
5056
- const transitionReport = projectedArtifact?.transitionReport
5057
- ? {
5058
- proposed:
5059
- hasGroupedGovernanceActions(projectedArtifact.transitionReport.proposed) || projectedProposedActions.length === 0
5060
- ? projectedArtifact.transitionReport.proposed
5061
- : fallbackTransitionReport.proposed,
5062
- applied:
5063
- hasGroupedGovernanceActions(projectedArtifact.transitionReport.applied) || projectedAppliedActions.length === 0
5064
- ? projectedArtifact.transitionReport.applied
5065
- : fallbackTransitionReport.applied,
5066
- }
5067
- : fallbackTransitionReport;
5068
- const qualityScore = projectedArtifact?.qualityScore ?? metrics?.qualityScore ?? buildQualityScore(metrics?.reviewReasons ?? {
5069
- exact_duplicate: 0,
5070
- semantic_duplicate_candidate: 0,
5071
- disputed_memory: 0,
5072
- speculative_low_confidence: 0,
5073
- archive_candidate: 0,
5074
- explicit_capture_review: 0,
5075
- malformed_import: 0,
5076
- });
5077
- const effectiveMetrics = metrics ? { ...metrics, qualityScore: metrics.qualityScore ?? qualityScore } : metrics;
5078
-
5079
- return {
5080
- found: true,
5081
- namespace: resolvedNamespace,
5082
- runId: projected.runId,
5083
- summary: projected.summary as Awaited<ReturnType<typeof readMemoryGovernanceRunArtifact>>["summary"],
5084
- metrics: effectiveMetrics,
5085
- qualityScore,
5086
- reviewQueue: projected.reviewQueueRows.map((row) => ({
5087
- entryId: row.entryId,
5088
- memoryId: row.memoryId,
5089
- path: row.path,
5090
- reasonCode: row.reasonCode,
5091
- severity: row.severity,
5092
- suggestedAction: row.suggestedAction,
5093
- suggestedStatus: row.suggestedStatus,
5094
- relatedMemoryIds: row.relatedMemoryIds,
5095
- })) as Awaited<
5096
- ReturnType<typeof readMemoryGovernanceRunArtifact>
5097
- >["reviewQueue"],
5098
- appliedActions: projectedAppliedActions,
5099
- transitionReport,
5100
- report: projected.report,
5101
- };
5102
- }
5103
-
5104
- const resolvedRunId = runId?.trim() || (await listMemoryGovernanceRuns(storage.dir))[0];
5105
- if (!resolvedRunId) return { found: false, namespace: resolvedNamespace };
5106
- const artifact = await readMemoryGovernanceRunArtifact(storage.dir, resolvedRunId);
5107
- return {
5108
- found: true,
5109
- namespace: resolvedNamespace,
5110
- runId: resolvedRunId,
5111
- summary: artifact.summary,
5112
- metrics: artifact.metrics,
5113
- qualityScore: artifact.qualityScore,
5114
- reviewQueue: artifact.reviewQueue,
5115
- appliedActions: artifact.appliedActions,
5116
- transitionReport: artifact.transitionReport,
5117
- report: artifact.report,
5118
- };
3851
+ return this.accessAdminOpsSurface.reviewQueue(
3852
+ runId,
3853
+ namespace,
3854
+ principal,
3855
+ );
5119
3856
  }
5120
3857
 
5121
3858
  async maintenance(namespace?: string, principal?: string): Promise<EngramAccessMaintenanceResponse> {
@@ -5200,56 +3937,10 @@ export class EngramAccessService {
5200
3937
  summaryPath: string;
5201
3938
  reportPath: string;
5202
3939
  }> {
5203
- const deepSleep = this.orchestrator.config.dreamsPhases.deepSleep;
5204
- if (deepSleep.enabled === false && deepSleep.enabledExplicitlySet === true) {
5205
- throw new Error("memory governance is disabled by dreams.phases.deepSleep.enabled=false");
5206
- }
5207
- const resolvedNamespace = this.writableNamespaceFor(
5208
- request.namespace,
5209
- undefined,
5210
- request.authenticatedPrincipal ?? principal,
3940
+ return this.accessAdminOpsSurface.governanceRun(
3941
+ request,
3942
+ principal,
5211
3943
  );
5212
- const storage = await this.orchestrator.getStorage(resolvedNamespace);
5213
- const mode = request.mode === "apply" ? "apply" : "shadow";
5214
- const boundedBatchSize =
5215
- typeof request.batchSize === "number" && Number.isFinite(request.batchSize)
5216
- ? Math.max(1, Math.floor(request.batchSize))
5217
- : undefined;
5218
- const result = await runMemoryGovernance({
5219
- memoryDir: storage.dir,
5220
- mode,
5221
- recentDays:
5222
- typeof request.recentDays === "number" && Number.isFinite(request.recentDays)
5223
- ? Math.max(1, Math.floor(request.recentDays))
5224
- : undefined,
5225
- maxMemories:
5226
- typeof request.maxMemories === "number" && Number.isFinite(request.maxMemories)
5227
- ? Math.max(1, Math.floor(request.maxMemories))
5228
- : undefined,
5229
- batchSize: boundedBatchSize,
5230
- });
5231
- if (mode === "apply") {
5232
- try {
5233
- await this.orchestrator.processEntitySynthesisQueue(
5234
- resolvedNamespace,
5235
- Math.min(boundedBatchSize ?? 5, 5),
5236
- );
5237
- } catch (error) {
5238
- log.debug(`governanceRun: entity synthesis refresh failed after governance apply: ${error}`);
5239
- }
5240
- }
5241
-
5242
- return {
5243
- namespace: resolvedNamespace,
5244
- runId: result.runId,
5245
- traceId: result.traceId,
5246
- mode: result.mode,
5247
- reviewQueueCount: result.reviewQueue.length,
5248
- proposedActionCount: result.proposedActions.length,
5249
- appliedActionCount: result.appliedActions.length,
5250
- summaryPath: result.summaryPath,
5251
- reportPath: result.reportPath,
5252
- };
5253
3944
  }
5254
3945
 
5255
3946
  async procedureMiningRun(
@@ -5415,160 +4106,17 @@ export class EngramAccessService {
5415
4106
  reason?: string;
5416
4107
  retryAfterMs?: number;
5417
4108
  }> {
5418
- if (!resolveIndexingCapabilities(this.orchestrator.config).conversationIndex) {
5419
- return {
5420
- enabled: false,
5421
- sessions: 0,
5422
- chunks: 0,
5423
- skipped: 0,
5424
- skippedSessionKeys: [],
5425
- embeddedRuns: 0,
5426
- reason: "disabled",
5427
- };
5428
- }
5429
-
5430
- const hours =
5431
- typeof request.hours === "number" && Number.isFinite(request.hours)
5432
- ? Math.max(1, Math.floor(request.hours))
5433
- : 24;
5434
-
5435
- let sessionKey: string | undefined;
5436
- if (request.sessionKey !== undefined) {
5437
- if (typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
5438
- throw new EngramAccessInputError("sessionKey must be a non-empty string when provided");
5439
- }
5440
- sessionKey = request.sessionKey.trim();
5441
- }
5442
-
5443
- if (sessionKey) {
5444
- const result = await this.orchestrator.updateConversationIndex(
5445
- sessionKey,
5446
- hours,
5447
- { embed: request.embed },
5448
- );
5449
- return {
5450
- enabled: true,
5451
- sessionKey,
5452
- sessions: 1,
5453
- chunks: result.chunks,
5454
- skipped: result.skipped ? 1 : 0,
5455
- skippedSessionKeys: result.skipped ? [sessionKey] : [],
5456
- embeddedRuns: result.embedded ? 1 : 0,
5457
- reason: result.reason,
5458
- retryAfterMs: result.retryAfterMs,
5459
- };
5460
- }
5461
-
5462
- const sessionKeys = await this.orchestrator.transcript.listSessionKeys();
5463
- let chunks = 0;
5464
- let skipped = 0;
5465
- const skippedSessionKeys: string[] = [];
5466
- let embeddedRuns = 0;
5467
-
5468
- for (const sessionKey of sessionKeys) {
5469
- const result = await this.orchestrator.updateConversationIndex(
5470
- sessionKey,
5471
- hours,
5472
- { embed: request.embed },
5473
- );
5474
- chunks += result.chunks;
5475
- if (result.skipped) {
5476
- skipped += 1;
5477
- skippedSessionKeys.push(sessionKey);
5478
- }
5479
- if (result.embedded) {
5480
- embeddedRuns += 1;
5481
- }
5482
- }
5483
-
5484
- return {
5485
- enabled: true,
5486
- sessions: sessionKeys.length,
5487
- chunks,
5488
- skipped,
5489
- skippedSessionKeys,
5490
- embeddedRuns,
5491
- };
4109
+ return this.accessAdminOpsSurface.conversationIndexUpdate(
4110
+ request,
4111
+ );
5492
4112
  }
5493
4113
 
5494
4114
  async profilingReport(
5495
4115
  request: AccessProfilingReportRequest = {},
5496
4116
  ): Promise<AccessProfilingReportResponse> {
5497
- const profiler = this.orchestrator.profiler;
5498
- if (!profiler.isEnabled) {
5499
- return {
5500
- enabled: false,
5501
- reason: "disabled",
5502
- message: "Profiling is disabled. Set profilingEnabled: true in your plugin config to enable.",
5503
- };
5504
- }
5505
-
5506
- const format = request.format ?? "ascii";
5507
- if (format !== "ascii" && format !== "json") {
5508
- throw new EngramAccessInputError("format must be one of: ascii, json");
5509
- }
5510
-
5511
- const limit = request.limit ?? 5;
5512
- if (!Number.isInteger(limit) || limit < 1 || limit > 20) {
5513
- throw new EngramAccessInputError("limit must be an integer between 1 and 20");
5514
- }
5515
-
5516
- const traces = profiler.getRecentTraces(limit);
5517
- const stats = profiler.getStats();
5518
- const bottleneck = profiler.identifyBottleneck();
5519
-
5520
- if (format === "json") {
5521
- return {
5522
- enabled: true,
5523
- format,
5524
- traces,
5525
- stats,
5526
- bottleneck,
5527
- };
5528
- }
5529
-
5530
- const lines: string[] = [];
5531
- lines.push("Engram Profiling Report");
5532
- lines.push("=".repeat(60));
5533
- lines.push("");
5534
-
5535
- type BucketEntry = { count: number; avgMs: number; p50Ms: number; p95Ms: number; maxMs: number };
5536
- const allBuckets: Array<[string, Record<string, BucketEntry>]> = [
5537
- ["byKind", stats.byKind],
5538
- ["bySpan", stats.bySpan],
5539
- ];
5540
- const hasStats = allBuckets.some(([, entries]) => Object.keys(entries).length > 0);
5541
- if (hasStats) {
5542
- lines.push("Aggregate Stats (all retained traces):");
5543
- for (const [bucket, entries] of allBuckets) {
5544
- for (const [key, summary] of Object.entries(entries)) {
5545
- lines.push(
5546
- ` ${bucket}/${key}: avg=${summary.avgMs}ms p50=${summary.p50Ms}ms p95=${summary.p95Ms}ms max=${summary.maxMs}ms (n=${summary.count})`,
5547
- );
5548
- }
5549
- }
5550
- lines.push("");
5551
- }
5552
-
5553
- if (bottleneck) {
5554
- lines.push(`Bottleneck: ${bottleneck}`);
5555
- lines.push("");
5556
- }
5557
-
5558
- if (traces.length === 0) {
5559
- lines.push("No traces recorded yet. Trigger a recall or extraction to see timing data.");
5560
- } else {
5561
- for (const trace of traces) {
5562
- lines.push(formatProfileTraceAscii(trace));
5563
- lines.push("");
5564
- }
5565
- }
5566
-
5567
- return {
5568
- enabled: true,
5569
- format,
5570
- report: lines.join("\n"),
5571
- };
4117
+ return this.accessAdminOpsSurface.profilingReport(
4118
+ request,
4119
+ );
5572
4120
  }
5573
4121
 
5574
4122
  async trustZoneStatus(namespace?: string, principal?: string): Promise<EngramAccessTrustZoneStatusResponse> {
@@ -7506,134 +6054,14 @@ export class EngramAccessService {
7506
6054
  return { explanation };
7507
6055
  }
7508
6056
 
7509
- /**
7510
- * Read-only graph snapshot for the admin pane (issue #691 PR 2/5).
7511
- *
7512
- * Reads adjacency from the JSONL edge store written by `GraphIndex` and
7513
- * resolves node metadata via the namespaced storage manager. Namespace
7514
- * resolution mirrors the read-side path used by `recall` /
7515
- * `procedureStats`, so multi-principal deployments can't leak edges from
7516
- * a peer namespace (CLAUDE.md rule 42).
7517
- */
7518
6057
  async graphSnapshot(
7519
6058
  request: GraphSnapshotRequest & { namespace?: string },
7520
6059
  authenticatedPrincipal?: string,
7521
6060
  ): Promise<GraphSnapshotResponse> {
7522
- const namespace = this.resolveReadableNamespace(
7523
- request.namespace,
6061
+ return this.accessAdminOpsSurface.graphSnapshot(
6062
+ request,
7524
6063
  authenticatedPrincipal,
7525
6064
  );
7526
- const storage = await this.orchestrator.getStorage(namespace);
7527
- const cfg = this.orchestrator.config;
7528
- const graphCaps = resolveGraphConstructionCapabilities(cfg);
7529
- // Canonicalize the storage root once — through `realpath` so that any
7530
- // symlink in the namespace root path itself is resolved before we
7531
- // compare children against it. This is required because
7532
- // `GraphEdge.from` / `to` are JSONL-parsed strings — a malformed edge
7533
- // with an absolute path, a `..` segment, OR a symlink that resolves
7534
- // to a file outside the namespace would otherwise read a memory file
7535
- // from a peer namespace, leaking metadata across tenants
7536
- // (codex P1 + follow-up on PR #734; CLAUDE.md rule 42).
7537
- let namespaceRootReal: string;
7538
- try {
7539
- namespaceRootReal = await nodeFs.realpath(storage.dir);
7540
- } catch {
7541
- // If the namespace root itself doesn't exist on disk yet (fresh
7542
- // install with no memories), fall back to the lexical resolve so
7543
- // the snapshot can still return an empty result rather than
7544
- // throwing. No symlink can resolve through a missing path, so
7545
- // this fallback is safe — every candidate we see will fail the
7546
- // realpath step below and surface as `null`.
7547
- namespaceRootReal = nodePath.resolve(storage.dir);
7548
- }
7549
- const namespaceRootWithSep = namespaceRootReal.endsWith(nodePath.sep)
7550
- ? namespaceRootReal
7551
- : namespaceRootReal + nodePath.sep;
7552
- const loadNode = async (relPath: string): Promise<GraphSnapshotNodeMetadata | null> => {
7553
- // `GraphEdge.from` / `to` are storage-relative paths; resolve against
7554
- // the namespaced storage root so the metadata read honors namespace
7555
- // boundaries even when the same memory id exists in multiple
7556
- // namespaces.
7557
- //
7558
- // Three-stage guard:
7559
- // 1. Reject absolute paths up front — only relative endpoints are
7560
- // ever produced by the writer, so anything else is malformed.
7561
- // 2. Lexical containment check on the resolved path. This catches
7562
- // `..` traversals before we touch the filesystem.
7563
- // 3. `fs.realpath` containment check — resolves symlinks so an
7564
- // in-namespace path that *points* at an out-of-namespace file
7565
- // is still rejected. Without this step a symlinked endpoint
7566
- // could leak a peer namespace's frontmatter.
7567
- // Bad paths surface a length-only warning (never echo the offending
7568
- // segments — those would themselves cross namespace boundaries
7569
- // through the log surface) and fall through to a `null` metadata
7570
- // result.
7571
- if (nodePath.isAbsolute(relPath)) {
7572
- log.warn(
7573
- `graphSnapshot: rejected absolute edge endpoint (len=${relPath.length}) `
7574
- + `outside namespace root`,
7575
- );
7576
- return null;
7577
- }
7578
- const candidate = nodePath.resolve(namespaceRootReal, relPath);
7579
- if (candidate !== namespaceRootReal && !candidate.startsWith(namespaceRootWithSep)) {
7580
- log.warn(
7581
- `graphSnapshot: rejected traversing edge endpoint (len=${relPath.length}) `
7582
- + `outside namespace root`,
7583
- );
7584
- return null;
7585
- }
7586
- let canonical: string;
7587
- try {
7588
- canonical = await nodeFs.realpath(candidate);
7589
- } catch {
7590
- // Missing file — `readMemoryByPath` will return null too. We
7591
- // intentionally still call it so callers see a consistent
7592
- // "unknown" result rather than special-casing missing edges.
7593
- canonical = candidate;
7594
- }
7595
- if (canonical !== namespaceRootReal && !canonical.startsWith(namespaceRootWithSep)) {
7596
- log.warn(
7597
- `graphSnapshot: rejected symlinked edge endpoint (len=${relPath.length}) `
7598
- + `that resolved outside namespace root`,
7599
- );
7600
- return null;
7601
- }
7602
- // Both `canonical` (realpath of candidate) and `namespaceRootReal`
7603
- // (realpath of storage.dir) are fully resolved here, so the
7604
- // containment check above is comparing apples-to-apples even when
7605
- // storage.dir (= storage.baseDir) is itself a symlink to the real
7606
- // directory. Pass `canonical` — not the pre-realpath `candidate` —
7607
- // so the storage read also uses the stable real path.
7608
- const memory = await storage.readMemoryByPath(canonical);
7609
- if (!memory) return null;
7610
- const fm = memory.frontmatter;
7611
- return {
7612
- category: fm.category ?? "unknown",
7613
- label: fm.id ?? nodePath.basename(canonical, nodePath.extname(canonical)),
7614
- updated: fm.updated,
7615
- };
7616
- };
7617
- // Use the realpath-resolved namespace root for the edge-file read so
7618
- // the JSONL location is stable whether storage.dir is a direct path
7619
- // or a symlink. namespaceRootReal was computed via `fs.realpath`
7620
- // above; using it here keeps both the graph-file I/O and the loadNode
7621
- // containment check on the same resolved base path.
7622
- return buildGraphSnapshot({
7623
- memoryDir: namespaceRootReal,
7624
- graphConfig: {
7625
- entityGraph: graphCaps.entityGraph,
7626
- timeGraph: graphCaps.timeGraph,
7627
- causalGraph: graphCaps.causalGraph,
7628
- },
7629
- request: {
7630
- limit: request.limit,
7631
- since: request.since,
7632
- focusNodeId: request.focusNodeId,
7633
- categories: request.categories,
7634
- },
7635
- loadNode,
7636
- });
7637
6065
  }
7638
6066
 
7639
6067
  async memoryFeedback(request: {
@@ -8330,108 +6758,13 @@ export class EngramAccessService {
8330
6758
  });
8331
6759
  }
8332
6760
 
8333
- /**
8334
- * List capsule archives in the namespace-scoped capsule store.
8335
- *
8336
- * MCP uses this access-layer method instead of reading arbitrary paths so
8337
- * capsule discovery remains bound to the same namespace ACLs as export and
8338
- * import.
8339
- */
8340
6761
  async capsuleList(options?: {
8341
6762
  namespace?: string;
8342
6763
  principal?: string;
8343
6764
  }): Promise<EngramAccessCapsuleListResponse> {
8344
- const resolvedNamespace = this.resolveReadableNamespace(options?.namespace, options?.principal);
8345
- const storage = await this.orchestrator.getStorage(resolvedNamespace);
8346
- const capsulesDir = defaultCapsulesDir(storage.dir);
8347
- let dirEntries: import("node:fs").Dirent[];
8348
- try {
8349
- const capsulesDirStat = await nodeFs.lstat(capsulesDir);
8350
- if (capsulesDirStat.isSymbolicLink()) {
8351
- throw new EngramAccessInputError("capsule list failed: capsule store directory must not be a symlink");
8352
- }
8353
- if (!capsulesDirStat.isDirectory()) {
8354
- throw new EngramAccessInputError("capsule list failed: capsule store path must be a directory");
8355
- }
8356
- dirEntries = await nodeFs.readdir(capsulesDir, { withFileTypes: true });
8357
- } catch (err) {
8358
- const code = typeof err === "object" && err !== null && "code" in err
8359
- ? (err as { code?: unknown }).code
8360
- : undefined;
8361
- if (code === "ENOENT") {
8362
- return { namespace: resolvedNamespace, capsulesDir, capsules: [] };
8363
- }
8364
- throw err;
8365
- }
8366
-
8367
- const archiveNames = dirEntries
8368
- .filter(
8369
- (entry) =>
8370
- entry.isFile() &&
8371
- (entry.name.endsWith(".capsule.json.gz") ||
8372
- entry.name.endsWith(".capsule.json.gz.enc")),
8373
- )
8374
- .map((entry) => entry.name)
8375
- .sort();
8376
-
8377
- const capsules: CapsuleListEntry[] = [];
8378
- for (const archiveName of archiveNames) {
8379
- const archivePath = nodePath.join(capsulesDir, archiveName);
8380
- const id = archiveName
8381
- .replace(/\.capsule\.json\.gz\.enc$/, "")
8382
- .replace(/\.capsule\.json\.gz$/, "");
8383
- const manifestPath = nodePath.join(capsulesDir, `${id}.manifest.json`);
8384
-
8385
- let createdAt: string | null = null;
8386
- let pluginVersion: string | null = null;
8387
- let fileCount: number | null = null;
8388
- let description: string | null = null;
8389
- let manifestPathOrNull: string | null = manifestPath;
8390
-
8391
- try {
8392
- const manifestStat = await nodeFs.lstat(manifestPath);
8393
- if (manifestStat.isSymbolicLink() || !manifestStat.isFile()) {
8394
- capsules.push({
8395
- id,
8396
- archivePath,
8397
- manifestPath: manifestPathOrNull,
8398
- createdAt,
8399
- pluginVersion,
8400
- fileCount,
8401
- description,
8402
- });
8403
- continue;
8404
- }
8405
- const raw = await nodeFs.readFile(manifestPath, "utf-8");
8406
- const sidecar = JSON.parse(raw) as Record<string, unknown>;
8407
- createdAt = typeof sidecar.createdAt === "string" ? sidecar.createdAt : null;
8408
- pluginVersion = typeof sidecar.pluginVersion === "string" ? sidecar.pluginVersion : null;
8409
- fileCount = Array.isArray(sidecar.files) ? sidecar.files.length : null;
8410
- const capsule = sidecar.capsule as Record<string, unknown> | undefined;
8411
- description = capsule && typeof capsule.description === "string"
8412
- ? capsule.description
8413
- : null;
8414
- } catch (err) {
8415
- const code = typeof err === "object" && err !== null && "code" in err
8416
- ? (err as { code?: unknown }).code
8417
- : undefined;
8418
- if (code === "ENOENT") {
8419
- manifestPathOrNull = null;
8420
- }
8421
- }
8422
-
8423
- capsules.push({
8424
- id,
8425
- archivePath,
8426
- manifestPath: manifestPathOrNull,
8427
- createdAt,
8428
- pluginVersion,
8429
- fileCount,
8430
- description,
8431
- });
8432
- }
8433
-
8434
- return { namespace: resolvedNamespace, capsulesDir, capsules };
6765
+ return this.accessAdminOpsSurface.capsuleList(
6766
+ options,
6767
+ );
8435
6768
  }
8436
6769
 
8437
6770
  /**
@@ -8674,87 +7007,15 @@ export class EngramAccessService {
8674
7007
  return getDreamsStatus(storage.dir, windowHours);
8675
7008
  }
8676
7009
 
8677
- /**
8678
- * Manually invoke a single Dreams phase pass (PR 4/4).
8679
- *
8680
- * Deep-sleep delegates to memory governance (shadow → dry-run, apply → live).
8681
- * Light-sleep and REM scan the observation ledger and memory corpus
8682
- * respectively, returning the same telemetry shape as a scheduled run.
8683
- */
8684
7010
  async dreamsRun(options: {
8685
7011
  phase: import("./types.js").DreamsPhase;
8686
7012
  dryRun?: boolean;
8687
7013
  namespace?: string;
8688
7014
  authenticatedPrincipal?: string;
8689
7015
  }): Promise<import("./types.js").DreamsRunResult> {
8690
- const { runDreamsPhase } = await import("./maintenance/dreams-ledger.js");
8691
- const validPhases = ["lightSleep", "rem", "deepSleep"];
8692
- if (!validPhases.includes(options.phase)) {
8693
- throw new EngramAccessInputError(
8694
- `Invalid phase: ${String(options.phase)}. Must be one of: ${validPhases.join(", ")}`,
8695
- );
8696
- }
8697
- const deepSleep = this.orchestrator.config.dreamsPhases.deepSleep;
8698
- if (
8699
- options.phase === "deepSleep" &&
8700
- deepSleep.enabled === false &&
8701
- deepSleep.enabledExplicitlySet === true
8702
- ) {
8703
- throw new EngramAccessInputError(
8704
- "memory governance is disabled by dreams.phases.deepSleep.enabled=false",
8705
- );
8706
- }
8707
- const dryRun = options.dryRun === true;
8708
- const resolvedNamespace = this.writableNamespaceFor(
8709
- options.namespace,
8710
- undefined,
8711
- options.authenticatedPrincipal,
7016
+ return this.accessAdminOpsSurface.dreamsRun(
7017
+ options,
8712
7018
  );
8713
- const storage = await this.orchestrator.getStorage(resolvedNamespace);
8714
- const memoryDir = storage.dir;
8715
- const phaseRunner = dryRun || options.phase === "deepSleep"
8716
- ? undefined
8717
- : async (_opts: { memoryDir: string; phase: "lightSleep" | "rem" }) => {
8718
- if (_opts.phase === "lightSleep") {
8719
- const result = await this.orchestrator.runLifecyclePolicyNow(storage);
8720
- return {
8721
- itemsProcessed: result.memoriesAssessed,
8722
- notes: `scored ${result.memoriesAssessed} memories`,
8723
- };
8724
- }
8725
- const result = await this.orchestrator.runSemanticConsolidationNow({
8726
- dryRun: false,
8727
- storage,
8728
- });
8729
- const itemsProcessed = result.clusters.reduce(
8730
- (sum, cluster) => sum + cluster.memories.length,
8731
- 0,
8732
- );
8733
- return {
8734
- itemsProcessed,
8735
- notes: `REM consolidation found ${result.clustersFound} clusters`,
8736
- };
8737
- };
8738
- const governanceRunner = options.phase === "deepSleep"
8739
- ? async (_opts: { memoryDir: string; dryRun: boolean }) => {
8740
- return this.orchestrator.runDeepSleepGovernanceNow({
8741
- storage,
8742
- dryRun: _opts.dryRun,
8743
- });
8744
- }
8745
- : undefined;
8746
- const result = await runDreamsPhase(
8747
- { memoryDir, phase: options.phase, dryRun },
8748
- governanceRunner,
8749
- phaseRunner,
8750
- );
8751
- return {
8752
- phase: result.phase,
8753
- dryRun: result.dryRun,
8754
- durationMs: result.durationMs,
8755
- itemsProcessed: result.itemsProcessed,
8756
- notes: result.notes,
8757
- };
8758
7019
  }
8759
7020
 
8760
7021
  // ---------------------------------------------------------------------------
@@ -8913,12 +7174,6 @@ export class EngramAccessService {
8913
7174
  return redactSensitive(result);
8914
7175
  }
8915
7176
 
8916
- /**
8917
- * Manually promote a memory into one or more authorized targets. Requires
8918
- * a non-empty reason (audit-logged). Reuses the scope-profile promotion
8919
- * resolution and `canWriteNamespace` gate — there is no dashboard-only
8920
- * write path.
8921
- */
8922
7177
  async adminPromoteMemory(request: {
8923
7178
  sourceMemoryId: string;
8924
7179
  namespace?: string;
@@ -8928,111 +7183,9 @@ export class EngramAccessService {
8928
7183
  reason: string;
8929
7184
  actor?: never; // ignored — actor is derived from the authenticated principal
8930
7185
  }) {
8931
- const config = this.orchestrator.config;
8932
- const namespacesEnabled = resolveNamespaceCapabilities(config).namespaces === true;
8933
- // Fix OdCl3: when no namespace is supplied, resolve the source via the
8934
- // scope plan (principal self / scope-profile write layer / coding overlay)
8935
- // instead of falling through to config.defaultNamespace. This matches the
8936
- // runtime observe/write path.
8937
- let sourceNamespace: string;
8938
- if (request.namespace) {
8939
- sourceNamespace = this.resolveReadableNamespace(
8940
- request.namespace,
8941
- request.principal,
8942
- );
8943
- } else {
8944
- const codingContext = request.sessionKey
8945
- ? this.orchestrator.getCodingContextForSession(request.sessionKey) ?? null
8946
- : null;
8947
- const plan = resolveScopePlan({
8948
- config,
8949
- sessionKey: request.sessionKey,
8950
- principalOverride: request.principal,
8951
- codingContext,
8952
- namespacesEnabled,
8953
- });
8954
- sourceNamespace = this.resolveReadableNamespace(
8955
- plan.baseNamespace,
8956
- request.principal,
8957
- );
8958
- }
8959
- // Fix OdB0c: thread coding context into the scope-profile plan so
8960
- // userProject/teamProject promotion targets resolve for project-scoped
8961
- // sessions (previously hard-coded to null).
8962
- const codingContext = request.sessionKey
8963
- ? this.orchestrator.getCodingContextForSession(request.sessionKey) ?? null
8964
- : null;
8965
- const codingOverlay = codingContext
8966
- ? resolveCodingNamespaceOverlay(
8967
- codingContext,
8968
- config.codingMode,
8969
- config.defaultNamespace,
8970
- )
8971
- : null;
8972
- const scopeProfilePlan = resolveScopeProfilePlan({
8973
- config,
8974
- principal: request.principal,
8975
- codingContext,
8976
- codingOverlay,
8977
- });
8978
- const storage: PromotionStorageProvider = {
8979
- readMemory: async (namespace, memoryId) => {
8980
- const resolved = await this.orchestrator.getStorage(namespace);
8981
- const memory = await resolved.getMemoryById(memoryId);
8982
- if (!memory) return null;
8983
- return {
8984
- category: memory.frontmatter.category,
8985
- content: memory.content,
8986
- frontmatter: memory.frontmatter,
8987
- };
8988
- },
8989
- writePromotedMemory: async (namespace, memory) => {
8990
- const resolved = await this.orchestrator.getStorage(namespace);
8991
- const { id, tombstoneBlocked } = await resolved.writeMemory(
8992
- memory.category,
8993
- memory.content,
8994
- {
8995
- confidence: memory.confidence,
8996
- tags: memory.tags,
8997
- entityRef: memory.entityRef,
8998
- source: `admin-promotion:${memory.sourceNamespace}:${memory.reason.slice(0, 120)}`,
8999
- lineage: memory.lineage,
9000
- sourceMemoryId: memory.sourceMemoryId,
9001
- actor: memory.actor,
9002
- validAt: memory.validAt,
9003
- },
9004
- );
9005
- // #1645: a tombstone-blocked promotion lands pending_review (no active
9006
- // copy in the target). Report it as a failed promotion so the admin
9007
- // sees an honest result — the content is queued for review, not
9008
- // actively promoted. promoteMemory's catch block sanitizes this into
9009
- // a generic "promotion write failed" audit entry.
9010
- if (tombstoneBlocked) {
9011
- throw new Error(
9012
- "target namespace tombstone-blocked the promoted content (pending_review)",
9013
- );
9014
- }
9015
- return id;
9016
- },
9017
- };
9018
- try {
9019
- return await promoteMemory({
9020
- config,
9021
- sourceMemoryId: request.sourceMemoryId,
9022
- sourceNamespace,
9023
- principal: request.principal,
9024
- targets: request.targets,
9025
- reason: request.reason,
9026
- actor: request.principal ?? "admin-console",
9027
- storage,
9028
- scopeProfilePlan,
9029
- });
9030
- } catch (err) {
9031
- if (err instanceof AdminPromotionError) {
9032
- throw new EngramAccessInputError(err.message);
9033
- }
9034
- throw err;
9035
- }
7186
+ return this.accessAdminOpsSurface.adminPromoteMemory(
7187
+ request,
7188
+ );
9036
7189
  }
9037
7190
  }
9038
7191