@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
package/src/storage.ts CHANGED
@@ -4,6 +4,9 @@ import { createHash } from "node:crypto"
4
4
  import { normalizeContent, computeContentHash } from "./content-hash.js";;
5
5
  import path from "node:path";
6
6
  import { log } from "./logger.js";
7
+ import { MemoryReadStore } from "./storage/memory-read-store.js";
8
+ import { selfDeps } from "./orchestration/self-deps.js";
9
+ import { EntityStore } from "./storage/entity-store.js";
7
10
  import { isErrnoCode } from "./utils/errno.js";
8
11
  import { RECALL_FALLBACK_DIRS, getCategoryDir, categoryDirName } from "./utils/category-dir.js";
9
12
  import { assertPathInsideRoot } from "./utils/path-containment.js";
@@ -519,7 +522,7 @@ function parseLinkReasonValue(rawValue: string): string {
519
522
  }
520
523
  }
521
524
 
522
- function parseFrontmatterStringValue(rawValue: string | undefined): string | undefined {
525
+ export function parseFrontmatterStringValue(rawValue: string | undefined): string | undefined {
523
526
  if (rawValue === undefined) return undefined;
524
527
  const trimmed = rawValue.trim();
525
528
  if (trimmed.length === 0) return undefined;
@@ -565,7 +568,7 @@ function parseReinforcementCountField(raw: string | undefined): number | undefin
565
568
  return n;
566
569
  }
567
570
 
568
- function parseFrontmatter(
571
+ export function parseFrontmatter(
569
572
  raw: string,
570
573
  ): { frontmatter: MemoryFrontmatter; content: string } | null {
571
574
  const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
@@ -952,7 +955,7 @@ function inferEntityTypeFromFilename(pathRel: string): string | undefined {
952
955
  return KNOWN_ENTITY_FILENAME_PREFIXES.has(candidate) ? candidate : undefined;
953
956
  }
954
957
 
955
- function normalizeFrontmatterForPath(frontmatter: MemoryFrontmatter, pathRel: string, content: string = ""): MemoryFrontmatter {
958
+ export function normalizeFrontmatterForPath(frontmatter: MemoryFrontmatter, pathRel: string, content: string = ""): MemoryFrontmatter {
956
959
  const normalizedPath = pathRel.split(path.sep).join("/");
957
960
  let normalizedFrontmatter = frontmatter;
958
961
 
@@ -1651,7 +1654,7 @@ function normalizeEntitySectionFact(value: string): string {
1651
1654
  return value.replace(/\s+/g, " ").trim();
1652
1655
  }
1653
1656
 
1654
- function normalizeStructuredSectionFacts(facts: string[]): string[] {
1657
+ export function normalizeStructuredSectionFacts(facts: string[]): string[] {
1655
1658
  return [...new Set(
1656
1659
  facts
1657
1660
  .map((fact) => normalizeEntitySectionFact(fact))
@@ -1671,7 +1674,7 @@ function collectStructuredSectionFacts(structuredSections: EntityStructuredSecti
1671
1674
  return [...new Set(facts)];
1672
1675
  }
1673
1676
 
1674
- function compileEntityFacts(
1677
+ export function compileEntityFacts(
1675
1678
  timeline: EntityTimelineEntry[],
1676
1679
  structuredSections: EntityStructuredSection[],
1677
1680
  ): string[] {
@@ -1811,7 +1814,7 @@ export function compareEntityTimestamps(left?: string, right?: string): number {
1811
1814
  return leftValue.localeCompare(rightValue);
1812
1815
  }
1813
1816
 
1814
- function countEntityStructuredFacts(entity: EntityFile): number {
1817
+ export function countEntityStructuredFacts(entity: EntityFile): number {
1815
1818
  return (entity.structuredSections ?? []).reduce((count, section) => count + section.facts.length, 0);
1816
1819
  }
1817
1820
 
@@ -2189,7 +2192,7 @@ export function serializeEntityFile(
2189
2192
  return lines.join("\n");
2190
2193
  }
2191
2194
 
2192
- function buildEntitySchemaCacheKey(entitySchemas?: PluginConfig["entitySchemas"]): string {
2195
+ export function buildEntitySchemaCacheKey(entitySchemas?: PluginConfig["entitySchemas"]): string {
2193
2196
  if (!entitySchemas) return "";
2194
2197
  const normalized = Object.entries(entitySchemas)
2195
2198
  .sort(([left], [right]) => left.localeCompare(right))
@@ -2220,7 +2223,7 @@ function buildEntitySchemaCacheKey(entitySchemas?: PluginConfig["entitySchemas"]
2220
2223
  * downstream report requires at read time so the limit semantics and
2221
2224
  * the distribution semantics stay consistent.
2222
2225
  */
2223
- function isValidBufferSurpriseEvent(value: unknown): value is BufferSurpriseEvent {
2226
+ export function isValidBufferSurpriseEvent(value: unknown): value is BufferSurpriseEvent {
2224
2227
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2225
2228
  const v = value as Record<string, unknown>;
2226
2229
  if (v.event !== "BUFFER_SURPRISE") return false;
@@ -2259,9 +2262,11 @@ export interface MemoryWriteResult {
2259
2262
 
2260
2263
  export class StorageManager {
2261
2264
  private knowledgeIndexCache: { result: string; builtAt: number } | null = null;
2262
- private static readonly KNOWLEDGE_INDEX_CACHE_TTL_MS = 600_000; // 10 minutes (entity mutations invalidate)
2263
2265
  private artifactIndexCache: { memories: MemoryFile[]; loadedAtMs: number; writeVersion: number } | null = null;
2264
- private static readonly ARTIFACT_INDEX_CACHE_TTL_MS = 60_000; // 1 minute
2266
+ /** Read by storage/entity-store.ts (decomposition), hence not `private`. */
2267
+ static readonly KNOWLEDGE_INDEX_CACHE_TTL_MS = 600_000; // 10 minutes (entity mutations invalidate)
2268
+ /** Read by storage/memory-read-store.ts (decomposition). */
2269
+ static readonly ARTIFACT_INDEX_CACHE_TTL_MS = 60_000; // 1 minute
2265
2270
  private static readonly artifactWriteVersionByDir = new Map<string, number>();
2266
2271
  private static readonly memoryStatusVersionByDir = new Map<string, number>();
2267
2272
  private static readonly secureStoreEntityCacheKeyIds = new WeakMap<Buffer, number>();
@@ -2290,14 +2295,18 @@ export class StorageManager {
2290
2295
  // on every cold-tier write, making the cache correct across process boundaries
2291
2296
  // (gateway + CLI). After Finding UTsP broadened the scan to the entire cold/
2292
2297
  // subtree, amortizing across back-to-back writes is even more important.
2293
- private static readonly COLD_SCAN_CACHE_TTL_MS = 30_000; // 30 seconds
2294
- private static readonly coldMemoriesCache = new Map<string, { memories: MemoryFile[]; loadedAt: number; coldVersion: number }>();
2298
+ /** Read by storage/memory-read-store.ts (decomposition). */
2299
+ static readonly COLD_SCAN_CACHE_TTL_MS = 30_000; // 30 seconds
2300
+ /** Read by storage/memory-read-store.ts (decomposition). */
2301
+ static readonly coldMemoriesCache = new Map<string, { memories: MemoryFile[]; loadedAt: number; coldVersion: number }>();
2295
2302
 
2296
2303
  // Cache for readQuestions() — avoids serially re-reading tens of thousands of
2297
2304
  // question files on every recall. 60-second TTL is intentionally short so that
2298
2305
  // newly written questions surface quickly.
2299
- private static readonly QUESTIONS_CACHE_TTL_MS = 60_000; // 1 minute
2300
- private static readonly questionsCache = new Map<
2306
+ /** Read by storage/memory-read-store.ts (decomposition). */
2307
+ static readonly QUESTIONS_CACHE_TTL_MS = 60_000; // 1 minute
2308
+ /** Read by storage/memory-read-store.ts (decomposition). */
2309
+ static readonly questionsCache = new Map<
2301
2310
  string,
2302
2311
  {
2303
2312
  questions: Array<{
@@ -2513,7 +2522,44 @@ export class StorageManager {
2513
2522
  this.loadAliasesSync();
2514
2523
  }
2515
2524
 
2525
+ /**
2526
+ * The LIVE class object of this instance (issue #1809 review of the
2527
+ * storage decomposition): extracted store modules must read/write the
2528
+ * shared static caches (questionsCache, coldMemoriesCache, ...) through
2529
+ * the SAME class binding the host methods use. Referencing the imported
2530
+ * `StorageManager` symbol from a split bundle chunk can yield a second
2531
+ * class copy with its own statics, split-braining the caches
2532
+ * (resolveQuestion clears copy A while readQuestions reads copy B).
2533
+ */
2534
+ get storageManagerClass(): typeof StorageManager {
2535
+ return this.constructor as typeof StorageManager;
2536
+ }
2537
+
2516
2538
  /** The root directory of this storage instance. */
2539
+ /** MemoryReadStore (storage.ts decomposition). Lazy; selfDeps live wiring. */
2540
+ private _memoryReadStore: MemoryReadStore | undefined;
2541
+
2542
+ private get memoryReadStore(): MemoryReadStore {
2543
+ if (!this._memoryReadStore) {
2544
+ this._memoryReadStore = new MemoryReadStore(
2545
+ selfDeps<ConstructorParameters<typeof MemoryReadStore>[0]>(this),
2546
+ );
2547
+ }
2548
+ return this._memoryReadStore;
2549
+ }
2550
+
2551
+ /** EntityStore (storage.ts decomposition). Lazy; selfDeps live wiring. */
2552
+ private _entityStore: EntityStore | undefined;
2553
+
2554
+ private get entityStore(): EntityStore {
2555
+ if (!this._entityStore) {
2556
+ this._entityStore = new EntityStore(
2557
+ selfDeps<ConstructorParameters<typeof EntityStore>[0]>(this),
2558
+ );
2559
+ }
2560
+ return this._entityStore;
2561
+ }
2562
+
2517
2563
  get dir(): string {
2518
2564
  return this.baseDir;
2519
2565
  }
@@ -2645,52 +2691,12 @@ export class StorageManager {
2645
2691
  }
2646
2692
  }
2647
2693
 
2648
- /**
2649
- * List stored transcript days, newest first, optionally scoped to one
2650
- * source. Non-transcript files in the tree are ignored.
2651
- */
2652
2694
  async listWearableTranscriptDays(
2653
2695
  sourceId?: string,
2654
2696
  ): Promise<Array<{ source: string; date: string }>> {
2655
- const days: Array<{ source: string; date: string }> = [];
2656
- let sources: string[];
2657
- if (sourceId !== undefined) {
2658
- sources = [sourceId];
2659
- } else {
2660
- try {
2661
- const entries = await readdir(this.wearablesDir, { withFileTypes: true });
2662
- sources = entries
2663
- .filter((entry) => entry.isDirectory())
2664
- .map((entry) => entry.name);
2665
- } catch (err) {
2666
- if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
2667
- throw err;
2668
- }
2669
- }
2670
- for (const source of sources) {
2671
- if (!/^[a-z][a-z0-9-]{0,63}$/.test(source)) continue;
2672
- let entries: string[];
2673
- try {
2674
- entries = await readdir(path.join(this.wearablesDir, source));
2675
- } catch (err) {
2676
- if ((err as NodeJS.ErrnoException).code === "ENOENT") continue;
2677
- throw err;
2678
- }
2679
- for (const entry of entries) {
2680
- if (!entry.endsWith(".md")) continue;
2681
- const date = entry.slice(0, -3);
2682
- if (!isValidTranscriptDate(date)) continue;
2683
- days.push({ source, date });
2684
- }
2685
- }
2686
- days.sort((a, b) => {
2687
- if (a.date > b.date) return -1;
2688
- if (a.date < b.date) return 1;
2689
- if (a.source < b.source) return -1;
2690
- if (a.source > b.source) return 1;
2691
- return 0;
2692
- });
2693
- return days;
2697
+ return this.memoryReadStore.listWearableTranscriptDays(
2698
+ sourceId,
2699
+ );
2694
2700
  }
2695
2701
 
2696
2702
  /**
@@ -3476,9 +3482,11 @@ export class StorageManager {
3476
3482
  */
3477
3483
  private userAliases: Record<string, string> = {};
3478
3484
 
3479
- /** Normalize an entity name using this store's alias table. */
3480
3485
  normalizeEntityName(raw: string, type: string): string {
3481
- return normalizeEntityName(raw, type, this.userAliases);
3486
+ return this.entityStore.normalizeEntityName(
3487
+ raw,
3488
+ type,
3489
+ );
3482
3490
  }
3483
3491
 
3484
3492
  /**
@@ -4027,55 +4035,8 @@ export class StorageManager {
4027
4035
  }
4028
4036
 
4029
4037
  private async readAllArtifactsCached(): Promise<MemoryFile[]> {
4030
- if (
4031
- this.artifactIndexCache &&
4032
- Date.now() - this.artifactIndexCache.loadedAtMs <= StorageManager.ARTIFACT_INDEX_CACHE_TTL_MS &&
4033
- this.artifactIndexCache.writeVersion === this.getArtifactWriteVersion()
4034
- ) {
4035
- return this.artifactIndexCache.memories;
4036
- }
4037
-
4038
- const scanArtifacts = async (): Promise<MemoryFile[]> => {
4039
- const artifacts: MemoryFile[] = [];
4040
- const readDir = async (dir: string) => {
4041
- try {
4042
- const entries = await readdir(dir, { withFileTypes: true });
4043
- for (const entry of entries) {
4044
- const fullPath = path.join(dir, entry.name);
4045
- if (entry.isDirectory()) {
4046
- await readDir(fullPath);
4047
- continue;
4048
- }
4049
- if (!entry.name.endsWith(".md")) continue;
4050
- const memory = await this.readMemoryByPath(fullPath);
4051
- if (!memory) continue;
4052
- artifacts.push(memory);
4053
- }
4054
- } catch {
4055
- // Directory doesn't exist yet
4056
- }
4057
- };
4058
- await readDir(this.artifactsDir);
4059
- return artifacts;
4060
- };
4061
-
4062
- const MAX_REBUILD_RETRIES = 2;
4063
- let latestArtifacts: MemoryFile[] = [];
4064
- for (let attempt = 0; attempt <= MAX_REBUILD_RETRIES; attempt += 1) {
4065
- const versionBefore = this.getArtifactWriteVersion();
4066
- const artifacts = await scanArtifacts();
4067
- const versionAfter = this.getArtifactWriteVersion();
4068
- latestArtifacts = artifacts;
4069
- if (versionAfter === versionBefore) {
4070
- this.artifactIndexCache = { memories: artifacts, loadedAtMs: Date.now(), writeVersion: versionAfter };
4071
- return artifacts;
4072
- }
4073
- }
4074
-
4075
- // Highly concurrent writer churn; keep cache invalid so next read retries a clean rebuild.
4076
- // Return best-effort latest scan instead of an empty set to avoid dropping recall entirely.
4077
- this.artifactIndexCache = null;
4078
- return latestArtifacts;
4038
+ return this.memoryReadStore.readAllArtifactsCached(
4039
+ );
4079
4040
  }
4080
4041
 
4081
4042
  async searchArtifacts(query: string, maxResults: number): Promise<MemoryFile[]> {
@@ -4109,124 +4070,12 @@ export class StorageManager {
4109
4070
  structuredSections?: EntityStructuredSection[];
4110
4071
  } = {},
4111
4072
  ): Promise<string> {
4112
- await this.ensureDirectories();
4113
- if (typeof name !== "string" || !name.trim() || typeof type !== "string" || !type.trim()) {
4114
- log.warn("writeEntity: invalid entity payload, skipping", {
4115
- nameType: typeof name,
4116
- typeType: typeof type,
4117
- });
4118
- return "";
4119
- }
4120
- const safeFacts = Array.isArray(facts)
4121
- ? [...new Set(
4122
- facts
4123
- .filter((fact) => typeof fact === "string")
4124
- .map((fact) => fact.trim())
4125
- .filter((fact) => fact.length > 0),
4126
- )]
4127
- : [];
4128
- let normalized = this.normalizeEntityName(name, type);
4129
-
4130
- // Check for fuzzy match against existing entities before creating a new file
4131
- const match = await this.findMatchingEntity(name, type);
4132
- if (match && match !== normalized) {
4133
- log.debug(`fuzzy match: "${normalized}" → existing "${match}"`);
4134
- normalized = match;
4135
- }
4136
-
4137
- const filePath = path.join(this.entitiesDir, `${normalized}.md`);
4138
-
4139
- // Parse existing file to preserve relationships/activity/aliases/summary
4140
- let entity: EntityFile = {
4073
+ return this.entityStore.writeEntity(
4141
4074
  name,
4142
4075
  type,
4143
- created: "",
4144
- updated: new Date().toISOString(),
4145
- facts: [],
4146
- summary: undefined,
4147
- synthesis: undefined,
4148
- synthesisUpdatedAt: undefined,
4149
- synthesisVersion: undefined,
4150
- synthesisStructuredFactCount: undefined,
4151
- synthesisStructuredFactDigest: undefined,
4152
- timeline: [],
4153
- relationships: [],
4154
- activity: [],
4155
- aliases: [],
4156
- };
4157
- try {
4158
- const existing = await this.readStorageSecureFile(filePath);
4159
- entity = parseEntityFile(existing, this.entitySchemas);
4160
- } catch (err) {
4161
- if (err instanceof SecureStoreLockedError) throw err;
4162
- if (!isErrnoCode(err, "ENOENT")) throw err;
4163
- // File doesn't exist yet
4164
- }
4165
-
4166
- const timestamp = options.timestamp?.trim() || new Date().toISOString();
4167
- const source = options.source?.trim() || undefined;
4168
- const sessionKey = options.sessionKey?.trim() || undefined;
4169
- const principal = options.principal?.trim() || undefined;
4170
- const structuredSectionMap = new Map(
4171
- (entity.structuredSections ?? []).map((section) => [section.key, {
4172
- ...section,
4173
- facts: [...section.facts],
4174
- }]),
4175
- );
4176
- for (const section of options.structuredSections ?? []) {
4177
- const normalizedSection = normalizeEntityStructuredSection(type, section, this.entitySchemas);
4178
- const normalizedFacts = normalizeStructuredSectionFacts(section.facts);
4179
- if (normalizedFacts.length === 0) continue;
4180
- const existingSection = structuredSectionMap.get(normalizedSection.key);
4181
- if (!existingSection) {
4182
- structuredSectionMap.set(normalizedSection.key, {
4183
- key: normalizedSection.key,
4184
- title: normalizedSection.title,
4185
- facts: normalizedFacts,
4186
- });
4187
- continue;
4188
- }
4189
- existingSection.facts = normalizeStructuredSectionFacts([...existingSection.facts, ...normalizedFacts]);
4190
- if (!existingSection.title.trim() && normalizedSection.title.trim()) {
4191
- existingSection.title = normalizedSection.title;
4192
- }
4193
- }
4194
- for (const fact of safeFacts) {
4195
- const nextEntry = {
4196
- timestamp,
4197
- text: fact,
4198
- ...(source ? { source } : {}),
4199
- ...(sessionKey ? { sessionKey } : {}),
4200
- ...(principal ? { principal } : {}),
4201
- };
4202
- const alreadyPresent = entity.timeline.some((entry) =>
4203
- entry.timestamp === nextEntry.timestamp
4204
- && entry.text === nextEntry.text
4205
- && entry.source === nextEntry.source
4206
- && entry.sessionKey === nextEntry.sessionKey
4207
- && entry.principal === nextEntry.principal
4208
- );
4209
- if (alreadyPresent) continue;
4210
- entity.timeline.push(nextEntry);
4211
- }
4212
- entity.structuredSections = sortStructuredSectionsBySchema(
4213
- type,
4214
- Array.from(structuredSectionMap.values()).filter((section) => section.facts.length > 0),
4215
- this.entitySchemas,
4076
+ facts,
4077
+ options,
4216
4078
  );
4217
- entity.facts = compileEntityFacts(entity.timeline, entity.structuredSections);
4218
- entity.summary = entity.synthesis || entity.summary;
4219
- entity.name = name;
4220
- entity.type = type;
4221
- entity.created = entity.created || timestamp;
4222
- entity.updated = new Date().toISOString();
4223
-
4224
- await this.snapshotBeforeWrite(filePath, "write");
4225
- await this.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.entitySchemas));
4226
- this.invalidateKnowledgeIndexCache();
4227
- this.bumpMemoryStatusVersion(); // invalidate entity cache
4228
- log.debug(`wrote entity ${normalized}`);
4229
- return normalized;
4230
4079
  }
4231
4080
 
4232
4081
  async readProfile(): Promise<string> {
@@ -4458,98 +4307,8 @@ export class StorageManager {
4458
4307
  }
4459
4308
 
4460
4309
  private async collectActiveMemoryPaths(): Promise<string[]> {
4461
- const filePaths: string[] = [];
4462
-
4463
- // Resolve the memory root once for containment checks below. A category dir
4464
- // symlinked outside memoryDir (e.g. decisions/ -> an external dir) must NOT
4465
- // pull out-of-store files into the QMD-unavailable recall fallback (info
4466
- // leak). Same walker-hardening pattern as document-scanner.ts / cli.ts /
4467
- // consolidation-provenance-check.ts; reuses the shared containment helper.
4468
- let memoryRootReal: string;
4469
- try {
4470
- memoryRootReal = await realpath(this.baseDir);
4471
- } catch {
4472
- return filePaths;
4473
- }
4474
-
4475
- const collectPaths = async (dir: string) => {
4476
- // Directory-level guard, isolated from per-entry handling: skip symlinked
4477
- // or non-directory category dirs and assert the resolved dir stays inside
4478
- // the memory root before reading. A failure here means the whole subtree
4479
- // does not exist or escaped the store — fail closed by skipping it.
4480
- let entries: Dirent[];
4481
- try {
4482
- const dirStat = await lstat(dir);
4483
- if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) return;
4484
- assertPathInsideRoot(memoryRootReal, await realpath(dir), dir);
4485
- entries = await readdir(dir, { withFileTypes: true });
4486
- } catch {
4487
- return;
4488
- }
4489
-
4490
- const subdirs: string[] = [];
4491
- for (const entry of entries) {
4492
- // Never follow symlinked entries out of the store.
4493
- if (entry.isSymbolicLink()) continue;
4494
- const fullPath = path.join(dir, entry.name);
4495
- if (entry.isDirectory()) {
4496
- subdirs.push(fullPath);
4497
- } else if (entry.name.endsWith(".md")) {
4498
- // Isolate per-entry failures in their own try/catch: a containment or
4499
- // realpath failure on ONE .md entry must not drop sibling files or,
4500
- // crucially, the deferred subdir recursion below (Cursor Bugbot:
4501
- // "Poisoned md skips sibling subdirs"). Mirrors the per-file try/catch
4502
- // in search/document-scanner.ts scanDir and
4503
- // consolidation-provenance-check.ts walkMarkdownFiles.
4504
- try {
4505
- assertPathInsideRoot(memoryRootReal, await realpath(fullPath), fullPath);
4506
- filePaths.push(fullPath);
4507
- } catch {
4508
- // Skip just this entry (symlink/containment/realpath failure).
4509
- }
4510
- }
4511
- }
4512
- // Recurse into real subdirectories regardless of any single poisoned entry
4513
- // above, so valid nested in-store memories are never dropped.
4514
- for (const subdir of subdirs) {
4515
- await collectPaths(subdir);
4516
- }
4517
- };
4518
-
4519
- // Scan EVERY supported memory category directory, not just the legacy four
4520
- // (facts/procedures/reasoning-traces/corrections). Issue #1497: the QMD
4521
- // filesystem-fallback recall path (orchestrator `recent_scan` ->
4522
- // readAllMemoriesForNamespaces -> readAllMemories -> here) must read every
4523
- // recall category dir so on-disk memories in preferences/decisions/moments/
4524
- // commitments/principles/rules/skills/relationships are not missed when QMD
4525
- // is disabled, missing, or unhealthy. RECALL_FALLBACK_DIRS is the single
4526
- // source of truth derived from ALL_CATEGORY_DIRS (shared with
4527
- // ensureDirectories() and the write routing in utils/category-dir.ts).
4528
- // These paths resolve identically to the legacy this.factsDir /
4529
- // this.correctionsDir / this.proceduresDir / this.reasoningTracesDir
4530
- // getters (all `path.join(this.baseDir, <dir>)`), so the scan stays
4531
- // namespace-aware: this.baseDir is per-namespace, set by the storage router.
4532
- // Deliberately EXCLUDED (issue #1497 + PR #1503 review): the non-category
4533
- // content dirs that ensureDirectories() also creates — entities/, state/,
4534
- // artifacts/, identity/, config/ — plus the root profile.md, AND the
4535
- // questions/ queue dir. questions/ holds operational question-QUEUE items
4536
- // written by writeQuestion() (frontmatter `{ id, created, priority,
4537
- // resolved }`), read only via readQuestions() and surfaced through the
4538
- // dedicated, disabled-by-default `injectQuestions` recall-pipeline stage —
4539
- // never as standard recall memories. The QMD primary recall corpus does not
4540
- // include them, so the fallback must not either (corpus parity; CLAUDE.md
4541
- // rule #39). Were questions/ scanned here, parseFrontmatter() would accept
4542
- // those files (they have a `---` frontmatter block) and leak queue items
4543
- // into recall. None of these excluded dirs are in RECALL_FALLBACK_DIRS; the
4544
- // exclusion is asserted by tests in storage-fallback-category-dirs.test.ts.
4545
- //
4546
- // collectPaths() already ignores missing dirs (try/catch) and the parser
4547
- // returns null for non-memory markdown, so unrelated files never crash the
4548
- // scan.
4549
- for (const dir of RECALL_FALLBACK_DIRS) {
4550
- await collectPaths(path.join(this.baseDir, dir));
4551
- }
4552
- return filePaths;
4310
+ return this.memoryReadStore.collectActiveMemoryPaths(
4311
+ );
4553
4312
  }
4554
4313
 
4555
4314
  private async readParsedMemoriesFromPaths(
@@ -4693,52 +4452,9 @@ export class StorageManager {
4693
4452
  batchSize?: number;
4694
4453
  updatedAfter?: Date;
4695
4454
  } = {}): Promise<{ memories: MemoryFile[]; filePaths: string[] }> {
4696
- const allPaths = await this.collectActiveMemoryPaths();
4697
- const sortedPaths = this.orderWindowPaths(allPaths);
4698
- const maxMemories =
4699
- typeof options.maxMemories === "number" && Number.isFinite(options.maxMemories)
4700
- ? Math.max(1, Math.floor(options.maxMemories))
4701
- : undefined;
4702
- const maxCandidatePaths = maxMemories === undefined ? undefined : maxMemories * 2;
4703
- const updatedAfterMs = options.updatedAfter?.getTime();
4704
- const normalizedBatchSize = this.normalizeMemoryReadBatchSize(options.batchSize);
4705
- const memories: MemoryFile[] = [];
4706
- const selectedPaths: string[] = [];
4707
-
4708
- for (let i = 0; i < sortedPaths.length; i += normalizedBatchSize) {
4709
- if (
4710
- maxMemories !== undefined
4711
- && (memories.length >= maxMemories || (maxCandidatePaths !== undefined && selectedPaths.length >= maxCandidatePaths))
4712
- ) {
4713
- return { memories, filePaths: selectedPaths };
4714
- }
4715
- const batchPaths = sortedPaths.slice(i, i + normalizedBatchSize);
4716
- const candidateBatchPaths = updatedAfterMs === undefined
4717
- ? batchPaths
4718
- : await this.filterWindowPathsByUpdatedAfter(batchPaths, updatedAfterMs);
4719
- const remainingSlots = maxMemories === undefined ? undefined : Math.max(0, maxMemories - memories.length);
4720
- const remainingInspectionBudget = maxCandidatePaths === undefined ? undefined : Math.max(0, maxCandidatePaths - selectedPaths.length);
4721
- const { memories: batchMemories, filePaths: parsedCandidatePaths } = remainingSlots === undefined
4722
- ? {
4723
- memories: await this.readParsedMemoriesFromPaths(candidateBatchPaths, normalizedBatchSize),
4724
- filePaths: candidateBatchPaths,
4725
- }
4726
- : await this.readWindowBoundedBatch(
4727
- candidateBatchPaths,
4728
- remainingSlots,
4729
- remainingInspectionBudget ?? remainingSlots,
4730
- normalizedBatchSize,
4731
- );
4732
- selectedPaths.push(...parsedCandidatePaths);
4733
- for (const memory of batchMemories) {
4734
- memories.push(memory);
4735
- if (maxMemories !== undefined && memories.length >= maxMemories) {
4736
- return { memories, filePaths: selectedPaths };
4737
- }
4738
- }
4739
- }
4740
-
4741
- return { memories, filePaths: selectedPaths };
4455
+ return this.memoryReadStore.readMemoriesWindow(
4456
+ options,
4457
+ );
4742
4458
  }
4743
4459
 
4744
4460
  private async _readAllMemoriesFromDisk(): Promise<MemoryFile[]> {
@@ -4746,85 +4462,9 @@ export class StorageManager {
4746
4462
  return this.readParsedMemoriesFromPaths(filePaths, 50);
4747
4463
  }
4748
4464
 
4749
- /**
4750
- * Read all memories from the cold tier by scanning the entire cold/ root
4751
- * tree. Previously this only scanned cold/facts/ and cold/corrections/, but
4752
- * structuredAttributes can appear on any MemoryCategory (preference, decision,
4753
- * entity, etc.). buildTierMemoryPath now routes each category to its own
4754
- * cold/<dir>/ subtree via the shared categoryDirName() chokepoint (issue
4755
- * #1546), so cold decisions/preferences/... live outside cold/facts/.
4756
- * Scanning the full coldRoot covers every category dir and guards against
4757
- * files placed in unexpected subdirectories during manual operations or future
4758
- * refactors.
4759
- *
4760
- * Broadened in PR #402 round-6 (Finding UTsP): scanning only facts/ and
4761
- * corrections/ was a narrower-than-necessary subset of the cold directory
4762
- * tree. Correctness trumps the minor performance difference — cold scans
4763
- * already happen at most once per supersession write.
4764
- *
4765
- * Used by applyTemporalSupersession so that memories already demoted to
4766
- * cold/ can still be marked superseded when a newer hot fact arrives.
4767
- *
4768
- * Cached with a TTL (Finding UOGi, PR #402 round-6): back-to-back
4769
- * structured-attribute writes in the same burst reuse the cached result
4770
- * instead of re-scanning the cold tree on every call. The cache is
4771
- * invalidated whenever a write calls invalidateAllMemoriesCache() (which
4772
- * covers any hot→cold demotion that changes cold-tier contents) and
4773
- * expires after COLD_SCAN_CACHE_TTL_MS as a safety net.
4774
- */
4775
4465
  async readAllColdMemories(): Promise<MemoryFile[]> {
4776
- const coldRoot = this.resolveTierRootDir("cold");
4777
-
4778
- // Read the on-disk cold-version sentinel BEFORE checking the cache so that
4779
- // writes made by other processes (gateway + CLI) are detected immediately.
4780
- // Finding UvUy (PR #402 round-11): without this check the cache served
4781
- // stale data for up to 30s when another process wrote a new cold memory.
4782
- const currentColdVersion = this.readColdWriteVersion();
4783
-
4784
- // Return cached result if still valid by both TTL and sentinel version.
4785
- const cached = StorageManager.coldMemoriesCache.get(coldRoot);
4786
- if (
4787
- cached &&
4788
- Date.now() - cached.loadedAt < StorageManager.COLD_SCAN_CACHE_TTL_MS &&
4789
- cached.coldVersion === currentColdVersion
4790
- ) {
4791
- return cached.memories;
4792
- }
4793
-
4794
- const filePaths: string[] = [];
4795
-
4796
- const collectPaths = async (dir: string) => {
4797
- try {
4798
- const entries = await readdir(dir, { withFileTypes: true });
4799
- const subdirs: string[] = [];
4800
- for (const entry of entries) {
4801
- const fullPath = path.join(dir, entry.name);
4802
- if (entry.isDirectory()) {
4803
- subdirs.push(fullPath);
4804
- } else if (entry.name.endsWith(".md")) {
4805
- filePaths.push(fullPath);
4806
- }
4807
- }
4808
- for (const subdir of subdirs) {
4809
- await collectPaths(subdir);
4810
- }
4811
- } catch {
4812
- // Directory does not exist yet — cold tier may be empty.
4813
- }
4814
- };
4815
-
4816
- // Scan the entire cold root so that memories in any subdirectory (facts/,
4817
- // corrections/, artifacts/, or any future category-specific subdirectory)
4818
- // are included. This is broader than the previous facts/+corrections/ scan
4819
- // and ensures that any memory with structuredAttributes is found regardless
4820
- // of which category it was written with.
4821
- await collectPaths(coldRoot);
4822
- const memories = await this.readParsedMemoriesFromPaths(filePaths, 50);
4823
-
4824
- // Store in cache with the sentinel version captured above so that any
4825
- // subsequent cold-version bump (by this or another process) invalidates it.
4826
- StorageManager.coldMemoriesCache.set(coldRoot, { memories, loadedAt: Date.now(), coldVersion: currentColdVersion });
4827
- return memories;
4466
+ return this.memoryReadStore.readAllColdMemories(
4467
+ );
4828
4468
  }
4829
4469
 
4830
4470
  /**
@@ -4874,67 +4514,10 @@ export class StorageManager {
4874
4514
  return memories;
4875
4515
  }
4876
4516
 
4877
- /** Read a single memory file by its absolute path. Returns null if unreadable. */
4878
4517
  async readMemoryByPath(filePath: string): Promise<MemoryFile | null> {
4879
- try {
4880
- const raw = await readMaybeEncryptedFile(filePath, this._secureStoreKey, this.baseDir);
4881
- // Note: the outer catch intentionally swallows most errors (ENOENT etc.)
4882
- // but SecureStoreLockedError must propagate — see re-throw below.
4883
- const parsed = parseFrontmatter(raw);
4884
- if (parsed) {
4885
- return {
4886
- path: filePath,
4887
- frontmatter: normalizeFrontmatterForPath(
4888
- parsed.frontmatter,
4889
- toMemoryPathRel(this.baseDir, filePath),
4890
- parsed.content,
4891
- ),
4892
- content: parsed.content,
4893
- };
4894
- }
4895
-
4896
- // Entity files use a `# Name` + `**Type:** ...` markdown format rather than
4897
- // YAML frontmatter. Build a synthetic MemoryFile so entity files returned by
4898
- // the direct retrieval agent participate in boostSearchResults and last-recall
4899
- // tracking rather than being silently dropped.
4900
- const normalizedPath = filePath.split(path.sep).join("/");
4901
- if (normalizedPath.includes("/entities/") && filePath.endsWith(".md")) {
4902
- const entity = parseEntityFile(raw, this.entitySchemas);
4903
- if (!entity.name) return null;
4904
- const nameWithoutExt = path.basename(filePath, ".md");
4905
- // Fall back to file mtime rather than new Date() so that entities without
4906
- // an explicit Updated: timestamp are not treated as freshly created on every
4907
- // read. Using new Date() would inflate boostSearchResults recency scores for
4908
- // every entity that lacks a timestamp.
4909
- // Use epoch as the last-resort fallback so that entities without a
4910
- // parseable timestamp don't appear as "freshly created" and inflate scores.
4911
- const fileMtime = entity.updated
4912
- || await stat(filePath).then((s) => s.mtime.toISOString()).catch(() => new Date(0).toISOString());
4913
- return {
4914
- path: filePath,
4915
- frontmatter: {
4916
- id: nameWithoutExt,
4917
- category: "entity",
4918
- created: fileMtime,
4919
- updated: fileMtime,
4920
- source: "entity_extraction",
4921
- confidence: 0.9,
4922
- confidenceTier: confidenceTier(0.9),
4923
- tags: entity.type ? [entity.type] : [],
4924
- },
4925
- content: raw,
4926
- };
4927
- }
4928
-
4929
- return null;
4930
- } catch (err) {
4931
- // Re-throw store-locked errors — callers need to distinguish "locked"
4932
- // from "file not found / parse error". Swallowing a locked error here
4933
- // would silently return null and leave the daemon appearing to work
4934
- // while returning no memories (subtle data loss).
4935
- if (err instanceof SecureStoreLockedError) throw err;
4936
- return null;
4937
- }
4518
+ return this.memoryReadStore.readMemoryByPath(
4519
+ filePath,
4520
+ );
4938
4521
  }
4939
4522
 
4940
4523
  private resolveTierRootDir(tier: "hot" | "cold"): string {
@@ -5106,30 +4689,14 @@ export class StorageManager {
5106
4689
  }
5107
4690
 
5108
4691
  async readEntity(name: string): Promise<string> {
5109
- const filePath = this.resolveEntityFilePath(name);
5110
- if (filePath === null) {
5111
- return "";
5112
- }
5113
- try {
5114
- return await this.readStorageSecureFile(filePath);
5115
- } catch (err) {
5116
- if (err instanceof SecureStoreLockedError) throw err;
5117
- if (!isErrnoCode(err, "ENOENT")) throw err;
5118
- return "";
5119
- }
4692
+ return this.entityStore.readEntity(
4693
+ name,
4694
+ );
5120
4695
  }
5121
4696
 
5122
- /** Return sorted list of entity filenames (without .md extension) */
5123
4697
  async listEntityNames(): Promise<string[]> {
5124
- try {
5125
- const entries = await readdir(this.entitiesDir);
5126
- return entries
5127
- .filter((e) => e.endsWith(".md"))
5128
- .map((e) => e.replace(".md", ""))
5129
- .sort();
5130
- } catch {
5131
- return [];
5132
- }
4698
+ return this.entityStore.listEntityNames(
4699
+ );
5133
4700
  }
5134
4701
 
5135
4702
  /**
@@ -5451,130 +5018,20 @@ export class StorageManager {
5451
5018
  return events.length;
5452
5019
  }
5453
5020
 
5454
- /**
5455
- * Append a batch of `BUFFER_SURPRISE` telemetry events (issue #563 PR 3).
5456
- *
5457
- * Each event records a single buffer flush decision driven by the
5458
- * surprise gate. The ledger is consumed by
5459
- * `reportBufferSurpriseDistribution` (Doctor report) and by downstream
5460
- * benchmark analysis. This method is fire-and-forget by contract:
5461
- * callers log but do not fail the hot path if the append throws.
5462
- */
5463
5021
  async appendBufferSurpriseEvents(
5464
5022
  events: BufferSurpriseEvent[],
5465
5023
  ): Promise<number> {
5466
- if (events.length === 0) return 0;
5467
- await this.ensureDirectories();
5468
-
5469
- const nowIso = new Date().toISOString();
5470
- const payload = events
5471
- .map((event) => {
5472
- const normalized: BufferSurpriseEvent = {
5473
- ...event,
5474
- event: "BUFFER_SURPRISE",
5475
- timestamp:
5476
- event.timestamp && event.timestamp.length > 0
5477
- ? event.timestamp
5478
- : nowIso,
5479
- };
5480
- return `${JSON.stringify(normalized)}\n`;
5481
- })
5482
- .join("");
5483
-
5484
- await this.appendStorageSecureFile(this.bufferSurpriseLedgerPath, payload);
5485
- return events.length;
5024
+ return this.memoryReadStore.appendBufferSurpriseEvents(
5025
+ events,
5026
+ );
5486
5027
  }
5487
5028
 
5488
- /**
5489
- * Read the buffer-surprise ledger, most recent rows last.
5490
- *
5491
- * `limit` bounds the number of **valid rows** returned (not the
5492
- * number of raw lines parsed). We parse every row, discard malformed
5493
- * ones, then take the tail — so a partial/truncated trailing line
5494
- * (the common failure mode after an interrupted append) cannot hide
5495
- * otherwise-valid recent data above it.
5496
- *
5497
- * Non-positive / non-integer / non-finite limits return `[]` rather
5498
- * than the entire file, matching the other ledger readers in this
5499
- * class and protecting against `slice(-0.5)` → `slice(-0)` silently
5500
- * devolving into an unbounded parse.
5501
- *
5502
- * # Performance note
5503
- *
5504
- * For very large ledgers (issue #563 follow-up), a tail-first reader
5505
- * would avoid parsing the full file when only a recent window is
5506
- * needed. We keep the full-scan implementation here because:
5507
- *
5508
- * - the ledger is opt-in (flag off by default), so early deployments
5509
- * accumulate rows slowly;
5510
- * - telemetry rows are small (~200 bytes), so even 100k rows parse
5511
- * in well under a second;
5512
- * - the governance archive/cleanup flow can trim the ledger when
5513
- * size becomes a concern, reusing the existing maintenance hooks.
5514
- *
5515
- * Swap to a chunked tail-reader if production logs show this is a
5516
- * hot path — leaving that work for a follow-up keeps this PR scoped
5517
- * to correctness, not optimization.
5518
- */
5519
5029
  async readBufferSurpriseEvents(
5520
5030
  options: { limit?: number } = {},
5521
5031
  ): Promise<BufferSurpriseEvent[]> {
5522
- let raw: string;
5523
- try {
5524
- raw = await this.readStorageSecureFile(this.bufferSurpriseLedgerPath);
5525
- } catch (err) {
5526
- if (err instanceof SecureStoreLockedError) throw err;
5527
- const code = (err as NodeJS.ErrnoException).code;
5528
- if (code === "ENOENT") return [];
5529
- throw err;
5530
- }
5531
-
5532
- // Resolve the effective limit up front. Any non-finite / non-positive
5533
- // value returns no rows — callers who want "everything" should OMIT
5534
- // the `limit` key (treated as "no bound" below). We intentionally
5535
- // reject `Infinity` too, because the slice math `events.slice(-Inf)`
5536
- // is surprising and ambiguous; omit the key instead. Fractional
5537
- // values <1 floor to 0, which would make `slice(-0)` return the
5538
- // entire file — guard against that too.
5539
- let effectiveLimit: number | null = null;
5540
- if (options.limit !== undefined) {
5541
- if (
5542
- typeof options.limit !== "number" ||
5543
- !Number.isFinite(options.limit) ||
5544
- options.limit <= 0
5545
- ) {
5546
- return [];
5547
- }
5548
- const floored = Math.floor(options.limit);
5549
- if (floored <= 0) return [];
5550
- effectiveLimit = floored;
5551
- }
5552
-
5553
- const lines = raw.split("\n");
5554
- const events: BufferSurpriseEvent[] = [];
5555
- for (const line of lines) {
5556
- const trimmed = line.trim();
5557
- if (trimmed.length === 0) continue;
5558
- try {
5559
- const parsed = JSON.parse(trimmed);
5560
- if (isValidBufferSurpriseEvent(parsed)) {
5561
- events.push(parsed);
5562
- }
5563
- } catch {
5564
- // Malformed row — fail open, skip.
5565
- }
5566
- }
5567
-
5568
- events.sort(
5569
- (left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp),
5032
+ return this.memoryReadStore.readBufferSurpriseEvents(
5033
+ options,
5570
5034
  );
5571
-
5572
- if (effectiveLimit === null) return events;
5573
- // Slice over VALID rows, not raw lines, so malformed tails cannot
5574
- // mask good data above them. Sort by event timestamp before slicing
5575
- // so concurrent probe completion order cannot make an older scored
5576
- // turn look newer than a later scored turn.
5577
- return events.slice(-effectiveLimit);
5578
5035
  }
5579
5036
 
5580
5037
  async appendBehaviorSignals(events: BehaviorSignalEvent[]): Promise<number> {
@@ -5892,106 +5349,9 @@ export class StorageManager {
5892
5349
  private async readCompressionGuidelineStateFile(
5893
5350
  filePath: string,
5894
5351
  ): Promise<CompressionGuidelineOptimizerState | null> {
5895
- const isFiniteNonNegativeInteger = (value: unknown): value is number =>
5896
- typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
5897
- const isValidActionSummary = (
5898
- value: unknown,
5899
- ): value is NonNullable<CompressionGuidelineOptimizerState["actionSummaries"]>[number] => {
5900
- if (!value || typeof value !== "object") return false;
5901
- const summary = value as NonNullable<CompressionGuidelineOptimizerState["actionSummaries"]>[number];
5902
- return (
5903
- typeof summary.action === "string" &&
5904
- isFiniteNonNegativeInteger(summary.total) &&
5905
- summary.outcomes !== null &&
5906
- typeof summary.outcomes === "object" &&
5907
- isFiniteNonNegativeInteger(summary.outcomes.applied) &&
5908
- isFiniteNonNegativeInteger(summary.outcomes.skipped) &&
5909
- isFiniteNonNegativeInteger(summary.outcomes.failed) &&
5910
- summary.quality !== null &&
5911
- typeof summary.quality === "object" &&
5912
- isFiniteNonNegativeInteger(summary.quality.good) &&
5913
- isFiniteNonNegativeInteger(summary.quality.poor) &&
5914
- isFiniteNonNegativeInteger(summary.quality.unknown)
5915
- );
5916
- };
5917
- const isValidRuleUpdate = (
5918
- value: unknown,
5919
- ): value is NonNullable<CompressionGuidelineOptimizerState["ruleUpdates"]>[number] => {
5920
- if (!value || typeof value !== "object") return false;
5921
- const rule = value as NonNullable<CompressionGuidelineOptimizerState["ruleUpdates"]>[number];
5922
- return (
5923
- typeof rule.action === "string" &&
5924
- typeof rule.delta === "number" &&
5925
- Number.isFinite(rule.delta) &&
5926
- (rule.direction === "increase" || rule.direction === "decrease" || rule.direction === "hold") &&
5927
- (rule.confidence === "low" || rule.confidence === "medium" || rule.confidence === "high") &&
5928
- Array.isArray(rule.notes) &&
5929
- rule.notes.every((note) => typeof note === "string")
5930
- );
5931
- };
5932
-
5933
- try {
5934
- const raw = await this.readStorageSecureFile(filePath);
5935
- const parsed = JSON.parse(raw) as Partial<CompressionGuidelineOptimizerState>;
5936
- const sourceWindow = parsed?.sourceWindow as Partial<CompressionGuidelineOptimizerState["sourceWindow"]>;
5937
- const eventCounts = parsed?.eventCounts as Partial<CompressionGuidelineOptimizerState["eventCounts"]>;
5938
- const activationState =
5939
- parsed?.activationState === "draft" || parsed?.activationState === "active"
5940
- ? parsed.activationState
5941
- : undefined;
5942
- const contentHash =
5943
- typeof parsed?.contentHash === "string" && parsed.contentHash.length > 0
5944
- ? parsed.contentHash
5945
- : undefined;
5946
- const actionSummaries = Array.isArray(parsed?.actionSummaries)
5947
- ? parsed.actionSummaries.filter(isValidActionSummary)
5948
- : undefined;
5949
- const ruleUpdates = Array.isArray(parsed?.ruleUpdates)
5950
- ? parsed.ruleUpdates.filter(isValidRuleUpdate)
5951
- : undefined;
5952
- if (
5953
- !isFiniteNonNegativeInteger(parsed?.version) ||
5954
- typeof parsed?.updatedAt !== "string" ||
5955
- parsed.updatedAt.length === 0 ||
5956
- !sourceWindow ||
5957
- typeof sourceWindow.from !== "string" ||
5958
- sourceWindow.from.length === 0 ||
5959
- typeof sourceWindow.to !== "string" ||
5960
- sourceWindow.to.length === 0 ||
5961
- !eventCounts ||
5962
- !isFiniteNonNegativeInteger(eventCounts.total) ||
5963
- !isFiniteNonNegativeInteger(eventCounts.applied) ||
5964
- !isFiniteNonNegativeInteger(eventCounts.skipped) ||
5965
- !isFiniteNonNegativeInteger(eventCounts.failed) ||
5966
- !isFiniteNonNegativeInteger(parsed?.guidelineVersion)
5967
- ) {
5968
- return null;
5969
- }
5970
-
5971
- return {
5972
- version: parsed.version,
5973
- updatedAt: parsed.updatedAt,
5974
- sourceWindow: {
5975
- from: sourceWindow.from,
5976
- to: sourceWindow.to,
5977
- },
5978
- eventCounts: {
5979
- total: eventCounts.total,
5980
- applied: eventCounts.applied,
5981
- skipped: eventCounts.skipped,
5982
- failed: eventCounts.failed,
5983
- },
5984
- guidelineVersion: parsed.guidelineVersion,
5985
- ...(contentHash ? { contentHash } : {}),
5986
- ...(activationState ? { activationState } : {}),
5987
- ...(actionSummaries ? { actionSummaries } : {}),
5988
- ...(ruleUpdates ? { ruleUpdates } : {}),
5989
- };
5990
- } catch (err) {
5991
- if (err instanceof SecureStoreLockedError) throw err;
5992
- if (!isErrnoCode(err, "ENOENT")) throw err;
5993
- return null;
5994
- }
5352
+ return this.memoryReadStore.readCompressionGuidelineStateFile(
5353
+ filePath,
5354
+ );
5995
5355
  }
5996
5356
 
5997
5357
  async writeIdentityAnchor(content: string): Promise<void> {
@@ -6220,40 +5580,9 @@ export class StorageManager {
6220
5580
  filePath: string;
6221
5581
  }>
6222
5582
  > {
6223
- const cacheKey = this.questionsDir;
6224
- const cached = StorageManager.questionsCache.get(cacheKey);
6225
- if (cached && Date.now() - cached.loadedAt < StorageManager.QUESTIONS_CACHE_TTL_MS) {
6226
- // Check dir mtime for cross-process invalidation — if another process
6227
- // wrote/resolved a question, the directory mtime will be newer than loadedAt.
6228
- try {
6229
- const dirStat = await stat(this.questionsDir);
6230
- if (dirStat.mtimeMs <= cached.loadedAt) {
6231
- const all = cached.questions;
6232
- return opts?.unresolvedOnly ? all.filter((q) => !q.resolved) : all;
6233
- }
6234
- } catch {
6235
- // Dir doesn't exist — fall through to re-read
6236
- }
6237
- }
6238
-
6239
- try {
6240
- const files = await readdir(this.questionsDir);
6241
- const questions = [];
6242
- for (const file of files) {
6243
- if (!file.endsWith(".md")) continue;
6244
- const filePath = path.join(this.questionsDir, file);
6245
- const raw = await readMaybeEncryptedFile(filePath, this._secureStoreKey, this.baseDir);
6246
- const parsed = this.parseQuestionFile(raw, filePath);
6247
- if (parsed) {
6248
- questions.push(parsed);
6249
- }
6250
- }
6251
- const sorted = questions.sort((a, b) => b.priority - a.priority);
6252
- StorageManager.questionsCache.set(cacheKey, { questions: sorted, loadedAt: Date.now() });
6253
- return opts?.unresolvedOnly ? sorted.filter((q) => !q.resolved) : sorted;
6254
- } catch {
6255
- return [];
6256
- }
5583
+ return this.memoryReadStore.readQuestions(
5584
+ opts,
5585
+ );
6257
5586
  }
6258
5587
 
6259
5588
  /** Invalidate the questions cache (call after writing a question). */
@@ -6554,9 +5883,6 @@ export class StorageManager {
6554
5883
  this.invalidateKnowledgeIndexCache();
6555
5884
  }
6556
5885
 
6557
- /**
6558
- * Set or rewrite the synthesis layer of an entity file.
6559
- */
6560
5886
  async updateEntitySynthesis(
6561
5887
  name: string,
6562
5888
  synthesis: string,
@@ -6569,45 +5895,11 @@ export class StorageManager {
6569
5895
  incrementVersion?: boolean;
6570
5896
  } = {},
6571
5897
  ): Promise<void> {
6572
- const filePath = path.join(this.entitiesDir, `${name}.md`);
6573
- let entity: EntityFile;
6574
- try {
6575
- const content = await this.readStorageSecureFile(filePath);
6576
- entity = parseEntityFile(content, this.entitySchemas);
6577
- } catch (err) {
6578
- if (err instanceof SecureStoreLockedError) throw err;
6579
- if (!isErrnoCode(err, "ENOENT")) throw err;
6580
- log.debug(`updateEntitySynthesis: entity file ${name}.md not found`);
6581
- return;
6582
- }
6583
-
6584
- const updatedAt = options.updatedAt?.trim() || entity.synthesisUpdatedAt?.trim() || undefined;
6585
- const entityUpdatedAt = options.entityUpdatedAt?.trim() || updatedAt || entity.updated || new Date().toISOString();
6586
- const synthesisTimelineCount = Number.isInteger(options.synthesisTimelineCount)
6587
- && (options.synthesisTimelineCount ?? 0) >= 0
6588
- ? options.synthesisTimelineCount
6589
- : undefined;
6590
- const synthesisStructuredFactCount = Number.isInteger(options.synthesisStructuredFactCount)
6591
- && (options.synthesisStructuredFactCount ?? 0) >= 0
6592
- ? options.synthesisStructuredFactCount
6593
- : countEntityStructuredFacts(entity);
6594
- const synthesisStructuredFactDigest = options.synthesisStructuredFactDigest?.trim()
6595
- || fingerprintEntityStructuredFacts(entity);
6596
- entity.synthesis = synthesis.trim();
6597
- entity.summary = entity.synthesis;
6598
- entity.synthesisUpdatedAt = updatedAt;
6599
- entity.synthesisTimelineCount = synthesisTimelineCount;
6600
- entity.synthesisStructuredFactCount = synthesisStructuredFactCount;
6601
- entity.synthesisStructuredFactDigest = synthesisStructuredFactDigest;
6602
- entity.synthesisVersion = Math.max(0, entity.synthesisVersion ?? 0)
6603
- + (options.incrementVersion === false ? 0 : 1);
6604
- entity.updated = entityUpdatedAt;
6605
- await this.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.entitySchemas));
6606
- await this.removeEntitySynthesisQueueEntries([
6607
- ...new Set([name, this.normalizeEntityName(entity.name, entity.type)]),
6608
- ]);
6609
- this.invalidateKnowledgeIndexCache();
6610
- this.bumpMemoryStatusVersion(); // invalidate entity cache
5898
+ return this.entityStore.updateEntitySynthesis(
5899
+ name,
5900
+ synthesis,
5901
+ options,
5902
+ );
6611
5903
  }
6612
5904
 
6613
5905
  /**
@@ -6731,52 +6023,9 @@ export class StorageManager {
6731
6023
  // Scoring + Knowledge Index (Knowledge Graph v7.0)
6732
6024
  // ---------------------------------------------------------------------------
6733
6025
 
6734
- /**
6735
- * Read all entity files and return lightweight EntityFile objects.
6736
- * Parsing is fast (~50-100ms for ~1,800 files) since entity files are small.
6737
- */
6738
6026
  async readAllEntityFiles(): Promise<EntityFile[]> {
6739
- const currentVersion = this.getMemoryStatusVersion();
6740
- const schemaCacheKey = buildEntitySchemaCacheKey(this.entitySchemas);
6741
- const cacheKey = `${this.getEntityCacheSecureStoreKey()}\u0000${schemaCacheKey}`;
6742
- const cached = getCachedEntities(this.baseDir, currentVersion, cacheKey);
6743
- if (cached) return cached;
6744
-
6745
- try {
6746
- const entries = await readdir(this.entitiesDir);
6747
- const mdFiles = entries.filter((e) => e.endsWith(".md"));
6748
- if (mdFiles.length === 0) return [];
6749
-
6750
- // Read all entity files in parallel batches to avoid O(N) sequential I/O.
6751
- // With 3000+ entity files, sequential reads can take 15-20s under load.
6752
- // Batching at 100 keeps file-descriptor pressure manageable while staying fast.
6753
- const BATCH_SIZE = 100;
6754
- const entities: EntityFile[] = [];
6755
- for (let i = 0; i < mdFiles.length; i += BATCH_SIZE) {
6756
- const batch = mdFiles.slice(i, i + BATCH_SIZE);
6757
- const results = await Promise.all(
6758
- batch.map(async (entry) => {
6759
- try {
6760
- return await this.readStorageSecureFile(path.join(this.entitiesDir, entry));
6761
- } catch (err) {
6762
- if (err instanceof SecureStoreLockedError) throw err;
6763
- if (!isErrnoCode(err, "ENOENT")) throw err;
6764
- return null;
6765
- }
6766
- }),
6767
- );
6768
- for (const content of results) {
6769
- if (content !== null) entities.push(parseEntityFile(content, this.entitySchemas));
6770
- }
6771
- }
6772
-
6773
- setCachedEntities(this.baseDir, entities, currentVersion, cacheKey);
6774
- return entities;
6775
- } catch (err) {
6776
- if (err instanceof SecureStoreLockedError || !isErrnoCode(err, "ENOENT")) throw err;
6777
- // Directory doesn't exist yet
6778
- return [];
6779
- }
6027
+ return this.entityStore.readAllEntityFiles(
6028
+ );
6780
6029
  }
6781
6030
 
6782
6031
  /**
@@ -6820,77 +6069,14 @@ export class StorageManager {
6820
6069
  );
6821
6070
  }
6822
6071
 
6823
- /**
6824
- * Build the Knowledge Index: a compact markdown table of top-scored entities.
6825
- * Respects maxEntities and maxChars limits from config.
6826
- */
6827
6072
  async buildKnowledgeIndex(
6828
6073
  config: PluginConfig,
6829
6074
  overrides?: { maxEntities?: number; maxChars?: number },
6830
6075
  ): Promise<{ result: string; cached: boolean }> {
6831
- const useDefaultLimits =
6832
- overrides?.maxEntities === undefined &&
6833
- overrides?.maxChars === undefined;
6834
- // Return cached index if still fresh
6835
- if (
6836
- useDefaultLimits &&
6837
- this.knowledgeIndexCache &&
6838
- Date.now() - this.knowledgeIndexCache.builtAt < StorageManager.KNOWLEDGE_INDEX_CACHE_TTL_MS
6839
- ) {
6840
- return { result: this.knowledgeIndexCache.result, cached: true };
6841
- }
6842
-
6843
- const entities = await this.readAllEntityFiles();
6844
- if (entities.length === 0) {
6845
- if (useDefaultLimits) this.knowledgeIndexCache = { result: "", builtAt: Date.now() };
6846
- return { result: "", cached: false };
6847
- }
6848
-
6849
- const now = new Date();
6850
- const scored: ScoredEntity[] = entities.map((e) => ({
6851
- name: e.name,
6852
- type: e.type,
6853
- score: StorageManager.scoreEntity(e, now),
6854
- factCount: e.facts.length,
6855
- summary: e.synthesis ?? e.summary,
6856
- topRelationships: e.relationships.slice(0, 3).map((r) => r.target),
6857
- }));
6858
-
6859
- // Sort by score descending, take top N
6860
- scored.sort((a, b) => b.score - a.score);
6861
- const maxEntities = typeof overrides?.maxEntities === "number"
6862
- ? Math.max(0, Math.floor(overrides.maxEntities))
6863
- : config.knowledgeIndexMaxEntities;
6864
- const topN = scored.slice(0, maxEntities);
6865
-
6866
- if (topN.length === 0) {
6867
- if (useDefaultLimits) this.knowledgeIndexCache = { result: "", builtAt: Date.now() };
6868
- return { result: "", cached: false };
6869
- }
6870
-
6871
- // Build markdown table
6872
- const header = "## Knowledge Index\n\n| Entity | Type | Summary | Connected to |\n|--------|------|---------|-------------|";
6873
- const rows: string[] = [];
6874
- let totalChars = header.length;
6875
- const maxChars = typeof overrides?.maxChars === "number"
6876
- ? Math.max(0, Math.floor(overrides.maxChars))
6877
- : config.knowledgeIndexMaxChars;
6878
-
6879
- for (const entity of topN) {
6880
- const summary = entity.summary || `${entity.factCount} facts`;
6881
- const connected = entity.topRelationships.length > 0
6882
- ? entity.topRelationships.join(", ")
6883
- : "—";
6884
- const row = `| ${entity.name} | ${entity.type} | ${summary} | ${connected} |`;
6885
-
6886
- if (totalChars + row.length + 1 > maxChars) break;
6887
- rows.push(row);
6888
- totalChars += row.length + 1;
6889
- }
6890
-
6891
- const result = rows.length === 0 ? "" : `${header}\n${rows.join("\n")}\n`;
6892
- if (useDefaultLimits) this.knowledgeIndexCache = { result, builtAt: Date.now() };
6893
- return { result, cached: false };
6076
+ return this.entityStore.buildKnowledgeIndex(
6077
+ config,
6078
+ overrides,
6079
+ );
6894
6080
  }
6895
6081
 
6896
6082
  /** Invalidate the Knowledge Index cache (call after entity mutations). */
@@ -6905,275 +6091,9 @@ export class StorageManager {
6905
6091
  /** Max lines for profile.md before LLM consolidation triggers */
6906
6092
  private static readonly PROFILE_MAX_LINES = 300;
6907
6093
 
6908
- /**
6909
- * Merge fragmented entity files that resolve to the same canonical name.
6910
- * Preserves relationships, activity, aliases, and summary from all fragments.
6911
- * Returns count of files merged.
6912
- */
6913
6094
  async mergeFragmentedEntities(): Promise<number> {
6914
- let merged = 0;
6915
- try {
6916
- const entries = await readdir(this.entitiesDir);
6917
- const mdFiles = entries.filter((e) => e.endsWith(".md"));
6918
-
6919
- // Group files by their canonical name
6920
- const groups = new Map<string, string[]>();
6921
- for (const file of mdFiles) {
6922
- const baseName = file.replace(".md", "");
6923
- // Extract type and name from filename (type-rest-of-name)
6924
- const dashIdx = baseName.indexOf("-");
6925
- if (dashIdx === -1) continue;
6926
- const type = baseName.slice(0, dashIdx);
6927
- const restOfName = baseName.slice(dashIdx + 1);
6928
- const canonical = this.normalizeEntityName(restOfName, type);
6929
-
6930
- if (!groups.has(canonical)) groups.set(canonical, []);
6931
- groups.get(canonical)!.push(file);
6932
- }
6933
-
6934
- // Merge groups with more than one file
6935
- for (const [canonical, files] of groups) {
6936
- if (files.length <= 1) continue;
6937
-
6938
- // Parse all files and merge into a single EntityFile
6939
- const mergedEntity: EntityFile = {
6940
- name: "",
6941
- type: "other",
6942
- created: "",
6943
- updated: "",
6944
- extraFrontmatterLines: [],
6945
- preSectionLines: [],
6946
- facts: [],
6947
- summary: undefined,
6948
- synthesis: undefined,
6949
- synthesisUpdatedAt: undefined,
6950
- synthesisTimelineCount: undefined,
6951
- synthesisStructuredFactCount: undefined,
6952
- synthesisStructuredFactDigest: undefined,
6953
- synthesisVersion: undefined,
6954
- timeline: [],
6955
- relationships: [],
6956
- activity: [],
6957
- aliases: [],
6958
- structuredSections: [],
6959
- extraSections: [],
6960
- };
6961
-
6962
- for (const file of files) {
6963
- const filePath = path.join(this.entitiesDir, file);
6964
- try {
6965
- const content = await this.readStorageSecureFile(filePath);
6966
- const parsed = parseEntityFile(content, this.entitySchemas);
6967
-
6968
- // Prefer specific types over "other"
6969
- if (!mergedEntity.type || mergedEntity.type === "other") {
6970
- mergedEntity.type = parsed.type;
6971
- }
6972
-
6973
- // Keep latest update time
6974
- if (!mergedEntity.updated || compareEntityTimestamps(parsed.updated, mergedEntity.updated) > 0) {
6975
- mergedEntity.updated = parsed.updated;
6976
- }
6977
-
6978
- const parsedCreated = parsed.created || parsed.updated;
6979
- const mergedCreated = mergedEntity.created?.trim() || "";
6980
- const parsedCreatedMs = parsedCreated ? Date.parse(parsedCreated) : Number.NaN;
6981
- const mergedCreatedMs = mergedCreated ? Date.parse(mergedCreated) : Number.NaN;
6982
- const parsedCreatedIsValid = Number.isFinite(parsedCreatedMs);
6983
- const mergedCreatedIsValid = Number.isFinite(mergedCreatedMs);
6984
- if (
6985
- parsedCreated &&
6986
- (
6987
- !mergedCreated
6988
- || (parsedCreatedIsValid && !mergedCreatedIsValid)
6989
- || (
6990
- parsedCreatedIsValid
6991
- && mergedCreatedIsValid
6992
- && parsedCreatedMs < mergedCreatedMs
6993
- )
6994
- || (
6995
- !parsedCreatedIsValid
6996
- && !mergedCreatedIsValid
6997
- && compareEntityTimestamps(parsedCreated, mergedCreated) < 0
6998
- )
6999
- )
7000
- ) {
7001
- mergedEntity.created = parsedCreated;
7002
- }
7003
-
7004
- // Keep longest/best name
7005
- if (parsed.name.length > mergedEntity.name.length) {
7006
- mergedEntity.name = parsed.name;
7007
- }
7008
-
7009
- const parsedSynthesisUpdatedAt = parsed.synthesisUpdatedAt?.trim() || undefined;
7010
- const mergedSynthesisUpdatedAt = mergedEntity.synthesisUpdatedAt?.trim() || undefined;
7011
-
7012
- // Prefer the freshest synthesis/summary available.
7013
- if (
7014
- parsed.synthesis &&
7015
- (!mergedEntity.synthesis
7016
- || (!mergedSynthesisUpdatedAt && Boolean(parsedSynthesisUpdatedAt))
7017
- || (Boolean(mergedSynthesisUpdatedAt)
7018
- && Boolean(parsedSynthesisUpdatedAt)
7019
- && compareEntityTimestamps(parsedSynthesisUpdatedAt, mergedSynthesisUpdatedAt) > 0))
7020
- ) {
7021
- mergedEntity.synthesis = parsed.synthesis;
7022
- mergedEntity.summary = parsed.synthesis;
7023
- mergedEntity.synthesisUpdatedAt = parsedSynthesisUpdatedAt;
7024
- mergedEntity.synthesisTimelineCount = parsed.synthesisTimelineCount;
7025
- mergedEntity.synthesisStructuredFactCount = parsed.synthesisStructuredFactCount;
7026
- mergedEntity.synthesisStructuredFactDigest = parsed.synthesisStructuredFactDigest;
7027
- mergedEntity.synthesisVersion = parsed.synthesisVersion;
7028
- } else if (!mergedEntity.summary && parsed.summary) {
7029
- mergedEntity.summary = parsed.summary;
7030
- mergedEntity.synthesis = parsed.summary;
7031
- mergedEntity.synthesisUpdatedAt = parsedSynthesisUpdatedAt;
7032
- mergedEntity.synthesisTimelineCount = parsed.synthesisTimelineCount;
7033
- mergedEntity.synthesisStructuredFactCount = parsed.synthesisStructuredFactCount;
7034
- mergedEntity.synthesisStructuredFactDigest = parsed.synthesisStructuredFactDigest;
7035
- mergedEntity.synthesisVersion = parsed.synthesisVersion;
7036
- }
7037
-
7038
- // Collect all timeline evidence; facts are derived below.
7039
- mergedEntity.timeline.push(...parsed.timeline);
7040
-
7041
- // Collect relationships (dedup later)
7042
- mergedEntity.relationships.push(...parsed.relationships);
7043
-
7044
- // Collect activity entries
7045
- mergedEntity.activity.push(...parsed.activity);
7046
-
7047
- // Collect aliases
7048
- mergedEntity.aliases.push(...parsed.aliases);
7049
-
7050
- const mergedStructuredSectionMap = new Map(
7051
- (mergedEntity.structuredSections ?? []).map((section) => [section.key, {
7052
- ...section,
7053
- facts: [...section.facts],
7054
- }]),
7055
- );
7056
- for (const section of parsed.structuredSections ?? []) {
7057
- const existingSection = mergedStructuredSectionMap.get(section.key);
7058
- if (!existingSection) {
7059
- mergedStructuredSectionMap.set(section.key, {
7060
- key: section.key,
7061
- title: section.title,
7062
- facts: [...new Set(section.facts.map((fact) => fact.trim()).filter((fact) => fact.length > 0))],
7063
- });
7064
- continue;
7065
- }
7066
-
7067
- const mergedFacts = new Set(existingSection.facts.map((fact) => fact.trim()));
7068
- for (const fact of section.facts) {
7069
- const trimmed = fact.trim();
7070
- if (!trimmed) continue;
7071
- mergedFacts.add(trimmed);
7072
- }
7073
- existingSection.facts = Array.from(mergedFacts);
7074
- if (!existingSection.title.trim() && section.title.trim()) {
7075
- existingSection.title = section.title;
7076
- }
7077
- }
7078
- mergedEntity.structuredSections = Array.from(mergedStructuredSectionMap.values());
7079
-
7080
- // Preserve custom metadata and user-authored freeform content from fragments.
7081
- mergedEntity.extraFrontmatterLines!.push(...(parsed.extraFrontmatterLines ?? []));
7082
- mergedEntity.preSectionLines!.push(...(parsed.preSectionLines ?? []));
7083
- mergedEntity.extraSections!.push(...(parsed.extraSections ?? []).map((section) => ({
7084
- title: section.title,
7085
- lines: [...section.lines],
7086
- })));
7087
- } catch (err) {
7088
- if (err instanceof SecureStoreLockedError) throw err;
7089
- if (!isErrnoCode(err, "ENOENT")) throw err;
7090
- // Skip unreadable
7091
- }
7092
- }
7093
-
7094
- // Deduplicate timeline entries and derive facts from the timeline.
7095
- const timelineKeys = new Set<string>();
7096
- mergedEntity.timeline = mergedEntity.timeline.filter((entry) => {
7097
- const key = JSON.stringify([
7098
- entry.timestamp,
7099
- entry.source ?? "",
7100
- entry.sessionKey ?? "",
7101
- entry.principal ?? "",
7102
- entry.text,
7103
- ]);
7104
- if (timelineKeys.has(key)) return false;
7105
- timelineKeys.add(key);
7106
- return true;
7107
- });
7108
- // Deduplicate relationships by target+label
7109
- const relKeys = new Set<string>();
7110
- mergedEntity.relationships = mergedEntity.relationships.filter((r) => {
7111
- const key = `${r.target}::${r.label}`;
7112
- if (relKeys.has(key)) return false;
7113
- relKeys.add(key);
7114
- return true;
7115
- });
7116
-
7117
- // Sort activity by date descending, deduplicate by date+note
7118
- const actKeys = new Set<string>();
7119
- mergedEntity.activity = mergedEntity.activity
7120
- .filter((a) => {
7121
- const key = `${a.date}::${a.note}`;
7122
- if (actKeys.has(key)) return false;
7123
- actKeys.add(key);
7124
- return true;
7125
- })
7126
- .sort((a, b) => b.date.localeCompare(a.date));
7127
-
7128
- // Deduplicate aliases
7129
- mergedEntity.aliases = [...new Set(mergedEntity.aliases)];
7130
- mergedEntity.structuredSections = sortStructuredSectionsBySchema(
7131
- mergedEntity.type,
7132
- mergedEntity.structuredSections ?? [],
7133
- this.entitySchemas,
7134
- );
7135
- mergedEntity.facts = compileEntityFacts(mergedEntity.timeline, mergedEntity.structuredSections);
7136
-
7137
- const extraSectionKeys = new Set<string>();
7138
- mergedEntity.extraSections = (mergedEntity.extraSections ?? []).filter((section) => {
7139
- const key = `${section.title}::${section.lines.join("\n")}`;
7140
- if (extraSectionKeys.has(key)) return false;
7141
- extraSectionKeys.add(key);
7142
- return true;
7143
- });
7144
-
7145
- // Fallback name from canonical
7146
- if (!mergedEntity.name) {
7147
- const dashIdx = canonical.indexOf("-");
7148
- mergedEntity.name = dashIdx !== -1 ? canonical.slice(dashIdx + 1) : canonical;
7149
- }
7150
-
7151
- mergedEntity.created = mergedEntity.created || mergedEntity.updated || new Date().toISOString();
7152
- mergedEntity.updated = mergedEntity.updated || new Date().toISOString();
7153
-
7154
- const canonicalPath = path.join(this.entitiesDir, `${canonical}.md`);
7155
- await this.writeStorageSecureFile(canonicalPath, serializeEntityFile(mergedEntity, this.entitySchemas));
7156
-
7157
- // Remove non-canonical files
7158
- for (const file of files) {
7159
- const filePath = path.join(this.entitiesDir, file);
7160
- if (filePath !== canonicalPath) {
7161
- try {
7162
- await unlink(filePath);
7163
- merged++;
7164
- log.debug(`merged entity ${file} → ${canonical}.md`);
7165
- } catch {
7166
- // Ignore
7167
- }
7168
- }
7169
- }
7170
- }
7171
- } catch (err) {
7172
- if (err instanceof SecureStoreLockedError || !isErrnoCode(err, "ENOENT")) throw err;
7173
- // Directory doesn't exist yet
7174
- }
7175
-
7176
- return merged;
6095
+ return this.entityStore.mergeFragmentedEntities(
6096
+ );
7177
6097
  }
7178
6098
 
7179
6099
  async cleanExpiredCommitments(decayDays: number): Promise<MemoryFile[]> {