@remnic/core 9.7.15 → 9.9.0

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 (929) hide show
  1. package/dist/access-admin-ops-surface.d.ts +13 -6
  2. package/dist/access-admin-ops-surface.js +85 -71
  3. package/dist/access-audit.js +2 -2
  4. package/dist/access-boundary.d.ts +35 -17
  5. package/dist/access-boundary.js +86 -74
  6. package/dist/access-citation.d.ts +28 -0
  7. package/dist/access-citation.js +9 -0
  8. package/dist/access-cli.js +194 -178
  9. package/dist/access-cli.js.map +1 -1
  10. package/dist/access-errors.d.ts +16 -1
  11. package/dist/access-errors.js +7 -3
  12. package/dist/access-http.d.ts +25 -19
  13. package/dist/access-http.js +100 -87
  14. package/dist/access-identity-continuity-surface.d.ts +13 -8
  15. package/dist/access-identity-continuity-surface.js +85 -71
  16. package/dist/access-lcm-surface.d.ts +16 -9
  17. package/dist/access-lcm-surface.js +85 -71
  18. package/dist/access-mcp.d.ts +23 -16
  19. package/dist/access-mcp.js +96 -84
  20. package/dist/access-memory-search-fanout.d.ts +93 -0
  21. package/dist/access-memory-search-fanout.js +18 -0
  22. package/dist/access-observe-write-surface.d.ts +26 -8
  23. package/dist/access-observe-write-surface.js +85 -71
  24. package/dist/access-operations-batch.js +92 -80
  25. package/dist/access-operations.d.ts +29 -22
  26. package/dist/access-operations.js +94 -82
  27. package/dist/access-recall-concurrency.d.ts +250 -0
  28. package/dist/access-recall-concurrency.js +157 -0
  29. package/dist/access-recall-response.d.ts +130 -0
  30. package/dist/access-recall-response.js +147 -0
  31. package/dist/access-recall-surface.d.ts +15 -7
  32. package/dist/access-recall-surface.js +85 -71
  33. package/dist/access-schema.d.ts +98 -98
  34. package/dist/access-schema.js +8 -6
  35. package/dist/{access-service-DpcYZ9Nl.d.ts → access-service-DbZ1G3zV.d.ts} +29 -36
  36. package/dist/access-service.d.ts +13 -6
  37. package/dist/access-service.js +89 -72
  38. package/dist/access-surface-catalog.d.ts +23 -16
  39. package/dist/access-surface-catalog.js +1 -0
  40. package/dist/access-surface-catalog.js.map +1 -1
  41. package/dist/access-token-capabilities.d.ts +2 -1
  42. package/dist/access-token-capabilities.js +4 -2
  43. package/dist/action-confidence.d.ts +3 -1
  44. package/dist/active-memory-bridge.d.ts +3 -1
  45. package/dist/active-memory-bridge.js +4 -4
  46. package/dist/active-recall.d.ts +4 -1
  47. package/dist/active-recall.js +23 -13
  48. package/dist/active-recall.js.map +1 -1
  49. package/dist/adapters/index.js +4 -4
  50. package/dist/adapters/registry.js +2 -2
  51. package/dist/{auto-sync-AAMTVZMK.js → auto-sync-UZG3XQZ5.js} +14 -8
  52. package/dist/{auto-sync-AAMTVZMK.js.map → auto-sync-UZG3XQZ5.js.map} +1 -1
  53. package/dist/behavior-learner.d.ts +3 -1
  54. package/dist/behavior-signals.d.ts +3 -1
  55. package/dist/bootstrap.d.ts +12 -7
  56. package/dist/bootstrap.js +2 -2
  57. package/dist/bounded-jsonl-state.d.ts +54 -0
  58. package/dist/bounded-jsonl-state.js +10 -0
  59. package/dist/boxes.js +2 -2
  60. package/dist/briefing.d.ts +5 -2
  61. package/dist/briefing.js +34 -24
  62. package/dist/buffer-surprise-report.d.ts +3 -1
  63. package/dist/buffer.d.ts +60 -2
  64. package/dist/buffer.js +3 -3
  65. package/dist/bulk-import/index.d.ts +5 -3
  66. package/dist/calibration.d.ts +3 -1
  67. package/dist/calibration.js +6 -6
  68. package/dist/capabilities.d.ts +7 -7
  69. package/dist/capabilities.js +1 -1
  70. package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-YO5QJ6L3.js} +3 -3
  71. package/dist/capsule-crypto-YO5QJ6L3.js.map +1 -0
  72. package/dist/{catalog-C9444P10.d.ts → catalog-BWqHb5HT.d.ts} +1 -1
  73. package/dist/causal-behavior.d.ts +3 -1
  74. package/dist/causal-behavior.js +6 -6
  75. package/dist/causal-chain.js +4 -4
  76. package/dist/causal-consolidation.d.ts +3 -1
  77. package/dist/causal-consolidation.js +43 -34
  78. package/dist/causal-consolidation.js.map +1 -1
  79. package/dist/causal-retrieval.js +6 -6
  80. package/dist/causal-trajectory-graph.d.ts +3 -1
  81. package/dist/causal-trajectory-graph.js +1 -1
  82. package/dist/causal-trajectory.js +1 -1
  83. package/dist/{chunk-A3GOMRJG.js → chunk-23L3IXFD.js} +2 -2
  84. package/dist/chunk-2CGLBUW3.js +15 -0
  85. package/dist/chunk-2CGLBUW3.js.map +1 -0
  86. package/dist/{chunk-MRHFJV44.js → chunk-2DQ2XREU.js} +2 -2
  87. package/dist/{chunk-L2B3EE7B.js → chunk-2QMKMVHH.js} +2 -2
  88. package/dist/{chunk-VJVNLNDQ.js → chunk-2UFO7X4Q.js} +7 -7
  89. package/dist/{chunk-2MR3MFQB.js → chunk-2VEOKUPP.js} +3 -3
  90. package/dist/chunk-2Z24E5W2.js +22 -0
  91. package/dist/chunk-2Z24E5W2.js.map +1 -0
  92. package/dist/{chunk-RHOZ5LLO.js → chunk-347WD2LE.js} +2 -2
  93. package/dist/{chunk-HDTQVNWD.js → chunk-3QAH2S33.js} +3 -3
  94. package/dist/{chunk-37CTGERM.js → chunk-3WCASNQZ.js} +12 -1
  95. package/dist/chunk-3WCASNQZ.js.map +1 -0
  96. package/dist/{chunk-LRNMRXJO.js → chunk-3Y224NYC.js} +3 -3
  97. package/dist/{chunk-3IY64MV6.js → chunk-44XZ75OQ.js} +44 -10
  98. package/dist/chunk-44XZ75OQ.js.map +1 -0
  99. package/dist/{chunk-VFSP73LP.js → chunk-45GF7Y3V.js} +2 -2
  100. package/dist/{chunk-K7LOWSLU.js → chunk-4BJQODZF.js} +2479 -1520
  101. package/dist/chunk-4BJQODZF.js.map +1 -0
  102. package/dist/{chunk-JREB6UFH.js → chunk-57OWKFUQ.js} +17 -6
  103. package/dist/chunk-57OWKFUQ.js.map +1 -0
  104. package/dist/{chunk-OEHCMALU.js → chunk-5I4M26Q7.js} +5 -5
  105. package/dist/{chunk-2AZXIZEN.js → chunk-5Q7EVV4X.js} +6 -6
  106. package/dist/{chunk-LC7OODWZ.js → chunk-5SB7SC2G.js} +80 -49
  107. package/dist/chunk-5SB7SC2G.js.map +1 -0
  108. package/dist/{chunk-PUBTHW5O.js → chunk-6DRPPQEJ.js} +2 -2
  109. package/dist/{chunk-NWTGD4AN.js → chunk-6KIQT7RG.js} +10 -9
  110. package/dist/chunk-6KIQT7RG.js.map +1 -0
  111. package/dist/{chunk-FDCTQWLW.js → chunk-6O22TZSB.js} +3 -3
  112. package/dist/{chunk-ZQ7UBUZ6.js → chunk-6QYST5XN.js} +6 -6
  113. package/dist/{chunk-W4PGTAS2.js → chunk-6WAFYNVT.js} +3 -3
  114. package/dist/chunk-6ZMAMSHS.js +20 -0
  115. package/dist/chunk-6ZMAMSHS.js.map +1 -0
  116. package/dist/{chunk-R5VAVVHP.js → chunk-72A4L5JJ.js} +2 -2
  117. package/dist/{chunk-X64YLC24.js → chunk-72VAXGEI.js} +5 -13
  118. package/dist/chunk-72VAXGEI.js.map +1 -0
  119. package/dist/{chunk-IKQTFQMI.js → chunk-755Q3DGF.js} +22 -3
  120. package/dist/chunk-755Q3DGF.js.map +1 -0
  121. package/dist/{chunk-D7FOKIY5.js → chunk-7AMFUATK.js} +12 -9
  122. package/dist/{chunk-D7FOKIY5.js.map → chunk-7AMFUATK.js.map} +1 -1
  123. package/dist/{chunk-BFVEEB7W.js → chunk-7ON2JW73.js} +57 -22
  124. package/dist/chunk-7ON2JW73.js.map +1 -0
  125. package/dist/chunk-7RNBVZZO.js +270 -0
  126. package/dist/chunk-7RNBVZZO.js.map +1 -0
  127. package/dist/{chunk-3FAMU5TX.js → chunk-A6USBZTO.js} +2 -2
  128. package/dist/{chunk-ZHIVCODT.js → chunk-A72SWTWW.js} +7 -7
  129. package/dist/{chunk-FC77VUX5.js → chunk-A7Q43EUZ.js} +2 -2
  130. package/dist/{chunk-UJBESW6X.js → chunk-AE7WMV4V.js} +7 -7
  131. package/dist/{chunk-J2HSAU72.js → chunk-AIK5W3IW.js} +29 -1
  132. package/dist/chunk-AIK5W3IW.js.map +1 -0
  133. package/dist/{chunk-A5UXZRRM.js → chunk-AJ7QCSIS.js} +2 -2
  134. package/dist/{chunk-FNMFJWBY.js → chunk-ANEUPF6O.js} +5 -5
  135. package/dist/chunk-AREGMX2J.js +50 -0
  136. package/dist/chunk-AREGMX2J.js.map +1 -0
  137. package/dist/chunk-AVPXXV6J.js +448 -0
  138. package/dist/chunk-AVPXXV6J.js.map +1 -0
  139. package/dist/{chunk-2A3OXAP2.js → chunk-B7RWBZXW.js} +2 -2
  140. package/dist/{chunk-QKQYXZVS.js → chunk-BI2OJF6J.js} +1819 -1481
  141. package/dist/chunk-BI2OJF6J.js.map +1 -0
  142. package/dist/{chunk-GWTFLF2P.js → chunk-BJMBJZ2Y.js} +59 -59
  143. package/dist/chunk-BJMBJZ2Y.js.map +1 -0
  144. package/dist/{chunk-74F5STS4.js → chunk-BLAKS3CP.js} +4 -4
  145. package/dist/{chunk-M7VGG3ID.js → chunk-BV5WMSIJ.js} +9 -9
  146. package/dist/{chunk-2BFLDH5F.js → chunk-BXLOS5AJ.js} +2 -2
  147. package/dist/{chunk-UXKMMNJD.js → chunk-C23BYTJV.js} +6 -6
  148. package/dist/{chunk-JZHHREBL.js → chunk-C3AKM55W.js} +2 -2
  149. package/dist/{chunk-RP7YXA67.js → chunk-C5JF6EZ7.js} +2 -2
  150. package/dist/{chunk-T7Y5KPLN.js → chunk-C6XBRYVD.js} +16 -15
  151. package/dist/chunk-C6XBRYVD.js.map +1 -0
  152. package/dist/{chunk-FTYDWKO6.js → chunk-CEN6DW6X.js} +12 -2
  153. package/dist/chunk-CEN6DW6X.js.map +1 -0
  154. package/dist/chunk-CGASUJVP.js +421 -0
  155. package/dist/chunk-CGASUJVP.js.map +1 -0
  156. package/dist/{chunk-EHCONI6E.js → chunk-CQMEM5CC.js} +2 -2
  157. package/dist/{chunk-PBNZVPFS.js → chunk-CXZQOVIE.js} +12 -12
  158. package/dist/chunk-D3V3UMET.js +44 -0
  159. package/dist/chunk-D3V3UMET.js.map +1 -0
  160. package/dist/{chunk-7N2RUBAP.js → chunk-DDA47TES.js} +2 -2
  161. package/dist/{chunk-64SE4X5G.js → chunk-DEBHXHJR.js} +3 -3
  162. package/dist/{chunk-EHK6KZHK.js → chunk-DNHVPZOD.js} +2 -2
  163. package/dist/{chunk-437S3G37.js → chunk-DQEMWVMT.js} +2 -2
  164. package/dist/{chunk-LZZNTPLR.js → chunk-DX3XRGLH.js} +2 -2
  165. package/dist/chunk-EA2H5LML.js +72 -0
  166. package/dist/chunk-EA2H5LML.js.map +1 -0
  167. package/dist/{chunk-INELTPRG.js → chunk-EEC7UJPX.js} +103 -106
  168. package/dist/chunk-EEC7UJPX.js.map +1 -0
  169. package/dist/{chunk-KSNVDPDF.js → chunk-EKBNDKDY.js} +21 -20
  170. package/dist/chunk-EKBNDKDY.js.map +1 -0
  171. package/dist/{chunk-YF6FMLBT.js → chunk-EQWEIIKQ.js} +64 -34
  172. package/dist/chunk-EQWEIIKQ.js.map +1 -0
  173. package/dist/{chunk-DZKHSDOM.js → chunk-ES36HTSC.js} +4 -4
  174. package/dist/{chunk-TN7THW5U.js → chunk-ET356KSB.js} +2 -2
  175. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  176. package/dist/{chunk-DMSDUGDM.js → chunk-F7GAIWFA.js} +20 -20
  177. package/dist/{chunk-G6XXLKC5.js → chunk-FFPJINJE.js} +6 -6
  178. package/dist/chunk-FMLKOL2Z.js +636 -0
  179. package/dist/chunk-FMLKOL2Z.js.map +1 -0
  180. package/dist/chunk-FQC3DHH2.js +20 -0
  181. package/dist/chunk-FQC3DHH2.js.map +1 -0
  182. package/dist/{chunk-G4CN3ZXL.js → chunk-FUTF72MJ.js} +15 -15
  183. package/dist/chunk-GCASGYIO.js +71 -0
  184. package/dist/chunk-GCASGYIO.js.map +1 -0
  185. package/dist/{chunk-HF77M34J.js → chunk-GFPZS3F5.js} +8 -8
  186. package/dist/{chunk-BCDKBDSY.js → chunk-GNVZQ5JN.js} +99 -41
  187. package/dist/chunk-GNVZQ5JN.js.map +1 -0
  188. package/dist/{chunk-G6NKALRN.js → chunk-H4WG3UQZ.js} +2 -2
  189. package/dist/{chunk-UDQUSU27.js → chunk-HNZKVLAN.js} +2 -2
  190. package/dist/{chunk-5EZXHU36.js → chunk-HYQJQGD3.js} +2 -2
  191. package/dist/{chunk-PLQE6MJA.js → chunk-IFH6LWTO.js} +50 -8
  192. package/dist/chunk-IFH6LWTO.js.map +1 -0
  193. package/dist/{chunk-RULE4VG5.js → chunk-ILGC6JH6.js} +21 -6
  194. package/dist/chunk-ILGC6JH6.js.map +1 -0
  195. package/dist/chunk-IUM3VSHI.js +113 -0
  196. package/dist/chunk-IUM3VSHI.js.map +1 -0
  197. package/dist/{chunk-TRWPCR6P.js → chunk-JECBQBEX.js} +3 -3
  198. package/dist/{chunk-E6CBU7XP.js → chunk-JYIFGGEM.js} +2 -2
  199. package/dist/{chunk-VA4WSW54.js → chunk-JYIJNNN3.js} +3 -3
  200. package/dist/{chunk-L5HJQC36.js → chunk-K2TDK7GG.js} +3 -3
  201. package/dist/{chunk-KDXVOFYY.js → chunk-KAVBJSBQ.js} +4 -4
  202. package/dist/{chunk-BS4SGLWV.js → chunk-KF5EUVKQ.js} +16 -6
  203. package/dist/chunk-KF5EUVKQ.js.map +1 -0
  204. package/dist/chunk-KFZY2OFH.js +42 -0
  205. package/dist/chunk-KFZY2OFH.js.map +1 -0
  206. package/dist/{chunk-4OKLXWDZ.js → chunk-KNB7IBBU.js} +2 -2
  207. package/dist/{chunk-QLULYKWZ.js → chunk-KSXJOC4F.js} +2 -2
  208. package/dist/{chunk-2S6QHZGJ.js → chunk-L2DPF4DF.js} +1 -1
  209. package/dist/{chunk-JI5AVXYO.js → chunk-LEPOP46W.js} +6 -6
  210. package/dist/{chunk-O7BBFJLP.js → chunk-LTAVSOZW.js} +2 -2
  211. package/dist/{chunk-E3VODCC3.js → chunk-LUDXM7MD.js} +14 -2
  212. package/dist/{chunk-E3VODCC3.js.map → chunk-LUDXM7MD.js.map} +1 -1
  213. package/dist/{chunk-2IZAGAJR.js → chunk-LULR4IUW.js} +3 -3
  214. package/dist/{chunk-QQGPUKMT.js → chunk-MC5Q3J7S.js} +398 -7
  215. package/dist/chunk-MC5Q3J7S.js.map +1 -0
  216. package/dist/{chunk-WMWVO45V.js → chunk-MC6XO7T4.js} +2 -2
  217. package/dist/chunk-MKQBXQKU.js +107 -0
  218. package/dist/chunk-MKQBXQKU.js.map +1 -0
  219. package/dist/{chunk-7PND4PUC.js → chunk-MMGGVZ4Q.js} +29 -9
  220. package/dist/{chunk-7PND4PUC.js.map → chunk-MMGGVZ4Q.js.map} +1 -1
  221. package/dist/{chunk-D6RIFG4O.js → chunk-MNB5RMAY.js} +2 -2
  222. package/dist/{chunk-Q5WG43OP.js → chunk-MYFQ3ED4.js} +7 -7
  223. package/dist/{chunk-V3UE5I5F.js → chunk-MYVPLJOO.js} +2 -2
  224. package/dist/chunk-NINSR6LX.js +83 -0
  225. package/dist/chunk-NINSR6LX.js.map +1 -0
  226. package/dist/{chunk-MTWKDOVO.js → chunk-NLE6TSLG.js} +2 -2
  227. package/dist/{chunk-OAZVR2BV.js → chunk-OGCAYLNA.js} +8 -6
  228. package/dist/{chunk-OAZVR2BV.js.map → chunk-OGCAYLNA.js.map} +1 -1
  229. package/dist/{chunk-DBRYEIGY.js → chunk-OJP3N7UK.js} +2 -2
  230. package/dist/{chunk-VDW4VU7B.js → chunk-OQJIB32H.js} +28 -23
  231. package/dist/chunk-OQJIB32H.js.map +1 -0
  232. package/dist/{chunk-SMX5267P.js → chunk-OZKOPQCM.js} +2 -2
  233. package/dist/{chunk-JCK7V6SN.js → chunk-P65YJEEL.js} +2 -2
  234. package/dist/{chunk-MTR7BZQN.js → chunk-PBGWYM3H.js} +3 -3
  235. package/dist/chunk-PIG4WJFQ.js +87 -0
  236. package/dist/chunk-PIG4WJFQ.js.map +1 -0
  237. package/dist/chunk-PJSK6OJM.js +201 -0
  238. package/dist/chunk-PJSK6OJM.js.map +1 -0
  239. package/dist/{chunk-OVJ2HIPE.js → chunk-PMUD7ERF.js} +158 -125
  240. package/dist/chunk-PMUD7ERF.js.map +1 -0
  241. package/dist/chunk-PQVCH2AE.js +76 -0
  242. package/dist/chunk-PQVCH2AE.js.map +1 -0
  243. package/dist/{chunk-AXTXN4T6.js → chunk-PWFCHOB3.js} +2 -2
  244. package/dist/{chunk-FDPZVY6D.js → chunk-QFN4U7AG.js} +2 -2
  245. package/dist/{chunk-K2ZLHESQ.js → chunk-QIEUU4UK.js} +2 -2
  246. package/dist/{chunk-DYP3YYJJ.js → chunk-QJAQNXWI.js} +39 -5
  247. package/dist/chunk-QJAQNXWI.js.map +1 -0
  248. package/dist/{chunk-3D6AFGQR.js → chunk-QVT3E4CE.js} +7 -7
  249. package/dist/{chunk-FIJUY7GG.js → chunk-R26ZMXR2.js} +23 -23
  250. package/dist/{chunk-FIJUY7GG.js.map → chunk-R26ZMXR2.js.map} +1 -1
  251. package/dist/chunk-R3DJVOSM.js +34 -0
  252. package/dist/chunk-R3DJVOSM.js.map +1 -0
  253. package/dist/{chunk-SFQ6QNL7.js → chunk-R6YJI3HE.js} +2 -1
  254. package/dist/{chunk-5CJVLG3H.js → chunk-RFDD3B7W.js} +5 -5
  255. package/dist/{chunk-JBPKEARU.js → chunk-RKNJBZ55.js} +4 -4
  256. package/dist/chunk-ROI7BK7H.js +88 -0
  257. package/dist/chunk-ROI7BK7H.js.map +1 -0
  258. package/dist/{chunk-FNZQGN76.js → chunk-S2BBISHU.js} +4 -4
  259. package/dist/{chunk-IALLX7EE.js → chunk-S2FSBU4H.js} +3 -3
  260. package/dist/{chunk-BFPETA3S.js → chunk-SCCO46KZ.js} +3 -2
  261. package/dist/chunk-SCCO46KZ.js.map +1 -0
  262. package/dist/chunk-SK7AMF2J.js +168 -0
  263. package/dist/chunk-SK7AMF2J.js.map +1 -0
  264. package/dist/{chunk-KSBEEMQF.js → chunk-TG6IHVE2.js} +151 -6
  265. package/dist/chunk-TG6IHVE2.js.map +1 -0
  266. package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
  267. package/dist/{chunk-DPAENM7O.js → chunk-U3ZAVRP3.js} +2 -2
  268. package/dist/{chunk-ODPLEWB6.js → chunk-UFGFMWAD.js} +47 -17
  269. package/dist/chunk-UFGFMWAD.js.map +1 -0
  270. package/dist/{chunk-4KJLYTLS.js → chunk-V57NLO5P.js} +2 -2
  271. package/dist/{chunk-FLVL4L3L.js → chunk-VDX2J7OX.js} +2 -2
  272. package/dist/{chunk-AJLLIIML.js → chunk-W4FBGYGK.js} +1 -1
  273. package/dist/{chunk-TXLGHDHS.js → chunk-W4XJFLAL.js} +53 -32
  274. package/dist/chunk-W4XJFLAL.js.map +1 -0
  275. package/dist/{chunk-6SXVCD7W.js → chunk-W7IGLX4G.js} +2 -2
  276. package/dist/{chunk-KQDNAXOM.js → chunk-WG5HSQ4P.js} +19 -6
  277. package/dist/chunk-WG5HSQ4P.js.map +1 -0
  278. package/dist/{chunk-LTJAMRGI.js → chunk-WL5EBRDH.js} +17 -3
  279. package/dist/chunk-WL5EBRDH.js.map +1 -0
  280. package/dist/{chunk-KAWTOYAW.js → chunk-WLZYGLJ4.js} +2 -2
  281. package/dist/{chunk-RO4LQGT3.js → chunk-WON4APQY.js} +4 -4
  282. package/dist/{chunk-PQVK2S3J.js → chunk-WZFAPHIK.js} +6 -6
  283. package/dist/{chunk-KQPJLK3X.js → chunk-XL24QQ63.js} +2 -2
  284. package/dist/{chunk-LZOV7MX7.js → chunk-XQDRKZ7V.js} +6476 -5747
  285. package/dist/chunk-XQDRKZ7V.js.map +1 -0
  286. package/dist/{chunk-RVZRYUQH.js → chunk-YROFNEG5.js} +7 -7
  287. package/dist/{chunk-RAR2BSEM.js → chunk-YV32N67O.js} +1 -1
  288. package/dist/chunk-YV32N67O.js.map +1 -0
  289. package/dist/{chunk-PBSAPHHA.js → chunk-YVKF3HEW.js} +1 -1
  290. package/dist/chunk-YVKF3HEW.js.map +1 -0
  291. package/dist/{chunk-VRAD2FGO.js → chunk-Z54EM5YZ.js} +12 -12
  292. package/dist/{chunk-2VN4JAX7.js → chunk-ZOKZIR5K.js} +95 -33
  293. package/dist/chunk-ZOKZIR5K.js.map +1 -0
  294. package/dist/{chunk-DCFV7A7H.js → chunk-ZVAYK7GU.js} +101 -91
  295. package/dist/chunk-ZVAYK7GU.js.map +1 -0
  296. package/dist/{cli-Cd5fMmhq.d.ts → cli-5tNLVTvJ.d.ts} +28 -12
  297. package/dist/cli.d.ts +16 -9
  298. package/dist/cli.js +142 -127
  299. package/dist/compounding/engine.d.ts +5 -2
  300. package/dist/compounding/engine.js +36 -26
  301. package/dist/compounding/preference-consolidator.d.ts +3 -1
  302. package/dist/compounding/preference-consolidator.js +1 -1
  303. package/dist/compression-optimizer.d.ts +3 -1
  304. package/dist/config-qmd.d.ts +8 -0
  305. package/dist/config-qmd.js +20 -0
  306. package/dist/config-qmd.js.map +1 -0
  307. package/dist/config.d.ts +3 -1
  308. package/dist/config.js +15 -10
  309. package/dist/connectors/codex-materialize-runner.d.ts +3 -1
  310. package/dist/connectors/codex-materialize-runner.js +36 -26
  311. package/dist/connectors/codex-materialize.d.ts +3 -1
  312. package/dist/connectors/codex-materialize.js +2 -2
  313. package/dist/connectors/index.d.ts +3 -1
  314. package/dist/connectors/index.js +41 -32
  315. package/dist/consolidation-provenance-check.d.ts +5 -2
  316. package/dist/consolidation-undo.d.ts +5 -2
  317. package/dist/contradiction/index.d.ts +5 -2
  318. package/dist/contradiction/index.js +10 -4
  319. package/dist/{contradiction-scan-J57T5QV7.js → contradiction-scan-RJBABNRH.js} +4 -4
  320. package/dist/contradiction-scan-RJBABNRH.js.map +1 -0
  321. package/dist/conversation-index/backend.d.ts +3 -1
  322. package/dist/conversation-index/backend.js +5 -5
  323. package/dist/conversation-index/chunker.d.ts +3 -1
  324. package/dist/conversation-index/cleanup.js +2 -2
  325. package/dist/conversation-index/faiss-adapter.d.ts +3 -1
  326. package/dist/conversation-index/faiss-adapter.js +2 -2
  327. package/dist/conversation-index/indexer.d.ts +3 -1
  328. package/dist/conversation-index/indexer.js +2 -2
  329. package/dist/conversation-index/search.d.ts +3 -1
  330. package/dist/conversation-index/search.js +2 -2
  331. package/dist/cross-namespace-budget.d.ts +45 -1
  332. package/dist/cross-namespace-budget.js +5 -3
  333. package/dist/dashboard-runtime.js +2 -2
  334. package/dist/day-summary.d.ts +3 -1
  335. package/dist/day-summary.js +3 -3
  336. package/dist/delinearize.d.ts +3 -1
  337. package/dist/direct-answer-wiring.d.ts +3 -1
  338. package/dist/direct-answer.d.ts +3 -1
  339. package/dist/{dreams-ledger-6UODQOX6.js → dreams-ledger-567VKVOU.js} +4 -2
  340. package/dist/dreams-ledger-567VKVOU.js.map +1 -0
  341. package/dist/embedding-fallback.d.ts +3 -1
  342. package/dist/embedding-fallback.js +3 -3
  343. package/dist/enrichment/index.d.ts +3 -1
  344. package/dist/entity-retrieval.d.ts +5 -2
  345. package/dist/entity-retrieval.js +35 -26
  346. package/dist/entity-schema.d.ts +3 -1
  347. package/dist/explicit-capture.d.ts +13 -8
  348. package/dist/explicit-capture.js +7 -3
  349. package/dist/extraction-faithfulness.d.ts +3 -1
  350. package/dist/extraction-faithfulness.js +5 -5
  351. package/dist/extraction-judge-telemetry.d.ts +3 -1
  352. package/dist/extraction-judge-telemetry.js +2 -2
  353. package/dist/extraction-judge-training.d.ts +3 -1
  354. package/dist/extraction-judge-training.js +2 -2
  355. package/dist/extraction-judge.d.ts +3 -1
  356. package/dist/extraction-judge.js +5 -5
  357. package/dist/extraction.d.ts +3 -1
  358. package/dist/extraction.js +13 -12
  359. package/dist/faithfulness-config.js +3 -2
  360. package/dist/fallback-llm.d.ts +3 -1
  361. package/dist/fallback-llm.js +4 -4
  362. package/dist/{first-start-migration-TI67YMLG.js → first-start-migration-CSRHCJIQ.js} +2 -2
  363. package/dist/{forget-CQCLB5LB.js → forget-HRPLVB3W.js} +5 -5
  364. package/dist/graph-dashboard-diff.d.ts +3 -1
  365. package/dist/graph-dashboard-key.d.ts +3 -1
  366. package/dist/graph-dashboard-parser.d.ts +3 -1
  367. package/dist/{graph-edge-decay-IOGBBAGC.js → graph-edge-decay-Z6ENALU2.js} +3 -3
  368. package/dist/graph-edge-decay-Z6ENALU2.js.map +1 -0
  369. package/dist/graph-edge-reinforcement.d.ts +3 -1
  370. package/dist/graph-snapshot.d.ts +3 -1
  371. package/dist/graph-snapshot.js +2 -2
  372. package/dist/graph.d.ts +3 -1
  373. package/dist/graph.js +1 -1
  374. package/dist/identity-continuity.d.ts +1 -2
  375. package/dist/importance.d.ts +3 -1
  376. package/dist/importers/index.d.ts +3 -1
  377. package/dist/in-flight-reads.d.ts +3 -1
  378. package/dist/index.d.ts +473 -464
  379. package/dist/index.js +287 -239
  380. package/dist/intent.d.ts +3 -1
  381. package/dist/lcm/archive.js +2 -2
  382. package/dist/lcm/engine.d.ts +3 -1
  383. package/dist/lcm/engine.js +6 -6
  384. package/dist/lcm/index.d.ts +3 -1
  385. package/dist/lcm/index.js +13 -13
  386. package/dist/lcm/schema.js +2 -2
  387. package/dist/lcm/summarizer.js +3 -3
  388. package/dist/lcm/tools.d.ts +3 -1
  389. package/dist/lifecycle.d.ts +3 -1
  390. package/dist/live-connectors-runner.d.ts +3 -1
  391. package/dist/local-llm.d.ts +5 -2
  392. package/dist/local-llm.js +3 -3
  393. package/dist/local-model-endpoint.d.ts +3 -1
  394. package/dist/logger.d.ts +3 -1
  395. package/dist/logger.js +7 -3
  396. package/dist/maintenance/archive-observations.js +2 -2
  397. package/dist/maintenance/memory-governance.d.ts +3 -1
  398. package/dist/maintenance/memory-governance.js +35 -25
  399. package/dist/maintenance/migrate-observations.js +3 -3
  400. package/dist/maintenance/observation-ledger-utils.js +2 -2
  401. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +102 -1
  402. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +37 -25
  403. package/dist/maintenance/rebuild-memory-projection.js +38 -28
  404. package/dist/maintenance/rebuild-observations.js +3 -3
  405. package/dist/mcp-memory-inspector-app.d.ts +22 -15
  406. package/dist/memory-action-policy.d.ts +3 -1
  407. package/dist/memory-cache.d.ts +3 -1
  408. package/dist/memory-lifecycle-ledger-utils.d.ts +25 -2
  409. package/dist/memory-lifecycle-ledger-utils.js +11 -1
  410. package/dist/memory-projection-store.d.ts +3 -1
  411. package/dist/memory-projection-store.js +2 -2
  412. package/dist/memory-provenance.d.ts +3 -1
  413. package/dist/memory-worth-outcomes.d.ts +5 -2
  414. package/dist/model-registry.js +2 -2
  415. package/dist/models-json.d.ts +3 -1
  416. package/dist/models-json.js +2 -2
  417. package/dist/namespaces/migrate.d.ts +6 -3
  418. package/dist/namespaces/migrate.js +51 -42
  419. package/dist/namespaces/principal.d.ts +27 -2
  420. package/dist/namespaces/principal.js +6 -2
  421. package/dist/namespaces/search.d.ts +16 -2
  422. package/dist/namespaces/search.js +24 -20
  423. package/dist/namespaces/storage.d.ts +24 -3
  424. package/dist/namespaces/storage.js +35 -27
  425. package/dist/native-knowledge.d.ts +3 -1
  426. package/dist/native-knowledge.js +2 -2
  427. package/dist/negative.js +2 -2
  428. package/dist/offline-sync-exclude-globs.d.ts +13 -0
  429. package/dist/offline-sync-exclude-globs.js +10 -0
  430. package/dist/offline-sync-exclude-globs.js.map +1 -0
  431. package/dist/offline-sync-impression-drain.d.ts +78 -0
  432. package/dist/offline-sync-impression-drain.js +37 -0
  433. package/dist/offline-sync-impression-drain.js.map +1 -0
  434. package/dist/offline-sync.js +4 -2
  435. package/dist/operator-toolkit.d.ts +5 -2
  436. package/dist/operator-toolkit.js +69 -54
  437. package/dist/orchestration/compression-guideline-coordinator.d.ts +5 -2
  438. package/dist/orchestration/compression-guideline-coordinator.js +3 -3
  439. package/dist/orchestration/maintenance.d.ts +121 -2
  440. package/dist/orchestration/maintenance.js +44 -31
  441. package/dist/{orchestrator-Bp_jeBHK.d.ts → orchestrator-CQJ8U7En.d.ts} +85 -79
  442. package/dist/orchestrator.d.ts +13 -8
  443. package/dist/orchestrator.js +194 -177
  444. package/dist/patterns-cli.d.ts +3 -1
  445. package/dist/{pipeline-BAj5bx7g.d.ts → pipeline-Cn-kvYeV.d.ts} +1 -1
  446. package/dist/policy-runtime.d.ts +3 -1
  447. package/dist/prioritized-embed.d.ts +19 -0
  448. package/dist/prioritized-embed.js +8 -0
  449. package/dist/prioritized-embed.js.map +1 -0
  450. package/dist/proactive-contention.d.ts +28 -0
  451. package/dist/proactive-contention.js +8 -0
  452. package/dist/proactive-contention.js.map +1 -0
  453. package/dist/profiling.js +2 -2
  454. package/dist/provenance.d.ts +3 -1
  455. package/dist/provenance.js +3 -2
  456. package/dist/{purge-NDWV6WYD.js → purge-IRF47ZTX.js} +3 -3
  457. package/dist/{qmd-_MDSlj1m.d.ts → qmd-oh9VXz1d.d.ts} +11 -1
  458. package/dist/qmd-preflight.d.ts +5 -2
  459. package/dist/qmd-recall-cache.d.ts +3 -1
  460. package/dist/qmd-status.d.ts +57 -0
  461. package/dist/qmd-status.js +18 -0
  462. package/dist/qmd-status.js.map +1 -0
  463. package/dist/qmd.d.ts +5 -2
  464. package/dist/qmd.js +7 -3
  465. package/dist/recall-concurrency-config.d.ts +16 -0
  466. package/dist/recall-concurrency-config.js +10 -0
  467. package/dist/recall-concurrency-config.js.map +1 -0
  468. package/dist/recall-disclosure-escalation.d.ts +3 -1
  469. package/dist/recall-explain-renderer.d.ts +4 -1
  470. package/dist/recall-explain-renderer.js +3 -3
  471. package/dist/recall-memory-map.d.ts +26 -0
  472. package/dist/recall-memory-map.js +18 -0
  473. package/dist/recall-memory-map.js.map +1 -0
  474. package/dist/recall-planner-llm.d.ts +3 -1
  475. package/dist/recall-planner-llm.js +4 -4
  476. package/dist/recall-qos.js +2 -2
  477. package/dist/recall-state.d.ts +163 -2
  478. package/dist/recall-state.js +6 -3
  479. package/dist/recall-tag-filter.d.ts +3 -1
  480. package/dist/recall-timings.d.ts +11 -2
  481. package/dist/recall-timings.js +3 -1
  482. package/dist/recall-xray-cli.d.ts +3 -1
  483. package/dist/recall-xray-cli.js +4 -4
  484. package/dist/recall-xray-renderer.d.ts +3 -1
  485. package/dist/recall-xray-renderer.js +3 -3
  486. package/dist/recall-xray.d.ts +3 -1
  487. package/dist/recall-xray.js +2 -2
  488. package/dist/relevance.js +3 -3
  489. package/dist/replay/normalizers/chatgpt.d.ts +3 -1
  490. package/dist/replay/normalizers/claude.d.ts +3 -1
  491. package/dist/replay/normalizers/openclaw.d.ts +3 -1
  492. package/dist/replay/normalizers/shared.d.ts +3 -1
  493. package/dist/replay/runner.d.ts +3 -1
  494. package/dist/replay/types.d.ts +3 -1
  495. package/dist/rerank.d.ts +8 -1
  496. package/dist/rerank.js +3 -1
  497. package/dist/resolution-3K27T4VO.js +18 -0
  498. package/dist/resolution-3K27T4VO.js.map +1 -0
  499. package/dist/resolve-auth-token.d.ts +3 -1
  500. package/dist/resolve-provider-secret.js +2 -2
  501. package/dist/resume-bundles.js +19 -14
  502. package/dist/retrieval-agents.d.ts +19 -16
  503. package/dist/retrieval-agents.js +3 -3
  504. package/dist/retrieval-tiers.d.ts +3 -1
  505. package/dist/routing/engine.d.ts +3 -1
  506. package/dist/routing/store.d.ts +3 -1
  507. package/dist/routing/store.js +2 -2
  508. package/dist/salvage-envelope.d.ts +18 -0
  509. package/dist/salvage-envelope.js +15 -0
  510. package/dist/salvage-envelope.js.map +1 -0
  511. package/dist/schemas.d.ts +98 -98
  512. package/dist/scope-profiles-EpJG7yKP.d.ts +30 -0
  513. package/dist/search/document-scanner.js +3 -2
  514. package/dist/search/embed-helper.d.ts +3 -1
  515. package/dist/search/embed-helper.js +3 -3
  516. package/dist/search/factory.d.ts +3 -1
  517. package/dist/search/factory.js +18 -16
  518. package/dist/search/index.d.ts +3 -1
  519. package/dist/search/index.js +22 -20
  520. package/dist/search/lancedb-backend.d.ts +3 -1
  521. package/dist/search/lancedb-backend.js +6 -5
  522. package/dist/search/meilisearch-backend.d.ts +3 -1
  523. package/dist/search/meilisearch-backend.js +6 -5
  524. package/dist/search/noop-backend.d.ts +3 -1
  525. package/dist/search/orama-backend.d.ts +3 -1
  526. package/dist/search/orama-backend.js +6 -5
  527. package/dist/search/port.d.ts +16 -2
  528. package/dist/search/port.js +1 -1
  529. package/dist/search/remote-backend.d.ts +3 -1
  530. package/dist/search/remote-backend.js +3 -3
  531. package/dist/secure-store/index.d.ts +12 -1
  532. package/dist/secure-store/index.js +6 -3
  533. package/dist/{semantic-consolidation-C2m5eHHE.d.ts → semantic-consolidation-DILZEQq3.d.ts} +1 -1
  534. package/dist/semantic-consolidation.d.ts +4 -2
  535. package/dist/semantic-consolidation.js +38 -29
  536. package/dist/semantic-rule-promotion.js +34 -24
  537. package/dist/semantic-rule-verifier.d.ts +3 -1
  538. package/dist/semantic-rule-verifier.js +35 -25
  539. package/dist/session-observer-bands.d.ts +3 -1
  540. package/dist/session-observer-state.d.ts +3 -1
  541. package/dist/session-observer-state.js +2 -2
  542. package/dist/session-transcript-migration.js +2 -2
  543. package/dist/shared-context/manager.d.ts +11 -9
  544. package/dist/shared-context/manager.js +3 -3
  545. package/dist/signal.d.ts +3 -1
  546. package/dist/{storage-hr_Y2z5R.d.ts → storage-NIyKYD8g.d.ts} +489 -110
  547. package/dist/storage-guards.js +2 -2
  548. package/dist/storage.d.ts +6 -3
  549. package/dist/storage.js +38 -26
  550. package/dist/structured-attributes.d.ts +23 -1
  551. package/dist/structured-attributes.js +7 -1
  552. package/dist/summarizer.d.ts +3 -1
  553. package/dist/summarizer.js +11 -11
  554. package/dist/summary-snapshot.d.ts +3 -1
  555. package/dist/summary-snapshot.js +2 -2
  556. package/dist/temporal-index-batch.d.ts +1 -0
  557. package/dist/temporal-index-batch.js +10 -0
  558. package/dist/temporal-index-batch.js.map +1 -0
  559. package/dist/temporal-index-compat.d.ts +279 -1
  560. package/dist/temporal-index-compat.js +1 -1
  561. package/dist/temporal-index.d.ts +1 -225
  562. package/dist/temporal-index.js +35 -3
  563. package/dist/temporal-supersession.d.ts +5 -2
  564. package/dist/temporal-supersession.js +2 -2
  565. package/dist/temporal-timeline-recall.d.ts +3 -1
  566. package/dist/temporal-validity.d.ts +3 -1
  567. package/dist/threading.d.ts +3 -1
  568. package/dist/threading.js +2 -2
  569. package/dist/tier-migration.d.ts +5 -2
  570. package/dist/tier-routing.d.ts +3 -1
  571. package/dist/{tier-stats-KV7IWCJR.js → tier-stats-QEJSPFV2.js} +2 -2
  572. package/dist/tokens.js +3 -3
  573. package/dist/topics.d.ts +3 -1
  574. package/dist/transcript.d.ts +3 -1
  575. package/dist/transcript.js +2 -2
  576. package/dist/transfer/backup.js +3 -3
  577. package/dist/transfer/capsule-export.js +3 -3
  578. package/dist/transfer/capsule-import.js +3 -3
  579. package/dist/transfer/types.d.ts +90 -90
  580. package/dist/trust-score-stage.d.ts +75 -38
  581. package/dist/trust-score-stage.js +11 -3
  582. package/dist/trust-score.d.ts +3 -1
  583. package/dist/{types-Cg8q7W7_.d.ts → types-D_fG2ix8.d.ts} +39 -63
  584. package/dist/types-continuity.d.ts +52 -0
  585. package/dist/types-continuity.js +1 -0
  586. package/dist/types-continuity.js.map +1 -0
  587. package/dist/types.d.ts +3 -1
  588. package/dist/types.js +1 -1
  589. package/dist/utility-runtime.d.ts +3 -1
  590. package/dist/utils/serialize-mutations.d.ts +31 -2
  591. package/dist/utils/serialize-mutations.js +1 -1
  592. package/dist/verified-recall.js +36 -26
  593. package/dist/write-envelope.d.ts +282 -0
  594. package/dist/write-envelope.js +35 -0
  595. package/dist/write-envelope.js.map +1 -0
  596. package/dist/write-quarantine.d.ts +94 -0
  597. package/dist/write-quarantine.js +11 -0
  598. package/dist/write-quarantine.js.map +1 -0
  599. package/dist/write-rate-limiter.d.ts +45 -0
  600. package/dist/write-rate-limiter.js +9 -0
  601. package/dist/write-rate-limiter.js.map +1 -0
  602. package/package.json +37 -2
  603. package/src/access-admin-ops-surface.ts +17 -7
  604. package/src/access-boundary.test.ts +28 -0
  605. package/src/access-boundary.ts +31 -2
  606. package/src/access-citation.test.ts +72 -0
  607. package/src/access-citation.ts +139 -0
  608. package/src/access-errors.ts +21 -0
  609. package/src/access-fingerprint-parity.test.ts +211 -0
  610. package/src/access-http-cancellation.test.ts +59 -21
  611. package/src/access-http.test.ts +90 -0
  612. package/src/access-http.ts +79 -79
  613. package/src/access-mcp-cancellation.test.ts +16 -7
  614. package/src/access-memory-search-fanout.test.ts +220 -0
  615. package/src/access-memory-search-fanout.ts +168 -0
  616. package/src/access-observe-quarantine.test.ts +172 -0
  617. package/src/access-observe-write-surface.ts +137 -11
  618. package/src/access-operations-batch.ts +2 -0
  619. package/src/access-recall-concurrency.ts +773 -0
  620. package/src/access-recall-response.ts +307 -0
  621. package/src/access-recall-surface.ts +100 -252
  622. package/src/access-service-coding-write.test.ts +22 -4
  623. package/src/access-service-health.test.ts +49 -0
  624. package/src/access-service-namespace.test.ts +486 -8
  625. package/src/access-service-observe-scope.test.ts +4 -0
  626. package/src/access-service-recall-concurrency.test.ts +1990 -0
  627. package/src/access-service.ts +186 -179
  628. package/src/access-surface-catalog.ts +1 -0
  629. package/src/access-token-capabilities.ts +16 -9
  630. package/src/active-recall.test.ts +35 -0
  631. package/src/active-recall.ts +7 -0
  632. package/src/bounded-jsonl-state.ts +93 -0
  633. package/src/buffer-session.test.ts +422 -5
  634. package/src/buffer-surprise-trigger.test.ts +41 -0
  635. package/src/buffer.ts +218 -3
  636. package/src/capabilities-cluster-b.test.ts +1 -0
  637. package/src/capabilities.ts +5 -6
  638. package/src/cli.ts +21 -22
  639. package/src/coding/architecture-surfaces.test.ts +6 -3
  640. package/src/coding/architecture-surfaces.ts +36 -25
  641. package/src/coding/decision-surfaces.ts +42 -34
  642. package/src/compounding/engine.test.ts +5 -1
  643. package/src/compounding/engine.ts +7 -7
  644. package/src/config-qmd.ts +89 -0
  645. package/src/config.test.ts +136 -0
  646. package/src/config.ts +113 -118
  647. package/src/connector-provenance-lifecycle.test.ts +4 -4
  648. package/src/connectors/coerce.test.ts +131 -0
  649. package/src/connectors/coerce.ts +80 -7
  650. package/src/contradiction/contradiction.test.ts +10 -6
  651. package/src/contradiction/resolution.ts +17 -4
  652. package/src/correction/correction-access-wiring.ts +62 -18
  653. package/src/cross-namespace-budget.test.ts +35 -0
  654. package/src/cross-namespace-budget.ts +86 -19
  655. package/src/explicit-capture.ts +54 -17
  656. package/src/extraction.ts +2 -3
  657. package/src/faithfulness-graph-guard.test.ts +16 -69
  658. package/src/graph-recall.test.ts +31 -0
  659. package/src/index.ts +22 -2
  660. package/src/local-llm.ts +4 -6
  661. package/src/logger.ts +16 -0
  662. package/src/maintenance/dreams-ledger.ts +100 -49
  663. package/src/maintenance/rebuild-memory-lifecycle-ledger-cli.ts +77 -0
  664. package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +373 -9
  665. package/src/memory-lifecycle-ledger-utils.ts +51 -5
  666. package/src/namespaces/principal.test.ts +37 -1
  667. package/src/namespaces/principal.ts +48 -0
  668. package/src/namespaces/search.test.ts +79 -5
  669. package/src/namespaces/search.ts +59 -6
  670. package/src/namespaces/storage.ts +43 -1
  671. package/src/offline-sync-exclude-globs.ts +58 -0
  672. package/src/offline-sync-impression-drain.test.ts +316 -0
  673. package/src/offline-sync-impression-drain.ts +260 -0
  674. package/src/offline-sync.test.ts +32 -5
  675. package/src/offline-sync.ts +1 -14
  676. package/src/operator-toolkit.ts +1 -1
  677. package/src/orchestration/batch-deindex-flush.test.ts +146 -0
  678. package/src/orchestration/consolidation-run.ts +96 -85
  679. package/src/orchestration/extraction-envelope.ts +94 -0
  680. package/src/orchestration/extraction-persist-promotion.ts +134 -0
  681. package/src/orchestration/extraction-persist.ts +325 -260
  682. package/src/orchestration/extraction-run.test.ts +1 -1
  683. package/src/orchestration/extraction-run.ts +2 -2
  684. package/src/orchestration/graph-recall-coordinator.ts +18 -7
  685. package/src/orchestration/lifecycle-policy-coordinator.ts +63 -48
  686. package/src/orchestration/maintenance.test.ts +2103 -3
  687. package/src/orchestration/maintenance.ts +574 -2
  688. package/src/orchestration/namespace-read-fanout.ts +14 -8
  689. package/src/orchestration/orchestrator-helpers.ts +8 -1
  690. package/src/orchestration/orchestrator-init.ts +22 -6
  691. package/src/orchestration/orchestrator-namespace-scope.ts +71 -0
  692. package/src/orchestration/persistence-index.ts +64 -9
  693. package/src/orchestration/qmd-result-resolver.ts +27 -0
  694. package/src/orchestration/recall-entry.ts +34 -19
  695. package/src/orchestration/recall-internal.ts +207 -79
  696. package/src/orchestration/recall-rerank-coordinator.ts +229 -82
  697. package/src/orchestration/recall-result-formatter.ts +76 -20
  698. package/src/orchestration/recall-search-pipeline.test.ts +70 -0
  699. package/src/orchestration/recall-search-pipeline.ts +102 -129
  700. package/src/orchestration/recall-section-coordinator.ts +315 -85
  701. package/src/orchestration/semantic-consolidation-coordinator.ts +69 -37
  702. package/src/orchestration/session-context.ts +21 -0
  703. package/src/orchestration/workspace-ops.test.ts +348 -0
  704. package/src/orchestration/workspace-ops.ts +168 -34
  705. package/src/orchestrator-chunked-hash-durability.test.ts +126 -0
  706. package/src/orchestrator-connector-dedup.test.ts +1061 -40
  707. package/src/orchestrator-flush.test.ts +155 -39
  708. package/src/orchestrator-nonchunked-hash-durability.test.ts +121 -0
  709. package/src/orchestrator-source-attribution.test.ts +9 -9
  710. package/src/orchestrator.ts +160 -149
  711. package/src/prioritized-embed.ts +123 -0
  712. package/src/proactive-contention.test.ts +80 -0
  713. package/src/proactive-contention.ts +39 -0
  714. package/src/procedural/procedure-miner.test.ts +19 -14
  715. package/src/procedural/procedure-miner.ts +44 -13
  716. package/src/qmd-client.test.ts +111 -1
  717. package/src/qmd-status.ts +186 -0
  718. package/src/qmd.ts +47 -47
  719. package/src/recall-budget-config.test.ts +30 -0
  720. package/src/recall-cold-deadline.test.ts +149 -0
  721. package/src/recall-concurrency-config.ts +28 -0
  722. package/src/recall-memory-map.ts +74 -0
  723. package/src/recall-rerank-coordinator.test.ts +508 -0
  724. package/src/recall-state.test.ts +825 -1
  725. package/src/recall-state.ts +406 -7
  726. package/src/recall-timings-access.test.ts +41 -0
  727. package/src/recall-timings.ts +16 -0
  728. package/src/relay/mission-access.test.ts +5 -5
  729. package/src/rerank.ts +22 -0
  730. package/src/retrieval-agents.test.ts +79 -0
  731. package/src/retrieval-agents.ts +71 -19
  732. package/src/salvage-envelope.ts +28 -0
  733. package/src/scopes/scope-plan.test.ts +42 -0
  734. package/src/scopes/scope-plan.ts +45 -0
  735. package/src/search/port.ts +13 -0
  736. package/src/secure-store/index.ts +1 -0
  737. package/src/secure-store/secure-fs.ts +48 -0
  738. package/src/secure-store/secure-store.test.ts +40 -0
  739. package/src/semantic-rule-promotion.ts +13 -4
  740. package/src/storage/content-hash-index.ts +655 -0
  741. package/src/storage/entity-timeline.ts +473 -0
  742. package/src/storage/memory-lifecycle-ledger-access.ts +603 -0
  743. package/src/storage/secure-line-reader.test.ts +197 -0
  744. package/src/storage/secure-line-reader.ts +181 -3
  745. package/src/storage-append-header-sniff.test.ts +195 -0
  746. package/src/storage-behavior-signals-cache.test.ts +237 -0
  747. package/src/storage-contract/cache-coherence.test.ts +96 -1
  748. package/src/storage-contract/content-hash.test.ts +47 -2
  749. package/src/storage-fact-hash-index-defer.test.ts +1200 -0
  750. package/src/storage-memory-lifecycle-ledger.test.ts +1146 -3
  751. package/src/storage.ts +882 -890
  752. package/src/structured-attributes.ts +52 -0
  753. package/src/temporal-index-batch.ts +112 -0
  754. package/src/temporal-index.test.ts +193 -0
  755. package/src/temporal-index.ts +49 -53
  756. package/src/testing/lifecycle-matrix.test.ts +212 -0
  757. package/src/testing/lifecycle-matrix.ts +271 -0
  758. package/src/testing/orchestrator-lite.ts +236 -0
  759. package/src/testing/subjects/dreams-ledger.test.ts +67 -0
  760. package/src/testing/subjects/extraction-lifecycle.test.ts +442 -0
  761. package/src/testing/subjects/lifecycle-ledger.test.ts +93 -0
  762. package/src/testing/subjects/recall-budget.test.ts +461 -0
  763. package/src/testing/subjects/serialized-write-chain.test.ts +224 -0
  764. package/src/trust-score-stage.test.ts +154 -0
  765. package/src/trust-score-stage.ts +216 -53
  766. package/src/types-continuity.ts +57 -0
  767. package/src/types.ts +51 -77
  768. package/src/utils/path-containment.test.ts +75 -0
  769. package/src/utils/path-containment.ts +76 -0
  770. package/src/utils/serialize-mutations.test.ts +122 -0
  771. package/src/utils/serialize-mutations.ts +68 -3
  772. package/src/wearables/memory-gen.test.ts +31 -9
  773. package/src/wearables/memory-gen.ts +95 -55
  774. package/src/wearables/pipeline.test.ts +10 -7
  775. package/src/wearables/service.test.ts +6 -8
  776. package/src/wearables/service.ts +2 -2
  777. package/src/write-envelope.extension.test.ts +111 -0
  778. package/src/write-envelope.test.ts +683 -0
  779. package/src/write-envelope.ts +902 -0
  780. package/src/write-quarantine.test.ts +245 -0
  781. package/src/write-quarantine.ts +290 -0
  782. package/src/write-rate-limiter.test.ts +105 -0
  783. package/src/write-rate-limiter.ts +135 -0
  784. package/src/write-sealed-parity.test.ts +204 -0
  785. package/dist/chunk-2VN4JAX7.js.map +0 -1
  786. package/dist/chunk-37CTGERM.js.map +0 -1
  787. package/dist/chunk-3IY64MV6.js.map +0 -1
  788. package/dist/chunk-BCDKBDSY.js.map +0 -1
  789. package/dist/chunk-BFPETA3S.js.map +0 -1
  790. package/dist/chunk-BFVEEB7W.js.map +0 -1
  791. package/dist/chunk-BS4SGLWV.js.map +0 -1
  792. package/dist/chunk-DCFV7A7H.js.map +0 -1
  793. package/dist/chunk-DYP3YYJJ.js.map +0 -1
  794. package/dist/chunk-E5BK6CGV.js +0 -335
  795. package/dist/chunk-E5BK6CGV.js.map +0 -1
  796. package/dist/chunk-FTYDWKO6.js.map +0 -1
  797. package/dist/chunk-GWTFLF2P.js.map +0 -1
  798. package/dist/chunk-IKQTFQMI.js.map +0 -1
  799. package/dist/chunk-INELTPRG.js.map +0 -1
  800. package/dist/chunk-J2HSAU72.js.map +0 -1
  801. package/dist/chunk-JREB6UFH.js.map +0 -1
  802. package/dist/chunk-K7LOWSLU.js.map +0 -1
  803. package/dist/chunk-KQDNAXOM.js.map +0 -1
  804. package/dist/chunk-KSBEEMQF.js.map +0 -1
  805. package/dist/chunk-KSNVDPDF.js.map +0 -1
  806. package/dist/chunk-LC7OODWZ.js.map +0 -1
  807. package/dist/chunk-LTJAMRGI.js.map +0 -1
  808. package/dist/chunk-LZOV7MX7.js.map +0 -1
  809. package/dist/chunk-NWTGD4AN.js.map +0 -1
  810. package/dist/chunk-ODPLEWB6.js.map +0 -1
  811. package/dist/chunk-OVJ2HIPE.js.map +0 -1
  812. package/dist/chunk-PBSAPHHA.js.map +0 -1
  813. package/dist/chunk-PHK3HARR.js +0 -32
  814. package/dist/chunk-PHK3HARR.js.map +0 -1
  815. package/dist/chunk-PLQE6MJA.js.map +0 -1
  816. package/dist/chunk-QKQYXZVS.js.map +0 -1
  817. package/dist/chunk-QQGPUKMT.js.map +0 -1
  818. package/dist/chunk-RANHFI3P.js +0 -8
  819. package/dist/chunk-RANHFI3P.js.map +0 -1
  820. package/dist/chunk-RAR2BSEM.js.map +0 -1
  821. package/dist/chunk-RULE4VG5.js.map +0 -1
  822. package/dist/chunk-SVOZFLIQ.js +0 -16
  823. package/dist/chunk-SVOZFLIQ.js.map +0 -1
  824. package/dist/chunk-T7Y5KPLN.js.map +0 -1
  825. package/dist/chunk-TXLGHDHS.js.map +0 -1
  826. package/dist/chunk-VDW4VU7B.js.map +0 -1
  827. package/dist/chunk-VSMRDBFT.js +0 -17
  828. package/dist/chunk-VSMRDBFT.js.map +0 -1
  829. package/dist/chunk-X64YLC24.js.map +0 -1
  830. package/dist/chunk-XDSXEKYK.js +0 -118
  831. package/dist/chunk-XDSXEKYK.js.map +0 -1
  832. package/dist/chunk-YF6FMLBT.js.map +0 -1
  833. package/dist/resolution-VH4NGENZ.js +0 -12
  834. /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-citation.js.map} +0 -0
  835. /package/dist/{contradiction-scan-J57T5QV7.js.map → access-memory-search-fanout.js.map} +0 -0
  836. /package/dist/{dreams-ledger-6UODQOX6.js.map → access-recall-concurrency.js.map} +0 -0
  837. /package/dist/{graph-edge-decay-IOGBBAGC.js.map → access-recall-response.js.map} +0 -0
  838. /package/dist/{resolution-VH4NGENZ.js.map → bounded-jsonl-state.js.map} +0 -0
  839. /package/dist/{chunk-A3GOMRJG.js.map → chunk-23L3IXFD.js.map} +0 -0
  840. /package/dist/{chunk-MRHFJV44.js.map → chunk-2DQ2XREU.js.map} +0 -0
  841. /package/dist/{chunk-L2B3EE7B.js.map → chunk-2QMKMVHH.js.map} +0 -0
  842. /package/dist/{chunk-VJVNLNDQ.js.map → chunk-2UFO7X4Q.js.map} +0 -0
  843. /package/dist/{chunk-2MR3MFQB.js.map → chunk-2VEOKUPP.js.map} +0 -0
  844. /package/dist/{chunk-RHOZ5LLO.js.map → chunk-347WD2LE.js.map} +0 -0
  845. /package/dist/{chunk-HDTQVNWD.js.map → chunk-3QAH2S33.js.map} +0 -0
  846. /package/dist/{chunk-LRNMRXJO.js.map → chunk-3Y224NYC.js.map} +0 -0
  847. /package/dist/{chunk-VFSP73LP.js.map → chunk-45GF7Y3V.js.map} +0 -0
  848. /package/dist/{chunk-OEHCMALU.js.map → chunk-5I4M26Q7.js.map} +0 -0
  849. /package/dist/{chunk-2AZXIZEN.js.map → chunk-5Q7EVV4X.js.map} +0 -0
  850. /package/dist/{chunk-PUBTHW5O.js.map → chunk-6DRPPQEJ.js.map} +0 -0
  851. /package/dist/{chunk-FDCTQWLW.js.map → chunk-6O22TZSB.js.map} +0 -0
  852. /package/dist/{chunk-ZQ7UBUZ6.js.map → chunk-6QYST5XN.js.map} +0 -0
  853. /package/dist/{chunk-W4PGTAS2.js.map → chunk-6WAFYNVT.js.map} +0 -0
  854. /package/dist/{chunk-R5VAVVHP.js.map → chunk-72A4L5JJ.js.map} +0 -0
  855. /package/dist/{chunk-3FAMU5TX.js.map → chunk-A6USBZTO.js.map} +0 -0
  856. /package/dist/{chunk-ZHIVCODT.js.map → chunk-A72SWTWW.js.map} +0 -0
  857. /package/dist/{chunk-FC77VUX5.js.map → chunk-A7Q43EUZ.js.map} +0 -0
  858. /package/dist/{chunk-UJBESW6X.js.map → chunk-AE7WMV4V.js.map} +0 -0
  859. /package/dist/{chunk-A5UXZRRM.js.map → chunk-AJ7QCSIS.js.map} +0 -0
  860. /package/dist/{chunk-FNMFJWBY.js.map → chunk-ANEUPF6O.js.map} +0 -0
  861. /package/dist/{chunk-2A3OXAP2.js.map → chunk-B7RWBZXW.js.map} +0 -0
  862. /package/dist/{chunk-74F5STS4.js.map → chunk-BLAKS3CP.js.map} +0 -0
  863. /package/dist/{chunk-M7VGG3ID.js.map → chunk-BV5WMSIJ.js.map} +0 -0
  864. /package/dist/{chunk-2BFLDH5F.js.map → chunk-BXLOS5AJ.js.map} +0 -0
  865. /package/dist/{chunk-UXKMMNJD.js.map → chunk-C23BYTJV.js.map} +0 -0
  866. /package/dist/{chunk-JZHHREBL.js.map → chunk-C3AKM55W.js.map} +0 -0
  867. /package/dist/{chunk-RP7YXA67.js.map → chunk-C5JF6EZ7.js.map} +0 -0
  868. /package/dist/{chunk-EHCONI6E.js.map → chunk-CQMEM5CC.js.map} +0 -0
  869. /package/dist/{chunk-PBNZVPFS.js.map → chunk-CXZQOVIE.js.map} +0 -0
  870. /package/dist/{chunk-7N2RUBAP.js.map → chunk-DDA47TES.js.map} +0 -0
  871. /package/dist/{chunk-64SE4X5G.js.map → chunk-DEBHXHJR.js.map} +0 -0
  872. /package/dist/{chunk-EHK6KZHK.js.map → chunk-DNHVPZOD.js.map} +0 -0
  873. /package/dist/{chunk-437S3G37.js.map → chunk-DQEMWVMT.js.map} +0 -0
  874. /package/dist/{chunk-LZZNTPLR.js.map → chunk-DX3XRGLH.js.map} +0 -0
  875. /package/dist/{chunk-DZKHSDOM.js.map → chunk-ES36HTSC.js.map} +0 -0
  876. /package/dist/{chunk-TN7THW5U.js.map → chunk-ET356KSB.js.map} +0 -0
  877. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  878. /package/dist/{chunk-DMSDUGDM.js.map → chunk-F7GAIWFA.js.map} +0 -0
  879. /package/dist/{chunk-G6XXLKC5.js.map → chunk-FFPJINJE.js.map} +0 -0
  880. /package/dist/{chunk-G4CN3ZXL.js.map → chunk-FUTF72MJ.js.map} +0 -0
  881. /package/dist/{chunk-HF77M34J.js.map → chunk-GFPZS3F5.js.map} +0 -0
  882. /package/dist/{chunk-G6NKALRN.js.map → chunk-H4WG3UQZ.js.map} +0 -0
  883. /package/dist/{chunk-UDQUSU27.js.map → chunk-HNZKVLAN.js.map} +0 -0
  884. /package/dist/{chunk-5EZXHU36.js.map → chunk-HYQJQGD3.js.map} +0 -0
  885. /package/dist/{chunk-TRWPCR6P.js.map → chunk-JECBQBEX.js.map} +0 -0
  886. /package/dist/{chunk-E6CBU7XP.js.map → chunk-JYIFGGEM.js.map} +0 -0
  887. /package/dist/{chunk-VA4WSW54.js.map → chunk-JYIJNNN3.js.map} +0 -0
  888. /package/dist/{chunk-L5HJQC36.js.map → chunk-K2TDK7GG.js.map} +0 -0
  889. /package/dist/{chunk-KDXVOFYY.js.map → chunk-KAVBJSBQ.js.map} +0 -0
  890. /package/dist/{chunk-4OKLXWDZ.js.map → chunk-KNB7IBBU.js.map} +0 -0
  891. /package/dist/{chunk-QLULYKWZ.js.map → chunk-KSXJOC4F.js.map} +0 -0
  892. /package/dist/{chunk-2S6QHZGJ.js.map → chunk-L2DPF4DF.js.map} +0 -0
  893. /package/dist/{chunk-JI5AVXYO.js.map → chunk-LEPOP46W.js.map} +0 -0
  894. /package/dist/{chunk-O7BBFJLP.js.map → chunk-LTAVSOZW.js.map} +0 -0
  895. /package/dist/{chunk-2IZAGAJR.js.map → chunk-LULR4IUW.js.map} +0 -0
  896. /package/dist/{chunk-WMWVO45V.js.map → chunk-MC6XO7T4.js.map} +0 -0
  897. /package/dist/{chunk-D6RIFG4O.js.map → chunk-MNB5RMAY.js.map} +0 -0
  898. /package/dist/{chunk-Q5WG43OP.js.map → chunk-MYFQ3ED4.js.map} +0 -0
  899. /package/dist/{chunk-V3UE5I5F.js.map → chunk-MYVPLJOO.js.map} +0 -0
  900. /package/dist/{chunk-MTWKDOVO.js.map → chunk-NLE6TSLG.js.map} +0 -0
  901. /package/dist/{chunk-DBRYEIGY.js.map → chunk-OJP3N7UK.js.map} +0 -0
  902. /package/dist/{chunk-SMX5267P.js.map → chunk-OZKOPQCM.js.map} +0 -0
  903. /package/dist/{chunk-JCK7V6SN.js.map → chunk-P65YJEEL.js.map} +0 -0
  904. /package/dist/{chunk-MTR7BZQN.js.map → chunk-PBGWYM3H.js.map} +0 -0
  905. /package/dist/{chunk-AXTXN4T6.js.map → chunk-PWFCHOB3.js.map} +0 -0
  906. /package/dist/{chunk-FDPZVY6D.js.map → chunk-QFN4U7AG.js.map} +0 -0
  907. /package/dist/{chunk-K2ZLHESQ.js.map → chunk-QIEUU4UK.js.map} +0 -0
  908. /package/dist/{chunk-3D6AFGQR.js.map → chunk-QVT3E4CE.js.map} +0 -0
  909. /package/dist/{chunk-SFQ6QNL7.js.map → chunk-R6YJI3HE.js.map} +0 -0
  910. /package/dist/{chunk-5CJVLG3H.js.map → chunk-RFDD3B7W.js.map} +0 -0
  911. /package/dist/{chunk-JBPKEARU.js.map → chunk-RKNJBZ55.js.map} +0 -0
  912. /package/dist/{chunk-FNZQGN76.js.map → chunk-S2BBISHU.js.map} +0 -0
  913. /package/dist/{chunk-IALLX7EE.js.map → chunk-S2FSBU4H.js.map} +0 -0
  914. /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
  915. /package/dist/{chunk-DPAENM7O.js.map → chunk-U3ZAVRP3.js.map} +0 -0
  916. /package/dist/{chunk-4KJLYTLS.js.map → chunk-V57NLO5P.js.map} +0 -0
  917. /package/dist/{chunk-FLVL4L3L.js.map → chunk-VDX2J7OX.js.map} +0 -0
  918. /package/dist/{chunk-AJLLIIML.js.map → chunk-W4FBGYGK.js.map} +0 -0
  919. /package/dist/{chunk-6SXVCD7W.js.map → chunk-W7IGLX4G.js.map} +0 -0
  920. /package/dist/{chunk-KAWTOYAW.js.map → chunk-WLZYGLJ4.js.map} +0 -0
  921. /package/dist/{chunk-RO4LQGT3.js.map → chunk-WON4APQY.js.map} +0 -0
  922. /package/dist/{chunk-PQVK2S3J.js.map → chunk-WZFAPHIK.js.map} +0 -0
  923. /package/dist/{chunk-KQPJLK3X.js.map → chunk-XL24QQ63.js.map} +0 -0
  924. /package/dist/{chunk-RVZRYUQH.js.map → chunk-YROFNEG5.js.map} +0 -0
  925. /package/dist/{chunk-VRAD2FGO.js.map → chunk-Z54EM5YZ.js.map} +0 -0
  926. /package/dist/{first-start-migration-TI67YMLG.js.map → first-start-migration-CSRHCJIQ.js.map} +0 -0
  927. /package/dist/{forget-CQCLB5LB.js.map → forget-HRPLVB3W.js.map} +0 -0
  928. /package/dist/{purge-NDWV6WYD.js.map → purge-IRF47ZTX.js.map} +0 -0
  929. /package/dist/{tier-stats-KV7IWCJR.js.map → tier-stats-QEJSPFV2.js.map} +0 -0
package/src/storage.ts CHANGED
@@ -20,13 +20,28 @@ import { log } from "./logger.js";
20
20
  import { assertMemoryFrontmatterId, warnProjectionFallback } from "./storage-guards.js";
21
21
  import { MemoryReadStore } from "./storage/memory-read-store.js";
22
22
  import { readMaybeEncryptedLines, readMemoryActionEventRowsFromLines } from "./storage/secure-line-reader.js";
23
+ import {
24
+ appendLifecycleEventsSerialized,
25
+ type DrainPendingLifecycleForSyncResult,
26
+ drainPendingLifecycleLedgerForSync,
27
+ drainPendingLifecycleLedgerIfAny,
28
+ readAllLifecycleEventsFromLedger,
29
+ readAllLifecycleEventsFromLedgerBuffer,
30
+ readBoundedLifecycleEventsFromLedger,
31
+ serializeLifecycleAppendPayload,
32
+ } from "./storage/memory-lifecycle-ledger-access.js";
23
33
  import { selfDeps } from "./orchestration/self-deps.js";
24
34
  import { EntityStore } from "./storage/entity-store.js";
25
35
  import { IdentityContinuityStore } from "./storage/identity-continuity-store.js";
26
36
  import { isErrnoCode } from "./utils/errno.js";
37
+ import { withHeldFileLock } from "./utils/serialize-mutations.js";
27
38
  import { RECALL_FALLBACK_DIRS, getCategoryDir, categoryDirName } from "./utils/category-dir.js";
28
39
  import { assertPathInsideRoot } from "./utils/path-containment.js";
29
40
  import { decodeYamlScalar } from "./utils/yaml-scalar.js";
41
+ import {
42
+ qmdCollectionPathParts,
43
+ qmdResultPathCandidates,
44
+ } from "./orchestration/qmd-result-resolver.js";
30
45
  import {
31
46
  clearMemoryCache,
32
47
  getCachedEntities,
@@ -63,6 +78,7 @@ import {
63
78
  SecureStoreLockedError,
64
79
  MAGIC_HEADER_SIZE,
65
80
  isEncryptedFile,
81
+ probeEncryptedRegularFileHeader,
66
82
  readMaybeEncryptedFileBuffer,
67
83
  readMaybeEncryptedFile,
68
84
  writeMaybeEncryptedFile,
@@ -138,6 +154,35 @@ import type {
138
154
  ProvenanceSource,
139
155
  } from "./types.js";
140
156
  import { confidenceTier, SPECULATIVE_TTL_DAYS } from "./types.js";
157
+ import {
158
+ collectStructuredSectionFacts,
159
+ compareEntityTimestamps,
160
+ compileEntityFacts,
161
+ isEntitySynthesisStale,
162
+ isEntitySynthesisTimelinePromotionBullet,
163
+ latestEntityTimelineTimestamp,
164
+ normalizeEntitySectionFact,
165
+ normalizeStructuredSectionFacts,
166
+ parseEntityTimelineBullet,
167
+ partitionEntityStructuredSections,
168
+ serializeEntityTimelineEntry,
169
+ } from "./storage/entity-timeline.js";
170
+ import {
171
+ ContentHashIndex,
172
+ FactHashIndexNotAuthoritativeError,
173
+ CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS,
174
+ type ContentHashIndexLockOptions,
175
+ } from "./storage/content-hash-index.js";
176
+ export { ContentHashIndex, FactHashIndexNotAuthoritativeError };
177
+ export type { ContentHashIndexLockOptions };
178
+ export {
179
+ compareEntityTimestamps,
180
+ compileEntityFacts,
181
+ countEntityStructuredFacts,
182
+ fingerprintEntityStructuredFacts,
183
+ isEntitySynthesisStale,
184
+ normalizeStructuredSectionFacts,
185
+ } from "./storage/entity-timeline.js";
141
186
  import {
142
187
  type ProjectedMemoryBrowseOptions,
143
188
  type ProjectedMemoryBrowsePage,
@@ -151,11 +196,16 @@ import {
151
196
  import {
152
197
  inferMemoryStatus,
153
198
  isArchivedMemoryPath,
154
- sortMemoryLifecycleEvents,
155
199
  toMemoryPathRel,
156
200
  } from "./memory-lifecycle-ledger-utils.js";
157
201
  import { normalizeProjectionPreview, normalizeProjectionTags } from "./memory-projection-format.js";
158
202
  import { parseFlexibleIsoTimestamp } from "./utils/iso-timestamp.js";
203
+ import {
204
+ composeMemoryEnvelope,
205
+ isSealedMemoryEnvelope,
206
+ sealedWriteToLegacyArgs,
207
+ type SealedMemoryEnvelope,
208
+ } from "./write-envelope.js";
159
209
  // stripCitation import removed: legacy rebuild fallback was replaced by a
160
210
  // skip-with-warning strategy (Finding 1 — Uhol). See ensureFactHashIndexAuthoritative.
161
211
 
@@ -1115,138 +1165,10 @@ function dehyphenate(s: string): string {
1115
1165
  return s.replace(/-/g, "");
1116
1166
  }
1117
1167
 
1118
- /**
1119
- * Content-hash dedup index for facts.
1120
- * Normalizes content (lowercase, strip punctuation, collapse whitespace),
1121
- * computes SHA-256, and stores hashes in a line-delimited file.
1122
- * Prevents writing semantically identical facts.
1123
- */
1124
- export class ContentHashIndex {
1125
- private hashes: Set<string> = new Set();
1126
- private dirty = false;
1127
- private readonly filePath: string;
1128
- private readonly secureStoreKeyProvider: () => Buffer | null;
1129
- private readonly secureStoreWriteKeyProvider: () => Buffer | null;
1130
- private readonly memoryDir: string;
1131
-
1132
- constructor(
1133
- stateDir: string,
1134
- secureStoreKeyProvider: () => Buffer | null = () => null,
1135
- secureStoreWriteKeyProvider: () => Buffer | null = secureStoreKeyProvider,
1136
- memoryDir: string = path.dirname(stateDir)
1137
- ) {
1138
- this.filePath = path.join(stateDir, "fact-hashes.txt");
1139
- this.secureStoreKeyProvider = secureStoreKeyProvider;
1140
- this.secureStoreWriteKeyProvider = secureStoreWriteKeyProvider;
1141
- this.memoryDir = memoryDir;
1142
- }
1143
-
1144
- /** Load existing hashes from disk. Safe to call multiple times. */
1145
- async load(): Promise<void> {
1146
- try {
1147
- const raw = await readMaybeEncryptedFile(this.filePath, this.secureStoreKeyProvider(), this.memoryDir);
1148
- for (const line of raw.split("\n")) {
1149
- const trimmed = line.trim();
1150
- if (trimmed.length > 0) {
1151
- this.hashes.add(trimmed);
1152
- }
1153
- }
1154
- log.debug(`content-hash index: loaded ${this.hashes.size} hashes`);
1155
- } catch (err) {
1156
- if (err instanceof SecureStoreLockedError) throw err;
1157
- if (!isErrnoCode(err, "ENOENT")) throw err;
1158
- log.debug("content-hash index: no existing index — starting fresh");
1159
- }
1160
- }
1161
-
1162
- /** Check if content already exists in the index. */
1163
- has(content: string): boolean {
1164
- return this.hashes.has(ContentHashIndex.computeHash(content));
1165
- }
1166
-
1167
- /** Add content hash to the index. */
1168
- add(content: string): void {
1169
- const hash = ContentHashIndex.computeHash(content);
1170
- if (!this.hashes.has(hash)) {
1171
- this.hashes.add(hash);
1172
- this.dirty = true;
1173
- }
1174
- }
1175
-
1176
- get size(): number {
1177
- return this.hashes.size;
1178
- }
1179
-
1180
- /** Clear all loaded hashes so the next save rewrites the index from scratch. */
1181
- clear(): void {
1182
- if (this.hashes.size > 0) {
1183
- this.hashes.clear();
1184
- }
1185
- this.dirty = true;
1186
- }
1187
-
1188
- /** Persist index to disk if changed. */
1189
- async save(): Promise<void> {
1190
- if (!this.dirty) return;
1191
- await mkdir(path.dirname(this.filePath), { recursive: true });
1192
- await writeMaybeEncryptedFile(
1193
- this.filePath,
1194
- [...this.hashes].join("\n") + "\n",
1195
- this.secureStoreWriteKeyProvider(),
1196
- {},
1197
- this.memoryDir
1198
- );
1199
- this.dirty = false;
1200
- log.debug(`content-hash index: saved ${this.hashes.size} hashes`);
1201
- }
1202
-
1203
- /** Remove a hash from the index (used when archiving/deleting). */
1204
- remove(content: string): void {
1205
- const hash = ContentHashIndex.computeHash(content);
1206
- if (this.hashes.delete(hash)) {
1207
- this.dirty = true;
1208
- }
1209
- }
1210
-
1211
- /**
1212
- * Remove a pre-computed SHA-256 hash directly from the index without
1213
- * re-hashing. Use this when the caller already holds the stored hash
1214
- * (e.g. `memory.frontmatter.contentHash`) to avoid the double-hash bug
1215
- * where `remove(hash)` would compute `hash(hash)` and never match the
1216
- * entry.
1217
- */
1218
- removeByHash(hash: string): void {
1219
- if (this.hashes.delete(hash)) {
1220
- this.dirty = true;
1221
- }
1222
- }
1223
-
1224
- /**
1225
- * Add a pre-computed SHA-256 hash directly to the index without re-hashing.
1226
- * Use this when the caller already holds the stored hash
1227
- * (e.g. `memory.frontmatter.contentHash`) so that the index records the raw
1228
- * content hash rather than re-hashing the citation-annotated body.
1229
- *
1230
- * @internal Only called from `StorageManager.ensureFactHashIndexAuthoritative`.
1231
- * Not part of the public API — prefer `add(content)` for external callers.
1232
- */
1233
- addByHash(hash: string): void {
1234
- if (!this.hashes.has(hash)) {
1235
- this.hashes.add(hash);
1236
- this.dirty = true;
1237
- }
1238
- }
1239
-
1240
- /** Normalize content (delegates to content-hash.ts for a single source of truth). */
1241
- static normalizeContent(content: string): string {
1242
- return normalizeContent(content);
1243
- }
1244
-
1245
- /** Compute SHA-256 hash (delegates to content-hash.ts for a single source of truth). */
1246
- static computeHash(content: string): string {
1247
- return computeContentHash(content);
1248
- }
1249
- }
1168
+ /** Bounded attempts to acquire the rebuild lock before ensureFactHashIndexAuthoritative surrenders authority (PR #2016). */
1169
+ const FACT_HASH_INDEX_REBUILD_MAX_ATTEMPTS = 3;
1170
+ /** Base backoff (ms) between rebuild-lock attempts; doubles each attempt, capped at CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS. */
1171
+ const FACT_HASH_INDEX_REBUILD_RETRY_BASE_MS = 50;
1250
1172
 
1251
1173
  // ---------------------------------------------------------------------------
1252
1174
  // Attribute normalization helper
@@ -1273,16 +1195,14 @@ export class ContentHashIndex {
1273
1195
  // the coding surfaces + wearable service). Imported here so internal callers
1274
1196
  // (snapshotBeforeWrite, snapshotForProvenance) resolve, and re-exported to
1275
1197
  // keep the public storage API stable for existing callers (wearables, dist).
1276
- import { stripAttributesSuffix } from "./structured-attributes.js";
1198
+ import { assemblePersistedBody, stripAttributesSuffix } from "./structured-attributes.js";
1277
1199
  export { stripAttributesSuffix };
1278
1200
 
1279
- export function normalizeAttributePairs(pairs: Record<string, string>): string {
1280
- return Object.entries(pairs)
1281
- .map(([k, v]) => [k.trim().toLowerCase(), v.trim()] as [string, string])
1282
- .sort(([a], [b]) => a.localeCompare(b))
1283
- .map(([k, v]) => `${k}: ${v}`)
1284
- .join("; ");
1285
- }
1201
+ // `normalizeAttributePairs` moved to ./structured-attributes.ts (issue #1989
1202
+ // PR2) beside `assemblePersistedBody` so the write path and the sealed
1203
+ // envelope composer share ONE assembly definition. Re-exported to keep the
1204
+ // public storage API stable for existing callers.
1205
+ export { normalizeAttributePairs } from "./structured-attributes.js";
1286
1206
 
1287
1207
  // ---------------------------------------------------------------------------
1288
1208
  // Entity file parsing / serialization (Knowledge Graph v7.0)
@@ -1429,471 +1349,6 @@ function readEntitySectionText(
1429
1349
  return sectionLines.join(options.preserveBullets === true ? "\n" : " ");
1430
1350
  }
1431
1351
 
1432
- function parseEntityTimelineBullet(bullet: string, fallbackTimestamp: string): EntityTimelineEntry | null {
1433
- const trimmed = bullet.trim();
1434
- if (!trimmed) return null;
1435
-
1436
- let rest = trimmed;
1437
- const entry: EntityTimelineEntry = {
1438
- timestamp: trimmed.startsWith("[") ? "" : fallbackTimestamp,
1439
- text: "",
1440
- };
1441
- const consumedMetadataSegments: string[] = [];
1442
- let literalSingleSourceSegment: string | undefined;
1443
-
1444
- if (!trimmed.startsWith("[")) {
1445
- entry.text = trimmed;
1446
- return entry.text ? entry : null;
1447
- }
1448
-
1449
- const firstEnd = trimmed.indexOf("]");
1450
- if (firstEnd === -1) {
1451
- entry.text = trimmed;
1452
- return entry.text ? entry : null;
1453
- }
1454
-
1455
- const firstToken = trimmed.slice(1, firstEnd).trim();
1456
- const parsedTimestamp = Date.parse(firstToken);
1457
- if (Number.isFinite(parsedTimestamp)) {
1458
- entry.timestamp = firstToken || fallbackTimestamp;
1459
- rest = trimmed.slice(firstEnd + 1).trimStart();
1460
- }
1461
-
1462
- while (rest.startsWith("[")) {
1463
- const end = findEntityTimelineTokenEnd(rest);
1464
- if (end === -1) break;
1465
- const rawSegment = rest.slice(0, end + 1);
1466
- const token = rest.slice(1, end).trim();
1467
- const equalsIdx = token.indexOf("=");
1468
- if (equalsIdx === -1) {
1469
- if (rest === trimmed) {
1470
- entry.text = trimmed;
1471
- return entry.text ? entry : null;
1472
- }
1473
- break;
1474
- }
1475
- const key = token.slice(0, equalsIdx).trim().toLowerCase();
1476
- const value = unescapeEntityTimelineMetadataValue(token.slice(equalsIdx + 1).trim());
1477
- if (!value) break;
1478
- const nextRest = rest.slice(end + 1).trimStart();
1479
- switch (key) {
1480
- case "source_meta":
1481
- entry.source = value;
1482
- break;
1483
- case "source":
1484
- if (
1485
- consumedMetadataSegments.length === 0 &&
1486
- !nextRest.startsWith("[") &&
1487
- nextRest.length > 0 &&
1488
- !isManagedEntityTimelineSource(value)
1489
- ) {
1490
- literalSingleSourceSegment = rawSegment;
1491
- rest = nextRest;
1492
- break;
1493
- }
1494
- entry.source = value;
1495
- break;
1496
- case "session":
1497
- case "sessionkey":
1498
- entry.sessionKey = value;
1499
- break;
1500
- case "principal":
1501
- entry.principal = value;
1502
- break;
1503
- default:
1504
- entry.text = rest.trim();
1505
- return entry.text ? entry : null;
1506
- }
1507
- if (literalSingleSourceSegment) break;
1508
- consumedMetadataSegments.push(rawSegment);
1509
- rest = nextRest;
1510
- }
1511
-
1512
- if (literalSingleSourceSegment) {
1513
- return {
1514
- timestamp: entry.timestamp,
1515
- text: `${literalSingleSourceSegment} ${rest}`.trim(),
1516
- };
1517
- }
1518
-
1519
- entry.text = rest.trim();
1520
- if (!entry.text) return null;
1521
- return entry;
1522
- }
1523
-
1524
- function isEntitySynthesisTimelinePromotionBullet(bullet: string): boolean {
1525
- const trimmed = bullet.trim();
1526
- if (!trimmed.startsWith("[")) return false;
1527
-
1528
- const firstEnd = findEntityTimelineTokenEnd(trimmed);
1529
- if (firstEnd === -1) return false;
1530
-
1531
- const firstToken = trimmed.slice(1, firstEnd).trim();
1532
- return looksLikeEntityTimelineTimestamp(firstToken);
1533
- }
1534
-
1535
- function looksLikeEntityTimelineTimestamp(token: string): boolean {
1536
- if (!/^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$/.test(token)) return false;
1537
- return Number.isFinite(Date.parse(token));
1538
- }
1539
-
1540
- function isManagedEntityTimelineSource(source: string): boolean {
1541
- switch (source.trim().toLowerCase()) {
1542
- case "artifact":
1543
- case "chunking":
1544
- case "cli-migrate":
1545
- case "compounding-promotion":
1546
- case "consolidation":
1547
- case "contradiction-detection":
1548
- case "entity_extraction":
1549
- case "explicit":
1550
- case "explicit-inline":
1551
- case "explicit-inline-review":
1552
- case "explicit-review":
1553
- case "extraction":
1554
- case "extraction-shared-promotion":
1555
- case "manual":
1556
- case "migration":
1557
- case "migration-rechunk":
1558
- case "proactive":
1559
- case "replay":
1560
- case "semantic-consolidation":
1561
- case "unknown":
1562
- return true;
1563
- default:
1564
- return false;
1565
- }
1566
- }
1567
-
1568
- function findEntityTimelineTokenEnd(input: string): number {
1569
- let escaped = false;
1570
- for (let index = 0; index < input.length; index += 1) {
1571
- const char = input[index];
1572
- if (escaped) {
1573
- escaped = false;
1574
- continue;
1575
- }
1576
- if (char === "\\") {
1577
- escaped = true;
1578
- continue;
1579
- }
1580
- if (char === "]") return index;
1581
- }
1582
- return -1;
1583
- }
1584
-
1585
- function escapeEntityTimelineMetadataValue(value: string): string {
1586
- let escaped = "";
1587
- for (const char of value) {
1588
- switch (char) {
1589
- case "\\":
1590
- escaped += "\\\\";
1591
- break;
1592
- case "]":
1593
- escaped += "\\]";
1594
- break;
1595
- case "\n":
1596
- escaped += "\\n";
1597
- break;
1598
- case "\r":
1599
- escaped += "\\r";
1600
- break;
1601
- case "\t":
1602
- escaped += "\\t";
1603
- break;
1604
- default: {
1605
- const codePoint = char.codePointAt(0) ?? 0;
1606
- if (codePoint < 0x20) {
1607
- escaped += `\\u${codePoint.toString(16).padStart(4, "0")}`;
1608
- } else {
1609
- escaped += char;
1610
- }
1611
- }
1612
- }
1613
- }
1614
- return escaped;
1615
- }
1616
-
1617
- function unescapeEntityTimelineMetadataValue(value: string): string {
1618
- if (!value.includes("\\")) return value;
1619
-
1620
- let result = "";
1621
- for (let index = 0; index < value.length; index += 1) {
1622
- const char = value[index];
1623
- if (char !== "\\") {
1624
- result += char;
1625
- continue;
1626
- }
1627
-
1628
- const next = value[index + 1];
1629
- if (!next) {
1630
- result += "\\";
1631
- break;
1632
- }
1633
-
1634
- switch (next) {
1635
- case "n":
1636
- result += "\n";
1637
- index += 1;
1638
- break;
1639
- case "r":
1640
- result += "\r";
1641
- index += 1;
1642
- break;
1643
- case "t":
1644
- result += "\t";
1645
- index += 1;
1646
- break;
1647
- case "u": {
1648
- const hex = value.slice(index + 2, index + 6);
1649
- if (/^[0-9a-fA-F]{4}$/.test(hex)) {
1650
- result += String.fromCharCode(parseInt(hex, 16));
1651
- index += 5;
1652
- break;
1653
- }
1654
- result += "u";
1655
- index += 1;
1656
- break;
1657
- }
1658
- default:
1659
- result += next;
1660
- index += 1;
1661
- break;
1662
- }
1663
- }
1664
- return result;
1665
- }
1666
-
1667
- function serializeEntityTimelineEntry(entry: EntityTimelineEntry): string {
1668
- const tokens: string[] = [];
1669
- if (entry.timestamp.trim().length > 0) {
1670
- tokens.push(`[${entry.timestamp}]`);
1671
- }
1672
- if (entry.source) {
1673
- const sourceKey = isManagedEntityTimelineSource(entry.source) ? "source" : "source_meta";
1674
- tokens.push(`[${sourceKey}=${escapeEntityTimelineMetadataValue(entry.source)}]`);
1675
- }
1676
- if (entry.sessionKey) {
1677
- tokens.push(`[session=${escapeEntityTimelineMetadataValue(entry.sessionKey)}]`);
1678
- }
1679
- if (entry.principal) {
1680
- tokens.push(`[principal=${escapeEntityTimelineMetadataValue(entry.principal)}]`);
1681
- }
1682
- const serializedMetadata = tokens.length > 0 ? `${tokens.join(" ")} ` : "";
1683
- return `- ${serializedMetadata}${entry.text}`.trimEnd();
1684
- }
1685
-
1686
- function dedupeEntityTimelineFacts(timeline: EntityTimelineEntry[]): string[] {
1687
- return [...new Set(timeline.map((entry) => entry.text.trim()).filter((entry) => entry.length > 0))];
1688
- }
1689
-
1690
- function normalizeEntitySectionFact(value: string): string {
1691
- return value.replace(/\s+/g, " ").trim();
1692
- }
1693
-
1694
- export function normalizeStructuredSectionFacts(facts: string[]): string[] {
1695
- return [...new Set(facts.map((fact) => normalizeEntitySectionFact(fact)).filter((fact) => fact.length > 0))];
1696
- }
1697
-
1698
- function collectStructuredSectionFacts(structuredSections: EntityStructuredSection[]): string[] {
1699
- const facts: string[] = [];
1700
- for (const section of structuredSections) {
1701
- for (const fact of section.facts) {
1702
- const normalized = normalizeEntitySectionFact(fact);
1703
- if (!normalized) continue;
1704
- facts.push(normalized);
1705
- }
1706
- }
1707
- return [...new Set(facts)];
1708
- }
1709
-
1710
- export function compileEntityFacts(
1711
- timeline: EntityTimelineEntry[],
1712
- structuredSections: EntityStructuredSection[]
1713
- ): string[] {
1714
- const facts: string[] = [];
1715
- const seen = new Set<string>();
1716
- for (const fact of dedupeEntityTimelineFacts(timeline)) {
1717
- if (seen.has(fact)) continue;
1718
- seen.add(fact);
1719
- facts.push(fact);
1720
- }
1721
- for (const fact of collectStructuredSectionFacts(structuredSections)) {
1722
- if (seen.has(fact)) continue;
1723
- seen.add(fact);
1724
- facts.push(fact);
1725
- }
1726
- return facts;
1727
- }
1728
-
1729
- function parseEntityStructuredSectionFacts(lines: string[]): string[] {
1730
- const facts: string[] = [];
1731
- let currentBlock: string[] = [];
1732
-
1733
- const flushCurrentBlock = (): void => {
1734
- const normalized = normalizeEntitySectionFact(currentBlock.join(" "));
1735
- if (normalized.length > 0) facts.push(normalized);
1736
- currentBlock = [];
1737
- };
1738
-
1739
- for (const rawLine of lines) {
1740
- const line = rawLine.trim();
1741
- if (!line) {
1742
- flushCurrentBlock();
1743
- continue;
1744
- }
1745
- if (line.startsWith("- ")) {
1746
- flushCurrentBlock();
1747
- currentBlock = [line.slice(2).trim()];
1748
- continue;
1749
- }
1750
- currentBlock.push(line);
1751
- }
1752
-
1753
- flushCurrentBlock();
1754
- return [...new Set(facts)];
1755
- }
1756
-
1757
- function looksLikeStructuredSectionFactList(lines: string[]): boolean {
1758
- const firstNonBlank = lines.find((line) => line.trim().length > 0)?.trim() ?? "";
1759
- return firstNonBlank.startsWith("- ");
1760
- }
1761
-
1762
- function partitionEntityStructuredSections(
1763
- entityType: string,
1764
- extraSections: Array<{ title: string; lines: string[] }>,
1765
- entitySchemas?: PluginConfig["entitySchemas"]
1766
- ): {
1767
- structuredSections: EntityStructuredSection[];
1768
- remainingExtraSections: Array<{ title: string; lines: string[] }>;
1769
- } {
1770
- const structuredSections: EntityStructuredSection[] = [];
1771
- const remainingExtraSections: Array<{ title: string; lines: string[] }> = [];
1772
- const structuredSectionIndex = new Map<string, EntityStructuredSection>();
1773
-
1774
- for (const section of extraSections) {
1775
- const matchedSection = matchEntitySchemaSection(entityType, section.title, entitySchemas);
1776
- if (!matchedSection && !looksLikeStructuredSectionFactList(section.lines)) {
1777
- remainingExtraSections.push(section);
1778
- continue;
1779
- }
1780
- const facts = parseEntityStructuredSectionFacts(section.lines);
1781
- if (!matchedSection && facts.length === 0) {
1782
- remainingExtraSections.push(section);
1783
- continue;
1784
- }
1785
- const normalizedSection = matchedSection
1786
- ? { key: matchedSection.key, title: matchedSection.title }
1787
- : normalizeEntityStructuredSection(entityType, { key: section.title, title: section.title }, entitySchemas);
1788
- if (facts.length === 0) {
1789
- remainingExtraSections.push(section);
1790
- continue;
1791
- }
1792
- const existing = structuredSectionIndex.get(normalizedSection.key);
1793
- if (existing) {
1794
- existing.facts = normalizeStructuredSectionFacts([...existing.facts, ...facts]);
1795
- continue;
1796
- }
1797
- const structuredSection: EntityStructuredSection = {
1798
- key: normalizedSection.key,
1799
- title: normalizedSection.title,
1800
- facts: normalizeStructuredSectionFacts(facts),
1801
- };
1802
- structuredSections.push(structuredSection);
1803
- structuredSectionIndex.set(normalizedSection.key, structuredSection);
1804
- }
1805
-
1806
- return {
1807
- structuredSections,
1808
- remainingExtraSections,
1809
- };
1810
- }
1811
-
1812
- function latestEntityTimelineTimestamp(entity: EntityFile): string | undefined {
1813
- let latestRaw: string | undefined;
1814
- for (const entry of entity.timeline) {
1815
- const timestamp = entry.timestamp.trim();
1816
- if (!timestamp) continue;
1817
- if (!latestRaw || compareEntityTimestamps(timestamp, latestRaw) > 0) {
1818
- latestRaw = timestamp;
1819
- }
1820
- }
1821
- return latestRaw;
1822
- }
1823
-
1824
- export function compareEntityTimestamps(left?: string, right?: string): number {
1825
- const leftValue = left?.trim() ?? "";
1826
- const rightValue = right?.trim() ?? "";
1827
-
1828
- if (!leftValue && !rightValue) return 0;
1829
- if (!leftValue) return -1;
1830
- if (!rightValue) return 1;
1831
-
1832
- const leftMs = Date.parse(leftValue);
1833
- const rightMs = Date.parse(rightValue);
1834
- const leftParsed = Number.isFinite(leftMs);
1835
- const rightParsed = Number.isFinite(rightMs);
1836
-
1837
- if (leftParsed && rightParsed) {
1838
- if (leftMs === rightMs) return 0;
1839
- return leftMs > rightMs ? 1 : -1;
1840
- }
1841
- if (leftParsed) return 1;
1842
- if (rightParsed) return -1;
1843
- return leftValue.localeCompare(rightValue);
1844
- }
1845
-
1846
- export function countEntityStructuredFacts(entity: EntityFile): number {
1847
- return (entity.structuredSections ?? []).reduce((count, section) => count + section.facts.length, 0);
1848
- }
1849
-
1850
- export function fingerprintEntityStructuredFacts(entity: Pick<EntityFile, "structuredSections">): string | undefined {
1851
- const normalizedSections = (entity.structuredSections ?? [])
1852
- .map((section) => ({
1853
- key: section.key.trim().toLowerCase(),
1854
- title: section.title.replace(/\s+/g, " ").trim(),
1855
- facts: normalizeStructuredSectionFacts(section.facts)
1856
- .slice()
1857
- .sort((left, right) => left.localeCompare(right)),
1858
- }))
1859
- .filter((section) => section.facts.length > 0)
1860
- .sort(
1861
- (left, right) =>
1862
- left.key.localeCompare(right.key) ||
1863
- left.title.localeCompare(right.title) ||
1864
- left.facts.join("\n").localeCompare(right.facts.join("\n"))
1865
- );
1866
- if (normalizedSections.length === 0) return undefined;
1867
- return createHash("sha256").update(JSON.stringify(normalizedSections)).digest("hex");
1868
- }
1869
-
1870
- export function isEntitySynthesisStale(entity: EntityFile): boolean {
1871
- const structuredFactCount = countEntityStructuredFacts(entity);
1872
- const structuredFactDigest = fingerprintEntityStructuredFacts(entity);
1873
- const storedStructuredFactDigest = entity.synthesisStructuredFactDigest?.trim() || undefined;
1874
- if (entity.timeline.length === 0 && structuredFactCount === 0) return false;
1875
- if (!entity.synthesis?.trim()) return true;
1876
- if (entity.synthesisTimelineCount === undefined) return true;
1877
- if (structuredFactCount > 0 && entity.synthesisStructuredFactCount === undefined) return true;
1878
- if (structuredFactCount > 0 && !storedStructuredFactDigest) return true;
1879
- const latestTimelineTimestamp = latestEntityTimelineTimestamp(entity);
1880
- if (!latestTimelineTimestamp) {
1881
- return (
1882
- entity.timeline.length > entity.synthesisTimelineCount ||
1883
- structuredFactCount > (entity.synthesisStructuredFactCount ?? 0) ||
1884
- structuredFactDigest !== storedStructuredFactDigest
1885
- );
1886
- }
1887
- if (!entity.synthesisUpdatedAt?.trim()) return true;
1888
- const timelineFreshness = compareEntityTimestamps(latestTimelineTimestamp, entity.synthesisUpdatedAt);
1889
- if (timelineFreshness > 0) return true;
1890
- return (
1891
- entity.timeline.length > entity.synthesisTimelineCount ||
1892
- structuredFactCount > (entity.synthesisStructuredFactCount ?? 0) ||
1893
- structuredFactDigest !== storedStructuredFactDigest
1894
- );
1895
- }
1896
-
1897
1352
  /**
1898
1353
  * Parse an entity markdown file into a structured EntityFile.
1899
1354
  * Backward compatible: old files without new sections get empty arrays.
@@ -2315,6 +1770,105 @@ function parseExtractionRetryStateEntries(
2315
1770
  return valid;
2316
1771
  }
2317
1772
 
1773
+ /**
1774
+ * Options accepted by `StorageManager.writeMemory` (extracted verbatim in
1775
+ * issue #1989 PR2 so `SealedWriteExtras` can be derived from it — the
1776
+ * sealed-envelope path takes envelope-owned fields from the envelope and
1777
+ * everything else from these extras).
1778
+ */
1779
+ export interface WriteMemoryOptions {
1780
+ actor?: string;
1781
+ confidence?: number;
1782
+ tags?: string[];
1783
+ entityRef?: string;
1784
+ source?: string;
1785
+ supersedes?: string;
1786
+ lineage?: string[];
1787
+ importance?: ImportanceScore;
1788
+ links?: MemoryLink[];
1789
+ intentGoal?: string;
1790
+ intentActionType?: string;
1791
+ intentEntityTypes?: string[];
1792
+ artifactType?: MemoryFrontmatter["artifactType"];
1793
+ sourceMemoryId?: string;
1794
+ sourceTurnId?: string;
1795
+ memoryKind?: MemoryFrontmatter["memoryKind"];
1796
+ expiresAt?: string;
1797
+ validAt?: string;
1798
+ // Issue #1578 — bi-temporal ingestion provenance. `observedAt` is the
1799
+ // ingestion time (when Remnic learned the fact); `eventTimeSource`
1800
+ // records whether `validAt` was resolved from an extracted expression
1801
+ // or assumed from the ingestion anchor. Both validate on serialize.
1802
+ observedAt?: string;
1803
+ eventTimeSource?: "extracted" | "assumed";
1804
+ invalidAt?: string;
1805
+ structuredAttributes?: Record<string, string>;
1806
+ /**
1807
+ * When provided, this string is used as the source for the fact-content
1808
+ * dedup hash index instead of the persisted body (`content`).
1809
+ *
1810
+ * Use this when the persisted body differs from the canonical fact text
1811
+ * — for example when `content` is a citation-annotated variant of a raw
1812
+ * fact. Passing the raw fact as `contentHashSource` ensures that
1813
+ * `hasFactContentHash(rawFact)` returns `true` after the write, so
1814
+ * subsequent extractions of the same logical fact are correctly deduped
1815
+ * even when their citation timestamp differs.
1816
+ */
1817
+ contentHashSource?: string;
1818
+ /**
1819
+ * When true, writeMemory marks the fact-hash index dirty via `.add(...)`
1820
+ * but does NOT flush it to disk — the caller is responsible for a
1821
+ * subsequent batch save (issue #1909). The extraction persist path sets
1822
+ * this and relies on the orchestrator's authoritative
1823
+ * `saveContentHashIndexes()` batch save plus a per-storage union flush,
1824
+ * avoiding a whole-index (which grows with corpus size) rewrite per fact.
1825
+ * Default false: every
1826
+ * other (single-write) caller keeps the immediate, crash-safe save.
1827
+ */
1828
+ deferHashIndexSave?: boolean;
1829
+ status?: MemoryStatus;
1830
+ /**
1831
+ * Consolidation provenance (issue #561 PR 2). When the caller is a
1832
+ * consolidation / supersession / dedup-merge path, these fields wire
1833
+ * the page-version snapshots the new memory was derived from and the
1834
+ * operator that produced it. Persisted onto frontmatter as
1835
+ * `derived_from` + `derived_via`; validated at serialize time.
1836
+ */
1837
+ derivedFrom?: string[];
1838
+ derivedVia?: ConsolidationOperator;
1839
+ /**
1840
+ * Faithfulness gate verdict (issue #1576). When provided, persisted to
1841
+ * frontmatter so downstream readers (TrustScore #1577, review queue)
1842
+ * can consume it. Absent = gate was off or fact predates #1576.
1843
+ */
1844
+ faithfulness?: import("./types.js").FaithfulnessFrontmatter;
1845
+ /**
1846
+ * Claim-level provenance spans (issue #1575 PR 2). When provided,
1847
+ * persisted to frontmatter so downstream readers (memory_get, x-ray,
1848
+ * faithfulness gate #1576) can consume them. Absent = fact predates
1849
+ * #1575 or provenance was disabled.
1850
+ */
1851
+ sources?: ProvenanceSource[];
1852
+ provenance?: "verified" | "unverified" | "none";
1853
+ sourceConnector?: string;
1854
+ }
1855
+
1856
+ /**
1857
+ * `WriteMemoryOptions` minus the fields a `SealedMemoryEnvelope` owns.
1858
+ * Passing an envelope-owned field twice is a compile error by construction.
1859
+ */
1860
+ export type SealedWriteExtras = Omit<
1861
+ WriteMemoryOptions,
1862
+ | "confidence"
1863
+ | "tags"
1864
+ | "entityRef"
1865
+ | "source"
1866
+ | "expiresAt"
1867
+ | "validAt"
1868
+ | "structuredAttributes"
1869
+ | "sourceConnector"
1870
+ >;
1871
+
2318
1872
  export class StorageManager {
2319
1873
  private knowledgeIndexCache: { result: string; builtAt: number } | null = null;
2320
1874
  private artifactIndexCache: { memories: MemoryFile[]; loadedAtMs: number; writeVersion: number } | null = null;
@@ -2442,8 +1996,39 @@ export class StorageManager {
2442
1996
  private factHashIndex: ContentHashIndex | null = null;
2443
1997
  private factHashIndexLoadPromise: Promise<ContentHashIndex> | null = null;
2444
1998
  private factHashIndexAuthoritative: boolean | null = null;
2445
- private factHashIndexAuthoritativePromise: Promise<void> | null = null;
1999
+ private factHashIndexAuthoritativePromise: Promise<boolean> | null = null;
2000
+ /**
2001
+ * Fact-ONLY hash membership (PR #2016). The shared `factHashIndex` above is
2002
+ * category-agnostic (the round-15 corpus rebuild + addContentHashDedup index
2003
+ * every category), so it cannot answer a fact-only question. This set carries
2004
+ * only `category === "fact"` hashes and is the sole source for
2005
+ * `hasFactContentHash`, so an over-included non-fact body can never suppress a
2006
+ * real fact candidate. Kept in lockstep with the shared index on EVERY path:
2007
+ * the authoritative corpus rebuild (repopulated in place), the write path
2008
+ * (`writeMemory` / `addActiveFactContentHash`), the storage-owned removal
2009
+ * (`removeFactContentHashesForMemories`), AND the orchestrator's archival /
2010
+ * consolidation removal (`removeContentHashForMemory` ->
2011
+ * `removeFactOnlyHashForMemory`). In-memory only; never persisted.
2012
+ */
2013
+ private factOnlyHashes: Set<string> = new Set();
2014
+ /** Optional lock/retry tuning for the fact-hash index cross-process lock (PR #2016; tests inject tight budgets). */
2015
+ factHashIndexLockOptions: ContentHashIndexLockOptions = {};
2446
2016
  private readonly secureAppendChains = new Map<string, Promise<void>>();
2017
+ /**
2018
+ * Cache of "is this file encrypted?" keyed by absolute path, VALIDATED by file
2019
+ * identity (size + mtimeMs) — issue #1909 review round 10 finding 1. A prior
2020
+ * design cached only a boolean and relied on this instance's own writes to
2021
+ * invalidate it; a PEER process encrypting the file would not invalidate,
2022
+ * leaving a stale `false` that made the append path write RAW bytes into an
2023
+ * encrypted file (corruption). Now each entry records the (size, mtime) the
2024
+ * classification was read at; `isEncryptedFileHeader` re-sniffs (header-only)
2025
+ * whenever the on-disk identity no longer matches, so a foreign rewrite is
2026
+ * detected. The whole-file read is still never performed.
2027
+ */
2028
+ private readonly secureFileEncryptionSniffCache = new Map<
2029
+ string,
2030
+ { identity: { size: number; mtimeMs: number }; encrypted: boolean }
2031
+ >();
2447
2032
  private offlineSyncDigestCache: Map<string, OfflineSyncDigestCacheEntry> | null = null;
2448
2033
  private offlineSyncDigestCacheLoadPromise: Promise<Map<string, OfflineSyncDigestCacheEntry>> | null = null;
2449
2034
  private offlineSyncDigestCacheWriteChain: Promise<void> = Promise.resolve();
@@ -2452,6 +2037,15 @@ export class StorageManager {
2452
2037
  citationTemplate: string = DEFAULT_CITATION_FORMAT;
2453
2038
  /** Post-write catalog hook (#1522). Installed by the namespace router; fire-and-forget. */
2454
2039
  onCatalogWrite?: () => void;
2040
+ /** Post-write embedding hook (#2019). Installed by the orchestrator; fire-and-forget. */
2041
+ onMemoryWrite?: (filePath: string) => void;
2042
+ private notifyMemoryWrite(filePath: string): void {
2043
+ try {
2044
+ this.onMemoryWrite?.(filePath);
2045
+ } catch {
2046
+ /* fire-and-forget — embedding failures must not block writes */
2047
+ }
2048
+ }
2455
2049
  private notifyCatalogWrite(): void {
2456
2050
  try {
2457
2051
  this.onCatalogWrite?.();
@@ -2550,6 +2144,12 @@ export class StorageManager {
2550
2144
  // decrypted under the previous key, not just the entity layer.
2551
2145
  invalidateAllForDir(this.baseDir);
2552
2146
  this.invalidateKnowledgeIndexCache();
2147
+ // A key change flips resolveWriteKey(), so a file's plain/encrypted class on
2148
+ // the next full rewrite may differ from what was cached (issue #1909).
2149
+ this.secureFileEncryptionSniffCache.clear();
2150
+ // Same rationale for the behavior-signals dedup key cache (issue #1909):
2151
+ // drop it on a key change so a re-encrypted/re-decrypted ledger is reloaded.
2152
+ this.behaviorSignalsKeyCache = null;
2553
2153
  }
2554
2154
 
2555
2155
  private getEntityCacheSecureStoreKey(): string {
@@ -2624,20 +2224,28 @@ export class StorageManager {
2624
2224
  return this._secureStoreKey !== null;
2625
2225
  }
2626
2226
 
2227
+ /** Whether a state-file write encrypts at rest (encrypt-on-write on AND key
2228
+ * set) — the write mode lifecycle compaction reserves the envelope for (#2033). */
2229
+ willEncryptStateWrites(): boolean {
2230
+ return this._secureStoreEncryptOnWrite && this._secureStoreKey !== null;
2231
+ }
2232
+
2627
2233
  /**
2628
- * Resolve the effective write key.
2234
+ * Resolve the effective write key: null when encrypt-on-write is off or the
2235
+ * store is unlocked-optional; the key when set; else throws
2236
+ * SecureStoreLockedError under a required-but-locked store (PR #767) instead
2237
+ * of silently writing plaintext.
2629
2238
  *
2630
- * - If `_secureStoreEncryptOnWrite` is false: returns null (plain write).
2631
- * - If `_secureStoreEncryptOnWrite` is true AND key is set: returns key.
2632
- * - If `_secureStoreEncryptOnWrite` is true AND key is null AND
2633
- * `_secureStoreRequired` is true: throws SecureStoreLockedError so the
2634
- * write fails loudly rather than silently writing plaintext (P1 finding
2635
- * from Cursor review of PR #767).
2636
- * - If `_secureStoreEncryptOnWrite` is true AND key is null AND
2637
- * `_secureStoreRequired` is false: returns null (unencrypted store).
2239
+ * `forceEncrypt` overrides the encrypt-on-write policy flag ONLY (issue
2240
+ * #2033): a lifecycle-ledger compaction of an already-encrypted ledger/backup
2241
+ * must preserve encryption at rest even when `secureStoreEncryptOnWrite` is
2242
+ * paused, or it would silently downgrade encrypted state to plaintext. It
2243
+ * never bypasses the LOCK: when the store has no key it still returns null
2244
+ * (optional) or throws (required), so a keyless caller can never fabricate an
2245
+ * encrypted write.
2638
2246
  */
2639
- private resolveWriteKey(): Buffer | null {
2640
- if (!this._secureStoreEncryptOnWrite) return null;
2247
+ private resolveWriteKey(forceEncrypt = false): Buffer | null {
2248
+ if (!forceEncrypt && !this._secureStoreEncryptOnWrite) return null;
2641
2249
  if (this._secureStoreKey !== null) return this._secureStoreKey;
2642
2250
  if (this._secureStoreRequired) {
2643
2251
  throw new SecureStoreLockedError(
@@ -3129,15 +2737,21 @@ export class StorageManager {
3129
2737
  private readStorageSecureFile(filePath: string): Promise<string> {
3130
2738
  return readMaybeEncryptedFile(filePath, this._secureStoreKey, this.baseDir);
3131
2739
  }
3132
- private writeStorageSecureFile(filePath: string, content: string | Buffer): Promise<void> {
3133
- return writeMaybeEncryptedFile(filePath, content, this.resolveWriteKey(), {}, this.baseDir).then(() =>
3134
- this.notifyCatalogWriteForPath(filePath)
3135
- );
2740
+ private writeStorageSecureFile(filePath: string, content: string | Buffer, forceEncrypt = false): Promise<void> {
2741
+ const writeKey = this.resolveWriteKey(forceEncrypt);
2742
+ return writeMaybeEncryptedFile(filePath, content, writeKey, {}, this.baseDir).then(() => {
2743
+ // No manual sniff-cache update needed (issue #1909 round 10): this rewrite
2744
+ // changes the file's (size, mtime), so isEncryptedFileHeader re-sniffs on
2745
+ // the next append when its identity check misses. Identity validation also
2746
+ // covers PEER rewrites that this hook never sees.
2747
+ this.notifyCatalogWriteForPath(filePath);
2748
+ });
3136
2749
  }
3137
2750
  private writeStorageSecureFileChunks(filePath: string, chunks: AsyncIterable<Buffer>): Promise<void> {
3138
- return writeMaybeEncryptedFileFromChunks(filePath, chunks, this.resolveWriteKey(), {}, this.baseDir).then(() =>
3139
- this.notifyCatalogWriteForPath(filePath)
3140
- );
2751
+ const writeKey = this.resolveWriteKey();
2752
+ return writeMaybeEncryptedFileFromChunks(filePath, chunks, writeKey, {}, this.baseDir).then(() => {
2753
+ this.notifyCatalogWriteForPath(filePath);
2754
+ });
3141
2755
  }
3142
2756
 
3143
2757
  private assertManagedStoragePath(filePath: string, method: string): string {
@@ -3343,11 +2957,9 @@ export class StorageManager {
3343
2957
  // chokepoint, which also covers the entity cache (issue #1535).
3344
2958
  this.invalidateAllMemoriesCache();
3345
2959
  this.invalidateKnowledgeIndexCache();
2960
+ // Force a corpus rebuild of the fact-hash index on next use (round 11: no
2961
+ // on-disk ready marker — the in-memory authoritative flag is the only gate).
3346
2962
  this.factHashIndexAuthoritative = false;
3347
- await unlink(this.factHashIndexReadyPath).catch((error: unknown) => {
3348
- if (isErrnoCode(error, "ENOENT")) return;
3349
- throw error;
3350
- });
3351
2963
  if (filePath.includes(`${path.sep}cold${path.sep}`)) {
3352
2964
  this.invalidateColdMemoriesCache();
3353
2965
  }
@@ -3362,7 +2974,8 @@ export class StorageManager {
3362
2974
  this.stateDir,
3363
2975
  () => this._secureStoreKey,
3364
2976
  () => this.resolveWriteKey(),
3365
- this.baseDir
2977
+ this.baseDir,
2978
+ this.factHashIndexLockOptions,
3366
2979
  );
3367
2980
  }
3368
2981
 
@@ -3380,12 +2993,60 @@ export class StorageManager {
3380
2993
  }
3381
2994
  }
3382
2995
 
2996
+ /**
2997
+ * Classify a file as encrypted by reading ONLY the fixed-size magic header
2998
+ * (issue #1909), never the whole file. The result is cached per-path but
2999
+ * VALIDATED against the file's (size, mtime) identity (review round 10 finding
3000
+ * 1): a `stat` on each call is O(1), and the 12-byte header read happens only
3001
+ * on a cache miss or when the on-disk identity changed — so a PEER process
3002
+ * rewriting/encrypting the file is detected and we never append raw bytes into
3003
+ * an encrypted body. ENOENT and short/empty files classify as not-encrypted
3004
+ * (fall through to a plain `appendFile`).
3005
+ */
3006
+ private async isEncryptedFileHeader(filePath: string): Promise<boolean> {
3007
+ let st;
3008
+ try {
3009
+ st = await stat(filePath);
3010
+ } catch (err) {
3011
+ if (isErrnoCode(err, "ENOENT")) return false;
3012
+ throw err;
3013
+ }
3014
+ const cached = this.secureFileEncryptionSniffCache.get(filePath);
3015
+ if (cached && cached.identity.size === st.size && cached.identity.mtimeMs === st.mtimeMs) {
3016
+ return cached.encrypted;
3017
+ }
3018
+ let handle;
3019
+ try {
3020
+ handle = await open(filePath, "r");
3021
+ } catch (err) {
3022
+ if (isErrnoCode(err, "ENOENT")) return false;
3023
+ throw err;
3024
+ }
3025
+ try {
3026
+ const header = Buffer.alloc(MAGIC_HEADER_SIZE);
3027
+ const { bytesRead } = await handle.read(header, 0, header.length, 0);
3028
+ const encrypted = isEncryptedFile(header.subarray(0, bytesRead));
3029
+ this.secureFileEncryptionSniffCache.set(filePath, {
3030
+ identity: { size: st.size, mtimeMs: st.mtimeMs },
3031
+ encrypted,
3032
+ });
3033
+ return encrypted;
3034
+ } finally {
3035
+ await handle.close().catch(() => undefined);
3036
+ }
3037
+ }
3038
+
3383
3039
  private async appendStorageSecureFileUnlocked(filePath: string, content: string): Promise<void> {
3384
3040
  const writeKey = this.resolveWriteKey();
3385
3041
  await mkdir(path.dirname(filePath), { recursive: true });
3386
3042
  if (writeKey === null) {
3387
3043
  try {
3388
- if (isEncryptedFile(await readFile(filePath))) {
3044
+ // Header-only sniff (issue #1909): reads at most MAGIC_HEADER_SIZE bytes
3045
+ // instead of the whole target file (a lifecycle ledger can grow to
3046
+ // hundreds of MB on a large corpus) to decide "is this encrypted?". A
3047
+ // plaintext append below cannot flip the classification, so a cached
3048
+ // `false` stays valid across appends.
3049
+ if (await this.isEncryptedFileHeader(filePath)) {
3389
3050
  const existing = await this.readStorageSecureFile(filePath);
3390
3051
  await writeMaybeEncryptedFile(filePath, `${existing}${content}`, null, {}, this.baseDir);
3391
3052
  this.notifyCatalogWriteForPath(filePath);
@@ -3394,11 +3055,21 @@ export class StorageManager {
3394
3055
  } catch (err) {
3395
3056
  if (!isErrnoCode(err, "ENOENT")) throw err;
3396
3057
  }
3058
+ // Plaintext append does not touch the header. isEncryptedFileHeader is now
3059
+ // (size, mtime)-identity-validated (issue #1909 round 10), so a PEER
3060
+ // process that encrypted this file since our last sniff is detected on the
3061
+ // stat above and re-sniffed — we will NOT append raw bytes into an
3062
+ // encrypted body. A residual sub-call TOCTOU (a peer encrypts between this
3063
+ // stat and the appendFile below) is inherent to any check-then-act and is
3064
+ // out of scope; the identity check closes the long-lived stale-cache hole.
3397
3065
  await appendFile(filePath, content, "utf-8");
3398
3066
  this.notifyCatalogWriteForPath(filePath);
3399
3067
  return;
3400
3068
  }
3401
3069
 
3070
+ // Encrypted-store branch (issue #1909, known limitation): the sealed format
3071
+ // has no append primitive, so this inherently decrypts + concatenates +
3072
+ // rewrites the whole file. Only the plaintext-path sniff read was wasteful.
3402
3073
  let existing = "";
3403
3074
  try {
3404
3075
  existing = await this.readStorageSecureFile(filePath);
@@ -3417,10 +3088,6 @@ export class StorageManager {
3417
3088
  private get entitySynthesisQueuePath(): string {
3418
3089
  return path.join(this.stateDir, "entity-synthesis-queue.json");
3419
3090
  }
3420
- private get factHashIndexReadyPath(): string {
3421
- return path.join(this.stateDir, "fact-hashes.ready");
3422
- }
3423
-
3424
3091
  // ── Tombstone store access (issue #1579) ─────────────────────────────────
3425
3092
  /**
3426
3093
  * The on-disk tombstone log path. Lives under `<stateDir>/tombstones.jsonl`
@@ -3618,101 +3285,200 @@ export class StorageManager {
3618
3285
  return this.factHashIndexLoadPromise;
3619
3286
  }
3620
3287
 
3621
- private async ensureFactHashIndexAuthoritative(): Promise<void> {
3288
+ /**
3289
+ * Return the fact-hash index after ensuring it is authoritative — i.e. rebuilt
3290
+ * from the durable fact corpus (issue #1909 review round 12). This is the ONE
3291
+ * coherent dedup source: the orchestrator's dedup layer
3292
+ * (contentHashIndexForStorage) shares THIS instance instead of loading a raw,
3293
+ * possibly-stale fact-hashes.txt, so after a crash+restart the orchestrator's
3294
+ * hasContentHashDedup sees the same corpus-rebuilt hashes StorageManager does
3295
+ * and never re-creates a fact whose per-write flush was deferred and lost.
3296
+ */
3297
+ async getAuthoritativeFactHashIndex(): Promise<ContentHashIndex> {
3298
+ if (!(await this.ensureFactHashIndexAuthoritative())) {
3299
+ throw new FactHashIndexNotAuthoritativeError(this.stateDir);
3300
+ }
3301
+ return this.getFactHashIndex();
3302
+ }
3303
+
3304
+ /**
3305
+ * Return the shared fact-hash index instance, corpus-rebuilt under the lock
3306
+ * when the lock is free. Unlike {@link getAuthoritativeFactHashIndex} this
3307
+ * NEVER throws on lock contention — it returns the shared instance (the loaded
3308
+ * snapshot when a locked rebuild could not run) so registration writes still
3309
+ * land in the one shared index and reconcile to disk. Callers that trust a
3310
+ * dedup MISS MUST also consult {@link isFactContentHashAuthoritative} and
3311
+ * confirm against the corpus when it is false (PR #2016).
3312
+ */
3313
+ async getSharedFactHashIndex(): Promise<ContentHashIndex> {
3314
+ await this.ensureFactHashIndexAuthoritative();
3315
+ return this.getFactHashIndex();
3316
+ }
3317
+
3318
+ private async ensureFactHashIndexAuthoritative(): Promise<boolean> {
3622
3319
  if (this.factHashIndexAuthoritative === true) {
3623
- return;
3320
+ // PR #2016 review: authority is NOT permanent. A peer process can advance
3321
+ // the durable fact-hash index after our rebuild, leaving our in-memory
3322
+ // snapshot stale but still flagged authoritative — so a dedup MISS is
3323
+ // wrongly trusted and a duplicate active memory is written. Gate the fast
3324
+ // path on a cheap one-stat freshness check: when the durable index file is
3325
+ // unchanged since our last sync we stay authoritative (hot path preserved);
3326
+ // when a peer advanced it (or freshness cannot be established) drop
3327
+ // authority and rebuild from the corpus below so the miss confirms against
3328
+ // ground truth.
3329
+ if (this.factHashIndex && (await this.factHashIndex.isDiskFingerprintCurrent())) {
3330
+ return true;
3331
+ }
3332
+ this.factHashIndexAuthoritative = null;
3624
3333
  }
3625
3334
  if (this.factHashIndexAuthoritativePromise) {
3626
- await this.factHashIndexAuthoritativePromise;
3627
- return;
3335
+ return this.factHashIndexAuthoritativePromise;
3628
3336
  }
3629
3337
 
3338
+ this.factHashIndexAuthoritative = false;
3630
3339
  this.factHashIndexAuthoritativePromise = (async () => {
3631
- try {
3632
- await access(this.factHashIndexReadyPath);
3633
- this.factHashIndexAuthoritative = true;
3634
- return;
3635
- } catch {
3636
- // Fall through and backfill from the live fact corpus once.
3637
- }
3638
-
3340
+ // Round 11: ALWAYS rebuild the fact-hash index from the durable corpus on
3341
+ // first use per process — no on-disk "ready" marker is written or trusted,
3342
+ // so a deferred write, crash, or multi-process interleave can never leave a
3343
+ // stale index trusted. The scan AND publish run under the SAME per-index
3344
+ // cross-process lock the reconciling saves use (rebuildUnderLock), so the
3345
+ // rebuild can never overwrite a peer's newer lock-merged or deferred
3346
+ // additions with an unlocked overwrite.
3347
+ //
3348
+ // PR #2016 (findings 1-2): bounded-retry the LOCKED rebuild so transient
3349
+ // contention (a peer mid reconcile-save) clears within a short budget
3350
+ // instead of surrendering authority on the first miss. Each attempt uses
3351
+ // the same non-reentrant, bounded-wait file lock, so it can never deadlock
3352
+ // with a deferred reconcile-retry. On exhaustion the index is left
3353
+ // non-authoritative: getAuthoritativeFactHashIndex() fails explicitly and
3354
+ // hasFactContentHash()/isFactContentHashAuthoritative() fall back to the
3355
+ // durable corpus so a stale loaded snapshot never answers as current.
3639
3356
  const factHashIndex = await this.getFactHashIndex();
3640
- factHashIndex.clear();
3641
- const existing = await this.readAllMemories();
3642
- let legacyRecovered = 0;
3643
- for (const memory of existing) {
3644
- if (memory.frontmatter.category !== "fact") continue;
3645
- if (inferMemoryStatus(memory.frontmatter, memory.path) !== "active") continue;
3646
- // Prefer the pre-computed raw-content hash stored in frontmatter
3647
- // (written since round 8 of issue #369). This hash was derived from
3648
- // the content BEFORE citation annotation, so it matches what
3649
- // hasFactContentHash(rawFact) would compute.
3650
- if (memory.frontmatter.contentHash) {
3651
- factHashIndex.addByHash(memory.frontmatter.contentHash);
3652
- continue;
3653
- }
3654
- // Legacy fact written before contentHash was introduced (Finding 1 —
3655
- // Uhol). Apply nuanced handling based on whether the citation can be
3656
- // reliably stripped:
3657
- //
3658
- // 1. Default citation present → strip it and index the raw body.
3659
- // 2. No citation at all → index the raw body as-is.
3660
- // 3. Unknown/custom citation template → skip with a warning.
3661
- //
3662
- // Rationale for (3): for facts annotated with a custom citation
3663
- // template, stripCitationForTemplate cannot reliably detect the inline
3664
- // marker and would hash the cited body — producing a hash that never
3665
- // matches what hasFactContentHash(rawContent) computes. A
3666
- // false-negative miss (the fact is not in the index) is preferable to
3667
- // a wrong index entry that permanently suppresses legitimate duplicate
3668
- // writes.
3669
- //
3670
- // Limitation (Thread 2 — stale hash): even when contentHash IS present
3671
- // it may be stale if updateMemory() rewrote the body without updating
3672
- // the frontmatter hash. The hash is trusted as-is here; a future
3673
- // migration pass can recompute it from the current content.
3674
- const content = memory.content;
3675
- // Use the configured template (Thread 1 fix): citationTemplate is set
3676
- // by the orchestrator to the active inlineSourceAttributionFormat so
3677
- // the rebuild can strip both the default and any custom template.
3678
- // Falls back to DEFAULT_CITATION_FORMAT when the orchestrator has not
3679
- // configured a custom template (e.g. direct StorageManager construction
3680
- // in tests).
3681
- const stripped = stripCitationForTemplate(content, this.citationTemplate);
3682
- if (stripped !== content) {
3683
- // Citation was stripped — index the bare body.
3684
- factHashIndex.addByHash(ContentHashIndex.computeHash(sanitizeMemoryContent(stripped).text));
3685
- continue;
3686
- }
3687
- // No citation was removed. Decide whether to index or skip.
3688
- // Thread 4 fix: use hasCitation() rather than the too-broad endsWith("]")
3689
- // heuristic. Facts that legitimately end with "]" (e.g. "User prefers
3690
- // [dark mode]") have no citation marker and should be indexed as-is.
3691
- // Only skip when hasCitation() confirms a citation is present — that
3692
- // means the citation is from an unknown/custom template we cannot strip.
3693
- if (!hasCitation(content)) {
3694
- // Content has no recognisable citation marker — index raw body.
3695
- factHashIndex.addByHash(ContentHashIndex.computeHash(sanitizeMemoryContent(content).text));
3696
- continue;
3697
- }
3698
- // Content carries a citation from an unknown/custom template
3699
- // that we cannot safely strip. Skip rather than index a wrong hash.
3700
- legacyRecovered++;
3701
- continue;
3357
+ const maxAttempts =
3358
+ this.factHashIndexLockOptions.retryMaxAttempts ?? FACT_HASH_INDEX_REBUILD_MAX_ATTEMPTS;
3359
+ const baseMs =
3360
+ this.factHashIndexLockOptions.retryBaseMs ?? FACT_HASH_INDEX_REBUILD_RETRY_BASE_MS;
3361
+ let published = false;
3362
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
3363
+ published = await factHashIndex.rebuildUnderLock(() =>
3364
+ this.rebuildFactHashIndexFromCorpus(factHashIndex),
3365
+ );
3366
+ if (published || attempt === maxAttempts - 1) break;
3367
+ const wait = Math.min(baseMs * 2 ** attempt, CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS);
3368
+ await new Promise<void>((resolve) => setTimeout(resolve, wait));
3702
3369
  }
3703
- if (legacyRecovered > 0) {
3704
- log.info(
3705
- `ensureFactHashIndexAuthoritative: skipped ${legacyRecovered} legacy fact(s) with no contentHash in frontmatter`
3370
+ this.factHashIndexAuthoritative = published;
3371
+ if (!published) {
3372
+ log.warn(
3373
+ `ensureFactHashIndexAuthoritative: fact-hash index lock unavailable after ${maxAttempts} attempt(s); ` +
3374
+ `index left non-authoritative (reads verify against the durable corpus; next use retries the locked rebuild)`,
3706
3375
  );
3707
3376
  }
3708
- await factHashIndex.save();
3709
- await mkdir(path.dirname(this.factHashIndexReadyPath), { recursive: true });
3710
- await writeFile(this.factHashIndexReadyPath, "v1\n", "utf-8");
3711
- this.factHashIndexAuthoritative = true;
3377
+ return published;
3712
3378
  })().finally(() => {
3713
3379
  this.factHashIndexAuthoritativePromise = null;
3714
3380
  });
3715
- await this.factHashIndexAuthoritativePromise;
3381
+ return this.factHashIndexAuthoritativePromise;
3382
+ }
3383
+
3384
+ /**
3385
+ * Repopulate `factHashIndex` in memory from the durable HOT+COLD corpus. Runs
3386
+ * WHILE the per-index cross-process lock is held (see
3387
+ * ContentHashIndex.rebuildUnderLock, which publishes the rebuilt set); this
3388
+ * method itself never writes to disk.
3389
+ */
3390
+ private async rebuildFactHashIndexFromCorpus(factHashIndex: ContentHashIndex): Promise<void> {
3391
+ factHashIndex.clear();
3392
+ // Fact-ONLY membership rebuilt in lockstep (PR #2016): hasFactContentHash
3393
+ // reads THIS set, never the category-agnostic shared index below, so an
3394
+ // over-included non-fact body can never satisfy a fact-hash check. Repopulate
3395
+ // the LIVE set in place (clear + add) rather than building a fresh set and
3396
+ // reassigning: a concurrent in-process writeMemory that adds a fact hash
3397
+ // during the corpus-read awaits below would be lost by a publish-time
3398
+ // reassignment (PR #2016 thread SDzOT), exactly as the shared index avoids
3399
+ // by mutating its `hashes` set in place.
3400
+ this.factOnlyHashes.clear();
3401
+ // #1909 review round 14: index the HOT and COLD tiers together. A fact or
3402
+ // procedure demoted to cold/ is still active and its content-hash must
3403
+ // survive the corpus rebuild, or a restart would drop the hash and let the
3404
+ // next extraction re-create the demoted memory. Matches the hot+cold union
3405
+ // that removeFactContentHashesForMemories already reconciles against.
3406
+ const existing = [
3407
+ ...(await this.readAllMemories()),
3408
+ ...(await this.readAllColdMemories()),
3409
+ ];
3410
+ let legacyRecovered = 0;
3411
+ for (const memory of existing) {
3412
+ // #1909 review round 15 (PR #2016): the content-hash dedup index is
3413
+ // SHARED across EVERY registered write category. persistExtraction calls
3414
+ // addContentHashDedup for every writeCategory it persists — fact,
3415
+ // procedure, preference, decision, commitment, correction, and any other
3416
+ // extracted category — into THIS one index. A rebuild restricted to
3417
+ // fact+procedure dropped every other category's hash on restart, so the
3418
+ // next extraction re-created identical active preference/decision/
3419
+ // commitment memories (the retired fact-hashes.txt load used to preserve
3420
+ // them). Index every active memory regardless of category so the corpus
3421
+ // rebuild covers the full registration surface. Over-inclusion is safe:
3422
+ // the dedup consumers (hasContentHashDedup in persistExtraction, the
3423
+ // explicit-capture negative pre-filter) confirm a hash hit with a
3424
+ // same-category corpus scan before dropping anything, so a surplus hash
3425
+ // only costs a scan — it can never wrongly suppress a write. Procedures
3426
+ // keep their round-13 behaviour: they carry no frontmatter contentHash
3427
+ // (writeMemory sets it only for facts), so they fall through to the
3428
+ // citation-strip reconstruction below, hashing the stored persist body
3429
+ // (title + steps) exactly as buildProcedurePersistBody registered it.
3430
+ if (inferMemoryStatus(memory.frontmatter, memory.path) !== "active") continue;
3431
+ const hash = this.corpusRegisteredHash(memory);
3432
+ if (hash === null) {
3433
+ // Body carries a citation from an unknown/custom template we cannot
3434
+ // safely strip — skip rather than register a wrong hash. A
3435
+ // false-negative miss beats a wrong entry that would permanently
3436
+ // suppress legitimate duplicate writes (see corpusRegisteredHash).
3437
+ legacyRecovered++;
3438
+ continue;
3439
+ }
3440
+ factHashIndex.addByHash(hash);
3441
+ if (memory.frontmatter.category === "fact") this.factOnlyHashes.add(hash);
3442
+ }
3443
+ if (legacyRecovered > 0) {
3444
+ log.info(
3445
+ `ensureFactHashIndexAuthoritative: skipped ${legacyRecovered} legacy memory(ies) with no contentHash in frontmatter`
3446
+ );
3447
+ }
3448
+ }
3449
+
3450
+ /**
3451
+ * The content-hash the corpus rebuild registers for `memory`, or null when the
3452
+ * body carries a citation from an unknown/custom template that cannot be
3453
+ * safely stripped. Shared by the authoritative rebuild and the fact-only
3454
+ * corpus confirmation (`hasFactContentHash`) so both derive the identical
3455
+ * hash for a given stored body.
3456
+ *
3457
+ * Preference order:
3458
+ * 1. frontmatter.contentHash — the raw pre-citation hash writeMemory records
3459
+ * for facts (issue #369 round 8); matches hasFactContentHash(rawFact).
3460
+ * 2. Reconstruct from the stored body: strip the "[Attributes: …]" suffix
3461
+ * writeMemory appends for structuredAttributes (registration hashed the
3462
+ * raw canonical content WITHOUT it — a no-op when absent), then strip a
3463
+ * recognised citation and hash the bare body, or hash a citation-free
3464
+ * body as-is.
3465
+ * 3. A body with a citation from an unknown/custom template → null: a
3466
+ * false-negative miss beats a wrong hash that would permanently suppress
3467
+ * legitimate duplicate writes.
3468
+ */
3469
+ private corpusRegisteredHash(memory: MemoryFile): string | null {
3470
+ if (memory.frontmatter.contentHash) {
3471
+ return memory.frontmatter.contentHash;
3472
+ }
3473
+ const content = stripAttributesSuffix(memory.content);
3474
+ const stripped = stripCitationForTemplate(content, this.citationTemplate);
3475
+ if (stripped !== content) {
3476
+ return ContentHashIndex.computeHash(sanitizeMemoryContent(stripped).text);
3477
+ }
3478
+ if (!hasCitation(content)) {
3479
+ return ContentHashIndex.computeHash(sanitizeMemoryContent(content).text);
3480
+ }
3481
+ return null;
3716
3482
  }
3717
3483
  private get questionsDir(): string {
3718
3484
  return path.join(this.baseDir, "questions");
@@ -3765,6 +3531,27 @@ export class StorageManager {
3765
3531
  private get behaviorSignalsPath(): string {
3766
3532
  return path.join(this.stateDir, "behavior-signals.jsonl");
3767
3533
  }
3534
+ /**
3535
+ * In-memory dedup key set for behavior-signals appends (issue #1909),
3536
+ * validated by (size, mtime) file identity like the catalog `compactedCache`.
3537
+ * Avoids re-reading + JSON.parsing the whole `behavior-signals.jsonl` on
3538
+ * every append. A foreign write changes size/mtime, forcing a reload; this
3539
+ * process's own appends refresh the identity in place. null => reload.
3540
+ */
3541
+ private behaviorSignalsKeyCache:
3542
+ | { identity: { size: number; mtimeMs: number }; keys: Set<string> }
3543
+ | null = null;
3544
+ /**
3545
+ * Per-instance serializer for `appendBehaviorSignals` (issue #1909 review
3546
+ * round 3). The read→dedup→append→cache-commit transaction must run to
3547
+ * completion before the next append starts; otherwise two concurrent callers
3548
+ * each snapshot their own `existingKeys`, and the later one commits an
3549
+ * INCOMPLETE set under the final file identity, so a subsequent call
3550
+ * cache-hits a set missing the earlier batch and writes duplicate signals.
3551
+ * The `.catch` links keep the chain alive after a rejected append
3552
+ * (AGENTS.md #28).
3553
+ */
3554
+ private behaviorSignalsAppendChain: Promise<unknown> = Promise.resolve();
3768
3555
  /**
3769
3556
  * Buffer surprise telemetry ledger (issue #563 PR 3).
3770
3557
  *
@@ -3892,71 +3679,7 @@ export class StorageManager {
3892
3679
  async writeMemory(
3893
3680
  category: MemoryCategory,
3894
3681
  content: string,
3895
- options: {
3896
- actor?: string;
3897
- confidence?: number;
3898
- tags?: string[];
3899
- entityRef?: string;
3900
- source?: string;
3901
- supersedes?: string;
3902
- lineage?: string[];
3903
- importance?: ImportanceScore;
3904
- links?: MemoryLink[];
3905
- intentGoal?: string;
3906
- intentActionType?: string;
3907
- intentEntityTypes?: string[];
3908
- artifactType?: MemoryFrontmatter["artifactType"];
3909
- sourceMemoryId?: string;
3910
- sourceTurnId?: string;
3911
- memoryKind?: MemoryFrontmatter["memoryKind"];
3912
- expiresAt?: string;
3913
- validAt?: string;
3914
- // Issue #1578 — bi-temporal ingestion provenance. `observedAt` is the
3915
- // ingestion time (when Remnic learned the fact); `eventTimeSource`
3916
- // records whether `validAt` was resolved from an extracted expression
3917
- // or assumed from the ingestion anchor. Both validate on serialize.
3918
- observedAt?: string;
3919
- eventTimeSource?: "extracted" | "assumed";
3920
- invalidAt?: string;
3921
- structuredAttributes?: Record<string, string>;
3922
- /**
3923
- * When provided, this string is used as the source for the fact-content
3924
- * dedup hash index instead of the persisted body (`content`).
3925
- *
3926
- * Use this when the persisted body differs from the canonical fact text
3927
- * — for example when `content` is a citation-annotated variant of a raw
3928
- * fact. Passing the raw fact as `contentHashSource` ensures that
3929
- * `hasFactContentHash(rawFact)` returns `true` after the write, so
3930
- * subsequent extractions of the same logical fact are correctly deduped
3931
- * even when their citation timestamp differs.
3932
- */
3933
- contentHashSource?: string;
3934
- status?: MemoryStatus;
3935
- /**
3936
- * Consolidation provenance (issue #561 PR 2). When the caller is a
3937
- * consolidation / supersession / dedup-merge path, these fields wire
3938
- * the page-version snapshots the new memory was derived from and the
3939
- * operator that produced it. Persisted onto frontmatter as
3940
- * `derived_from` + `derived_via`; validated at serialize time.
3941
- */
3942
- derivedFrom?: string[];
3943
- derivedVia?: ConsolidationOperator;
3944
- /**
3945
- * Faithfulness gate verdict (issue #1576). When provided, persisted to
3946
- * frontmatter so downstream readers (TrustScore #1577, review queue)
3947
- * can consume it. Absent = gate was off or fact predates #1576.
3948
- */
3949
- faithfulness?: import("./types.js").FaithfulnessFrontmatter;
3950
- /**
3951
- * Claim-level provenance spans (issue #1575 PR 2). When provided,
3952
- * persisted to frontmatter so downstream readers (memory_get, x-ray,
3953
- * faithfulness gate #1576) can consume them. Absent = fact predates
3954
- * #1575 or provenance was disabled.
3955
- */
3956
- sources?: ProvenanceSource[];
3957
- provenance?: "verified" | "unverified" | "none";
3958
- sourceConnector?: string;
3959
- } = {}
3682
+ options: WriteMemoryOptions = {}
3960
3683
  ): Promise<MemoryWriteResult> {
3961
3684
  await this.ensureDirectories();
3962
3685
  const now = new Date();
@@ -4039,17 +3762,10 @@ export class StorageManager {
4039
3762
  fm.sourceConnector = options.sourceConnector;
4040
3763
  }
4041
3764
 
4042
- // Append structured attributes as searchable suffix so QMD indexes them.
4043
- // normalizeAttributePairs sorts and lowercases keys so the enriched content
4044
- // is stable regardless of the insertion order or key casing supplied by the
4045
- // caller this must stay in sync with the dedupContent built in the
4046
- // orchestrator's hash-dedup path.
4047
- let enrichedContent = content;
4048
- if (options.structuredAttributes && Object.keys(options.structuredAttributes).length > 0) {
4049
- enrichedContent = `${content}\n[Attributes: ${normalizeAttributePairs(options.structuredAttributes)}]`;
4050
- }
4051
-
4052
- const sanitized = sanitizeMemoryContent(enrichedContent);
3765
+ // Assemble the persisted body (attribute-suffix enrichment + combined
3766
+ // sanitize) via the SHARED helper the sealed-envelope composer uses the
3767
+ // same one, so the two write paths cannot diverge (issue #1989 PR2).
3768
+ const sanitized = assemblePersistedBody(content, options.structuredAttributes);
4053
3769
  if (!sanitized.clean) {
4054
3770
  log.warn(`memory content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
4055
3771
  }
@@ -4133,6 +3849,7 @@ export class StorageManager {
4133
3849
  await this.snapshotBeforeWrite(filePath, "write");
4134
3850
  await this.writeStorageSecureFile(filePath, fileContent);
4135
3851
  await this.patchHotMemoriesCache({ addedPath: filePath }, "memory-create");
3852
+ this.notifyMemoryWrite(filePath);
4136
3853
  await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.writeMemory", {
4137
3854
  memoryId: id,
4138
3855
  eventType: "created",
@@ -4155,13 +3872,31 @@ export class StorageManager {
4155
3872
  // fact text before citation annotation), index THAT string so that
4156
3873
  // hasFactContentHash(rawFact) returns true on subsequent extractions.
4157
3874
  // Otherwise fall back to the sanitized persisted body as before.
4158
- if (options.contentHashSource !== undefined && options.contentHashSource.length > 0) {
4159
- const hashSourceSanitized = sanitizeMemoryContent(options.contentHashSource);
4160
- factHashIndex.add(hashSourceSanitized.text);
4161
- } else {
4162
- factHashIndex.add(sanitized.text);
3875
+ const hashText =
3876
+ options.contentHashSource !== undefined && options.contentHashSource.length > 0
3877
+ ? sanitizeMemoryContent(options.contentHashSource).text
3878
+ : sanitized.text;
3879
+ factHashIndex.add(hashText);
3880
+ this.factOnlyHashes.add(ContentHashIndex.computeHash(hashText));
3881
+ // Gate only the flush (issue #1909): the `.add(...)` above already set
3882
+ // dirty=true. When the caller defers, it owns the batch save
3883
+ // (extraction persist -> saveContentHashIndexes()). Single-write callers
3884
+ // (explicit capture, import, wearable, native writes) flush immediately —
3885
+ // via the SAME cross-process locked reconcile the batch/append and
3886
+ // rebuild paths use (PR #2016 thread SDyCk), never the unlocked whole-file
3887
+ // save() that could clobber, or be clobbered by, a peer's concurrent
3888
+ // locked rebuild/reconcile and drop this durable fact from the index.
3889
+ if (!options.deferHashIndexSave) {
3890
+ await factHashIndex.saveMergingWithDisk();
3891
+ // A locked reconcile that times out defers to an unref'd background
3892
+ // retry and returns WITHOUT publishing (dirty retained). A single-write
3893
+ // caller must not observe that as durable (PR #2016 thread SD7Tk):
3894
+ // drain the deferred retry inline so the addition lands on disk (or
3895
+ // exhausts its bounded attempts, falling back to the corpus-rebuild
3896
+ // safety net) before writeMemory returns. No-op when the save already
3897
+ // published (not dirty).
3898
+ await factHashIndex.flushReconcileRetry();
4163
3899
  }
4164
- await factHashIndex.save();
4165
3900
  } catch (err) {
4166
3901
  log.warn(`storage.writeMemory completed but failed to update fact hash index: ${err}`);
4167
3902
  }
@@ -4170,11 +3905,73 @@ export class StorageManager {
4170
3905
  return { id, tombstoneBlocked, ...(fm.blockedBy ? { blockedBy: fm.blockedBy } : {}) };
4171
3906
  }
4172
3907
 
3908
+ /**
3909
+ * Sealed-envelope write entry point (issue #1989 PR2).
3910
+ *
3911
+ * Byte-identity with `writeMemory` is BY DELEGATION: the envelope-owned
3912
+ * fields are unpacked into the exact `writeMemory` arguments a legacy
3913
+ * caller would have passed, so the persisted output is produced by the
3914
+ * same code path. The composer's `persistedBody` was assembled with the
3915
+ * same `assemblePersistedBody` helper `writeMemory` uses, so fingerprints
3916
+ * derived from the envelope match the stored body (§13).
3917
+ *
3918
+ * Differences from a raw `writeMemory` call are REJECTIONS, not silent
3919
+ * changes: the composer enforces tag/attribute caps, strict validAt, and
3920
+ * plain-object attributes that the legacy path never validated.
3921
+ *
3922
+ * `envelope.ttl` maps to `expiresAt` verbatim; converting duration
3923
+ * expressions (`"90d"`) to instants remains the access layer's job (PR3).
3924
+ * `envelope.sourceReason` is access-layer metadata with no frontmatter
3925
+ * field and is deliberately not persisted here.
3926
+ */
3927
+ async writeSealedMemory(
3928
+ envelope: SealedMemoryEnvelope,
3929
+ extras: SealedWriteExtras = {},
3930
+ ): Promise<MemoryWriteResult> {
3931
+ if (!isSealedMemoryEnvelope(envelope)) {
3932
+ throw new Error(
3933
+ "writeSealedMemory: value is not a valid sealed memory envelope (fails the composeMemoryEnvelope contract)",
3934
+ );
3935
+ }
3936
+ const { category, content, options } = sealedWriteToLegacyArgs(envelope, extras);
3937
+ return this.writeMemory(category, content, options as WriteMemoryOptions);
3938
+ }
3939
+
4173
3940
  async hasFactContentHash(content: string): Promise<boolean> {
4174
- await this.ensureFactHashIndexAuthoritative();
4175
- const factHashIndex = await this.getFactHashIndex();
3941
+ const authoritative = await this.ensureFactHashIndexAuthoritative();
4176
3942
  const sanitized = sanitizeMemoryContent(content);
4177
- return factHashIndex.has(sanitized.text);
3943
+ const hash = ContentHashIndex.computeHash(sanitized.text);
3944
+ // Fact-ONLY answer (PR #2016). The shared content-hash index is
3945
+ // category-agnostic — the round-15 authoritative rebuild indexes EVERY
3946
+ // active category and addContentHashDedup registers all of them — so it
3947
+ // cannot tell a FACT from an unrelated preference/decision/note/moment with
3948
+ // the same normalized body. Direct consumers
3949
+ // (createWearableMemoryWriter.hasFactContentHash, the explicit-capture
3950
+ // negative pre-filter) treat a hit as terminal BEFORE their own source/
3951
+ // category confirmation, so answering from the shared index would let a
3952
+ // non-fact suppress a real fact candidate. Only category === "fact" registers
3953
+ // a fact-content hash at write time (writeMemory), so answer from the
3954
+ // fact-only membership rebuilt in lockstep with the shared index. When the
3955
+ // index is authoritative that set is current (rebuild + write/removal
3956
+ // upkeep); otherwise the snapshot may be stale, so verify against the durable
3957
+ // fact corpus (ground truth) so a lock-contended read never suppresses a fact.
3958
+ if (authoritative) {
3959
+ return this.factOnlyHashes.has(hash);
3960
+ }
3961
+ return await this.factContentHashPresentInCorpus(hash);
3962
+ }
3963
+
3964
+ private async factContentHashPresentInCorpus(targetHash: string): Promise<boolean> {
3965
+ const existing = [
3966
+ ...(await this.readAllMemories()),
3967
+ ...(await this.readAllColdMemories()),
3968
+ ];
3969
+ for (const memory of existing) {
3970
+ if (memory.frontmatter.category !== "fact") continue;
3971
+ if (inferMemoryStatus(memory.frontmatter, memory.path) !== "active") continue;
3972
+ if (this.corpusRegisteredHash(memory) === targetHash) return true;
3973
+ }
3974
+ return false;
4178
3975
  }
4179
3976
 
4180
3977
  private factContentHashForRemoval(memory: MemoryFile): string | null {
@@ -4199,7 +3996,22 @@ export class StorageManager {
4199
3996
  await this.ensureFactHashIndexAuthoritative();
4200
3997
  const factHashIndex = await this.getFactHashIndex();
4201
3998
  factHashIndex.addByHash(hash);
4202
- await factHashIndex.save();
3999
+ this.factOnlyHashes.add(hash);
4000
+ // PR #2016 thread SDzOP: flush through the SAME cross-process locked
4001
+ // reconcile the write/batch/rebuild paths use, never the unlocked whole-file
4002
+ // save() — an unlocked overwrite drops a concurrent extraction's appended
4003
+ // hash and can be clobbered by a peer's locked publish. saveMergingWithDisk
4004
+ // republishes only OUR delta ((on-disk \ removed) ∪ added) under the lock.
4005
+ await factHashIndex.saveMergingWithDisk();
4006
+ // A locked reconcile that times out defers to an unref'd background retry
4007
+ // and returns WITHOUT publishing (dirty retained). The reactivation path is
4008
+ // a lifecycle boundary just like writeMemory (PR #2016 thread
4009
+ // PRRT_kwDORJXyws6SEHvh): a short-lived caller must not observe the deferral
4010
+ // as durable. Drain the deferred retry inline so the reintroduced hash lands
4011
+ // on disk (or exhausts its bounded attempts, falling back to the
4012
+ // corpus-rebuild safety net) before returning. No-op — no duplicated retry
4013
+ // work — when the save already published (not dirty).
4014
+ await factHashIndex.flushReconcileRetry();
4203
4015
  }
4204
4016
 
4205
4017
  /**
@@ -4268,14 +4080,38 @@ export class StorageManager {
4268
4080
  for (const hash of removedHashes.values()) {
4269
4081
  if (!remainingActiveHashes.has(hash)) {
4270
4082
  factHashIndex.removeByHash(hash);
4083
+ this.factOnlyHashes.delete(hash);
4271
4084
  }
4272
4085
  }
4273
- await factHashIndex.save();
4086
+ // PR #2016 thread SDzOP: serialize the removal with the per-index lock via
4087
+ // the removal-aware reconcile, never the unlocked whole-file save(). The
4088
+ // unlocked overwrite republished this instance's stale in-memory set,
4089
+ // silently clobbering a concurrent extraction's appended hash; the reconcile
4090
+ // reads the latest on-disk state, drops only OUR removed hashes, and keeps a
4091
+ // peer's concurrent append.
4092
+ await factHashIndex.saveMergingWithDisk();
4093
+ }
4094
+
4095
+ /**
4096
+ * Remove a memory's fact-ONLY hash membership in lockstep with the shared,
4097
+ * category-agnostic index removal the orchestrator's
4098
+ * `removeContentHashForMemory` performs on archival / semantic consolidation
4099
+ * (PR #2016 threads SDzOP / SDzOR). Without this, `factOnlyHashes` kept a
4100
+ * removed/superseded fact's hash and `hasFactContentHash` returned a stale
4101
+ * `true` until the next corpus rebuild, so wearable / explicit-capture /
4102
+ * promotion callers skipped a valid write. In-memory only — `factOnlyHashes`
4103
+ * is never persisted; it is rebuilt from the corpus. No-op for non-facts.
4104
+ * Matches the shared index's unconditional per-hash removal so the two stay
4105
+ * coherent.
4106
+ */
4107
+ removeFactOnlyHashForMemory(memory: MemoryFile): void {
4108
+ if (memory.frontmatter.category !== "fact") return;
4109
+ const hash = this.factContentHashForRemoval(memory);
4110
+ if (hash) this.factOnlyHashes.delete(hash);
4274
4111
  }
4275
4112
 
4276
4113
  async isFactContentHashAuthoritative(): Promise<boolean> {
4277
- await this.ensureFactHashIndexAuthoritative();
4278
- return true;
4114
+ return await this.ensureFactHashIndexAuthoritative();
4279
4115
  }
4280
4116
 
4281
4117
  async writeArtifact(
@@ -5468,20 +5304,77 @@ export class StorageManager {
5468
5304
  async appendMemoryLifecycleEvents(events: MemoryLifecycleEvent[]): Promise<number> {
5469
5305
  if (events.length === 0) return 0;
5470
5306
  await this.ensureDirectories();
5307
+ // Lock-serialized against compaction; on lock-budget exhaustion the event
5308
+ // spills to a durable encrypted pending queue, not dropped (#1910, #2033).
5309
+ await appendLifecycleEventsSerialized(
5310
+ this.memoryLifecycleLedgerPath,
5311
+ (p) => this.appendStorageSecureFile(this.memoryLifecycleLedgerPath, p),
5312
+ serializeLifecycleAppendPayload(events),
5313
+ { writeSecure: (p, c) => this.writeStorageSecureFile(p, c), readSecure: (p) => this.readStorageSecureFile(p) },
5314
+ );
5315
+ return events.length;
5316
+ }
5471
5317
 
5472
- const nowIso = new Date().toISOString();
5473
- const payload = events
5474
- .map((event) => {
5475
- const normalized: MemoryLifecycleEvent = {
5476
- ...event,
5477
- timestamp: event.timestamp && event.timestamp.length > 0 ? event.timestamp : nowIso,
5478
- };
5479
- return `${JSON.stringify(normalized)}\n`;
5480
- })
5481
- .join("");
5318
+ /** Drain the durable pending lifecycle-append spill into the ledger (#2033);
5319
+ * fast no-op when nothing is pending. Returns true when rows were drained. */
5320
+ async drainPendingMemoryLifecycleEvents(): Promise<boolean> {
5321
+ return drainPendingLifecycleLedgerIfAny(
5322
+ this.memoryLifecycleLedgerPath,
5323
+ { writeSecure: (p, c) => this.writeStorageSecureFile(p, c), readSecure: (p) => this.readStorageSecureFile(p) },
5324
+ (p) => this.appendStorageSecureFile(this.memoryLifecycleLedgerPath, p),
5325
+ () => this.ensureDirectories(),
5326
+ );
5327
+ }
5482
5328
 
5483
- await this.appendStorageSecureFile(this.memoryLifecycleLedgerPath, payload);
5484
- return events.length;
5329
+ /** Offline-sync pre-snapshot drain (#2033): fold pending lifecycle spills into
5330
+ * the active ledger and report whether durable rows STILL remain in the
5331
+ * offline-sync-EXCLUDED pending queue. The caller aborts the snapshot when
5332
+ * `pendingDeferred` is true so append-only rows are never silently omitted. */
5333
+ async drainPendingMemoryLifecycleEventsForSync(): Promise<DrainPendingLifecycleForSyncResult> {
5334
+ return drainPendingLifecycleLedgerForSync(
5335
+ this.memoryLifecycleLedgerPath,
5336
+ { writeSecure: (p, c) => this.writeStorageSecureFile(p, c), readSecure: (p) => this.readStorageSecureFile(p) },
5337
+ (p) => this.appendStorageSecureFile(this.memoryLifecycleLedgerPath, p),
5338
+ () => this.ensureDirectories(),
5339
+ );
5340
+ }
5341
+
5342
+ /** Drain pending lifecycle spills for any ledger inside this storage root.
5343
+ * The offline CLI uses this path-aware variant for per-namespace ledgers so
5344
+ * secure-store encryption and path-bound authentication remain consistent. */
5345
+ async drainPendingMemoryLifecycleEventsForSyncAt(
5346
+ ledgerPath: string,
5347
+ ): Promise<DrainPendingLifecycleForSyncResult> {
5348
+ const target = this.assertManagedStoragePath(
5349
+ ledgerPath,
5350
+ "storage.drainPendingMemoryLifecycleEventsForSyncAt",
5351
+ );
5352
+ return drainPendingLifecycleLedgerForSync(
5353
+ target,
5354
+ {
5355
+ writeSecure: (p, c) => this.writeStorageSecureFile(p, c),
5356
+ readSecure: (p) => this.readStorageSecureFile(p),
5357
+ },
5358
+ (payload) => this.appendStorageSecureFile(target, payload),
5359
+ async () => {
5360
+ await mkdir(path.dirname(target), { recursive: true });
5361
+ },
5362
+ );
5363
+ }
5364
+
5365
+ /** Rewrite the ledger through the secure writer (#1910); re-encrypts when unlocked, throws when locked.
5366
+ * `targetPath` re-encrypts under a decryptable backup path's own AAD;
5367
+ * Buffer content passes through verbatim (#2033). `forceEncrypt` preserves
5368
+ * encryption at rest for an already-encrypted ledger/backup even when the
5369
+ * `secureStoreEncryptOnWrite` policy is paused, so a compaction never
5370
+ * downgrades encrypted state to plaintext (#2033); it never bypasses the lock. */
5371
+ async writeMemoryLifecycleLedgerContent(
5372
+ content: string | Buffer,
5373
+ targetPath: string = this.memoryLifecycleLedgerPath,
5374
+ forceEncrypt = false,
5375
+ ): Promise<void> {
5376
+ await this.ensureDirectories();
5377
+ await this.writeStorageSecureFile(targetPath, content, forceEncrypt);
5485
5378
  }
5486
5379
 
5487
5380
  async appendBufferSurpriseEvents(events: BufferSurpriseEvent[]): Promise<number> {
@@ -5493,46 +5386,113 @@ export class StorageManager {
5493
5386
  }
5494
5387
 
5495
5388
  async appendBehaviorSignals(events: BehaviorSignalEvent[]): Promise<number> {
5389
+ if (events.length === 0) return 0;
5390
+ // Serialize the whole read→dedup→append→cache-commit transaction per
5391
+ // instance (issue #1909 review round 3) so concurrent callers cannot each
5392
+ // commit an incomplete dedup set. The chain recovers after a rejection.
5393
+ const run = this.behaviorSignalsAppendChain
5394
+ .catch(() => undefined)
5395
+ .then(() => this.appendBehaviorSignalsUnlocked(events));
5396
+ this.behaviorSignalsAppendChain = run.catch(() => undefined);
5397
+ return run;
5398
+ }
5399
+
5400
+ private async appendBehaviorSignalsUnlocked(events: BehaviorSignalEvent[]): Promise<number> {
5496
5401
  if (events.length === 0) return 0;
5497
5402
  await this.ensureDirectories();
5498
5403
 
5499
- let existingKeys = new Set<string>();
5404
+ let existingKeys: Set<string>;
5405
+ let identity: { size: number; mtimeMs: number } | null = null;
5500
5406
  try {
5501
- const raw = await this.readStorageSecureFile(this.behaviorSignalsPath);
5502
- const lines = raw.split("\n");
5503
- for (const line of lines) {
5504
- const row = line.trim();
5505
- if (!row) continue;
5506
- try {
5507
- const parsed = JSON.parse(row) as Partial<BehaviorSignalEvent>;
5508
- if (typeof parsed.memoryId === "string" && typeof parsed.signalHash === "string") {
5509
- existingKeys.add(`${parsed.memoryId}:${parsed.signalHash}`);
5407
+ const st = await stat(this.behaviorSignalsPath);
5408
+ identity = { size: st.size, mtimeMs: st.mtimeMs };
5409
+ if (
5410
+ this.behaviorSignalsKeyCache &&
5411
+ this.behaviorSignalsKeyCache.identity.size === identity.size &&
5412
+ this.behaviorSignalsKeyCache.identity.mtimeMs === identity.mtimeMs
5413
+ ) {
5414
+ // Cache hit (issue #1909): reuse the dedup set no read, no parse.
5415
+ existingKeys = this.behaviorSignalsKeyCache.keys;
5416
+ } else {
5417
+ // Foreign change (or first load): rebuild by streaming the file so we
5418
+ // never materialize the whole (unbounded) ledger as one string.
5419
+ existingKeys = new Set<string>();
5420
+ for await (const line of readMaybeEncryptedLines(this.behaviorSignalsPath, () =>
5421
+ this.readStorageSecureFile(this.behaviorSignalsPath)
5422
+ )) {
5423
+ const row = line.trim();
5424
+ if (!row) continue;
5425
+ try {
5426
+ const parsed = JSON.parse(row) as Partial<BehaviorSignalEvent>;
5427
+ if (typeof parsed.memoryId === "string" && typeof parsed.signalHash === "string") {
5428
+ existingKeys.add(`${parsed.memoryId}:${parsed.signalHash}`);
5429
+ }
5430
+ } catch {
5431
+ // Ignore malformed rows (fail-open).
5510
5432
  }
5511
- } catch {
5512
- // Ignore malformed rows (fail-open).
5513
5433
  }
5514
5434
  }
5515
5435
  } catch (err) {
5516
5436
  if (err instanceof SecureStoreLockedError) throw err;
5517
5437
  if (!isErrnoCode(err, "ENOENT")) throw err;
5518
5438
  existingKeys = new Set<string>();
5439
+ identity = null;
5519
5440
  }
5520
5441
 
5521
5442
  const nowIso = new Date().toISOString();
5443
+ // Collect newly-seen keys in a SEPARATE set — never mutate `existingKeys`
5444
+ // (which may alias the cached set) before the append is durable. A failed
5445
+ // append must leave the cached dedup set matching what is actually on disk,
5446
+ // otherwise a retry would cache-hit a poisoned set and silently drop the
5447
+ // events forever (issue #1909 review: pre-durability aliasing).
5448
+ const pending = new Set<string>();
5522
5449
  const deduped: BehaviorSignalEvent[] = [];
5523
5450
  for (const event of events) {
5524
5451
  const key = `${event.memoryId}:${event.signalHash}`;
5525
- if (existingKeys.has(key)) continue;
5526
- existingKeys.add(key);
5452
+ if (existingKeys.has(key) || pending.has(key)) continue;
5453
+ pending.add(key);
5527
5454
  deduped.push({
5528
5455
  ...event,
5529
5456
  timestamp: event.timestamp && event.timestamp.length > 0 ? event.timestamp : nowIso,
5530
5457
  });
5531
5458
  }
5532
5459
 
5533
- if (deduped.length === 0) return 0;
5460
+ if (deduped.length === 0) {
5461
+ // Nothing appended — the file identity is unchanged and `existingKeys` was
5462
+ // not mutated, so caching it lets the next append hit (issue #1909).
5463
+ if (identity) this.behaviorSignalsKeyCache = { identity, keys: existingKeys };
5464
+ return 0;
5465
+ }
5534
5466
  const payload = deduped.map((event) => `${JSON.stringify(event)}\n`).join("");
5467
+ // May throw (I/O error, SecureStoreLockedError). If it does, we fall through
5468
+ // to the caller WITHOUT having touched `existingKeys` or the cache, so the
5469
+ // dropped events can be retried.
5535
5470
  await this.appendStorageSecureFile(this.behaviorSignalsPath, payload);
5471
+ // Durable now: fold the new keys into the set (in place, O(new keys) — keeps
5472
+ // the per-append win). Only cache the refreshed identity if the file grew by
5473
+ // EXACTLY our payload — otherwise a foreign writer (another instance/process)
5474
+ // interleaved and the file now holds rows whose keys are NOT in existingKeys;
5475
+ // caching that identity would let a later same-instance append cache-hit an
5476
+ // incomplete set and write duplicates (review round 8 thread 5). On any
5477
+ // mismatch, invalidate so the next append reloads from disk.
5478
+ for (const key of pending) existingKeys.add(key);
5479
+ try {
5480
+ const st = await stat(this.behaviorSignalsPath);
5481
+ const expectedSize = (identity?.size ?? 0) + Buffer.byteLength(payload, "utf-8");
5482
+ if (st.size === expectedSize) {
5483
+ this.behaviorSignalsKeyCache = {
5484
+ identity: { size: st.size, mtimeMs: st.mtimeMs },
5485
+ keys: existingKeys,
5486
+ };
5487
+ } else {
5488
+ // Foreign interleave (or encrypted whole-file rewrite): our key set may
5489
+ // be missing peer rows — force a reload on the next append.
5490
+ this.behaviorSignalsKeyCache = null;
5491
+ }
5492
+ } catch {
5493
+ // Fail-open: force a reload on the next append.
5494
+ this.behaviorSignalsKeyCache = null;
5495
+ }
5536
5496
  return deduped.length;
5537
5497
  }
5538
5498
 
@@ -5649,42 +5609,30 @@ export class StorageManager {
5649
5609
  }
5650
5610
 
5651
5611
  async readAllMemoryLifecycleEvents(): Promise<MemoryLifecycleEvent[]> {
5652
- try {
5653
- const out: MemoryLifecycleEvent[] = [];
5654
- for await (const line of readMaybeEncryptedLines(this.memoryLifecycleLedgerPath, () =>
5655
- this.readStorageSecureFile(this.memoryLifecycleLedgerPath)
5656
- )) {
5657
- const row = line.trim();
5658
- if (!row) continue;
5659
- try {
5660
- const parsed = JSON.parse(row) as Partial<MemoryLifecycleEvent>;
5661
- if (
5662
- typeof parsed.eventId === "string" &&
5663
- typeof parsed.memoryId === "string" &&
5664
- typeof parsed.eventType === "string" &&
5665
- typeof parsed.timestamp === "string" &&
5666
- typeof parsed.actor === "string" &&
5667
- typeof parsed.ruleVersion === "string"
5668
- ) {
5669
- out.push(parsed as MemoryLifecycleEvent);
5670
- }
5671
- } catch {
5672
- // Ignore malformed rows (fail-open).
5673
- }
5674
- }
5675
- return sortMemoryLifecycleEvents(out);
5676
- } catch (err) {
5677
- if (err instanceof SecureStoreLockedError) throw err;
5678
- if (!isErrnoCode(err, "ENOENT")) throw err;
5679
- return [];
5612
+ return readAllLifecycleEventsFromLedger(this.memoryLifecycleLedgerPath, (p) => this.readStorageSecureFile(p));
5613
+ }
5614
+
5615
+ /** Raw decrypted ledger bytes as a Buffer, never a string, so an oversized
5616
+ * ledger cannot throw on decode before recovery (#2033). */
5617
+ async readMemoryLifecycleLedgerRawBufferForCompaction(): Promise<Buffer> {
5618
+ return readMaybeEncryptedFileBuffer(this.memoryLifecycleLedgerPath, this._secureStoreKey, this.baseDir);
5619
+ }
5620
+ async readAllMemoryLifecycleEventsForCompaction(): Promise<MemoryLifecycleEvent[]> {
5621
+ if (!(await probeEncryptedRegularFileHeader(this.memoryLifecycleLedgerPath))) {
5622
+ return readAllLifecycleEventsFromLedger(
5623
+ this.memoryLifecycleLedgerPath,
5624
+ (p) => this.readStorageSecureFile(p),
5625
+ );
5680
5626
  }
5627
+ return readAllLifecycleEventsFromLedgerBuffer(
5628
+ this.memoryLifecycleLedgerPath,
5629
+ (p) => readMaybeEncryptedFileBuffer(p, this._secureStoreKey, this.baseDir),
5630
+ );
5681
5631
  }
5682
5632
 
5683
5633
  async readMemoryLifecycleEvents(limit: number = 200): Promise<MemoryLifecycleEvent[]> {
5684
- const cappedLimit = Math.max(0, Math.floor(limit));
5685
- if (cappedLimit === 0) return [];
5686
- const events = await this.readAllMemoryLifecycleEvents();
5687
- return events.slice(-cappedLimit);
5634
+ return readBoundedLifecycleEventsFromLedger(
5635
+ this.memoryLifecycleLedgerPath, (p) => this.readStorageSecureFile(p), limit);
5688
5636
  }
5689
5637
 
5690
5638
  async writeCompressionGuidelines(content: string): Promise<void> {
@@ -6412,6 +6360,7 @@ export class StorageManager {
6412
6360
 
6413
6361
  const memories = await this.readAllMemories();
6414
6362
  const memoryMap = new Map(memories.map((m) => [m.frontmatter.id, m]));
6363
+ const memoryPathMap = new Map(memories.map((m) => [path.resolve(m.path), m]));
6415
6364
  let updated = 0;
6416
6365
  // Capture the corpus version + warmth BEFORE writing so we can patch the hot
6417
6366
  // entries in place and then re-key them to the version this flush produces
@@ -6431,7 +6380,9 @@ export class StorageManager {
6431
6380
  const appliedPatches = new Map<string, { accessCount: number; lastAccessed: string }>();
6432
6381
 
6433
6382
  for (const entry of entries) {
6434
- const memory = memoryMap.get(entry.memoryId);
6383
+ const memory = entry.memoryPath
6384
+ ? memoryPathMap.get(path.resolve(entry.memoryPath))
6385
+ : memoryMap.get(entry.memoryId);
6435
6386
  if (!memory) continue;
6436
6387
 
6437
6388
  const newFm: MemoryFrontmatter = {
@@ -6450,7 +6401,10 @@ export class StorageManager {
6450
6401
  if (warm) {
6451
6402
  updateCacheOnWrite(this.baseDir, { ...memory, frontmatter: newFm }, keyId);
6452
6403
  }
6453
- appliedPatches.set(entry.memoryId, { accessCount: entry.newCount, lastAccessed: entry.lastAccessed });
6404
+ appliedPatches.set(path.resolve(memory.path), {
6405
+ accessCount: entry.newCount,
6406
+ lastAccessed: entry.lastAccessed,
6407
+ });
6454
6408
  updated++;
6455
6409
  } catch (err) {
6456
6410
  log.debug(`failed to update access tracking for ${entry.memoryId}: ${err}`);
@@ -6483,7 +6437,7 @@ export class StorageManager {
6483
6437
  // re-key robust to that race; the publish guard blocks any scan
6484
6438
  // finishing AFTER our bump, so this is the only window.
6485
6439
  const reapplied = cur.map((m) => {
6486
- const patch = appliedPatches.get(m.frontmatter.id);
6440
+ const patch = appliedPatches.get(path.resolve(m.path));
6487
6441
  return patch
6488
6442
  ? { ...m, frontmatter: { ...m.frontmatter, accessCount: patch.accessCount, lastAccessed: patch.lastAccessed } }
6489
6443
  : m;
@@ -6511,28 +6465,68 @@ export class StorageManager {
6511
6465
  }
6512
6466
 
6513
6467
  /**
6514
- * Check which of the given memory IDs actually exist on disk.
6468
+ * Resolve existing active memory IDs to their on-disk paths.
6515
6469
  *
6516
6470
  * Uses a lightweight directory scan (collectActiveMemoryPaths) that reads
6517
6471
  * file names without parsing frontmatter — much cheaper than readAllMemories()
6518
- * for simple existence checks like citation usage tracking.
6519
- *
6520
- * Returns the subset of `ids` that correspond to real memory files.
6472
+ * for citation usage tracking and other existence checks.
6521
6473
  */
6522
- async filterExistingMemoryIds(ids: string[]): Promise<Set<string>> {
6523
- if (ids.length === 0) return new Set();
6474
+ async findExistingMemoryPaths(
6475
+ ids: string[],
6476
+ preferredPaths: Map<string, string[]> = new Map(),
6477
+ ): Promise<Map<string, string[]>> {
6478
+ if (ids.length === 0) return new Map();
6524
6479
  const wantedIds = new Set(ids);
6525
6480
  const filePaths = await this.collectActiveMemoryPaths();
6526
- const foundIds = new Set<string>();
6481
+ const pathsById = new Map<string, string[]>();
6482
+ const filePathsById = new Map<string, string[]>();
6527
6483
  for (const filePath of filePaths) {
6528
- const basename = path.basename(filePath, ".md");
6529
- if (wantedIds.has(basename)) {
6530
- foundIds.add(basename);
6531
- // Short-circuit once all requested IDs are found.
6532
- if (foundIds.size === wantedIds.size) break;
6484
+ const memoryId = path.basename(filePath, ".md");
6485
+ if (!wantedIds.has(memoryId)) continue;
6486
+ const paths = filePathsById.get(memoryId) ?? [];
6487
+ paths.push(filePath);
6488
+ filePathsById.set(memoryId, paths);
6489
+ }
6490
+ for (const id of wantedIds) {
6491
+ const existingPaths = filePathsById.get(id) ?? [];
6492
+ const preferred = preferredPaths.get(id) ?? [];
6493
+ const preferredMatches: string[] = [];
6494
+ for (const preferredPath of preferred) {
6495
+ const directCandidates = new Set(
6496
+ qmdResultPathCandidates(this.baseDir, preferredPath),
6497
+ );
6498
+ const directMatch = existingPaths.find((filePath) =>
6499
+ directCandidates.has(path.resolve(filePath)),
6500
+ );
6501
+ if (directMatch) {
6502
+ preferredMatches.push(directMatch);
6503
+ continue;
6504
+ }
6505
+
6506
+ const candidates = new Set<string>();
6507
+ const parts = qmdCollectionPathParts(preferredPath);
6508
+ if (parts) {
6509
+ for (const candidate of qmdResultPathCandidates(this.baseDir, parts.relativePath)) {
6510
+ candidates.add(candidate);
6511
+ }
6512
+ }
6513
+ const match = existingPaths.find((filePath) => candidates.has(path.resolve(filePath)));
6514
+ if (match) preferredMatches.push(match);
6515
+ }
6516
+ if (preferredMatches.length > 0) {
6517
+ pathsById.set(id, preferredMatches);
6518
+ } else if (existingPaths.length > 0) {
6519
+ pathsById.set(id, existingPaths);
6533
6520
  }
6534
6521
  }
6535
- return foundIds;
6522
+ return pathsById;
6523
+ }
6524
+
6525
+ /**
6526
+ * Check which of the given memory IDs actually exist on disk.
6527
+ */
6528
+ async filterExistingMemoryIds(ids: string[]): Promise<Set<string>> {
6529
+ return new Set((await this.findExistingMemoryPaths(ids)).keys());
6536
6530
  }
6537
6531
 
6538
6532
  async getProjectedMemoryState(id: string): Promise<MemoryProjectionCurrentState | null> {
@@ -6591,8 +6585,8 @@ export class StorageManager {
6591
6585
  const projected = readProjectedMemoryTimeline(this.baseDir, memoryId, cappedLimit);
6592
6586
  if (projected && projected.length > 0) return projected;
6593
6587
  warnProjectionFallback(this.baseDir, "getMemoryTimeline");
6594
- const events = await this.readAllMemoryLifecycleEvents();
6595
- return events.filter((event) => event.memoryId === memoryId).slice(-cappedLimit);
6588
+ return readBoundedLifecycleEventsFromLedger(
6589
+ this.memoryLifecycleLedgerPath, (p) => this.readStorageSecureFile(p), cappedLimit, memoryId);
6596
6590
  }
6597
6591
 
6598
6592
  // ---------------------------------------------------------------------------
@@ -6783,13 +6777,11 @@ export class StorageManager {
6783
6777
  }
6784
6778
  }
6785
6779
 
6786
- // Also write a correction entry for the audit trail
6787
- await this.writeMemory("correction", `Superseded: ${oldMemory.content}\n\nReason: ${reason}`, {
6788
- confidence: 1.0,
6789
- tags: ["supersession", "auto-resolved"],
6790
- source: "contradiction-detection",
6791
- lineage: [oldMemoryId, newMemoryId],
6792
- });
6780
+ // Audit-trail correction sealed even INSIDE the engine (#2022 review).
6781
+ const auditBody = `Superseded: ${oldMemory.content}\n\nReason: ${reason}`;
6782
+ const auditInput = { content: auditBody, category: "correction" as const, confidence: 1.0, tags: ["supersession", "auto-resolved"] };
6783
+ const auditEnvelope = composeMemoryEnvelope(auditInput, { source: "contradiction-detection" });
6784
+ await this.writeSealedMemory(auditEnvelope, { lineage: [oldMemoryId, newMemoryId] });
6793
6785
 
6794
6786
  return true;
6795
6787
  } catch (err) {