@remnic/core 9.3.754 → 9.3.756

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 (306) hide show
  1. package/dist/access-admin-ops-surface.d.ts +5 -5
  2. package/dist/access-admin-ops-surface.js +34 -34
  3. package/dist/access-boundary.d.ts +5 -5
  4. package/dist/access-boundary.js +35 -35
  5. package/dist/access-cli.js +83 -83
  6. package/dist/access-http.d.ts +5 -5
  7. package/dist/access-http.js +44 -44
  8. package/dist/access-lcm-surface.d.ts +5 -5
  9. package/dist/access-lcm-surface.js +34 -34
  10. package/dist/access-mcp.d.ts +5 -5
  11. package/dist/access-mcp.js +41 -41
  12. package/dist/access-observe-write-surface.d.ts +225 -0
  13. package/dist/access-observe-write-surface.js +128 -0
  14. package/dist/access-operations-batch.js +38 -38
  15. package/dist/access-operations.d.ts +5 -5
  16. package/dist/access-operations.js +40 -40
  17. package/dist/access-recall-surface.d.ts +5 -5
  18. package/dist/access-recall-surface.js +34 -34
  19. package/dist/access-schema.d.ts +10 -10
  20. package/dist/access-schema.js +3 -3
  21. package/dist/{access-service-DKRV1zxh.d.ts → access-service-CJwcDoQp.d.ts} +7 -8
  22. package/dist/access-service.d.ts +5 -5
  23. package/dist/access-service.js +34 -34
  24. package/dist/access-surface-catalog.d.ts +5 -5
  25. package/dist/action-confidence.d.ts +1 -1
  26. package/dist/active-memory-bridge.d.ts +1 -1
  27. package/dist/active-recall.d.ts +1 -1
  28. package/dist/active-recall.js +1 -1
  29. package/dist/adapters/index.js +4 -4
  30. package/dist/adapters/registry.js +2 -2
  31. package/dist/{auto-sync-G4YKBPOU.js → auto-sync-RZO5ZPHR.js} +3 -3
  32. package/dist/behavior-learner.d.ts +1 -1
  33. package/dist/behavior-signals.d.ts +1 -1
  34. package/dist/bootstrap.d.ts +4 -4
  35. package/dist/briefing.d.ts +1 -1
  36. package/dist/briefing.js +7 -7
  37. package/dist/buffer-surprise-report.d.ts +1 -1
  38. package/dist/buffer.d.ts +1 -1
  39. package/dist/calibration.d.ts +1 -1
  40. package/dist/capabilities.d.ts +1 -1
  41. package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-FFWJ4X4A.js} +2 -2
  42. package/dist/{catalog-BEmNVRN0.d.ts → catalog-DaKAFbSJ.d.ts} +1 -1
  43. package/dist/causal-behavior.d.ts +1 -1
  44. package/dist/causal-behavior.js +2 -2
  45. package/dist/causal-chain.js +2 -2
  46. package/dist/causal-consolidation.d.ts +1 -1
  47. package/dist/causal-consolidation.js +15 -15
  48. package/dist/causal-retrieval.js +2 -2
  49. package/dist/causal-trajectory-graph.d.ts +1 -1
  50. package/dist/causal-trajectory.js +1 -1
  51. package/dist/{chunk-M5QKGHCR.js → chunk-3I2IASNK.js} +2 -2
  52. package/dist/{chunk-JBO6UBIE.js → chunk-4AC57RCM.js} +4 -4
  53. package/dist/{chunk-3DOD6ICJ.js → chunk-4I45UZ4V.js} +5 -5
  54. package/dist/{chunk-TAPSNIMA.js → chunk-5CEJH5ZN.js} +4 -4
  55. package/dist/{chunk-CXCZ4LAI.js → chunk-5VWRYFQE.js} +7 -7
  56. package/dist/{chunk-TXALJRYO.js → chunk-5ZKT7WTR.js} +7 -7
  57. package/dist/{chunk-PYVZKEZZ.js → chunk-6DA5BBLS.js} +120 -120
  58. package/dist/{chunk-WS6GLJ7P.js → chunk-6DXXIXED.js} +4 -4
  59. package/dist/{chunk-THZ4WR5L.js → chunk-7APVFQ34.js} +1182 -1009
  60. package/dist/chunk-7APVFQ34.js.map +1 -0
  61. package/dist/{chunk-OH66EBCS.js → chunk-7CFGXWYX.js} +2 -2
  62. package/dist/{chunk-TJATYB3F.js → chunk-A6XCHI53.js} +5 -5
  63. package/dist/{chunk-2MHCWXX6.js → chunk-BTH6MQJT.js} +7 -7
  64. package/dist/{chunk-G7LCG56O.js → chunk-CXW6Y7SI.js} +4 -4
  65. package/dist/{chunk-L4AS7TIG.js → chunk-DWDDQLSZ.js} +2300 -2239
  66. package/dist/chunk-DWDDQLSZ.js.map +1 -0
  67. package/dist/{chunk-DS4QM6I7.js → chunk-EEKDRAWX.js} +4 -4
  68. package/dist/{chunk-GAH5755F.js → chunk-EF3VFQC4.js} +2 -2
  69. package/dist/{chunk-437S3G37.js → chunk-EJJQDP3H.js} +1 -1
  70. package/dist/{chunk-V4C4SFZB.js → chunk-EVVLGYHR.js} +3 -3
  71. package/dist/{chunk-ZJMSTFOR.js → chunk-GBXP62CL.js} +10 -10
  72. package/dist/{chunk-7AL5WNPW.js → chunk-GG6G2RYR.js} +9 -9
  73. package/dist/{chunk-BERWPOXU.js → chunk-H2T4OJNI.js} +4 -4
  74. package/dist/{chunk-2MR3MFQB.js → chunk-JTYM4HPA.js} +2 -2
  75. package/dist/{chunk-LD53WPMU.js → chunk-LQ6JI4VH.js} +4 -4
  76. package/dist/{chunk-GPFXPC2V.js → chunk-NG2NHB4C.js} +7 -7
  77. package/dist/{chunk-COZEVVXZ.js → chunk-NLXKSM46.js} +2 -2
  78. package/dist/{chunk-VSU2NZGC.js → chunk-OLAVQS6S.js} +6 -6
  79. package/dist/{chunk-ER3CCWK2.js → chunk-ORAK3LNZ.js} +2 -2
  80. package/dist/{chunk-WUVUTOUV.js → chunk-PBZQVVLG.js} +5 -5
  81. package/dist/{chunk-PJQMISPD.js → chunk-PPAEJXVF.js} +5 -5
  82. package/dist/{chunk-KURUULFF.js → chunk-PRX77NPC.js} +5 -5
  83. package/dist/{chunk-24463PIV.js → chunk-PS572XYO.js} +2 -2
  84. package/dist/{chunk-U2SMR57B.js → chunk-REZ4TUD2.js} +10 -10
  85. package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
  86. package/dist/{chunk-V5UV7BOA.js → chunk-SQNED75R.js} +1 -1
  87. package/dist/{chunk-FLVL4L3L.js → chunk-TCKP4EHY.js} +2 -2
  88. package/dist/{chunk-YRBXBHU7.js → chunk-TXV3I2O6.js} +7 -7
  89. package/dist/{chunk-CJ276MDI.js → chunk-UNV6ZGTS.js} +2 -2
  90. package/dist/{chunk-2BFLDH5F.js → chunk-WEPWY4HB.js} +2 -2
  91. package/dist/{chunk-YQGHW5ML.js → chunk-WRFKZEO6.js} +4 -4
  92. package/dist/{chunk-7CAJLUNW.js → chunk-Y4TAFMUE.js} +6 -6
  93. package/dist/{chunk-TIFF32QK.js → chunk-YMGQXBPS.js} +2 -2
  94. package/dist/{chunk-T422SYM6.js → chunk-ZUDM75KG.js} +5 -5
  95. package/dist/{chunk-4DVYRPLL.js → chunk-ZUHD6XZZ.js} +43 -43
  96. package/dist/{chunk-BQ6J6P3K.js → chunk-ZW5SZ3TH.js} +12 -12
  97. package/dist/{cli-D-xw8Sp1.d.ts → cli-cZyANcmY.d.ts} +3 -3
  98. package/dist/cli.d.ts +6 -6
  99. package/dist/cli.js +59 -59
  100. package/dist/compounding/engine.d.ts +1 -1
  101. package/dist/compounding/engine.js +8 -8
  102. package/dist/compounding/preference-consolidator.d.ts +1 -1
  103. package/dist/compression-optimizer.d.ts +1 -1
  104. package/dist/config.d.ts +1 -1
  105. package/dist/config.js +1 -1
  106. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  107. package/dist/connectors/codex-materialize-runner.js +8 -8
  108. package/dist/connectors/codex-materialize.d.ts +1 -1
  109. package/dist/connectors/index.d.ts +1 -1
  110. package/dist/connectors/index.js +9 -9
  111. package/dist/consolidation-provenance-check.d.ts +1 -1
  112. package/dist/consolidation-undo.d.ts +1 -1
  113. package/dist/contradiction/index.d.ts +1 -1
  114. package/dist/contradiction/index.js +2 -2
  115. package/dist/{contradiction-scan-2SODRNUF.js → contradiction-scan-3FJYWS3G.js} +3 -3
  116. package/dist/contradiction-scan-3FJYWS3G.js.map +1 -0
  117. package/dist/conversation-index/backend.d.ts +1 -1
  118. package/dist/conversation-index/backend.js +2 -2
  119. package/dist/conversation-index/chunker.d.ts +1 -1
  120. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  121. package/dist/conversation-index/indexer.d.ts +1 -1
  122. package/dist/conversation-index/search.d.ts +1 -1
  123. package/dist/day-summary.d.ts +1 -1
  124. package/dist/delinearize.d.ts +1 -1
  125. package/dist/direct-answer-wiring.d.ts +1 -1
  126. package/dist/direct-answer.d.ts +1 -1
  127. package/dist/embedding-fallback.d.ts +1 -1
  128. package/dist/enrichment/index.d.ts +1 -1
  129. package/dist/entity-retrieval.d.ts +1 -1
  130. package/dist/entity-retrieval.js +8 -8
  131. package/dist/entity-schema.d.ts +1 -1
  132. package/dist/explicit-capture.d.ts +4 -4
  133. package/dist/extraction-faithfulness.d.ts +1 -1
  134. package/dist/extraction-judge-telemetry.d.ts +1 -1
  135. package/dist/extraction-judge-training.d.ts +1 -1
  136. package/dist/extraction-judge.d.ts +1 -1
  137. package/dist/extraction.d.ts +1 -1
  138. package/dist/extraction.js +2 -2
  139. package/dist/fallback-llm.d.ts +1 -1
  140. package/dist/graph-dashboard-diff.d.ts +1 -1
  141. package/dist/graph-dashboard-key.d.ts +1 -1
  142. package/dist/graph-dashboard-parser.d.ts +1 -1
  143. package/dist/graph-edge-reinforcement.d.ts +1 -1
  144. package/dist/graph-snapshot.d.ts +1 -1
  145. package/dist/graph.d.ts +1 -1
  146. package/dist/identity-continuity.d.ts +1 -1
  147. package/dist/importance.d.ts +1 -1
  148. package/dist/index.d.ts +9 -9
  149. package/dist/index.js +126 -126
  150. package/dist/intent.d.ts +1 -1
  151. package/dist/lcm/engine.d.ts +1 -1
  152. package/dist/lcm/engine.js +2 -2
  153. package/dist/lcm/index.d.ts +1 -1
  154. package/dist/lcm/index.js +2 -2
  155. package/dist/lcm/tools.d.ts +1 -1
  156. package/dist/lifecycle.d.ts +1 -1
  157. package/dist/live-connectors-runner.d.ts +1 -1
  158. package/dist/local-llm.d.ts +1 -1
  159. package/dist/local-model-endpoint.d.ts +1 -1
  160. package/dist/maintenance/memory-governance.d.ts +1 -1
  161. package/dist/maintenance/memory-governance.js +8 -8
  162. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  163. package/dist/maintenance/rebuild-memory-projection.js +9 -9
  164. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  165. package/dist/memory-action-policy.d.ts +1 -1
  166. package/dist/memory-cache.d.ts +1 -1
  167. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  168. package/dist/memory-projection-store.d.ts +1 -1
  169. package/dist/memory-provenance.d.ts +1 -1
  170. package/dist/memory-worth-outcomes.d.ts +1 -1
  171. package/dist/models-json.d.ts +1 -1
  172. package/dist/namespaces/migrate.d.ts +2 -2
  173. package/dist/namespaces/migrate.js +18 -18
  174. package/dist/namespaces/principal.d.ts +1 -1
  175. package/dist/namespaces/search.d.ts +1 -1
  176. package/dist/namespaces/search.js +9 -9
  177. package/dist/namespaces/storage.d.ts +2 -2
  178. package/dist/namespaces/storage.js +9 -9
  179. package/dist/native-knowledge.d.ts +1 -1
  180. package/dist/operator-toolkit.d.ts +1 -1
  181. package/dist/operator-toolkit.js +24 -24
  182. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  183. package/dist/orchestration/maintenance.d.ts +2 -2
  184. package/dist/orchestration/maintenance.js +12 -12
  185. package/dist/{orchestrator-D7ZGOQLX.d.ts → orchestrator-Xm6sK5jw.d.ts} +3 -3
  186. package/dist/orchestrator.d.ts +4 -4
  187. package/dist/orchestrator.js +82 -82
  188. package/dist/patterns-cli.d.ts +1 -1
  189. package/dist/policy-runtime.d.ts +1 -1
  190. package/dist/provenance.d.ts +1 -1
  191. package/dist/qmd-recall-cache.d.ts +1 -1
  192. package/dist/qmd.d.ts +1 -1
  193. package/dist/recall-disclosure-escalation.d.ts +1 -1
  194. package/dist/recall-explain-renderer.d.ts +1 -1
  195. package/dist/recall-planner-llm.d.ts +1 -1
  196. package/dist/recall-state.d.ts +1 -1
  197. package/dist/recall-tag-filter.d.ts +1 -1
  198. package/dist/recall-xray-cli.d.ts +1 -1
  199. package/dist/recall-xray-renderer.d.ts +1 -1
  200. package/dist/recall-xray.d.ts +1 -1
  201. package/dist/resolve-auth-token.d.ts +1 -1
  202. package/dist/resume-bundles.js +2 -2
  203. package/dist/retrieval-agents.d.ts +1 -1
  204. package/dist/retrieval-tiers.d.ts +1 -1
  205. package/dist/routing/engine.d.ts +1 -1
  206. package/dist/routing/store.d.ts +1 -1
  207. package/dist/search/embed-helper.d.ts +1 -1
  208. package/dist/search/factory.d.ts +1 -1
  209. package/dist/search/factory.js +8 -8
  210. package/dist/search/index.d.ts +1 -1
  211. package/dist/search/index.js +8 -8
  212. package/dist/search/lancedb-backend.d.ts +1 -1
  213. package/dist/search/lancedb-backend.js +2 -2
  214. package/dist/search/meilisearch-backend.d.ts +1 -1
  215. package/dist/search/meilisearch-backend.js +2 -2
  216. package/dist/search/noop-backend.d.ts +1 -1
  217. package/dist/search/orama-backend.d.ts +1 -1
  218. package/dist/search/orama-backend.js +2 -2
  219. package/dist/search/port.d.ts +1 -1
  220. package/dist/search/remote-backend.d.ts +1 -1
  221. package/dist/{semantic-consolidation-7Sndms2M.d.ts → semantic-consolidation-bfXJNKVf.d.ts} +1 -1
  222. package/dist/semantic-consolidation.d.ts +2 -2
  223. package/dist/semantic-consolidation.js +11 -11
  224. package/dist/semantic-rule-promotion.js +7 -7
  225. package/dist/semantic-rule-verifier.d.ts +1 -1
  226. package/dist/semantic-rule-verifier.js +8 -8
  227. package/dist/session-observer-bands.d.ts +1 -1
  228. package/dist/session-observer-state.d.ts +1 -1
  229. package/dist/shared-context/manager.d.ts +3 -3
  230. package/dist/signal.d.ts +1 -1
  231. package/dist/storage.d.ts +69 -97
  232. package/dist/storage.js +22 -6
  233. package/dist/summarizer.d.ts +1 -1
  234. package/dist/summarizer.js +2 -2
  235. package/dist/summary-snapshot.d.ts +1 -1
  236. package/dist/temporal-supersession.d.ts +1 -1
  237. package/dist/temporal-validity.d.ts +1 -1
  238. package/dist/threading.d.ts +1 -1
  239. package/dist/tier-migration.d.ts +1 -1
  240. package/dist/tier-routing.d.ts +1 -1
  241. package/dist/topics.d.ts +1 -1
  242. package/dist/transcript.d.ts +1 -1
  243. package/dist/transfer/backup.js +2 -2
  244. package/dist/transfer/capsule-export.js +2 -2
  245. package/dist/transfer/capsule-import.js +2 -2
  246. package/dist/transfer/types.d.ts +12 -12
  247. package/dist/trust-score-stage.d.ts +1 -1
  248. package/dist/trust-score.d.ts +1 -1
  249. package/dist/{types-DTEFxFuL.d.ts → types-D1iBqhJA.d.ts} +1 -1
  250. package/dist/types.d.ts +1 -1
  251. package/dist/utility-runtime.d.ts +1 -1
  252. package/dist/verified-recall.js +8 -8
  253. package/package.json +2 -2
  254. package/src/access-observe-write-surface.ts +1007 -0
  255. package/src/access-service.ts +48 -794
  256. package/src/orchestration/self-deps.ts +9 -1
  257. package/src/storage/entity-store.ts +674 -0
  258. package/src/storage/memory-read-store.ts +756 -0
  259. package/src/storage.ts +111 -1191
  260. package/dist/chunk-L4AS7TIG.js.map +0 -1
  261. package/dist/chunk-THZ4WR5L.js.map +0 -1
  262. /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-observe-write-surface.js.map} +0 -0
  263. /package/dist/{auto-sync-G4YKBPOU.js.map → auto-sync-RZO5ZPHR.js.map} +0 -0
  264. /package/dist/{contradiction-scan-2SODRNUF.js.map → capsule-crypto-FFWJ4X4A.js.map} +0 -0
  265. /package/dist/{chunk-M5QKGHCR.js.map → chunk-3I2IASNK.js.map} +0 -0
  266. /package/dist/{chunk-JBO6UBIE.js.map → chunk-4AC57RCM.js.map} +0 -0
  267. /package/dist/{chunk-3DOD6ICJ.js.map → chunk-4I45UZ4V.js.map} +0 -0
  268. /package/dist/{chunk-TAPSNIMA.js.map → chunk-5CEJH5ZN.js.map} +0 -0
  269. /package/dist/{chunk-CXCZ4LAI.js.map → chunk-5VWRYFQE.js.map} +0 -0
  270. /package/dist/{chunk-TXALJRYO.js.map → chunk-5ZKT7WTR.js.map} +0 -0
  271. /package/dist/{chunk-PYVZKEZZ.js.map → chunk-6DA5BBLS.js.map} +0 -0
  272. /package/dist/{chunk-WS6GLJ7P.js.map → chunk-6DXXIXED.js.map} +0 -0
  273. /package/dist/{chunk-OH66EBCS.js.map → chunk-7CFGXWYX.js.map} +0 -0
  274. /package/dist/{chunk-TJATYB3F.js.map → chunk-A6XCHI53.js.map} +0 -0
  275. /package/dist/{chunk-2MHCWXX6.js.map → chunk-BTH6MQJT.js.map} +0 -0
  276. /package/dist/{chunk-G7LCG56O.js.map → chunk-CXW6Y7SI.js.map} +0 -0
  277. /package/dist/{chunk-DS4QM6I7.js.map → chunk-EEKDRAWX.js.map} +0 -0
  278. /package/dist/{chunk-GAH5755F.js.map → chunk-EF3VFQC4.js.map} +0 -0
  279. /package/dist/{chunk-437S3G37.js.map → chunk-EJJQDP3H.js.map} +0 -0
  280. /package/dist/{chunk-V4C4SFZB.js.map → chunk-EVVLGYHR.js.map} +0 -0
  281. /package/dist/{chunk-ZJMSTFOR.js.map → chunk-GBXP62CL.js.map} +0 -0
  282. /package/dist/{chunk-7AL5WNPW.js.map → chunk-GG6G2RYR.js.map} +0 -0
  283. /package/dist/{chunk-BERWPOXU.js.map → chunk-H2T4OJNI.js.map} +0 -0
  284. /package/dist/{chunk-2MR3MFQB.js.map → chunk-JTYM4HPA.js.map} +0 -0
  285. /package/dist/{chunk-LD53WPMU.js.map → chunk-LQ6JI4VH.js.map} +0 -0
  286. /package/dist/{chunk-GPFXPC2V.js.map → chunk-NG2NHB4C.js.map} +0 -0
  287. /package/dist/{chunk-COZEVVXZ.js.map → chunk-NLXKSM46.js.map} +0 -0
  288. /package/dist/{chunk-VSU2NZGC.js.map → chunk-OLAVQS6S.js.map} +0 -0
  289. /package/dist/{chunk-ER3CCWK2.js.map → chunk-ORAK3LNZ.js.map} +0 -0
  290. /package/dist/{chunk-WUVUTOUV.js.map → chunk-PBZQVVLG.js.map} +0 -0
  291. /package/dist/{chunk-PJQMISPD.js.map → chunk-PPAEJXVF.js.map} +0 -0
  292. /package/dist/{chunk-KURUULFF.js.map → chunk-PRX77NPC.js.map} +0 -0
  293. /package/dist/{chunk-24463PIV.js.map → chunk-PS572XYO.js.map} +0 -0
  294. /package/dist/{chunk-U2SMR57B.js.map → chunk-REZ4TUD2.js.map} +0 -0
  295. /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
  296. /package/dist/{chunk-V5UV7BOA.js.map → chunk-SQNED75R.js.map} +0 -0
  297. /package/dist/{chunk-FLVL4L3L.js.map → chunk-TCKP4EHY.js.map} +0 -0
  298. /package/dist/{chunk-YRBXBHU7.js.map → chunk-TXV3I2O6.js.map} +0 -0
  299. /package/dist/{chunk-CJ276MDI.js.map → chunk-UNV6ZGTS.js.map} +0 -0
  300. /package/dist/{chunk-2BFLDH5F.js.map → chunk-WEPWY4HB.js.map} +0 -0
  301. /package/dist/{chunk-YQGHW5ML.js.map → chunk-WRFKZEO6.js.map} +0 -0
  302. /package/dist/{chunk-7CAJLUNW.js.map → chunk-Y4TAFMUE.js.map} +0 -0
  303. /package/dist/{chunk-TIFF32QK.js.map → chunk-YMGQXBPS.js.map} +0 -0
  304. /package/dist/{chunk-T422SYM6.js.map → chunk-ZUDM75KG.js.map} +0 -0
  305. /package/dist/{chunk-4DVYRPLL.js.map → chunk-ZUHD6XZZ.js.map} +0 -0
  306. /package/dist/{chunk-BQ6J6P3K.js.map → chunk-ZW5SZ3TH.js.map} +0 -0
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-GDB4J2H3.js";
9
9
  import {
10
10
  importCapsule
11
- } from "./chunk-M5QKGHCR.js";
11
+ } from "./chunk-3I2IASNK.js";
12
12
  import {
13
13
  getTrustZoneStoreStatus,
14
14
  isTrustZoneName,
@@ -29,6 +29,9 @@ import {
29
29
  normalizeTags,
30
30
  parseTagMatch
31
31
  } from "./chunk-BT7NVCML.js";
32
+ import {
33
+ decideDisclosureEscalation
34
+ } from "./chunk-H7XKCNR6.js";
32
35
  import {
33
36
  toRecallExplainJson
34
37
  } from "./chunk-2KK5WNQB.js";
@@ -36,14 +39,8 @@ import {
36
39
  estimateRecallTokens
37
40
  } from "./chunk-6MMONPHM.js";
38
41
  import {
39
- decideDisclosureEscalation
40
- } from "./chunk-H7XKCNR6.js";
41
- import {
42
- recordObjectiveStateSnapshotsFromObservedMessages
43
- } from "./chunk-ICP53SKG.js";
44
- import {
45
- objectiveStateStoreOverrideForNamespace
46
- } from "./chunk-6HMYUWXR.js";
42
+ namespaceCollectionName
43
+ } from "./chunk-PS572XYO.js";
47
44
  import {
48
45
  combineNamespaces,
49
46
  defaultGitInvoker,
@@ -57,9 +54,6 @@ import {
57
54
  resolveWritableNamespaceValue,
58
55
  sanitizeFragment
59
56
  } from "./chunk-W3M34XT4.js";
60
- import {
61
- namespaceCollectionName
62
- } from "./chunk-24463PIV.js";
63
57
  import {
64
58
  namespaceIdentityFromToken
65
59
  } from "./chunk-ZFXCQPNO.js";
@@ -70,13 +64,24 @@ import {
70
64
  listMemoryGovernanceRuns,
71
65
  readMemoryGovernanceRunArtifact,
72
66
  runMemoryGovernance
73
- } from "./chunk-KURUULFF.js";
67
+ } from "./chunk-PRX77NPC.js";
68
+ import {
69
+ recordObjectiveStateSnapshotsFromObservedMessages
70
+ } from "./chunk-ICP53SKG.js";
71
+ import {
72
+ objectiveStateStoreOverrideForNamespace
73
+ } from "./chunk-6HMYUWXR.js";
74
74
  import {
75
75
  recordMemoryOutcome
76
76
  } from "./chunk-EIR5VLIH.js";
77
77
  import {
78
78
  buildGraphSnapshot
79
79
  } from "./chunk-TQT3USCG.js";
80
+ import {
81
+ persistExplicitCapture,
82
+ queueExplicitCaptureForReview,
83
+ validateExplicitCaptureInput
84
+ } from "./chunk-K5LLP4CY.js";
80
85
  import {
81
86
  wrapWorkLayerContext
82
87
  } from "./chunk-EI6V5UXY.js";
@@ -87,53 +92,20 @@ import {
87
92
  buildProcedurePersistBody,
88
93
  normalizeProcedureSteps
89
94
  } from "./chunk-QDW3E4RD.js";
90
- import {
91
- persistExplicitCapture,
92
- queueExplicitCaptureForReview,
93
- validateExplicitCaptureInput
94
- } from "./chunk-K5LLP4CY.js";
95
95
  import {
96
96
  CrossNamespaceBudget
97
97
  } from "./chunk-ODPLEWB6.js";
98
- import {
99
- defaultCapsulesDir
100
- } from "./chunk-ZY2MNJR6.js";
101
- import {
102
- filterTrajectoriesByLookbackDays,
103
- readCausalTrajectoryRecords
104
- } from "./chunk-V5UV7BOA.js";
105
- import {
106
- canReadNamespace,
107
- canWriteNamespace,
108
- defaultNamespaceForPrincipal,
109
- recallNamespacesForPrincipal,
110
- resolvePrincipal
111
- } from "./chunk-PE6XR4GG.js";
112
- import {
113
- resolveAccessSetupCapabilities,
114
- resolveCompressionCapabilities,
115
- resolveConsolidationCapabilities,
116
- resolveGraphConstructionCapabilities,
117
- resolveIdentityContinuityCapabilities,
118
- resolveIndexingCapabilities,
119
- resolveMemoryLifecycleCapabilities,
120
- resolveNamespaceCapabilities,
121
- resolveObjectiveStateCapabilities,
122
- resolveQmdCapabilities,
123
- resolveRecallAuxiliaryCapabilities,
124
- resolveRecallEnhancementCapabilities,
125
- resolveSecurityCapabilities
126
- } from "./chunk-VSF7AAGE.js";
127
98
  import {
128
99
  FileCalendarSource,
129
100
  buildBriefing,
130
101
  enqueuePassiveCorrectionNotification,
131
102
  parseBriefingFocus,
132
103
  parseBriefingWindow
133
- } from "./chunk-TIFF32QK.js";
104
+ } from "./chunk-YMGQXBPS.js";
134
105
  import {
135
- parseEntityFile
136
- } from "./chunk-THZ4WR5L.js";
106
+ parseEntityFile,
107
+ selfDeps
108
+ } from "./chunk-7APVFQ34.js";
137
109
  import {
138
110
  supersessionKeysForFact
139
111
  } from "./chunk-TKZ3AL5V.js";
@@ -147,6 +119,10 @@ import {
147
119
  DEFAULT_RECALL_DISCLOSURE,
148
120
  isRecallDisclosure
149
121
  } from "./chunk-JS2HKKOK.js";
122
+ import {
123
+ inferMemoryStatus,
124
+ toMemoryPathRel
125
+ } from "./chunk-RULE4VG5.js";
150
126
  import {
151
127
  normalizeProjectionPreview,
152
128
  normalizeProjectionTags
@@ -158,15 +134,40 @@ import {
158
134
  displayErrorDetail
159
135
  } from "./chunk-6KYMPV2O.js";
160
136
  import {
161
- inferMemoryStatus,
162
- toMemoryPathRel
163
- } from "./chunk-RULE4VG5.js";
137
+ computeContentHash
138
+ } from "./chunk-W5H6ZRPT.js";
164
139
  import {
165
140
  createVersion
166
141
  } from "./chunk-VF4XKTX3.js";
167
142
  import {
168
- computeContentHash
169
- } from "./chunk-W5H6ZRPT.js";
143
+ defaultCapsulesDir
144
+ } from "./chunk-ZY2MNJR6.js";
145
+ import {
146
+ filterTrajectoriesByLookbackDays,
147
+ readCausalTrajectoryRecords
148
+ } from "./chunk-SQNED75R.js";
149
+ import {
150
+ canReadNamespace,
151
+ canWriteNamespace,
152
+ defaultNamespaceForPrincipal,
153
+ recallNamespacesForPrincipal,
154
+ resolvePrincipal
155
+ } from "./chunk-PE6XR4GG.js";
156
+ import {
157
+ resolveAccessSetupCapabilities,
158
+ resolveCompressionCapabilities,
159
+ resolveConsolidationCapabilities,
160
+ resolveGraphConstructionCapabilities,
161
+ resolveIdentityContinuityCapabilities,
162
+ resolveIndexingCapabilities,
163
+ resolveMemoryLifecycleCapabilities,
164
+ resolveNamespaceCapabilities,
165
+ resolveObjectiveStateCapabilities,
166
+ resolveQmdCapabilities,
167
+ resolveRecallAuxiliaryCapabilities,
168
+ resolveRecallEnhancementCapabilities,
169
+ resolveSecurityCapabilities
170
+ } from "./chunk-VSF7AAGE.js";
170
171
  import {
171
172
  sanitizeMemoryContent
172
173
  } from "./chunk-5GKPFL3S.js";
@@ -181,7 +182,7 @@ import {
181
182
  } from "./chunk-NU3CSQ4H.js";
182
183
  import {
183
184
  exportCapsule
184
- } from "./chunk-FLVL4L3L.js";
185
+ } from "./chunk-TCKP4EHY.js";
185
186
  import {
186
187
  evaluateActionConfidence
187
188
  } from "./chunk-Z2YFLXHU.js";
@@ -207,6 +208,9 @@ import {
207
208
  hashAccessIdempotencyPayload
208
209
  } from "./chunk-VFUEZZBS.js";
209
210
 
211
+ // src/access-observe-write-surface.ts
212
+ import { createHash as createHash6 } from "crypto";
213
+
210
214
  // src/access-service.ts
211
215
  import { stat as stat2 } from "fs/promises";
212
216
  import * as nodeFs2 from "fs/promises";
@@ -4748,1266 +4752,1574 @@ async function promoteMemory(options) {
4748
4752
  // src/access-service.ts
4749
4753
  import * as nodePath3 from "path";
4750
4754
 
4751
- // src/access-admin-ops-surface.ts
4752
- import * as nodeFs from "fs/promises";
4753
- import * as nodePath from "path";
4754
- var AccessAdminOpsSurface = class {
4755
+ // src/access-lcm-surface.ts
4756
+ var AccessLcmSurface = class {
4755
4757
  constructor(deps) {
4756
4758
  this.deps = deps;
4757
4759
  }
4758
4760
  deps;
4759
- async reviewQueue(runId, namespace, principal) {
4760
- const resolvedNamespace = this.deps.resolveReadableNamespace(namespace, principal);
4761
- const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
4762
- const projected = await storage.getProjectedGovernanceRecord();
4763
- if (projected && (!runId || projected.runId === runId.trim())) {
4764
- const projectedAppliedActions = projected.appliedActionRows.map((row) => ({
4765
- action: row.action,
4766
- memoryId: row.memoryId,
4767
- reasonCode: row.reasonCode,
4768
- beforeStatus: row.beforeStatus,
4769
- afterStatus: row.afterStatus,
4770
- originalPath: row.originalPath,
4771
- currentPath: row.currentPath
4772
- }));
4773
- const projectedProposedActions = await buildProjectedGovernanceProposedActions(storage, projected);
4774
- const projectedArtifact = await (async () => {
4775
- try {
4776
- return await readMemoryGovernanceRunArtifact(storage.dir, projected.runId);
4777
- } catch {
4778
- return null;
4779
- }
4780
- })();
4781
- const metrics = projected.metrics;
4782
- const fallbackTransitionReport = {
4783
- proposed: groupActionsByStatus(projectedProposedActions),
4784
- applied: groupActionsByStatus(projectedAppliedActions)
4761
+ async lcmSearch(request) {
4762
+ if (!request.query || typeof request.query !== "string" || request.query.trim().length === 0) {
4763
+ throw new EngramAccessInputError("query is required and must be a non-empty string");
4764
+ }
4765
+ const principal = this.deps.resolveRequestPrincipal(request.sessionKey, request.authenticatedPrincipal);
4766
+ const hasExplicitNamespace = typeof request.namespace === "string" && request.namespace.trim().length > 0;
4767
+ const profileLcmReadNamespaces = hasExplicitNamespace ? null : this.deps.resolveScopeProfileLcmReadNamespaces(
4768
+ request.sessionKey,
4769
+ request.authenticatedPrincipal
4770
+ );
4771
+ const namespace = hasExplicitNamespace ? this.deps.resolveReadableNamespace(request.namespace, principal) : profileLcmReadNamespaces !== null ? profileLcmReadNamespaces[0] : this.deps.resolveImplicitLcmReadFallbackNamespace(principal);
4772
+ if (!this.deps.orchestrator.lcmEngine || !this.deps.orchestrator.lcmEngine.enabled) {
4773
+ return {
4774
+ query: request.query,
4775
+ namespace: namespace ?? this.deps.orchestrator.config.defaultNamespace,
4776
+ results: [],
4777
+ count: 0,
4778
+ lcmEnabled: false
4785
4779
  };
4786
- const transitionReport = projectedArtifact?.transitionReport ? {
4787
- proposed: hasGroupedGovernanceActions(projectedArtifact.transitionReport.proposed) || projectedProposedActions.length === 0 ? projectedArtifact.transitionReport.proposed : fallbackTransitionReport.proposed,
4788
- applied: hasGroupedGovernanceActions(projectedArtifact.transitionReport.applied) || projectedAppliedActions.length === 0 ? projectedArtifact.transitionReport.applied : fallbackTransitionReport.applied
4789
- } : fallbackTransitionReport;
4790
- const qualityScore = projectedArtifact?.qualityScore ?? metrics?.qualityScore ?? buildQualityScore(metrics?.reviewReasons ?? {
4791
- exact_duplicate: 0,
4792
- semantic_duplicate_candidate: 0,
4793
- disputed_memory: 0,
4794
- speculative_low_confidence: 0,
4795
- archive_candidate: 0,
4796
- explicit_capture_review: 0,
4797
- malformed_import: 0
4798
- });
4799
- const effectiveMetrics = metrics ? { ...metrics, qualityScore: metrics.qualityScore ?? qualityScore } : metrics;
4780
+ }
4781
+ if (profileLcmReadNamespaces !== null && profileLcmReadNamespaces.length === 0) {
4800
4782
  return {
4801
- found: true,
4802
- namespace: resolvedNamespace,
4803
- runId: projected.runId,
4804
- summary: projected.summary,
4805
- metrics: effectiveMetrics,
4806
- qualityScore,
4807
- reviewQueue: projected.reviewQueueRows.map((row) => ({
4808
- entryId: row.entryId,
4809
- memoryId: row.memoryId,
4810
- path: row.path,
4811
- reasonCode: row.reasonCode,
4812
- severity: row.severity,
4813
- suggestedAction: row.suggestedAction,
4814
- suggestedStatus: row.suggestedStatus,
4815
- relatedMemoryIds: row.relatedMemoryIds
4816
- })),
4817
- appliedActions: projectedAppliedActions,
4818
- transitionReport,
4819
- report: projected.report
4783
+ query: request.query,
4784
+ namespace: this.deps.orchestrator.config.defaultNamespace,
4785
+ results: [],
4786
+ count: 0,
4787
+ lcmEnabled: true
4820
4788
  };
4821
4789
  }
4822
- const resolvedRunId = runId?.trim() || (await listMemoryGovernanceRuns(storage.dir))[0];
4823
- if (!resolvedRunId) return { found: false, namespace: resolvedNamespace };
4824
- const artifact = await readMemoryGovernanceRunArtifact(storage.dir, resolvedRunId);
4825
- return {
4826
- found: true,
4827
- namespace: resolvedNamespace,
4828
- runId: resolvedRunId,
4829
- summary: artifact.summary,
4830
- metrics: artifact.metrics,
4831
- qualityScore: artifact.qualityScore,
4832
- reviewQueue: artifact.reviewQueue,
4833
- appliedActions: artifact.appliedActions,
4834
- transitionReport: artifact.transitionReport,
4835
- report: artifact.report
4836
- };
4837
- }
4838
- async governanceRun(request, principal) {
4839
- const deepSleep = this.deps.orchestrator.config.dreamsPhases.deepSleep;
4840
- if (deepSleep.enabled === false && deepSleep.enabledExplicitlySet === true) {
4841
- throw new Error("memory governance is disabled by dreams.phases.deepSleep.enabled=false");
4790
+ if (namespace === void 0) {
4791
+ return {
4792
+ query: request.query,
4793
+ namespace: this.deps.orchestrator.config.defaultNamespace,
4794
+ results: [],
4795
+ count: 0,
4796
+ lcmEnabled: true
4797
+ };
4842
4798
  }
4843
- const resolvedNamespace = this.deps.writableNamespaceFor(
4799
+ const limit = Math.max(1, Math.min(request.limit ?? 10, 100));
4800
+ const lcmReadNamespace = profileLcmReadNamespaces !== null ? profileLcmReadNamespaces[0] ?? this.deps.orchestrator.config.defaultNamespace : this.deps.resolveLcmReadNamespace(
4844
4801
  request.namespace,
4845
- void 0,
4846
- request.authenticatedPrincipal ?? principal
4802
+ namespace,
4803
+ request.sessionKey,
4804
+ request.authenticatedPrincipal
4847
4805
  );
4848
- const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
4849
- const mode = request.mode === "apply" ? "apply" : "shadow";
4850
- const boundedBatchSize = typeof request.batchSize === "number" && Number.isFinite(request.batchSize) ? Math.max(1, Math.floor(request.batchSize)) : void 0;
4851
- const result = await runMemoryGovernance({
4852
- memoryDir: storage.dir,
4853
- mode,
4854
- recentDays: typeof request.recentDays === "number" && Number.isFinite(request.recentDays) ? Math.max(1, Math.floor(request.recentDays)) : void 0,
4855
- maxMemories: typeof request.maxMemories === "number" && Number.isFinite(request.maxMemories) ? Math.max(1, Math.floor(request.maxMemories)) : void 0,
4856
- batchSize: boundedBatchSize
4857
- });
4858
- if (mode === "apply") {
4859
- try {
4860
- await this.deps.orchestrator.processEntitySynthesisQueue(
4861
- resolvedNamespace,
4862
- Math.min(boundedBatchSize ?? 5, 5)
4863
- );
4864
- } catch (error) {
4865
- log.debug(`governanceRun: entity synthesis refresh failed after governance apply: ${error}`);
4866
- }
4867
- }
4868
- return {
4869
- namespace: resolvedNamespace,
4870
- runId: result.runId,
4871
- traceId: result.traceId,
4872
- mode: result.mode,
4873
- reviewQueueCount: result.reviewQueue.length,
4874
- proposedActionCount: result.proposedActions.length,
4875
- appliedActionCount: result.appliedActions.length,
4876
- summaryPath: result.summaryPath,
4877
- reportPath: result.reportPath
4878
- };
4879
- }
4880
- async conversationIndexUpdate(request = {}) {
4881
- if (!resolveIndexingCapabilities(this.deps.orchestrator.config).conversationIndex) {
4882
- return {
4883
- enabled: false,
4884
- sessions: 0,
4885
- chunks: 0,
4886
- skipped: 0,
4887
- skippedSessionKeys: [],
4888
- embeddedRuns: 0,
4889
- reason: "disabled"
4890
- };
4891
- }
4892
- const hours = typeof request.hours === "number" && Number.isFinite(request.hours) ? Math.max(1, Math.floor(request.hours)) : 24;
4893
- let sessionKey;
4894
- if (request.sessionKey !== void 0) {
4895
- if (typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
4896
- throw new EngramAccessInputError("sessionKey must be a non-empty string when provided");
4897
- }
4898
- sessionKey = request.sessionKey.trim();
4899
- }
4900
- if (sessionKey) {
4901
- const result = await this.deps.orchestrator.updateConversationIndex(
4902
- sessionKey,
4903
- hours,
4904
- { embed: request.embed }
4905
- );
4806
+ const lcmSessionKeyIds = request.sessionKey ? profileLcmReadNamespaces !== null ? this.deps.lcmSessionIdsForNamespaces(
4807
+ profileLcmReadNamespaces,
4808
+ request.sessionKey
4809
+ ) : this.deps.resolveLcmReadSessionIds(
4810
+ request.namespace,
4811
+ namespace,
4812
+ request.sessionKey,
4813
+ request.authenticatedPrincipal
4814
+ ) : [void 0];
4815
+ const lcmSessionPrefixes = request.sessionPrefix ? profileLcmReadNamespaces !== null && !request.sessionKey ? this.deps.lcmSessionIdsForNamespaces(
4816
+ profileLcmReadNamespaces,
4817
+ request.sessionPrefix
4818
+ ) : [
4819
+ lcmSessionKeyForNamespace(
4820
+ lcmReadNamespace,
4821
+ request.sessionPrefix,
4822
+ this.deps.orchestrator.config.defaultNamespace
4823
+ ) ?? request.sessionPrefix
4824
+ ] : [void 0];
4825
+ const hasScopedSession = typeof request.sessionKey === "string" && request.sessionKey.length > 0 || lcmSessionPrefixes.some((prefix) => typeof prefix === "string" && prefix.length > 0);
4826
+ if (!hasScopedSession && resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces === true) {
4906
4827
  return {
4907
- enabled: true,
4908
- sessionKey,
4909
- sessions: 1,
4910
- chunks: result.chunks,
4911
- skipped: result.skipped ? 1 : 0,
4912
- skippedSessionKeys: result.skipped ? [sessionKey] : [],
4913
- embeddedRuns: result.embedded ? 1 : 0,
4914
- reason: result.reason,
4915
- retryAfterMs: result.retryAfterMs
4828
+ query: request.query,
4829
+ namespace,
4830
+ results: [],
4831
+ count: 0,
4832
+ lcmEnabled: true
4916
4833
  };
4917
4834
  }
4918
- const sessionKeys = await this.deps.orchestrator.transcript.listSessionKeys();
4919
- let chunks = 0;
4920
- let skipped = 0;
4921
- const skippedSessionKeys = [];
4922
- let embeddedRuns = 0;
4923
- for (const sessionKey2 of sessionKeys) {
4924
- const result = await this.deps.orchestrator.updateConversationIndex(
4925
- sessionKey2,
4926
- hours,
4927
- { embed: request.embed }
4928
- );
4929
- chunks += result.chunks;
4930
- if (result.skipped) {
4931
- skipped += 1;
4932
- skippedSessionKeys.push(sessionKey2);
4933
- }
4934
- if (result.embedded) {
4935
- embeddedRuns += 1;
4835
+ const seenRows = /* @__PURE__ */ new Set();
4836
+ const results = [];
4837
+ const lcmSearches = [];
4838
+ for (const lcmSessionKey of lcmSessionKeyIds) {
4839
+ for (const lcmSessionPrefix of lcmSessionPrefixes) {
4840
+ lcmSearches.push({
4841
+ key: lcmSessionKey,
4842
+ prefix: lcmSessionPrefix,
4843
+ promise: this.deps.orchestrator.lcmEngine.searchContextFull(
4844
+ request.query,
4845
+ limit,
4846
+ lcmSessionKey,
4847
+ lcmSessionPrefix
4848
+ )
4849
+ });
4936
4850
  }
4937
4851
  }
4938
- return {
4939
- enabled: true,
4940
- sessions: sessionKeys.length,
4941
- chunks,
4942
- skipped,
4943
- skippedSessionKeys,
4944
- embeddedRuns
4945
- };
4946
- }
4947
- async profilingReport(request = {}) {
4948
- const profiler = this.deps.orchestrator.profiler;
4949
- if (!profiler.isEnabled) {
4950
- return {
4951
- enabled: false,
4952
- reason: "disabled",
4953
- message: "Profiling is disabled. Set profilingEnabled: true in your plugin config to enable."
4954
- };
4955
- }
4956
- const format = request.format ?? "ascii";
4957
- if (format !== "ascii" && format !== "json") {
4958
- throw new EngramAccessInputError("format must be one of: ascii, json");
4959
- }
4960
- const limit = request.limit ?? 5;
4961
- if (!Number.isInteger(limit) || limit < 1 || limit > 20) {
4962
- throw new EngramAccessInputError("limit must be an integer between 1 and 20");
4963
- }
4964
- const traces = profiler.getRecentTraces(limit);
4965
- const stats = profiler.getStats();
4966
- const bottleneck = profiler.identifyBottleneck();
4967
- if (format === "json") {
4968
- return {
4969
- enabled: true,
4970
- format,
4971
- traces,
4972
- stats,
4973
- bottleneck
4974
- };
4975
- }
4976
- const lines = [];
4977
- lines.push("Engram Profiling Report");
4978
- lines.push("=".repeat(60));
4979
- lines.push("");
4980
- const allBuckets = [
4981
- ["byKind", stats.byKind],
4982
- ["bySpan", stats.bySpan]
4983
- ];
4984
- const hasStats = allBuckets.some(([, entries]) => Object.keys(entries).length > 0);
4985
- if (hasStats) {
4986
- lines.push("Aggregate Stats (all retained traces):");
4987
- for (const [bucket, entries] of allBuckets) {
4988
- for (const [key, summary] of Object.entries(entries)) {
4989
- lines.push(
4990
- ` ${bucket}/${key}: avg=${summary.avgMs}ms p50=${summary.p50Ms}ms p95=${summary.p95Ms}ms max=${summary.maxMs}ms (n=${summary.count})`
4991
- );
4992
- }
4852
+ const settledSearches = await Promise.allSettled(
4853
+ lcmSearches.map((search) => search.promise)
4854
+ );
4855
+ for (let i = 0; i < settledSearches.length; i += 1) {
4856
+ if (results.length >= limit) break;
4857
+ const settled = settledSearches[i];
4858
+ if (!settled || settled.status === "rejected") {
4859
+ const failed = lcmSearches[i];
4860
+ log.warn(
4861
+ `lcmSearch: failed for key=${failed?.key ?? "<none>"} prefix=${failed?.prefix ?? "<none>"}: ${settled?.status === "rejected" ? settled.reason : "missing result"}`
4862
+ );
4863
+ continue;
4993
4864
  }
4994
- lines.push("");
4995
- }
4996
- if (bottleneck) {
4997
- lines.push(`Bottleneck: ${bottleneck}`);
4998
- lines.push("");
4999
- }
5000
- if (traces.length === 0) {
5001
- lines.push("No traces recorded yet. Trigger a recall or extraction to see timing data.");
5002
- } else {
5003
- for (const trace of traces) {
5004
- lines.push(formatProfileTraceAscii(trace));
5005
- lines.push("");
4865
+ for (const r of settled.value) {
4866
+ const dedupeKey = `${r.session_id}\0${r.turn_index}`;
4867
+ if (seenRows.has(dedupeKey)) continue;
4868
+ seenRows.add(dedupeKey);
4869
+ results.push({
4870
+ sessionId: r.session_id,
4871
+ content: r.content,
4872
+ turnIndex: r.turn_index
4873
+ });
4874
+ if (results.length >= limit) break;
5006
4875
  }
5007
4876
  }
5008
4877
  return {
5009
- enabled: true,
5010
- format,
5011
- report: lines.join("\n")
4878
+ query: request.query,
4879
+ namespace,
4880
+ results,
4881
+ count: results.length,
4882
+ lcmEnabled: true
5012
4883
  };
5013
4884
  }
5014
4885
  /**
5015
- * Read-only graph snapshot for the admin pane (issue #691 PR 2/5).
4886
+ * Resolve the effective LCM NAMESPACE a same-session operation must prefix
4887
+ * with (the namespace half of {@link resolveLcmReadSessionKey}). Split out so
4888
+ * `lcmSearch` can apply ONE namespace to BOTH its `sessionKey` and its
4889
+ * `sessionPrefix` — the prefix is a search fragment, not a real session, so its
4890
+ * own coding context can't be looked up; it must inherit the namespace resolved
4891
+ * from the real session (`sessionKeyForOverlay`).
5016
4892
  *
5017
- * Reads adjacency from the JSONL edge store written by `GraphIndex` and
5018
- * resolves node metadata via the namespaced storage manager. Namespace
5019
- * resolution mirrors the read-side path used by `recall` /
5020
- * `procedureStats`, so multi-principal deployments can't leak edges from
5021
- * a peer namespace (CLAUDE.md rule 42).
4893
+ * `purpose` selects the AUTHORIZATION gate applied before honouring the
4894
+ * coding overlay (#1505 round 3 + round 4, codex P2):
4895
+ *
4896
+ * - `"read"` (`lcmSearch` / raw-excerpt recall): the overlay rows are only
4897
+ * visible when the principal SELF base is in the READABLE RECALL SET — the
4898
+ * same gate the orchestrator's `lcmReadNamespaceForSession` and the recall
4899
+ * namespace set use (`recallNamespacesForPrincipal`, gated by both
4900
+ * `defaultRecallNamespaces.includes("self")` AND `canReadNamespace`). A
4901
+ * caller that passed the default read check must NOT receive
4902
+ * `<principal>-project-*` rows the policy never granted (cross-tenant read
4903
+ * leak). When the self base is not readable, keep the just-authorized
4904
+ * namespace (collapses to the raw key on the default store).
4905
+ *
4906
+ * - `"write"` (`lcmCompactionFlush` / `lcmCompactionRecord`): these are
4907
+ * write/maintenance operations on the SAME queue `observe` just wrote, so
4908
+ * the gate must mirror observe's WRITE authorization (`canWriteNamespace`
4909
+ * on the self base), NOT readability. A principal that can WRITE but not
4910
+ * READ its self namespace (or whose `defaultRecallNamespaces` omits `self`)
4911
+ * archived under the overlay key via `observe`; gating compaction by
4912
+ * readability would fall back to the default/raw key and leave that queue
4913
+ * never flushed/recorded (round-4 codex P2). Write-authorized ⇒ overlay
4914
+ * key, matching the observe write key (rule 42 read/write parity; rule 39
4915
+ * identical gates across paths).
5022
4916
  */
5023
- async graphSnapshot(request, authenticatedPrincipal) {
5024
- const namespace = this.deps.resolveReadableNamespace(
5025
- request.namespace,
4917
+ resolveLcmReadNamespace(explicitNamespace, resolvedNamespace, sessionKeyForOverlay, authenticatedPrincipal, purpose = "read") {
4918
+ const hasExplicitNamespace = typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
4919
+ if (hasExplicitNamespace) return resolvedNamespace;
4920
+ const principal = this.deps.resolveRequestPrincipal(
4921
+ sessionKeyForOverlay,
5026
4922
  authenticatedPrincipal
5027
4923
  );
5028
- const storage = await this.deps.orchestrator.getStorage(namespace);
5029
- const cfg = this.deps.orchestrator.config;
5030
- const graphCaps = resolveGraphConstructionCapabilities(cfg);
5031
- let namespaceRootReal;
5032
- try {
5033
- namespaceRootReal = await nodeFs.realpath(storage.dir);
5034
- } catch {
5035
- namespaceRootReal = nodePath.resolve(storage.dir);
5036
- }
5037
- const namespaceRootWithSep = namespaceRootReal.endsWith(nodePath.sep) ? namespaceRootReal : namespaceRootReal + nodePath.sep;
5038
- const loadNode = async (relPath) => {
5039
- if (nodePath.isAbsolute(relPath)) {
5040
- log.warn(
5041
- `graphSnapshot: rejected absolute edge endpoint (len=${relPath.length}) outside namespace root`
5042
- );
5043
- return null;
5044
- }
5045
- const candidate = nodePath.resolve(namespaceRootReal, relPath);
5046
- if (candidate !== namespaceRootReal && !candidate.startsWith(namespaceRootWithSep)) {
5047
- log.warn(
5048
- `graphSnapshot: rejected traversing edge endpoint (len=${relPath.length}) outside namespace root`
5049
- );
5050
- return null;
5051
- }
5052
- let canonical;
5053
- try {
5054
- canonical = await nodeFs.realpath(candidate);
5055
- } catch {
5056
- canonical = candidate;
5057
- }
5058
- if (canonical !== namespaceRootReal && !canonical.startsWith(namespaceRootWithSep)) {
5059
- log.warn(
5060
- `graphSnapshot: rejected symlinked edge endpoint (len=${relPath.length}) that resolved outside namespace root`
5061
- );
5062
- return null;
5063
- }
5064
- const memory = await storage.readMemoryByPath(canonical);
5065
- if (!memory) return null;
5066
- const fm = memory.frontmatter;
5067
- return {
5068
- category: fm.category ?? "unknown",
5069
- label: fm.id ?? nodePath.basename(canonical, nodePath.extname(canonical)),
5070
- updated: fm.updated
5071
- };
5072
- };
5073
- return buildGraphSnapshot({
5074
- memoryDir: namespaceRootReal,
5075
- graphConfig: {
5076
- entityGraph: graphCaps.entityGraph,
5077
- timeGraph: graphCaps.timeGraph,
5078
- causalGraph: graphCaps.causalGraph
5079
- },
5080
- request: {
5081
- limit: request.limit,
5082
- since: request.since,
5083
- focusNodeId: request.focusNodeId,
5084
- categories: request.categories
5085
- },
5086
- loadNode
5087
- });
4924
+ const base = defaultNamespaceForPrincipal(principal, this.deps.orchestrator.config);
4925
+ const overlaid = this.deps.orchestrator.applyCodingNamespaceOverlay(
4926
+ sessionKeyForOverlay,
4927
+ base
4928
+ );
4929
+ if (overlaid === base) return this.deps.orchestrator.config.defaultNamespace;
4930
+ const authorized = purpose === "write" ? canWriteNamespace(principal, base, this.deps.orchestrator.config) : recallNamespacesForPrincipal(
4931
+ principal,
4932
+ this.deps.orchestrator.config
4933
+ ).includes(base);
4934
+ if (authorized) return overlaid;
4935
+ if (purpose === "read") return this.deps.orchestrator.config.defaultNamespace;
4936
+ return resolvedNamespace;
5088
4937
  }
5089
4938
  /**
5090
- * List capsule archives in the namespace-scoped capsule store.
4939
+ * Resolve the namespace the raw-disclosure excerpt lookup
4940
+ * ({@link fetchRawExcerpts}) must prefix its LCM `session_id` with (#1505
4941
+ * thread 2f7). Raw disclosure reads the SAME LCM archive `lcmSearch` and the
4942
+ * in-prompt LCM sections read, so it MUST pass through the identical
4943
+ * read-authorization gate — NOT `snapshot.namespace`, which records the
4944
+ * effective WRITE/overlay namespace (`<principal>-project-*`) even when the
4945
+ * principal can WRITE but not READ its self base (or `defaultRecallNamespaces`
4946
+ * omits `self`). Routing through `resolveLcmReadNamespace(..., "read")` makes
4947
+ * raw disclosure fall back to the default store exactly like normal recall +
4948
+ * `lcmSearch`, so it never attaches overlay transcript rows the read gate
4949
+ * excludes (cross-tenant read leak). Collapses to the default store / raw
4950
+ * sessionKey for single-store / no-overlay / explicit-default flows, so
4951
+ * single-user recall is byte-for-byte unchanged.
5091
4952
  *
5092
- * MCP uses this access-layer method instead of reading arbitrary paths so
5093
- * capsule discovery remains bound to the same namespace ACLs as export and
5094
- * import.
5095
- */
5096
- async capsuleList(options) {
5097
- const resolvedNamespace = this.deps.resolveReadableNamespace(options?.namespace, options?.principal);
5098
- const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
5099
- const capsulesDir = defaultCapsulesDir(storage.dir);
5100
- let dirEntries;
5101
- try {
5102
- const capsulesDirStat = await nodeFs.lstat(capsulesDir);
5103
- if (capsulesDirStat.isSymbolicLink()) {
5104
- throw new EngramAccessInputError("capsule list failed: capsule store directory must not be a symlink");
5105
- }
5106
- if (!capsulesDirStat.isDirectory()) {
5107
- throw new EngramAccessInputError("capsule list failed: capsule store path must be a directory");
5108
- }
5109
- dirEntries = await nodeFs.readdir(capsulesDir, { withFileTypes: true });
5110
- } catch (err) {
5111
- const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
5112
- if (code === "ENOENT") {
5113
- return { namespace: resolvedNamespace, capsulesDir, capsules: [] };
5114
- }
5115
- throw err;
5116
- }
5117
- const archiveNames = dirEntries.filter(
5118
- (entry) => entry.isFile() && (entry.name.endsWith(".capsule.json.gz") || entry.name.endsWith(".capsule.json.gz.enc"))
5119
- ).map((entry) => entry.name).sort();
5120
- const capsules = [];
5121
- for (const archiveName of archiveNames) {
5122
- const archivePath = nodePath.join(capsulesDir, archiveName);
5123
- const id = archiveName.replace(/\.capsule\.json\.gz\.enc$/, "").replace(/\.capsule\.json\.gz$/, "");
5124
- const manifestPath = nodePath.join(capsulesDir, `${id}.manifest.json`);
5125
- let createdAt = null;
5126
- let pluginVersion = null;
5127
- let fileCount = null;
5128
- let description = null;
5129
- let manifestPathOrNull = manifestPath;
5130
- try {
5131
- const manifestStat = await nodeFs.lstat(manifestPath);
5132
- if (manifestStat.isSymbolicLink() || !manifestStat.isFile()) {
5133
- capsules.push({
5134
- id,
5135
- archivePath,
5136
- manifestPath: manifestPathOrNull,
5137
- createdAt,
5138
- pluginVersion,
5139
- fileCount,
5140
- description
5141
- });
5142
- continue;
5143
- }
5144
- const raw = await nodeFs.readFile(manifestPath, "utf-8");
5145
- const sidecar = JSON.parse(raw);
5146
- createdAt = typeof sidecar.createdAt === "string" ? sidecar.createdAt : null;
5147
- pluginVersion = typeof sidecar.pluginVersion === "string" ? sidecar.pluginVersion : null;
5148
- fileCount = Array.isArray(sidecar.files) ? sidecar.files.length : null;
5149
- const capsule = sidecar.capsule;
5150
- description = capsule && typeof capsule.description === "string" ? capsule.description : null;
5151
- } catch (err) {
5152
- const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
5153
- if (code === "ENOENT") {
5154
- manifestPathOrNull = null;
5155
- }
5156
- }
5157
- capsules.push({
5158
- id,
5159
- archivePath,
5160
- manifestPath: manifestPathOrNull,
5161
- createdAt,
5162
- pluginVersion,
5163
- fileCount,
5164
- description
5165
- });
5166
- }
5167
- return { namespace: resolvedNamespace, capsulesDir, capsules };
5168
- }
5169
- /**
5170
- * Manually invoke a single Dreams phase pass (PR 4/4).
4953
+ * Returns `undefined` when NO readable LCM namespace exists for an IMPLICIT
4954
+ * (no explicit `namespace`) raw recall i.e. a restrictive `default` READ
4955
+ * policy denies the principal `default` AND no overlay/self namespace is
4956
+ * readable. In that case the caller emits NO excerpts rather than throwing
4957
+ * `namespace is not readable: default` (#1505 thread NBHWz): normal recall
4958
+ * still succeeds via `recallNamespacesForPrincipal`, so `disclosure: "raw"`
4959
+ * must degrade gracefully (empty excerpts), never pre-authorize `default`.
5171
4960
  *
5172
- * Deep-sleep delegates to memory governance (shadow → dry-run, apply → live).
5173
- * Light-sleep and REM scan the observation ledger and memory corpus
5174
- * respectively, returning the same telemetry shape as a scheduled run.
4961
+ * IMPLICIT-namespace fallback selection derives from the ALREADY
4962
+ * read-authorized recall namespace set (`recallNamespacesForPrincipal` +
4963
+ * `canReadNamespace`) the principal's self base when it is in the readable
4964
+ * recall set, else `config.defaultNamespace` ONLY when the principal may read
4965
+ * it. It NEVER pre-authorizes `default`. An EXPLICIT `namespace` is still
4966
+ * authorized strictly via `resolveReadableNamespace` (explicit reads must pass
4967
+ * the ACL — no behavior change).
5175
4968
  */
5176
- async dreamsRun(options) {
5177
- const { runDreamsPhase } = await import("./dreams-ledger-6UODQOX6.js");
5178
- const validPhases = ["lightSleep", "rem", "deepSleep"];
5179
- if (!validPhases.includes(options.phase)) {
5180
- throw new EngramAccessInputError(
5181
- `Invalid phase: ${String(options.phase)}. Must be one of: ${validPhases.join(", ")}`
4969
+ resolveRawExcerptReadNamespace(explicitNamespace, sessionKey, authenticatedPrincipal) {
4970
+ const principal = this.deps.resolveRequestPrincipal(
4971
+ sessionKey,
4972
+ authenticatedPrincipal
4973
+ );
4974
+ const hasExplicitNamespace = typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
4975
+ if (hasExplicitNamespace) {
4976
+ const resolvedNamespace = this.deps.resolveReadableNamespace(
4977
+ explicitNamespace,
4978
+ principal
5182
4979
  );
5183
- }
5184
- const deepSleep = this.deps.orchestrator.config.dreamsPhases.deepSleep;
5185
- if (options.phase === "deepSleep" && deepSleep.enabled === false && deepSleep.enabledExplicitlySet === true) {
5186
- throw new EngramAccessInputError(
5187
- "memory governance is disabled by dreams.phases.deepSleep.enabled=false"
4980
+ return this.deps.resolveLcmReadNamespace(
4981
+ explicitNamespace,
4982
+ resolvedNamespace,
4983
+ sessionKey,
4984
+ authenticatedPrincipal,
4985
+ "read"
5188
4986
  );
5189
4987
  }
5190
- const dryRun = options.dryRun === true;
5191
- const resolvedNamespace = this.deps.writableNamespaceFor(
5192
- options.namespace,
5193
- void 0,
5194
- options.authenticatedPrincipal
4988
+ const profileReadNamespaces = this.deps.resolveScopeProfileLcmReadNamespaces(
4989
+ sessionKey,
4990
+ authenticatedPrincipal
5195
4991
  );
5196
- const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
5197
- const memoryDir = storage.dir;
5198
- const phaseRunner = dryRun || options.phase === "deepSleep" ? void 0 : async (_opts) => {
5199
- if (_opts.phase === "lightSleep") {
5200
- const result3 = await this.deps.orchestrator.runLifecyclePolicyNow(storage);
5201
- return {
5202
- itemsProcessed: result3.memoriesAssessed,
5203
- notes: `scored ${result3.memoriesAssessed} memories`
5204
- };
5205
- }
5206
- const result2 = await this.deps.orchestrator.runSemanticConsolidationNow({
5207
- dryRun: false,
5208
- storage
5209
- });
5210
- const itemsProcessed = result2.clusters.reduce(
5211
- (sum, cluster) => sum + cluster.memories.length,
5212
- 0
5213
- );
5214
- return {
5215
- itemsProcessed,
5216
- notes: `REM consolidation found ${result2.clustersFound} clusters`
5217
- };
5218
- };
5219
- const governanceRunner = options.phase === "deepSleep" ? async (_opts) => {
5220
- return this.deps.orchestrator.runDeepSleepGovernanceNow({
5221
- storage,
5222
- dryRun: _opts.dryRun
5223
- });
5224
- } : void 0;
5225
- const result = await runDreamsPhase(
5226
- { memoryDir, phase: options.phase, dryRun },
5227
- governanceRunner,
5228
- phaseRunner
4992
+ if (profileReadNamespaces !== null) return profileReadNamespaces[0];
4993
+ const fallbackNamespace = this.deps.resolveImplicitLcmReadFallbackNamespace(principal);
4994
+ if (fallbackNamespace === void 0) return void 0;
4995
+ return this.deps.resolveLcmReadNamespace(
4996
+ explicitNamespace,
4997
+ fallbackNamespace,
4998
+ sessionKey,
4999
+ authenticatedPrincipal,
5000
+ "read"
5229
5001
  );
5230
- return {
5231
- phase: result.phase,
5232
- dryRun: result.dryRun,
5233
- durationMs: result.durationMs,
5234
- itemsProcessed: result.itemsProcessed,
5235
- notes: result.notes
5236
- };
5237
5002
  }
5238
- /**
5239
- * Manually promote a memory into one or more authorized targets. Requires
5240
- * a non-empty reason (audit-logged). Reuses the scope-profile promotion
5241
- * resolution and `canWriteNamespace` gate — there is no dashboard-only
5242
- * write path.
5243
- */
5244
- async adminPromoteMemory(request) {
5245
- const config = this.deps.orchestrator.config;
5246
- const namespacesEnabled = resolveNamespaceCapabilities(config).namespaces === true;
5247
- let sourceNamespace;
5248
- if (request.namespace) {
5249
- sourceNamespace = this.deps.resolveReadableNamespace(
5250
- request.namespace,
5251
- request.principal
5252
- );
5253
- } else {
5254
- const codingContext2 = request.sessionKey ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey) ?? null : null;
5255
- const plan = resolveScopePlan({
5256
- config,
5257
- sessionKey: request.sessionKey,
5258
- principalOverride: request.principal,
5259
- codingContext: codingContext2,
5260
- namespacesEnabled
5261
- });
5262
- sourceNamespace = this.deps.resolveReadableNamespace(
5263
- plan.baseNamespace,
5264
- request.principal
5265
- );
5003
+ resolveLcmReadSessionIds(explicitNamespace, resolvedNamespace, sessionKey, authenticatedPrincipal) {
5004
+ const primary = this.deps.resolveLcmReadSessionKey(
5005
+ explicitNamespace,
5006
+ resolvedNamespace,
5007
+ sessionKey,
5008
+ authenticatedPrincipal,
5009
+ "read"
5010
+ );
5011
+ const hasExplicitNamespace = typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
5012
+ if (hasExplicitNamespace) return [primary];
5013
+ const profileReadNamespaces = this.deps.resolveScopeProfileLcmReadNamespaces(
5014
+ sessionKey,
5015
+ authenticatedPrincipal
5016
+ );
5017
+ if (profileReadNamespaces !== null) {
5018
+ return this.deps.lcmSessionIdsForNamespaces(profileReadNamespaces, sessionKey);
5266
5019
  }
5267
- const codingContext = request.sessionKey ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey) ?? null : null;
5268
- const codingOverlay = codingContext ? resolveCodingNamespaceOverlay(
5269
- codingContext,
5270
- config.codingMode,
5271
- config.defaultNamespace
5272
- ) : null;
5273
- const scopeProfilePlan = resolveScopeProfilePlan({
5274
- config,
5275
- principal: request.principal,
5276
- codingContext,
5277
- codingOverlay
5278
- });
5279
- const storage = {
5280
- readMemory: async (namespace, memoryId) => {
5281
- const resolved = await this.deps.orchestrator.getStorage(namespace);
5282
- const memory = await resolved.getMemoryById(memoryId);
5283
- if (!memory) return null;
5284
- return {
5285
- category: memory.frontmatter.category,
5286
- content: memory.content,
5287
- frontmatter: memory.frontmatter
5288
- };
5289
- },
5290
- writePromotedMemory: async (namespace, memory) => {
5291
- const resolved = await this.deps.orchestrator.getStorage(namespace);
5292
- const { id, tombstoneBlocked } = await resolved.writeMemory(
5293
- memory.category,
5294
- memory.content,
5295
- {
5296
- confidence: memory.confidence,
5297
- tags: memory.tags,
5298
- entityRef: memory.entityRef,
5299
- source: `admin-promotion:${memory.sourceNamespace}:${memory.reason.slice(0, 120)}`,
5300
- lineage: memory.lineage,
5301
- sourceMemoryId: memory.sourceMemoryId,
5302
- actor: memory.actor,
5303
- validAt: memory.validAt
5304
- }
5305
- );
5306
- if (tombstoneBlocked) {
5307
- throw new Error(
5308
- "target namespace tombstone-blocked the promoted content (pending_review)"
5309
- );
5310
- }
5311
- return id;
5312
- }
5313
- };
5314
- try {
5315
- return await promoteMemory({
5316
- config,
5317
- sourceMemoryId: request.sourceMemoryId,
5318
- sourceNamespace,
5319
- principal: request.principal,
5320
- targets: request.targets,
5321
- reason: request.reason,
5322
- actor: request.principal ?? "admin-console",
5323
- storage,
5324
- scopeProfilePlan
5325
- });
5326
- } catch (err) {
5327
- if (err instanceof AdminPromotionError) {
5328
- throw new EngramAccessInputError(err.message);
5020
+ const principal = this.deps.resolveRequestPrincipal(
5021
+ sessionKey,
5022
+ authenticatedPrincipal
5023
+ );
5024
+ const base = defaultNamespaceForPrincipal(
5025
+ principal,
5026
+ this.deps.orchestrator.config
5027
+ );
5028
+ const overlaid = this.deps.orchestrator.applyCodingNamespaceOverlay(
5029
+ sessionKey,
5030
+ base
5031
+ );
5032
+ if (overlaid === base) return [primary];
5033
+ const selfReadableInRecall = recallNamespacesForPrincipal(
5034
+ principal,
5035
+ this.deps.orchestrator.config
5036
+ ).includes(base);
5037
+ if (!selfReadableInRecall) return [primary];
5038
+ const overlay = resolveCodingNamespaceOverlay(
5039
+ this.deps.orchestrator.getCodingContextForSession(sessionKey),
5040
+ this.deps.orchestrator.config.codingMode,
5041
+ this.deps.orchestrator.config.defaultNamespace
5042
+ );
5043
+ const fallbackNamespaces = (overlay?.readFallbacks ?? []).map(
5044
+ (fallback) => combineNamespaces(base, fallback)
5045
+ );
5046
+ const out = [primary];
5047
+ const seen = /* @__PURE__ */ new Set([primary]);
5048
+ for (const ns of fallbackNamespaces) {
5049
+ const key = lcmSessionKeyForNamespace(
5050
+ ns,
5051
+ sessionKey,
5052
+ this.deps.orchestrator.config.defaultNamespace
5053
+ ) ?? sessionKey;
5054
+ if (!seen.has(key)) {
5055
+ seen.add(key);
5056
+ out.push(key);
5329
5057
  }
5330
- throw err;
5331
5058
  }
5059
+ return out;
5332
5060
  }
5333
5061
  };
5334
5062
 
5335
- // src/access-recall-surface.ts
5336
- import { createHash as createHash4 } from "crypto";
5337
- import * as nodePath2 from "path";
5338
-
5339
- // src/orchestration/qmd-result-resolver.ts
5340
- import path8 from "path";
5341
- function qmdCollectionPathParts(resultPath) {
5342
- if (!resultPath || path8.isAbsolute(resultPath)) return null;
5343
- const normalized = resultPath.replace(/\\/g, "/").replace(/^\/+/, "");
5344
- const slashIndex = normalized.indexOf("/");
5345
- if (slashIndex <= 0 || slashIndex >= normalized.length - 1) return null;
5346
- const collection = normalized.slice(0, slashIndex);
5347
- if (/^\d{4}-\d{2}-\d{2}$/.test(collection)) return null;
5348
- return {
5349
- collection,
5350
- relativePath: normalized.slice(slashIndex + 1)
5351
- };
5352
- }
5353
- function qmdResultPathCandidates(storageDir, resultPath) {
5354
- const candidates = /* @__PURE__ */ new Set();
5355
- const storageRoot = path8.resolve(storageDir);
5356
- const addCandidate = (candidate) => {
5357
- const resolved = path8.resolve(candidate);
5358
- if (isPathInsideStorageRoot(storageRoot, resolved)) {
5359
- candidates.add(resolved);
5360
- }
5361
- };
5362
- const addRelativeCandidates = (relativePath) => {
5363
- const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
5364
- if (!normalized) return;
5365
- addCandidate(path8.join(storageRoot, normalized));
5366
- if (/^\d{4}-\d{2}-\d{2}\//.test(normalized)) {
5367
- addCandidate(path8.join(storageRoot, "facts", normalized));
5368
- }
5369
- };
5370
- if (path8.isAbsolute(resultPath)) {
5371
- addCandidate(resultPath);
5372
- } else {
5373
- addRelativeCandidates(resultPath);
5374
- }
5375
- return [...candidates];
5376
- }
5377
- function storageDirOrNull(storage) {
5378
- const dir = storage.dir;
5379
- return typeof dir === "string" && dir.length > 0 ? dir : null;
5380
- }
5381
- var QmdResultResolver = class {
5382
- getConfig;
5383
- storageFor;
5384
- storageDirNamespace;
5385
- qmdCollectionNamespaceFromPrefix;
5386
- namespaceFromPath;
5387
- constructor(options) {
5388
- this.getConfig = options.getConfig;
5389
- this.storageFor = options.storageFor;
5390
- this.storageDirNamespace = options.storageDirNamespace;
5391
- this.qmdCollectionNamespaceFromPrefix = options.qmdCollectionNamespaceFromPrefix;
5392
- this.namespaceFromPath = options.namespaceFromPath;
5063
+ // src/access-admin-ops-surface.ts
5064
+ import * as nodeFs from "fs/promises";
5065
+ import * as nodePath from "path";
5066
+ var AccessAdminOpsSurface = class {
5067
+ constructor(deps) {
5068
+ this.deps = deps;
5393
5069
  }
5394
- async readQmdResultMemory(resultPath, fallbackStorage, recallNamespaces = []) {
5395
- const parts = qmdCollectionPathParts(resultPath);
5396
- const fallbackStorageDir = storageDirOrNull(fallbackStorage);
5397
- const config = this.getConfig();
5398
- const coldCollection = config.qmdColdCollection ?? "openclaw-engram-cold";
5399
- if (parts && parts.collection === coldCollection) {
5400
- const storages = [];
5401
- const seenStorageDirs = /* @__PURE__ */ new Set();
5402
- const addStorage = (storage) => {
5403
- const storageDir = storageDirOrNull(storage);
5404
- const storageKey = storageDir ? path8.resolve(storageDir) : `storage-without-dir-${storages.length}`;
5405
- if (seenStorageDirs.has(storageKey)) return;
5406
- seenStorageDirs.add(storageKey);
5407
- storages.push(storage);
5408
- };
5409
- const fallbackNamespace = fallbackStorageDir !== null ? this.storageDirNamespace(fallbackStorageDir) : config.defaultNamespace;
5410
- if (recallNamespaces.length === 0 || !resolveNamespaceCapabilities(config).namespaces || recallNamespaces.includes(fallbackNamespace)) {
5411
- addStorage(fallbackStorage);
5412
- }
5413
- if (recallNamespaces.length > 0) {
5414
- for (const namespace of recallNamespaces) {
5415
- try {
5416
- addStorage(await this.storageFor(namespace));
5417
- } catch (err) {
5418
- log.debug("qmd cold result namespace storage lookup skipped", {
5419
- path: resultPath,
5420
- namespace,
5421
- error: err.message
5422
- });
5423
- }
5424
- }
5425
- }
5426
- for (const storage of storages) {
5427
- const storageDir = storageDirOrNull(storage);
5428
- if (!storageDir) {
5429
- const memory = await storage.readMemoryByPath(resultPath);
5430
- if (memory) return memory;
5431
- continue;
5432
- }
5070
+ deps;
5071
+ async reviewQueue(runId, namespace, principal) {
5072
+ const resolvedNamespace = this.deps.resolveReadableNamespace(namespace, principal);
5073
+ const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
5074
+ const projected = await storage.getProjectedGovernanceRecord();
5075
+ if (projected && (!runId || projected.runId === runId.trim())) {
5076
+ const projectedAppliedActions = projected.appliedActionRows.map((row) => ({
5077
+ action: row.action,
5078
+ memoryId: row.memoryId,
5079
+ reasonCode: row.reasonCode,
5080
+ beforeStatus: row.beforeStatus,
5081
+ afterStatus: row.afterStatus,
5082
+ originalPath: row.originalPath,
5083
+ currentPath: row.currentPath
5084
+ }));
5085
+ const projectedProposedActions = await buildProjectedGovernanceProposedActions(storage, projected);
5086
+ const projectedArtifact = await (async () => {
5433
5087
  try {
5434
- const coldRoot = path8.join(storageDir, "cold");
5435
- for (const candidate of qmdResultPathCandidates(
5436
- coldRoot,
5437
- parts.relativePath
5438
- )) {
5439
- const memory = await storage.readMemoryByPath(candidate);
5440
- if (memory) return memory;
5441
- }
5442
- } catch (err) {
5443
- if (err instanceof SecureStoreLockedError) throw err;
5444
- log.debug("qmd cold result path lookup failed open", {
5445
- path: resultPath,
5446
- collection: coldCollection,
5447
- error: err.message
5448
- });
5088
+ return await readMemoryGovernanceRunArtifact(storage.dir, projected.runId);
5089
+ } catch {
5090
+ return null;
5449
5091
  }
5450
- }
5451
- return null;
5092
+ })();
5093
+ const metrics = projected.metrics;
5094
+ const fallbackTransitionReport = {
5095
+ proposed: groupActionsByStatus(projectedProposedActions),
5096
+ applied: groupActionsByStatus(projectedAppliedActions)
5097
+ };
5098
+ const transitionReport = projectedArtifact?.transitionReport ? {
5099
+ proposed: hasGroupedGovernanceActions(projectedArtifact.transitionReport.proposed) || projectedProposedActions.length === 0 ? projectedArtifact.transitionReport.proposed : fallbackTransitionReport.proposed,
5100
+ applied: hasGroupedGovernanceActions(projectedArtifact.transitionReport.applied) || projectedAppliedActions.length === 0 ? projectedArtifact.transitionReport.applied : fallbackTransitionReport.applied
5101
+ } : fallbackTransitionReport;
5102
+ const qualityScore = projectedArtifact?.qualityScore ?? metrics?.qualityScore ?? buildQualityScore(metrics?.reviewReasons ?? {
5103
+ exact_duplicate: 0,
5104
+ semantic_duplicate_candidate: 0,
5105
+ disputed_memory: 0,
5106
+ speculative_low_confidence: 0,
5107
+ archive_candidate: 0,
5108
+ explicit_capture_review: 0,
5109
+ malformed_import: 0
5110
+ });
5111
+ const effectiveMetrics = metrics ? { ...metrics, qualityScore: metrics.qualityScore ?? qualityScore } : metrics;
5112
+ return {
5113
+ found: true,
5114
+ namespace: resolvedNamespace,
5115
+ runId: projected.runId,
5116
+ summary: projected.summary,
5117
+ metrics: effectiveMetrics,
5118
+ qualityScore,
5119
+ reviewQueue: projected.reviewQueueRows.map((row) => ({
5120
+ entryId: row.entryId,
5121
+ memoryId: row.memoryId,
5122
+ path: row.path,
5123
+ reasonCode: row.reasonCode,
5124
+ severity: row.severity,
5125
+ suggestedAction: row.suggestedAction,
5126
+ suggestedStatus: row.suggestedStatus,
5127
+ relatedMemoryIds: row.relatedMemoryIds
5128
+ })),
5129
+ appliedActions: projectedAppliedActions,
5130
+ transitionReport,
5131
+ report: projected.report
5132
+ };
5452
5133
  }
5453
- const collectionNamespace = parts ? this.qmdCollectionNamespaceFromPrefix(parts.collection) : null;
5454
- if (parts && collectionNamespace) {
5455
- try {
5456
- const collectionStorage = await this.storageFor(collectionNamespace);
5457
- for (const candidate of qmdResultPathCandidates(
5458
- collectionStorage.dir,
5459
- parts.relativePath
5460
- )) {
5461
- const memory = await collectionStorage.readMemoryByPath(candidate);
5462
- if (memory) return memory;
5463
- }
5464
- return null;
5465
- } catch (err) {
5466
- if (err instanceof SecureStoreLockedError) throw err;
5467
- log.debug("qmd result namespace path lookup failed open", {
5468
- path: resultPath,
5469
- namespace: collectionNamespace,
5470
- error: err.message
5471
- });
5472
- return null;
5473
- }
5134
+ const resolvedRunId = runId?.trim() || (await listMemoryGovernanceRuns(storage.dir))[0];
5135
+ if (!resolvedRunId) return { found: false, namespace: resolvedNamespace };
5136
+ const artifact = await readMemoryGovernanceRunArtifact(storage.dir, resolvedRunId);
5137
+ return {
5138
+ found: true,
5139
+ namespace: resolvedNamespace,
5140
+ runId: resolvedRunId,
5141
+ summary: artifact.summary,
5142
+ metrics: artifact.metrics,
5143
+ qualityScore: artifact.qualityScore,
5144
+ reviewQueue: artifact.reviewQueue,
5145
+ appliedActions: artifact.appliedActions,
5146
+ transitionReport: artifact.transitionReport,
5147
+ report: artifact.report
5148
+ };
5149
+ }
5150
+ async governanceRun(request, principal) {
5151
+ const deepSleep = this.deps.orchestrator.config.dreamsPhases.deepSleep;
5152
+ if (deepSleep.enabled === false && deepSleep.enabledExplicitlySet === true) {
5153
+ throw new Error("memory governance is disabled by dreams.phases.deepSleep.enabled=false");
5474
5154
  }
5475
- if (path8.isAbsolute(resultPath)) {
5476
- if (!fallbackStorageDir) {
5477
- return await fallbackStorage.readMemoryByPath(resultPath);
5478
- }
5479
- const ownerStorage = await this.storageForAbsoluteQmdResultPath(
5480
- resultPath,
5481
- fallbackStorage,
5482
- recallNamespaces
5483
- );
5484
- if (!ownerStorage) return null;
5485
- for (const candidate of qmdResultPathCandidates(
5486
- ownerStorage.dir,
5487
- resultPath
5488
- )) {
5489
- const memory = await ownerStorage.storage.readMemoryByPath(candidate);
5490
- if (memory) return memory;
5155
+ const resolvedNamespace = this.deps.writableNamespaceFor(
5156
+ request.namespace,
5157
+ void 0,
5158
+ request.authenticatedPrincipal ?? principal
5159
+ );
5160
+ const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
5161
+ const mode = request.mode === "apply" ? "apply" : "shadow";
5162
+ const boundedBatchSize = typeof request.batchSize === "number" && Number.isFinite(request.batchSize) ? Math.max(1, Math.floor(request.batchSize)) : void 0;
5163
+ const result = await runMemoryGovernance({
5164
+ memoryDir: storage.dir,
5165
+ mode,
5166
+ recentDays: typeof request.recentDays === "number" && Number.isFinite(request.recentDays) ? Math.max(1, Math.floor(request.recentDays)) : void 0,
5167
+ maxMemories: typeof request.maxMemories === "number" && Number.isFinite(request.maxMemories) ? Math.max(1, Math.floor(request.maxMemories)) : void 0,
5168
+ batchSize: boundedBatchSize
5169
+ });
5170
+ if (mode === "apply") {
5171
+ try {
5172
+ await this.deps.orchestrator.processEntitySynthesisQueue(
5173
+ resolvedNamespace,
5174
+ Math.min(boundedBatchSize ?? 5, 5)
5175
+ );
5176
+ } catch (error) {
5177
+ log.debug(`governanceRun: entity synthesis refresh failed after governance apply: ${error}`);
5491
5178
  }
5492
- return null;
5493
5179
  }
5494
- if (!fallbackStorageDir) {
5495
- return await fallbackStorage.readMemoryByPath(resultPath);
5180
+ return {
5181
+ namespace: resolvedNamespace,
5182
+ runId: result.runId,
5183
+ traceId: result.traceId,
5184
+ mode: result.mode,
5185
+ reviewQueueCount: result.reviewQueue.length,
5186
+ proposedActionCount: result.proposedActions.length,
5187
+ appliedActionCount: result.appliedActions.length,
5188
+ summaryPath: result.summaryPath,
5189
+ reportPath: result.reportPath
5190
+ };
5191
+ }
5192
+ async conversationIndexUpdate(request = {}) {
5193
+ if (!resolveIndexingCapabilities(this.deps.orchestrator.config).conversationIndex) {
5194
+ return {
5195
+ enabled: false,
5196
+ sessions: 0,
5197
+ chunks: 0,
5198
+ skipped: 0,
5199
+ skippedSessionKeys: [],
5200
+ embeddedRuns: 0,
5201
+ reason: "disabled"
5202
+ };
5496
5203
  }
5497
- for (const candidate of qmdResultPathCandidates(
5498
- fallbackStorageDir,
5499
- resultPath
5500
- )) {
5501
- const memory = await fallbackStorage.readMemoryByPath(candidate);
5502
- if (memory) return memory;
5204
+ const hours = typeof request.hours === "number" && Number.isFinite(request.hours) ? Math.max(1, Math.floor(request.hours)) : 24;
5205
+ let sessionKey;
5206
+ if (request.sessionKey !== void 0) {
5207
+ if (typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
5208
+ throw new EngramAccessInputError("sessionKey must be a non-empty string when provided");
5209
+ }
5210
+ sessionKey = request.sessionKey.trim();
5503
5211
  }
5504
- return null;
5505
- }
5506
- async resolveColdQmdResultForRecall(result, fallbackStorage, recallNamespaces = []) {
5507
- const memory = await this.readQmdResultMemory(
5508
- result.path,
5509
- fallbackStorage,
5510
- recallNamespaces
5511
- );
5512
- if (!memory) return null;
5513
- let ownerNamespace = null;
5514
- if (path8.isAbsolute(memory.path)) {
5515
- const ownerStorage = await this.storageForAbsoluteQmdResultPath(
5516
- memory.path,
5517
- fallbackStorage,
5518
- recallNamespaces
5212
+ if (sessionKey) {
5213
+ const result = await this.deps.orchestrator.updateConversationIndex(
5214
+ sessionKey,
5215
+ hours,
5216
+ { embed: request.embed }
5519
5217
  );
5520
- ownerNamespace = ownerStorage?.namespace ?? null;
5521
- if (!ownerNamespace && resolveNamespaceCapabilities(this.getConfig()).namespaces) return null;
5218
+ return {
5219
+ enabled: true,
5220
+ sessionKey,
5221
+ sessions: 1,
5222
+ chunks: result.chunks,
5223
+ skipped: result.skipped ? 1 : 0,
5224
+ skippedSessionKeys: result.skipped ? [sessionKey] : [],
5225
+ embeddedRuns: result.embedded ? 1 : 0,
5226
+ reason: result.reason,
5227
+ retryAfterMs: result.retryAfterMs
5228
+ };
5522
5229
  }
5523
- ownerNamespace ??= this.namespaceFromPath(memory.path);
5524
- if (recallNamespaces.length > 0 && !recallNamespaces.includes(ownerNamespace)) {
5525
- return null;
5230
+ const sessionKeys = await this.deps.orchestrator.transcript.listSessionKeys();
5231
+ let chunks = 0;
5232
+ let skipped = 0;
5233
+ const skippedSessionKeys = [];
5234
+ let embeddedRuns = 0;
5235
+ for (const sessionKey2 of sessionKeys) {
5236
+ const result = await this.deps.orchestrator.updateConversationIndex(
5237
+ sessionKey2,
5238
+ hours,
5239
+ { embed: request.embed }
5240
+ );
5241
+ chunks += result.chunks;
5242
+ if (result.skipped) {
5243
+ skipped += 1;
5244
+ skippedSessionKeys.push(sessionKey2);
5245
+ }
5246
+ if (result.embedded) {
5247
+ embeddedRuns += 1;
5248
+ }
5526
5249
  }
5527
5250
  return {
5528
- namespace: ownerNamespace,
5529
- result: {
5530
- ...result,
5531
- docid: result.docid || memory.frontmatter.id,
5532
- path: memory.path,
5533
- snippet: result.snippet || memory.content.slice(0, 400)
5534
- }
5251
+ enabled: true,
5252
+ sessions: sessionKeys.length,
5253
+ chunks,
5254
+ skipped,
5255
+ skippedSessionKeys,
5256
+ embeddedRuns
5535
5257
  };
5536
5258
  }
5537
- async storageForAbsoluteQmdResultPath(resultPath, fallbackStorage, recallNamespaces = []) {
5538
- const resolvedPath = path8.resolve(resultPath);
5539
- const config = this.getConfig();
5540
- const memoryRoot = path8.resolve(config.memoryDir);
5541
- const namespacesRoot = path8.join(memoryRoot, "namespaces");
5542
- const fallbackStorageDir = storageDirOrNull(fallbackStorage);
5543
- const matches = [];
5544
- const seenDirs = /* @__PURE__ */ new Set();
5545
- const maybeAddStorage = (storage, namespace) => {
5546
- const storageDir = storageDirOrNull(storage);
5547
- if (!storageDir) return;
5548
- const candidateRoot = path8.resolve(storageDir);
5549
- if (seenDirs.has(candidateRoot)) return;
5550
- if (!isPathInsideStorageRoot(candidateRoot, resolvedPath)) return;
5551
- if (candidateRoot === memoryRoot && isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
5552
- return;
5553
- }
5554
- seenDirs.add(candidateRoot);
5555
- matches.push({ storage, dir: candidateRoot, namespace });
5556
- };
5557
- const fallbackNamespace = fallbackStorageDir !== null ? this.storageDirNamespace(fallbackStorageDir) : config.defaultNamespace;
5558
- maybeAddStorage(fallbackStorage, fallbackNamespace);
5559
- const candidateNamespaces = /* @__PURE__ */ new Set();
5560
- candidateNamespaces.add(config.defaultNamespace);
5561
- candidateNamespaces.add(config.sharedNamespace);
5562
- for (const ns of recallNamespaces) {
5563
- candidateNamespaces.add(ns);
5259
+ async profilingReport(request = {}) {
5260
+ const profiler = this.deps.orchestrator.profiler;
5261
+ if (!profiler.isEnabled) {
5262
+ return {
5263
+ enabled: false,
5264
+ reason: "disabled",
5265
+ message: "Profiling is disabled. Set profilingEnabled: true in your plugin config to enable."
5266
+ };
5564
5267
  }
5565
- if (isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
5566
- const relativeToNamespaces = path8.relative(namespacesRoot, resolvedPath);
5567
- const [namespaceSegment] = relativeToNamespaces.split(/[\\/]/);
5568
- if (namespaceSegment) {
5569
- candidateNamespaces.add(
5570
- namespaceIdentityFromToken(namespaceSegment) ?? namespaceSegment
5571
- );
5572
- }
5268
+ const format = request.format ?? "ascii";
5269
+ if (format !== "ascii" && format !== "json") {
5270
+ throw new EngramAccessInputError("format must be one of: ascii, json");
5573
5271
  }
5574
- for (const policy of config.namespacePolicies ?? []) {
5575
- candidateNamespaces.add(policy.name);
5272
+ const limit = request.limit ?? 5;
5273
+ if (!Number.isInteger(limit) || limit < 1 || limit > 20) {
5274
+ throw new EngramAccessInputError("limit must be an integer between 1 and 20");
5576
5275
  }
5577
- for (const ns of candidateNamespaces) {
5578
- if (!ns) continue;
5579
- try {
5580
- maybeAddStorage(await this.storageFor(ns), ns);
5581
- } catch {
5582
- continue;
5276
+ const traces = profiler.getRecentTraces(limit);
5277
+ const stats = profiler.getStats();
5278
+ const bottleneck = profiler.identifyBottleneck();
5279
+ if (format === "json") {
5280
+ return {
5281
+ enabled: true,
5282
+ format,
5283
+ traces,
5284
+ stats,
5285
+ bottleneck
5286
+ };
5287
+ }
5288
+ const lines = [];
5289
+ lines.push("Engram Profiling Report");
5290
+ lines.push("=".repeat(60));
5291
+ lines.push("");
5292
+ const allBuckets = [
5293
+ ["byKind", stats.byKind],
5294
+ ["bySpan", stats.bySpan]
5295
+ ];
5296
+ const hasStats = allBuckets.some(([, entries]) => Object.keys(entries).length > 0);
5297
+ if (hasStats) {
5298
+ lines.push("Aggregate Stats (all retained traces):");
5299
+ for (const [bucket, entries] of allBuckets) {
5300
+ for (const [key, summary] of Object.entries(entries)) {
5301
+ lines.push(
5302
+ ` ${bucket}/${key}: avg=${summary.avgMs}ms p50=${summary.p50Ms}ms p95=${summary.p95Ms}ms max=${summary.maxMs}ms (n=${summary.count})`
5303
+ );
5304
+ }
5583
5305
  }
5306
+ lines.push("");
5584
5307
  }
5585
- matches.sort((a, b) => b.dir.length - a.dir.length);
5586
- return matches[0] ?? null;
5587
- }
5588
- };
5589
-
5590
- // src/access-recall-surface.ts
5591
- var AccessRecallSurface = class {
5592
- constructor(deps) {
5593
- this.deps = deps;
5594
- }
5595
- deps;
5596
- async buildRecallResponseFromXraySnapshot(options) {
5597
- const memoryIds = options.snapshot.results.map((result) => result.memoryId);
5598
- const resultPaths = options.snapshot.results.map((result) => result.path);
5599
- const namespace = options.snapshot.namespace ? this.deps.resolveNamespace(options.snapshot.namespace) : this.deps.orchestrator.config.defaultNamespace;
5600
- const sourcesUsed = Array.from(
5601
- new Set(options.snapshot.results.map((result) => result.servedBy))
5602
- );
5603
- const snapshotForSerialization = {
5604
- sessionKey: options.sessionKey ?? "",
5605
- recordedAt: new Date(options.snapshot.capturedAt).toISOString(),
5606
- queryHash: createHash4("sha256").update(options.query).digest("hex"),
5607
- queryLen: options.query.length,
5608
- memoryIds,
5609
- namespace,
5610
- recallNamespaces: [namespace],
5611
- traceId: options.snapshot.traceId,
5612
- plannerMode: options.normalizedMode,
5613
- requestedMode: options.requestedMode && options.requestedMode !== "auto" ? options.requestedMode : void 0,
5614
- sourcesUsed,
5615
- budgetsApplied: {
5616
- appliedTopK: memoryIds.length,
5617
- recallBudgetChars: options.snapshot.budget.chars,
5618
- maxMemoryTokens: this.deps.orchestrator.config.maxMemoryTokens,
5619
- finalContextChars: options.snapshot.budget.used
5620
- },
5621
- latencyMs: Date.now() - options.startedAt,
5622
- resultPaths
5623
- };
5624
- const results = await this.deps.serializeRecallResults(
5625
- snapshotForSerialization,
5626
- options.disclosure,
5627
- {
5628
- query: options.query,
5629
- ...options.sessionKey ? { sessionKey: options.sessionKey } : {},
5630
- ...options.rawExcerptNamespace ? { rawExcerptNamespace: options.rawExcerptNamespace } : {},
5631
- ...options.rawExcerptSessionIds !== void 0 ? { rawExcerptSessionIds: options.rawExcerptSessionIds } : {},
5632
- ...options.rawExcerptsSuppressed ? { rawExcerptsSuppressed: options.rawExcerptsSuppressed } : {}
5308
+ if (bottleneck) {
5309
+ lines.push(`Bottleneck: ${bottleneck}`);
5310
+ lines.push("");
5311
+ }
5312
+ if (traces.length === 0) {
5313
+ lines.push("No traces recorded yet. Trigger a recall or extraction to see timing data.");
5314
+ } else {
5315
+ for (const trace of traces) {
5316
+ lines.push(formatProfileTraceAscii(trace));
5317
+ lines.push("");
5633
5318
  }
5634
- );
5635
- const context = results.map((result) => {
5636
- const content = typeof result.content === "string" && result.content.length > 0 ? result.content : "";
5637
- return content || result.preview;
5638
- }).filter((text) => text.length > 0).join("\n\n");
5319
+ }
5639
5320
  return {
5640
- query: options.query,
5641
- ...options.sessionKey ? { sessionKey: options.sessionKey } : {},
5642
- namespace,
5643
- context,
5644
- count: memoryIds.length,
5645
- memoryIds,
5646
- results,
5647
- recordedAt: snapshotForSerialization.recordedAt,
5648
- traceId: options.snapshot.traceId,
5649
- plannerMode: options.normalizedMode,
5650
- fallbackUsed: sourcesUsed.some((source) => source !== "hybrid"),
5651
- sourcesUsed,
5652
- disclosure: options.disclosure,
5653
- budgetsApplied: snapshotForSerialization.budgetsApplied,
5654
- latencyMs: snapshotForSerialization.latencyMs
5321
+ enabled: true,
5322
+ format,
5323
+ report: lines.join("\n")
5655
5324
  };
5656
5325
  }
5657
- async serializeRecallResults(snapshot, disclosure, rawContext = null) {
5658
- if (!snapshot) return [];
5659
- const namespace = snapshot.namespace ? this.deps.resolveNamespace(snapshot.namespace) : this.deps.orchestrator.config.defaultNamespace;
5660
- const storage = await this.deps.orchestrator.getStorage(namespace);
5661
- const storageDir = storage.dir;
5662
- const recallNamespaces = Array.from(
5663
- new Set(
5664
- [
5665
- namespace,
5666
- ...Array.isArray(snapshot.recallNamespaces) ? snapshot.recallNamespaces.map((ns) => this.deps.resolveNamespace(ns)) : [],
5667
- this.deps.orchestrator.config.defaultNamespace,
5668
- this.deps.orchestrator.config.sharedNamespace,
5669
- ...(this.deps.orchestrator.config.namespacePolicies ?? []).map((p) => p.name)
5670
- ].filter((ns) => typeof ns === "string" && ns.length > 0)
5671
- )
5326
+ /**
5327
+ * Read-only graph snapshot for the admin pane (issue #691 PR 2/5).
5328
+ *
5329
+ * Reads adjacency from the JSONL edge store written by `GraphIndex` and
5330
+ * resolves node metadata via the namespaced storage manager. Namespace
5331
+ * resolution mirrors the read-side path used by `recall` /
5332
+ * `procedureStats`, so multi-principal deployments can't leak edges from
5333
+ * a peer namespace (CLAUDE.md rule 42).
5334
+ */
5335
+ async graphSnapshot(request, authenticatedPrincipal) {
5336
+ const namespace = this.deps.resolveReadableNamespace(
5337
+ request.namespace,
5338
+ authenticatedPrincipal
5672
5339
  );
5673
- const results = [];
5674
- const seen = /* @__PURE__ */ new Set();
5675
- const collectionNamespaceFromPrefix = (collectionPrefix) => {
5676
- const baseCollection = this.deps.orchestrator.config.qmdCollection;
5677
- if (collectionPrefix === baseCollection) return this.deps.orchestrator.config.defaultNamespace;
5678
- const namespaceSuffix = collectionPrefix.startsWith(`${baseCollection}--`) ? collectionPrefix.slice(baseCollection.length + 2) : "";
5679
- if (!namespaceSuffix) return null;
5680
- const decoded = namespaceIdentityFromToken(namespaceSuffix);
5681
- if (decoded !== null) return decoded || this.deps.orchestrator.config.defaultNamespace;
5682
- if (namespaceSuffix.startsWith("ns--")) {
5683
- const legacyNamespace = namespaceSuffix.slice("ns--".length).trim();
5684
- return legacyNamespace || null;
5340
+ const storage = await this.deps.orchestrator.getStorage(namespace);
5341
+ const cfg = this.deps.orchestrator.config;
5342
+ const graphCaps = resolveGraphConstructionCapabilities(cfg);
5343
+ let namespaceRootReal;
5344
+ try {
5345
+ namespaceRootReal = await nodeFs.realpath(storage.dir);
5346
+ } catch {
5347
+ namespaceRootReal = nodePath.resolve(storage.dir);
5348
+ }
5349
+ const namespaceRootWithSep = namespaceRootReal.endsWith(nodePath.sep) ? namespaceRootReal : namespaceRootReal + nodePath.sep;
5350
+ const loadNode = async (relPath) => {
5351
+ if (nodePath.isAbsolute(relPath)) {
5352
+ log.warn(
5353
+ `graphSnapshot: rejected absolute edge endpoint (len=${relPath.length}) outside namespace root`
5354
+ );
5355
+ return null;
5685
5356
  }
5686
- return null;
5687
- };
5688
- const readResultPath = async (memoryPath) => {
5689
- const parts = qmdCollectionPathParts(memoryPath);
5690
- const coldCollection = this.deps.orchestrator.config.qmdColdCollection ?? "openclaw-engram-cold";
5691
- if (parts && parts.collection === coldCollection) {
5692
- const storages = [];
5693
- const seenStorageDirs = /* @__PURE__ */ new Set();
5694
- const addStorage = (candidateStorage) => {
5695
- const candidateDir = nodePath2.resolve(candidateStorage.dir);
5696
- if (seenStorageDirs.has(candidateDir)) return;
5697
- seenStorageDirs.add(candidateDir);
5698
- storages.push({ storage: candidateStorage, dir: candidateDir });
5699
- };
5700
- addStorage(storage);
5701
- for (const recallNamespace of recallNamespaces) {
5702
- try {
5703
- addStorage(await this.deps.orchestrator.getStorage(recallNamespace));
5704
- } catch {
5705
- continue;
5706
- }
5707
- }
5708
- for (const candidateStorage of storages) {
5709
- try {
5710
- const coldRoot = nodePath2.join(candidateStorage.dir, "cold");
5711
- for (const candidatePath of qmdResultPathCandidates(
5712
- coldRoot,
5713
- parts.relativePath
5714
- )) {
5715
- const memory = await candidateStorage.storage.readMemoryByPath(candidatePath);
5716
- if (memory) return { memory, baseDir: candidateStorage.dir };
5717
- }
5718
- } catch (err) {
5719
- if (err instanceof SecureStoreLockedError) throw err;
5720
- continue;
5721
- }
5722
- }
5357
+ const candidate = nodePath.resolve(namespaceRootReal, relPath);
5358
+ if (candidate !== namespaceRootReal && !candidate.startsWith(namespaceRootWithSep)) {
5359
+ log.warn(
5360
+ `graphSnapshot: rejected traversing edge endpoint (len=${relPath.length}) outside namespace root`
5361
+ );
5723
5362
  return null;
5724
5363
  }
5725
- const collectionNamespace = parts ? collectionNamespaceFromPrefix(parts.collection) : null;
5726
- if (parts && collectionNamespace) {
5727
- try {
5728
- const collectionStorage = await this.deps.orchestrator.getStorage(collectionNamespace);
5729
- for (const candidate of qmdResultPathCandidates(
5730
- collectionStorage.dir,
5731
- parts.relativePath
5732
- )) {
5733
- const memory = await collectionStorage.readMemoryByPath(candidate);
5734
- if (memory) return { memory, baseDir: collectionStorage.dir };
5735
- }
5736
- return null;
5737
- } catch (err) {
5738
- if (err instanceof SecureStoreLockedError) throw err;
5739
- return null;
5740
- }
5364
+ let canonical;
5365
+ try {
5366
+ canonical = await nodeFs.realpath(candidate);
5367
+ } catch {
5368
+ canonical = candidate;
5741
5369
  }
5742
- if (nodePath2.isAbsolute(memoryPath)) {
5743
- const ownerStorage = await this.deps.storageForAbsoluteRecallPath(
5744
- memoryPath,
5745
- namespace,
5746
- recallNamespaces
5370
+ if (canonical !== namespaceRootReal && !canonical.startsWith(namespaceRootWithSep)) {
5371
+ log.warn(
5372
+ `graphSnapshot: rejected symlinked edge endpoint (len=${relPath.length}) that resolved outside namespace root`
5747
5373
  );
5748
- if (!ownerStorage) return null;
5749
- for (const candidate of qmdResultPathCandidates(
5750
- ownerStorage.dir,
5751
- memoryPath
5752
- )) {
5753
- const memory = await ownerStorage.storage.readMemoryByPath(candidate);
5754
- if (memory) return { memory, baseDir: ownerStorage.dir };
5755
- }
5756
5374
  return null;
5757
5375
  }
5758
- for (const candidate of qmdResultPathCandidates(storageDir, memoryPath)) {
5759
- const memory = await storage.readMemoryByPath(candidate);
5760
- if (memory) return { memory, baseDir: storageDir };
5761
- }
5762
- return null;
5376
+ const memory = await storage.readMemoryByPath(canonical);
5377
+ if (!memory) return null;
5378
+ const fm = memory.frontmatter;
5379
+ return {
5380
+ category: fm.category ?? "unknown",
5381
+ label: fm.id ?? nodePath.basename(canonical, nodePath.extname(canonical)),
5382
+ updated: fm.updated
5383
+ };
5763
5384
  };
5764
- const rawExcerptsResult = rawContext?.rawExcerptsSuppressed === true ? (
5765
- // Implicit raw recall with NO readable LCM namespace (#1505 thread
5766
- // NBHWz): emit empty excerpts rather than falling back to the
5767
- // write/overlay `namespace` the read gate excludes.
5768
- []
5769
- ) : await this.deps.fetchRawExcerpts(
5770
- disclosure,
5771
- rawContext ? {
5772
- query: rawContext.query,
5773
- ...rawContext.sessionKey ? { sessionKey: rawContext.sessionKey } : {},
5774
- namespace: rawContext.rawExcerptNamespace ?? namespace,
5775
- ...rawContext.rawExcerptSessionIds !== void 0 ? { lcmSessionIds: rawContext.rawExcerptSessionIds } : {}
5776
- } : null
5777
- );
5778
- const rawExcerpts = rawExcerptsResult ?? void 0;
5779
- for (const memoryPath of snapshot.resultPaths ?? []) {
5780
- if (!memoryPath || seen.has(memoryPath)) continue;
5781
- const resolved = await readResultPath(memoryPath);
5782
- if (!resolved) continue;
5783
- const { memory, baseDir } = resolved;
5784
- seen.add(memoryPath);
5785
- results.push(
5786
- this.deps.serializeMemorySummary(
5787
- memory,
5788
- baseDir,
5789
- disclosure,
5790
- // Attach the (possibly empty) raw excerpts to the first raw
5791
- // result; subsequent results do not duplicate the array.
5792
- results.length === 0 ? rawExcerpts : void 0
5793
- )
5794
- );
5795
- }
5796
- if (results.length > 0) return results;
5797
- for (const memoryId of snapshot.memoryIds) {
5798
- const memory = await storage.getMemoryById(memoryId);
5799
- if (!memory || seen.has(memory.path)) continue;
5800
- seen.add(memory.path);
5801
- results.push(
5802
- this.deps.serializeMemorySummary(
5803
- memory,
5804
- storageDir,
5805
- disclosure,
5806
- results.length === 0 ? rawExcerpts : void 0
5807
- )
5808
- );
5809
- }
5810
- return results;
5385
+ return buildGraphSnapshot({
5386
+ memoryDir: namespaceRootReal,
5387
+ graphConfig: {
5388
+ entityGraph: graphCaps.entityGraph,
5389
+ timeGraph: graphCaps.timeGraph,
5390
+ causalGraph: graphCaps.causalGraph
5391
+ },
5392
+ request: {
5393
+ limit: request.limit,
5394
+ since: request.since,
5395
+ focusNodeId: request.focusNodeId,
5396
+ categories: request.categories
5397
+ },
5398
+ loadNode
5399
+ });
5811
5400
  }
5812
5401
  /**
5813
- * Fetch raw transcript excerpts from the LCM archive for `disclosure ===
5814
- * "raw"` recalls (issue #677 PR 2/4). Returns `null` for non-raw recall
5815
- * depths, an empty array when LCM is disabled / not initialized / has no
5816
- * matches, and an array of LCM-side excerpts otherwise. Errors are
5817
- * swallowed and treated as "no excerpts" so a failing LCM never breaks
5818
- * the recall response.
5402
+ * List capsule archives in the namespace-scoped capsule store.
5819
5403
  *
5820
- * Namespace handling: LCM archival prefixes non-default-namespace
5821
- * sessions with `${namespace}:${sessionKey}` (see `observe()` around
5822
- * line 2498), so the lookup must mirror that prefix or raw recalls in
5823
- * non-default namespaces miss their own excerpts.
5404
+ * MCP uses this access-layer method instead of reading arbitrary paths so
5405
+ * capsule discovery remains bound to the same namespace ACLs as export and
5406
+ * import.
5824
5407
  */
5825
- async fetchRawExcerpts(disclosure, context) {
5826
- if (disclosure !== "raw") return null;
5827
- if (!context || !context.query) return [];
5828
- if (!context.sessionKey) return [];
5829
- const lcm = this.deps.orchestrator.lcmEngine;
5830
- if (!lcm || !lcm.enabled) return [];
5408
+ async capsuleList(options) {
5409
+ const resolvedNamespace = this.deps.resolveReadableNamespace(options?.namespace, options?.principal);
5410
+ const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
5411
+ const capsulesDir = defaultCapsulesDir(storage.dir);
5412
+ let dirEntries;
5831
5413
  try {
5832
- const legacyKey = context.namespace && context.namespace !== this.deps.orchestrator.config.defaultNamespace ? `${context.namespace}:${context.sessionKey}` : context.sessionKey;
5833
- const lcmSessionIds = context.lcmSessionIds !== void 0 ? context.lcmSessionIds : [legacyKey];
5834
- const limit = 5;
5835
- const seenRows = /* @__PURE__ */ new Set();
5836
- const excerpts = [];
5837
- const settledRows = await Promise.allSettled(
5838
- lcmSessionIds.map(
5839
- async (lcmSessionKey) => lcm.searchContextFull(context.query, limit, lcmSessionKey)
5840
- )
5841
- );
5842
- for (const result of settledRows) {
5843
- if (excerpts.length >= limit) break;
5844
- if (result.status !== "fulfilled") continue;
5845
- for (const r of result.value) {
5846
- const dedupeKey = `${r.session_id} ${r.turn_index}`;
5847
- if (seenRows.has(dedupeKey)) continue;
5848
- seenRows.add(dedupeKey);
5849
- excerpts.push({
5850
- turnIndex: r.turn_index,
5851
- role: r.role,
5852
- content: r.content,
5853
- sessionId: r.session_id
5854
- });
5855
- if (excerpts.length >= limit) break;
5856
- }
5414
+ const capsulesDirStat = await nodeFs.lstat(capsulesDir);
5415
+ if (capsulesDirStat.isSymbolicLink()) {
5416
+ throw new EngramAccessInputError("capsule list failed: capsule store directory must not be a symlink");
5857
5417
  }
5858
- return excerpts;
5859
- } catch {
5860
- return [];
5861
- }
5862
- }
5863
- async executeRecall(request) {
5864
- const query = request.query;
5865
- const callerProvidedDisclosure = request.disclosure !== void 0 && request.disclosure !== null;
5866
- const requestedDisclosure = (() => {
5867
- if (!callerProvidedDisclosure) {
5868
- return DEFAULT_RECALL_DISCLOSURE;
5418
+ if (!capsulesDirStat.isDirectory()) {
5419
+ throw new EngramAccessInputError("capsule list failed: capsule store path must be a directory");
5869
5420
  }
5870
- if (!isRecallDisclosure(request.disclosure)) {
5871
- throw new EngramAccessInputError(
5872
- `disclosure must be one of: chunk, section, raw (got: ${String(request.disclosure)})`
5873
- );
5421
+ dirEntries = await nodeFs.readdir(capsulesDir, { withFileTypes: true });
5422
+ } catch (err) {
5423
+ const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
5424
+ if (code === "ENOENT") {
5425
+ return { namespace: resolvedNamespace, capsulesDir, capsules: [] };
5874
5426
  }
5875
- return request.disclosure;
5876
- })();
5877
- if (request.codingContext !== void 0 && request.sessionKey) {
5878
- this.deps.setCodingContext({
5879
- sessionKey: request.sessionKey,
5880
- codingContext: request.codingContext
5881
- });
5427
+ throw err;
5882
5428
  }
5883
- if (request.codingContext === void 0 && request.sessionKey) {
5884
- await this.deps.maybeAttachCodingContext(request.sessionKey, {
5885
- cwd: request.cwd,
5886
- projectTag: request.projectTag
5429
+ const archiveNames = dirEntries.filter(
5430
+ (entry) => entry.isFile() && (entry.name.endsWith(".capsule.json.gz") || entry.name.endsWith(".capsule.json.gz.enc"))
5431
+ ).map((entry) => entry.name).sort();
5432
+ const capsules = [];
5433
+ for (const archiveName of archiveNames) {
5434
+ const archivePath = nodePath.join(capsulesDir, archiveName);
5435
+ const id = archiveName.replace(/\.capsule\.json\.gz\.enc$/, "").replace(/\.capsule\.json\.gz$/, "");
5436
+ const manifestPath = nodePath.join(capsulesDir, `${id}.manifest.json`);
5437
+ let createdAt = null;
5438
+ let pluginVersion = null;
5439
+ let fileCount = null;
5440
+ let description = null;
5441
+ let manifestPathOrNull = manifestPath;
5442
+ try {
5443
+ const manifestStat = await nodeFs.lstat(manifestPath);
5444
+ if (manifestStat.isSymbolicLink() || !manifestStat.isFile()) {
5445
+ capsules.push({
5446
+ id,
5447
+ archivePath,
5448
+ manifestPath: manifestPathOrNull,
5449
+ createdAt,
5450
+ pluginVersion,
5451
+ fileCount,
5452
+ description
5453
+ });
5454
+ continue;
5455
+ }
5456
+ const raw = await nodeFs.readFile(manifestPath, "utf-8");
5457
+ const sidecar = JSON.parse(raw);
5458
+ createdAt = typeof sidecar.createdAt === "string" ? sidecar.createdAt : null;
5459
+ pluginVersion = typeof sidecar.pluginVersion === "string" ? sidecar.pluginVersion : null;
5460
+ fileCount = Array.isArray(sidecar.files) ? sidecar.files.length : null;
5461
+ const capsule = sidecar.capsule;
5462
+ description = capsule && typeof capsule.description === "string" ? capsule.description : null;
5463
+ } catch (err) {
5464
+ const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
5465
+ if (code === "ENOENT") {
5466
+ manifestPathOrNull = null;
5467
+ }
5468
+ }
5469
+ capsules.push({
5470
+ id,
5471
+ archivePath,
5472
+ manifestPath: manifestPathOrNull,
5473
+ createdAt,
5474
+ pluginVersion,
5475
+ fileCount,
5476
+ description
5887
5477
  });
5888
5478
  }
5889
- const authenticatedPrincipal = request.authenticatedPrincipal?.trim();
5890
- const namespaceOverride = this.deps.resolveRecallNamespace(
5891
- request.namespace,
5892
- request.sessionKey,
5893
- authenticatedPrincipal
5894
- );
5895
- const namespace = namespaceOverride ?? this.deps.orchestrator.config.defaultNamespace;
5896
- const mode = this.deps.normalizeRecallMode(request.mode);
5897
- const maybePrincipal = this.deps.resolveRequestPrincipal(request.sessionKey, authenticatedPrincipal);
5898
- if (resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces && !maybePrincipal) {
5479
+ return { namespace: resolvedNamespace, capsulesDir, capsules };
5480
+ }
5481
+ /**
5482
+ * Manually invoke a single Dreams phase pass (PR 4/4).
5483
+ *
5484
+ * Deep-sleep delegates to memory governance (shadow → dry-run, apply → live).
5485
+ * Light-sleep and REM scan the observation ledger and memory corpus
5486
+ * respectively, returning the same telemetry shape as a scheduled run.
5487
+ */
5488
+ async dreamsRun(options) {
5489
+ const { runDreamsPhase } = await import("./dreams-ledger-6UODQOX6.js");
5490
+ const validPhases = ["lightSleep", "rem", "deepSleep"];
5491
+ if (!validPhases.includes(options.phase)) {
5899
5492
  throw new EngramAccessInputError(
5900
- "authentication required: namespaces are enabled and no principal was supplied"
5493
+ `Invalid phase: ${String(options.phase)}. Must be one of: ${validPhases.join(", ")}`
5901
5494
  );
5902
5495
  }
5903
- const principal = maybePrincipal ?? "default";
5904
- const principalNamespace = defaultNamespaceForPrincipal(principal, this.deps.orchestrator.config);
5905
- const profileCodingContext = request.sessionKey && typeof this.deps.orchestrator.getCodingContextForSession === "function" ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey) : null;
5906
- const profileCodingOverlay = !namespaceOverride && profileCodingContext && resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces && this.deps.orchestrator.config.codingMode?.projectScope ? resolveCodingNamespaceOverlay(
5907
- profileCodingContext,
5908
- this.deps.orchestrator.config.codingMode,
5909
- this.deps.orchestrator.config.defaultNamespace
5910
- ) : null;
5911
- const profilePlan = namespaceOverride ? null : resolveScopeProfilePlan({
5912
- config: this.deps.orchestrator.config,
5913
- principal,
5914
- codingContext: profileCodingContext,
5915
- codingOverlay: profileCodingOverlay
5916
- });
5917
- const modeSkipsBudget = mode === "no_recall";
5918
- const legacyRecallNamespaces = Array.isArray(this.deps.orchestrator.config.defaultRecallNamespaces) ? recallNamespacesForPrincipal(principal, this.deps.orchestrator.config) : [];
5919
- const effectiveNamespaces = namespaceOverride ? [namespaceOverride] : profilePlan ? expandScopeProfileReadNamespaces({
5920
- profilePlan,
5921
- principalSelfNamespace: profilePlan.baseNamespace,
5922
- config: this.deps.orchestrator.config,
5923
- principal,
5924
- codingOverlay: profileCodingOverlay,
5925
- legacyRecallNamespaces
5926
- }) : legacyRecallNamespaces;
5927
- const budgetPrincipalNamespace = profilePlan?.baseNamespace ?? principalNamespace;
5928
- let budgetDecision;
5929
- let recordBudgetAfterSuccess = false;
5930
- if (modeSkipsBudget) {
5931
- budgetDecision = {
5932
- allowed: true,
5933
- reason: "allowed-same-namespace",
5934
- count: 0,
5935
- limit: {
5936
- softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
5937
- hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
5938
- windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 6e4
5939
- }
5496
+ const deepSleep = this.deps.orchestrator.config.dreamsPhases.deepSleep;
5497
+ if (options.phase === "deepSleep" && deepSleep.enabled === false && deepSleep.enabledExplicitlySet === true) {
5498
+ throw new EngramAccessInputError(
5499
+ "memory governance is disabled by dreams.phases.deepSleep.enabled=false"
5500
+ );
5501
+ }
5502
+ const dryRun = options.dryRun === true;
5503
+ const resolvedNamespace = this.deps.writableNamespaceFor(
5504
+ options.namespace,
5505
+ void 0,
5506
+ options.authenticatedPrincipal
5507
+ );
5508
+ const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
5509
+ const memoryDir = storage.dir;
5510
+ const phaseRunner = dryRun || options.phase === "deepSleep" ? void 0 : async (_opts) => {
5511
+ if (_opts.phase === "lightSleep") {
5512
+ const result3 = await this.deps.orchestrator.runLifecyclePolicyNow(storage);
5513
+ return {
5514
+ itemsProcessed: result3.memoriesAssessed,
5515
+ notes: `scored ${result3.memoriesAssessed} memories`
5516
+ };
5517
+ }
5518
+ const result2 = await this.deps.orchestrator.runSemanticConsolidationNow({
5519
+ dryRun: false,
5520
+ storage
5521
+ });
5522
+ const itemsProcessed = result2.clusters.reduce(
5523
+ (sum, cluster) => sum + cluster.memories.length,
5524
+ 0
5525
+ );
5526
+ return {
5527
+ itemsProcessed,
5528
+ notes: `REM consolidation found ${result2.clustersFound} clusters`
5940
5529
  };
5530
+ };
5531
+ const governanceRunner = options.phase === "deepSleep" ? async (_opts) => {
5532
+ return this.deps.orchestrator.runDeepSleepGovernanceNow({
5533
+ storage,
5534
+ dryRun: _opts.dryRun
5535
+ });
5536
+ } : void 0;
5537
+ const result = await runDreamsPhase(
5538
+ { memoryDir, phase: options.phase, dryRun },
5539
+ governanceRunner,
5540
+ phaseRunner
5541
+ );
5542
+ return {
5543
+ phase: result.phase,
5544
+ dryRun: result.dryRun,
5545
+ durationMs: result.durationMs,
5546
+ itemsProcessed: result.itemsProcessed,
5547
+ notes: result.notes
5548
+ };
5549
+ }
5550
+ /**
5551
+ * Manually promote a memory into one or more authorized targets. Requires
5552
+ * a non-empty reason (audit-logged). Reuses the scope-profile promotion
5553
+ * resolution and `canWriteNamespace` gate — there is no dashboard-only
5554
+ * write path.
5555
+ */
5556
+ async adminPromoteMemory(request) {
5557
+ const config = this.deps.orchestrator.config;
5558
+ const namespacesEnabled = resolveNamespaceCapabilities(config).namespaces === true;
5559
+ let sourceNamespace;
5560
+ if (request.namespace) {
5561
+ sourceNamespace = this.deps.resolveReadableNamespace(
5562
+ request.namespace,
5563
+ request.principal
5564
+ );
5941
5565
  } else {
5942
- let anyCrossNamespace = false;
5943
- let denied = null;
5944
- let crossNamespaceDecision = null;
5945
- for (const ns of effectiveNamespaces) {
5946
- const peek = this.deps.budget.peek({
5947
- principal,
5948
- principalNamespace: budgetPrincipalNamespace,
5949
- queryNamespace: ns
5950
- });
5951
- if (peek.reason !== "allowed-same-namespace") {
5952
- anyCrossNamespace = true;
5953
- crossNamespaceDecision ??= peek;
5954
- }
5955
- if (!peek.allowed) {
5956
- denied = peek;
5957
- break;
5958
- }
5959
- }
5960
- if (denied) {
5961
- budgetDecision = denied;
5962
- } else if (anyCrossNamespace) {
5963
- budgetDecision = crossNamespaceDecision ?? {
5964
- allowed: true,
5965
- reason: "allowed-under-soft",
5966
- count: 0,
5967
- limit: {
5968
- softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
5969
- hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
5970
- windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 6e4
5971
- }
5566
+ const codingContext2 = request.sessionKey ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey) ?? null : null;
5567
+ const plan = resolveScopePlan({
5568
+ config,
5569
+ sessionKey: request.sessionKey,
5570
+ principalOverride: request.principal,
5571
+ codingContext: codingContext2,
5572
+ namespacesEnabled
5573
+ });
5574
+ sourceNamespace = this.deps.resolveReadableNamespace(
5575
+ plan.baseNamespace,
5576
+ request.principal
5577
+ );
5578
+ }
5579
+ const codingContext = request.sessionKey ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey) ?? null : null;
5580
+ const codingOverlay = codingContext ? resolveCodingNamespaceOverlay(
5581
+ codingContext,
5582
+ config.codingMode,
5583
+ config.defaultNamespace
5584
+ ) : null;
5585
+ const scopeProfilePlan = resolveScopeProfilePlan({
5586
+ config,
5587
+ principal: request.principal,
5588
+ codingContext,
5589
+ codingOverlay
5590
+ });
5591
+ const storage = {
5592
+ readMemory: async (namespace, memoryId) => {
5593
+ const resolved = await this.deps.orchestrator.getStorage(namespace);
5594
+ const memory = await resolved.getMemoryById(memoryId);
5595
+ if (!memory) return null;
5596
+ return {
5597
+ category: memory.frontmatter.category,
5598
+ content: memory.content,
5599
+ frontmatter: memory.frontmatter
5972
5600
  };
5973
- recordBudgetAfterSuccess = true;
5974
- } else {
5975
- budgetDecision = {
5976
- allowed: true,
5977
- reason: "allowed-same-namespace",
5978
- count: 0,
5979
- limit: {
5980
- softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
5981
- hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
5982
- windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 6e4
5601
+ },
5602
+ writePromotedMemory: async (namespace, memory) => {
5603
+ const resolved = await this.deps.orchestrator.getStorage(namespace);
5604
+ const { id, tombstoneBlocked } = await resolved.writeMemory(
5605
+ memory.category,
5606
+ memory.content,
5607
+ {
5608
+ confidence: memory.confidence,
5609
+ tags: memory.tags,
5610
+ entityRef: memory.entityRef,
5611
+ source: `admin-promotion:${memory.sourceNamespace}:${memory.reason.slice(0, 120)}`,
5612
+ lineage: memory.lineage,
5613
+ sourceMemoryId: memory.sourceMemoryId,
5614
+ actor: memory.actor,
5615
+ validAt: memory.validAt
5983
5616
  }
5984
- };
5985
- }
5986
- if (!budgetDecision.allowed) {
5987
- throw new EngramAccessInputError(
5988
- `recall denied: cross-namespace budget exceeded (${budgetDecision.count}/${budgetDecision.limit.hardLimit} in ${budgetDecision.limit.windowMs}ms window)`
5989
- );
5990
- }
5991
- this.deps.budget.gc();
5992
- }
5993
- const topK = Number.isFinite(request.topK) ? Math.max(0, Math.floor(request.topK ?? 0)) : void 0;
5994
- let asOf;
5995
- if (request.asOf !== void 0 && request.asOf !== null) {
5996
- if (typeof request.asOf !== "string" || request.asOf.trim().length === 0) {
5997
- throw new EngramAccessInputError(
5998
- "asOf must be a non-empty ISO 8601 timestamp string"
5999
5617
  );
5618
+ if (tombstoneBlocked) {
5619
+ throw new Error(
5620
+ "target namespace tombstone-blocked the promoted content (pending_review)"
5621
+ );
5622
+ }
5623
+ return id;
6000
5624
  }
6001
- const parsed = Date.parse(request.asOf);
6002
- if (!Number.isFinite(parsed)) {
6003
- throw new EngramAccessInputError(
6004
- `asOf must be a parseable ISO 8601 timestamp (got: "${request.asOf}")`
6005
- );
5625
+ };
5626
+ try {
5627
+ return await promoteMemory({
5628
+ config,
5629
+ sourceMemoryId: request.sourceMemoryId,
5630
+ sourceNamespace,
5631
+ principal: request.principal,
5632
+ targets: request.targets,
5633
+ reason: request.reason,
5634
+ actor: request.principal ?? "admin-console",
5635
+ storage,
5636
+ scopeProfilePlan
5637
+ });
5638
+ } catch (err) {
5639
+ if (err instanceof AdminPromotionError) {
5640
+ throw new EngramAccessInputError(err.message);
6006
5641
  }
6007
- asOf = request.asOf;
5642
+ throw err;
6008
5643
  }
6009
- const recallOptions = {
6010
- namespace: namespaceOverride,
5644
+ }
5645
+ };
5646
+
5647
+ // src/access-recall-surface.ts
5648
+ import { createHash as createHash4 } from "crypto";
5649
+ import * as nodePath2 from "path";
5650
+
5651
+ // src/orchestration/qmd-result-resolver.ts
5652
+ import path8 from "path";
5653
+ function qmdCollectionPathParts(resultPath) {
5654
+ if (!resultPath || path8.isAbsolute(resultPath)) return null;
5655
+ const normalized = resultPath.replace(/\\/g, "/").replace(/^\/+/, "");
5656
+ const slashIndex = normalized.indexOf("/");
5657
+ if (slashIndex <= 0 || slashIndex >= normalized.length - 1) return null;
5658
+ const collection = normalized.slice(0, slashIndex);
5659
+ if (/^\d{4}-\d{2}-\d{2}$/.test(collection)) return null;
5660
+ return {
5661
+ collection,
5662
+ relativePath: normalized.slice(slashIndex + 1)
5663
+ };
5664
+ }
5665
+ function qmdResultPathCandidates(storageDir, resultPath) {
5666
+ const candidates = /* @__PURE__ */ new Set();
5667
+ const storageRoot = path8.resolve(storageDir);
5668
+ const addCandidate = (candidate) => {
5669
+ const resolved = path8.resolve(candidate);
5670
+ if (isPathInsideStorageRoot(storageRoot, resolved)) {
5671
+ candidates.add(resolved);
5672
+ }
5673
+ };
5674
+ const addRelativeCandidates = (relativePath) => {
5675
+ const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
5676
+ if (!normalized) return;
5677
+ addCandidate(path8.join(storageRoot, normalized));
5678
+ if (/^\d{4}-\d{2}-\d{2}\//.test(normalized)) {
5679
+ addCandidate(path8.join(storageRoot, "facts", normalized));
5680
+ }
5681
+ };
5682
+ if (path8.isAbsolute(resultPath)) {
5683
+ addCandidate(resultPath);
5684
+ } else {
5685
+ addRelativeCandidates(resultPath);
5686
+ }
5687
+ return [...candidates];
5688
+ }
5689
+ function storageDirOrNull(storage) {
5690
+ const dir = storage.dir;
5691
+ return typeof dir === "string" && dir.length > 0 ? dir : null;
5692
+ }
5693
+ var QmdResultResolver = class {
5694
+ getConfig;
5695
+ storageFor;
5696
+ storageDirNamespace;
5697
+ qmdCollectionNamespaceFromPrefix;
5698
+ namespaceFromPath;
5699
+ constructor(options) {
5700
+ this.getConfig = options.getConfig;
5701
+ this.storageFor = options.storageFor;
5702
+ this.storageDirNamespace = options.storageDirNamespace;
5703
+ this.qmdCollectionNamespaceFromPrefix = options.qmdCollectionNamespaceFromPrefix;
5704
+ this.namespaceFromPath = options.namespaceFromPath;
5705
+ }
5706
+ async readQmdResultMemory(resultPath, fallbackStorage, recallNamespaces = []) {
5707
+ const parts = qmdCollectionPathParts(resultPath);
5708
+ const fallbackStorageDir = storageDirOrNull(fallbackStorage);
5709
+ const config = this.getConfig();
5710
+ const coldCollection = config.qmdColdCollection ?? "openclaw-engram-cold";
5711
+ if (parts && parts.collection === coldCollection) {
5712
+ const storages = [];
5713
+ const seenStorageDirs = /* @__PURE__ */ new Set();
5714
+ const addStorage = (storage) => {
5715
+ const storageDir = storageDirOrNull(storage);
5716
+ const storageKey = storageDir ? path8.resolve(storageDir) : `storage-without-dir-${storages.length}`;
5717
+ if (seenStorageDirs.has(storageKey)) return;
5718
+ seenStorageDirs.add(storageKey);
5719
+ storages.push(storage);
5720
+ };
5721
+ const fallbackNamespace = fallbackStorageDir !== null ? this.storageDirNamespace(fallbackStorageDir) : config.defaultNamespace;
5722
+ if (recallNamespaces.length === 0 || !resolveNamespaceCapabilities(config).namespaces || recallNamespaces.includes(fallbackNamespace)) {
5723
+ addStorage(fallbackStorage);
5724
+ }
5725
+ if (recallNamespaces.length > 0) {
5726
+ for (const namespace of recallNamespaces) {
5727
+ try {
5728
+ addStorage(await this.storageFor(namespace));
5729
+ } catch (err) {
5730
+ log.debug("qmd cold result namespace storage lookup skipped", {
5731
+ path: resultPath,
5732
+ namespace,
5733
+ error: err.message
5734
+ });
5735
+ }
5736
+ }
5737
+ }
5738
+ for (const storage of storages) {
5739
+ const storageDir = storageDirOrNull(storage);
5740
+ if (!storageDir) {
5741
+ const memory = await storage.readMemoryByPath(resultPath);
5742
+ if (memory) return memory;
5743
+ continue;
5744
+ }
5745
+ try {
5746
+ const coldRoot = path8.join(storageDir, "cold");
5747
+ for (const candidate of qmdResultPathCandidates(
5748
+ coldRoot,
5749
+ parts.relativePath
5750
+ )) {
5751
+ const memory = await storage.readMemoryByPath(candidate);
5752
+ if (memory) return memory;
5753
+ }
5754
+ } catch (err) {
5755
+ if (err instanceof SecureStoreLockedError) throw err;
5756
+ log.debug("qmd cold result path lookup failed open", {
5757
+ path: resultPath,
5758
+ collection: coldCollection,
5759
+ error: err.message
5760
+ });
5761
+ }
5762
+ }
5763
+ return null;
5764
+ }
5765
+ const collectionNamespace = parts ? this.qmdCollectionNamespaceFromPrefix(parts.collection) : null;
5766
+ if (parts && collectionNamespace) {
5767
+ try {
5768
+ const collectionStorage = await this.storageFor(collectionNamespace);
5769
+ for (const candidate of qmdResultPathCandidates(
5770
+ collectionStorage.dir,
5771
+ parts.relativePath
5772
+ )) {
5773
+ const memory = await collectionStorage.readMemoryByPath(candidate);
5774
+ if (memory) return memory;
5775
+ }
5776
+ return null;
5777
+ } catch (err) {
5778
+ if (err instanceof SecureStoreLockedError) throw err;
5779
+ log.debug("qmd result namespace path lookup failed open", {
5780
+ path: resultPath,
5781
+ namespace: collectionNamespace,
5782
+ error: err.message
5783
+ });
5784
+ return null;
5785
+ }
5786
+ }
5787
+ if (path8.isAbsolute(resultPath)) {
5788
+ if (!fallbackStorageDir) {
5789
+ return await fallbackStorage.readMemoryByPath(resultPath);
5790
+ }
5791
+ const ownerStorage = await this.storageForAbsoluteQmdResultPath(
5792
+ resultPath,
5793
+ fallbackStorage,
5794
+ recallNamespaces
5795
+ );
5796
+ if (!ownerStorage) return null;
5797
+ for (const candidate of qmdResultPathCandidates(
5798
+ ownerStorage.dir,
5799
+ resultPath
5800
+ )) {
5801
+ const memory = await ownerStorage.storage.readMemoryByPath(candidate);
5802
+ if (memory) return memory;
5803
+ }
5804
+ return null;
5805
+ }
5806
+ if (!fallbackStorageDir) {
5807
+ return await fallbackStorage.readMemoryByPath(resultPath);
5808
+ }
5809
+ for (const candidate of qmdResultPathCandidates(
5810
+ fallbackStorageDir,
5811
+ resultPath
5812
+ )) {
5813
+ const memory = await fallbackStorage.readMemoryByPath(candidate);
5814
+ if (memory) return memory;
5815
+ }
5816
+ return null;
5817
+ }
5818
+ async resolveColdQmdResultForRecall(result, fallbackStorage, recallNamespaces = []) {
5819
+ const memory = await this.readQmdResultMemory(
5820
+ result.path,
5821
+ fallbackStorage,
5822
+ recallNamespaces
5823
+ );
5824
+ if (!memory) return null;
5825
+ let ownerNamespace = null;
5826
+ if (path8.isAbsolute(memory.path)) {
5827
+ const ownerStorage = await this.storageForAbsoluteQmdResultPath(
5828
+ memory.path,
5829
+ fallbackStorage,
5830
+ recallNamespaces
5831
+ );
5832
+ ownerNamespace = ownerStorage?.namespace ?? null;
5833
+ if (!ownerNamespace && resolveNamespaceCapabilities(this.getConfig()).namespaces) return null;
5834
+ }
5835
+ ownerNamespace ??= this.namespaceFromPath(memory.path);
5836
+ if (recallNamespaces.length > 0 && !recallNamespaces.includes(ownerNamespace)) {
5837
+ return null;
5838
+ }
5839
+ return {
5840
+ namespace: ownerNamespace,
5841
+ result: {
5842
+ ...result,
5843
+ docid: result.docid || memory.frontmatter.id,
5844
+ path: memory.path,
5845
+ snippet: result.snippet || memory.content.slice(0, 400)
5846
+ }
5847
+ };
5848
+ }
5849
+ async storageForAbsoluteQmdResultPath(resultPath, fallbackStorage, recallNamespaces = []) {
5850
+ const resolvedPath = path8.resolve(resultPath);
5851
+ const config = this.getConfig();
5852
+ const memoryRoot = path8.resolve(config.memoryDir);
5853
+ const namespacesRoot = path8.join(memoryRoot, "namespaces");
5854
+ const fallbackStorageDir = storageDirOrNull(fallbackStorage);
5855
+ const matches = [];
5856
+ const seenDirs = /* @__PURE__ */ new Set();
5857
+ const maybeAddStorage = (storage, namespace) => {
5858
+ const storageDir = storageDirOrNull(storage);
5859
+ if (!storageDir) return;
5860
+ const candidateRoot = path8.resolve(storageDir);
5861
+ if (seenDirs.has(candidateRoot)) return;
5862
+ if (!isPathInsideStorageRoot(candidateRoot, resolvedPath)) return;
5863
+ if (candidateRoot === memoryRoot && isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
5864
+ return;
5865
+ }
5866
+ seenDirs.add(candidateRoot);
5867
+ matches.push({ storage, dir: candidateRoot, namespace });
5868
+ };
5869
+ const fallbackNamespace = fallbackStorageDir !== null ? this.storageDirNamespace(fallbackStorageDir) : config.defaultNamespace;
5870
+ maybeAddStorage(fallbackStorage, fallbackNamespace);
5871
+ const candidateNamespaces = /* @__PURE__ */ new Set();
5872
+ candidateNamespaces.add(config.defaultNamespace);
5873
+ candidateNamespaces.add(config.sharedNamespace);
5874
+ for (const ns of recallNamespaces) {
5875
+ candidateNamespaces.add(ns);
5876
+ }
5877
+ if (isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
5878
+ const relativeToNamespaces = path8.relative(namespacesRoot, resolvedPath);
5879
+ const [namespaceSegment] = relativeToNamespaces.split(/[\\/]/);
5880
+ if (namespaceSegment) {
5881
+ candidateNamespaces.add(
5882
+ namespaceIdentityFromToken(namespaceSegment) ?? namespaceSegment
5883
+ );
5884
+ }
5885
+ }
5886
+ for (const policy of config.namespacePolicies ?? []) {
5887
+ candidateNamespaces.add(policy.name);
5888
+ }
5889
+ for (const ns of candidateNamespaces) {
5890
+ if (!ns) continue;
5891
+ try {
5892
+ maybeAddStorage(await this.storageFor(ns), ns);
5893
+ } catch {
5894
+ continue;
5895
+ }
5896
+ }
5897
+ matches.sort((a, b) => b.dir.length - a.dir.length);
5898
+ return matches[0] ?? null;
5899
+ }
5900
+ };
5901
+
5902
+ // src/access-recall-surface.ts
5903
+ var AccessRecallSurface = class {
5904
+ constructor(deps) {
5905
+ this.deps = deps;
5906
+ }
5907
+ deps;
5908
+ async buildRecallResponseFromXraySnapshot(options) {
5909
+ const memoryIds = options.snapshot.results.map((result) => result.memoryId);
5910
+ const resultPaths = options.snapshot.results.map((result) => result.path);
5911
+ const namespace = options.snapshot.namespace ? this.deps.resolveNamespace(options.snapshot.namespace) : this.deps.orchestrator.config.defaultNamespace;
5912
+ const sourcesUsed = Array.from(
5913
+ new Set(options.snapshot.results.map((result) => result.servedBy))
5914
+ );
5915
+ const snapshotForSerialization = {
5916
+ sessionKey: options.sessionKey ?? "",
5917
+ recordedAt: new Date(options.snapshot.capturedAt).toISOString(),
5918
+ queryHash: createHash4("sha256").update(options.query).digest("hex"),
5919
+ queryLen: options.query.length,
5920
+ memoryIds,
5921
+ namespace,
5922
+ recallNamespaces: [namespace],
5923
+ traceId: options.snapshot.traceId,
5924
+ plannerMode: options.normalizedMode,
5925
+ requestedMode: options.requestedMode && options.requestedMode !== "auto" ? options.requestedMode : void 0,
5926
+ sourcesUsed,
5927
+ budgetsApplied: {
5928
+ appliedTopK: memoryIds.length,
5929
+ recallBudgetChars: options.snapshot.budget.chars,
5930
+ maxMemoryTokens: this.deps.orchestrator.config.maxMemoryTokens,
5931
+ finalContextChars: options.snapshot.budget.used
5932
+ },
5933
+ latencyMs: Date.now() - options.startedAt,
5934
+ resultPaths
5935
+ };
5936
+ const results = await this.deps.serializeRecallResults(
5937
+ snapshotForSerialization,
5938
+ options.disclosure,
5939
+ {
5940
+ query: options.query,
5941
+ ...options.sessionKey ? { sessionKey: options.sessionKey } : {},
5942
+ ...options.rawExcerptNamespace ? { rawExcerptNamespace: options.rawExcerptNamespace } : {},
5943
+ ...options.rawExcerptSessionIds !== void 0 ? { rawExcerptSessionIds: options.rawExcerptSessionIds } : {},
5944
+ ...options.rawExcerptsSuppressed ? { rawExcerptsSuppressed: options.rawExcerptsSuppressed } : {}
5945
+ }
5946
+ );
5947
+ const context = results.map((result) => {
5948
+ const content = typeof result.content === "string" && result.content.length > 0 ? result.content : "";
5949
+ return content || result.preview;
5950
+ }).filter((text) => text.length > 0).join("\n\n");
5951
+ return {
5952
+ query: options.query,
5953
+ ...options.sessionKey ? { sessionKey: options.sessionKey } : {},
5954
+ namespace,
5955
+ context,
5956
+ count: memoryIds.length,
5957
+ memoryIds,
5958
+ results,
5959
+ recordedAt: snapshotForSerialization.recordedAt,
5960
+ traceId: options.snapshot.traceId,
5961
+ plannerMode: options.normalizedMode,
5962
+ fallbackUsed: sourcesUsed.some((source) => source !== "hybrid"),
5963
+ sourcesUsed,
5964
+ disclosure: options.disclosure,
5965
+ budgetsApplied: snapshotForSerialization.budgetsApplied,
5966
+ latencyMs: snapshotForSerialization.latencyMs
5967
+ };
5968
+ }
5969
+ async serializeRecallResults(snapshot, disclosure, rawContext = null) {
5970
+ if (!snapshot) return [];
5971
+ const namespace = snapshot.namespace ? this.deps.resolveNamespace(snapshot.namespace) : this.deps.orchestrator.config.defaultNamespace;
5972
+ const storage = await this.deps.orchestrator.getStorage(namespace);
5973
+ const storageDir = storage.dir;
5974
+ const recallNamespaces = Array.from(
5975
+ new Set(
5976
+ [
5977
+ namespace,
5978
+ ...Array.isArray(snapshot.recallNamespaces) ? snapshot.recallNamespaces.map((ns) => this.deps.resolveNamespace(ns)) : [],
5979
+ this.deps.orchestrator.config.defaultNamespace,
5980
+ this.deps.orchestrator.config.sharedNamespace,
5981
+ ...(this.deps.orchestrator.config.namespacePolicies ?? []).map((p) => p.name)
5982
+ ].filter((ns) => typeof ns === "string" && ns.length > 0)
5983
+ )
5984
+ );
5985
+ const results = [];
5986
+ const seen = /* @__PURE__ */ new Set();
5987
+ const collectionNamespaceFromPrefix = (collectionPrefix) => {
5988
+ const baseCollection = this.deps.orchestrator.config.qmdCollection;
5989
+ if (collectionPrefix === baseCollection) return this.deps.orchestrator.config.defaultNamespace;
5990
+ const namespaceSuffix = collectionPrefix.startsWith(`${baseCollection}--`) ? collectionPrefix.slice(baseCollection.length + 2) : "";
5991
+ if (!namespaceSuffix) return null;
5992
+ const decoded = namespaceIdentityFromToken(namespaceSuffix);
5993
+ if (decoded !== null) return decoded || this.deps.orchestrator.config.defaultNamespace;
5994
+ if (namespaceSuffix.startsWith("ns--")) {
5995
+ const legacyNamespace = namespaceSuffix.slice("ns--".length).trim();
5996
+ return legacyNamespace || null;
5997
+ }
5998
+ return null;
5999
+ };
6000
+ const readResultPath = async (memoryPath) => {
6001
+ const parts = qmdCollectionPathParts(memoryPath);
6002
+ const coldCollection = this.deps.orchestrator.config.qmdColdCollection ?? "openclaw-engram-cold";
6003
+ if (parts && parts.collection === coldCollection) {
6004
+ const storages = [];
6005
+ const seenStorageDirs = /* @__PURE__ */ new Set();
6006
+ const addStorage = (candidateStorage) => {
6007
+ const candidateDir = nodePath2.resolve(candidateStorage.dir);
6008
+ if (seenStorageDirs.has(candidateDir)) return;
6009
+ seenStorageDirs.add(candidateDir);
6010
+ storages.push({ storage: candidateStorage, dir: candidateDir });
6011
+ };
6012
+ addStorage(storage);
6013
+ for (const recallNamespace of recallNamespaces) {
6014
+ try {
6015
+ addStorage(await this.deps.orchestrator.getStorage(recallNamespace));
6016
+ } catch {
6017
+ continue;
6018
+ }
6019
+ }
6020
+ for (const candidateStorage of storages) {
6021
+ try {
6022
+ const coldRoot = nodePath2.join(candidateStorage.dir, "cold");
6023
+ for (const candidatePath of qmdResultPathCandidates(
6024
+ coldRoot,
6025
+ parts.relativePath
6026
+ )) {
6027
+ const memory = await candidateStorage.storage.readMemoryByPath(candidatePath);
6028
+ if (memory) return { memory, baseDir: candidateStorage.dir };
6029
+ }
6030
+ } catch (err) {
6031
+ if (err instanceof SecureStoreLockedError) throw err;
6032
+ continue;
6033
+ }
6034
+ }
6035
+ return null;
6036
+ }
6037
+ const collectionNamespace = parts ? collectionNamespaceFromPrefix(parts.collection) : null;
6038
+ if (parts && collectionNamespace) {
6039
+ try {
6040
+ const collectionStorage = await this.deps.orchestrator.getStorage(collectionNamespace);
6041
+ for (const candidate of qmdResultPathCandidates(
6042
+ collectionStorage.dir,
6043
+ parts.relativePath
6044
+ )) {
6045
+ const memory = await collectionStorage.readMemoryByPath(candidate);
6046
+ if (memory) return { memory, baseDir: collectionStorage.dir };
6047
+ }
6048
+ return null;
6049
+ } catch (err) {
6050
+ if (err instanceof SecureStoreLockedError) throw err;
6051
+ return null;
6052
+ }
6053
+ }
6054
+ if (nodePath2.isAbsolute(memoryPath)) {
6055
+ const ownerStorage = await this.deps.storageForAbsoluteRecallPath(
6056
+ memoryPath,
6057
+ namespace,
6058
+ recallNamespaces
6059
+ );
6060
+ if (!ownerStorage) return null;
6061
+ for (const candidate of qmdResultPathCandidates(
6062
+ ownerStorage.dir,
6063
+ memoryPath
6064
+ )) {
6065
+ const memory = await ownerStorage.storage.readMemoryByPath(candidate);
6066
+ if (memory) return { memory, baseDir: ownerStorage.dir };
6067
+ }
6068
+ return null;
6069
+ }
6070
+ for (const candidate of qmdResultPathCandidates(storageDir, memoryPath)) {
6071
+ const memory = await storage.readMemoryByPath(candidate);
6072
+ if (memory) return { memory, baseDir: storageDir };
6073
+ }
6074
+ return null;
6075
+ };
6076
+ const rawExcerptsResult = rawContext?.rawExcerptsSuppressed === true ? (
6077
+ // Implicit raw recall with NO readable LCM namespace (#1505 thread
6078
+ // NBHWz): emit empty excerpts rather than falling back to the
6079
+ // write/overlay `namespace` the read gate excludes.
6080
+ []
6081
+ ) : await this.deps.fetchRawExcerpts(
6082
+ disclosure,
6083
+ rawContext ? {
6084
+ query: rawContext.query,
6085
+ ...rawContext.sessionKey ? { sessionKey: rawContext.sessionKey } : {},
6086
+ namespace: rawContext.rawExcerptNamespace ?? namespace,
6087
+ ...rawContext.rawExcerptSessionIds !== void 0 ? { lcmSessionIds: rawContext.rawExcerptSessionIds } : {}
6088
+ } : null
6089
+ );
6090
+ const rawExcerpts = rawExcerptsResult ?? void 0;
6091
+ for (const memoryPath of snapshot.resultPaths ?? []) {
6092
+ if (!memoryPath || seen.has(memoryPath)) continue;
6093
+ const resolved = await readResultPath(memoryPath);
6094
+ if (!resolved) continue;
6095
+ const { memory, baseDir } = resolved;
6096
+ seen.add(memoryPath);
6097
+ results.push(
6098
+ this.deps.serializeMemorySummary(
6099
+ memory,
6100
+ baseDir,
6101
+ disclosure,
6102
+ // Attach the (possibly empty) raw excerpts to the first raw
6103
+ // result; subsequent results do not duplicate the array.
6104
+ results.length === 0 ? rawExcerpts : void 0
6105
+ )
6106
+ );
6107
+ }
6108
+ if (results.length > 0) return results;
6109
+ for (const memoryId of snapshot.memoryIds) {
6110
+ const memory = await storage.getMemoryById(memoryId);
6111
+ if (!memory || seen.has(memory.path)) continue;
6112
+ seen.add(memory.path);
6113
+ results.push(
6114
+ this.deps.serializeMemorySummary(
6115
+ memory,
6116
+ storageDir,
6117
+ disclosure,
6118
+ results.length === 0 ? rawExcerpts : void 0
6119
+ )
6120
+ );
6121
+ }
6122
+ return results;
6123
+ }
6124
+ /**
6125
+ * Fetch raw transcript excerpts from the LCM archive for `disclosure ===
6126
+ * "raw"` recalls (issue #677 PR 2/4). Returns `null` for non-raw recall
6127
+ * depths, an empty array when LCM is disabled / not initialized / has no
6128
+ * matches, and an array of LCM-side excerpts otherwise. Errors are
6129
+ * swallowed and treated as "no excerpts" so a failing LCM never breaks
6130
+ * the recall response.
6131
+ *
6132
+ * Namespace handling: LCM archival prefixes non-default-namespace
6133
+ * sessions with `${namespace}:${sessionKey}` (see `observe()` around
6134
+ * line 2498), so the lookup must mirror that prefix or raw recalls in
6135
+ * non-default namespaces miss their own excerpts.
6136
+ */
6137
+ async fetchRawExcerpts(disclosure, context) {
6138
+ if (disclosure !== "raw") return null;
6139
+ if (!context || !context.query) return [];
6140
+ if (!context.sessionKey) return [];
6141
+ const lcm = this.deps.orchestrator.lcmEngine;
6142
+ if (!lcm || !lcm.enabled) return [];
6143
+ try {
6144
+ const legacyKey = context.namespace && context.namespace !== this.deps.orchestrator.config.defaultNamespace ? `${context.namespace}:${context.sessionKey}` : context.sessionKey;
6145
+ const lcmSessionIds = context.lcmSessionIds !== void 0 ? context.lcmSessionIds : [legacyKey];
6146
+ const limit = 5;
6147
+ const seenRows = /* @__PURE__ */ new Set();
6148
+ const excerpts = [];
6149
+ const settledRows = await Promise.allSettled(
6150
+ lcmSessionIds.map(
6151
+ async (lcmSessionKey) => lcm.searchContextFull(context.query, limit, lcmSessionKey)
6152
+ )
6153
+ );
6154
+ for (const result of settledRows) {
6155
+ if (excerpts.length >= limit) break;
6156
+ if (result.status !== "fulfilled") continue;
6157
+ for (const r of result.value) {
6158
+ const dedupeKey = `${r.session_id} ${r.turn_index}`;
6159
+ if (seenRows.has(dedupeKey)) continue;
6160
+ seenRows.add(dedupeKey);
6161
+ excerpts.push({
6162
+ turnIndex: r.turn_index,
6163
+ role: r.role,
6164
+ content: r.content,
6165
+ sessionId: r.session_id
6166
+ });
6167
+ if (excerpts.length >= limit) break;
6168
+ }
6169
+ }
6170
+ return excerpts;
6171
+ } catch {
6172
+ return [];
6173
+ }
6174
+ }
6175
+ async executeRecall(request) {
6176
+ const query = request.query;
6177
+ const callerProvidedDisclosure = request.disclosure !== void 0 && request.disclosure !== null;
6178
+ const requestedDisclosure = (() => {
6179
+ if (!callerProvidedDisclosure) {
6180
+ return DEFAULT_RECALL_DISCLOSURE;
6181
+ }
6182
+ if (!isRecallDisclosure(request.disclosure)) {
6183
+ throw new EngramAccessInputError(
6184
+ `disclosure must be one of: chunk, section, raw (got: ${String(request.disclosure)})`
6185
+ );
6186
+ }
6187
+ return request.disclosure;
6188
+ })();
6189
+ if (request.codingContext !== void 0 && request.sessionKey) {
6190
+ this.deps.setCodingContext({
6191
+ sessionKey: request.sessionKey,
6192
+ codingContext: request.codingContext
6193
+ });
6194
+ }
6195
+ if (request.codingContext === void 0 && request.sessionKey) {
6196
+ await this.deps.maybeAttachCodingContext(request.sessionKey, {
6197
+ cwd: request.cwd,
6198
+ projectTag: request.projectTag
6199
+ });
6200
+ }
6201
+ const authenticatedPrincipal = request.authenticatedPrincipal?.trim();
6202
+ const namespaceOverride = this.deps.resolveRecallNamespace(
6203
+ request.namespace,
6204
+ request.sessionKey,
6205
+ authenticatedPrincipal
6206
+ );
6207
+ const namespace = namespaceOverride ?? this.deps.orchestrator.config.defaultNamespace;
6208
+ const mode = this.deps.normalizeRecallMode(request.mode);
6209
+ const maybePrincipal = this.deps.resolveRequestPrincipal(request.sessionKey, authenticatedPrincipal);
6210
+ if (resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces && !maybePrincipal) {
6211
+ throw new EngramAccessInputError(
6212
+ "authentication required: namespaces are enabled and no principal was supplied"
6213
+ );
6214
+ }
6215
+ const principal = maybePrincipal ?? "default";
6216
+ const principalNamespace = defaultNamespaceForPrincipal(principal, this.deps.orchestrator.config);
6217
+ const profileCodingContext = request.sessionKey && typeof this.deps.orchestrator.getCodingContextForSession === "function" ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey) : null;
6218
+ const profileCodingOverlay = !namespaceOverride && profileCodingContext && resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces && this.deps.orchestrator.config.codingMode?.projectScope ? resolveCodingNamespaceOverlay(
6219
+ profileCodingContext,
6220
+ this.deps.orchestrator.config.codingMode,
6221
+ this.deps.orchestrator.config.defaultNamespace
6222
+ ) : null;
6223
+ const profilePlan = namespaceOverride ? null : resolveScopeProfilePlan({
6224
+ config: this.deps.orchestrator.config,
6225
+ principal,
6226
+ codingContext: profileCodingContext,
6227
+ codingOverlay: profileCodingOverlay
6228
+ });
6229
+ const modeSkipsBudget = mode === "no_recall";
6230
+ const legacyRecallNamespaces = Array.isArray(this.deps.orchestrator.config.defaultRecallNamespaces) ? recallNamespacesForPrincipal(principal, this.deps.orchestrator.config) : [];
6231
+ const effectiveNamespaces = namespaceOverride ? [namespaceOverride] : profilePlan ? expandScopeProfileReadNamespaces({
6232
+ profilePlan,
6233
+ principalSelfNamespace: profilePlan.baseNamespace,
6234
+ config: this.deps.orchestrator.config,
6235
+ principal,
6236
+ codingOverlay: profileCodingOverlay,
6237
+ legacyRecallNamespaces
6238
+ }) : legacyRecallNamespaces;
6239
+ const budgetPrincipalNamespace = profilePlan?.baseNamespace ?? principalNamespace;
6240
+ let budgetDecision;
6241
+ let recordBudgetAfterSuccess = false;
6242
+ if (modeSkipsBudget) {
6243
+ budgetDecision = {
6244
+ allowed: true,
6245
+ reason: "allowed-same-namespace",
6246
+ count: 0,
6247
+ limit: {
6248
+ softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
6249
+ hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
6250
+ windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 6e4
6251
+ }
6252
+ };
6253
+ } else {
6254
+ let anyCrossNamespace = false;
6255
+ let denied = null;
6256
+ let crossNamespaceDecision = null;
6257
+ for (const ns of effectiveNamespaces) {
6258
+ const peek = this.deps.budget.peek({
6259
+ principal,
6260
+ principalNamespace: budgetPrincipalNamespace,
6261
+ queryNamespace: ns
6262
+ });
6263
+ if (peek.reason !== "allowed-same-namespace") {
6264
+ anyCrossNamespace = true;
6265
+ crossNamespaceDecision ??= peek;
6266
+ }
6267
+ if (!peek.allowed) {
6268
+ denied = peek;
6269
+ break;
6270
+ }
6271
+ }
6272
+ if (denied) {
6273
+ budgetDecision = denied;
6274
+ } else if (anyCrossNamespace) {
6275
+ budgetDecision = crossNamespaceDecision ?? {
6276
+ allowed: true,
6277
+ reason: "allowed-under-soft",
6278
+ count: 0,
6279
+ limit: {
6280
+ softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
6281
+ hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
6282
+ windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 6e4
6283
+ }
6284
+ };
6285
+ recordBudgetAfterSuccess = true;
6286
+ } else {
6287
+ budgetDecision = {
6288
+ allowed: true,
6289
+ reason: "allowed-same-namespace",
6290
+ count: 0,
6291
+ limit: {
6292
+ softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
6293
+ hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
6294
+ windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 6e4
6295
+ }
6296
+ };
6297
+ }
6298
+ if (!budgetDecision.allowed) {
6299
+ throw new EngramAccessInputError(
6300
+ `recall denied: cross-namespace budget exceeded (${budgetDecision.count}/${budgetDecision.limit.hardLimit} in ${budgetDecision.limit.windowMs}ms window)`
6301
+ );
6302
+ }
6303
+ this.deps.budget.gc();
6304
+ }
6305
+ const topK = Number.isFinite(request.topK) ? Math.max(0, Math.floor(request.topK ?? 0)) : void 0;
6306
+ let asOf;
6307
+ if (request.asOf !== void 0 && request.asOf !== null) {
6308
+ if (typeof request.asOf !== "string" || request.asOf.trim().length === 0) {
6309
+ throw new EngramAccessInputError(
6310
+ "asOf must be a non-empty ISO 8601 timestamp string"
6311
+ );
6312
+ }
6313
+ const parsed = Date.parse(request.asOf);
6314
+ if (!Number.isFinite(parsed)) {
6315
+ throw new EngramAccessInputError(
6316
+ `asOf must be a parseable ISO 8601 timestamp (got: "${request.asOf}")`
6317
+ );
6318
+ }
6319
+ asOf = request.asOf;
6320
+ }
6321
+ const recallOptions = {
6322
+ namespace: namespaceOverride,
6011
6323
  topK,
6012
6324
  mode,
6013
6325
  ...authenticatedPrincipal ? { principalOverride: authenticatedPrincipal } : {},
@@ -6370,25 +6682,6 @@ var AccessRecallSurface = class {
6370
6682
  }
6371
6683
  };
6372
6684
 
6373
- // src/orchestration/self-deps.ts
6374
- function selfDeps(self) {
6375
- const source = self;
6376
- return new Proxy(/* @__PURE__ */ Object.create(null), {
6377
- get(_target, prop) {
6378
- const value = source[prop];
6379
- return typeof value === "function" ? value.bind(self) : value;
6380
- },
6381
- set(_target, prop, value) {
6382
- if (!(prop in source)) return false;
6383
- source[prop] = value;
6384
- return true;
6385
- },
6386
- has(_target, prop) {
6387
- return prop in source;
6388
- }
6389
- });
6390
- }
6391
-
6392
6685
  // src/access-service.ts
6393
6686
  var EngramAccessInputError = class extends Error {
6394
6687
  };
@@ -6572,6 +6865,16 @@ var EngramAccessService = class {
6572
6865
  budgetLocks = /* @__PURE__ */ new Map();
6573
6866
  budget;
6574
6867
  auditAdapter;
6868
+ /** AccessObserveWriteSurface (access-service decomposition). Lazy; selfDeps live wiring. */
6869
+ _accessObserveWriteSurface;
6870
+ get accessObserveWriteSurface() {
6871
+ if (!this._accessObserveWriteSurface) {
6872
+ this._accessObserveWriteSurface = new AccessObserveWriteSurface(
6873
+ selfDeps(this)
6874
+ );
6875
+ }
6876
+ return this._accessObserveWriteSurface;
6877
+ }
6575
6878
  /** AccessLcmSurface (access-service decomposition). Lazy; selfDeps live wiring. */
6576
6879
  _accessLcmSurface;
6577
6880
  get accessLcmSurface() {
@@ -7309,114 +7612,20 @@ var EngramAccessService = class {
7309
7612
  });
7310
7613
  }
7311
7614
  async qmdHealth(searchBackend, qmdEnabled, namespace, collection) {
7312
- if (searchBackend !== "qmd" || !qmdEnabled) {
7313
- return {
7314
- enabled: qmdEnabled,
7315
- active: false,
7316
- degraded: false,
7317
- mode: searchBackend !== "qmd" ? "not-selected" : "disabled",
7318
- collection,
7319
- collectionState: "skipped",
7320
- installedVersion: null,
7321
- supportedVersion: null,
7322
- supported: null,
7323
- upgradeAvailable: null,
7324
- doctorAvailable: null,
7325
- debugStatus: searchBackend !== "qmd" ? `backend=${searchBackend}` : "backend=disabled"
7326
- };
7327
- }
7328
- if (resolveNamespaceCapabilities(this.orchestrator.config).namespaces === true) {
7329
- const namespaceHealth = await this.namespaceQmdHealth(searchBackend, qmdEnabled, namespace, collection);
7330
- if (namespaceHealth) return namespaceHealth;
7331
- }
7332
- const qmd = this.orchestrator.qmd;
7333
- if (!qmd) {
7334
- return {
7335
- enabled: true,
7336
- active: false,
7337
- degraded: true,
7338
- mode: "fallback",
7339
- collection,
7340
- collectionState: "unknown",
7341
- installedVersion: null,
7342
- supportedVersion: null,
7343
- supported: null,
7344
- upgradeAvailable: null,
7345
- doctorAvailable: null,
7346
- debugStatus: "backend=unavailable"
7347
- };
7348
- }
7349
- const diagnosticAvailable = await this.qmdProbeAvailable(searchBackend, qmdEnabled);
7350
- const operationalAvailable = diagnosticAvailable || qmd.isAvailable();
7351
- const collectionState = diagnosticAvailable ? await this.qmdCollectionState(searchBackend, qmdEnabled, collection) : "unknown";
7352
- const active = operationalAvailable && collectionState !== "missing";
7353
- const degraded = searchBackend === "qmd" && qmdEnabled && (!active || !diagnosticAvailable || collectionState === "unknown");
7354
- const debugStatus = qmd.debugStatus();
7355
- const versionStatus = "getVersionStatus" in qmd && typeof qmd.getVersionStatus === "function" ? qmd.getVersionStatus() : null;
7356
- const daemonMode = "isDaemonMode" in qmd && typeof qmd.isDaemonMode === "function" ? qmd.isDaemonMode() === true : false;
7357
- const mode = searchBackend !== "qmd" ? "not-selected" : !qmdEnabled ? "disabled" : !active ? "fallback" : daemonMode ? "daemon" : "cli";
7358
- return {
7359
- enabled: qmdEnabled,
7360
- active,
7361
- degraded,
7362
- mode,
7363
- collection,
7364
- collectionState,
7365
- installedVersion: versionStatus?.installedVersion ?? null,
7366
- supportedVersion: versionStatus?.supportedVersion ?? null,
7367
- supported: versionStatus?.supported ?? null,
7368
- upgradeAvailable: versionStatus?.upgradeAvailable ?? null,
7369
- doctorAvailable: versionStatus?.capabilities?.doctor ?? null,
7370
- debugStatus
7371
- };
7615
+ return this.accessObserveWriteSurface.qmdHealth(
7616
+ searchBackend,
7617
+ qmdEnabled,
7618
+ namespace,
7619
+ collection
7620
+ );
7372
7621
  }
7373
7622
  async namespaceQmdHealth(searchBackend, qmdEnabled, namespace, fallbackCollection) {
7374
- if (searchBackend !== "qmd" || !qmdEnabled) return null;
7375
- const searchHealthForNamespace = this.orchestrator.searchHealthForNamespace;
7376
- if (typeof searchHealthForNamespace !== "function") return null;
7377
- const controller = new AbortController();
7378
- const timer = setTimeout(() => controller.abort(), 2e3);
7379
- timer.unref?.();
7380
- try {
7381
- const health = await searchHealthForNamespace.call(this.orchestrator, namespace, {
7382
- signal: controller.signal
7383
- });
7384
- const active = health.available && health.collectionState !== "missing";
7385
- const degraded = !active || health.collectionState === "unknown";
7386
- const mode = !active ? "fallback" : health.daemonMode === true ? "daemon" : "cli";
7387
- return {
7388
- enabled: true,
7389
- active,
7390
- degraded,
7391
- mode,
7392
- collection: health.collection || fallbackCollection,
7393
- collectionState: health.collectionState,
7394
- installedVersion: health.installedVersion,
7395
- supportedVersion: health.supportedVersion,
7396
- supported: health.supported,
7397
- upgradeAvailable: health.upgradeAvailable,
7398
- doctorAvailable: health.doctorAvailable,
7399
- debugStatus: health.debugStatus
7400
- };
7401
- } catch (error) {
7402
- const detail = displayErrorDetail(error) || "unknown";
7403
- return {
7404
- enabled: true,
7405
- active: false,
7406
- degraded: true,
7407
- mode: "fallback",
7408
- collection: fallbackCollection,
7409
- collectionState: "unknown",
7410
- installedVersion: null,
7411
- supportedVersion: null,
7412
- supported: null,
7413
- upgradeAvailable: null,
7414
- doctorAvailable: null,
7415
- debugStatus: `backend=namespace-unavailable error=${detail}`
7416
- };
7417
- } finally {
7418
- clearTimeout(timer);
7419
- }
7623
+ return this.accessObserveWriteSurface.namespaceQmdHealth(
7624
+ searchBackend,
7625
+ qmdEnabled,
7626
+ namespace,
7627
+ fallbackCollection
7628
+ );
7420
7629
  }
7421
7630
  async qmdCollectionState(searchBackend, qmdEnabled, collection) {
7422
7631
  if (searchBackend !== "qmd" || !qmdEnabled) return "skipped";
@@ -7471,70 +7680,14 @@ var EngramAccessService = class {
7471
7680
  if (memories.length === 0) {
7472
7681
  return this.orchestrator.generateDaySummaryAuto(namespace, {
7473
7682
  timeZone: request.timeZone
7474
- });
7475
- }
7476
- return this.orchestrator.generateDaySummary(memories);
7477
- }
7478
- /**
7479
- * Build a daily context briefing. Gracefully degrades when the OpenAI key
7480
- * or Responses API is unavailable — never throws for LLM-related problems.
7481
- */
7482
- async briefing(request) {
7483
- const config = this.orchestrator.config;
7484
- if (!config.briefing.enabled) {
7485
- throw new EngramAccessInputError("briefing is disabled");
7486
- }
7487
- const namespace = this.resolveReadableNamespace(request.namespace, request.principal);
7488
- const storage = await this.orchestrator.getStorage(namespace);
7489
- const token = typeof request.since === "string" && request.since.trim().length > 0 ? request.since.trim() : config.briefing.defaultWindow;
7490
- const window = parseBriefingWindow(token);
7491
- if (!window) {
7492
- throw new EngramAccessInputError(`invalid briefing window: ${token}`);
7493
- }
7494
- const rawFocus = typeof request.focus === "string" ? request.focus.trim() : "";
7495
- let focus = null;
7496
- if (rawFocus.length > 0) {
7497
- focus = parseBriefingFocus(rawFocus);
7498
- if (!focus) {
7499
- throw new EngramAccessInputError(
7500
- `invalid briefing focus filter: ${request.focus}`
7501
- );
7502
- }
7503
- }
7504
- const SUPPORTED_FORMATS = ["markdown", "json"];
7505
- if (typeof request.format === "string" && !SUPPORTED_FORMATS.includes(request.format)) {
7506
- throw new EngramAccessInputError(
7507
- `unsupported briefing format: "${request.format}". Accepted: ${SUPPORTED_FORMATS.join(", ")}.`
7508
- );
7683
+ });
7509
7684
  }
7510
- const format = request.format === "json" ? "json" : request.format === "markdown" ? "markdown" : config.briefing.defaultFormat;
7511
- const maxFollowups = typeof request.maxFollowups === "number" && Number.isFinite(request.maxFollowups) ? Math.max(0, Math.min(10, Math.floor(request.maxFollowups))) : config.briefing.maxFollowups;
7512
- const calendarSource = config.briefing.calendarSource ? new FileCalendarSource(config.briefing.calendarSource) : void 0;
7513
- const result = await buildBriefing({
7514
- storage,
7515
- namespace,
7516
- window,
7517
- focus,
7518
- calendarSource,
7519
- maxFollowups,
7520
- allowLlm: config.briefing.llmFollowups,
7521
- openaiApiKey: config.openaiApiKey,
7522
- openaiBaseUrl: config.openaiBaseUrl,
7523
- model: config.model,
7524
- // Without a direct OpenAI key, route follow-ups through the configured
7525
- // LLM chain (gateway model source or local LLM) — same fallback every
7526
- // other LLM feature uses. A configured key keeps its precedence so
7527
- // existing deployments are unchanged.
7528
- followupGenerator: config.openaiApiKey ? void 0 : this.orchestrator.briefingChainFollowupGenerator
7529
- });
7530
- return {
7531
- format,
7532
- window: result.window,
7533
- namespace,
7534
- markdown: result.markdown,
7535
- json: result.json,
7536
- followupsUnavailableReason: result.followupsUnavailableReason
7537
- };
7685
+ return this.orchestrator.generateDaySummary(memories);
7686
+ }
7687
+ async briefing(request) {
7688
+ return this.accessObserveWriteSurface.briefing(
7689
+ request
7690
+ );
7538
7691
  }
7539
7692
  /**
7540
7693
  * Attach a coding context to a session (issue #569). Used by the Claude
@@ -7760,63 +7913,10 @@ var EngramAccessService = class {
7760
7913
  // per-tenant) do not block each other.
7761
7914
  xrayQueue = Promise.resolve();
7762
7915
  async memoryStore(request, hooks) {
7763
- const namespace = await this.resolveCodingScopedWriteNamespace(request);
7764
- const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
7765
- if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
7766
- throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
7767
- }
7768
- const execute = async () => {
7769
- const candidate = this.validateWriteCandidate(request, namespace);
7770
- if (request.dryRun === true) {
7771
- return {
7772
- schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
7773
- operation: "memory_store",
7774
- namespace,
7775
- dryRun: true,
7776
- accepted: true,
7777
- queued: false,
7778
- status: "validated",
7779
- idempotencyKey: request.idempotencyKey?.trim() || void 0
7780
- };
7781
- }
7782
- const result = await persistExplicitCapture(this.orchestrator, candidate, "memory_store");
7783
- await this.attachCodingContextAfterScopedWrite(request);
7784
- const blocked = result.tombstoneBlocked === true;
7785
- const response = {
7786
- schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
7787
- operation: "memory_store",
7788
- namespace,
7789
- dryRun: false,
7790
- accepted: true,
7791
- queued: blocked,
7792
- status: blocked ? "queued_for_review" : result.duplicateOf ? "duplicate" : "stored",
7793
- memoryId: result.id,
7794
- duplicateOf: result.duplicateOf,
7795
- idempotencyKey: request.idempotencyKey?.trim() || void 0
7796
- };
7797
- log.info(
7798
- `access-write op=memory_store namespace=${namespace} dryRun=false status=${response.status} memoryId=${response.memoryId ?? "-"} idempotency=${response.idempotencyKey ? "yes" : "no"}`
7799
- );
7800
- return response;
7801
- };
7802
- return this.handleIdempotentWrite({
7803
- operation: "memory_store",
7804
- idempotencyKey: request.idempotencyKey,
7805
- requestFingerprint: {
7806
- schemaVersion,
7807
- content: request.content,
7808
- category: request.category,
7809
- confidence: request.confidence,
7810
- namespace,
7811
- tags: request.tags,
7812
- entityRef: request.entityRef,
7813
- ttl: request.ttl,
7814
- sourceReason: request.sourceReason
7815
- },
7816
- skip: request.dryRun === true,
7817
- beforeExecute: hooks?.enforceWriteQuota,
7818
- execute
7819
- });
7916
+ return this.accessObserveWriteSurface.memoryStore(
7917
+ request,
7918
+ hooks
7919
+ );
7820
7920
  }
7821
7921
  async peekMemoryStoreIdempotency(request) {
7822
7922
  const namespace = await this.resolveCodingScopedWriteNamespace(request);
@@ -7842,67 +7942,10 @@ var EngramAccessService = class {
7842
7942
  });
7843
7943
  }
7844
7944
  async suggestionSubmit(request, hooks) {
7845
- const namespace = await this.resolveCodingScopedWriteNamespace(request);
7846
- const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
7847
- if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
7848
- throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
7849
- }
7850
- const execute = async () => {
7851
- const candidate = this.validateWriteCandidate(request, namespace);
7852
- if (request.dryRun === true) {
7853
- return {
7854
- schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
7855
- operation: "suggestion_submit",
7856
- namespace,
7857
- dryRun: true,
7858
- accepted: true,
7859
- queued: true,
7860
- status: "validated",
7861
- idempotencyKey: request.idempotencyKey?.trim() || void 0
7862
- };
7863
- }
7864
- const result = await queueExplicitCaptureForReview(
7865
- this.orchestrator,
7866
- candidate,
7867
- "suggestion_submit",
7868
- new Error(request.sourceReason?.trim() || "submitted via engram suggestion_submit")
7869
- );
7870
- await this.attachCodingContextAfterScopedWrite(request);
7871
- const response = {
7872
- schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
7873
- operation: "suggestion_submit",
7874
- namespace,
7875
- dryRun: false,
7876
- accepted: true,
7877
- queued: true,
7878
- status: "queued_for_review",
7879
- memoryId: result.id,
7880
- duplicateOf: result.duplicateOf,
7881
- idempotencyKey: request.idempotencyKey?.trim() || void 0
7882
- };
7883
- log.info(
7884
- `access-write op=suggestion_submit namespace=${namespace} dryRun=false status=${response.status} memoryId=${response.memoryId ?? "-"} idempotency=${response.idempotencyKey ? "yes" : "no"}`
7885
- );
7886
- return response;
7887
- };
7888
- return this.handleIdempotentWrite({
7889
- operation: "suggestion_submit",
7890
- idempotencyKey: request.idempotencyKey,
7891
- requestFingerprint: {
7892
- schemaVersion,
7893
- content: request.content,
7894
- category: request.category,
7895
- confidence: request.confidence,
7896
- namespace,
7897
- tags: request.tags,
7898
- entityRef: request.entityRef,
7899
- ttl: request.ttl,
7900
- sourceReason: request.sourceReason
7901
- },
7902
- skip: request.dryRun === true,
7903
- beforeExecute: hooks?.enforceWriteQuota,
7904
- execute
7905
- });
7945
+ return this.accessObserveWriteSurface.suggestionSubmit(
7946
+ request,
7947
+ hooks
7948
+ );
7906
7949
  }
7907
7950
  async peekSuggestionSubmitIdempotency(request) {
7908
7951
  const namespace = await this.resolveCodingScopedWriteNamespace(request);
@@ -8716,145 +8759,34 @@ var EngramAccessService = class {
8716
8759
  }
8717
8760
  serializeMemorySummary(memory, baseDir, disclosure, rawExcerpts) {
8718
8761
  return shapeMemorySummary(memory, baseDir, disclosure, rawExcerpts);
8719
- }
8720
- async observe(request, hooks) {
8721
- const idempotencyKey = request.idempotencyKey?.trim();
8722
- let effectiveCodingContext;
8723
- if (idempotencyKey && !(typeof request.namespace === "string" && request.namespace.trim().length > 0)) {
8724
- effectiveCodingContext = (typeof this.orchestrator.getCodingContextForSession === "function" ? this.orchestrator.getCodingContextForSession(request.sessionKey) : null) ?? await this.resolveCodingContextFromOptions(request);
8725
- }
8726
- return this.handleIdempotentWrite({
8727
- operation: "observe",
8728
- idempotencyKey: request.idempotencyKey,
8729
- requestFingerprint: {
8730
- sessionKey: request.sessionKey,
8731
- messages: request.messages,
8732
- namespace: request.namespace,
8733
- skipExtraction: request.skipExtraction,
8734
- authenticatedPrincipal: request.authenticatedPrincipal,
8735
- cwd: request.cwd,
8736
- projectTag: request.projectTag,
8737
- effectiveCodingContext: effectiveCodingContext ?? null
8738
- },
8739
- beforeExecute: hooks?.enforceWriteQuota,
8740
- execute: () => this.runObserve(request)
8741
- });
8742
- }
8743
- async runObserve(request) {
8744
- if (!request.sessionKey || typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
8745
- throw new EngramAccessInputError("sessionKey is required and must be a non-empty string");
8746
- }
8747
- if (!Array.isArray(request.messages) || request.messages.length === 0) {
8748
- throw new EngramAccessInputError("messages is required and must be a non-empty array");
8749
- }
8750
- for (const msg of request.messages) {
8751
- if (!msg || typeof msg !== "object" || typeof msg.role !== "string" || typeof msg.content !== "string") {
8752
- throw new EngramAccessInputError("each message must have a string 'role' and 'content'");
8753
- }
8754
- if (msg.role !== "user" && msg.role !== "assistant") {
8755
- throw new EngramAccessInputError(`invalid message role: ${msg.role} (expected 'user' or 'assistant')`);
8756
- }
8757
- }
8758
- const scope = await this.resolveMemoryScopePlan(request);
8759
- const writeNamespace = scope.writeNamespace;
8760
- const namespace = this.legacyResponseNamespaceForScope(scope);
8761
- const shouldWriteObjectiveState = resolveObjectiveStateCapabilities(this.orchestrator.config).objectiveStateMemory === true && resolveObjectiveStateCapabilities(this.orchestrator.config).objectiveStateSnapshotWrites === true;
8762
- await this.maybeAttachCodingContext(request.sessionKey, {
8763
- cwd: request.cwd,
8764
- projectTag: request.projectTag
8765
- });
8766
- const lcmSessionKey = lcmSessionKeyForNamespace(
8767
- writeNamespace,
8768
- request.sessionKey,
8769
- this.orchestrator.config.defaultNamespace
8770
- ) ?? request.sessionKey;
8771
- if (shouldWriteObjectiveState) {
8772
- try {
8773
- const objectiveStateLocation = await this.objectiveStateStoreLocationForNamespace(
8774
- scope.objectiveStateNamespace
8775
- );
8776
- await recordObjectiveStateSnapshotsFromObservedMessages({
8777
- memoryDir: objectiveStateLocation.memoryDir,
8778
- objectiveStateStoreDir: objectiveStateLocation.objectiveStateStoreDir,
8779
- objectiveStateMemoryEnabled: resolveObjectiveStateCapabilities(this.orchestrator.config).objectiveStateMemory,
8780
- objectiveStateSnapshotWritesEnabled: resolveObjectiveStateCapabilities(this.orchestrator.config).objectiveStateSnapshotWrites,
8781
- sessionKey: request.sessionKey,
8782
- recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
8783
- messages: request.messages
8784
- });
8785
- } catch (err) {
8786
- log.error(`access-observe objective-state snapshot write failed: ${err}`);
8787
- }
8788
- }
8789
- let lcmArchived = false;
8790
- if (this.orchestrator.lcmEngine && this.orchestrator.lcmEngine.enabled) {
8791
- try {
8792
- this.orchestrator.lcmEngine.enqueueObserveMessages(lcmSessionKey, request.messages);
8793
- lcmArchived = true;
8794
- } catch (err) {
8795
- log.error(`access-observe LCM enqueue failed: ${err}`);
8796
- }
8797
- }
8798
- let extractionQueued = false;
8799
- if (request.skipExtraction !== true) {
8800
- const turns = request.messages.map((m) => ({
8801
- source: "openclaw",
8802
- // Identity-vs-routing separation (#1505 thread 1, cursor): extraction
8803
- // derives the provenance principal via `resolvePrincipal(turn.sessionKey)`
8804
- // and threads `turn.sessionKey` into conversation threading. Feeding the
8805
- // namespace-PREFIXED `lcmSessionKey` here mis-derived the principal to
8806
- // `default` (a `<ns>:<key>` string matches no prefix/map rule and fails
8807
- // the `agent:` heuristic). Pass the ORIGINAL sessionKey so identity is
8808
- // correct; storage routing is pinned separately via
8809
- // writeNamespaceOverride below, and the authenticated principal is pinned
8810
- // via principalOverride.
8811
- sessionKey: request.sessionKey,
8812
- role: m.role,
8813
- content: m.content,
8814
- parts: m.parts,
8815
- rawContent: m.rawContent,
8816
- sourceFormat: m.sourceFormat,
8817
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
8818
- }));
8819
- const writeNamespaceOverride = resolveNamespaceCapabilities(this.orchestrator.config).namespaces === true ? writeNamespace : void 0;
8820
- const principalOverride = typeof scope.principal === "string" && scope.principal.length > 0 ? scope.principal : void 0;
8821
- try {
8822
- const extractionPromise = this.orchestrator.ingestReplayBatch(turns, {
8823
- archiveLcm: false,
8824
- writeNamespaceOverride,
8825
- principalOverride
8826
- });
8827
- extractionPromise.catch((err) => {
8828
- log.error(`access-observe background extraction failed: ${err}`);
8829
- });
8830
- extractionQueued = true;
8831
- } catch (err) {
8832
- log.error(`access-observe extraction enqueue failed: ${err}`);
8833
- }
8762
+ }
8763
+ async observe(request, hooks) {
8764
+ const idempotencyKey = request.idempotencyKey?.trim();
8765
+ let effectiveCodingContext;
8766
+ if (idempotencyKey && !(typeof request.namespace === "string" && request.namespace.trim().length > 0)) {
8767
+ effectiveCodingContext = (typeof this.orchestrator.getCodingContextForSession === "function" ? this.orchestrator.getCodingContextForSession(request.sessionKey) : null) ?? await this.resolveCodingContextFromOptions(request);
8834
8768
  }
8835
- log.info(
8836
- `access-observe namespace=${namespace} effectiveNamespace=${writeNamespace} sessionKey=${request.sessionKey} messages=${request.messages.length} lcm=${lcmArchived} extraction=${extractionQueued}`
8837
- );
8838
- return {
8839
- accepted: request.messages.length,
8840
- sessionKey: request.sessionKey,
8841
- namespace,
8842
- effectiveNamespace: writeNamespace,
8843
- scopeDebug: {
8844
- principal: scope.principal,
8845
- explicitNamespace: scope.explicitNamespace,
8846
- baseNamespace: scope.baseNamespace,
8847
- writeNamespace: scope.writeNamespace,
8848
- codingOverlayApplied: scope.codingOverlayApplied,
8849
- readNamespaces: scope.readNamespaces,
8850
- scopeProfile: scope.scopeProfile,
8851
- writeLayer: scope.writeLayer,
8852
- layers: scope.layers,
8853
- promotionTargets: scope.promotionTargets
8769
+ return this.handleIdempotentWrite({
8770
+ operation: "observe",
8771
+ idempotencyKey: request.idempotencyKey,
8772
+ requestFingerprint: {
8773
+ sessionKey: request.sessionKey,
8774
+ messages: request.messages,
8775
+ namespace: request.namespace,
8776
+ skipExtraction: request.skipExtraction,
8777
+ authenticatedPrincipal: request.authenticatedPrincipal,
8778
+ cwd: request.cwd,
8779
+ projectTag: request.projectTag,
8780
+ effectiveCodingContext: effectiveCodingContext ?? null
8854
8781
  },
8855
- lcmArchived,
8856
- extractionQueued
8857
- };
8782
+ beforeExecute: hooks?.enforceWriteQuota,
8783
+ execute: () => this.runObserve(request)
8784
+ });
8785
+ }
8786
+ async runObserve(request) {
8787
+ return this.accessObserveWriteSurface.runObserve(
8788
+ request
8789
+ );
8858
8790
  }
8859
8791
  async lcmSearch(request) {
8860
8792
  return this.accessLcmSurface.lcmSearch(
@@ -9251,137 +9183,19 @@ var EngramAccessService = class {
9251
9183
  }
9252
9184
  // ── Work Layer ──────────────────────────────────────────────────────────
9253
9185
  async workTask(request) {
9254
- const STATUSES = /* @__PURE__ */ new Set(["todo", "in_progress", "blocked", "done", "cancelled"]);
9255
- const PRIORITIES = /* @__PURE__ */ new Set(["low", "medium", "high"]);
9256
- const asStatus = (v) => v && STATUSES.has(v) ? v : void 0;
9257
- const asPriority = (v) => v && PRIORITIES.has(v) ? v : void 0;
9258
- const storage = new WorkStorage(this.orchestrator.config.memoryDir);
9259
- await storage.ensureDirectories();
9260
- const action = request.action;
9261
- if (action === "create") {
9262
- if (!request.title?.trim()) throw new EngramAccessInputError("title is required for create");
9263
- const task = await storage.createTask({
9264
- title: request.title,
9265
- description: request.description,
9266
- status: asStatus(request.status),
9267
- priority: asPriority(request.priority),
9268
- owner: request.owner?.trim() || void 0,
9269
- assignee: request.assignee?.trim() || void 0,
9270
- projectId: request.projectId?.trim() || void 0,
9271
- tags: request.tags,
9272
- dueAt: request.dueAt?.trim() || void 0
9273
- });
9274
- return { action, task };
9275
- }
9276
- if (action === "get") {
9277
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for get");
9278
- return { action, task: await storage.getTask(request.id) };
9279
- }
9280
- if (action === "list") {
9281
- const tasks = await storage.listTasks({
9282
- status: asStatus(request.status),
9283
- owner: request.owner?.trim() || void 0,
9284
- assignee: request.assignee?.trim() || void 0,
9285
- projectId: request.projectId?.trim() || void 0
9286
- });
9287
- return { action, count: tasks.length, tasks };
9288
- }
9289
- if (action === "update") {
9290
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for update");
9291
- const patch = {};
9292
- if (request.title !== void 0) patch.title = request.title;
9293
- if (request.description !== void 0) patch.description = request.description;
9294
- const st = asStatus(request.status);
9295
- if (st) patch.status = st;
9296
- const pr = asPriority(request.priority);
9297
- if (pr) patch.priority = pr;
9298
- if (request.owner !== void 0) patch.owner = request.owner || null;
9299
- if (request.assignee !== void 0) patch.assignee = request.assignee || null;
9300
- if (request.projectId !== void 0) patch.projectId = request.projectId || null;
9301
- if (request.tags) patch.tags = request.tags;
9302
- if (request.dueAt !== void 0) patch.dueAt = request.dueAt || null;
9303
- return { action, task: await storage.updateTask(request.id, patch) };
9304
- }
9305
- if (action === "transition") {
9306
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for transition");
9307
- const st = asStatus(request.status);
9308
- if (!st) throw new EngramAccessInputError("valid status is required for transition");
9309
- return { action, task: await storage.transitionTask(request.id, st) };
9310
- }
9311
- if (action === "delete") {
9312
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for delete");
9313
- return { action, deleted: await storage.deleteTask(request.id) };
9314
- }
9315
- throw new EngramAccessInputError(`Unsupported work_task action: ${action}`);
9186
+ return this.accessObserveWriteSurface.workTask(
9187
+ request
9188
+ );
9316
9189
  }
9317
9190
  async workProject(request) {
9318
- const STATUSES = /* @__PURE__ */ new Set(["active", "on_hold", "completed", "archived"]);
9319
- const asStatus = (v) => v && STATUSES.has(v) ? v : void 0;
9320
- const storage = new WorkStorage(this.orchestrator.config.memoryDir);
9321
- await storage.ensureDirectories();
9322
- const action = request.action;
9323
- if (action === "create") {
9324
- if (!request.name?.trim()) throw new EngramAccessInputError("name is required for create");
9325
- const project = await storage.createProject({
9326
- name: request.name,
9327
- description: request.description,
9328
- status: asStatus(request.status),
9329
- owner: request.owner?.trim() || void 0,
9330
- tags: request.tags
9331
- });
9332
- return { action, project };
9333
- }
9334
- if (action === "get") {
9335
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for get");
9336
- return { action, project: await storage.getProject(request.id) };
9337
- }
9338
- if (action === "list") {
9339
- const projects = await storage.listProjects();
9340
- return { action, count: projects.length, projects };
9341
- }
9342
- if (action === "update") {
9343
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for update");
9344
- const patch = {};
9345
- if (request.name !== void 0) patch.name = request.name;
9346
- if (request.description !== void 0) patch.description = request.description;
9347
- const st = asStatus(request.status);
9348
- if (st) patch.status = st;
9349
- if (request.owner !== void 0) patch.owner = request.owner || null;
9350
- if (request.tags) patch.tags = request.tags;
9351
- return { action, project: await storage.updateProject(request.id, patch) };
9352
- }
9353
- if (action === "delete") {
9354
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for delete");
9355
- return { action, deleted: await storage.deleteProject(request.id) };
9356
- }
9357
- if (action === "link_task") {
9358
- if (!request.taskId?.trim() || !request.projectId?.trim()) {
9359
- throw new EngramAccessInputError("taskId and projectId are required for link_task");
9360
- }
9361
- return { action, linked: await storage.linkTaskToProject(request.taskId, request.projectId) };
9362
- }
9363
- throw new EngramAccessInputError(`Unsupported work_project action: ${action}`);
9191
+ return this.accessObserveWriteSurface.workProject(
9192
+ request
9193
+ );
9364
9194
  }
9365
9195
  async workBoard(request) {
9366
- const memoryDir = this.orchestrator.config.memoryDir;
9367
- await new WorkStorage(memoryDir).ensureDirectories();
9368
- const action = request.action;
9369
- const projectId = request.projectId?.trim() || void 0;
9370
- if (action === "export_markdown") {
9371
- const markdown = await exportWorkBoardMarkdown({ memoryDir, projectId });
9372
- return { action, markdown: wrapWorkLayerContext(markdown, { linkToMemory: request.linkToMemory === true }) };
9373
- }
9374
- if (action === "export_snapshot") {
9375
- const snapshot = await exportWorkBoardSnapshot({ memoryDir, projectId });
9376
- return { action, snapshot };
9377
- }
9378
- if (action === "import_snapshot") {
9379
- if (!request.snapshotJson?.trim()) throw new EngramAccessInputError("snapshotJson is required for import_snapshot");
9380
- const snapshot = JSON.parse(request.snapshotJson);
9381
- const result = await importWorkBoardSnapshot({ memoryDir, snapshot, projectId });
9382
- return { action, result };
9383
- }
9384
- throw new EngramAccessInputError(`Unsupported work_board action: ${action}`);
9196
+ return this.accessObserveWriteSurface.workBoard(
9197
+ request
9198
+ );
9385
9199
  }
9386
9200
  // ── Shared Context / Compounding ────────────────────────────────────────
9387
9201
  async sharedContextWriteOutput(request) {
@@ -9665,90 +9479,35 @@ ${next}`);
9665
9479
  */
9666
9480
  async memoryOutcome(request) {
9667
9481
  if (request.memoryId.includes("/") || request.memoryId.includes("\\")) {
9668
- throw new EngramAccessInputError(
9669
- "memoryId must not contain path separators"
9670
- );
9671
- }
9672
- const resolvedNs = this.writableNamespaceFor(
9673
- request.namespace,
9674
- request.sessionKey,
9675
- request.principal
9676
- );
9677
- const storage = await this.orchestrator.getStorage(resolvedNs);
9678
- return recordMemoryOutcome(storage, {
9679
- memoryPath: `${request.memoryId}.md`,
9680
- outcome: request.outcome,
9681
- timestamp: request.timestamp
9682
- });
9683
- }
9684
- async memoryPromote(request) {
9685
- const resolvedNs = this.writableNamespaceFor(request.namespace, request.sessionKey, request.principal);
9686
- const storage = await this.orchestrator.getStorage(resolvedNs);
9687
- await storage.updateMemoryFrontmatter(request.memoryId, {
9688
- lifecycleState: "active",
9689
- updated: (/* @__PURE__ */ new Date()).toISOString()
9690
- });
9691
- return { promoted: true, memoryId: request.memoryId };
9692
- }
9693
- async memoryActionApply(request) {
9694
- const actionTypes = /* @__PURE__ */ new Set([
9695
- "store_episode",
9696
- "store_note",
9697
- "update_note",
9698
- "create_artifact",
9699
- "summarize_node",
9700
- "discard",
9701
- "link_graph"
9702
- ]);
9703
- if (!actionTypes.has(request.action)) {
9704
- throw new EngramAccessInputError(
9705
- `memory_action_apply: invalid action ${JSON.stringify(request.action)}`
9706
- );
9707
- }
9708
- if (resolveCompressionCapabilities(this.orchestrator.config).contextCompressionActions !== true) {
9709
- throw new EngramAccessInputError(
9710
- "memory_action_apply is disabled; enable contextCompressionActionsEnabled to use this tool"
9711
- );
9712
- }
9713
- const outcome = request.outcome ?? "skipped";
9714
- if (outcome !== "applied" && outcome !== "skipped" && outcome !== "failed") {
9715
- throw new EngramAccessInputError(
9716
- `memory_action_apply: outcome must be "applied", "skipped", or "failed"; got ${JSON.stringify(outcome)}`
9717
- );
9718
- }
9719
- const resolvedNs = this.writableNamespaceFor(
9720
- request.namespace,
9721
- request.sessionKey,
9722
- request.principal
9723
- );
9724
- const inputSummaryParts = [
9725
- request.content,
9726
- request.category ? `category=${request.category}` : void 0,
9727
- request.linkTargetId ? `linkTargetId=${request.linkTargetId}` : void 0,
9728
- request.linkType ? `linkType=${request.linkType}` : void 0,
9729
- typeof request.linkStrength === "number" ? `linkStrength=${request.linkStrength}` : void 0,
9730
- request.artifactType ? `artifactType=${request.artifactType}` : void 0,
9731
- typeof request.execute === "boolean" ? `execute=${request.execute}` : void 0
9732
- ].filter((part) => typeof part === "string" && part.length > 0);
9733
- const event = {
9734
- action: request.action,
9735
- outcome,
9736
- namespace: resolvedNs,
9737
- actor: "access.memory_action_apply",
9738
- subsystem: "access.memory_action_apply",
9739
- reason: request.reason,
9740
- memoryId: request.memoryId,
9741
- sourceSessionKey: request.sessionKey,
9742
- inputSummary: inputSummaryParts.length > 0 ? inputSummaryParts.join(" | ").slice(0, 500) : void 0,
9743
- dryRun: request.dryRun === true,
9744
- promptHash: typeof request.sourcePrompt === "string" && request.sourcePrompt.length > 0 ? createHash5("sha256").update(request.sourcePrompt).digest("hex") : void 0
9745
- };
9746
- const preview = this.orchestrator.previewMemoryActionEvent(event);
9747
- if (request.dryRun === true) {
9748
- return { recorded: false, dryRun: true, event: preview };
9482
+ throw new EngramAccessInputError(
9483
+ "memoryId must not contain path separators"
9484
+ );
9749
9485
  }
9750
- const recorded = await this.orchestrator.appendMemoryActionEvent(event);
9751
- return { recorded, event: preview };
9486
+ const resolvedNs = this.writableNamespaceFor(
9487
+ request.namespace,
9488
+ request.sessionKey,
9489
+ request.principal
9490
+ );
9491
+ const storage = await this.orchestrator.getStorage(resolvedNs);
9492
+ return recordMemoryOutcome(storage, {
9493
+ memoryPath: `${request.memoryId}.md`,
9494
+ outcome: request.outcome,
9495
+ timestamp: request.timestamp
9496
+ });
9497
+ }
9498
+ async memoryPromote(request) {
9499
+ const resolvedNs = this.writableNamespaceFor(request.namespace, request.sessionKey, request.principal);
9500
+ const storage = await this.orchestrator.getStorage(resolvedNs);
9501
+ await storage.updateMemoryFrontmatter(request.memoryId, {
9502
+ lifecycleState: "active",
9503
+ updated: (/* @__PURE__ */ new Date()).toISOString()
9504
+ });
9505
+ return { promoted: true, memoryId: request.memoryId };
9506
+ }
9507
+ async memoryActionApply(request) {
9508
+ return this.accessObserveWriteSurface.memoryActionApply(
9509
+ request
9510
+ );
9752
9511
  }
9753
9512
  async contextCheckpoint(request) {
9754
9513
  const resolvedNs = this.writableNamespaceFor(request.namespace, request.sessionKey, request.principal);
@@ -10365,397 +10124,699 @@ ${next}`);
10365
10124
  limit: request.limit
10366
10125
  });
10367
10126
  }
10368
- async wearablesTranscriptMemories(request) {
10369
- return this.orchestrator.getWearablesService().transcriptMemories({
10370
- source: request.source,
10371
- date: request.date,
10372
- limit: request.limit
10127
+ async wearablesTranscriptMemories(request) {
10128
+ return this.orchestrator.getWearablesService().transcriptMemories({
10129
+ source: request.source,
10130
+ date: request.date,
10131
+ limit: request.limit
10132
+ });
10133
+ }
10134
+ // ── Admin console surfaces (issue #1502) ────────────────────────────────
10135
+ //
10136
+ // Thin delegators. Every method threads `this.orchestrator` references
10137
+ // into the pure `admin/admin-surfaces.ts` functions so the dashboard
10138
+ // never re-resolves scope, re-derives promotion targets, or re-lists
10139
+ // namespaces.
10140
+ /**
10141
+ * Resolve an effective scope inspection for the console. Inputs are
10142
+ * redacted before return so an operator-supplied principal preview /
10143
+ * namespace override never echoes a pasted credential back.
10144
+ */
10145
+ async adminInspectScope(options) {
10146
+ const config = this.orchestrator.config;
10147
+ const codingContext = options.sessionKey ? this.orchestrator.getCodingContextForSession(options.sessionKey) ?? null : null;
10148
+ const inspection = inspectScope({
10149
+ config,
10150
+ sessionKey: options.sessionKey,
10151
+ namespace: options.namespace,
10152
+ principalOverride: options.principalOverride,
10153
+ codingContext,
10154
+ operation: options.operation
10155
+ });
10156
+ return redactSensitive(inspection);
10157
+ }
10158
+ /**
10159
+ * List configured + discovered namespaces from the catalog with the
10160
+ * admin filters the console exposes.
10161
+ */
10162
+ async adminListNamespaces(filter) {
10163
+ const result = await listAdminNamespaces({
10164
+ catalog: this.orchestrator.namespaceCatalog,
10165
+ filter
10166
+ });
10167
+ return redactSensitive(result);
10168
+ }
10169
+ /**
10170
+ * Per-namespace maintenance + QMD health. QMD diagnostics are probed via
10171
+ * the orchestrator's search health accessor (one probe per namespace).
10172
+ */
10173
+ async adminMaintenanceHealth() {
10174
+ const report = await gatherMaintenanceHealth({
10175
+ catalog: this.orchestrator.namespaceCatalog,
10176
+ qmdHealthProvider: async (namespace) => {
10177
+ try {
10178
+ const health = await this.orchestrator.searchHealthForNamespace(namespace);
10179
+ return {
10180
+ namespace,
10181
+ collection: health.collection,
10182
+ available: health.available,
10183
+ collectionState: health.collectionState,
10184
+ debugStatus: health.debugStatus,
10185
+ installedVersion: health.installedVersion,
10186
+ supportedVersion: health.supportedVersion,
10187
+ supported: health.supported,
10188
+ upgradeAvailable: health.upgradeAvailable,
10189
+ daemonMode: health.daemonMode
10190
+ };
10191
+ } catch (err) {
10192
+ throw err;
10193
+ }
10194
+ }
10195
+ });
10196
+ return redactSensitive(report);
10197
+ }
10198
+ /**
10199
+ * Dry-run transcript/session audit. Detects mixed `other/default` data
10200
+ * and legacy stranded directories. Never applies a migration.
10201
+ */
10202
+ async adminTranscriptAudit() {
10203
+ const result = await auditTranscripts(this.orchestrator.config.memoryDir);
10204
+ return redactSensitive(result);
10205
+ }
10206
+ async adminPromoteMemory(request) {
10207
+ return this.accessAdminOpsSurface.adminPromoteMemory(
10208
+ request
10209
+ );
10210
+ }
10211
+ };
10212
+
10213
+ // src/access-observe-write-surface.ts
10214
+ var AccessObserveWriteSurface = class {
10215
+ constructor(deps) {
10216
+ this.deps = deps;
10217
+ }
10218
+ deps;
10219
+ async qmdHealth(searchBackend, qmdEnabled, namespace, collection) {
10220
+ if (searchBackend !== "qmd" || !qmdEnabled) {
10221
+ return {
10222
+ enabled: qmdEnabled,
10223
+ active: false,
10224
+ degraded: false,
10225
+ mode: searchBackend !== "qmd" ? "not-selected" : "disabled",
10226
+ collection,
10227
+ collectionState: "skipped",
10228
+ installedVersion: null,
10229
+ supportedVersion: null,
10230
+ supported: null,
10231
+ upgradeAvailable: null,
10232
+ doctorAvailable: null,
10233
+ debugStatus: searchBackend !== "qmd" ? `backend=${searchBackend}` : "backend=disabled"
10234
+ };
10235
+ }
10236
+ if (resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces === true) {
10237
+ const namespaceHealth = await this.deps.namespaceQmdHealth(searchBackend, qmdEnabled, namespace, collection);
10238
+ if (namespaceHealth) return namespaceHealth;
10239
+ }
10240
+ const qmd = this.deps.orchestrator.qmd;
10241
+ if (!qmd) {
10242
+ return {
10243
+ enabled: true,
10244
+ active: false,
10245
+ degraded: true,
10246
+ mode: "fallback",
10247
+ collection,
10248
+ collectionState: "unknown",
10249
+ installedVersion: null,
10250
+ supportedVersion: null,
10251
+ supported: null,
10252
+ upgradeAvailable: null,
10253
+ doctorAvailable: null,
10254
+ debugStatus: "backend=unavailable"
10255
+ };
10256
+ }
10257
+ const diagnosticAvailable = await this.deps.qmdProbeAvailable(searchBackend, qmdEnabled);
10258
+ const operationalAvailable = diagnosticAvailable || qmd.isAvailable();
10259
+ const collectionState = diagnosticAvailable ? await this.deps.qmdCollectionState(searchBackend, qmdEnabled, collection) : "unknown";
10260
+ const active = operationalAvailable && collectionState !== "missing";
10261
+ const degraded = searchBackend === "qmd" && qmdEnabled && (!active || !diagnosticAvailable || collectionState === "unknown");
10262
+ const debugStatus = qmd.debugStatus();
10263
+ const versionStatus = "getVersionStatus" in qmd && typeof qmd.getVersionStatus === "function" ? qmd.getVersionStatus() : null;
10264
+ const daemonMode = "isDaemonMode" in qmd && typeof qmd.isDaemonMode === "function" ? qmd.isDaemonMode() === true : false;
10265
+ const mode = searchBackend !== "qmd" ? "not-selected" : !qmdEnabled ? "disabled" : !active ? "fallback" : daemonMode ? "daemon" : "cli";
10266
+ return {
10267
+ enabled: qmdEnabled,
10268
+ active,
10269
+ degraded,
10270
+ mode,
10271
+ collection,
10272
+ collectionState,
10273
+ installedVersion: versionStatus?.installedVersion ?? null,
10274
+ supportedVersion: versionStatus?.supportedVersion ?? null,
10275
+ supported: versionStatus?.supported ?? null,
10276
+ upgradeAvailable: versionStatus?.upgradeAvailable ?? null,
10277
+ doctorAvailable: versionStatus?.capabilities?.doctor ?? null,
10278
+ debugStatus
10279
+ };
10280
+ }
10281
+ async namespaceQmdHealth(searchBackend, qmdEnabled, namespace, fallbackCollection) {
10282
+ if (searchBackend !== "qmd" || !qmdEnabled) return null;
10283
+ const searchHealthForNamespace = this.deps.orchestrator.searchHealthForNamespace;
10284
+ if (typeof searchHealthForNamespace !== "function") return null;
10285
+ const controller = new AbortController();
10286
+ const timer = setTimeout(() => controller.abort(), 2e3);
10287
+ timer.unref?.();
10288
+ try {
10289
+ const health = await searchHealthForNamespace.call(this.deps.orchestrator, namespace, {
10290
+ signal: controller.signal
10291
+ });
10292
+ const active = health.available && health.collectionState !== "missing";
10293
+ const degraded = !active || health.collectionState === "unknown";
10294
+ const mode = !active ? "fallback" : health.daemonMode === true ? "daemon" : "cli";
10295
+ return {
10296
+ enabled: true,
10297
+ active,
10298
+ degraded,
10299
+ mode,
10300
+ collection: health.collection || fallbackCollection,
10301
+ collectionState: health.collectionState,
10302
+ installedVersion: health.installedVersion,
10303
+ supportedVersion: health.supportedVersion,
10304
+ supported: health.supported,
10305
+ upgradeAvailable: health.upgradeAvailable,
10306
+ doctorAvailable: health.doctorAvailable,
10307
+ debugStatus: health.debugStatus
10308
+ };
10309
+ } catch (error) {
10310
+ const detail = displayErrorDetail(error) || "unknown";
10311
+ return {
10312
+ enabled: true,
10313
+ active: false,
10314
+ degraded: true,
10315
+ mode: "fallback",
10316
+ collection: fallbackCollection,
10317
+ collectionState: "unknown",
10318
+ installedVersion: null,
10319
+ supportedVersion: null,
10320
+ supported: null,
10321
+ upgradeAvailable: null,
10322
+ doctorAvailable: null,
10323
+ debugStatus: `backend=namespace-unavailable error=${detail}`
10324
+ };
10325
+ } finally {
10326
+ clearTimeout(timer);
10327
+ }
10328
+ }
10329
+ /**
10330
+ * Build a daily context briefing. Gracefully degrades when the OpenAI key
10331
+ * or Responses API is unavailable — never throws for LLM-related problems.
10332
+ */
10333
+ async briefing(request) {
10334
+ const config = this.deps.orchestrator.config;
10335
+ if (!config.briefing.enabled) {
10336
+ throw new EngramAccessInputError("briefing is disabled");
10337
+ }
10338
+ const namespace = this.deps.resolveReadableNamespace(request.namespace, request.principal);
10339
+ const storage = await this.deps.orchestrator.getStorage(namespace);
10340
+ const token = typeof request.since === "string" && request.since.trim().length > 0 ? request.since.trim() : config.briefing.defaultWindow;
10341
+ const window = parseBriefingWindow(token);
10342
+ if (!window) {
10343
+ throw new EngramAccessInputError(`invalid briefing window: ${token}`);
10344
+ }
10345
+ const rawFocus = typeof request.focus === "string" ? request.focus.trim() : "";
10346
+ let focus = null;
10347
+ if (rawFocus.length > 0) {
10348
+ focus = parseBriefingFocus(rawFocus);
10349
+ if (!focus) {
10350
+ throw new EngramAccessInputError(
10351
+ `invalid briefing focus filter: ${request.focus}`
10352
+ );
10353
+ }
10354
+ }
10355
+ const SUPPORTED_FORMATS = ["markdown", "json"];
10356
+ if (typeof request.format === "string" && !SUPPORTED_FORMATS.includes(request.format)) {
10357
+ throw new EngramAccessInputError(
10358
+ `unsupported briefing format: "${request.format}". Accepted: ${SUPPORTED_FORMATS.join(", ")}.`
10359
+ );
10360
+ }
10361
+ const format = request.format === "json" ? "json" : request.format === "markdown" ? "markdown" : config.briefing.defaultFormat;
10362
+ const maxFollowups = typeof request.maxFollowups === "number" && Number.isFinite(request.maxFollowups) ? Math.max(0, Math.min(10, Math.floor(request.maxFollowups))) : config.briefing.maxFollowups;
10363
+ const calendarSource = config.briefing.calendarSource ? new FileCalendarSource(config.briefing.calendarSource) : void 0;
10364
+ const result = await buildBriefing({
10365
+ storage,
10366
+ namespace,
10367
+ window,
10368
+ focus,
10369
+ calendarSource,
10370
+ maxFollowups,
10371
+ allowLlm: config.briefing.llmFollowups,
10372
+ openaiApiKey: config.openaiApiKey,
10373
+ openaiBaseUrl: config.openaiBaseUrl,
10374
+ model: config.model,
10375
+ // Without a direct OpenAI key, route follow-ups through the configured
10376
+ // LLM chain (gateway model source or local LLM) — same fallback every
10377
+ // other LLM feature uses. A configured key keeps its precedence so
10378
+ // existing deployments are unchanged.
10379
+ followupGenerator: config.openaiApiKey ? void 0 : this.deps.orchestrator.briefingChainFollowupGenerator
10380
+ });
10381
+ return {
10382
+ format,
10383
+ window: result.window,
10384
+ namespace,
10385
+ markdown: result.markdown,
10386
+ json: result.json,
10387
+ followupsUnavailableReason: result.followupsUnavailableReason
10388
+ };
10389
+ }
10390
+ async memoryStore(request, hooks) {
10391
+ const namespace = await this.deps.resolveCodingScopedWriteNamespace(request);
10392
+ const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
10393
+ if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
10394
+ throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
10395
+ }
10396
+ const execute = async () => {
10397
+ const candidate = this.deps.validateWriteCandidate(request, namespace);
10398
+ if (request.dryRun === true) {
10399
+ return {
10400
+ schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
10401
+ operation: "memory_store",
10402
+ namespace,
10403
+ dryRun: true,
10404
+ accepted: true,
10405
+ queued: false,
10406
+ status: "validated",
10407
+ idempotencyKey: request.idempotencyKey?.trim() || void 0
10408
+ };
10409
+ }
10410
+ const result = await persistExplicitCapture(this.deps.orchestrator, candidate, "memory_store");
10411
+ await this.deps.attachCodingContextAfterScopedWrite(request);
10412
+ const blocked = result.tombstoneBlocked === true;
10413
+ const response = {
10414
+ schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
10415
+ operation: "memory_store",
10416
+ namespace,
10417
+ dryRun: false,
10418
+ accepted: true,
10419
+ queued: blocked,
10420
+ status: blocked ? "queued_for_review" : result.duplicateOf ? "duplicate" : "stored",
10421
+ memoryId: result.id,
10422
+ duplicateOf: result.duplicateOf,
10423
+ idempotencyKey: request.idempotencyKey?.trim() || void 0
10424
+ };
10425
+ log.info(
10426
+ `access-write op=memory_store namespace=${namespace} dryRun=false status=${response.status} memoryId=${response.memoryId ?? "-"} idempotency=${response.idempotencyKey ? "yes" : "no"}`
10427
+ );
10428
+ return response;
10429
+ };
10430
+ return this.deps.handleIdempotentWrite({
10431
+ operation: "memory_store",
10432
+ idempotencyKey: request.idempotencyKey,
10433
+ requestFingerprint: {
10434
+ schemaVersion,
10435
+ content: request.content,
10436
+ category: request.category,
10437
+ confidence: request.confidence,
10438
+ namespace,
10439
+ tags: request.tags,
10440
+ entityRef: request.entityRef,
10441
+ ttl: request.ttl,
10442
+ sourceReason: request.sourceReason
10443
+ },
10444
+ skip: request.dryRun === true,
10445
+ beforeExecute: hooks?.enforceWriteQuota,
10446
+ execute
10373
10447
  });
10374
10448
  }
10375
- // ── Admin console surfaces (issue #1502) ────────────────────────────────
10376
- //
10377
- // Thin delegators. Every method threads `this.orchestrator` references
10378
- // into the pure `admin/admin-surfaces.ts` functions so the dashboard
10379
- // never re-resolves scope, re-derives promotion targets, or re-lists
10380
- // namespaces.
10381
- /**
10382
- * Resolve an effective scope inspection for the console. Inputs are
10383
- * redacted before return so an operator-supplied principal preview /
10384
- * namespace override never echoes a pasted credential back.
10385
- */
10386
- async adminInspectScope(options) {
10387
- const config = this.orchestrator.config;
10388
- const codingContext = options.sessionKey ? this.orchestrator.getCodingContextForSession(options.sessionKey) ?? null : null;
10389
- const inspection = inspectScope({
10390
- config,
10391
- sessionKey: options.sessionKey,
10392
- namespace: options.namespace,
10393
- principalOverride: options.principalOverride,
10394
- codingContext,
10395
- operation: options.operation
10449
+ async suggestionSubmit(request, hooks) {
10450
+ const namespace = await this.deps.resolveCodingScopedWriteNamespace(request);
10451
+ const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
10452
+ if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
10453
+ throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
10454
+ }
10455
+ const execute = async () => {
10456
+ const candidate = this.deps.validateWriteCandidate(request, namespace);
10457
+ if (request.dryRun === true) {
10458
+ return {
10459
+ schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
10460
+ operation: "suggestion_submit",
10461
+ namespace,
10462
+ dryRun: true,
10463
+ accepted: true,
10464
+ queued: true,
10465
+ status: "validated",
10466
+ idempotencyKey: request.idempotencyKey?.trim() || void 0
10467
+ };
10468
+ }
10469
+ const result = await queueExplicitCaptureForReview(
10470
+ this.deps.orchestrator,
10471
+ candidate,
10472
+ "suggestion_submit",
10473
+ new Error(request.sourceReason?.trim() || "submitted via engram suggestion_submit")
10474
+ );
10475
+ await this.deps.attachCodingContextAfterScopedWrite(request);
10476
+ const response = {
10477
+ schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
10478
+ operation: "suggestion_submit",
10479
+ namespace,
10480
+ dryRun: false,
10481
+ accepted: true,
10482
+ queued: true,
10483
+ status: "queued_for_review",
10484
+ memoryId: result.id,
10485
+ duplicateOf: result.duplicateOf,
10486
+ idempotencyKey: request.idempotencyKey?.trim() || void 0
10487
+ };
10488
+ log.info(
10489
+ `access-write op=suggestion_submit namespace=${namespace} dryRun=false status=${response.status} memoryId=${response.memoryId ?? "-"} idempotency=${response.idempotencyKey ? "yes" : "no"}`
10490
+ );
10491
+ return response;
10492
+ };
10493
+ return this.deps.handleIdempotentWrite({
10494
+ operation: "suggestion_submit",
10495
+ idempotencyKey: request.idempotencyKey,
10496
+ requestFingerprint: {
10497
+ schemaVersion,
10498
+ content: request.content,
10499
+ category: request.category,
10500
+ confidence: request.confidence,
10501
+ namespace,
10502
+ tags: request.tags,
10503
+ entityRef: request.entityRef,
10504
+ ttl: request.ttl,
10505
+ sourceReason: request.sourceReason
10506
+ },
10507
+ skip: request.dryRun === true,
10508
+ beforeExecute: hooks?.enforceWriteQuota,
10509
+ execute
10396
10510
  });
10397
- return redactSensitive(inspection);
10398
10511
  }
10399
- /**
10400
- * List configured + discovered namespaces from the catalog with the
10401
- * admin filters the console exposes.
10402
- */
10403
- async adminListNamespaces(filter) {
10404
- const result = await listAdminNamespaces({
10405
- catalog: this.orchestrator.namespaceCatalog,
10406
- filter
10512
+ async runObserve(request) {
10513
+ if (!request.sessionKey || typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
10514
+ throw new EngramAccessInputError("sessionKey is required and must be a non-empty string");
10515
+ }
10516
+ if (!Array.isArray(request.messages) || request.messages.length === 0) {
10517
+ throw new EngramAccessInputError("messages is required and must be a non-empty array");
10518
+ }
10519
+ for (const msg of request.messages) {
10520
+ if (!msg || typeof msg !== "object" || typeof msg.role !== "string" || typeof msg.content !== "string") {
10521
+ throw new EngramAccessInputError("each message must have a string 'role' and 'content'");
10522
+ }
10523
+ if (msg.role !== "user" && msg.role !== "assistant") {
10524
+ throw new EngramAccessInputError(`invalid message role: ${msg.role} (expected 'user' or 'assistant')`);
10525
+ }
10526
+ }
10527
+ const scope = await this.deps.resolveMemoryScopePlan(request);
10528
+ const writeNamespace = scope.writeNamespace;
10529
+ const namespace = this.deps.legacyResponseNamespaceForScope(scope);
10530
+ const shouldWriteObjectiveState = resolveObjectiveStateCapabilities(this.deps.orchestrator.config).objectiveStateMemory === true && resolveObjectiveStateCapabilities(this.deps.orchestrator.config).objectiveStateSnapshotWrites === true;
10531
+ await this.deps.maybeAttachCodingContext(request.sessionKey, {
10532
+ cwd: request.cwd,
10533
+ projectTag: request.projectTag
10407
10534
  });
10408
- return redactSensitive(result);
10409
- }
10410
- /**
10411
- * Per-namespace maintenance + QMD health. QMD diagnostics are probed via
10412
- * the orchestrator's search health accessor (one probe per namespace).
10413
- */
10414
- async adminMaintenanceHealth() {
10415
- const report = await gatherMaintenanceHealth({
10416
- catalog: this.orchestrator.namespaceCatalog,
10417
- qmdHealthProvider: async (namespace) => {
10418
- try {
10419
- const health = await this.orchestrator.searchHealthForNamespace(namespace);
10420
- return {
10421
- namespace,
10422
- collection: health.collection,
10423
- available: health.available,
10424
- collectionState: health.collectionState,
10425
- debugStatus: health.debugStatus,
10426
- installedVersion: health.installedVersion,
10427
- supportedVersion: health.supportedVersion,
10428
- supported: health.supported,
10429
- upgradeAvailable: health.upgradeAvailable,
10430
- daemonMode: health.daemonMode
10431
- };
10432
- } catch (err) {
10433
- throw err;
10434
- }
10535
+ const lcmSessionKey = lcmSessionKeyForNamespace(
10536
+ writeNamespace,
10537
+ request.sessionKey,
10538
+ this.deps.orchestrator.config.defaultNamespace
10539
+ ) ?? request.sessionKey;
10540
+ if (shouldWriteObjectiveState) {
10541
+ try {
10542
+ const objectiveStateLocation = await this.deps.objectiveStateStoreLocationForNamespace(
10543
+ scope.objectiveStateNamespace
10544
+ );
10545
+ await recordObjectiveStateSnapshotsFromObservedMessages({
10546
+ memoryDir: objectiveStateLocation.memoryDir,
10547
+ objectiveStateStoreDir: objectiveStateLocation.objectiveStateStoreDir,
10548
+ objectiveStateMemoryEnabled: resolveObjectiveStateCapabilities(this.deps.orchestrator.config).objectiveStateMemory,
10549
+ objectiveStateSnapshotWritesEnabled: resolveObjectiveStateCapabilities(this.deps.orchestrator.config).objectiveStateSnapshotWrites,
10550
+ sessionKey: request.sessionKey,
10551
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
10552
+ messages: request.messages
10553
+ });
10554
+ } catch (err) {
10555
+ log.error(`access-observe objective-state snapshot write failed: ${err}`);
10556
+ }
10557
+ }
10558
+ let lcmArchived = false;
10559
+ if (this.deps.orchestrator.lcmEngine && this.deps.orchestrator.lcmEngine.enabled) {
10560
+ try {
10561
+ this.deps.orchestrator.lcmEngine.enqueueObserveMessages(lcmSessionKey, request.messages);
10562
+ lcmArchived = true;
10563
+ } catch (err) {
10564
+ log.error(`access-observe LCM enqueue failed: ${err}`);
10565
+ }
10566
+ }
10567
+ let extractionQueued = false;
10568
+ if (request.skipExtraction !== true) {
10569
+ const turns = request.messages.map((m) => ({
10570
+ source: "openclaw",
10571
+ // Identity-vs-routing separation (#1505 thread 1, cursor): extraction
10572
+ // derives the provenance principal via `resolvePrincipal(turn.sessionKey)`
10573
+ // and threads `turn.sessionKey` into conversation threading. Feeding the
10574
+ // namespace-PREFIXED `lcmSessionKey` here mis-derived the principal to
10575
+ // `default` (a `<ns>:<key>` string matches no prefix/map rule and fails
10576
+ // the `agent:` heuristic). Pass the ORIGINAL sessionKey so identity is
10577
+ // correct; storage routing is pinned separately via
10578
+ // writeNamespaceOverride below, and the authenticated principal is pinned
10579
+ // via principalOverride.
10580
+ sessionKey: request.sessionKey,
10581
+ role: m.role,
10582
+ content: m.content,
10583
+ parts: m.parts,
10584
+ rawContent: m.rawContent,
10585
+ sourceFormat: m.sourceFormat,
10586
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10587
+ }));
10588
+ const writeNamespaceOverride = resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces === true ? writeNamespace : void 0;
10589
+ const principalOverride = typeof scope.principal === "string" && scope.principal.length > 0 ? scope.principal : void 0;
10590
+ try {
10591
+ const extractionPromise = this.deps.orchestrator.ingestReplayBatch(turns, {
10592
+ archiveLcm: false,
10593
+ writeNamespaceOverride,
10594
+ principalOverride
10595
+ });
10596
+ extractionPromise.catch((err) => {
10597
+ log.error(`access-observe background extraction failed: ${err}`);
10598
+ });
10599
+ extractionQueued = true;
10600
+ } catch (err) {
10601
+ log.error(`access-observe extraction enqueue failed: ${err}`);
10435
10602
  }
10436
- });
10437
- return redactSensitive(report);
10438
- }
10439
- /**
10440
- * Dry-run transcript/session audit. Detects mixed `other/default` data
10441
- * and legacy stranded directories. Never applies a migration.
10442
- */
10443
- async adminTranscriptAudit() {
10444
- const result = await auditTranscripts(this.orchestrator.config.memoryDir);
10445
- return redactSensitive(result);
10446
- }
10447
- async adminPromoteMemory(request) {
10448
- return this.accessAdminOpsSurface.adminPromoteMemory(
10449
- request
10603
+ }
10604
+ log.info(
10605
+ `access-observe namespace=${namespace} effectiveNamespace=${writeNamespace} sessionKey=${request.sessionKey} messages=${request.messages.length} lcm=${lcmArchived} extraction=${extractionQueued}`
10450
10606
  );
10607
+ return {
10608
+ accepted: request.messages.length,
10609
+ sessionKey: request.sessionKey,
10610
+ namespace,
10611
+ effectiveNamespace: writeNamespace,
10612
+ scopeDebug: {
10613
+ principal: scope.principal,
10614
+ explicitNamespace: scope.explicitNamespace,
10615
+ baseNamespace: scope.baseNamespace,
10616
+ writeNamespace: scope.writeNamespace,
10617
+ codingOverlayApplied: scope.codingOverlayApplied,
10618
+ readNamespaces: scope.readNamespaces,
10619
+ scopeProfile: scope.scopeProfile,
10620
+ writeLayer: scope.writeLayer,
10621
+ layers: scope.layers,
10622
+ promotionTargets: scope.promotionTargets
10623
+ },
10624
+ lcmArchived,
10625
+ extractionQueued
10626
+ };
10451
10627
  }
10452
- };
10453
-
10454
- // src/access-lcm-surface.ts
10455
- var AccessLcmSurface = class {
10456
- constructor(deps) {
10457
- this.deps = deps;
10458
- }
10459
- deps;
10460
- async lcmSearch(request) {
10461
- if (!request.query || typeof request.query !== "string" || request.query.trim().length === 0) {
10462
- throw new EngramAccessInputError("query is required and must be a non-empty string");
10628
+ async workTask(request) {
10629
+ const STATUSES = /* @__PURE__ */ new Set(["todo", "in_progress", "blocked", "done", "cancelled"]);
10630
+ const PRIORITIES = /* @__PURE__ */ new Set(["low", "medium", "high"]);
10631
+ const asStatus = (v) => v && STATUSES.has(v) ? v : void 0;
10632
+ const asPriority = (v) => v && PRIORITIES.has(v) ? v : void 0;
10633
+ const storage = new WorkStorage(this.deps.orchestrator.config.memoryDir);
10634
+ await storage.ensureDirectories();
10635
+ const action = request.action;
10636
+ if (action === "create") {
10637
+ if (!request.title?.trim()) throw new EngramAccessInputError("title is required for create");
10638
+ const task = await storage.createTask({
10639
+ title: request.title,
10640
+ description: request.description,
10641
+ status: asStatus(request.status),
10642
+ priority: asPriority(request.priority),
10643
+ owner: request.owner?.trim() || void 0,
10644
+ assignee: request.assignee?.trim() || void 0,
10645
+ projectId: request.projectId?.trim() || void 0,
10646
+ tags: request.tags,
10647
+ dueAt: request.dueAt?.trim() || void 0
10648
+ });
10649
+ return { action, task };
10463
10650
  }
10464
- const principal = this.deps.resolveRequestPrincipal(request.sessionKey, request.authenticatedPrincipal);
10465
- const hasExplicitNamespace = typeof request.namespace === "string" && request.namespace.trim().length > 0;
10466
- const profileLcmReadNamespaces = hasExplicitNamespace ? null : this.deps.resolveScopeProfileLcmReadNamespaces(
10467
- request.sessionKey,
10468
- request.authenticatedPrincipal
10469
- );
10470
- const namespace = hasExplicitNamespace ? this.deps.resolveReadableNamespace(request.namespace, principal) : profileLcmReadNamespaces !== null ? profileLcmReadNamespaces[0] : this.deps.resolveImplicitLcmReadFallbackNamespace(principal);
10471
- if (!this.deps.orchestrator.lcmEngine || !this.deps.orchestrator.lcmEngine.enabled) {
10472
- return {
10473
- query: request.query,
10474
- namespace: namespace ?? this.deps.orchestrator.config.defaultNamespace,
10475
- results: [],
10476
- count: 0,
10477
- lcmEnabled: false
10478
- };
10651
+ if (action === "get") {
10652
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for get");
10653
+ return { action, task: await storage.getTask(request.id) };
10479
10654
  }
10480
- if (profileLcmReadNamespaces !== null && profileLcmReadNamespaces.length === 0) {
10481
- return {
10482
- query: request.query,
10483
- namespace: this.deps.orchestrator.config.defaultNamespace,
10484
- results: [],
10485
- count: 0,
10486
- lcmEnabled: true
10487
- };
10655
+ if (action === "list") {
10656
+ const tasks = await storage.listTasks({
10657
+ status: asStatus(request.status),
10658
+ owner: request.owner?.trim() || void 0,
10659
+ assignee: request.assignee?.trim() || void 0,
10660
+ projectId: request.projectId?.trim() || void 0
10661
+ });
10662
+ return { action, count: tasks.length, tasks };
10488
10663
  }
10489
- if (namespace === void 0) {
10490
- return {
10491
- query: request.query,
10492
- namespace: this.deps.orchestrator.config.defaultNamespace,
10493
- results: [],
10494
- count: 0,
10495
- lcmEnabled: true
10496
- };
10664
+ if (action === "update") {
10665
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for update");
10666
+ const patch = {};
10667
+ if (request.title !== void 0) patch.title = request.title;
10668
+ if (request.description !== void 0) patch.description = request.description;
10669
+ const st = asStatus(request.status);
10670
+ if (st) patch.status = st;
10671
+ const pr = asPriority(request.priority);
10672
+ if (pr) patch.priority = pr;
10673
+ if (request.owner !== void 0) patch.owner = request.owner || null;
10674
+ if (request.assignee !== void 0) patch.assignee = request.assignee || null;
10675
+ if (request.projectId !== void 0) patch.projectId = request.projectId || null;
10676
+ if (request.tags) patch.tags = request.tags;
10677
+ if (request.dueAt !== void 0) patch.dueAt = request.dueAt || null;
10678
+ return { action, task: await storage.updateTask(request.id, patch) };
10497
10679
  }
10498
- const limit = Math.max(1, Math.min(request.limit ?? 10, 100));
10499
- const lcmReadNamespace = profileLcmReadNamespaces !== null ? profileLcmReadNamespaces[0] ?? this.deps.orchestrator.config.defaultNamespace : this.deps.resolveLcmReadNamespace(
10500
- request.namespace,
10501
- namespace,
10502
- request.sessionKey,
10503
- request.authenticatedPrincipal
10504
- );
10505
- const lcmSessionKeyIds = request.sessionKey ? profileLcmReadNamespaces !== null ? this.deps.lcmSessionIdsForNamespaces(
10506
- profileLcmReadNamespaces,
10507
- request.sessionKey
10508
- ) : this.deps.resolveLcmReadSessionIds(
10509
- request.namespace,
10510
- namespace,
10511
- request.sessionKey,
10512
- request.authenticatedPrincipal
10513
- ) : [void 0];
10514
- const lcmSessionPrefixes = request.sessionPrefix ? profileLcmReadNamespaces !== null && !request.sessionKey ? this.deps.lcmSessionIdsForNamespaces(
10515
- profileLcmReadNamespaces,
10516
- request.sessionPrefix
10517
- ) : [
10518
- lcmSessionKeyForNamespace(
10519
- lcmReadNamespace,
10520
- request.sessionPrefix,
10521
- this.deps.orchestrator.config.defaultNamespace
10522
- ) ?? request.sessionPrefix
10523
- ] : [void 0];
10524
- const hasScopedSession = typeof request.sessionKey === "string" && request.sessionKey.length > 0 || lcmSessionPrefixes.some((prefix) => typeof prefix === "string" && prefix.length > 0);
10525
- if (!hasScopedSession && resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces === true) {
10526
- return {
10527
- query: request.query,
10528
- namespace,
10529
- results: [],
10530
- count: 0,
10531
- lcmEnabled: true
10532
- };
10680
+ if (action === "transition") {
10681
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for transition");
10682
+ const st = asStatus(request.status);
10683
+ if (!st) throw new EngramAccessInputError("valid status is required for transition");
10684
+ return { action, task: await storage.transitionTask(request.id, st) };
10533
10685
  }
10534
- const seenRows = /* @__PURE__ */ new Set();
10535
- const results = [];
10536
- const lcmSearches = [];
10537
- for (const lcmSessionKey of lcmSessionKeyIds) {
10538
- for (const lcmSessionPrefix of lcmSessionPrefixes) {
10539
- lcmSearches.push({
10540
- key: lcmSessionKey,
10541
- prefix: lcmSessionPrefix,
10542
- promise: this.deps.orchestrator.lcmEngine.searchContextFull(
10543
- request.query,
10544
- limit,
10545
- lcmSessionKey,
10546
- lcmSessionPrefix
10547
- )
10548
- });
10549
- }
10686
+ if (action === "delete") {
10687
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for delete");
10688
+ return { action, deleted: await storage.deleteTask(request.id) };
10550
10689
  }
10551
- const settledSearches = await Promise.allSettled(
10552
- lcmSearches.map((search) => search.promise)
10553
- );
10554
- for (let i = 0; i < settledSearches.length; i += 1) {
10555
- if (results.length >= limit) break;
10556
- const settled = settledSearches[i];
10557
- if (!settled || settled.status === "rejected") {
10558
- const failed = lcmSearches[i];
10559
- log.warn(
10560
- `lcmSearch: failed for key=${failed?.key ?? "<none>"} prefix=${failed?.prefix ?? "<none>"}: ${settled?.status === "rejected" ? settled.reason : "missing result"}`
10561
- );
10562
- continue;
10563
- }
10564
- for (const r of settled.value) {
10565
- const dedupeKey = `${r.session_id}\0${r.turn_index}`;
10566
- if (seenRows.has(dedupeKey)) continue;
10567
- seenRows.add(dedupeKey);
10568
- results.push({
10569
- sessionId: r.session_id,
10570
- content: r.content,
10571
- turnIndex: r.turn_index
10572
- });
10573
- if (results.length >= limit) break;
10690
+ throw new EngramAccessInputError(`Unsupported work_task action: ${action}`);
10691
+ }
10692
+ async workProject(request) {
10693
+ const STATUSES = /* @__PURE__ */ new Set(["active", "on_hold", "completed", "archived"]);
10694
+ const asStatus = (v) => v && STATUSES.has(v) ? v : void 0;
10695
+ const storage = new WorkStorage(this.deps.orchestrator.config.memoryDir);
10696
+ await storage.ensureDirectories();
10697
+ const action = request.action;
10698
+ if (action === "create") {
10699
+ if (!request.name?.trim()) throw new EngramAccessInputError("name is required for create");
10700
+ const project = await storage.createProject({
10701
+ name: request.name,
10702
+ description: request.description,
10703
+ status: asStatus(request.status),
10704
+ owner: request.owner?.trim() || void 0,
10705
+ tags: request.tags
10706
+ });
10707
+ return { action, project };
10708
+ }
10709
+ if (action === "get") {
10710
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for get");
10711
+ return { action, project: await storage.getProject(request.id) };
10712
+ }
10713
+ if (action === "list") {
10714
+ const projects = await storage.listProjects();
10715
+ return { action, count: projects.length, projects };
10716
+ }
10717
+ if (action === "update") {
10718
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for update");
10719
+ const patch = {};
10720
+ if (request.name !== void 0) patch.name = request.name;
10721
+ if (request.description !== void 0) patch.description = request.description;
10722
+ const st = asStatus(request.status);
10723
+ if (st) patch.status = st;
10724
+ if (request.owner !== void 0) patch.owner = request.owner || null;
10725
+ if (request.tags) patch.tags = request.tags;
10726
+ return { action, project: await storage.updateProject(request.id, patch) };
10727
+ }
10728
+ if (action === "delete") {
10729
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for delete");
10730
+ return { action, deleted: await storage.deleteProject(request.id) };
10731
+ }
10732
+ if (action === "link_task") {
10733
+ if (!request.taskId?.trim() || !request.projectId?.trim()) {
10734
+ throw new EngramAccessInputError("taskId and projectId are required for link_task");
10574
10735
  }
10736
+ return { action, linked: await storage.linkTaskToProject(request.taskId, request.projectId) };
10575
10737
  }
10576
- return {
10577
- query: request.query,
10578
- namespace,
10579
- results,
10580
- count: results.length,
10581
- lcmEnabled: true
10582
- };
10738
+ throw new EngramAccessInputError(`Unsupported work_project action: ${action}`);
10583
10739
  }
10584
- /**
10585
- * Resolve the effective LCM NAMESPACE a same-session operation must prefix
10586
- * with (the namespace half of {@link resolveLcmReadSessionKey}). Split out so
10587
- * `lcmSearch` can apply ONE namespace to BOTH its `sessionKey` and its
10588
- * `sessionPrefix` the prefix is a search fragment, not a real session, so its
10589
- * own coding context can't be looked up; it must inherit the namespace resolved
10590
- * from the real session (`sessionKeyForOverlay`).
10591
- *
10592
- * `purpose` selects the AUTHORIZATION gate applied before honouring the
10593
- * coding overlay (#1505 round 3 + round 4, codex P2):
10594
- *
10595
- * - `"read"` (`lcmSearch` / raw-excerpt recall): the overlay rows are only
10596
- * visible when the principal SELF base is in the READABLE RECALL SET — the
10597
- * same gate the orchestrator's `lcmReadNamespaceForSession` and the recall
10598
- * namespace set use (`recallNamespacesForPrincipal`, gated by both
10599
- * `defaultRecallNamespaces.includes("self")` AND `canReadNamespace`). A
10600
- * caller that passed the default read check must NOT receive
10601
- * `<principal>-project-*` rows the policy never granted (cross-tenant read
10602
- * leak). When the self base is not readable, keep the just-authorized
10603
- * namespace (collapses to the raw key on the default store).
10604
- *
10605
- * - `"write"` (`lcmCompactionFlush` / `lcmCompactionRecord`): these are
10606
- * write/maintenance operations on the SAME queue `observe` just wrote, so
10607
- * the gate must mirror observe's WRITE authorization (`canWriteNamespace`
10608
- * on the self base), NOT readability. A principal that can WRITE but not
10609
- * READ its self namespace (or whose `defaultRecallNamespaces` omits `self`)
10610
- * archived under the overlay key via `observe`; gating compaction by
10611
- * readability would fall back to the default/raw key and leave that queue
10612
- * never flushed/recorded (round-4 codex P2). Write-authorized ⇒ overlay
10613
- * key, matching the observe write key (rule 42 read/write parity; rule 39
10614
- * identical gates across paths).
10615
- */
10616
- resolveLcmReadNamespace(explicitNamespace, resolvedNamespace, sessionKeyForOverlay, authenticatedPrincipal, purpose = "read") {
10617
- const hasExplicitNamespace = typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
10618
- if (hasExplicitNamespace) return resolvedNamespace;
10619
- const principal = this.deps.resolveRequestPrincipal(
10620
- sessionKeyForOverlay,
10621
- authenticatedPrincipal
10622
- );
10623
- const base = defaultNamespaceForPrincipal(principal, this.deps.orchestrator.config);
10624
- const overlaid = this.deps.orchestrator.applyCodingNamespaceOverlay(
10625
- sessionKeyForOverlay,
10626
- base
10627
- );
10628
- if (overlaid === base) return this.deps.orchestrator.config.defaultNamespace;
10629
- const authorized = purpose === "write" ? canWriteNamespace(principal, base, this.deps.orchestrator.config) : recallNamespacesForPrincipal(
10630
- principal,
10631
- this.deps.orchestrator.config
10632
- ).includes(base);
10633
- if (authorized) return overlaid;
10634
- if (purpose === "read") return this.deps.orchestrator.config.defaultNamespace;
10635
- return resolvedNamespace;
10740
+ async workBoard(request) {
10741
+ const memoryDir = this.deps.orchestrator.config.memoryDir;
10742
+ await new WorkStorage(memoryDir).ensureDirectories();
10743
+ const action = request.action;
10744
+ const projectId = request.projectId?.trim() || void 0;
10745
+ if (action === "export_markdown") {
10746
+ const markdown = await exportWorkBoardMarkdown({ memoryDir, projectId });
10747
+ return { action, markdown: wrapWorkLayerContext(markdown, { linkToMemory: request.linkToMemory === true }) };
10748
+ }
10749
+ if (action === "export_snapshot") {
10750
+ const snapshot = await exportWorkBoardSnapshot({ memoryDir, projectId });
10751
+ return { action, snapshot };
10752
+ }
10753
+ if (action === "import_snapshot") {
10754
+ if (!request.snapshotJson?.trim()) throw new EngramAccessInputError("snapshotJson is required for import_snapshot");
10755
+ const snapshot = JSON.parse(request.snapshotJson);
10756
+ const result = await importWorkBoardSnapshot({ memoryDir, snapshot, projectId });
10757
+ return { action, result };
10758
+ }
10759
+ throw new EngramAccessInputError(`Unsupported work_board action: ${action}`);
10636
10760
  }
10637
- /**
10638
- * Resolve the namespace the raw-disclosure excerpt lookup
10639
- * ({@link fetchRawExcerpts}) must prefix its LCM `session_id` with (#1505
10640
- * thread 2f7). Raw disclosure reads the SAME LCM archive `lcmSearch` and the
10641
- * in-prompt LCM sections read, so it MUST pass through the identical
10642
- * read-authorization gate — NOT `snapshot.namespace`, which records the
10643
- * effective WRITE/overlay namespace (`<principal>-project-*`) even when the
10644
- * principal can WRITE but not READ its self base (or `defaultRecallNamespaces`
10645
- * omits `self`). Routing through `resolveLcmReadNamespace(..., "read")` makes
10646
- * raw disclosure fall back to the default store exactly like normal recall +
10647
- * `lcmSearch`, so it never attaches overlay transcript rows the read gate
10648
- * excludes (cross-tenant read leak). Collapses to the default store / raw
10649
- * sessionKey for single-store / no-overlay / explicit-default flows, so
10650
- * single-user recall is byte-for-byte unchanged.
10651
- *
10652
- * Returns `undefined` when NO readable LCM namespace exists for an IMPLICIT
10653
- * (no explicit `namespace`) raw recall — i.e. a restrictive `default` READ
10654
- * policy denies the principal `default` AND no overlay/self namespace is
10655
- * readable. In that case the caller emits NO excerpts rather than throwing
10656
- * `namespace is not readable: default` (#1505 thread NBHWz): normal recall
10657
- * still succeeds via `recallNamespacesForPrincipal`, so `disclosure: "raw"`
10658
- * must degrade gracefully (empty excerpts), never pre-authorize `default`.
10659
- *
10660
- * IMPLICIT-namespace fallback selection derives from the ALREADY
10661
- * read-authorized recall namespace set (`recallNamespacesForPrincipal` +
10662
- * `canReadNamespace`) — the principal's self base when it is in the readable
10663
- * recall set, else `config.defaultNamespace` ONLY when the principal may read
10664
- * it. It NEVER pre-authorizes `default`. An EXPLICIT `namespace` is still
10665
- * authorized strictly via `resolveReadableNamespace` (explicit reads must pass
10666
- * the ACL — no behavior change).
10667
- */
10668
- resolveRawExcerptReadNamespace(explicitNamespace, sessionKey, authenticatedPrincipal) {
10669
- const principal = this.deps.resolveRequestPrincipal(
10670
- sessionKey,
10671
- authenticatedPrincipal
10672
- );
10673
- const hasExplicitNamespace = typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
10674
- if (hasExplicitNamespace) {
10675
- const resolvedNamespace = this.deps.resolveReadableNamespace(
10676
- explicitNamespace,
10677
- principal
10761
+ async memoryActionApply(request) {
10762
+ const actionTypes = /* @__PURE__ */ new Set([
10763
+ "store_episode",
10764
+ "store_note",
10765
+ "update_note",
10766
+ "create_artifact",
10767
+ "summarize_node",
10768
+ "discard",
10769
+ "link_graph"
10770
+ ]);
10771
+ if (!actionTypes.has(request.action)) {
10772
+ throw new EngramAccessInputError(
10773
+ `memory_action_apply: invalid action ${JSON.stringify(request.action)}`
10678
10774
  );
10679
- return this.deps.resolveLcmReadNamespace(
10680
- explicitNamespace,
10681
- resolvedNamespace,
10682
- sessionKey,
10683
- authenticatedPrincipal,
10684
- "read"
10775
+ }
10776
+ if (resolveCompressionCapabilities(this.deps.orchestrator.config).contextCompressionActions !== true) {
10777
+ throw new EngramAccessInputError(
10778
+ "memory_action_apply is disabled; enable contextCompressionActionsEnabled to use this tool"
10685
10779
  );
10686
10780
  }
10687
- const profileReadNamespaces = this.deps.resolveScopeProfileLcmReadNamespaces(
10688
- sessionKey,
10689
- authenticatedPrincipal
10690
- );
10691
- if (profileReadNamespaces !== null) return profileReadNamespaces[0];
10692
- const fallbackNamespace = this.deps.resolveImplicitLcmReadFallbackNamespace(principal);
10693
- if (fallbackNamespace === void 0) return void 0;
10694
- return this.deps.resolveLcmReadNamespace(
10695
- explicitNamespace,
10696
- fallbackNamespace,
10697
- sessionKey,
10698
- authenticatedPrincipal,
10699
- "read"
10700
- );
10701
- }
10702
- resolveLcmReadSessionIds(explicitNamespace, resolvedNamespace, sessionKey, authenticatedPrincipal) {
10703
- const primary = this.deps.resolveLcmReadSessionKey(
10704
- explicitNamespace,
10705
- resolvedNamespace,
10706
- sessionKey,
10707
- authenticatedPrincipal,
10708
- "read"
10709
- );
10710
- const hasExplicitNamespace = typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
10711
- if (hasExplicitNamespace) return [primary];
10712
- const profileReadNamespaces = this.deps.resolveScopeProfileLcmReadNamespaces(
10713
- sessionKey,
10714
- authenticatedPrincipal
10715
- );
10716
- if (profileReadNamespaces !== null) {
10717
- return this.deps.lcmSessionIdsForNamespaces(profileReadNamespaces, sessionKey);
10781
+ const outcome = request.outcome ?? "skipped";
10782
+ if (outcome !== "applied" && outcome !== "skipped" && outcome !== "failed") {
10783
+ throw new EngramAccessInputError(
10784
+ `memory_action_apply: outcome must be "applied", "skipped", or "failed"; got ${JSON.stringify(outcome)}`
10785
+ );
10718
10786
  }
10719
- const principal = this.deps.resolveRequestPrincipal(
10720
- sessionKey,
10721
- authenticatedPrincipal
10722
- );
10723
- const base = defaultNamespaceForPrincipal(
10724
- principal,
10725
- this.deps.orchestrator.config
10726
- );
10727
- const overlaid = this.deps.orchestrator.applyCodingNamespaceOverlay(
10728
- sessionKey,
10729
- base
10730
- );
10731
- if (overlaid === base) return [primary];
10732
- const selfReadableInRecall = recallNamespacesForPrincipal(
10733
- principal,
10734
- this.deps.orchestrator.config
10735
- ).includes(base);
10736
- if (!selfReadableInRecall) return [primary];
10737
- const overlay = resolveCodingNamespaceOverlay(
10738
- this.deps.orchestrator.getCodingContextForSession(sessionKey),
10739
- this.deps.orchestrator.config.codingMode,
10740
- this.deps.orchestrator.config.defaultNamespace
10741
- );
10742
- const fallbackNamespaces = (overlay?.readFallbacks ?? []).map(
10743
- (fallback) => combineNamespaces(base, fallback)
10787
+ const resolvedNs = this.deps.writableNamespaceFor(
10788
+ request.namespace,
10789
+ request.sessionKey,
10790
+ request.principal
10744
10791
  );
10745
- const out = [primary];
10746
- const seen = /* @__PURE__ */ new Set([primary]);
10747
- for (const ns of fallbackNamespaces) {
10748
- const key = lcmSessionKeyForNamespace(
10749
- ns,
10750
- sessionKey,
10751
- this.deps.orchestrator.config.defaultNamespace
10752
- ) ?? sessionKey;
10753
- if (!seen.has(key)) {
10754
- seen.add(key);
10755
- out.push(key);
10756
- }
10792
+ const inputSummaryParts = [
10793
+ request.content,
10794
+ request.category ? `category=${request.category}` : void 0,
10795
+ request.linkTargetId ? `linkTargetId=${request.linkTargetId}` : void 0,
10796
+ request.linkType ? `linkType=${request.linkType}` : void 0,
10797
+ typeof request.linkStrength === "number" ? `linkStrength=${request.linkStrength}` : void 0,
10798
+ request.artifactType ? `artifactType=${request.artifactType}` : void 0,
10799
+ typeof request.execute === "boolean" ? `execute=${request.execute}` : void 0
10800
+ ].filter((part) => typeof part === "string" && part.length > 0);
10801
+ const event = {
10802
+ action: request.action,
10803
+ outcome,
10804
+ namespace: resolvedNs,
10805
+ actor: "access.memory_action_apply",
10806
+ subsystem: "access.memory_action_apply",
10807
+ reason: request.reason,
10808
+ memoryId: request.memoryId,
10809
+ sourceSessionKey: request.sessionKey,
10810
+ inputSummary: inputSummaryParts.length > 0 ? inputSummaryParts.join(" | ").slice(0, 500) : void 0,
10811
+ dryRun: request.dryRun === true,
10812
+ promptHash: typeof request.sourcePrompt === "string" && request.sourcePrompt.length > 0 ? createHash6("sha256").update(request.sourcePrompt).digest("hex") : void 0
10813
+ };
10814
+ const preview = this.deps.orchestrator.previewMemoryActionEvent(event);
10815
+ if (request.dryRun === true) {
10816
+ return { recorded: false, dryRun: true, event: preview };
10757
10817
  }
10758
- return out;
10818
+ const recorded = await this.deps.orchestrator.appendMemoryActionEvent(event);
10819
+ return { recorded, event: preview };
10759
10820
  }
10760
10821
  };
10761
10822
 
@@ -10770,11 +10831,11 @@ export {
10770
10831
  clusterByKey,
10771
10832
  computeProcedureStats,
10772
10833
  formatProcedureStatsText,
10834
+ AccessObserveWriteSurface,
10773
10835
  AccessLcmSurface,
10774
10836
  qmdCollectionPathParts,
10775
10837
  QmdResultResolver,
10776
10838
  AccessRecallSurface,
10777
- selfDeps,
10778
10839
  EngramAccessInputError,
10779
10840
  ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
10780
10841
  buildProjectedGovernanceProposedActions,
@@ -10783,4 +10844,4 @@ export {
10783
10844
  EngramAccessService,
10784
10845
  AccessAdminOpsSurface
10785
10846
  };
10786
- //# sourceMappingURL=chunk-L4AS7TIG.js.map
10847
+ //# sourceMappingURL=chunk-DWDDQLSZ.js.map