@remnic/core 9.7.14 → 9.8.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 (950) hide show
  1. package/dist/access-admin-ops-surface.d.ts +13 -6
  2. package/dist/access-admin-ops-surface.js +88 -74
  3. package/dist/access-audit.js +2 -2
  4. package/dist/access-boundary.d.ts +35 -17
  5. package/dist/access-boundary.js +89 -77
  6. package/dist/access-citation.d.ts +28 -0
  7. package/dist/access-citation.js +9 -0
  8. package/dist/access-cli.js +197 -181
  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 +103 -90
  14. package/dist/access-identity-continuity-surface.d.ts +13 -8
  15. package/dist/access-identity-continuity-surface.js +88 -74
  16. package/dist/access-lcm-surface.d.ts +16 -9
  17. package/dist/access-lcm-surface.js +88 -74
  18. package/dist/access-mcp.d.ts +23 -16
  19. package/dist/access-mcp.js +99 -87
  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 +88 -74
  24. package/dist/access-operations-batch.js +95 -83
  25. package/dist/access-operations.d.ts +26 -19
  26. package/dist/access-operations.js +97 -85
  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 +88 -74
  33. package/dist/access-schema.d.ts +34 -34
  34. package/dist/access-schema.js +8 -6
  35. package/dist/{access-service-CPhSnLOH.d.ts → access-service-DEL4qCrc.d.ts} +25 -32
  36. package/dist/access-service.d.ts +13 -6
  37. package/dist/access-service.js +92 -75
  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 +37 -27
  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/{capsule-merge-Z2HUO6IK.js → capsule-merge-5KKJQBWQ.js} +4 -4
  73. package/dist/{catalog-uX9sT9ws.d.ts → catalog-BWqHb5HT.d.ts} +1 -1
  74. package/dist/causal-behavior.d.ts +3 -1
  75. package/dist/causal-behavior.js +6 -6
  76. package/dist/causal-chain.js +4 -4
  77. package/dist/causal-consolidation.d.ts +3 -1
  78. package/dist/causal-consolidation.js +46 -37
  79. package/dist/causal-consolidation.js.map +1 -1
  80. package/dist/causal-retrieval.js +6 -6
  81. package/dist/causal-trajectory-graph.d.ts +3 -1
  82. package/dist/causal-trajectory-graph.js +1 -1
  83. package/dist/causal-trajectory.js +1 -1
  84. package/dist/{chunk-A3GOMRJG.js → chunk-23L3IXFD.js} +2 -2
  85. package/dist/chunk-2CGLBUW3.js +15 -0
  86. package/dist/chunk-2CGLBUW3.js.map +1 -0
  87. package/dist/{chunk-MRHFJV44.js → chunk-2DQ2XREU.js} +2 -2
  88. package/dist/{chunk-ASKEM4AR.js → chunk-2EJOEICD.js} +1823 -1483
  89. package/dist/chunk-2EJOEICD.js.map +1 -0
  90. package/dist/{chunk-L2B3EE7B.js → chunk-2QMKMVHH.js} +2 -2
  91. package/dist/{chunk-6CALL5VN.js → chunk-2UFO7X4Q.js} +7 -7
  92. package/dist/{chunk-2MR3MFQB.js → chunk-2VEOKUPP.js} +3 -3
  93. package/dist/chunk-2Z24E5W2.js +22 -0
  94. package/dist/chunk-2Z24E5W2.js.map +1 -0
  95. package/dist/{chunk-RHOZ5LLO.js → chunk-347WD2LE.js} +2 -2
  96. package/dist/{chunk-5S7Z5SHA.js → chunk-34RC5RT7.js} +2 -2
  97. package/dist/{chunk-KGWXHWAH.js → chunk-3QAH2S33.js} +3 -3
  98. package/dist/{chunk-37CTGERM.js → chunk-3WCASNQZ.js} +12 -1
  99. package/dist/chunk-3WCASNQZ.js.map +1 -0
  100. package/dist/{chunk-ZIDSD6VC.js → chunk-3Y224NYC.js} +3 -3
  101. package/dist/{chunk-RBQLJSJ7.js → chunk-44XZ75OQ.js} +46 -10
  102. package/dist/chunk-44XZ75OQ.js.map +1 -0
  103. package/dist/{chunk-VFSP73LP.js → chunk-45GF7Y3V.js} +2 -2
  104. package/dist/{chunk-7ICYQUOL.js → chunk-4BJQODZF.js} +2514 -1526
  105. package/dist/chunk-4BJQODZF.js.map +1 -0
  106. package/dist/{chunk-JMBVCAR4.js → chunk-57OWKFUQ.js} +17 -6
  107. package/dist/chunk-57OWKFUQ.js.map +1 -0
  108. package/dist/{chunk-OEHCMALU.js → chunk-5I4M26Q7.js} +5 -5
  109. package/dist/{chunk-2E6G7FCE.js → chunk-5Q7EVV4X.js} +7 -7
  110. package/dist/{chunk-LC7OODWZ.js → chunk-5SB7SC2G.js} +80 -49
  111. package/dist/chunk-5SB7SC2G.js.map +1 -0
  112. package/dist/{chunk-VZ5OJE4X.js → chunk-5VQJBH2G.js} +2 -2
  113. package/dist/{chunk-PUBTHW5O.js → chunk-6DRPPQEJ.js} +2 -2
  114. package/dist/{chunk-NWTGD4AN.js → chunk-6KIQT7RG.js} +10 -9
  115. package/dist/chunk-6KIQT7RG.js.map +1 -0
  116. package/dist/{chunk-4LEF6XWJ.js → chunk-6O22TZSB.js} +3 -3
  117. package/dist/{chunk-ZQ7UBUZ6.js → chunk-6QYST5XN.js} +6 -6
  118. package/dist/{chunk-W4PGTAS2.js → chunk-6WAFYNVT.js} +3 -3
  119. package/dist/chunk-6ZMAMSHS.js +20 -0
  120. package/dist/chunk-6ZMAMSHS.js.map +1 -0
  121. package/dist/{chunk-R5VAVVHP.js → chunk-72A4L5JJ.js} +2 -2
  122. package/dist/{chunk-X64YLC24.js → chunk-72VAXGEI.js} +5 -13
  123. package/dist/chunk-72VAXGEI.js.map +1 -0
  124. package/dist/{chunk-IKQTFQMI.js → chunk-755Q3DGF.js} +22 -3
  125. package/dist/chunk-755Q3DGF.js.map +1 -0
  126. package/dist/{chunk-F7AU34FC.js → chunk-76GLCM57.js} +160 -127
  127. package/dist/chunk-76GLCM57.js.map +1 -0
  128. package/dist/{chunk-D7FOKIY5.js → chunk-7AMFUATK.js} +12 -9
  129. package/dist/{chunk-D7FOKIY5.js.map → chunk-7AMFUATK.js.map} +1 -1
  130. package/dist/{chunk-BFVEEB7W.js → chunk-7ON2JW73.js} +57 -22
  131. package/dist/chunk-7ON2JW73.js.map +1 -0
  132. package/dist/chunk-7RNBVZZO.js +270 -0
  133. package/dist/chunk-7RNBVZZO.js.map +1 -0
  134. package/dist/{chunk-3FAMU5TX.js → chunk-A6USBZTO.js} +2 -2
  135. package/dist/{chunk-TXVD7PLT.js → chunk-A72SWTWW.js} +7 -7
  136. package/dist/{chunk-FC77VUX5.js → chunk-A7Q43EUZ.js} +2 -2
  137. package/dist/{chunk-UJBESW6X.js → chunk-AE7WMV4V.js} +7 -7
  138. package/dist/{chunk-J2HSAU72.js → chunk-AIK5W3IW.js} +29 -1
  139. package/dist/chunk-AIK5W3IW.js.map +1 -0
  140. package/dist/{chunk-OKUEZOGW.js → chunk-AITCS2RC.js} +2 -2
  141. package/dist/{chunk-OC35KQIN.js → chunk-AJ7QCSIS.js} +4 -4
  142. package/dist/{chunk-FNMFJWBY.js → chunk-ANEUPF6O.js} +5 -5
  143. package/dist/chunk-AREGMX2J.js +50 -0
  144. package/dist/chunk-AREGMX2J.js.map +1 -0
  145. package/dist/chunk-AVPXXV6J.js +448 -0
  146. package/dist/chunk-AVPXXV6J.js.map +1 -0
  147. package/dist/{chunk-2A3OXAP2.js → chunk-B7RWBZXW.js} +2 -2
  148. package/dist/{chunk-GWTFLF2P.js → chunk-BJMBJZ2Y.js} +59 -59
  149. package/dist/chunk-BJMBJZ2Y.js.map +1 -0
  150. package/dist/{chunk-N3JDIRBW.js → chunk-BLAKS3CP.js} +4 -4
  151. package/dist/{chunk-M7VGG3ID.js → chunk-BV5WMSIJ.js} +9 -9
  152. package/dist/{chunk-2BFLDH5F.js → chunk-BXLOS5AJ.js} +2 -2
  153. package/dist/{chunk-UXKMMNJD.js → chunk-C23BYTJV.js} +6 -6
  154. package/dist/{chunk-JZHHREBL.js → chunk-C3AKM55W.js} +2 -2
  155. package/dist/{chunk-RP7YXA67.js → chunk-C5JF6EZ7.js} +2 -2
  156. package/dist/{chunk-FBNWOMCE.js → chunk-C6XBRYVD.js} +16 -15
  157. package/dist/chunk-C6XBRYVD.js.map +1 -0
  158. package/dist/{chunk-FTYDWKO6.js → chunk-CEN6DW6X.js} +12 -2
  159. package/dist/chunk-CEN6DW6X.js.map +1 -0
  160. package/dist/chunk-CGASUJVP.js +421 -0
  161. package/dist/chunk-CGASUJVP.js.map +1 -0
  162. package/dist/{chunk-DDGV7NAX.js → chunk-CQMEM5CC.js} +2 -2
  163. package/dist/{chunk-A7B4RMQG.js → chunk-CXZQOVIE.js} +12 -12
  164. package/dist/chunk-D3V3UMET.js +44 -0
  165. package/dist/chunk-D3V3UMET.js.map +1 -0
  166. package/dist/{chunk-7N2RUBAP.js → chunk-DDA47TES.js} +2 -2
  167. package/dist/{chunk-64SE4X5G.js → chunk-DEBHXHJR.js} +3 -3
  168. package/dist/{chunk-EHK6KZHK.js → chunk-DNHVPZOD.js} +2 -2
  169. package/dist/{chunk-437S3G37.js → chunk-DQEMWVMT.js} +2 -2
  170. package/dist/{chunk-LZZNTPLR.js → chunk-DX3XRGLH.js} +2 -2
  171. package/dist/chunk-EA2H5LML.js +72 -0
  172. package/dist/chunk-EA2H5LML.js.map +1 -0
  173. package/dist/{chunk-IFJ23BHU.js → chunk-EEC7UJPX.js} +112 -106
  174. package/dist/chunk-EEC7UJPX.js.map +1 -0
  175. package/dist/{chunk-KSNVDPDF.js → chunk-EKBNDKDY.js} +21 -20
  176. package/dist/chunk-EKBNDKDY.js.map +1 -0
  177. package/dist/{chunk-YF6FMLBT.js → chunk-EQWEIIKQ.js} +64 -34
  178. package/dist/chunk-EQWEIIKQ.js.map +1 -0
  179. package/dist/{chunk-7MW3CVLD.js → chunk-ES36HTSC.js} +4 -4
  180. package/dist/{chunk-TN7THW5U.js → chunk-ET356KSB.js} +2 -2
  181. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  182. package/dist/{chunk-7LFXDGLH.js → chunk-F7GAIWFA.js} +20 -20
  183. package/dist/{chunk-IKSELOIV.js → chunk-FFPJINJE.js} +7 -7
  184. package/dist/chunk-FMLKOL2Z.js +636 -0
  185. package/dist/chunk-FMLKOL2Z.js.map +1 -0
  186. package/dist/chunk-FQC3DHH2.js +20 -0
  187. package/dist/chunk-FQC3DHH2.js.map +1 -0
  188. package/dist/{chunk-V53GJUSL.js → chunk-FUTF72MJ.js} +15 -15
  189. package/dist/chunk-GCASGYIO.js +71 -0
  190. package/dist/chunk-GCASGYIO.js.map +1 -0
  191. package/dist/{chunk-5UNWK7KU.js → chunk-GFPZS3F5.js} +8 -8
  192. package/dist/{chunk-BCDKBDSY.js → chunk-GNVZQ5JN.js} +99 -41
  193. package/dist/chunk-GNVZQ5JN.js.map +1 -0
  194. package/dist/{chunk-G6NKALRN.js → chunk-H4WG3UQZ.js} +2 -2
  195. package/dist/{chunk-HLTAPHZZ.js → chunk-HNZKVLAN.js} +2 -2
  196. package/dist/{chunk-5EZXHU36.js → chunk-HYQJQGD3.js} +2 -2
  197. package/dist/{chunk-ANADNYWK.js → chunk-IDZFBSHN.js} +8 -6
  198. package/dist/{chunk-ANADNYWK.js.map → chunk-IDZFBSHN.js.map} +1 -1
  199. package/dist/{chunk-XVLEGMJI.js → chunk-IFH6LWTO.js} +50 -8
  200. package/dist/chunk-IFH6LWTO.js.map +1 -0
  201. package/dist/{chunk-RULE4VG5.js → chunk-ILGC6JH6.js} +21 -6
  202. package/dist/chunk-ILGC6JH6.js.map +1 -0
  203. package/dist/chunk-IUM3VSHI.js +113 -0
  204. package/dist/chunk-IUM3VSHI.js.map +1 -0
  205. package/dist/{chunk-ND5M64JJ.js → chunk-IYNGIMVG.js} +101 -91
  206. package/dist/chunk-IYNGIMVG.js.map +1 -0
  207. package/dist/{chunk-TRWPCR6P.js → chunk-JECBQBEX.js} +3 -3
  208. package/dist/{chunk-HG32URJJ.js → chunk-JYIFGGEM.js} +2 -2
  209. package/dist/{chunk-VA4WSW54.js → chunk-JYIJNNN3.js} +3 -3
  210. package/dist/{chunk-L5HJQC36.js → chunk-K2TDK7GG.js} +3 -3
  211. package/dist/{chunk-MKDRQAA3.js → chunk-KAVBJSBQ.js} +4 -4
  212. package/dist/{chunk-EEARRVZ2.js → chunk-KF5EUVKQ.js} +16 -6
  213. package/dist/chunk-KF5EUVKQ.js.map +1 -0
  214. package/dist/chunk-KFZY2OFH.js +42 -0
  215. package/dist/chunk-KFZY2OFH.js.map +1 -0
  216. package/dist/{chunk-4OKLXWDZ.js → chunk-KNB7IBBU.js} +2 -2
  217. package/dist/{chunk-QLULYKWZ.js → chunk-KSXJOC4F.js} +2 -2
  218. package/dist/{chunk-OBMR4VIX.js → chunk-L2DPF4DF.js} +186 -15
  219. package/dist/chunk-L2DPF4DF.js.map +1 -0
  220. package/dist/{chunk-JI5AVXYO.js → chunk-LEPOP46W.js} +6 -6
  221. package/dist/{chunk-O7BBFJLP.js → chunk-LTAVSOZW.js} +2 -2
  222. package/dist/{chunk-E3VODCC3.js → chunk-LUDXM7MD.js} +14 -2
  223. package/dist/{chunk-E3VODCC3.js.map → chunk-LUDXM7MD.js.map} +1 -1
  224. package/dist/{chunk-2IZAGAJR.js → chunk-LULR4IUW.js} +3 -3
  225. package/dist/{chunk-NUWYOKS6.js → chunk-MC5Q3J7S.js} +398 -7
  226. package/dist/chunk-MC5Q3J7S.js.map +1 -0
  227. package/dist/{chunk-WMWVO45V.js → chunk-MC6XO7T4.js} +2 -2
  228. package/dist/chunk-MKQBXQKU.js +107 -0
  229. package/dist/chunk-MKQBXQKU.js.map +1 -0
  230. package/dist/{chunk-7PND4PUC.js → chunk-MMGGVZ4Q.js} +29 -9
  231. package/dist/{chunk-7PND4PUC.js.map → chunk-MMGGVZ4Q.js.map} +1 -1
  232. package/dist/{chunk-D6RIFG4O.js → chunk-MNB5RMAY.js} +2 -2
  233. package/dist/{chunk-67ZCJLJY.js → chunk-MYFQ3ED4.js} +8 -8
  234. package/dist/{chunk-V3UE5I5F.js → chunk-MYVPLJOO.js} +2 -2
  235. package/dist/chunk-NINSR6LX.js +83 -0
  236. package/dist/chunk-NINSR6LX.js.map +1 -0
  237. package/dist/{chunk-MTWKDOVO.js → chunk-NLE6TSLG.js} +2 -2
  238. package/dist/{chunk-DBRYEIGY.js → chunk-OJP3N7UK.js} +2 -2
  239. package/dist/{chunk-AD6AMF2N.js → chunk-OQJIB32H.js} +28 -23
  240. package/dist/chunk-OQJIB32H.js.map +1 -0
  241. package/dist/{chunk-SMX5267P.js → chunk-OZKOPQCM.js} +2 -2
  242. package/dist/{chunk-JCK7V6SN.js → chunk-P65YJEEL.js} +2 -2
  243. package/dist/{chunk-MTR7BZQN.js → chunk-PBGWYM3H.js} +3 -3
  244. package/dist/chunk-PIG4WJFQ.js +87 -0
  245. package/dist/chunk-PIG4WJFQ.js.map +1 -0
  246. package/dist/chunk-PQVCH2AE.js +76 -0
  247. package/dist/chunk-PQVCH2AE.js.map +1 -0
  248. package/dist/{chunk-AXTXN4T6.js → chunk-PWFCHOB3.js} +2 -2
  249. package/dist/{chunk-FDPZVY6D.js → chunk-QFN4U7AG.js} +2 -2
  250. package/dist/{chunk-K2ZLHESQ.js → chunk-QIEUU4UK.js} +2 -2
  251. package/dist/{chunk-AHLPZ2SF.js → chunk-QJAQNXWI.js} +44 -6
  252. package/dist/chunk-QJAQNXWI.js.map +1 -0
  253. package/dist/{chunk-3D6AFGQR.js → chunk-QVT3E4CE.js} +7 -7
  254. package/dist/{chunk-VYOFVVNK.js → chunk-R26ZMXR2.js} +24 -24
  255. package/dist/{chunk-VYOFVVNK.js.map → chunk-R26ZMXR2.js.map} +1 -1
  256. package/dist/chunk-R3DJVOSM.js +34 -0
  257. package/dist/chunk-R3DJVOSM.js.map +1 -0
  258. package/dist/{chunk-SFQ6QNL7.js → chunk-R6YJI3HE.js} +2 -1
  259. package/dist/{chunk-5CJVLG3H.js → chunk-RFDD3B7W.js} +5 -5
  260. package/dist/{chunk-JBPKEARU.js → chunk-RKNJBZ55.js} +4 -4
  261. package/dist/chunk-ROI7BK7H.js +88 -0
  262. package/dist/chunk-ROI7BK7H.js.map +1 -0
  263. package/dist/{chunk-UTRJFDP2.js → chunk-S2BBISHU.js} +4 -4
  264. package/dist/{chunk-IALLX7EE.js → chunk-S2FSBU4H.js} +3 -3
  265. package/dist/{chunk-BFPETA3S.js → chunk-SCCO46KZ.js} +3 -2
  266. package/dist/chunk-SCCO46KZ.js.map +1 -0
  267. package/dist/chunk-SICXL4MK.js +165 -0
  268. package/dist/chunk-SICXL4MK.js.map +1 -0
  269. package/dist/chunk-SK7AMF2J.js +168 -0
  270. package/dist/chunk-SK7AMF2J.js.map +1 -0
  271. package/dist/{chunk-GMSDQUCR.js → chunk-TDTXKOAJ.js} +6475 -5748
  272. package/dist/chunk-TDTXKOAJ.js.map +1 -0
  273. package/dist/{chunk-KSBEEMQF.js → chunk-TG6IHVE2.js} +151 -6
  274. package/dist/chunk-TG6IHVE2.js.map +1 -0
  275. package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
  276. package/dist/{chunk-PK5XBDA6.js → chunk-U3ZAVRP3.js} +2 -2
  277. package/dist/{chunk-ODPLEWB6.js → chunk-UFGFMWAD.js} +47 -17
  278. package/dist/chunk-UFGFMWAD.js.map +1 -0
  279. package/dist/{chunk-4KJLYTLS.js → chunk-V57NLO5P.js} +2 -2
  280. package/dist/{chunk-FLVL4L3L.js → chunk-VDX2J7OX.js} +2 -2
  281. package/dist/{chunk-7XGSIUP5.js → chunk-W4FBGYGK.js} +1 -1
  282. package/dist/{chunk-TXLGHDHS.js → chunk-W4XJFLAL.js} +53 -32
  283. package/dist/chunk-W4XJFLAL.js.map +1 -0
  284. package/dist/{chunk-6SXVCD7W.js → chunk-W7IGLX4G.js} +2 -2
  285. package/dist/{chunk-KQDNAXOM.js → chunk-WG5HSQ4P.js} +19 -6
  286. package/dist/chunk-WG5HSQ4P.js.map +1 -0
  287. package/dist/{chunk-LTJAMRGI.js → chunk-WL5EBRDH.js} +17 -3
  288. package/dist/chunk-WL5EBRDH.js.map +1 -0
  289. package/dist/{chunk-KAWTOYAW.js → chunk-WLZYGLJ4.js} +2 -2
  290. package/dist/{chunk-W5NKDU24.js → chunk-WON4APQY.js} +4 -4
  291. package/dist/{chunk-NKCPUHCI.js → chunk-WZFAPHIK.js} +6 -6
  292. package/dist/{chunk-KQPJLK3X.js → chunk-XL24QQ63.js} +2 -2
  293. package/dist/{chunk-X47LWMKA.js → chunk-XMMESCPD.js} +26 -1
  294. package/dist/chunk-XMMESCPD.js.map +1 -0
  295. package/dist/{chunk-4B4MUS5D.js → chunk-YROFNEG5.js} +7 -7
  296. package/dist/{chunk-WU46YPY6.js → chunk-YV32N67O.js} +1 -1
  297. package/dist/chunk-YV32N67O.js.map +1 -0
  298. package/dist/{chunk-B3F4CWQR.js → chunk-YVKF3HEW.js} +1 -1
  299. package/dist/chunk-YVKF3HEW.js.map +1 -0
  300. package/dist/{chunk-VRAD2FGO.js → chunk-Z54EM5YZ.js} +12 -12
  301. package/dist/{chunk-2VN4JAX7.js → chunk-ZOKZIR5K.js} +95 -33
  302. package/dist/chunk-ZOKZIR5K.js.map +1 -0
  303. package/dist/{cli-_Y2Bmt6A.d.ts → cli-C_pqqTCw.d.ts} +28 -12
  304. package/dist/cli.d.ts +16 -9
  305. package/dist/cli.js +145 -130
  306. package/dist/compounding/engine.d.ts +5 -2
  307. package/dist/compounding/engine.js +39 -29
  308. package/dist/compounding/preference-consolidator.d.ts +3 -1
  309. package/dist/compounding/preference-consolidator.js +1 -1
  310. package/dist/compression-optimizer.d.ts +3 -1
  311. package/dist/config-qmd.d.ts +8 -0
  312. package/dist/config-qmd.js +20 -0
  313. package/dist/config-qmd.js.map +1 -0
  314. package/dist/config.d.ts +3 -1
  315. package/dist/config.js +15 -10
  316. package/dist/connectors/codex-materialize-runner.d.ts +3 -1
  317. package/dist/connectors/codex-materialize-runner.js +39 -29
  318. package/dist/connectors/codex-materialize.d.ts +3 -1
  319. package/dist/connectors/codex-materialize.js +2 -2
  320. package/dist/connectors/index.d.ts +3 -1
  321. package/dist/connectors/index.js +44 -35
  322. package/dist/consolidation-provenance-check.d.ts +5 -2
  323. package/dist/consolidation-provenance-check.js +4 -4
  324. package/dist/consolidation-undo.d.ts +5 -2
  325. package/dist/consolidation-undo.js +3 -3
  326. package/dist/contradiction/index.d.ts +5 -2
  327. package/dist/contradiction/index.js +10 -4
  328. package/dist/{contradiction-scan-J57T5QV7.js → contradiction-scan-RJBABNRH.js} +4 -4
  329. package/dist/contradiction-scan-RJBABNRH.js.map +1 -0
  330. package/dist/conversation-index/backend.d.ts +3 -1
  331. package/dist/conversation-index/backend.js +5 -5
  332. package/dist/conversation-index/chunker.d.ts +3 -1
  333. package/dist/conversation-index/cleanup.js +2 -2
  334. package/dist/conversation-index/faiss-adapter.d.ts +3 -1
  335. package/dist/conversation-index/faiss-adapter.js +2 -2
  336. package/dist/conversation-index/indexer.d.ts +3 -1
  337. package/dist/conversation-index/indexer.js +2 -2
  338. package/dist/conversation-index/search.d.ts +3 -1
  339. package/dist/conversation-index/search.js +2 -2
  340. package/dist/cross-namespace-budget.d.ts +45 -1
  341. package/dist/cross-namespace-budget.js +5 -3
  342. package/dist/dashboard-runtime.js +2 -2
  343. package/dist/day-summary.d.ts +3 -1
  344. package/dist/day-summary.js +3 -3
  345. package/dist/delinearize.d.ts +3 -1
  346. package/dist/direct-answer-wiring.d.ts +3 -1
  347. package/dist/direct-answer.d.ts +3 -1
  348. package/dist/{dreams-ledger-6UODQOX6.js → dreams-ledger-567VKVOU.js} +4 -2
  349. package/dist/dreams-ledger-567VKVOU.js.map +1 -0
  350. package/dist/embedding-fallback.d.ts +3 -1
  351. package/dist/embedding-fallback.js +3 -3
  352. package/dist/enrichment/index.d.ts +3 -1
  353. package/dist/entity-retrieval.d.ts +5 -2
  354. package/dist/entity-retrieval.js +38 -29
  355. package/dist/entity-schema.d.ts +3 -1
  356. package/dist/explicit-capture.d.ts +13 -8
  357. package/dist/explicit-capture.js +7 -3
  358. package/dist/extraction-faithfulness.d.ts +3 -1
  359. package/dist/extraction-faithfulness.js +5 -5
  360. package/dist/extraction-judge-telemetry.d.ts +3 -1
  361. package/dist/extraction-judge-telemetry.js +2 -2
  362. package/dist/extraction-judge-training.d.ts +3 -1
  363. package/dist/extraction-judge-training.js +2 -2
  364. package/dist/extraction-judge.d.ts +3 -1
  365. package/dist/extraction-judge.js +5 -5
  366. package/dist/extraction.d.ts +3 -1
  367. package/dist/extraction.js +13 -12
  368. package/dist/faithfulness-config.js +3 -2
  369. package/dist/fallback-llm.d.ts +3 -1
  370. package/dist/fallback-llm.js +4 -4
  371. package/dist/{first-start-migration-TI67YMLG.js → first-start-migration-CSRHCJIQ.js} +2 -2
  372. package/dist/{forget-CQCLB5LB.js → forget-HRPLVB3W.js} +5 -5
  373. package/dist/graph-dashboard-diff.d.ts +3 -1
  374. package/dist/graph-dashboard-key.d.ts +3 -1
  375. package/dist/graph-dashboard-parser.d.ts +3 -1
  376. package/dist/{graph-edge-decay-FO7AM4UR.js → graph-edge-decay-Z6ENALU2.js} +3 -3
  377. package/dist/graph-edge-decay-Z6ENALU2.js.map +1 -0
  378. package/dist/graph-edge-reinforcement.d.ts +3 -1
  379. package/dist/graph-snapshot.d.ts +3 -1
  380. package/dist/graph-snapshot.js +2 -2
  381. package/dist/graph.d.ts +57 -10
  382. package/dist/graph.js +1 -1
  383. package/dist/identity-continuity.d.ts +1 -2
  384. package/dist/importance.d.ts +3 -1
  385. package/dist/importers/index.d.ts +3 -1
  386. package/dist/in-flight-reads.d.ts +3 -1
  387. package/dist/index.d.ts +101 -92
  388. package/dist/index.js +290 -242
  389. package/dist/intent.d.ts +3 -1
  390. package/dist/lcm/archive.js +2 -2
  391. package/dist/lcm/engine.d.ts +3 -1
  392. package/dist/lcm/engine.js +6 -6
  393. package/dist/lcm/index.d.ts +3 -1
  394. package/dist/lcm/index.js +13 -13
  395. package/dist/lcm/schema.js +2 -2
  396. package/dist/lcm/summarizer.js +3 -3
  397. package/dist/lcm/tools.d.ts +3 -1
  398. package/dist/lifecycle.d.ts +3 -1
  399. package/dist/live-connectors-runner.d.ts +3 -1
  400. package/dist/local-llm.d.ts +5 -2
  401. package/dist/local-llm.js +3 -3
  402. package/dist/local-model-endpoint.d.ts +3 -1
  403. package/dist/logger.d.ts +3 -1
  404. package/dist/logger.js +7 -3
  405. package/dist/maintenance/archive-observations.js +2 -2
  406. package/dist/maintenance/memory-governance.d.ts +3 -1
  407. package/dist/maintenance/memory-governance.js +38 -28
  408. package/dist/maintenance/migrate-observations.js +3 -3
  409. package/dist/maintenance/observation-ledger-utils.js +2 -2
  410. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +102 -1
  411. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +40 -28
  412. package/dist/maintenance/rebuild-memory-projection.js +41 -31
  413. package/dist/maintenance/rebuild-observations.js +3 -3
  414. package/dist/mcp-memory-inspector-app.d.ts +22 -15
  415. package/dist/memory-action-policy.d.ts +3 -1
  416. package/dist/memory-cache.d.ts +46 -2
  417. package/dist/memory-cache.js +7 -1
  418. package/dist/memory-corpus-version.js +2 -2
  419. package/dist/memory-lifecycle-ledger-utils.d.ts +25 -2
  420. package/dist/memory-lifecycle-ledger-utils.js +11 -1
  421. package/dist/memory-projection-store.d.ts +3 -1
  422. package/dist/memory-projection-store.js +2 -2
  423. package/dist/memory-provenance.d.ts +3 -1
  424. package/dist/memory-worth-outcomes.d.ts +5 -2
  425. package/dist/model-registry.js +2 -2
  426. package/dist/models-json.d.ts +3 -1
  427. package/dist/models-json.js +2 -2
  428. package/dist/namespaces/migrate.d.ts +6 -3
  429. package/dist/namespaces/migrate.js +54 -45
  430. package/dist/namespaces/principal.d.ts +27 -2
  431. package/dist/namespaces/principal.js +6 -2
  432. package/dist/namespaces/search.d.ts +16 -2
  433. package/dist/namespaces/search.js +25 -21
  434. package/dist/namespaces/storage.d.ts +24 -3
  435. package/dist/namespaces/storage.js +38 -30
  436. package/dist/native-knowledge.d.ts +3 -1
  437. package/dist/native-knowledge.js +2 -2
  438. package/dist/negative.js +2 -2
  439. package/dist/offline-sync-exclude-globs.d.ts +13 -0
  440. package/dist/offline-sync-exclude-globs.js +10 -0
  441. package/dist/offline-sync-exclude-globs.js.map +1 -0
  442. package/dist/offline-sync-impression-drain.d.ts +78 -0
  443. package/dist/offline-sync-impression-drain.js +37 -0
  444. package/dist/offline-sync-impression-drain.js.map +1 -0
  445. package/dist/offline-sync.js +4 -2
  446. package/dist/operator-toolkit.d.ts +5 -2
  447. package/dist/operator-toolkit.js +73 -58
  448. package/dist/orchestration/compression-guideline-coordinator.d.ts +5 -2
  449. package/dist/orchestration/compression-guideline-coordinator.js +3 -3
  450. package/dist/orchestration/maintenance.d.ts +121 -2
  451. package/dist/orchestration/maintenance.js +47 -34
  452. package/dist/{orchestrator-BbZ_dQSU.d.ts → orchestrator-CQJ8U7En.d.ts} +85 -79
  453. package/dist/orchestrator.d.ts +13 -8
  454. package/dist/orchestrator.js +197 -180
  455. package/dist/page-versioning.js +3 -3
  456. package/dist/patterns-cli.d.ts +3 -1
  457. package/dist/{pipeline-FRMDb1fn.d.ts → pipeline-Cn-kvYeV.d.ts} +1 -1
  458. package/dist/policy-runtime.d.ts +3 -1
  459. package/dist/prioritized-embed.d.ts +19 -0
  460. package/dist/prioritized-embed.js +8 -0
  461. package/dist/prioritized-embed.js.map +1 -0
  462. package/dist/proactive-contention.d.ts +28 -0
  463. package/dist/proactive-contention.js +8 -0
  464. package/dist/proactive-contention.js.map +1 -0
  465. package/dist/profiling.js +2 -2
  466. package/dist/provenance.d.ts +3 -1
  467. package/dist/provenance.js +3 -2
  468. package/dist/{purge-NDWV6WYD.js → purge-IRF47ZTX.js} +3 -3
  469. package/dist/{qmd-xJRrHFbA.d.ts → qmd-oh9VXz1d.d.ts} +11 -1
  470. package/dist/qmd-preflight.d.ts +5 -2
  471. package/dist/qmd-recall-cache.d.ts +3 -1
  472. package/dist/qmd-status.d.ts +57 -0
  473. package/dist/qmd-status.js +18 -0
  474. package/dist/qmd-status.js.map +1 -0
  475. package/dist/qmd.d.ts +5 -2
  476. package/dist/qmd.js +8 -4
  477. package/dist/recall-concurrency-config.d.ts +16 -0
  478. package/dist/recall-concurrency-config.js +10 -0
  479. package/dist/recall-concurrency-config.js.map +1 -0
  480. package/dist/recall-disclosure-escalation.d.ts +3 -1
  481. package/dist/recall-explain-renderer.d.ts +4 -1
  482. package/dist/recall-explain-renderer.js +3 -3
  483. package/dist/recall-memory-map.d.ts +26 -0
  484. package/dist/recall-memory-map.js +18 -0
  485. package/dist/recall-memory-map.js.map +1 -0
  486. package/dist/recall-planner-llm.d.ts +3 -1
  487. package/dist/recall-planner-llm.js +4 -4
  488. package/dist/recall-qos.js +2 -2
  489. package/dist/recall-state.d.ts +163 -2
  490. package/dist/recall-state.js +6 -3
  491. package/dist/recall-tag-filter.d.ts +3 -1
  492. package/dist/recall-timings.d.ts +11 -2
  493. package/dist/recall-timings.js +3 -1
  494. package/dist/recall-xray-cli.d.ts +3 -1
  495. package/dist/recall-xray-cli.js +4 -4
  496. package/dist/recall-xray-renderer.d.ts +3 -1
  497. package/dist/recall-xray-renderer.js +3 -3
  498. package/dist/recall-xray.d.ts +3 -1
  499. package/dist/recall-xray.js +2 -2
  500. package/dist/relevance.js +3 -3
  501. package/dist/replay/normalizers/chatgpt.d.ts +3 -1
  502. package/dist/replay/normalizers/claude.d.ts +3 -1
  503. package/dist/replay/normalizers/openclaw.d.ts +3 -1
  504. package/dist/replay/normalizers/shared.d.ts +3 -1
  505. package/dist/replay/runner.d.ts +3 -1
  506. package/dist/replay/types.d.ts +3 -1
  507. package/dist/rerank.d.ts +8 -1
  508. package/dist/rerank.js +3 -1
  509. package/dist/resolution-3K27T4VO.js +18 -0
  510. package/dist/resolution-3K27T4VO.js.map +1 -0
  511. package/dist/resolve-auth-token.d.ts +3 -1
  512. package/dist/resolve-provider-secret.js +2 -2
  513. package/dist/resume-bundles.js +19 -14
  514. package/dist/retrieval-agents.d.ts +19 -16
  515. package/dist/retrieval-agents.js +3 -3
  516. package/dist/retrieval-tiers.d.ts +3 -1
  517. package/dist/routing/engine.d.ts +3 -1
  518. package/dist/routing/store.d.ts +3 -1
  519. package/dist/routing/store.js +2 -2
  520. package/dist/salvage-envelope.d.ts +18 -0
  521. package/dist/salvage-envelope.js +15 -0
  522. package/dist/salvage-envelope.js.map +1 -0
  523. package/dist/schemas.d.ts +24 -24
  524. package/dist/scope-profiles-EpJG7yKP.d.ts +30 -0
  525. package/dist/search/document-scanner.js +3 -2
  526. package/dist/search/embed-helper.d.ts +3 -1
  527. package/dist/search/embed-helper.js +3 -3
  528. package/dist/search/factory.d.ts +3 -1
  529. package/dist/search/factory.js +19 -17
  530. package/dist/search/index.d.ts +3 -1
  531. package/dist/search/index.js +23 -21
  532. package/dist/search/lancedb-backend.d.ts +3 -1
  533. package/dist/search/lancedb-backend.js +6 -5
  534. package/dist/search/meilisearch-backend.d.ts +3 -1
  535. package/dist/search/meilisearch-backend.js +6 -5
  536. package/dist/search/noop-backend.d.ts +3 -1
  537. package/dist/search/orama-backend.d.ts +3 -1
  538. package/dist/search/orama-backend.js +6 -5
  539. package/dist/search/port.d.ts +27 -2
  540. package/dist/search/port.js +1 -1
  541. package/dist/search/remote-backend.d.ts +3 -1
  542. package/dist/search/remote-backend.js +3 -3
  543. package/dist/secure-store/index.d.ts +12 -1
  544. package/dist/secure-store/index.js +6 -3
  545. package/dist/{semantic-consolidation-fvdZRa5Z.d.ts → semantic-consolidation-DILZEQq3.d.ts} +1 -1
  546. package/dist/semantic-consolidation.d.ts +4 -2
  547. package/dist/semantic-consolidation.js +41 -32
  548. package/dist/semantic-rule-promotion.js +37 -27
  549. package/dist/semantic-rule-verifier.d.ts +3 -1
  550. package/dist/semantic-rule-verifier.js +38 -28
  551. package/dist/session-observer-bands.d.ts +3 -1
  552. package/dist/session-observer-state.d.ts +3 -1
  553. package/dist/session-observer-state.js +2 -2
  554. package/dist/session-transcript-migration.js +2 -2
  555. package/dist/shared-context/manager.d.ts +3 -1
  556. package/dist/shared-context/manager.js +3 -3
  557. package/dist/signal.d.ts +3 -1
  558. package/dist/{storage-CrvzaM9C.d.ts → storage-NIyKYD8g.d.ts} +505 -110
  559. package/dist/storage-guards.js +2 -2
  560. package/dist/storage.d.ts +6 -3
  561. package/dist/storage.js +41 -29
  562. package/dist/structured-attributes.d.ts +23 -1
  563. package/dist/structured-attributes.js +7 -1
  564. package/dist/summarizer.d.ts +3 -1
  565. package/dist/summarizer.js +11 -11
  566. package/dist/summary-snapshot.d.ts +3 -1
  567. package/dist/summary-snapshot.js +2 -2
  568. package/dist/temporal-index-batch.d.ts +1 -0
  569. package/dist/temporal-index-batch.js +10 -0
  570. package/dist/temporal-index-batch.js.map +1 -0
  571. package/dist/temporal-index-compat.d.ts +279 -1
  572. package/dist/temporal-index-compat.js +1 -1
  573. package/dist/temporal-index.d.ts +1 -225
  574. package/dist/temporal-index.js +35 -3
  575. package/dist/temporal-supersession.d.ts +5 -2
  576. package/dist/temporal-supersession.js +2 -2
  577. package/dist/temporal-timeline-recall.d.ts +3 -1
  578. package/dist/temporal-validity.d.ts +3 -1
  579. package/dist/threading.d.ts +3 -1
  580. package/dist/threading.js +2 -2
  581. package/dist/tier-migration.d.ts +5 -2
  582. package/dist/tier-routing.d.ts +3 -1
  583. package/dist/{tier-stats-KV7IWCJR.js → tier-stats-QEJSPFV2.js} +2 -2
  584. package/dist/tokens.js +3 -3
  585. package/dist/topics.d.ts +3 -1
  586. package/dist/transcript.d.ts +3 -1
  587. package/dist/transcript.js +2 -2
  588. package/dist/transfer/backup.js +3 -3
  589. package/dist/transfer/capsule-export.js +3 -3
  590. package/dist/transfer/capsule-import.js +6 -6
  591. package/dist/transfer/types.d.ts +24 -24
  592. package/dist/trust-score-stage.d.ts +75 -38
  593. package/dist/trust-score-stage.js +11 -3
  594. package/dist/trust-score.d.ts +3 -1
  595. package/dist/{types-Bl4BWDM8.d.ts → types-D_fG2ix8.d.ts} +49 -63
  596. package/dist/types-continuity.d.ts +52 -0
  597. package/dist/types-continuity.js +1 -0
  598. package/dist/types-continuity.js.map +1 -0
  599. package/dist/types.d.ts +3 -1
  600. package/dist/types.js +1 -1
  601. package/dist/utility-runtime.d.ts +3 -1
  602. package/dist/utils/serialize-mutations.d.ts +31 -2
  603. package/dist/utils/serialize-mutations.js +1 -1
  604. package/dist/verified-recall.js +39 -29
  605. package/dist/write-envelope.d.ts +282 -0
  606. package/dist/write-envelope.js +35 -0
  607. package/dist/write-envelope.js.map +1 -0
  608. package/dist/write-quarantine.d.ts +79 -0
  609. package/dist/write-quarantine.js +11 -0
  610. package/dist/write-quarantine.js.map +1 -0
  611. package/dist/write-rate-limiter.d.ts +45 -0
  612. package/dist/write-rate-limiter.js +9 -0
  613. package/dist/write-rate-limiter.js.map +1 -0
  614. package/package.json +37 -2
  615. package/src/access-admin-ops-surface.ts +17 -7
  616. package/src/access-boundary.test.ts +28 -0
  617. package/src/access-boundary.ts +31 -2
  618. package/src/access-citation.test.ts +72 -0
  619. package/src/access-citation.ts +139 -0
  620. package/src/access-errors.ts +21 -0
  621. package/src/access-fingerprint-parity.test.ts +211 -0
  622. package/src/access-http-cancellation.test.ts +59 -21
  623. package/src/access-http.test.ts +90 -0
  624. package/src/access-http.ts +79 -79
  625. package/src/access-mcp-cancellation.test.ts +16 -7
  626. package/src/access-memory-search-fanout.test.ts +220 -0
  627. package/src/access-memory-search-fanout.ts +168 -0
  628. package/src/access-observe-quarantine.test.ts +120 -0
  629. package/src/access-observe-write-surface.ts +130 -11
  630. package/src/access-operations-batch.ts +2 -0
  631. package/src/access-recall-concurrency.ts +773 -0
  632. package/src/access-recall-response.ts +307 -0
  633. package/src/access-recall-surface.ts +100 -252
  634. package/src/access-service-coding-write.test.ts +22 -4
  635. package/src/access-service-health.test.ts +49 -0
  636. package/src/access-service-namespace.test.ts +486 -8
  637. package/src/access-service-observe-scope.test.ts +4 -0
  638. package/src/access-service-recall-concurrency.test.ts +1990 -0
  639. package/src/access-service.ts +182 -175
  640. package/src/access-surface-catalog.ts +1 -0
  641. package/src/access-token-capabilities.ts +16 -9
  642. package/src/active-recall.test.ts +35 -0
  643. package/src/active-recall.ts +7 -0
  644. package/src/bounded-jsonl-state.ts +93 -0
  645. package/src/buffer-session.test.ts +422 -5
  646. package/src/buffer-surprise-trigger.test.ts +41 -0
  647. package/src/buffer.ts +218 -3
  648. package/src/capabilities-cluster-b.test.ts +1 -0
  649. package/src/capabilities.ts +5 -6
  650. package/src/cli.ts +21 -22
  651. package/src/coding/architecture-surfaces.test.ts +6 -3
  652. package/src/coding/architecture-surfaces.ts +36 -25
  653. package/src/coding/decision-surfaces.ts +42 -34
  654. package/src/compounding/engine.test.ts +5 -1
  655. package/src/compounding/engine.ts +7 -7
  656. package/src/config-qmd.ts +89 -0
  657. package/src/config.test.ts +136 -0
  658. package/src/config.ts +122 -118
  659. package/src/connector-provenance-lifecycle.test.ts +4 -4
  660. package/src/connectors/coerce.test.ts +131 -0
  661. package/src/connectors/coerce.ts +80 -7
  662. package/src/contradiction/contradiction.test.ts +10 -6
  663. package/src/contradiction/resolution.ts +17 -4
  664. package/src/correction/correction-access-wiring.ts +62 -18
  665. package/src/cross-namespace-budget.test.ts +35 -0
  666. package/src/cross-namespace-budget.ts +86 -19
  667. package/src/explicit-capture.ts +54 -17
  668. package/src/extraction.ts +2 -3
  669. package/src/faithfulness-graph-guard.test.ts +16 -69
  670. package/src/graph-recall.test.ts +31 -0
  671. package/src/graph.test.ts +137 -1
  672. package/src/graph.ts +274 -17
  673. package/src/index.ts +22 -2
  674. package/src/local-llm.ts +4 -6
  675. package/src/logger.ts +16 -0
  676. package/src/maintenance/dreams-ledger.ts +100 -49
  677. package/src/maintenance/rebuild-memory-lifecycle-ledger-cli.ts +77 -0
  678. package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +373 -9
  679. package/src/memory-cache.ts +70 -0
  680. package/src/memory-lifecycle-ledger-utils.ts +51 -5
  681. package/src/namespaces/principal.test.ts +37 -1
  682. package/src/namespaces/principal.ts +48 -0
  683. package/src/namespaces/search.test.ts +79 -5
  684. package/src/namespaces/search.ts +59 -6
  685. package/src/namespaces/storage.ts +45 -1
  686. package/src/offline-sync-exclude-globs.ts +58 -0
  687. package/src/offline-sync-impression-drain.test.ts +316 -0
  688. package/src/offline-sync-impression-drain.ts +260 -0
  689. package/src/offline-sync.test.ts +32 -5
  690. package/src/offline-sync.ts +1 -14
  691. package/src/operator-toolkit.ts +1 -1
  692. package/src/orchestration/batch-deindex-flush.test.ts +146 -0
  693. package/src/orchestration/consolidation-run.ts +96 -85
  694. package/src/orchestration/extraction-envelope.ts +94 -0
  695. package/src/orchestration/extraction-persist-promotion.ts +134 -0
  696. package/src/orchestration/extraction-persist.ts +325 -260
  697. package/src/orchestration/extraction-run.test.ts +1 -1
  698. package/src/orchestration/extraction-run.ts +2 -2
  699. package/src/orchestration/graph-recall-coordinator.ts +18 -7
  700. package/src/orchestration/lifecycle-policy-coordinator.ts +63 -48
  701. package/src/orchestration/maintenance.test.ts +2103 -3
  702. package/src/orchestration/maintenance.ts +581 -2
  703. package/src/orchestration/namespace-read-fanout.ts +14 -8
  704. package/src/orchestration/orchestrator-helpers.ts +8 -1
  705. package/src/orchestration/orchestrator-init.ts +22 -6
  706. package/src/orchestration/orchestrator-namespace-scope.ts +71 -0
  707. package/src/orchestration/persistence-index.ts +64 -9
  708. package/src/orchestration/qmd-result-resolver.ts +27 -0
  709. package/src/orchestration/recall-entry.ts +34 -19
  710. package/src/orchestration/recall-internal.ts +207 -79
  711. package/src/orchestration/recall-rerank-coordinator.ts +229 -82
  712. package/src/orchestration/recall-result-formatter.ts +76 -20
  713. package/src/orchestration/recall-search-pipeline.test.ts +70 -0
  714. package/src/orchestration/recall-search-pipeline.ts +102 -129
  715. package/src/orchestration/recall-section-coordinator.ts +315 -85
  716. package/src/orchestration/semantic-consolidation-coordinator.ts +69 -37
  717. package/src/orchestration/session-context.ts +21 -0
  718. package/src/orchestration/workspace-ops.test.ts +348 -0
  719. package/src/orchestration/workspace-ops.ts +172 -34
  720. package/src/orchestrator-chunked-hash-durability.test.ts +126 -0
  721. package/src/orchestrator-connector-dedup.test.ts +1061 -40
  722. package/src/orchestrator-flush.test.ts +155 -39
  723. package/src/orchestrator-nonchunked-hash-durability.test.ts +121 -0
  724. package/src/orchestrator-source-attribution.test.ts +9 -9
  725. package/src/orchestrator.ts +166 -150
  726. package/src/prioritized-embed.ts +123 -0
  727. package/src/proactive-contention.test.ts +80 -0
  728. package/src/proactive-contention.ts +39 -0
  729. package/src/procedural/procedure-miner.test.ts +19 -14
  730. package/src/procedural/procedure-miner.ts +44 -13
  731. package/src/qmd-client.test.ts +111 -1
  732. package/src/qmd-status.ts +186 -0
  733. package/src/qmd.test.ts +22 -0
  734. package/src/qmd.ts +60 -48
  735. package/src/recall-budget-config.test.ts +30 -0
  736. package/src/recall-cold-deadline.test.ts +149 -0
  737. package/src/recall-concurrency-config.ts +28 -0
  738. package/src/recall-memory-map.ts +74 -0
  739. package/src/recall-rerank-coordinator.test.ts +508 -0
  740. package/src/recall-state.test.ts +825 -1
  741. package/src/recall-state.ts +406 -7
  742. package/src/recall-timings-access.test.ts +41 -0
  743. package/src/recall-timings.ts +16 -0
  744. package/src/relay/mission-access.test.ts +5 -5
  745. package/src/rerank.ts +22 -0
  746. package/src/retrieval-agents.test.ts +79 -0
  747. package/src/retrieval-agents.ts +71 -19
  748. package/src/salvage-envelope.ts +28 -0
  749. package/src/scopes/scope-plan.test.ts +42 -0
  750. package/src/scopes/scope-plan.ts +45 -0
  751. package/src/search/port.ts +24 -0
  752. package/src/secure-store/index.ts +1 -0
  753. package/src/secure-store/secure-fs.ts +48 -0
  754. package/src/secure-store/secure-store.test.ts +40 -0
  755. package/src/semantic-rule-promotion.ts +13 -4
  756. package/src/storage/content-hash-index.ts +655 -0
  757. package/src/storage/entity-timeline.ts +473 -0
  758. package/src/storage/memory-lifecycle-ledger-access.ts +603 -0
  759. package/src/storage/secure-line-reader.test.ts +197 -0
  760. package/src/storage/secure-line-reader.ts +181 -3
  761. package/src/storage-append-header-sniff.test.ts +195 -0
  762. package/src/storage-behavior-signals-cache.test.ts +237 -0
  763. package/src/storage-contract/cache-coherence.test.ts +96 -1
  764. package/src/storage-contract/content-hash.test.ts +47 -2
  765. package/src/storage-fact-hash-index-defer.test.ts +1200 -0
  766. package/src/storage-memory-lifecycle-ledger.test.ts +1146 -3
  767. package/src/storage.ts +942 -903
  768. package/src/structured-attributes.ts +52 -0
  769. package/src/temporal-index-batch.ts +112 -0
  770. package/src/temporal-index.test.ts +193 -0
  771. package/src/temporal-index.ts +49 -53
  772. package/src/testing/lifecycle-matrix.test.ts +212 -0
  773. package/src/testing/lifecycle-matrix.ts +271 -0
  774. package/src/testing/orchestrator-lite.ts +236 -0
  775. package/src/testing/subjects/dreams-ledger.test.ts +67 -0
  776. package/src/testing/subjects/extraction-lifecycle.test.ts +442 -0
  777. package/src/testing/subjects/lifecycle-ledger.test.ts +93 -0
  778. package/src/testing/subjects/recall-budget.test.ts +461 -0
  779. package/src/testing/subjects/serialized-write-chain.test.ts +224 -0
  780. package/src/trust-score-stage.test.ts +154 -0
  781. package/src/trust-score-stage.ts +216 -53
  782. package/src/types-continuity.ts +57 -0
  783. package/src/types.ts +66 -77
  784. package/src/utils/path-containment.test.ts +75 -0
  785. package/src/utils/path-containment.ts +76 -0
  786. package/src/utils/serialize-mutations.test.ts +122 -0
  787. package/src/utils/serialize-mutations.ts +68 -3
  788. package/src/wearables/memory-gen.test.ts +31 -9
  789. package/src/wearables/memory-gen.ts +95 -55
  790. package/src/wearables/pipeline.test.ts +10 -7
  791. package/src/wearables/service.test.ts +6 -8
  792. package/src/wearables/service.ts +2 -2
  793. package/src/write-envelope.extension.test.ts +111 -0
  794. package/src/write-envelope.test.ts +683 -0
  795. package/src/write-envelope.ts +902 -0
  796. package/src/write-quarantine.test.ts +193 -0
  797. package/src/write-quarantine.ts +241 -0
  798. package/src/write-rate-limiter.test.ts +105 -0
  799. package/src/write-rate-limiter.ts +135 -0
  800. package/src/write-sealed-parity.test.ts +204 -0
  801. package/dist/chunk-2VN4JAX7.js.map +0 -1
  802. package/dist/chunk-37CTGERM.js.map +0 -1
  803. package/dist/chunk-7ICYQUOL.js.map +0 -1
  804. package/dist/chunk-AD6AMF2N.js.map +0 -1
  805. package/dist/chunk-AHLPZ2SF.js.map +0 -1
  806. package/dist/chunk-ASKEM4AR.js.map +0 -1
  807. package/dist/chunk-B3F4CWQR.js.map +0 -1
  808. package/dist/chunk-BCDKBDSY.js.map +0 -1
  809. package/dist/chunk-BFPETA3S.js.map +0 -1
  810. package/dist/chunk-BFVEEB7W.js.map +0 -1
  811. package/dist/chunk-E5BK6CGV.js +0 -335
  812. package/dist/chunk-E5BK6CGV.js.map +0 -1
  813. package/dist/chunk-EEARRVZ2.js.map +0 -1
  814. package/dist/chunk-F7AU34FC.js.map +0 -1
  815. package/dist/chunk-FBNWOMCE.js.map +0 -1
  816. package/dist/chunk-FTYDWKO6.js.map +0 -1
  817. package/dist/chunk-GMSDQUCR.js.map +0 -1
  818. package/dist/chunk-GWTFLF2P.js.map +0 -1
  819. package/dist/chunk-IFJ23BHU.js.map +0 -1
  820. package/dist/chunk-IKQTFQMI.js.map +0 -1
  821. package/dist/chunk-J2HSAU72.js.map +0 -1
  822. package/dist/chunk-JMBVCAR4.js.map +0 -1
  823. package/dist/chunk-KQDNAXOM.js.map +0 -1
  824. package/dist/chunk-KSBEEMQF.js.map +0 -1
  825. package/dist/chunk-KSNVDPDF.js.map +0 -1
  826. package/dist/chunk-LC7OODWZ.js.map +0 -1
  827. package/dist/chunk-LTJAMRGI.js.map +0 -1
  828. package/dist/chunk-ND5M64JJ.js.map +0 -1
  829. package/dist/chunk-NUWYOKS6.js.map +0 -1
  830. package/dist/chunk-NWTGD4AN.js.map +0 -1
  831. package/dist/chunk-OBMR4VIX.js.map +0 -1
  832. package/dist/chunk-ODPLEWB6.js.map +0 -1
  833. package/dist/chunk-PHK3HARR.js +0 -32
  834. package/dist/chunk-PHK3HARR.js.map +0 -1
  835. package/dist/chunk-RANHFI3P.js +0 -8
  836. package/dist/chunk-RANHFI3P.js.map +0 -1
  837. package/dist/chunk-RBQLJSJ7.js.map +0 -1
  838. package/dist/chunk-RULE4VG5.js.map +0 -1
  839. package/dist/chunk-SVOZFLIQ.js +0 -16
  840. package/dist/chunk-SVOZFLIQ.js.map +0 -1
  841. package/dist/chunk-TXLGHDHS.js.map +0 -1
  842. package/dist/chunk-UN3CH3CO.js +0 -118
  843. package/dist/chunk-UN3CH3CO.js.map +0 -1
  844. package/dist/chunk-VSMRDBFT.js +0 -17
  845. package/dist/chunk-VSMRDBFT.js.map +0 -1
  846. package/dist/chunk-WU46YPY6.js.map +0 -1
  847. package/dist/chunk-X47LWMKA.js.map +0 -1
  848. package/dist/chunk-X64YLC24.js.map +0 -1
  849. package/dist/chunk-XVLEGMJI.js.map +0 -1
  850. package/dist/chunk-YF6FMLBT.js.map +0 -1
  851. package/dist/resolution-VH4NGENZ.js +0 -12
  852. /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-citation.js.map} +0 -0
  853. /package/dist/{contradiction-scan-J57T5QV7.js.map → access-memory-search-fanout.js.map} +0 -0
  854. /package/dist/{dreams-ledger-6UODQOX6.js.map → access-recall-concurrency.js.map} +0 -0
  855. /package/dist/{graph-edge-decay-FO7AM4UR.js.map → access-recall-response.js.map} +0 -0
  856. /package/dist/{resolution-VH4NGENZ.js.map → bounded-jsonl-state.js.map} +0 -0
  857. /package/dist/{capsule-merge-Z2HUO6IK.js.map → capsule-merge-5KKJQBWQ.js.map} +0 -0
  858. /package/dist/{chunk-A3GOMRJG.js.map → chunk-23L3IXFD.js.map} +0 -0
  859. /package/dist/{chunk-MRHFJV44.js.map → chunk-2DQ2XREU.js.map} +0 -0
  860. /package/dist/{chunk-L2B3EE7B.js.map → chunk-2QMKMVHH.js.map} +0 -0
  861. /package/dist/{chunk-6CALL5VN.js.map → chunk-2UFO7X4Q.js.map} +0 -0
  862. /package/dist/{chunk-2MR3MFQB.js.map → chunk-2VEOKUPP.js.map} +0 -0
  863. /package/dist/{chunk-RHOZ5LLO.js.map → chunk-347WD2LE.js.map} +0 -0
  864. /package/dist/{chunk-5S7Z5SHA.js.map → chunk-34RC5RT7.js.map} +0 -0
  865. /package/dist/{chunk-KGWXHWAH.js.map → chunk-3QAH2S33.js.map} +0 -0
  866. /package/dist/{chunk-ZIDSD6VC.js.map → chunk-3Y224NYC.js.map} +0 -0
  867. /package/dist/{chunk-VFSP73LP.js.map → chunk-45GF7Y3V.js.map} +0 -0
  868. /package/dist/{chunk-OEHCMALU.js.map → chunk-5I4M26Q7.js.map} +0 -0
  869. /package/dist/{chunk-2E6G7FCE.js.map → chunk-5Q7EVV4X.js.map} +0 -0
  870. /package/dist/{chunk-VZ5OJE4X.js.map → chunk-5VQJBH2G.js.map} +0 -0
  871. /package/dist/{chunk-PUBTHW5O.js.map → chunk-6DRPPQEJ.js.map} +0 -0
  872. /package/dist/{chunk-4LEF6XWJ.js.map → chunk-6O22TZSB.js.map} +0 -0
  873. /package/dist/{chunk-ZQ7UBUZ6.js.map → chunk-6QYST5XN.js.map} +0 -0
  874. /package/dist/{chunk-W4PGTAS2.js.map → chunk-6WAFYNVT.js.map} +0 -0
  875. /package/dist/{chunk-R5VAVVHP.js.map → chunk-72A4L5JJ.js.map} +0 -0
  876. /package/dist/{chunk-3FAMU5TX.js.map → chunk-A6USBZTO.js.map} +0 -0
  877. /package/dist/{chunk-TXVD7PLT.js.map → chunk-A72SWTWW.js.map} +0 -0
  878. /package/dist/{chunk-FC77VUX5.js.map → chunk-A7Q43EUZ.js.map} +0 -0
  879. /package/dist/{chunk-UJBESW6X.js.map → chunk-AE7WMV4V.js.map} +0 -0
  880. /package/dist/{chunk-OKUEZOGW.js.map → chunk-AITCS2RC.js.map} +0 -0
  881. /package/dist/{chunk-OC35KQIN.js.map → chunk-AJ7QCSIS.js.map} +0 -0
  882. /package/dist/{chunk-FNMFJWBY.js.map → chunk-ANEUPF6O.js.map} +0 -0
  883. /package/dist/{chunk-2A3OXAP2.js.map → chunk-B7RWBZXW.js.map} +0 -0
  884. /package/dist/{chunk-N3JDIRBW.js.map → chunk-BLAKS3CP.js.map} +0 -0
  885. /package/dist/{chunk-M7VGG3ID.js.map → chunk-BV5WMSIJ.js.map} +0 -0
  886. /package/dist/{chunk-2BFLDH5F.js.map → chunk-BXLOS5AJ.js.map} +0 -0
  887. /package/dist/{chunk-UXKMMNJD.js.map → chunk-C23BYTJV.js.map} +0 -0
  888. /package/dist/{chunk-JZHHREBL.js.map → chunk-C3AKM55W.js.map} +0 -0
  889. /package/dist/{chunk-RP7YXA67.js.map → chunk-C5JF6EZ7.js.map} +0 -0
  890. /package/dist/{chunk-DDGV7NAX.js.map → chunk-CQMEM5CC.js.map} +0 -0
  891. /package/dist/{chunk-A7B4RMQG.js.map → chunk-CXZQOVIE.js.map} +0 -0
  892. /package/dist/{chunk-7N2RUBAP.js.map → chunk-DDA47TES.js.map} +0 -0
  893. /package/dist/{chunk-64SE4X5G.js.map → chunk-DEBHXHJR.js.map} +0 -0
  894. /package/dist/{chunk-EHK6KZHK.js.map → chunk-DNHVPZOD.js.map} +0 -0
  895. /package/dist/{chunk-437S3G37.js.map → chunk-DQEMWVMT.js.map} +0 -0
  896. /package/dist/{chunk-LZZNTPLR.js.map → chunk-DX3XRGLH.js.map} +0 -0
  897. /package/dist/{chunk-7MW3CVLD.js.map → chunk-ES36HTSC.js.map} +0 -0
  898. /package/dist/{chunk-TN7THW5U.js.map → chunk-ET356KSB.js.map} +0 -0
  899. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  900. /package/dist/{chunk-7LFXDGLH.js.map → chunk-F7GAIWFA.js.map} +0 -0
  901. /package/dist/{chunk-IKSELOIV.js.map → chunk-FFPJINJE.js.map} +0 -0
  902. /package/dist/{chunk-V53GJUSL.js.map → chunk-FUTF72MJ.js.map} +0 -0
  903. /package/dist/{chunk-5UNWK7KU.js.map → chunk-GFPZS3F5.js.map} +0 -0
  904. /package/dist/{chunk-G6NKALRN.js.map → chunk-H4WG3UQZ.js.map} +0 -0
  905. /package/dist/{chunk-HLTAPHZZ.js.map → chunk-HNZKVLAN.js.map} +0 -0
  906. /package/dist/{chunk-5EZXHU36.js.map → chunk-HYQJQGD3.js.map} +0 -0
  907. /package/dist/{chunk-TRWPCR6P.js.map → chunk-JECBQBEX.js.map} +0 -0
  908. /package/dist/{chunk-HG32URJJ.js.map → chunk-JYIFGGEM.js.map} +0 -0
  909. /package/dist/{chunk-VA4WSW54.js.map → chunk-JYIJNNN3.js.map} +0 -0
  910. /package/dist/{chunk-L5HJQC36.js.map → chunk-K2TDK7GG.js.map} +0 -0
  911. /package/dist/{chunk-MKDRQAA3.js.map → chunk-KAVBJSBQ.js.map} +0 -0
  912. /package/dist/{chunk-4OKLXWDZ.js.map → chunk-KNB7IBBU.js.map} +0 -0
  913. /package/dist/{chunk-QLULYKWZ.js.map → chunk-KSXJOC4F.js.map} +0 -0
  914. /package/dist/{chunk-JI5AVXYO.js.map → chunk-LEPOP46W.js.map} +0 -0
  915. /package/dist/{chunk-O7BBFJLP.js.map → chunk-LTAVSOZW.js.map} +0 -0
  916. /package/dist/{chunk-2IZAGAJR.js.map → chunk-LULR4IUW.js.map} +0 -0
  917. /package/dist/{chunk-WMWVO45V.js.map → chunk-MC6XO7T4.js.map} +0 -0
  918. /package/dist/{chunk-D6RIFG4O.js.map → chunk-MNB5RMAY.js.map} +0 -0
  919. /package/dist/{chunk-67ZCJLJY.js.map → chunk-MYFQ3ED4.js.map} +0 -0
  920. /package/dist/{chunk-V3UE5I5F.js.map → chunk-MYVPLJOO.js.map} +0 -0
  921. /package/dist/{chunk-MTWKDOVO.js.map → chunk-NLE6TSLG.js.map} +0 -0
  922. /package/dist/{chunk-DBRYEIGY.js.map → chunk-OJP3N7UK.js.map} +0 -0
  923. /package/dist/{chunk-SMX5267P.js.map → chunk-OZKOPQCM.js.map} +0 -0
  924. /package/dist/{chunk-JCK7V6SN.js.map → chunk-P65YJEEL.js.map} +0 -0
  925. /package/dist/{chunk-MTR7BZQN.js.map → chunk-PBGWYM3H.js.map} +0 -0
  926. /package/dist/{chunk-AXTXN4T6.js.map → chunk-PWFCHOB3.js.map} +0 -0
  927. /package/dist/{chunk-FDPZVY6D.js.map → chunk-QFN4U7AG.js.map} +0 -0
  928. /package/dist/{chunk-K2ZLHESQ.js.map → chunk-QIEUU4UK.js.map} +0 -0
  929. /package/dist/{chunk-3D6AFGQR.js.map → chunk-QVT3E4CE.js.map} +0 -0
  930. /package/dist/{chunk-SFQ6QNL7.js.map → chunk-R6YJI3HE.js.map} +0 -0
  931. /package/dist/{chunk-5CJVLG3H.js.map → chunk-RFDD3B7W.js.map} +0 -0
  932. /package/dist/{chunk-JBPKEARU.js.map → chunk-RKNJBZ55.js.map} +0 -0
  933. /package/dist/{chunk-UTRJFDP2.js.map → chunk-S2BBISHU.js.map} +0 -0
  934. /package/dist/{chunk-IALLX7EE.js.map → chunk-S2FSBU4H.js.map} +0 -0
  935. /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
  936. /package/dist/{chunk-PK5XBDA6.js.map → chunk-U3ZAVRP3.js.map} +0 -0
  937. /package/dist/{chunk-4KJLYTLS.js.map → chunk-V57NLO5P.js.map} +0 -0
  938. /package/dist/{chunk-FLVL4L3L.js.map → chunk-VDX2J7OX.js.map} +0 -0
  939. /package/dist/{chunk-7XGSIUP5.js.map → chunk-W4FBGYGK.js.map} +0 -0
  940. /package/dist/{chunk-6SXVCD7W.js.map → chunk-W7IGLX4G.js.map} +0 -0
  941. /package/dist/{chunk-KAWTOYAW.js.map → chunk-WLZYGLJ4.js.map} +0 -0
  942. /package/dist/{chunk-W5NKDU24.js.map → chunk-WON4APQY.js.map} +0 -0
  943. /package/dist/{chunk-NKCPUHCI.js.map → chunk-WZFAPHIK.js.map} +0 -0
  944. /package/dist/{chunk-KQPJLK3X.js.map → chunk-XL24QQ63.js.map} +0 -0
  945. /package/dist/{chunk-4B4MUS5D.js.map → chunk-YROFNEG5.js.map} +0 -0
  946. /package/dist/{chunk-VRAD2FGO.js.map → chunk-Z54EM5YZ.js.map} +0 -0
  947. /package/dist/{first-start-migration-TI67YMLG.js.map → first-start-migration-CSRHCJIQ.js.map} +0 -0
  948. /package/dist/{forget-CQCLB5LB.js.map → forget-HRPLVB3W.js.map} +0 -0
  949. /package/dist/{purge-NDWV6WYD.js.map → purge-IRF47ZTX.js.map} +0 -0
  950. /package/dist/{tier-stats-KV7IWCJR.js.map → tier-stats-QEJSPFV2.js.map} +0 -0
package/src/storage.ts CHANGED
@@ -20,19 +20,36 @@ 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,
33
48
  getCachedMemories,
34
49
  invalidateAllForDir,
35
50
  invalidateDerivedAndGlobalForDir,
51
+ invalidateForScope,
52
+ invalidateForScopeExceptHot,
36
53
  setCachedEntities,
37
54
  setCachedMemories,
38
55
  updateCacheOnDelete,
@@ -61,6 +78,7 @@ import {
61
78
  SecureStoreLockedError,
62
79
  MAGIC_HEADER_SIZE,
63
80
  isEncryptedFile,
81
+ probeEncryptedRegularFileHeader,
64
82
  readMaybeEncryptedFileBuffer,
65
83
  readMaybeEncryptedFile,
66
84
  writeMaybeEncryptedFile,
@@ -136,6 +154,35 @@ import type {
136
154
  ProvenanceSource,
137
155
  } from "./types.js";
138
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";
139
186
  import {
140
187
  type ProjectedMemoryBrowseOptions,
141
188
  type ProjectedMemoryBrowsePage,
@@ -149,11 +196,16 @@ import {
149
196
  import {
150
197
  inferMemoryStatus,
151
198
  isArchivedMemoryPath,
152
- sortMemoryLifecycleEvents,
153
199
  toMemoryPathRel,
154
200
  } from "./memory-lifecycle-ledger-utils.js";
155
201
  import { normalizeProjectionPreview, normalizeProjectionTags } from "./memory-projection-format.js";
156
202
  import { parseFlexibleIsoTimestamp } from "./utils/iso-timestamp.js";
203
+ import {
204
+ composeMemoryEnvelope,
205
+ isSealedMemoryEnvelope,
206
+ sealedWriteToLegacyArgs,
207
+ type SealedMemoryEnvelope,
208
+ } from "./write-envelope.js";
157
209
  // stripCitation import removed: legacy rebuild fallback was replaced by a
158
210
  // skip-with-warning strategy (Finding 1 — Uhol). See ensureFactHashIndexAuthoritative.
159
211
 
@@ -1113,138 +1165,10 @@ function dehyphenate(s: string): string {
1113
1165
  return s.replace(/-/g, "");
1114
1166
  }
1115
1167
 
1116
- /**
1117
- * Content-hash dedup index for facts.
1118
- * Normalizes content (lowercase, strip punctuation, collapse whitespace),
1119
- * computes SHA-256, and stores hashes in a line-delimited file.
1120
- * Prevents writing semantically identical facts.
1121
- */
1122
- export class ContentHashIndex {
1123
- private hashes: Set<string> = new Set();
1124
- private dirty = false;
1125
- private readonly filePath: string;
1126
- private readonly secureStoreKeyProvider: () => Buffer | null;
1127
- private readonly secureStoreWriteKeyProvider: () => Buffer | null;
1128
- private readonly memoryDir: string;
1129
-
1130
- constructor(
1131
- stateDir: string,
1132
- secureStoreKeyProvider: () => Buffer | null = () => null,
1133
- secureStoreWriteKeyProvider: () => Buffer | null = secureStoreKeyProvider,
1134
- memoryDir: string = path.dirname(stateDir)
1135
- ) {
1136
- this.filePath = path.join(stateDir, "fact-hashes.txt");
1137
- this.secureStoreKeyProvider = secureStoreKeyProvider;
1138
- this.secureStoreWriteKeyProvider = secureStoreWriteKeyProvider;
1139
- this.memoryDir = memoryDir;
1140
- }
1141
-
1142
- /** Load existing hashes from disk. Safe to call multiple times. */
1143
- async load(): Promise<void> {
1144
- try {
1145
- const raw = await readMaybeEncryptedFile(this.filePath, this.secureStoreKeyProvider(), this.memoryDir);
1146
- for (const line of raw.split("\n")) {
1147
- const trimmed = line.trim();
1148
- if (trimmed.length > 0) {
1149
- this.hashes.add(trimmed);
1150
- }
1151
- }
1152
- log.debug(`content-hash index: loaded ${this.hashes.size} hashes`);
1153
- } catch (err) {
1154
- if (err instanceof SecureStoreLockedError) throw err;
1155
- if (!isErrnoCode(err, "ENOENT")) throw err;
1156
- log.debug("content-hash index: no existing index — starting fresh");
1157
- }
1158
- }
1159
-
1160
- /** Check if content already exists in the index. */
1161
- has(content: string): boolean {
1162
- return this.hashes.has(ContentHashIndex.computeHash(content));
1163
- }
1164
-
1165
- /** Add content hash to the index. */
1166
- add(content: string): void {
1167
- const hash = ContentHashIndex.computeHash(content);
1168
- if (!this.hashes.has(hash)) {
1169
- this.hashes.add(hash);
1170
- this.dirty = true;
1171
- }
1172
- }
1173
-
1174
- get size(): number {
1175
- return this.hashes.size;
1176
- }
1177
-
1178
- /** Clear all loaded hashes so the next save rewrites the index from scratch. */
1179
- clear(): void {
1180
- if (this.hashes.size > 0) {
1181
- this.hashes.clear();
1182
- }
1183
- this.dirty = true;
1184
- }
1185
-
1186
- /** Persist index to disk if changed. */
1187
- async save(): Promise<void> {
1188
- if (!this.dirty) return;
1189
- await mkdir(path.dirname(this.filePath), { recursive: true });
1190
- await writeMaybeEncryptedFile(
1191
- this.filePath,
1192
- [...this.hashes].join("\n") + "\n",
1193
- this.secureStoreWriteKeyProvider(),
1194
- {},
1195
- this.memoryDir
1196
- );
1197
- this.dirty = false;
1198
- log.debug(`content-hash index: saved ${this.hashes.size} hashes`);
1199
- }
1200
-
1201
- /** Remove a hash from the index (used when archiving/deleting). */
1202
- remove(content: string): void {
1203
- const hash = ContentHashIndex.computeHash(content);
1204
- if (this.hashes.delete(hash)) {
1205
- this.dirty = true;
1206
- }
1207
- }
1208
-
1209
- /**
1210
- * Remove a pre-computed SHA-256 hash directly from the index without
1211
- * re-hashing. Use this when the caller already holds the stored hash
1212
- * (e.g. `memory.frontmatter.contentHash`) to avoid the double-hash bug
1213
- * where `remove(hash)` would compute `hash(hash)` and never match the
1214
- * entry.
1215
- */
1216
- removeByHash(hash: string): void {
1217
- if (this.hashes.delete(hash)) {
1218
- this.dirty = true;
1219
- }
1220
- }
1221
-
1222
- /**
1223
- * Add a pre-computed SHA-256 hash directly to the index without re-hashing.
1224
- * Use this when the caller already holds the stored hash
1225
- * (e.g. `memory.frontmatter.contentHash`) so that the index records the raw
1226
- * content hash rather than re-hashing the citation-annotated body.
1227
- *
1228
- * @internal Only called from `StorageManager.ensureFactHashIndexAuthoritative`.
1229
- * Not part of the public API — prefer `add(content)` for external callers.
1230
- */
1231
- addByHash(hash: string): void {
1232
- if (!this.hashes.has(hash)) {
1233
- this.hashes.add(hash);
1234
- this.dirty = true;
1235
- }
1236
- }
1237
-
1238
- /** Normalize content (delegates to content-hash.ts for a single source of truth). */
1239
- static normalizeContent(content: string): string {
1240
- return normalizeContent(content);
1241
- }
1242
-
1243
- /** Compute SHA-256 hash (delegates to content-hash.ts for a single source of truth). */
1244
- static computeHash(content: string): string {
1245
- return computeContentHash(content);
1246
- }
1247
- }
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;
1248
1172
 
1249
1173
  // ---------------------------------------------------------------------------
1250
1174
  // Attribute normalization helper
@@ -1271,16 +1195,14 @@ export class ContentHashIndex {
1271
1195
  // the coding surfaces + wearable service). Imported here so internal callers
1272
1196
  // (snapshotBeforeWrite, snapshotForProvenance) resolve, and re-exported to
1273
1197
  // keep the public storage API stable for existing callers (wearables, dist).
1274
- import { stripAttributesSuffix } from "./structured-attributes.js";
1198
+ import { assemblePersistedBody, stripAttributesSuffix } from "./structured-attributes.js";
1275
1199
  export { stripAttributesSuffix };
1276
1200
 
1277
- export function normalizeAttributePairs(pairs: Record<string, string>): string {
1278
- return Object.entries(pairs)
1279
- .map(([k, v]) => [k.trim().toLowerCase(), v.trim()] as [string, string])
1280
- .sort(([a], [b]) => a.localeCompare(b))
1281
- .map(([k, v]) => `${k}: ${v}`)
1282
- .join("; ");
1283
- }
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";
1284
1206
 
1285
1207
  // ---------------------------------------------------------------------------
1286
1208
  // Entity file parsing / serialization (Knowledge Graph v7.0)
@@ -1427,471 +1349,6 @@ function readEntitySectionText(
1427
1349
  return sectionLines.join(options.preserveBullets === true ? "\n" : " ");
1428
1350
  }
1429
1351
 
1430
- function parseEntityTimelineBullet(bullet: string, fallbackTimestamp: string): EntityTimelineEntry | null {
1431
- const trimmed = bullet.trim();
1432
- if (!trimmed) return null;
1433
-
1434
- let rest = trimmed;
1435
- const entry: EntityTimelineEntry = {
1436
- timestamp: trimmed.startsWith("[") ? "" : fallbackTimestamp,
1437
- text: "",
1438
- };
1439
- const consumedMetadataSegments: string[] = [];
1440
- let literalSingleSourceSegment: string | undefined;
1441
-
1442
- if (!trimmed.startsWith("[")) {
1443
- entry.text = trimmed;
1444
- return entry.text ? entry : null;
1445
- }
1446
-
1447
- const firstEnd = trimmed.indexOf("]");
1448
- if (firstEnd === -1) {
1449
- entry.text = trimmed;
1450
- return entry.text ? entry : null;
1451
- }
1452
-
1453
- const firstToken = trimmed.slice(1, firstEnd).trim();
1454
- const parsedTimestamp = Date.parse(firstToken);
1455
- if (Number.isFinite(parsedTimestamp)) {
1456
- entry.timestamp = firstToken || fallbackTimestamp;
1457
- rest = trimmed.slice(firstEnd + 1).trimStart();
1458
- }
1459
-
1460
- while (rest.startsWith("[")) {
1461
- const end = findEntityTimelineTokenEnd(rest);
1462
- if (end === -1) break;
1463
- const rawSegment = rest.slice(0, end + 1);
1464
- const token = rest.slice(1, end).trim();
1465
- const equalsIdx = token.indexOf("=");
1466
- if (equalsIdx === -1) {
1467
- if (rest === trimmed) {
1468
- entry.text = trimmed;
1469
- return entry.text ? entry : null;
1470
- }
1471
- break;
1472
- }
1473
- const key = token.slice(0, equalsIdx).trim().toLowerCase();
1474
- const value = unescapeEntityTimelineMetadataValue(token.slice(equalsIdx + 1).trim());
1475
- if (!value) break;
1476
- const nextRest = rest.slice(end + 1).trimStart();
1477
- switch (key) {
1478
- case "source_meta":
1479
- entry.source = value;
1480
- break;
1481
- case "source":
1482
- if (
1483
- consumedMetadataSegments.length === 0 &&
1484
- !nextRest.startsWith("[") &&
1485
- nextRest.length > 0 &&
1486
- !isManagedEntityTimelineSource(value)
1487
- ) {
1488
- literalSingleSourceSegment = rawSegment;
1489
- rest = nextRest;
1490
- break;
1491
- }
1492
- entry.source = value;
1493
- break;
1494
- case "session":
1495
- case "sessionkey":
1496
- entry.sessionKey = value;
1497
- break;
1498
- case "principal":
1499
- entry.principal = value;
1500
- break;
1501
- default:
1502
- entry.text = rest.trim();
1503
- return entry.text ? entry : null;
1504
- }
1505
- if (literalSingleSourceSegment) break;
1506
- consumedMetadataSegments.push(rawSegment);
1507
- rest = nextRest;
1508
- }
1509
-
1510
- if (literalSingleSourceSegment) {
1511
- return {
1512
- timestamp: entry.timestamp,
1513
- text: `${literalSingleSourceSegment} ${rest}`.trim(),
1514
- };
1515
- }
1516
-
1517
- entry.text = rest.trim();
1518
- if (!entry.text) return null;
1519
- return entry;
1520
- }
1521
-
1522
- function isEntitySynthesisTimelinePromotionBullet(bullet: string): boolean {
1523
- const trimmed = bullet.trim();
1524
- if (!trimmed.startsWith("[")) return false;
1525
-
1526
- const firstEnd = findEntityTimelineTokenEnd(trimmed);
1527
- if (firstEnd === -1) return false;
1528
-
1529
- const firstToken = trimmed.slice(1, firstEnd).trim();
1530
- return looksLikeEntityTimelineTimestamp(firstToken);
1531
- }
1532
-
1533
- function looksLikeEntityTimelineTimestamp(token: string): boolean {
1534
- if (!/^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$/.test(token)) return false;
1535
- return Number.isFinite(Date.parse(token));
1536
- }
1537
-
1538
- function isManagedEntityTimelineSource(source: string): boolean {
1539
- switch (source.trim().toLowerCase()) {
1540
- case "artifact":
1541
- case "chunking":
1542
- case "cli-migrate":
1543
- case "compounding-promotion":
1544
- case "consolidation":
1545
- case "contradiction-detection":
1546
- case "entity_extraction":
1547
- case "explicit":
1548
- case "explicit-inline":
1549
- case "explicit-inline-review":
1550
- case "explicit-review":
1551
- case "extraction":
1552
- case "extraction-shared-promotion":
1553
- case "manual":
1554
- case "migration":
1555
- case "migration-rechunk":
1556
- case "proactive":
1557
- case "replay":
1558
- case "semantic-consolidation":
1559
- case "unknown":
1560
- return true;
1561
- default:
1562
- return false;
1563
- }
1564
- }
1565
-
1566
- function findEntityTimelineTokenEnd(input: string): number {
1567
- let escaped = false;
1568
- for (let index = 0; index < input.length; index += 1) {
1569
- const char = input[index];
1570
- if (escaped) {
1571
- escaped = false;
1572
- continue;
1573
- }
1574
- if (char === "\\") {
1575
- escaped = true;
1576
- continue;
1577
- }
1578
- if (char === "]") return index;
1579
- }
1580
- return -1;
1581
- }
1582
-
1583
- function escapeEntityTimelineMetadataValue(value: string): string {
1584
- let escaped = "";
1585
- for (const char of value) {
1586
- switch (char) {
1587
- case "\\":
1588
- escaped += "\\\\";
1589
- break;
1590
- case "]":
1591
- escaped += "\\]";
1592
- break;
1593
- case "\n":
1594
- escaped += "\\n";
1595
- break;
1596
- case "\r":
1597
- escaped += "\\r";
1598
- break;
1599
- case "\t":
1600
- escaped += "\\t";
1601
- break;
1602
- default: {
1603
- const codePoint = char.codePointAt(0) ?? 0;
1604
- if (codePoint < 0x20) {
1605
- escaped += `\\u${codePoint.toString(16).padStart(4, "0")}`;
1606
- } else {
1607
- escaped += char;
1608
- }
1609
- }
1610
- }
1611
- }
1612
- return escaped;
1613
- }
1614
-
1615
- function unescapeEntityTimelineMetadataValue(value: string): string {
1616
- if (!value.includes("\\")) return value;
1617
-
1618
- let result = "";
1619
- for (let index = 0; index < value.length; index += 1) {
1620
- const char = value[index];
1621
- if (char !== "\\") {
1622
- result += char;
1623
- continue;
1624
- }
1625
-
1626
- const next = value[index + 1];
1627
- if (!next) {
1628
- result += "\\";
1629
- break;
1630
- }
1631
-
1632
- switch (next) {
1633
- case "n":
1634
- result += "\n";
1635
- index += 1;
1636
- break;
1637
- case "r":
1638
- result += "\r";
1639
- index += 1;
1640
- break;
1641
- case "t":
1642
- result += "\t";
1643
- index += 1;
1644
- break;
1645
- case "u": {
1646
- const hex = value.slice(index + 2, index + 6);
1647
- if (/^[0-9a-fA-F]{4}$/.test(hex)) {
1648
- result += String.fromCharCode(parseInt(hex, 16));
1649
- index += 5;
1650
- break;
1651
- }
1652
- result += "u";
1653
- index += 1;
1654
- break;
1655
- }
1656
- default:
1657
- result += next;
1658
- index += 1;
1659
- break;
1660
- }
1661
- }
1662
- return result;
1663
- }
1664
-
1665
- function serializeEntityTimelineEntry(entry: EntityTimelineEntry): string {
1666
- const tokens: string[] = [];
1667
- if (entry.timestamp.trim().length > 0) {
1668
- tokens.push(`[${entry.timestamp}]`);
1669
- }
1670
- if (entry.source) {
1671
- const sourceKey = isManagedEntityTimelineSource(entry.source) ? "source" : "source_meta";
1672
- tokens.push(`[${sourceKey}=${escapeEntityTimelineMetadataValue(entry.source)}]`);
1673
- }
1674
- if (entry.sessionKey) {
1675
- tokens.push(`[session=${escapeEntityTimelineMetadataValue(entry.sessionKey)}]`);
1676
- }
1677
- if (entry.principal) {
1678
- tokens.push(`[principal=${escapeEntityTimelineMetadataValue(entry.principal)}]`);
1679
- }
1680
- const serializedMetadata = tokens.length > 0 ? `${tokens.join(" ")} ` : "";
1681
- return `- ${serializedMetadata}${entry.text}`.trimEnd();
1682
- }
1683
-
1684
- function dedupeEntityTimelineFacts(timeline: EntityTimelineEntry[]): string[] {
1685
- return [...new Set(timeline.map((entry) => entry.text.trim()).filter((entry) => entry.length > 0))];
1686
- }
1687
-
1688
- function normalizeEntitySectionFact(value: string): string {
1689
- return value.replace(/\s+/g, " ").trim();
1690
- }
1691
-
1692
- export function normalizeStructuredSectionFacts(facts: string[]): string[] {
1693
- return [...new Set(facts.map((fact) => normalizeEntitySectionFact(fact)).filter((fact) => fact.length > 0))];
1694
- }
1695
-
1696
- function collectStructuredSectionFacts(structuredSections: EntityStructuredSection[]): string[] {
1697
- const facts: string[] = [];
1698
- for (const section of structuredSections) {
1699
- for (const fact of section.facts) {
1700
- const normalized = normalizeEntitySectionFact(fact);
1701
- if (!normalized) continue;
1702
- facts.push(normalized);
1703
- }
1704
- }
1705
- return [...new Set(facts)];
1706
- }
1707
-
1708
- export function compileEntityFacts(
1709
- timeline: EntityTimelineEntry[],
1710
- structuredSections: EntityStructuredSection[]
1711
- ): string[] {
1712
- const facts: string[] = [];
1713
- const seen = new Set<string>();
1714
- for (const fact of dedupeEntityTimelineFacts(timeline)) {
1715
- if (seen.has(fact)) continue;
1716
- seen.add(fact);
1717
- facts.push(fact);
1718
- }
1719
- for (const fact of collectStructuredSectionFacts(structuredSections)) {
1720
- if (seen.has(fact)) continue;
1721
- seen.add(fact);
1722
- facts.push(fact);
1723
- }
1724
- return facts;
1725
- }
1726
-
1727
- function parseEntityStructuredSectionFacts(lines: string[]): string[] {
1728
- const facts: string[] = [];
1729
- let currentBlock: string[] = [];
1730
-
1731
- const flushCurrentBlock = (): void => {
1732
- const normalized = normalizeEntitySectionFact(currentBlock.join(" "));
1733
- if (normalized.length > 0) facts.push(normalized);
1734
- currentBlock = [];
1735
- };
1736
-
1737
- for (const rawLine of lines) {
1738
- const line = rawLine.trim();
1739
- if (!line) {
1740
- flushCurrentBlock();
1741
- continue;
1742
- }
1743
- if (line.startsWith("- ")) {
1744
- flushCurrentBlock();
1745
- currentBlock = [line.slice(2).trim()];
1746
- continue;
1747
- }
1748
- currentBlock.push(line);
1749
- }
1750
-
1751
- flushCurrentBlock();
1752
- return [...new Set(facts)];
1753
- }
1754
-
1755
- function looksLikeStructuredSectionFactList(lines: string[]): boolean {
1756
- const firstNonBlank = lines.find((line) => line.trim().length > 0)?.trim() ?? "";
1757
- return firstNonBlank.startsWith("- ");
1758
- }
1759
-
1760
- function partitionEntityStructuredSections(
1761
- entityType: string,
1762
- extraSections: Array<{ title: string; lines: string[] }>,
1763
- entitySchemas?: PluginConfig["entitySchemas"]
1764
- ): {
1765
- structuredSections: EntityStructuredSection[];
1766
- remainingExtraSections: Array<{ title: string; lines: string[] }>;
1767
- } {
1768
- const structuredSections: EntityStructuredSection[] = [];
1769
- const remainingExtraSections: Array<{ title: string; lines: string[] }> = [];
1770
- const structuredSectionIndex = new Map<string, EntityStructuredSection>();
1771
-
1772
- for (const section of extraSections) {
1773
- const matchedSection = matchEntitySchemaSection(entityType, section.title, entitySchemas);
1774
- if (!matchedSection && !looksLikeStructuredSectionFactList(section.lines)) {
1775
- remainingExtraSections.push(section);
1776
- continue;
1777
- }
1778
- const facts = parseEntityStructuredSectionFacts(section.lines);
1779
- if (!matchedSection && facts.length === 0) {
1780
- remainingExtraSections.push(section);
1781
- continue;
1782
- }
1783
- const normalizedSection = matchedSection
1784
- ? { key: matchedSection.key, title: matchedSection.title }
1785
- : normalizeEntityStructuredSection(entityType, { key: section.title, title: section.title }, entitySchemas);
1786
- if (facts.length === 0) {
1787
- remainingExtraSections.push(section);
1788
- continue;
1789
- }
1790
- const existing = structuredSectionIndex.get(normalizedSection.key);
1791
- if (existing) {
1792
- existing.facts = normalizeStructuredSectionFacts([...existing.facts, ...facts]);
1793
- continue;
1794
- }
1795
- const structuredSection: EntityStructuredSection = {
1796
- key: normalizedSection.key,
1797
- title: normalizedSection.title,
1798
- facts: normalizeStructuredSectionFacts(facts),
1799
- };
1800
- structuredSections.push(structuredSection);
1801
- structuredSectionIndex.set(normalizedSection.key, structuredSection);
1802
- }
1803
-
1804
- return {
1805
- structuredSections,
1806
- remainingExtraSections,
1807
- };
1808
- }
1809
-
1810
- function latestEntityTimelineTimestamp(entity: EntityFile): string | undefined {
1811
- let latestRaw: string | undefined;
1812
- for (const entry of entity.timeline) {
1813
- const timestamp = entry.timestamp.trim();
1814
- if (!timestamp) continue;
1815
- if (!latestRaw || compareEntityTimestamps(timestamp, latestRaw) > 0) {
1816
- latestRaw = timestamp;
1817
- }
1818
- }
1819
- return latestRaw;
1820
- }
1821
-
1822
- export function compareEntityTimestamps(left?: string, right?: string): number {
1823
- const leftValue = left?.trim() ?? "";
1824
- const rightValue = right?.trim() ?? "";
1825
-
1826
- if (!leftValue && !rightValue) return 0;
1827
- if (!leftValue) return -1;
1828
- if (!rightValue) return 1;
1829
-
1830
- const leftMs = Date.parse(leftValue);
1831
- const rightMs = Date.parse(rightValue);
1832
- const leftParsed = Number.isFinite(leftMs);
1833
- const rightParsed = Number.isFinite(rightMs);
1834
-
1835
- if (leftParsed && rightParsed) {
1836
- if (leftMs === rightMs) return 0;
1837
- return leftMs > rightMs ? 1 : -1;
1838
- }
1839
- if (leftParsed) return 1;
1840
- if (rightParsed) return -1;
1841
- return leftValue.localeCompare(rightValue);
1842
- }
1843
-
1844
- export function countEntityStructuredFacts(entity: EntityFile): number {
1845
- return (entity.structuredSections ?? []).reduce((count, section) => count + section.facts.length, 0);
1846
- }
1847
-
1848
- export function fingerprintEntityStructuredFacts(entity: Pick<EntityFile, "structuredSections">): string | undefined {
1849
- const normalizedSections = (entity.structuredSections ?? [])
1850
- .map((section) => ({
1851
- key: section.key.trim().toLowerCase(),
1852
- title: section.title.replace(/\s+/g, " ").trim(),
1853
- facts: normalizeStructuredSectionFacts(section.facts)
1854
- .slice()
1855
- .sort((left, right) => left.localeCompare(right)),
1856
- }))
1857
- .filter((section) => section.facts.length > 0)
1858
- .sort(
1859
- (left, right) =>
1860
- left.key.localeCompare(right.key) ||
1861
- left.title.localeCompare(right.title) ||
1862
- left.facts.join("\n").localeCompare(right.facts.join("\n"))
1863
- );
1864
- if (normalizedSections.length === 0) return undefined;
1865
- return createHash("sha256").update(JSON.stringify(normalizedSections)).digest("hex");
1866
- }
1867
-
1868
- export function isEntitySynthesisStale(entity: EntityFile): boolean {
1869
- const structuredFactCount = countEntityStructuredFacts(entity);
1870
- const structuredFactDigest = fingerprintEntityStructuredFacts(entity);
1871
- const storedStructuredFactDigest = entity.synthesisStructuredFactDigest?.trim() || undefined;
1872
- if (entity.timeline.length === 0 && structuredFactCount === 0) return false;
1873
- if (!entity.synthesis?.trim()) return true;
1874
- if (entity.synthesisTimelineCount === undefined) return true;
1875
- if (structuredFactCount > 0 && entity.synthesisStructuredFactCount === undefined) return true;
1876
- if (structuredFactCount > 0 && !storedStructuredFactDigest) return true;
1877
- const latestTimelineTimestamp = latestEntityTimelineTimestamp(entity);
1878
- if (!latestTimelineTimestamp) {
1879
- return (
1880
- entity.timeline.length > entity.synthesisTimelineCount ||
1881
- structuredFactCount > (entity.synthesisStructuredFactCount ?? 0) ||
1882
- structuredFactDigest !== storedStructuredFactDigest
1883
- );
1884
- }
1885
- if (!entity.synthesisUpdatedAt?.trim()) return true;
1886
- const timelineFreshness = compareEntityTimestamps(latestTimelineTimestamp, entity.synthesisUpdatedAt);
1887
- if (timelineFreshness > 0) return true;
1888
- return (
1889
- entity.timeline.length > entity.synthesisTimelineCount ||
1890
- structuredFactCount > (entity.synthesisStructuredFactCount ?? 0) ||
1891
- structuredFactDigest !== storedStructuredFactDigest
1892
- );
1893
- }
1894
-
1895
1352
  /**
1896
1353
  * Parse an entity markdown file into a structured EntityFile.
1897
1354
  * Backward compatible: old files without new sections get empty arrays.
@@ -2313,6 +1770,105 @@ function parseExtractionRetryStateEntries(
2313
1770
  return valid;
2314
1771
  }
2315
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
+
2316
1872
  export class StorageManager {
2317
1873
  private knowledgeIndexCache: { result: string; builtAt: number } | null = null;
2318
1874
  private artifactIndexCache: { memories: MemoryFile[]; loadedAtMs: number; writeVersion: number } | null = null;
@@ -2385,6 +1941,17 @@ export class StorageManager {
2385
1941
  */
2386
1942
  private static hotMemoriesCacheProcessDefaultSeeded = false;
2387
1943
 
1944
+ /**
1945
+ * Per-memory-directory gate for scope-aware cache invalidation (issue #1904).
1946
+ * Registered from config by the orchestrator via
1947
+ * setScopedCacheInvalidationDefault() so every StorageManager built over a
1948
+ * memory dir — including ephemeral recall sub-stage instances — honors the
1949
+ * operator's setting. Unregistered dirs default to `true` (scoped on). Set
1950
+ * `false` to restore the pre-#1904 full-clear-per-write behavior (rollback
1951
+ * lever). Read once per write via isScopedCacheInvalidationEnabled().
1952
+ */
1953
+ private static readonly scopedCacheInvalidationByDir = new Map<string, boolean>();
1954
+
2388
1955
  // Module-level maps for readAllMemories() keyed by base directory, shared
2389
1956
  // across all StorageManager instances (static) so concurrent callers over
2390
1957
  // the same dir cooperate.
@@ -2429,8 +1996,39 @@ export class StorageManager {
2429
1996
  private factHashIndex: ContentHashIndex | null = null;
2430
1997
  private factHashIndexLoadPromise: Promise<ContentHashIndex> | null = null;
2431
1998
  private factHashIndexAuthoritative: boolean | null = null;
2432
- 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 = {};
2433
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
+ >();
2434
2032
  private offlineSyncDigestCache: Map<string, OfflineSyncDigestCacheEntry> | null = null;
2435
2033
  private offlineSyncDigestCacheLoadPromise: Promise<Map<string, OfflineSyncDigestCacheEntry>> | null = null;
2436
2034
  private offlineSyncDigestCacheWriteChain: Promise<void> = Promise.resolve();
@@ -2439,6 +2037,15 @@ export class StorageManager {
2439
2037
  citationTemplate: string = DEFAULT_CITATION_FORMAT;
2440
2038
  /** Post-write catalog hook (#1522). Installed by the namespace router; fire-and-forget. */
2441
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
+ }
2442
2049
  private notifyCatalogWrite(): void {
2443
2050
  try {
2444
2051
  this.onCatalogWrite?.();
@@ -2537,6 +2144,12 @@ export class StorageManager {
2537
2144
  // decrypted under the previous key, not just the entity layer.
2538
2145
  invalidateAllForDir(this.baseDir);
2539
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;
2540
2153
  }
2541
2154
 
2542
2155
  private getEntityCacheSecureStoreKey(): string {
@@ -2611,20 +2224,28 @@ export class StorageManager {
2611
2224
  return this._secureStoreKey !== null;
2612
2225
  }
2613
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
+
2614
2233
  /**
2615
- * 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.
2616
2238
  *
2617
- * - If `_secureStoreEncryptOnWrite` is false: returns null (plain write).
2618
- * - If `_secureStoreEncryptOnWrite` is true AND key is set: returns key.
2619
- * - If `_secureStoreEncryptOnWrite` is true AND key is null AND
2620
- * `_secureStoreRequired` is true: throws SecureStoreLockedError so the
2621
- * write fails loudly rather than silently writing plaintext (P1 finding
2622
- * from Cursor review of PR #767).
2623
- * - If `_secureStoreEncryptOnWrite` is true AND key is null AND
2624
- * `_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.
2625
2246
  */
2626
- private resolveWriteKey(): Buffer | null {
2627
- if (!this._secureStoreEncryptOnWrite) return null;
2247
+ private resolveWriteKey(forceEncrypt = false): Buffer | null {
2248
+ if (!forceEncrypt && !this._secureStoreEncryptOnWrite) return null;
2628
2249
  if (this._secureStoreKey !== null) return this._secureStoreKey;
2629
2250
  if (this._secureStoreRequired) {
2630
2251
  throw new SecureStoreLockedError(
@@ -2752,6 +2373,15 @@ export class StorageManager {
2752
2373
  }
2753
2374
  }
2754
2375
 
2376
+ /** Register the operator's scope-aware cache invalidation gate for a memory
2377
+ * dir (issue #1904). The orchestrator/access layer calls this with
2378
+ * the operator's scopedCacheInvalidationEnabled flag so the setting reaches every
2379
+ * StorageManager built over the dir, including ephemeral recall-sub-stage
2380
+ * instances. Only an explicit `false` disables; `undefined` leaves it on. */
2381
+ static setScopedCacheInvalidationDefault(memoryDir: string, enabled: boolean | undefined): void {
2382
+ StorageManager.scopedCacheInvalidationByDir.set(memoryDir, enabled !== false);
2383
+ }
2384
+
2755
2385
  /**
2756
2386
  * The LIVE class object of this instance (issue #1809 review of the
2757
2387
  * storage decomposition): extracted store modules must read/write the
@@ -3107,15 +2737,21 @@ export class StorageManager {
3107
2737
  private readStorageSecureFile(filePath: string): Promise<string> {
3108
2738
  return readMaybeEncryptedFile(filePath, this._secureStoreKey, this.baseDir);
3109
2739
  }
3110
- private writeStorageSecureFile(filePath: string, content: string | Buffer): Promise<void> {
3111
- return writeMaybeEncryptedFile(filePath, content, this.resolveWriteKey(), {}, this.baseDir).then(() =>
3112
- this.notifyCatalogWriteForPath(filePath)
3113
- );
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
+ });
3114
2749
  }
3115
2750
  private writeStorageSecureFileChunks(filePath: string, chunks: AsyncIterable<Buffer>): Promise<void> {
3116
- return writeMaybeEncryptedFileFromChunks(filePath, chunks, this.resolveWriteKey(), {}, this.baseDir).then(() =>
3117
- this.notifyCatalogWriteForPath(filePath)
3118
- );
2751
+ const writeKey = this.resolveWriteKey();
2752
+ return writeMaybeEncryptedFileFromChunks(filePath, chunks, writeKey, {}, this.baseDir).then(() => {
2753
+ this.notifyCatalogWriteForPath(filePath);
2754
+ });
3119
2755
  }
3120
2756
 
3121
2757
  private assertManagedStoragePath(filePath: string, method: string): string {
@@ -3321,11 +2957,9 @@ export class StorageManager {
3321
2957
  // chokepoint, which also covers the entity cache (issue #1535).
3322
2958
  this.invalidateAllMemoriesCache();
3323
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).
3324
2962
  this.factHashIndexAuthoritative = false;
3325
- await unlink(this.factHashIndexReadyPath).catch((error: unknown) => {
3326
- if (isErrnoCode(error, "ENOENT")) return;
3327
- throw error;
3328
- });
3329
2963
  if (filePath.includes(`${path.sep}cold${path.sep}`)) {
3330
2964
  this.invalidateColdMemoriesCache();
3331
2965
  }
@@ -3340,7 +2974,8 @@ export class StorageManager {
3340
2974
  this.stateDir,
3341
2975
  () => this._secureStoreKey,
3342
2976
  () => this.resolveWriteKey(),
3343
- this.baseDir
2977
+ this.baseDir,
2978
+ this.factHashIndexLockOptions,
3344
2979
  );
3345
2980
  }
3346
2981
 
@@ -3358,12 +2993,60 @@ export class StorageManager {
3358
2993
  }
3359
2994
  }
3360
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
+
3361
3039
  private async appendStorageSecureFileUnlocked(filePath: string, content: string): Promise<void> {
3362
3040
  const writeKey = this.resolveWriteKey();
3363
3041
  await mkdir(path.dirname(filePath), { recursive: true });
3364
3042
  if (writeKey === null) {
3365
3043
  try {
3366
- 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)) {
3367
3050
  const existing = await this.readStorageSecureFile(filePath);
3368
3051
  await writeMaybeEncryptedFile(filePath, `${existing}${content}`, null, {}, this.baseDir);
3369
3052
  this.notifyCatalogWriteForPath(filePath);
@@ -3372,11 +3055,21 @@ export class StorageManager {
3372
3055
  } catch (err) {
3373
3056
  if (!isErrnoCode(err, "ENOENT")) throw err;
3374
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.
3375
3065
  await appendFile(filePath, content, "utf-8");
3376
3066
  this.notifyCatalogWriteForPath(filePath);
3377
3067
  return;
3378
3068
  }
3379
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.
3380
3073
  let existing = "";
3381
3074
  try {
3382
3075
  existing = await this.readStorageSecureFile(filePath);
@@ -3395,10 +3088,6 @@ export class StorageManager {
3395
3088
  private get entitySynthesisQueuePath(): string {
3396
3089
  return path.join(this.stateDir, "entity-synthesis-queue.json");
3397
3090
  }
3398
- private get factHashIndexReadyPath(): string {
3399
- return path.join(this.stateDir, "fact-hashes.ready");
3400
- }
3401
-
3402
3091
  // ── Tombstone store access (issue #1579) ─────────────────────────────────
3403
3092
  /**
3404
3093
  * The on-disk tombstone log path. Lives under `<stateDir>/tombstones.jsonl`
@@ -3596,101 +3285,200 @@ export class StorageManager {
3596
3285
  return this.factHashIndexLoadPromise;
3597
3286
  }
3598
3287
 
3599
- 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> {
3600
3319
  if (this.factHashIndexAuthoritative === true) {
3601
- 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;
3602
3333
  }
3603
3334
  if (this.factHashIndexAuthoritativePromise) {
3604
- await this.factHashIndexAuthoritativePromise;
3605
- return;
3335
+ return this.factHashIndexAuthoritativePromise;
3606
3336
  }
3607
3337
 
3338
+ this.factHashIndexAuthoritative = false;
3608
3339
  this.factHashIndexAuthoritativePromise = (async () => {
3609
- try {
3610
- await access(this.factHashIndexReadyPath);
3611
- this.factHashIndexAuthoritative = true;
3612
- return;
3613
- } catch {
3614
- // Fall through and backfill from the live fact corpus once.
3615
- }
3616
-
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.
3617
3356
  const factHashIndex = await this.getFactHashIndex();
3618
- factHashIndex.clear();
3619
- const existing = await this.readAllMemories();
3620
- let legacyRecovered = 0;
3621
- for (const memory of existing) {
3622
- if (memory.frontmatter.category !== "fact") continue;
3623
- if (inferMemoryStatus(memory.frontmatter, memory.path) !== "active") continue;
3624
- // Prefer the pre-computed raw-content hash stored in frontmatter
3625
- // (written since round 8 of issue #369). This hash was derived from
3626
- // the content BEFORE citation annotation, so it matches what
3627
- // hasFactContentHash(rawFact) would compute.
3628
- if (memory.frontmatter.contentHash) {
3629
- factHashIndex.addByHash(memory.frontmatter.contentHash);
3630
- continue;
3631
- }
3632
- // Legacy fact written before contentHash was introduced (Finding 1 —
3633
- // Uhol). Apply nuanced handling based on whether the citation can be
3634
- // reliably stripped:
3635
- //
3636
- // 1. Default citation present → strip it and index the raw body.
3637
- // 2. No citation at all → index the raw body as-is.
3638
- // 3. Unknown/custom citation template → skip with a warning.
3639
- //
3640
- // Rationale for (3): for facts annotated with a custom citation
3641
- // template, stripCitationForTemplate cannot reliably detect the inline
3642
- // marker and would hash the cited body — producing a hash that never
3643
- // matches what hasFactContentHash(rawContent) computes. A
3644
- // false-negative miss (the fact is not in the index) is preferable to
3645
- // a wrong index entry that permanently suppresses legitimate duplicate
3646
- // writes.
3647
- //
3648
- // Limitation (Thread 2 — stale hash): even when contentHash IS present
3649
- // it may be stale if updateMemory() rewrote the body without updating
3650
- // the frontmatter hash. The hash is trusted as-is here; a future
3651
- // migration pass can recompute it from the current content.
3652
- const content = memory.content;
3653
- // Use the configured template (Thread 1 fix): citationTemplate is set
3654
- // by the orchestrator to the active inlineSourceAttributionFormat so
3655
- // the rebuild can strip both the default and any custom template.
3656
- // Falls back to DEFAULT_CITATION_FORMAT when the orchestrator has not
3657
- // configured a custom template (e.g. direct StorageManager construction
3658
- // in tests).
3659
- const stripped = stripCitationForTemplate(content, this.citationTemplate);
3660
- if (stripped !== content) {
3661
- // Citation was stripped — index the bare body.
3662
- factHashIndex.addByHash(ContentHashIndex.computeHash(sanitizeMemoryContent(stripped).text));
3663
- continue;
3664
- }
3665
- // No citation was removed. Decide whether to index or skip.
3666
- // Thread 4 fix: use hasCitation() rather than the too-broad endsWith("]")
3667
- // heuristic. Facts that legitimately end with "]" (e.g. "User prefers
3668
- // [dark mode]") have no citation marker and should be indexed as-is.
3669
- // Only skip when hasCitation() confirms a citation is present — that
3670
- // means the citation is from an unknown/custom template we cannot strip.
3671
- if (!hasCitation(content)) {
3672
- // Content has no recognisable citation marker — index raw body.
3673
- factHashIndex.addByHash(ContentHashIndex.computeHash(sanitizeMemoryContent(content).text));
3674
- continue;
3675
- }
3676
- // Content carries a citation from an unknown/custom template
3677
- // that we cannot safely strip. Skip rather than index a wrong hash.
3678
- legacyRecovered++;
3679
- 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));
3680
3369
  }
3681
- if (legacyRecovered > 0) {
3682
- log.info(
3683
- `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)`,
3684
3375
  );
3685
3376
  }
3686
- await factHashIndex.save();
3687
- await mkdir(path.dirname(this.factHashIndexReadyPath), { recursive: true });
3688
- await writeFile(this.factHashIndexReadyPath, "v1\n", "utf-8");
3689
- this.factHashIndexAuthoritative = true;
3377
+ return published;
3690
3378
  })().finally(() => {
3691
3379
  this.factHashIndexAuthoritativePromise = null;
3692
3380
  });
3693
- 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;
3694
3482
  }
3695
3483
  private get questionsDir(): string {
3696
3484
  return path.join(this.baseDir, "questions");
@@ -3743,6 +3531,27 @@ export class StorageManager {
3743
3531
  private get behaviorSignalsPath(): string {
3744
3532
  return path.join(this.stateDir, "behavior-signals.jsonl");
3745
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();
3746
3555
  /**
3747
3556
  * Buffer surprise telemetry ledger (issue #563 PR 3).
3748
3557
  *
@@ -3870,71 +3679,7 @@ export class StorageManager {
3870
3679
  async writeMemory(
3871
3680
  category: MemoryCategory,
3872
3681
  content: string,
3873
- options: {
3874
- actor?: string;
3875
- confidence?: number;
3876
- tags?: string[];
3877
- entityRef?: string;
3878
- source?: string;
3879
- supersedes?: string;
3880
- lineage?: string[];
3881
- importance?: ImportanceScore;
3882
- links?: MemoryLink[];
3883
- intentGoal?: string;
3884
- intentActionType?: string;
3885
- intentEntityTypes?: string[];
3886
- artifactType?: MemoryFrontmatter["artifactType"];
3887
- sourceMemoryId?: string;
3888
- sourceTurnId?: string;
3889
- memoryKind?: MemoryFrontmatter["memoryKind"];
3890
- expiresAt?: string;
3891
- validAt?: string;
3892
- // Issue #1578 — bi-temporal ingestion provenance. `observedAt` is the
3893
- // ingestion time (when Remnic learned the fact); `eventTimeSource`
3894
- // records whether `validAt` was resolved from an extracted expression
3895
- // or assumed from the ingestion anchor. Both validate on serialize.
3896
- observedAt?: string;
3897
- eventTimeSource?: "extracted" | "assumed";
3898
- invalidAt?: string;
3899
- structuredAttributes?: Record<string, string>;
3900
- /**
3901
- * When provided, this string is used as the source for the fact-content
3902
- * dedup hash index instead of the persisted body (`content`).
3903
- *
3904
- * Use this when the persisted body differs from the canonical fact text
3905
- * — for example when `content` is a citation-annotated variant of a raw
3906
- * fact. Passing the raw fact as `contentHashSource` ensures that
3907
- * `hasFactContentHash(rawFact)` returns `true` after the write, so
3908
- * subsequent extractions of the same logical fact are correctly deduped
3909
- * even when their citation timestamp differs.
3910
- */
3911
- contentHashSource?: string;
3912
- status?: MemoryStatus;
3913
- /**
3914
- * Consolidation provenance (issue #561 PR 2). When the caller is a
3915
- * consolidation / supersession / dedup-merge path, these fields wire
3916
- * the page-version snapshots the new memory was derived from and the
3917
- * operator that produced it. Persisted onto frontmatter as
3918
- * `derived_from` + `derived_via`; validated at serialize time.
3919
- */
3920
- derivedFrom?: string[];
3921
- derivedVia?: ConsolidationOperator;
3922
- /**
3923
- * Faithfulness gate verdict (issue #1576). When provided, persisted to
3924
- * frontmatter so downstream readers (TrustScore #1577, review queue)
3925
- * can consume it. Absent = gate was off or fact predates #1576.
3926
- */
3927
- faithfulness?: import("./types.js").FaithfulnessFrontmatter;
3928
- /**
3929
- * Claim-level provenance spans (issue #1575 PR 2). When provided,
3930
- * persisted to frontmatter so downstream readers (memory_get, x-ray,
3931
- * faithfulness gate #1576) can consume them. Absent = fact predates
3932
- * #1575 or provenance was disabled.
3933
- */
3934
- sources?: ProvenanceSource[];
3935
- provenance?: "verified" | "unverified" | "none";
3936
- sourceConnector?: string;
3937
- } = {}
3682
+ options: WriteMemoryOptions = {}
3938
3683
  ): Promise<MemoryWriteResult> {
3939
3684
  await this.ensureDirectories();
3940
3685
  const now = new Date();
@@ -4017,17 +3762,10 @@ export class StorageManager {
4017
3762
  fm.sourceConnector = options.sourceConnector;
4018
3763
  }
4019
3764
 
4020
- // Append structured attributes as searchable suffix so QMD indexes them.
4021
- // normalizeAttributePairs sorts and lowercases keys so the enriched content
4022
- // is stable regardless of the insertion order or key casing supplied by the
4023
- // caller this must stay in sync with the dedupContent built in the
4024
- // orchestrator's hash-dedup path.
4025
- let enrichedContent = content;
4026
- if (options.structuredAttributes && Object.keys(options.structuredAttributes).length > 0) {
4027
- enrichedContent = `${content}\n[Attributes: ${normalizeAttributePairs(options.structuredAttributes)}]`;
4028
- }
4029
-
4030
- 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);
4031
3769
  if (!sanitized.clean) {
4032
3770
  log.warn(`memory content sanitized for ${id}; violations=${sanitized.violations.join(", ")}`);
4033
3771
  }
@@ -4110,7 +3848,8 @@ export class StorageManager {
4110
3848
 
4111
3849
  await this.snapshotBeforeWrite(filePath, "write");
4112
3850
  await this.writeStorageSecureFile(filePath, fileContent);
4113
- await this.patchHotMemoriesCache({ addedPath: filePath });
3851
+ await this.patchHotMemoriesCache({ addedPath: filePath }, "memory-create");
3852
+ this.notifyMemoryWrite(filePath);
4114
3853
  await this.appendGeneratedMemoryLifecycleEventFailOpen("storage.writeMemory", {
4115
3854
  memoryId: id,
4116
3855
  eventType: "created",
@@ -4133,13 +3872,31 @@ export class StorageManager {
4133
3872
  // fact text before citation annotation), index THAT string so that
4134
3873
  // hasFactContentHash(rawFact) returns true on subsequent extractions.
4135
3874
  // Otherwise fall back to the sanitized persisted body as before.
4136
- if (options.contentHashSource !== undefined && options.contentHashSource.length > 0) {
4137
- const hashSourceSanitized = sanitizeMemoryContent(options.contentHashSource);
4138
- factHashIndex.add(hashSourceSanitized.text);
4139
- } else {
4140
- 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();
4141
3899
  }
4142
- await factHashIndex.save();
4143
3900
  } catch (err) {
4144
3901
  log.warn(`storage.writeMemory completed but failed to update fact hash index: ${err}`);
4145
3902
  }
@@ -4148,11 +3905,73 @@ export class StorageManager {
4148
3905
  return { id, tombstoneBlocked, ...(fm.blockedBy ? { blockedBy: fm.blockedBy } : {}) };
4149
3906
  }
4150
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
+
4151
3940
  async hasFactContentHash(content: string): Promise<boolean> {
4152
- await this.ensureFactHashIndexAuthoritative();
4153
- const factHashIndex = await this.getFactHashIndex();
3941
+ const authoritative = await this.ensureFactHashIndexAuthoritative();
4154
3942
  const sanitized = sanitizeMemoryContent(content);
4155
- 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;
4156
3975
  }
4157
3976
 
4158
3977
  private factContentHashForRemoval(memory: MemoryFile): string | null {
@@ -4177,7 +3996,22 @@ export class StorageManager {
4177
3996
  await this.ensureFactHashIndexAuthoritative();
4178
3997
  const factHashIndex = await this.getFactHashIndex();
4179
3998
  factHashIndex.addByHash(hash);
4180
- 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();
4181
4015
  }
4182
4016
 
4183
4017
  /**
@@ -4246,14 +4080,38 @@ export class StorageManager {
4246
4080
  for (const hash of removedHashes.values()) {
4247
4081
  if (!remainingActiveHashes.has(hash)) {
4248
4082
  factHashIndex.removeByHash(hash);
4083
+ this.factOnlyHashes.delete(hash);
4249
4084
  }
4250
4085
  }
4251
- 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);
4252
4111
  }
4253
4112
 
4254
4113
  async isFactContentHashAuthoritative(): Promise<boolean> {
4255
- await this.ensureFactHashIndexAuthoritative();
4256
- return true;
4114
+ return await this.ensureFactHashIndexAuthoritative();
4257
4115
  }
4258
4116
 
4259
4117
  async writeArtifact(
@@ -4559,6 +4417,10 @@ export class StorageManager {
4559
4417
  StorageManager.hotMemoriesCacheDefault = true;
4560
4418
  StorageManager.hotMemoriesCacheTtlMs = 60_000;
4561
4419
  StorageManager.hotMemoriesCacheProcessDefaultSeeded = false;
4420
+ // Reset the #1904 scope-invalidation gate so a legacy-mode (=false) test
4421
+ // dir cannot leak its setting into a later test constructing over a reused
4422
+ // temp path.
4423
+ StorageManager.scopedCacheInvalidationByDir.clear();
4562
4424
  }
4563
4425
 
4564
4426
  /** Cancel any in-flight concurrent read so the next readAllMemories()
@@ -4579,13 +4441,21 @@ export class StorageManager {
4579
4441
  // corpus sentinel too so PEER processes rescan instead of serving a warm
4580
4442
  // pre-mutation corpus entry (issue #1902 cross-process coherence).
4581
4443
  this.bumpMemoryCorpusVersion();
4582
- // Invalidation chokepoint (issue #1535): eagerly evict EVERY process-level
4583
- // cache layer that can serve memory-derived content for this dir hot,
4584
- // archive, entity, derived episode/rule views, and both QMD result caches
4585
- // (qmdSearchCache and qmdRecallCache). Before this call was added, the QMD
4586
- // recall cache was never invalidated on mutations, so recall served
4587
- // pre-edit bundles for the remainder of its fresh/stale TTL window.
4588
- invalidateAllForDir(this.baseDir);
4444
+ // Invalidation chokepoint (issue #1535 / #1904): evict the layers a
4445
+ // memory-mutate can affect hot, archive, derived episode/rule views, and
4446
+ // both QMD result caches (qmdSearchCache and qmdRecallCache). Before the QMD
4447
+ // caches were invalidated on mutations, recall served pre-edit bundles for
4448
+ // the remainder of its fresh/stale TTL window. The `entities` layer is NOT
4449
+ // in memory-mutate scope: these funnel callers (update/frontmatter/move/
4450
+ // archive/invalidate/bulk/offline-sync) never rewrite entity files, and the
4451
+ // ones that touch entity-derived state additionally bump the status version
4452
+ // (which does the full clear). scopedCacheInvalidationEnabled=false restores
4453
+ // the pre-#1904 full clear of every layer (rollback lever).
4454
+ if (StorageManager.scopedCacheInvalidationByDir.get(this.baseDir) ?? true) {
4455
+ invalidateForScope(this.baseDir, "memory-mutate");
4456
+ } else {
4457
+ invalidateAllForDir(this.baseDir);
4458
+ }
4589
4459
  // Issue #1579 — tombstone store is a cache layer too (rule 25: clear ALL
4590
4460
  // cache layers). A tombstone append from another path (supersession /
4591
4461
  // correction) must be visible to the next writeMemory lookup on this
@@ -4614,7 +4484,10 @@ export class StorageManager {
4614
4484
  * coherence is preserved. Bulk/ambiguous mutations keep the wholesale
4615
4485
  * invalidateAllMemoriesCache() drop instead (issue #1902 Step 5).
4616
4486
  */
4617
- private async patchHotMemoriesCache(opts: { addedPath?: string; removedPath?: string }): Promise<void> {
4487
+ private async patchHotMemoriesCache(
4488
+ opts: { addedPath?: string; removedPath?: string },
4489
+ scope: "memory-create" | "memory-mutate" = "memory-mutate",
4490
+ ): Promise<void> {
4618
4491
  const prevVersion = this.getMemoryCorpusVersion();
4619
4492
  // Snapshot the key identity once (issue #1902, Codex Medium): a mid-await
4620
4493
  // setSecureStoreKey change would otherwise let the post-await patch/re-key
@@ -4684,9 +4557,19 @@ export class StorageManager {
4684
4557
  const patched = getCachedMemories(this.baseDir, prevVersion, keyId);
4685
4558
  if (patched) setCachedMemories(this.baseDir, patched, produced, keyId, this.hotCacheTtlMs());
4686
4559
  }
4687
- // Evict every OTHER layer; the hot layer is patched (or intentionally left
4688
- // to be version-rejected) above, not dropped here.
4689
- invalidateDerivedAndGlobalForDir(this.baseDir);
4560
+ // Evict the non-hot layers; the hot layer is patched (or intentionally left
4561
+ // to be version-rejected) above, not dropped here. Scope-aware (issue #1904):
4562
+ // a `memory-create` evicts ONLY the derived episode/rule views, keeping the
4563
+ // global QMD result caches and the version-keyed entity cache warm (a create
4564
+ // adds a doc that is not yet in the QMD index and changes no existing doc's
4565
+ // recall visibility). A `memory-mutate` (or scoped-off rollback) still evicts
4566
+ // the derived + global QMD views so a superseded/edited memory cannot resurface
4567
+ // from a stale recall bundle (issue #1535 correctness contract).
4568
+ if (scope === "memory-create" && (StorageManager.scopedCacheInvalidationByDir.get(this.baseDir) ?? true)) {
4569
+ invalidateForScopeExceptHot(this.baseDir, "memory-create");
4570
+ } else {
4571
+ invalidateDerivedAndGlobalForDir(this.baseDir);
4572
+ }
4690
4573
  if (this.tombstoneStore) {
4691
4574
  this.tombstoneStore.invalidate();
4692
4575
  this.tombstoneStore = null;
@@ -5421,20 +5304,77 @@ export class StorageManager {
5421
5304
  async appendMemoryLifecycleEvents(events: MemoryLifecycleEvent[]): Promise<number> {
5422
5305
  if (events.length === 0) return 0;
5423
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
+ }
5424
5317
 
5425
- const nowIso = new Date().toISOString();
5426
- const payload = events
5427
- .map((event) => {
5428
- const normalized: MemoryLifecycleEvent = {
5429
- ...event,
5430
- timestamp: event.timestamp && event.timestamp.length > 0 ? event.timestamp : nowIso,
5431
- };
5432
- return `${JSON.stringify(normalized)}\n`;
5433
- })
5434
- .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
+ }
5435
5328
 
5436
- await this.appendStorageSecureFile(this.memoryLifecycleLedgerPath, payload);
5437
- 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);
5438
5378
  }
5439
5379
 
5440
5380
  async appendBufferSurpriseEvents(events: BufferSurpriseEvent[]): Promise<number> {
@@ -5446,46 +5386,113 @@ export class StorageManager {
5446
5386
  }
5447
5387
 
5448
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> {
5449
5401
  if (events.length === 0) return 0;
5450
5402
  await this.ensureDirectories();
5451
5403
 
5452
- let existingKeys = new Set<string>();
5404
+ let existingKeys: Set<string>;
5405
+ let identity: { size: number; mtimeMs: number } | null = null;
5453
5406
  try {
5454
- const raw = await this.readStorageSecureFile(this.behaviorSignalsPath);
5455
- const lines = raw.split("\n");
5456
- for (const line of lines) {
5457
- const row = line.trim();
5458
- if (!row) continue;
5459
- try {
5460
- const parsed = JSON.parse(row) as Partial<BehaviorSignalEvent>;
5461
- if (typeof parsed.memoryId === "string" && typeof parsed.signalHash === "string") {
5462
- 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).
5463
5432
  }
5464
- } catch {
5465
- // Ignore malformed rows (fail-open).
5466
5433
  }
5467
5434
  }
5468
5435
  } catch (err) {
5469
5436
  if (err instanceof SecureStoreLockedError) throw err;
5470
5437
  if (!isErrnoCode(err, "ENOENT")) throw err;
5471
5438
  existingKeys = new Set<string>();
5439
+ identity = null;
5472
5440
  }
5473
5441
 
5474
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>();
5475
5449
  const deduped: BehaviorSignalEvent[] = [];
5476
5450
  for (const event of events) {
5477
5451
  const key = `${event.memoryId}:${event.signalHash}`;
5478
- if (existingKeys.has(key)) continue;
5479
- existingKeys.add(key);
5452
+ if (existingKeys.has(key) || pending.has(key)) continue;
5453
+ pending.add(key);
5480
5454
  deduped.push({
5481
5455
  ...event,
5482
5456
  timestamp: event.timestamp && event.timestamp.length > 0 ? event.timestamp : nowIso,
5483
5457
  });
5484
5458
  }
5485
5459
 
5486
- 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
+ }
5487
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.
5488
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
+ }
5489
5496
  return deduped.length;
5490
5497
  }
5491
5498
 
@@ -5602,42 +5609,30 @@ export class StorageManager {
5602
5609
  }
5603
5610
 
5604
5611
  async readAllMemoryLifecycleEvents(): Promise<MemoryLifecycleEvent[]> {
5605
- try {
5606
- const out: MemoryLifecycleEvent[] = [];
5607
- for await (const line of readMaybeEncryptedLines(this.memoryLifecycleLedgerPath, () =>
5608
- this.readStorageSecureFile(this.memoryLifecycleLedgerPath)
5609
- )) {
5610
- const row = line.trim();
5611
- if (!row) continue;
5612
- try {
5613
- const parsed = JSON.parse(row) as Partial<MemoryLifecycleEvent>;
5614
- if (
5615
- typeof parsed.eventId === "string" &&
5616
- typeof parsed.memoryId === "string" &&
5617
- typeof parsed.eventType === "string" &&
5618
- typeof parsed.timestamp === "string" &&
5619
- typeof parsed.actor === "string" &&
5620
- typeof parsed.ruleVersion === "string"
5621
- ) {
5622
- out.push(parsed as MemoryLifecycleEvent);
5623
- }
5624
- } catch {
5625
- // Ignore malformed rows (fail-open).
5626
- }
5627
- }
5628
- return sortMemoryLifecycleEvents(out);
5629
- } catch (err) {
5630
- if (err instanceof SecureStoreLockedError) throw err;
5631
- if (!isErrnoCode(err, "ENOENT")) throw err;
5632
- 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
+ );
5633
5626
  }
5627
+ return readAllLifecycleEventsFromLedgerBuffer(
5628
+ this.memoryLifecycleLedgerPath,
5629
+ (p) => readMaybeEncryptedFileBuffer(p, this._secureStoreKey, this.baseDir),
5630
+ );
5634
5631
  }
5635
5632
 
5636
5633
  async readMemoryLifecycleEvents(limit: number = 200): Promise<MemoryLifecycleEvent[]> {
5637
- const cappedLimit = Math.max(0, Math.floor(limit));
5638
- if (cappedLimit === 0) return [];
5639
- const events = await this.readAllMemoryLifecycleEvents();
5640
- return events.slice(-cappedLimit);
5634
+ return readBoundedLifecycleEventsFromLedger(
5635
+ this.memoryLifecycleLedgerPath, (p) => this.readStorageSecureFile(p), limit);
5641
5636
  }
5642
5637
 
5643
5638
  async writeCompressionGuidelines(content: string): Promise<void> {
@@ -6365,6 +6360,7 @@ export class StorageManager {
6365
6360
 
6366
6361
  const memories = await this.readAllMemories();
6367
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]));
6368
6364
  let updated = 0;
6369
6365
  // Capture the corpus version + warmth BEFORE writing so we can patch the hot
6370
6366
  // entries in place and then re-key them to the version this flush produces
@@ -6384,7 +6380,9 @@ export class StorageManager {
6384
6380
  const appliedPatches = new Map<string, { accessCount: number; lastAccessed: string }>();
6385
6381
 
6386
6382
  for (const entry of entries) {
6387
- const memory = memoryMap.get(entry.memoryId);
6383
+ const memory = entry.memoryPath
6384
+ ? memoryPathMap.get(path.resolve(entry.memoryPath))
6385
+ : memoryMap.get(entry.memoryId);
6388
6386
  if (!memory) continue;
6389
6387
 
6390
6388
  const newFm: MemoryFrontmatter = {
@@ -6403,7 +6401,10 @@ export class StorageManager {
6403
6401
  if (warm) {
6404
6402
  updateCacheOnWrite(this.baseDir, { ...memory, frontmatter: newFm }, keyId);
6405
6403
  }
6406
- 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
+ });
6407
6408
  updated++;
6408
6409
  } catch (err) {
6409
6410
  log.debug(`failed to update access tracking for ${entry.memoryId}: ${err}`);
@@ -6436,7 +6437,7 @@ export class StorageManager {
6436
6437
  // re-key robust to that race; the publish guard blocks any scan
6437
6438
  // finishing AFTER our bump, so this is the only window.
6438
6439
  const reapplied = cur.map((m) => {
6439
- const patch = appliedPatches.get(m.frontmatter.id);
6440
+ const patch = appliedPatches.get(path.resolve(m.path));
6440
6441
  return patch
6441
6442
  ? { ...m, frontmatter: { ...m.frontmatter, accessCount: patch.accessCount, lastAccessed: patch.lastAccessed } }
6442
6443
  : m;
@@ -6464,28 +6465,68 @@ export class StorageManager {
6464
6465
  }
6465
6466
 
6466
6467
  /**
6467
- * Check which of the given memory IDs actually exist on disk.
6468
+ * Resolve existing active memory IDs to their on-disk paths.
6468
6469
  *
6469
6470
  * Uses a lightweight directory scan (collectActiveMemoryPaths) that reads
6470
6471
  * file names without parsing frontmatter — much cheaper than readAllMemories()
6471
- * for simple existence checks like citation usage tracking.
6472
- *
6473
- * Returns the subset of `ids` that correspond to real memory files.
6472
+ * for citation usage tracking and other existence checks.
6474
6473
  */
6475
- async filterExistingMemoryIds(ids: string[]): Promise<Set<string>> {
6476
- 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();
6477
6479
  const wantedIds = new Set(ids);
6478
6480
  const filePaths = await this.collectActiveMemoryPaths();
6479
- const foundIds = new Set<string>();
6481
+ const pathsById = new Map<string, string[]>();
6482
+ const filePathsById = new Map<string, string[]>();
6480
6483
  for (const filePath of filePaths) {
6481
- const basename = path.basename(filePath, ".md");
6482
- if (wantedIds.has(basename)) {
6483
- foundIds.add(basename);
6484
- // Short-circuit once all requested IDs are found.
6485
- 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);
6486
6520
  }
6487
6521
  }
6488
- 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());
6489
6530
  }
6490
6531
 
6491
6532
  async getProjectedMemoryState(id: string): Promise<MemoryProjectionCurrentState | null> {
@@ -6544,8 +6585,8 @@ export class StorageManager {
6544
6585
  const projected = readProjectedMemoryTimeline(this.baseDir, memoryId, cappedLimit);
6545
6586
  if (projected && projected.length > 0) return projected;
6546
6587
  warnProjectionFallback(this.baseDir, "getMemoryTimeline");
6547
- const events = await this.readAllMemoryLifecycleEvents();
6548
- return events.filter((event) => event.memoryId === memoryId).slice(-cappedLimit);
6588
+ return readBoundedLifecycleEventsFromLedger(
6589
+ this.memoryLifecycleLedgerPath, (p) => this.readStorageSecureFile(p), cappedLimit, memoryId);
6549
6590
  }
6550
6591
 
6551
6592
  // ---------------------------------------------------------------------------
@@ -6648,7 +6689,7 @@ export class StorageManager {
6648
6689
  await this.writeStorageSecureFile(filePath, fileContent);
6649
6690
  // Keep the version-keyed hot-memories cache coherent with the new chunk
6650
6691
  // file (issue #1902) — same single-file patch path writeMemory uses.
6651
- await this.patchHotMemoriesCache({ addedPath: filePath });
6692
+ await this.patchHotMemoriesCache({ addedPath: filePath }, "memory-create");
6652
6693
  log.debug(`wrote chunk ${id} (${chunkIndex + 1}/${chunkTotal}) to ${filePath}`);
6653
6694
  return id;
6654
6695
  }
@@ -6736,13 +6777,11 @@ export class StorageManager {
6736
6777
  }
6737
6778
  }
6738
6779
 
6739
- // Also write a correction entry for the audit trail
6740
- await this.writeMemory("correction", `Superseded: ${oldMemory.content}\n\nReason: ${reason}`, {
6741
- confidence: 1.0,
6742
- tags: ["supersession", "auto-resolved"],
6743
- source: "contradiction-detection",
6744
- lineage: [oldMemoryId, newMemoryId],
6745
- });
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] });
6746
6785
 
6747
6786
  return true;
6748
6787
  } catch (err) {