@remnic/core 9.7.15 → 9.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (929) hide show
  1. package/dist/access-admin-ops-surface.d.ts +13 -6
  2. package/dist/access-admin-ops-surface.js +85 -71
  3. package/dist/access-audit.js +2 -2
  4. package/dist/access-boundary.d.ts +35 -17
  5. package/dist/access-boundary.js +86 -74
  6. package/dist/access-citation.d.ts +28 -0
  7. package/dist/access-citation.js +9 -0
  8. package/dist/access-cli.js +194 -178
  9. package/dist/access-cli.js.map +1 -1
  10. package/dist/access-errors.d.ts +16 -1
  11. package/dist/access-errors.js +7 -3
  12. package/dist/access-http.d.ts +25 -19
  13. package/dist/access-http.js +100 -87
  14. package/dist/access-identity-continuity-surface.d.ts +13 -8
  15. package/dist/access-identity-continuity-surface.js +85 -71
  16. package/dist/access-lcm-surface.d.ts +16 -9
  17. package/dist/access-lcm-surface.js +85 -71
  18. package/dist/access-mcp.d.ts +23 -16
  19. package/dist/access-mcp.js +96 -84
  20. package/dist/access-memory-search-fanout.d.ts +93 -0
  21. package/dist/access-memory-search-fanout.js +18 -0
  22. package/dist/access-observe-write-surface.d.ts +26 -8
  23. package/dist/access-observe-write-surface.js +85 -71
  24. package/dist/access-operations-batch.js +92 -80
  25. package/dist/access-operations.d.ts +29 -22
  26. package/dist/access-operations.js +94 -82
  27. package/dist/access-recall-concurrency.d.ts +250 -0
  28. package/dist/access-recall-concurrency.js +157 -0
  29. package/dist/access-recall-response.d.ts +130 -0
  30. package/dist/access-recall-response.js +147 -0
  31. package/dist/access-recall-surface.d.ts +15 -7
  32. package/dist/access-recall-surface.js +85 -71
  33. package/dist/access-schema.d.ts +98 -98
  34. package/dist/access-schema.js +8 -6
  35. package/dist/{access-service-DpcYZ9Nl.d.ts → access-service-DbZ1G3zV.d.ts} +29 -36
  36. package/dist/access-service.d.ts +13 -6
  37. package/dist/access-service.js +89 -72
  38. package/dist/access-surface-catalog.d.ts +23 -16
  39. package/dist/access-surface-catalog.js +1 -0
  40. package/dist/access-surface-catalog.js.map +1 -1
  41. package/dist/access-token-capabilities.d.ts +2 -1
  42. package/dist/access-token-capabilities.js +4 -2
  43. package/dist/action-confidence.d.ts +3 -1
  44. package/dist/active-memory-bridge.d.ts +3 -1
  45. package/dist/active-memory-bridge.js +4 -4
  46. package/dist/active-recall.d.ts +4 -1
  47. package/dist/active-recall.js +23 -13
  48. package/dist/active-recall.js.map +1 -1
  49. package/dist/adapters/index.js +4 -4
  50. package/dist/adapters/registry.js +2 -2
  51. package/dist/{auto-sync-AAMTVZMK.js → auto-sync-UZG3XQZ5.js} +14 -8
  52. package/dist/{auto-sync-AAMTVZMK.js.map → auto-sync-UZG3XQZ5.js.map} +1 -1
  53. package/dist/behavior-learner.d.ts +3 -1
  54. package/dist/behavior-signals.d.ts +3 -1
  55. package/dist/bootstrap.d.ts +12 -7
  56. package/dist/bootstrap.js +2 -2
  57. package/dist/bounded-jsonl-state.d.ts +54 -0
  58. package/dist/bounded-jsonl-state.js +10 -0
  59. package/dist/boxes.js +2 -2
  60. package/dist/briefing.d.ts +5 -2
  61. package/dist/briefing.js +34 -24
  62. package/dist/buffer-surprise-report.d.ts +3 -1
  63. package/dist/buffer.d.ts +60 -2
  64. package/dist/buffer.js +3 -3
  65. package/dist/bulk-import/index.d.ts +5 -3
  66. package/dist/calibration.d.ts +3 -1
  67. package/dist/calibration.js +6 -6
  68. package/dist/capabilities.d.ts +7 -7
  69. package/dist/capabilities.js +1 -1
  70. package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-YO5QJ6L3.js} +3 -3
  71. package/dist/capsule-crypto-YO5QJ6L3.js.map +1 -0
  72. package/dist/{catalog-C9444P10.d.ts → catalog-BWqHb5HT.d.ts} +1 -1
  73. package/dist/causal-behavior.d.ts +3 -1
  74. package/dist/causal-behavior.js +6 -6
  75. package/dist/causal-chain.js +4 -4
  76. package/dist/causal-consolidation.d.ts +3 -1
  77. package/dist/causal-consolidation.js +43 -34
  78. package/dist/causal-consolidation.js.map +1 -1
  79. package/dist/causal-retrieval.js +6 -6
  80. package/dist/causal-trajectory-graph.d.ts +3 -1
  81. package/dist/causal-trajectory-graph.js +1 -1
  82. package/dist/causal-trajectory.js +1 -1
  83. package/dist/{chunk-A3GOMRJG.js → chunk-23L3IXFD.js} +2 -2
  84. package/dist/chunk-2CGLBUW3.js +15 -0
  85. package/dist/chunk-2CGLBUW3.js.map +1 -0
  86. package/dist/{chunk-MRHFJV44.js → chunk-2DQ2XREU.js} +2 -2
  87. package/dist/{chunk-L2B3EE7B.js → chunk-2QMKMVHH.js} +2 -2
  88. package/dist/{chunk-VJVNLNDQ.js → chunk-2UFO7X4Q.js} +7 -7
  89. package/dist/{chunk-2MR3MFQB.js → chunk-2VEOKUPP.js} +3 -3
  90. package/dist/chunk-2Z24E5W2.js +22 -0
  91. package/dist/chunk-2Z24E5W2.js.map +1 -0
  92. package/dist/{chunk-RHOZ5LLO.js → chunk-347WD2LE.js} +2 -2
  93. package/dist/{chunk-HDTQVNWD.js → chunk-3QAH2S33.js} +3 -3
  94. package/dist/{chunk-37CTGERM.js → chunk-3WCASNQZ.js} +12 -1
  95. package/dist/chunk-3WCASNQZ.js.map +1 -0
  96. package/dist/{chunk-LRNMRXJO.js → chunk-3Y224NYC.js} +3 -3
  97. package/dist/{chunk-3IY64MV6.js → chunk-44XZ75OQ.js} +44 -10
  98. package/dist/chunk-44XZ75OQ.js.map +1 -0
  99. package/dist/{chunk-VFSP73LP.js → chunk-45GF7Y3V.js} +2 -2
  100. package/dist/{chunk-K7LOWSLU.js → chunk-4BJQODZF.js} +2479 -1520
  101. package/dist/chunk-4BJQODZF.js.map +1 -0
  102. package/dist/{chunk-JREB6UFH.js → chunk-57OWKFUQ.js} +17 -6
  103. package/dist/chunk-57OWKFUQ.js.map +1 -0
  104. package/dist/{chunk-OEHCMALU.js → chunk-5I4M26Q7.js} +5 -5
  105. package/dist/{chunk-2AZXIZEN.js → chunk-5Q7EVV4X.js} +6 -6
  106. package/dist/{chunk-LC7OODWZ.js → chunk-5SB7SC2G.js} +80 -49
  107. package/dist/chunk-5SB7SC2G.js.map +1 -0
  108. package/dist/{chunk-PUBTHW5O.js → chunk-6DRPPQEJ.js} +2 -2
  109. package/dist/{chunk-NWTGD4AN.js → chunk-6KIQT7RG.js} +10 -9
  110. package/dist/chunk-6KIQT7RG.js.map +1 -0
  111. package/dist/{chunk-FDCTQWLW.js → chunk-6O22TZSB.js} +3 -3
  112. package/dist/{chunk-ZQ7UBUZ6.js → chunk-6QYST5XN.js} +6 -6
  113. package/dist/{chunk-W4PGTAS2.js → chunk-6WAFYNVT.js} +3 -3
  114. package/dist/chunk-6ZMAMSHS.js +20 -0
  115. package/dist/chunk-6ZMAMSHS.js.map +1 -0
  116. package/dist/{chunk-R5VAVVHP.js → chunk-72A4L5JJ.js} +2 -2
  117. package/dist/{chunk-X64YLC24.js → chunk-72VAXGEI.js} +5 -13
  118. package/dist/chunk-72VAXGEI.js.map +1 -0
  119. package/dist/{chunk-IKQTFQMI.js → chunk-755Q3DGF.js} +22 -3
  120. package/dist/chunk-755Q3DGF.js.map +1 -0
  121. package/dist/{chunk-D7FOKIY5.js → chunk-7AMFUATK.js} +12 -9
  122. package/dist/{chunk-D7FOKIY5.js.map → chunk-7AMFUATK.js.map} +1 -1
  123. package/dist/{chunk-BFVEEB7W.js → chunk-7ON2JW73.js} +57 -22
  124. package/dist/chunk-7ON2JW73.js.map +1 -0
  125. package/dist/chunk-7RNBVZZO.js +270 -0
  126. package/dist/chunk-7RNBVZZO.js.map +1 -0
  127. package/dist/{chunk-3FAMU5TX.js → chunk-A6USBZTO.js} +2 -2
  128. package/dist/{chunk-ZHIVCODT.js → chunk-A72SWTWW.js} +7 -7
  129. package/dist/{chunk-FC77VUX5.js → chunk-A7Q43EUZ.js} +2 -2
  130. package/dist/{chunk-UJBESW6X.js → chunk-AE7WMV4V.js} +7 -7
  131. package/dist/{chunk-J2HSAU72.js → chunk-AIK5W3IW.js} +29 -1
  132. package/dist/chunk-AIK5W3IW.js.map +1 -0
  133. package/dist/{chunk-A5UXZRRM.js → chunk-AJ7QCSIS.js} +2 -2
  134. package/dist/{chunk-FNMFJWBY.js → chunk-ANEUPF6O.js} +5 -5
  135. package/dist/chunk-AREGMX2J.js +50 -0
  136. package/dist/chunk-AREGMX2J.js.map +1 -0
  137. package/dist/chunk-AVPXXV6J.js +448 -0
  138. package/dist/chunk-AVPXXV6J.js.map +1 -0
  139. package/dist/{chunk-2A3OXAP2.js → chunk-B7RWBZXW.js} +2 -2
  140. package/dist/{chunk-QKQYXZVS.js → chunk-BI2OJF6J.js} +1819 -1481
  141. package/dist/chunk-BI2OJF6J.js.map +1 -0
  142. package/dist/{chunk-GWTFLF2P.js → chunk-BJMBJZ2Y.js} +59 -59
  143. package/dist/chunk-BJMBJZ2Y.js.map +1 -0
  144. package/dist/{chunk-74F5STS4.js → chunk-BLAKS3CP.js} +4 -4
  145. package/dist/{chunk-M7VGG3ID.js → chunk-BV5WMSIJ.js} +9 -9
  146. package/dist/{chunk-2BFLDH5F.js → chunk-BXLOS5AJ.js} +2 -2
  147. package/dist/{chunk-UXKMMNJD.js → chunk-C23BYTJV.js} +6 -6
  148. package/dist/{chunk-JZHHREBL.js → chunk-C3AKM55W.js} +2 -2
  149. package/dist/{chunk-RP7YXA67.js → chunk-C5JF6EZ7.js} +2 -2
  150. package/dist/{chunk-T7Y5KPLN.js → chunk-C6XBRYVD.js} +16 -15
  151. package/dist/chunk-C6XBRYVD.js.map +1 -0
  152. package/dist/{chunk-FTYDWKO6.js → chunk-CEN6DW6X.js} +12 -2
  153. package/dist/chunk-CEN6DW6X.js.map +1 -0
  154. package/dist/chunk-CGASUJVP.js +421 -0
  155. package/dist/chunk-CGASUJVP.js.map +1 -0
  156. package/dist/{chunk-EHCONI6E.js → chunk-CQMEM5CC.js} +2 -2
  157. package/dist/{chunk-PBNZVPFS.js → chunk-CXZQOVIE.js} +12 -12
  158. package/dist/chunk-D3V3UMET.js +44 -0
  159. package/dist/chunk-D3V3UMET.js.map +1 -0
  160. package/dist/{chunk-7N2RUBAP.js → chunk-DDA47TES.js} +2 -2
  161. package/dist/{chunk-64SE4X5G.js → chunk-DEBHXHJR.js} +3 -3
  162. package/dist/{chunk-EHK6KZHK.js → chunk-DNHVPZOD.js} +2 -2
  163. package/dist/{chunk-437S3G37.js → chunk-DQEMWVMT.js} +2 -2
  164. package/dist/{chunk-LZZNTPLR.js → chunk-DX3XRGLH.js} +2 -2
  165. package/dist/chunk-EA2H5LML.js +72 -0
  166. package/dist/chunk-EA2H5LML.js.map +1 -0
  167. package/dist/{chunk-INELTPRG.js → chunk-EEC7UJPX.js} +103 -106
  168. package/dist/chunk-EEC7UJPX.js.map +1 -0
  169. package/dist/{chunk-KSNVDPDF.js → chunk-EKBNDKDY.js} +21 -20
  170. package/dist/chunk-EKBNDKDY.js.map +1 -0
  171. package/dist/{chunk-YF6FMLBT.js → chunk-EQWEIIKQ.js} +64 -34
  172. package/dist/chunk-EQWEIIKQ.js.map +1 -0
  173. package/dist/{chunk-DZKHSDOM.js → chunk-ES36HTSC.js} +4 -4
  174. package/dist/{chunk-TN7THW5U.js → chunk-ET356KSB.js} +2 -2
  175. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  176. package/dist/{chunk-DMSDUGDM.js → chunk-F7GAIWFA.js} +20 -20
  177. package/dist/{chunk-G6XXLKC5.js → chunk-FFPJINJE.js} +6 -6
  178. package/dist/chunk-FMLKOL2Z.js +636 -0
  179. package/dist/chunk-FMLKOL2Z.js.map +1 -0
  180. package/dist/chunk-FQC3DHH2.js +20 -0
  181. package/dist/chunk-FQC3DHH2.js.map +1 -0
  182. package/dist/{chunk-G4CN3ZXL.js → chunk-FUTF72MJ.js} +15 -15
  183. package/dist/chunk-GCASGYIO.js +71 -0
  184. package/dist/chunk-GCASGYIO.js.map +1 -0
  185. package/dist/{chunk-HF77M34J.js → chunk-GFPZS3F5.js} +8 -8
  186. package/dist/{chunk-BCDKBDSY.js → chunk-GNVZQ5JN.js} +99 -41
  187. package/dist/chunk-GNVZQ5JN.js.map +1 -0
  188. package/dist/{chunk-G6NKALRN.js → chunk-H4WG3UQZ.js} +2 -2
  189. package/dist/{chunk-UDQUSU27.js → chunk-HNZKVLAN.js} +2 -2
  190. package/dist/{chunk-5EZXHU36.js → chunk-HYQJQGD3.js} +2 -2
  191. package/dist/{chunk-PLQE6MJA.js → chunk-IFH6LWTO.js} +50 -8
  192. package/dist/chunk-IFH6LWTO.js.map +1 -0
  193. package/dist/{chunk-RULE4VG5.js → chunk-ILGC6JH6.js} +21 -6
  194. package/dist/chunk-ILGC6JH6.js.map +1 -0
  195. package/dist/chunk-IUM3VSHI.js +113 -0
  196. package/dist/chunk-IUM3VSHI.js.map +1 -0
  197. package/dist/{chunk-TRWPCR6P.js → chunk-JECBQBEX.js} +3 -3
  198. package/dist/{chunk-E6CBU7XP.js → chunk-JYIFGGEM.js} +2 -2
  199. package/dist/{chunk-VA4WSW54.js → chunk-JYIJNNN3.js} +3 -3
  200. package/dist/{chunk-L5HJQC36.js → chunk-K2TDK7GG.js} +3 -3
  201. package/dist/{chunk-KDXVOFYY.js → chunk-KAVBJSBQ.js} +4 -4
  202. package/dist/{chunk-BS4SGLWV.js → chunk-KF5EUVKQ.js} +16 -6
  203. package/dist/chunk-KF5EUVKQ.js.map +1 -0
  204. package/dist/chunk-KFZY2OFH.js +42 -0
  205. package/dist/chunk-KFZY2OFH.js.map +1 -0
  206. package/dist/{chunk-4OKLXWDZ.js → chunk-KNB7IBBU.js} +2 -2
  207. package/dist/{chunk-QLULYKWZ.js → chunk-KSXJOC4F.js} +2 -2
  208. package/dist/{chunk-2S6QHZGJ.js → chunk-L2DPF4DF.js} +1 -1
  209. package/dist/{chunk-JI5AVXYO.js → chunk-LEPOP46W.js} +6 -6
  210. package/dist/{chunk-O7BBFJLP.js → chunk-LTAVSOZW.js} +2 -2
  211. package/dist/{chunk-E3VODCC3.js → chunk-LUDXM7MD.js} +14 -2
  212. package/dist/{chunk-E3VODCC3.js.map → chunk-LUDXM7MD.js.map} +1 -1
  213. package/dist/{chunk-2IZAGAJR.js → chunk-LULR4IUW.js} +3 -3
  214. package/dist/{chunk-QQGPUKMT.js → chunk-MC5Q3J7S.js} +398 -7
  215. package/dist/chunk-MC5Q3J7S.js.map +1 -0
  216. package/dist/{chunk-WMWVO45V.js → chunk-MC6XO7T4.js} +2 -2
  217. package/dist/chunk-MKQBXQKU.js +107 -0
  218. package/dist/chunk-MKQBXQKU.js.map +1 -0
  219. package/dist/{chunk-7PND4PUC.js → chunk-MMGGVZ4Q.js} +29 -9
  220. package/dist/{chunk-7PND4PUC.js.map → chunk-MMGGVZ4Q.js.map} +1 -1
  221. package/dist/{chunk-D6RIFG4O.js → chunk-MNB5RMAY.js} +2 -2
  222. package/dist/{chunk-Q5WG43OP.js → chunk-MYFQ3ED4.js} +7 -7
  223. package/dist/{chunk-V3UE5I5F.js → chunk-MYVPLJOO.js} +2 -2
  224. package/dist/chunk-NINSR6LX.js +83 -0
  225. package/dist/chunk-NINSR6LX.js.map +1 -0
  226. package/dist/{chunk-MTWKDOVO.js → chunk-NLE6TSLG.js} +2 -2
  227. package/dist/{chunk-OAZVR2BV.js → chunk-OGCAYLNA.js} +8 -6
  228. package/dist/{chunk-OAZVR2BV.js.map → chunk-OGCAYLNA.js.map} +1 -1
  229. package/dist/{chunk-DBRYEIGY.js → chunk-OJP3N7UK.js} +2 -2
  230. package/dist/{chunk-VDW4VU7B.js → chunk-OQJIB32H.js} +28 -23
  231. package/dist/chunk-OQJIB32H.js.map +1 -0
  232. package/dist/{chunk-SMX5267P.js → chunk-OZKOPQCM.js} +2 -2
  233. package/dist/{chunk-JCK7V6SN.js → chunk-P65YJEEL.js} +2 -2
  234. package/dist/{chunk-MTR7BZQN.js → chunk-PBGWYM3H.js} +3 -3
  235. package/dist/chunk-PIG4WJFQ.js +87 -0
  236. package/dist/chunk-PIG4WJFQ.js.map +1 -0
  237. package/dist/chunk-PJSK6OJM.js +201 -0
  238. package/dist/chunk-PJSK6OJM.js.map +1 -0
  239. package/dist/{chunk-OVJ2HIPE.js → chunk-PMUD7ERF.js} +158 -125
  240. package/dist/chunk-PMUD7ERF.js.map +1 -0
  241. package/dist/chunk-PQVCH2AE.js +76 -0
  242. package/dist/chunk-PQVCH2AE.js.map +1 -0
  243. package/dist/{chunk-AXTXN4T6.js → chunk-PWFCHOB3.js} +2 -2
  244. package/dist/{chunk-FDPZVY6D.js → chunk-QFN4U7AG.js} +2 -2
  245. package/dist/{chunk-K2ZLHESQ.js → chunk-QIEUU4UK.js} +2 -2
  246. package/dist/{chunk-DYP3YYJJ.js → chunk-QJAQNXWI.js} +39 -5
  247. package/dist/chunk-QJAQNXWI.js.map +1 -0
  248. package/dist/{chunk-3D6AFGQR.js → chunk-QVT3E4CE.js} +7 -7
  249. package/dist/{chunk-FIJUY7GG.js → chunk-R26ZMXR2.js} +23 -23
  250. package/dist/{chunk-FIJUY7GG.js.map → chunk-R26ZMXR2.js.map} +1 -1
  251. package/dist/chunk-R3DJVOSM.js +34 -0
  252. package/dist/chunk-R3DJVOSM.js.map +1 -0
  253. package/dist/{chunk-SFQ6QNL7.js → chunk-R6YJI3HE.js} +2 -1
  254. package/dist/{chunk-5CJVLG3H.js → chunk-RFDD3B7W.js} +5 -5
  255. package/dist/{chunk-JBPKEARU.js → chunk-RKNJBZ55.js} +4 -4
  256. package/dist/chunk-ROI7BK7H.js +88 -0
  257. package/dist/chunk-ROI7BK7H.js.map +1 -0
  258. package/dist/{chunk-FNZQGN76.js → chunk-S2BBISHU.js} +4 -4
  259. package/dist/{chunk-IALLX7EE.js → chunk-S2FSBU4H.js} +3 -3
  260. package/dist/{chunk-BFPETA3S.js → chunk-SCCO46KZ.js} +3 -2
  261. package/dist/chunk-SCCO46KZ.js.map +1 -0
  262. package/dist/chunk-SK7AMF2J.js +168 -0
  263. package/dist/chunk-SK7AMF2J.js.map +1 -0
  264. package/dist/{chunk-KSBEEMQF.js → chunk-TG6IHVE2.js} +151 -6
  265. package/dist/chunk-TG6IHVE2.js.map +1 -0
  266. package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
  267. package/dist/{chunk-DPAENM7O.js → chunk-U3ZAVRP3.js} +2 -2
  268. package/dist/{chunk-ODPLEWB6.js → chunk-UFGFMWAD.js} +47 -17
  269. package/dist/chunk-UFGFMWAD.js.map +1 -0
  270. package/dist/{chunk-4KJLYTLS.js → chunk-V57NLO5P.js} +2 -2
  271. package/dist/{chunk-FLVL4L3L.js → chunk-VDX2J7OX.js} +2 -2
  272. package/dist/{chunk-AJLLIIML.js → chunk-W4FBGYGK.js} +1 -1
  273. package/dist/{chunk-TXLGHDHS.js → chunk-W4XJFLAL.js} +53 -32
  274. package/dist/chunk-W4XJFLAL.js.map +1 -0
  275. package/dist/{chunk-6SXVCD7W.js → chunk-W7IGLX4G.js} +2 -2
  276. package/dist/{chunk-KQDNAXOM.js → chunk-WG5HSQ4P.js} +19 -6
  277. package/dist/chunk-WG5HSQ4P.js.map +1 -0
  278. package/dist/{chunk-LTJAMRGI.js → chunk-WL5EBRDH.js} +17 -3
  279. package/dist/chunk-WL5EBRDH.js.map +1 -0
  280. package/dist/{chunk-KAWTOYAW.js → chunk-WLZYGLJ4.js} +2 -2
  281. package/dist/{chunk-RO4LQGT3.js → chunk-WON4APQY.js} +4 -4
  282. package/dist/{chunk-PQVK2S3J.js → chunk-WZFAPHIK.js} +6 -6
  283. package/dist/{chunk-KQPJLK3X.js → chunk-XL24QQ63.js} +2 -2
  284. package/dist/{chunk-LZOV7MX7.js → chunk-XQDRKZ7V.js} +6476 -5747
  285. package/dist/chunk-XQDRKZ7V.js.map +1 -0
  286. package/dist/{chunk-RVZRYUQH.js → chunk-YROFNEG5.js} +7 -7
  287. package/dist/{chunk-RAR2BSEM.js → chunk-YV32N67O.js} +1 -1
  288. package/dist/chunk-YV32N67O.js.map +1 -0
  289. package/dist/{chunk-PBSAPHHA.js → chunk-YVKF3HEW.js} +1 -1
  290. package/dist/chunk-YVKF3HEW.js.map +1 -0
  291. package/dist/{chunk-VRAD2FGO.js → chunk-Z54EM5YZ.js} +12 -12
  292. package/dist/{chunk-2VN4JAX7.js → chunk-ZOKZIR5K.js} +95 -33
  293. package/dist/chunk-ZOKZIR5K.js.map +1 -0
  294. package/dist/{chunk-DCFV7A7H.js → chunk-ZVAYK7GU.js} +101 -91
  295. package/dist/chunk-ZVAYK7GU.js.map +1 -0
  296. package/dist/{cli-Cd5fMmhq.d.ts → cli-5tNLVTvJ.d.ts} +28 -12
  297. package/dist/cli.d.ts +16 -9
  298. package/dist/cli.js +142 -127
  299. package/dist/compounding/engine.d.ts +5 -2
  300. package/dist/compounding/engine.js +36 -26
  301. package/dist/compounding/preference-consolidator.d.ts +3 -1
  302. package/dist/compounding/preference-consolidator.js +1 -1
  303. package/dist/compression-optimizer.d.ts +3 -1
  304. package/dist/config-qmd.d.ts +8 -0
  305. package/dist/config-qmd.js +20 -0
  306. package/dist/config-qmd.js.map +1 -0
  307. package/dist/config.d.ts +3 -1
  308. package/dist/config.js +15 -10
  309. package/dist/connectors/codex-materialize-runner.d.ts +3 -1
  310. package/dist/connectors/codex-materialize-runner.js +36 -26
  311. package/dist/connectors/codex-materialize.d.ts +3 -1
  312. package/dist/connectors/codex-materialize.js +2 -2
  313. package/dist/connectors/index.d.ts +3 -1
  314. package/dist/connectors/index.js +41 -32
  315. package/dist/consolidation-provenance-check.d.ts +5 -2
  316. package/dist/consolidation-undo.d.ts +5 -2
  317. package/dist/contradiction/index.d.ts +5 -2
  318. package/dist/contradiction/index.js +10 -4
  319. package/dist/{contradiction-scan-J57T5QV7.js → contradiction-scan-RJBABNRH.js} +4 -4
  320. package/dist/contradiction-scan-RJBABNRH.js.map +1 -0
  321. package/dist/conversation-index/backend.d.ts +3 -1
  322. package/dist/conversation-index/backend.js +5 -5
  323. package/dist/conversation-index/chunker.d.ts +3 -1
  324. package/dist/conversation-index/cleanup.js +2 -2
  325. package/dist/conversation-index/faiss-adapter.d.ts +3 -1
  326. package/dist/conversation-index/faiss-adapter.js +2 -2
  327. package/dist/conversation-index/indexer.d.ts +3 -1
  328. package/dist/conversation-index/indexer.js +2 -2
  329. package/dist/conversation-index/search.d.ts +3 -1
  330. package/dist/conversation-index/search.js +2 -2
  331. package/dist/cross-namespace-budget.d.ts +45 -1
  332. package/dist/cross-namespace-budget.js +5 -3
  333. package/dist/dashboard-runtime.js +2 -2
  334. package/dist/day-summary.d.ts +3 -1
  335. package/dist/day-summary.js +3 -3
  336. package/dist/delinearize.d.ts +3 -1
  337. package/dist/direct-answer-wiring.d.ts +3 -1
  338. package/dist/direct-answer.d.ts +3 -1
  339. package/dist/{dreams-ledger-6UODQOX6.js → dreams-ledger-567VKVOU.js} +4 -2
  340. package/dist/dreams-ledger-567VKVOU.js.map +1 -0
  341. package/dist/embedding-fallback.d.ts +3 -1
  342. package/dist/embedding-fallback.js +3 -3
  343. package/dist/enrichment/index.d.ts +3 -1
  344. package/dist/entity-retrieval.d.ts +5 -2
  345. package/dist/entity-retrieval.js +35 -26
  346. package/dist/entity-schema.d.ts +3 -1
  347. package/dist/explicit-capture.d.ts +13 -8
  348. package/dist/explicit-capture.js +7 -3
  349. package/dist/extraction-faithfulness.d.ts +3 -1
  350. package/dist/extraction-faithfulness.js +5 -5
  351. package/dist/extraction-judge-telemetry.d.ts +3 -1
  352. package/dist/extraction-judge-telemetry.js +2 -2
  353. package/dist/extraction-judge-training.d.ts +3 -1
  354. package/dist/extraction-judge-training.js +2 -2
  355. package/dist/extraction-judge.d.ts +3 -1
  356. package/dist/extraction-judge.js +5 -5
  357. package/dist/extraction.d.ts +3 -1
  358. package/dist/extraction.js +13 -12
  359. package/dist/faithfulness-config.js +3 -2
  360. package/dist/fallback-llm.d.ts +3 -1
  361. package/dist/fallback-llm.js +4 -4
  362. package/dist/{first-start-migration-TI67YMLG.js → first-start-migration-CSRHCJIQ.js} +2 -2
  363. package/dist/{forget-CQCLB5LB.js → forget-HRPLVB3W.js} +5 -5
  364. package/dist/graph-dashboard-diff.d.ts +3 -1
  365. package/dist/graph-dashboard-key.d.ts +3 -1
  366. package/dist/graph-dashboard-parser.d.ts +3 -1
  367. package/dist/{graph-edge-decay-IOGBBAGC.js → graph-edge-decay-Z6ENALU2.js} +3 -3
  368. package/dist/graph-edge-decay-Z6ENALU2.js.map +1 -0
  369. package/dist/graph-edge-reinforcement.d.ts +3 -1
  370. package/dist/graph-snapshot.d.ts +3 -1
  371. package/dist/graph-snapshot.js +2 -2
  372. package/dist/graph.d.ts +3 -1
  373. package/dist/graph.js +1 -1
  374. package/dist/identity-continuity.d.ts +1 -2
  375. package/dist/importance.d.ts +3 -1
  376. package/dist/importers/index.d.ts +3 -1
  377. package/dist/in-flight-reads.d.ts +3 -1
  378. package/dist/index.d.ts +473 -464
  379. package/dist/index.js +287 -239
  380. package/dist/intent.d.ts +3 -1
  381. package/dist/lcm/archive.js +2 -2
  382. package/dist/lcm/engine.d.ts +3 -1
  383. package/dist/lcm/engine.js +6 -6
  384. package/dist/lcm/index.d.ts +3 -1
  385. package/dist/lcm/index.js +13 -13
  386. package/dist/lcm/schema.js +2 -2
  387. package/dist/lcm/summarizer.js +3 -3
  388. package/dist/lcm/tools.d.ts +3 -1
  389. package/dist/lifecycle.d.ts +3 -1
  390. package/dist/live-connectors-runner.d.ts +3 -1
  391. package/dist/local-llm.d.ts +5 -2
  392. package/dist/local-llm.js +3 -3
  393. package/dist/local-model-endpoint.d.ts +3 -1
  394. package/dist/logger.d.ts +3 -1
  395. package/dist/logger.js +7 -3
  396. package/dist/maintenance/archive-observations.js +2 -2
  397. package/dist/maintenance/memory-governance.d.ts +3 -1
  398. package/dist/maintenance/memory-governance.js +35 -25
  399. package/dist/maintenance/migrate-observations.js +3 -3
  400. package/dist/maintenance/observation-ledger-utils.js +2 -2
  401. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +102 -1
  402. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +37 -25
  403. package/dist/maintenance/rebuild-memory-projection.js +38 -28
  404. package/dist/maintenance/rebuild-observations.js +3 -3
  405. package/dist/mcp-memory-inspector-app.d.ts +22 -15
  406. package/dist/memory-action-policy.d.ts +3 -1
  407. package/dist/memory-cache.d.ts +3 -1
  408. package/dist/memory-lifecycle-ledger-utils.d.ts +25 -2
  409. package/dist/memory-lifecycle-ledger-utils.js +11 -1
  410. package/dist/memory-projection-store.d.ts +3 -1
  411. package/dist/memory-projection-store.js +2 -2
  412. package/dist/memory-provenance.d.ts +3 -1
  413. package/dist/memory-worth-outcomes.d.ts +5 -2
  414. package/dist/model-registry.js +2 -2
  415. package/dist/models-json.d.ts +3 -1
  416. package/dist/models-json.js +2 -2
  417. package/dist/namespaces/migrate.d.ts +6 -3
  418. package/dist/namespaces/migrate.js +51 -42
  419. package/dist/namespaces/principal.d.ts +27 -2
  420. package/dist/namespaces/principal.js +6 -2
  421. package/dist/namespaces/search.d.ts +16 -2
  422. package/dist/namespaces/search.js +24 -20
  423. package/dist/namespaces/storage.d.ts +24 -3
  424. package/dist/namespaces/storage.js +35 -27
  425. package/dist/native-knowledge.d.ts +3 -1
  426. package/dist/native-knowledge.js +2 -2
  427. package/dist/negative.js +2 -2
  428. package/dist/offline-sync-exclude-globs.d.ts +13 -0
  429. package/dist/offline-sync-exclude-globs.js +10 -0
  430. package/dist/offline-sync-exclude-globs.js.map +1 -0
  431. package/dist/offline-sync-impression-drain.d.ts +78 -0
  432. package/dist/offline-sync-impression-drain.js +37 -0
  433. package/dist/offline-sync-impression-drain.js.map +1 -0
  434. package/dist/offline-sync.js +4 -2
  435. package/dist/operator-toolkit.d.ts +5 -2
  436. package/dist/operator-toolkit.js +69 -54
  437. package/dist/orchestration/compression-guideline-coordinator.d.ts +5 -2
  438. package/dist/orchestration/compression-guideline-coordinator.js +3 -3
  439. package/dist/orchestration/maintenance.d.ts +121 -2
  440. package/dist/orchestration/maintenance.js +44 -31
  441. package/dist/{orchestrator-Bp_jeBHK.d.ts → orchestrator-CQJ8U7En.d.ts} +85 -79
  442. package/dist/orchestrator.d.ts +13 -8
  443. package/dist/orchestrator.js +194 -177
  444. package/dist/patterns-cli.d.ts +3 -1
  445. package/dist/{pipeline-BAj5bx7g.d.ts → pipeline-Cn-kvYeV.d.ts} +1 -1
  446. package/dist/policy-runtime.d.ts +3 -1
  447. package/dist/prioritized-embed.d.ts +19 -0
  448. package/dist/prioritized-embed.js +8 -0
  449. package/dist/prioritized-embed.js.map +1 -0
  450. package/dist/proactive-contention.d.ts +28 -0
  451. package/dist/proactive-contention.js +8 -0
  452. package/dist/proactive-contention.js.map +1 -0
  453. package/dist/profiling.js +2 -2
  454. package/dist/provenance.d.ts +3 -1
  455. package/dist/provenance.js +3 -2
  456. package/dist/{purge-NDWV6WYD.js → purge-IRF47ZTX.js} +3 -3
  457. package/dist/{qmd-_MDSlj1m.d.ts → qmd-oh9VXz1d.d.ts} +11 -1
  458. package/dist/qmd-preflight.d.ts +5 -2
  459. package/dist/qmd-recall-cache.d.ts +3 -1
  460. package/dist/qmd-status.d.ts +57 -0
  461. package/dist/qmd-status.js +18 -0
  462. package/dist/qmd-status.js.map +1 -0
  463. package/dist/qmd.d.ts +5 -2
  464. package/dist/qmd.js +7 -3
  465. package/dist/recall-concurrency-config.d.ts +16 -0
  466. package/dist/recall-concurrency-config.js +10 -0
  467. package/dist/recall-concurrency-config.js.map +1 -0
  468. package/dist/recall-disclosure-escalation.d.ts +3 -1
  469. package/dist/recall-explain-renderer.d.ts +4 -1
  470. package/dist/recall-explain-renderer.js +3 -3
  471. package/dist/recall-memory-map.d.ts +26 -0
  472. package/dist/recall-memory-map.js +18 -0
  473. package/dist/recall-memory-map.js.map +1 -0
  474. package/dist/recall-planner-llm.d.ts +3 -1
  475. package/dist/recall-planner-llm.js +4 -4
  476. package/dist/recall-qos.js +2 -2
  477. package/dist/recall-state.d.ts +163 -2
  478. package/dist/recall-state.js +6 -3
  479. package/dist/recall-tag-filter.d.ts +3 -1
  480. package/dist/recall-timings.d.ts +11 -2
  481. package/dist/recall-timings.js +3 -1
  482. package/dist/recall-xray-cli.d.ts +3 -1
  483. package/dist/recall-xray-cli.js +4 -4
  484. package/dist/recall-xray-renderer.d.ts +3 -1
  485. package/dist/recall-xray-renderer.js +3 -3
  486. package/dist/recall-xray.d.ts +3 -1
  487. package/dist/recall-xray.js +2 -2
  488. package/dist/relevance.js +3 -3
  489. package/dist/replay/normalizers/chatgpt.d.ts +3 -1
  490. package/dist/replay/normalizers/claude.d.ts +3 -1
  491. package/dist/replay/normalizers/openclaw.d.ts +3 -1
  492. package/dist/replay/normalizers/shared.d.ts +3 -1
  493. package/dist/replay/runner.d.ts +3 -1
  494. package/dist/replay/types.d.ts +3 -1
  495. package/dist/rerank.d.ts +8 -1
  496. package/dist/rerank.js +3 -1
  497. package/dist/resolution-3K27T4VO.js +18 -0
  498. package/dist/resolution-3K27T4VO.js.map +1 -0
  499. package/dist/resolve-auth-token.d.ts +3 -1
  500. package/dist/resolve-provider-secret.js +2 -2
  501. package/dist/resume-bundles.js +19 -14
  502. package/dist/retrieval-agents.d.ts +19 -16
  503. package/dist/retrieval-agents.js +3 -3
  504. package/dist/retrieval-tiers.d.ts +3 -1
  505. package/dist/routing/engine.d.ts +3 -1
  506. package/dist/routing/store.d.ts +3 -1
  507. package/dist/routing/store.js +2 -2
  508. package/dist/salvage-envelope.d.ts +18 -0
  509. package/dist/salvage-envelope.js +15 -0
  510. package/dist/salvage-envelope.js.map +1 -0
  511. package/dist/schemas.d.ts +98 -98
  512. package/dist/scope-profiles-EpJG7yKP.d.ts +30 -0
  513. package/dist/search/document-scanner.js +3 -2
  514. package/dist/search/embed-helper.d.ts +3 -1
  515. package/dist/search/embed-helper.js +3 -3
  516. package/dist/search/factory.d.ts +3 -1
  517. package/dist/search/factory.js +18 -16
  518. package/dist/search/index.d.ts +3 -1
  519. package/dist/search/index.js +22 -20
  520. package/dist/search/lancedb-backend.d.ts +3 -1
  521. package/dist/search/lancedb-backend.js +6 -5
  522. package/dist/search/meilisearch-backend.d.ts +3 -1
  523. package/dist/search/meilisearch-backend.js +6 -5
  524. package/dist/search/noop-backend.d.ts +3 -1
  525. package/dist/search/orama-backend.d.ts +3 -1
  526. package/dist/search/orama-backend.js +6 -5
  527. package/dist/search/port.d.ts +16 -2
  528. package/dist/search/port.js +1 -1
  529. package/dist/search/remote-backend.d.ts +3 -1
  530. package/dist/search/remote-backend.js +3 -3
  531. package/dist/secure-store/index.d.ts +12 -1
  532. package/dist/secure-store/index.js +6 -3
  533. package/dist/{semantic-consolidation-C2m5eHHE.d.ts → semantic-consolidation-DILZEQq3.d.ts} +1 -1
  534. package/dist/semantic-consolidation.d.ts +4 -2
  535. package/dist/semantic-consolidation.js +38 -29
  536. package/dist/semantic-rule-promotion.js +34 -24
  537. package/dist/semantic-rule-verifier.d.ts +3 -1
  538. package/dist/semantic-rule-verifier.js +35 -25
  539. package/dist/session-observer-bands.d.ts +3 -1
  540. package/dist/session-observer-state.d.ts +3 -1
  541. package/dist/session-observer-state.js +2 -2
  542. package/dist/session-transcript-migration.js +2 -2
  543. package/dist/shared-context/manager.d.ts +11 -9
  544. package/dist/shared-context/manager.js +3 -3
  545. package/dist/signal.d.ts +3 -1
  546. package/dist/{storage-hr_Y2z5R.d.ts → storage-NIyKYD8g.d.ts} +489 -110
  547. package/dist/storage-guards.js +2 -2
  548. package/dist/storage.d.ts +6 -3
  549. package/dist/storage.js +38 -26
  550. package/dist/structured-attributes.d.ts +23 -1
  551. package/dist/structured-attributes.js +7 -1
  552. package/dist/summarizer.d.ts +3 -1
  553. package/dist/summarizer.js +11 -11
  554. package/dist/summary-snapshot.d.ts +3 -1
  555. package/dist/summary-snapshot.js +2 -2
  556. package/dist/temporal-index-batch.d.ts +1 -0
  557. package/dist/temporal-index-batch.js +10 -0
  558. package/dist/temporal-index-batch.js.map +1 -0
  559. package/dist/temporal-index-compat.d.ts +279 -1
  560. package/dist/temporal-index-compat.js +1 -1
  561. package/dist/temporal-index.d.ts +1 -225
  562. package/dist/temporal-index.js +35 -3
  563. package/dist/temporal-supersession.d.ts +5 -2
  564. package/dist/temporal-supersession.js +2 -2
  565. package/dist/temporal-timeline-recall.d.ts +3 -1
  566. package/dist/temporal-validity.d.ts +3 -1
  567. package/dist/threading.d.ts +3 -1
  568. package/dist/threading.js +2 -2
  569. package/dist/tier-migration.d.ts +5 -2
  570. package/dist/tier-routing.d.ts +3 -1
  571. package/dist/{tier-stats-KV7IWCJR.js → tier-stats-QEJSPFV2.js} +2 -2
  572. package/dist/tokens.js +3 -3
  573. package/dist/topics.d.ts +3 -1
  574. package/dist/transcript.d.ts +3 -1
  575. package/dist/transcript.js +2 -2
  576. package/dist/transfer/backup.js +3 -3
  577. package/dist/transfer/capsule-export.js +3 -3
  578. package/dist/transfer/capsule-import.js +3 -3
  579. package/dist/transfer/types.d.ts +90 -90
  580. package/dist/trust-score-stage.d.ts +75 -38
  581. package/dist/trust-score-stage.js +11 -3
  582. package/dist/trust-score.d.ts +3 -1
  583. package/dist/{types-Cg8q7W7_.d.ts → types-D_fG2ix8.d.ts} +39 -63
  584. package/dist/types-continuity.d.ts +52 -0
  585. package/dist/types-continuity.js +1 -0
  586. package/dist/types-continuity.js.map +1 -0
  587. package/dist/types.d.ts +3 -1
  588. package/dist/types.js +1 -1
  589. package/dist/utility-runtime.d.ts +3 -1
  590. package/dist/utils/serialize-mutations.d.ts +31 -2
  591. package/dist/utils/serialize-mutations.js +1 -1
  592. package/dist/verified-recall.js +36 -26
  593. package/dist/write-envelope.d.ts +282 -0
  594. package/dist/write-envelope.js +35 -0
  595. package/dist/write-envelope.js.map +1 -0
  596. package/dist/write-quarantine.d.ts +94 -0
  597. package/dist/write-quarantine.js +11 -0
  598. package/dist/write-quarantine.js.map +1 -0
  599. package/dist/write-rate-limiter.d.ts +45 -0
  600. package/dist/write-rate-limiter.js +9 -0
  601. package/dist/write-rate-limiter.js.map +1 -0
  602. package/package.json +37 -2
  603. package/src/access-admin-ops-surface.ts +17 -7
  604. package/src/access-boundary.test.ts +28 -0
  605. package/src/access-boundary.ts +31 -2
  606. package/src/access-citation.test.ts +72 -0
  607. package/src/access-citation.ts +139 -0
  608. package/src/access-errors.ts +21 -0
  609. package/src/access-fingerprint-parity.test.ts +211 -0
  610. package/src/access-http-cancellation.test.ts +59 -21
  611. package/src/access-http.test.ts +90 -0
  612. package/src/access-http.ts +79 -79
  613. package/src/access-mcp-cancellation.test.ts +16 -7
  614. package/src/access-memory-search-fanout.test.ts +220 -0
  615. package/src/access-memory-search-fanout.ts +168 -0
  616. package/src/access-observe-quarantine.test.ts +172 -0
  617. package/src/access-observe-write-surface.ts +137 -11
  618. package/src/access-operations-batch.ts +2 -0
  619. package/src/access-recall-concurrency.ts +773 -0
  620. package/src/access-recall-response.ts +307 -0
  621. package/src/access-recall-surface.ts +100 -252
  622. package/src/access-service-coding-write.test.ts +22 -4
  623. package/src/access-service-health.test.ts +49 -0
  624. package/src/access-service-namespace.test.ts +486 -8
  625. package/src/access-service-observe-scope.test.ts +4 -0
  626. package/src/access-service-recall-concurrency.test.ts +1990 -0
  627. package/src/access-service.ts +186 -179
  628. package/src/access-surface-catalog.ts +1 -0
  629. package/src/access-token-capabilities.ts +16 -9
  630. package/src/active-recall.test.ts +35 -0
  631. package/src/active-recall.ts +7 -0
  632. package/src/bounded-jsonl-state.ts +93 -0
  633. package/src/buffer-session.test.ts +422 -5
  634. package/src/buffer-surprise-trigger.test.ts +41 -0
  635. package/src/buffer.ts +218 -3
  636. package/src/capabilities-cluster-b.test.ts +1 -0
  637. package/src/capabilities.ts +5 -6
  638. package/src/cli.ts +21 -22
  639. package/src/coding/architecture-surfaces.test.ts +6 -3
  640. package/src/coding/architecture-surfaces.ts +36 -25
  641. package/src/coding/decision-surfaces.ts +42 -34
  642. package/src/compounding/engine.test.ts +5 -1
  643. package/src/compounding/engine.ts +7 -7
  644. package/src/config-qmd.ts +89 -0
  645. package/src/config.test.ts +136 -0
  646. package/src/config.ts +113 -118
  647. package/src/connector-provenance-lifecycle.test.ts +4 -4
  648. package/src/connectors/coerce.test.ts +131 -0
  649. package/src/connectors/coerce.ts +80 -7
  650. package/src/contradiction/contradiction.test.ts +10 -6
  651. package/src/contradiction/resolution.ts +17 -4
  652. package/src/correction/correction-access-wiring.ts +62 -18
  653. package/src/cross-namespace-budget.test.ts +35 -0
  654. package/src/cross-namespace-budget.ts +86 -19
  655. package/src/explicit-capture.ts +54 -17
  656. package/src/extraction.ts +2 -3
  657. package/src/faithfulness-graph-guard.test.ts +16 -69
  658. package/src/graph-recall.test.ts +31 -0
  659. package/src/index.ts +22 -2
  660. package/src/local-llm.ts +4 -6
  661. package/src/logger.ts +16 -0
  662. package/src/maintenance/dreams-ledger.ts +100 -49
  663. package/src/maintenance/rebuild-memory-lifecycle-ledger-cli.ts +77 -0
  664. package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +373 -9
  665. package/src/memory-lifecycle-ledger-utils.ts +51 -5
  666. package/src/namespaces/principal.test.ts +37 -1
  667. package/src/namespaces/principal.ts +48 -0
  668. package/src/namespaces/search.test.ts +79 -5
  669. package/src/namespaces/search.ts +59 -6
  670. package/src/namespaces/storage.ts +43 -1
  671. package/src/offline-sync-exclude-globs.ts +58 -0
  672. package/src/offline-sync-impression-drain.test.ts +316 -0
  673. package/src/offline-sync-impression-drain.ts +260 -0
  674. package/src/offline-sync.test.ts +32 -5
  675. package/src/offline-sync.ts +1 -14
  676. package/src/operator-toolkit.ts +1 -1
  677. package/src/orchestration/batch-deindex-flush.test.ts +146 -0
  678. package/src/orchestration/consolidation-run.ts +96 -85
  679. package/src/orchestration/extraction-envelope.ts +94 -0
  680. package/src/orchestration/extraction-persist-promotion.ts +134 -0
  681. package/src/orchestration/extraction-persist.ts +325 -260
  682. package/src/orchestration/extraction-run.test.ts +1 -1
  683. package/src/orchestration/extraction-run.ts +2 -2
  684. package/src/orchestration/graph-recall-coordinator.ts +18 -7
  685. package/src/orchestration/lifecycle-policy-coordinator.ts +63 -48
  686. package/src/orchestration/maintenance.test.ts +2103 -3
  687. package/src/orchestration/maintenance.ts +574 -2
  688. package/src/orchestration/namespace-read-fanout.ts +14 -8
  689. package/src/orchestration/orchestrator-helpers.ts +8 -1
  690. package/src/orchestration/orchestrator-init.ts +22 -6
  691. package/src/orchestration/orchestrator-namespace-scope.ts +71 -0
  692. package/src/orchestration/persistence-index.ts +64 -9
  693. package/src/orchestration/qmd-result-resolver.ts +27 -0
  694. package/src/orchestration/recall-entry.ts +34 -19
  695. package/src/orchestration/recall-internal.ts +207 -79
  696. package/src/orchestration/recall-rerank-coordinator.ts +229 -82
  697. package/src/orchestration/recall-result-formatter.ts +76 -20
  698. package/src/orchestration/recall-search-pipeline.test.ts +70 -0
  699. package/src/orchestration/recall-search-pipeline.ts +102 -129
  700. package/src/orchestration/recall-section-coordinator.ts +315 -85
  701. package/src/orchestration/semantic-consolidation-coordinator.ts +69 -37
  702. package/src/orchestration/session-context.ts +21 -0
  703. package/src/orchestration/workspace-ops.test.ts +348 -0
  704. package/src/orchestration/workspace-ops.ts +168 -34
  705. package/src/orchestrator-chunked-hash-durability.test.ts +126 -0
  706. package/src/orchestrator-connector-dedup.test.ts +1061 -40
  707. package/src/orchestrator-flush.test.ts +155 -39
  708. package/src/orchestrator-nonchunked-hash-durability.test.ts +121 -0
  709. package/src/orchestrator-source-attribution.test.ts +9 -9
  710. package/src/orchestrator.ts +160 -149
  711. package/src/prioritized-embed.ts +123 -0
  712. package/src/proactive-contention.test.ts +80 -0
  713. package/src/proactive-contention.ts +39 -0
  714. package/src/procedural/procedure-miner.test.ts +19 -14
  715. package/src/procedural/procedure-miner.ts +44 -13
  716. package/src/qmd-client.test.ts +111 -1
  717. package/src/qmd-status.ts +186 -0
  718. package/src/qmd.ts +47 -47
  719. package/src/recall-budget-config.test.ts +30 -0
  720. package/src/recall-cold-deadline.test.ts +149 -0
  721. package/src/recall-concurrency-config.ts +28 -0
  722. package/src/recall-memory-map.ts +74 -0
  723. package/src/recall-rerank-coordinator.test.ts +508 -0
  724. package/src/recall-state.test.ts +825 -1
  725. package/src/recall-state.ts +406 -7
  726. package/src/recall-timings-access.test.ts +41 -0
  727. package/src/recall-timings.ts +16 -0
  728. package/src/relay/mission-access.test.ts +5 -5
  729. package/src/rerank.ts +22 -0
  730. package/src/retrieval-agents.test.ts +79 -0
  731. package/src/retrieval-agents.ts +71 -19
  732. package/src/salvage-envelope.ts +28 -0
  733. package/src/scopes/scope-plan.test.ts +42 -0
  734. package/src/scopes/scope-plan.ts +45 -0
  735. package/src/search/port.ts +13 -0
  736. package/src/secure-store/index.ts +1 -0
  737. package/src/secure-store/secure-fs.ts +48 -0
  738. package/src/secure-store/secure-store.test.ts +40 -0
  739. package/src/semantic-rule-promotion.ts +13 -4
  740. package/src/storage/content-hash-index.ts +655 -0
  741. package/src/storage/entity-timeline.ts +473 -0
  742. package/src/storage/memory-lifecycle-ledger-access.ts +603 -0
  743. package/src/storage/secure-line-reader.test.ts +197 -0
  744. package/src/storage/secure-line-reader.ts +181 -3
  745. package/src/storage-append-header-sniff.test.ts +195 -0
  746. package/src/storage-behavior-signals-cache.test.ts +237 -0
  747. package/src/storage-contract/cache-coherence.test.ts +96 -1
  748. package/src/storage-contract/content-hash.test.ts +47 -2
  749. package/src/storage-fact-hash-index-defer.test.ts +1200 -0
  750. package/src/storage-memory-lifecycle-ledger.test.ts +1146 -3
  751. package/src/storage.ts +882 -890
  752. package/src/structured-attributes.ts +52 -0
  753. package/src/temporal-index-batch.ts +112 -0
  754. package/src/temporal-index.test.ts +193 -0
  755. package/src/temporal-index.ts +49 -53
  756. package/src/testing/lifecycle-matrix.test.ts +212 -0
  757. package/src/testing/lifecycle-matrix.ts +271 -0
  758. package/src/testing/orchestrator-lite.ts +236 -0
  759. package/src/testing/subjects/dreams-ledger.test.ts +67 -0
  760. package/src/testing/subjects/extraction-lifecycle.test.ts +442 -0
  761. package/src/testing/subjects/lifecycle-ledger.test.ts +93 -0
  762. package/src/testing/subjects/recall-budget.test.ts +461 -0
  763. package/src/testing/subjects/serialized-write-chain.test.ts +224 -0
  764. package/src/trust-score-stage.test.ts +154 -0
  765. package/src/trust-score-stage.ts +216 -53
  766. package/src/types-continuity.ts +57 -0
  767. package/src/types.ts +51 -77
  768. package/src/utils/path-containment.test.ts +75 -0
  769. package/src/utils/path-containment.ts +76 -0
  770. package/src/utils/serialize-mutations.test.ts +122 -0
  771. package/src/utils/serialize-mutations.ts +68 -3
  772. package/src/wearables/memory-gen.test.ts +31 -9
  773. package/src/wearables/memory-gen.ts +95 -55
  774. package/src/wearables/pipeline.test.ts +10 -7
  775. package/src/wearables/service.test.ts +6 -8
  776. package/src/wearables/service.ts +2 -2
  777. package/src/write-envelope.extension.test.ts +111 -0
  778. package/src/write-envelope.test.ts +683 -0
  779. package/src/write-envelope.ts +902 -0
  780. package/src/write-quarantine.test.ts +245 -0
  781. package/src/write-quarantine.ts +290 -0
  782. package/src/write-rate-limiter.test.ts +105 -0
  783. package/src/write-rate-limiter.ts +135 -0
  784. package/src/write-sealed-parity.test.ts +204 -0
  785. package/dist/chunk-2VN4JAX7.js.map +0 -1
  786. package/dist/chunk-37CTGERM.js.map +0 -1
  787. package/dist/chunk-3IY64MV6.js.map +0 -1
  788. package/dist/chunk-BCDKBDSY.js.map +0 -1
  789. package/dist/chunk-BFPETA3S.js.map +0 -1
  790. package/dist/chunk-BFVEEB7W.js.map +0 -1
  791. package/dist/chunk-BS4SGLWV.js.map +0 -1
  792. package/dist/chunk-DCFV7A7H.js.map +0 -1
  793. package/dist/chunk-DYP3YYJJ.js.map +0 -1
  794. package/dist/chunk-E5BK6CGV.js +0 -335
  795. package/dist/chunk-E5BK6CGV.js.map +0 -1
  796. package/dist/chunk-FTYDWKO6.js.map +0 -1
  797. package/dist/chunk-GWTFLF2P.js.map +0 -1
  798. package/dist/chunk-IKQTFQMI.js.map +0 -1
  799. package/dist/chunk-INELTPRG.js.map +0 -1
  800. package/dist/chunk-J2HSAU72.js.map +0 -1
  801. package/dist/chunk-JREB6UFH.js.map +0 -1
  802. package/dist/chunk-K7LOWSLU.js.map +0 -1
  803. package/dist/chunk-KQDNAXOM.js.map +0 -1
  804. package/dist/chunk-KSBEEMQF.js.map +0 -1
  805. package/dist/chunk-KSNVDPDF.js.map +0 -1
  806. package/dist/chunk-LC7OODWZ.js.map +0 -1
  807. package/dist/chunk-LTJAMRGI.js.map +0 -1
  808. package/dist/chunk-LZOV7MX7.js.map +0 -1
  809. package/dist/chunk-NWTGD4AN.js.map +0 -1
  810. package/dist/chunk-ODPLEWB6.js.map +0 -1
  811. package/dist/chunk-OVJ2HIPE.js.map +0 -1
  812. package/dist/chunk-PBSAPHHA.js.map +0 -1
  813. package/dist/chunk-PHK3HARR.js +0 -32
  814. package/dist/chunk-PHK3HARR.js.map +0 -1
  815. package/dist/chunk-PLQE6MJA.js.map +0 -1
  816. package/dist/chunk-QKQYXZVS.js.map +0 -1
  817. package/dist/chunk-QQGPUKMT.js.map +0 -1
  818. package/dist/chunk-RANHFI3P.js +0 -8
  819. package/dist/chunk-RANHFI3P.js.map +0 -1
  820. package/dist/chunk-RAR2BSEM.js.map +0 -1
  821. package/dist/chunk-RULE4VG5.js.map +0 -1
  822. package/dist/chunk-SVOZFLIQ.js +0 -16
  823. package/dist/chunk-SVOZFLIQ.js.map +0 -1
  824. package/dist/chunk-T7Y5KPLN.js.map +0 -1
  825. package/dist/chunk-TXLGHDHS.js.map +0 -1
  826. package/dist/chunk-VDW4VU7B.js.map +0 -1
  827. package/dist/chunk-VSMRDBFT.js +0 -17
  828. package/dist/chunk-VSMRDBFT.js.map +0 -1
  829. package/dist/chunk-X64YLC24.js.map +0 -1
  830. package/dist/chunk-XDSXEKYK.js +0 -118
  831. package/dist/chunk-XDSXEKYK.js.map +0 -1
  832. package/dist/chunk-YF6FMLBT.js.map +0 -1
  833. package/dist/resolution-VH4NGENZ.js +0 -12
  834. /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-citation.js.map} +0 -0
  835. /package/dist/{contradiction-scan-J57T5QV7.js.map → access-memory-search-fanout.js.map} +0 -0
  836. /package/dist/{dreams-ledger-6UODQOX6.js.map → access-recall-concurrency.js.map} +0 -0
  837. /package/dist/{graph-edge-decay-IOGBBAGC.js.map → access-recall-response.js.map} +0 -0
  838. /package/dist/{resolution-VH4NGENZ.js.map → bounded-jsonl-state.js.map} +0 -0
  839. /package/dist/{chunk-A3GOMRJG.js.map → chunk-23L3IXFD.js.map} +0 -0
  840. /package/dist/{chunk-MRHFJV44.js.map → chunk-2DQ2XREU.js.map} +0 -0
  841. /package/dist/{chunk-L2B3EE7B.js.map → chunk-2QMKMVHH.js.map} +0 -0
  842. /package/dist/{chunk-VJVNLNDQ.js.map → chunk-2UFO7X4Q.js.map} +0 -0
  843. /package/dist/{chunk-2MR3MFQB.js.map → chunk-2VEOKUPP.js.map} +0 -0
  844. /package/dist/{chunk-RHOZ5LLO.js.map → chunk-347WD2LE.js.map} +0 -0
  845. /package/dist/{chunk-HDTQVNWD.js.map → chunk-3QAH2S33.js.map} +0 -0
  846. /package/dist/{chunk-LRNMRXJO.js.map → chunk-3Y224NYC.js.map} +0 -0
  847. /package/dist/{chunk-VFSP73LP.js.map → chunk-45GF7Y3V.js.map} +0 -0
  848. /package/dist/{chunk-OEHCMALU.js.map → chunk-5I4M26Q7.js.map} +0 -0
  849. /package/dist/{chunk-2AZXIZEN.js.map → chunk-5Q7EVV4X.js.map} +0 -0
  850. /package/dist/{chunk-PUBTHW5O.js.map → chunk-6DRPPQEJ.js.map} +0 -0
  851. /package/dist/{chunk-FDCTQWLW.js.map → chunk-6O22TZSB.js.map} +0 -0
  852. /package/dist/{chunk-ZQ7UBUZ6.js.map → chunk-6QYST5XN.js.map} +0 -0
  853. /package/dist/{chunk-W4PGTAS2.js.map → chunk-6WAFYNVT.js.map} +0 -0
  854. /package/dist/{chunk-R5VAVVHP.js.map → chunk-72A4L5JJ.js.map} +0 -0
  855. /package/dist/{chunk-3FAMU5TX.js.map → chunk-A6USBZTO.js.map} +0 -0
  856. /package/dist/{chunk-ZHIVCODT.js.map → chunk-A72SWTWW.js.map} +0 -0
  857. /package/dist/{chunk-FC77VUX5.js.map → chunk-A7Q43EUZ.js.map} +0 -0
  858. /package/dist/{chunk-UJBESW6X.js.map → chunk-AE7WMV4V.js.map} +0 -0
  859. /package/dist/{chunk-A5UXZRRM.js.map → chunk-AJ7QCSIS.js.map} +0 -0
  860. /package/dist/{chunk-FNMFJWBY.js.map → chunk-ANEUPF6O.js.map} +0 -0
  861. /package/dist/{chunk-2A3OXAP2.js.map → chunk-B7RWBZXW.js.map} +0 -0
  862. /package/dist/{chunk-74F5STS4.js.map → chunk-BLAKS3CP.js.map} +0 -0
  863. /package/dist/{chunk-M7VGG3ID.js.map → chunk-BV5WMSIJ.js.map} +0 -0
  864. /package/dist/{chunk-2BFLDH5F.js.map → chunk-BXLOS5AJ.js.map} +0 -0
  865. /package/dist/{chunk-UXKMMNJD.js.map → chunk-C23BYTJV.js.map} +0 -0
  866. /package/dist/{chunk-JZHHREBL.js.map → chunk-C3AKM55W.js.map} +0 -0
  867. /package/dist/{chunk-RP7YXA67.js.map → chunk-C5JF6EZ7.js.map} +0 -0
  868. /package/dist/{chunk-EHCONI6E.js.map → chunk-CQMEM5CC.js.map} +0 -0
  869. /package/dist/{chunk-PBNZVPFS.js.map → chunk-CXZQOVIE.js.map} +0 -0
  870. /package/dist/{chunk-7N2RUBAP.js.map → chunk-DDA47TES.js.map} +0 -0
  871. /package/dist/{chunk-64SE4X5G.js.map → chunk-DEBHXHJR.js.map} +0 -0
  872. /package/dist/{chunk-EHK6KZHK.js.map → chunk-DNHVPZOD.js.map} +0 -0
  873. /package/dist/{chunk-437S3G37.js.map → chunk-DQEMWVMT.js.map} +0 -0
  874. /package/dist/{chunk-LZZNTPLR.js.map → chunk-DX3XRGLH.js.map} +0 -0
  875. /package/dist/{chunk-DZKHSDOM.js.map → chunk-ES36HTSC.js.map} +0 -0
  876. /package/dist/{chunk-TN7THW5U.js.map → chunk-ET356KSB.js.map} +0 -0
  877. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  878. /package/dist/{chunk-DMSDUGDM.js.map → chunk-F7GAIWFA.js.map} +0 -0
  879. /package/dist/{chunk-G6XXLKC5.js.map → chunk-FFPJINJE.js.map} +0 -0
  880. /package/dist/{chunk-G4CN3ZXL.js.map → chunk-FUTF72MJ.js.map} +0 -0
  881. /package/dist/{chunk-HF77M34J.js.map → chunk-GFPZS3F5.js.map} +0 -0
  882. /package/dist/{chunk-G6NKALRN.js.map → chunk-H4WG3UQZ.js.map} +0 -0
  883. /package/dist/{chunk-UDQUSU27.js.map → chunk-HNZKVLAN.js.map} +0 -0
  884. /package/dist/{chunk-5EZXHU36.js.map → chunk-HYQJQGD3.js.map} +0 -0
  885. /package/dist/{chunk-TRWPCR6P.js.map → chunk-JECBQBEX.js.map} +0 -0
  886. /package/dist/{chunk-E6CBU7XP.js.map → chunk-JYIFGGEM.js.map} +0 -0
  887. /package/dist/{chunk-VA4WSW54.js.map → chunk-JYIJNNN3.js.map} +0 -0
  888. /package/dist/{chunk-L5HJQC36.js.map → chunk-K2TDK7GG.js.map} +0 -0
  889. /package/dist/{chunk-KDXVOFYY.js.map → chunk-KAVBJSBQ.js.map} +0 -0
  890. /package/dist/{chunk-4OKLXWDZ.js.map → chunk-KNB7IBBU.js.map} +0 -0
  891. /package/dist/{chunk-QLULYKWZ.js.map → chunk-KSXJOC4F.js.map} +0 -0
  892. /package/dist/{chunk-2S6QHZGJ.js.map → chunk-L2DPF4DF.js.map} +0 -0
  893. /package/dist/{chunk-JI5AVXYO.js.map → chunk-LEPOP46W.js.map} +0 -0
  894. /package/dist/{chunk-O7BBFJLP.js.map → chunk-LTAVSOZW.js.map} +0 -0
  895. /package/dist/{chunk-2IZAGAJR.js.map → chunk-LULR4IUW.js.map} +0 -0
  896. /package/dist/{chunk-WMWVO45V.js.map → chunk-MC6XO7T4.js.map} +0 -0
  897. /package/dist/{chunk-D6RIFG4O.js.map → chunk-MNB5RMAY.js.map} +0 -0
  898. /package/dist/{chunk-Q5WG43OP.js.map → chunk-MYFQ3ED4.js.map} +0 -0
  899. /package/dist/{chunk-V3UE5I5F.js.map → chunk-MYVPLJOO.js.map} +0 -0
  900. /package/dist/{chunk-MTWKDOVO.js.map → chunk-NLE6TSLG.js.map} +0 -0
  901. /package/dist/{chunk-DBRYEIGY.js.map → chunk-OJP3N7UK.js.map} +0 -0
  902. /package/dist/{chunk-SMX5267P.js.map → chunk-OZKOPQCM.js.map} +0 -0
  903. /package/dist/{chunk-JCK7V6SN.js.map → chunk-P65YJEEL.js.map} +0 -0
  904. /package/dist/{chunk-MTR7BZQN.js.map → chunk-PBGWYM3H.js.map} +0 -0
  905. /package/dist/{chunk-AXTXN4T6.js.map → chunk-PWFCHOB3.js.map} +0 -0
  906. /package/dist/{chunk-FDPZVY6D.js.map → chunk-QFN4U7AG.js.map} +0 -0
  907. /package/dist/{chunk-K2ZLHESQ.js.map → chunk-QIEUU4UK.js.map} +0 -0
  908. /package/dist/{chunk-3D6AFGQR.js.map → chunk-QVT3E4CE.js.map} +0 -0
  909. /package/dist/{chunk-SFQ6QNL7.js.map → chunk-R6YJI3HE.js.map} +0 -0
  910. /package/dist/{chunk-5CJVLG3H.js.map → chunk-RFDD3B7W.js.map} +0 -0
  911. /package/dist/{chunk-JBPKEARU.js.map → chunk-RKNJBZ55.js.map} +0 -0
  912. /package/dist/{chunk-FNZQGN76.js.map → chunk-S2BBISHU.js.map} +0 -0
  913. /package/dist/{chunk-IALLX7EE.js.map → chunk-S2FSBU4H.js.map} +0 -0
  914. /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
  915. /package/dist/{chunk-DPAENM7O.js.map → chunk-U3ZAVRP3.js.map} +0 -0
  916. /package/dist/{chunk-4KJLYTLS.js.map → chunk-V57NLO5P.js.map} +0 -0
  917. /package/dist/{chunk-FLVL4L3L.js.map → chunk-VDX2J7OX.js.map} +0 -0
  918. /package/dist/{chunk-AJLLIIML.js.map → chunk-W4FBGYGK.js.map} +0 -0
  919. /package/dist/{chunk-6SXVCD7W.js.map → chunk-W7IGLX4G.js.map} +0 -0
  920. /package/dist/{chunk-KAWTOYAW.js.map → chunk-WLZYGLJ4.js.map} +0 -0
  921. /package/dist/{chunk-RO4LQGT3.js.map → chunk-WON4APQY.js.map} +0 -0
  922. /package/dist/{chunk-PQVK2S3J.js.map → chunk-WZFAPHIK.js.map} +0 -0
  923. /package/dist/{chunk-KQPJLK3X.js.map → chunk-XL24QQ63.js.map} +0 -0
  924. /package/dist/{chunk-RVZRYUQH.js.map → chunk-YROFNEG5.js.map} +0 -0
  925. /package/dist/{chunk-VRAD2FGO.js.map → chunk-Z54EM5YZ.js.map} +0 -0
  926. /package/dist/{first-start-migration-TI67YMLG.js.map → first-start-migration-CSRHCJIQ.js.map} +0 -0
  927. /package/dist/{forget-CQCLB5LB.js.map → forget-HRPLVB3W.js.map} +0 -0
  928. /package/dist/{purge-NDWV6WYD.js.map → purge-IRF47ZTX.js.map} +0 -0
  929. /package/dist/{tier-stats-KV7IWCJR.js.map → tier-stats-QEJSPFV2.js.map} +0 -0
@@ -0,0 +1,1200 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import test, { mock } from "node:test";
7
+
8
+ import { ContentHashIndex, FactHashIndexNotAuthoritativeError, StorageManager } from "./storage.js";
9
+ import type { MemoryFile } from "./types.js";
10
+
11
+ // Issue #1909 (Part B): writeMemory("fact") used to rewrite the whole
12
+ // fact-hash index (which grows with corpus size) on EVERY fact, even though
13
+ // the extraction persist path already batch-saves the authoritative superset
14
+ // once at the end. The
15
+ // `deferHashIndexSave` option marks the index dirty but skips the per-fact
16
+ // flush; single-write callers keep the immediate, crash-safe save.
17
+
18
+ async function withMemoryDir(run: (dir: string) => Promise<void>): Promise<void> {
19
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-hash-defer-"));
20
+ try {
21
+ await run(dir);
22
+ } finally {
23
+ await rm(dir, { recursive: true, force: true });
24
+ }
25
+ }
26
+
27
+ test("deferred fact writes do NO per-fact index saves; a single flush persists all", async () => {
28
+ await withMemoryDir(async (dir) => {
29
+ const saveSpy = mock.method(ContentHashIndex.prototype, "save");
30
+ try {
31
+ const storage = new StorageManager(dir);
32
+
33
+ // Write 10 facts with the defer flag — the per-fact hot path in
34
+ // extraction persist. None of them should flush the index.
35
+ for (let i = 0; i < 10; i += 1) {
36
+ await storage.writeMemory("fact", `deferred fact number ${i}`, {
37
+ source: "extraction",
38
+ deferHashIndexSave: true,
39
+ });
40
+ }
41
+ assert.equal(
42
+ saveSpy.mock.callCount(),
43
+ 0,
44
+ "deferred fact writes must not flush the index per fact (was 10 flushes)",
45
+ );
46
+
47
+ // All 10 hashes are live in the in-memory index (dirty), so same-session
48
+ // dedup still works despite no disk flush.
49
+ for (let i = 0; i < 10; i += 1) {
50
+ assert.equal(await storage.hasFactContentHash(`deferred fact number ${i}`), true);
51
+ }
52
+
53
+ // One authoritative batch save (mirrors saveContentHashIndexes) persists
54
+ // the whole superset in a single rewrite.
55
+ saveSpy.mock.resetCalls();
56
+ const index = await (storage as unknown as {
57
+ getFactHashIndex: () => Promise<ContentHashIndex>;
58
+ }).getFactHashIndex.call(storage);
59
+ await index.save();
60
+ assert.equal(saveSpy.mock.callCount(), 1, "exactly one batch save, down from 10+");
61
+ } finally {
62
+ saveSpy.mock.restore();
63
+ }
64
+
65
+ // A fresh session (new StorageManager) sees every hash on disk.
66
+ const reopened = new StorageManager(dir);
67
+ for (let i = 0; i < 10; i += 1) {
68
+ assert.equal(
69
+ await reopened.hasFactContentHash(`deferred fact number ${i}`),
70
+ true,
71
+ "batch-saved hashes survive into a new session",
72
+ );
73
+ }
74
+ });
75
+ });
76
+
77
+ test("direct writeMemory('fact') without the flag persists the hash via the LOCKED reconcile", async () => {
78
+ // PR #2016 thread SDyCk: a direct (non-deferred) fact write used to publish
79
+ // fact-hashes.txt with the unlocked whole-file save(), which can clobber — or
80
+ // be clobbered by — a peer's concurrent locked rebuild/reconcile. It now flushes
81
+ // through the SAME cross-process locked reconcile the batch/append/rebuild paths
82
+ // use (saveMergingWithDisk), never the unlocked save().
83
+ await withMemoryDir(async (dir) => {
84
+ const saveSpy = mock.method(ContentHashIndex.prototype, "save");
85
+ const reconcileSpy = mock.method(ContentHashIndex.prototype, "saveMergingWithDisk");
86
+ try {
87
+ const storage = new StorageManager(dir);
88
+ await storage.writeMemory("fact", "single write fact", { source: "manual" });
89
+ assert.ok(
90
+ reconcileSpy.mock.callCount() >= 1,
91
+ "single-write callers flush via the locked reconcile (saveMergingWithDisk)",
92
+ );
93
+ assert.equal(
94
+ saveSpy.mock.callCount(),
95
+ 0,
96
+ "the direct write must NOT use the unlocked whole-file save()",
97
+ );
98
+ assert.equal(await storage.hasFactContentHash("single write fact"), true);
99
+ } finally {
100
+ saveSpy.mock.restore();
101
+ reconcileSpy.mock.restore();
102
+ }
103
+
104
+ // No batch save was performed by the caller, yet a fresh session still finds
105
+ // the hash — it was flushed at write time (crash-safe).
106
+ const reopened = new StorageManager(dir);
107
+ assert.equal(await reopened.hasFactContentHash("single write fact"), true);
108
+ });
109
+ });
110
+
111
+ test("#2016 thread SDzOT: a fact hash added during the authoritative rebuild's corpus scan is not lost", async () => {
112
+ // The rebuild used to build a fresh `factOnly` set and publish it with
113
+ // `this.factOnlyHashes = factOnly`, dropping any concurrent in-process
114
+ // writeMemory that added to the LIVE set during the readAllMemories /
115
+ // readAllColdMemories awaits. The rebuild now repopulates the live set in place
116
+ // (clear + add), so a concurrent add survives publication — exactly as the
117
+ // shared index preserves concurrent adds by mutating its `hashes` set in place.
118
+ await withMemoryDir(async (dir) => {
119
+ const storage = new StorageManager(dir);
120
+ // One durable fact so the rebuild has corpus content to scan.
121
+ await storage.writeMemory("fact", "alpha established fact", { source: "extraction" });
122
+
123
+ const s = storage as unknown as {
124
+ factOnlyHashes: Set<string>;
125
+ factHashIndexAuthoritative: boolean | null;
126
+ readAllColdMemories: () => Promise<unknown[]>;
127
+ ensureFactHashIndexAuthoritative: () => Promise<boolean>;
128
+ };
129
+ const sentinel = "sentinel-concurrent-fact-hash";
130
+ const realCold = s.readAllColdMemories.bind(storage);
131
+ let injected = false;
132
+ // Interpose on the SECOND corpus await inside the rebuild (after clear()):
133
+ // simulate a concurrent writeMemory adding a fact hash to the live set while
134
+ // the rebuild is mid-scan.
135
+ s.readAllColdMemories = async () => {
136
+ const res = await realCold();
137
+ if (!injected) {
138
+ injected = true;
139
+ s.factOnlyHashes.add(sentinel);
140
+ }
141
+ return res;
142
+ };
143
+
144
+ // Force a fresh authoritative rebuild (clear -> scan -> publish).
145
+ s.factHashIndexAuthoritative = null;
146
+ assert.equal(await s.ensureFactHashIndexAuthoritative(), true, "rebuild published");
147
+
148
+ assert.equal(
149
+ s.factOnlyHashes.has(sentinel),
150
+ true,
151
+ "a fact hash added during the rebuild scan must survive publication (lost under reassign)",
152
+ );
153
+ // The corpus fact is present too — the in-place repopulate did not drop it.
154
+ assert.equal(await storage.hasFactContentHash("alpha established fact"), true);
155
+ });
156
+ });
157
+
158
+ test("#2016 thread PRRT_kwDORJXyws6SEBri: a hash added during rebuildUnderLock's save() overwrite is not lost", async () => {
159
+ // The plain overwrite save() materializes its body, then awaits disk. A
160
+ // same-process add() that lands during that await used to be dropped: save()
161
+ // unconditionally cleared `added` and set `dirty = false` after the write, so
162
+ // the late hash lived only in memory (never in fact-hashes.txt) with no retry
163
+ // armed. The delta-preserving consume now keeps late deltas pending and arms a
164
+ // durable reconcile retry. We drive the exact interleave deterministically by
165
+ // injecting the add() from the write-key provider, which save() invokes while
166
+ // evaluating writeMaybeEncryptedFile's arguments — AFTER the overwrite body is
167
+ // materialized but BEFORE the disk write resolves.
168
+ await withMemoryDir(async (dir) => {
169
+ const stateDir = path.join(dir, "state");
170
+ const lateContent = "late-arriving-fact-body";
171
+ const lateHash = ContentHashIndex.computeHash(lateContent);
172
+ let idx!: ContentHashIndex;
173
+ let injected = false;
174
+ const injectingWriteKeyProvider = (): Buffer | null => {
175
+ if (!injected) {
176
+ injected = true;
177
+ idx.add(lateContent);
178
+ }
179
+ return null;
180
+ };
181
+ idx = new ContentHashIndex(stateDir, () => null, injectingWriteKeyProvider);
182
+
183
+ // Publish an authoritative rebuild under the lock; save() runs inside it.
184
+ const published = await idx.rebuildUnderLock(async () => {
185
+ idx.clear();
186
+ idx.addByHash(ContentHashIndex.computeHash("corpus fact one"));
187
+ idx.addByHash(ContentHashIndex.computeHash("corpus fact two"));
188
+ });
189
+ assert.equal(published, true, "rebuild published under the lock");
190
+ assert.equal(injected, true, "the concurrent add fired during save()'s write window");
191
+
192
+ // Drive the deferred reconcile retry inline. On the buggy source save()
193
+ // cleared dirty and armed nothing, so this is a no-op and the late hash is
194
+ // gone; on the fixed source it republishes the pending delta onto disk.
195
+ await idx.flushReconcileRetry();
196
+
197
+ const fresh = new ContentHashIndex(stateDir);
198
+ await fresh.load();
199
+ assert.ok(
200
+ fresh.has(lateContent),
201
+ "a hash added during the locked rebuild save must reach disk (lost under the unconditional clear)",
202
+ );
203
+ // The rebuilt corpus set survives too — the late-delta handling did not drop it.
204
+ assert.ok(fresh.has("corpus fact one"), "corpus hash one survives the rebuild+reconcile");
205
+ assert.ok(fresh.has("corpus fact two"), "corpus hash two survives the rebuild+reconcile");
206
+ assert.equal(ContentHashIndex.computeHash(lateContent), lateHash);
207
+ });
208
+ });
209
+
210
+ test("promotion-style immediate write survives restart even with fact-hashes.ready present", async () => {
211
+ // Issue #1909 review finding 2: promotion writes (profile/shared) do NOT
212
+ // register with the orchestrator's batch save (no addContentHashDedup), so
213
+ // they must keep their immediate per-fact index save. If they deferred, the
214
+ // hash would be absent from fact-hashes.txt and — with fact-hashes.ready
215
+ // present — a restart would trust the on-disk index (no rebuild) and MISS the
216
+ // hash, re-creating the promoted fact on the next extraction.
217
+ await withMemoryDir(async (dir) => {
218
+ // Warm the index authoritative and create the .ready marker (empty backfill).
219
+ const warm = new StorageManager(dir);
220
+ assert.equal(await warm.hasFactContentHash("nothing yet"), false);
221
+ // Promotion-style write: no deferHashIndexSave → immediate index flush.
222
+ await warm.writeMemory("fact", "promoted profile fact", { source: "extraction" });
223
+
224
+ // A fresh session with .ready present trusts the on-disk index (no rebuild).
225
+ const restarted = new StorageManager(dir);
226
+ assert.equal(
227
+ await restarted.hasFactContentHash("promoted profile fact"),
228
+ true,
229
+ "the promoted hash is on disk and found after restart — no duplicate re-creation",
230
+ );
231
+ });
232
+ });
233
+
234
+ test("durability: deferred writes with NO batch save are rebuildable from on-disk facts (fail-open)", async () => {
235
+ await withMemoryDir(async (dir) => {
236
+ // Simulate a crash mid-extraction: facts written with deferHashIndexSave but
237
+ // the process dies before saveContentHashIndexes runs. The fact-hash file is
238
+ // never written, but the fact .md files ARE on disk.
239
+ {
240
+ const storage = new StorageManager(dir);
241
+ for (let i = 0; i < 5; i += 1) {
242
+ await storage.writeMemory("fact", `crash fact ${i}`, {
243
+ source: "extraction",
244
+ deferHashIndexSave: true,
245
+ });
246
+ }
247
+ // Intentionally no batch save — the crash window.
248
+ }
249
+
250
+ // A new session rebuilds the index authoritatively from the on-disk facts
251
+ // (ensureFactHashIndexAuthoritative), so dedup is correct and no data is lost.
252
+ const recovered = new StorageManager(dir);
253
+ for (let i = 0; i < 5; i += 1) {
254
+ assert.equal(
255
+ await recovered.hasFactContentHash(`crash fact ${i}`),
256
+ true,
257
+ "a lost hash is rebuilt from the durable fact corpus — never a data loss",
258
+ );
259
+ }
260
+ });
261
+ });
262
+
263
+ test("concurrent deferred fact writes both land in the index (no lost update)", async () => {
264
+ await withMemoryDir(async (dir) => {
265
+ const storage = new StorageManager(dir);
266
+ await Promise.all([
267
+ storage.writeMemory("fact", "concurrent alpha", {
268
+ source: "extraction",
269
+ deferHashIndexSave: true,
270
+ }),
271
+ storage.writeMemory("fact", "concurrent beta", {
272
+ source: "extraction",
273
+ deferHashIndexSave: true,
274
+ }),
275
+ ]);
276
+ const index = await (storage as unknown as {
277
+ getFactHashIndex: () => Promise<ContentHashIndex>;
278
+ }).getFactHashIndex.call(storage);
279
+ await index.save();
280
+
281
+ const reopened = new StorageManager(dir);
282
+ assert.equal(await reopened.hasFactContentHash("concurrent alpha"), true);
283
+ assert.equal(await reopened.hasFactContentHash("concurrent beta"), true);
284
+ });
285
+ });
286
+
287
+ test("#1909 round 11: a deferred write with no batch save is rebuilt from the corpus on restart", async () => {
288
+ // No fact-hashes.ready marker exists anymore — the fact-hash index is ALWAYS
289
+ // rebuilt from the durable fact corpus on first use per process. So a deferred
290
+ // write that never got flushed (crash before the batch save) is still deduped
291
+ // after restart because the fact .md is on disk and the rebuild includes it.
292
+ await withMemoryDir(async (dir) => {
293
+ const readyPath = path.join(dir, "state", "fact-hashes.ready");
294
+ const warm = new StorageManager(dir);
295
+ await warm.writeMemory("fact", "windowed fact", {
296
+ source: "extraction",
297
+ deferHashIndexSave: true,
298
+ });
299
+ // CRASH: no batch save. Drop the instance.
300
+ assert.equal(existsSync(readyPath), false, "no ready marker is ever written (round 11)");
301
+
302
+ const restarted = new StorageManager(dir);
303
+ assert.equal(
304
+ await restarted.hasFactContentHash("windowed fact"),
305
+ true,
306
+ "the fact is rebuilt from the corpus — never lost from dedup",
307
+ );
308
+ });
309
+ });
310
+
311
+ test("#1909 round 11: the fact-hash ready marker is never written (always rebuild on start)", async () => {
312
+ await withMemoryDir(async (dir) => {
313
+ const readyPath = path.join(dir, "state", "fact-hashes.ready");
314
+ const storage = new StorageManager(dir);
315
+ await storage.writeMemory("fact", "a durable fact", { source: "manual" });
316
+ assert.equal(await storage.hasFactContentHash("a durable fact"), true); // triggers rebuild
317
+ assert.equal(existsSync(readyPath), false, "no ready marker written after a rebuild");
318
+
319
+ // A fresh instance rebuilds authoritatively from the corpus and still dedups.
320
+ const restarted = new StorageManager(dir);
321
+ assert.equal(await restarted.hasFactContentHash("a durable fact"), true);
322
+ assert.equal(existsSync(readyPath), false, "still no marker after the restart rebuild");
323
+ });
324
+ });
325
+
326
+ test("#1909: saveMergingWithDisk unions with a concurrent writer instead of clobbering", async () => {
327
+ // Review round 6 finding 2: two independent index instances (two processes)
328
+ // that both snapshot an empty file must not clobber each other on save.
329
+ await withMemoryDir(async (dir) => {
330
+ const stateDir = path.join(dir, "state");
331
+ await mkdir(stateDir, { recursive: true });
332
+ const a = new ContentHashIndex(stateDir);
333
+ const b = new ContentHashIndex(stateDir);
334
+ await a.load();
335
+ await b.load();
336
+ a.add("interleaved fact A");
337
+ b.add("interleaved fact B");
338
+
339
+ // Interleave the saves: a blind whole-file overwrite by B would drop A's
340
+ // hash; the union-merge preserves both regardless of order.
341
+ await a.saveMergingWithDisk();
342
+ await b.saveMergingWithDisk();
343
+
344
+ const fresh = new ContentHashIndex(stateDir);
345
+ await fresh.load();
346
+ assert.ok(fresh.has("interleaved fact A"), "A's hash preserved after B's concurrent save");
347
+ assert.ok(fresh.has("interleaved fact B"), "B's hash preserved");
348
+ });
349
+ });
350
+
351
+ test("#1909: concurrent (parallel) merge-saves serialize via the file lock — both survive", async () => {
352
+ // Review round 7 finding 2: the per-file advisory lock serializes the
353
+ // read-union→write window across truly concurrent writers (here two instances
354
+ // saving in parallel), closing the TOCTOU where both read {prior} then both
355
+ // atomically replace and one drop wins.
356
+ await withMemoryDir(async (dir) => {
357
+ const stateDir = path.join(dir, "state");
358
+ await mkdir(stateDir, { recursive: true });
359
+ const a = new ContentHashIndex(stateDir);
360
+ const b = new ContentHashIndex(stateDir);
361
+ await a.load();
362
+ await b.load();
363
+ a.add("parallel fact A");
364
+ b.add("parallel fact B");
365
+
366
+ await Promise.all([a.saveMergingWithDisk(), b.saveMergingWithDisk()]);
367
+
368
+ const fresh = new ContentHashIndex(stateDir);
369
+ await fresh.load();
370
+ assert.ok(fresh.has("parallel fact A"), "A survived the concurrent publish");
371
+ assert.ok(fresh.has("parallel fact B"), "B survived the concurrent publish");
372
+ });
373
+ });
374
+
375
+ test("#1909: removing a hash and reconcile-saving drops it from disk (no resurrection)", async () => {
376
+ // Review round 7/8: a removal must never resurrect. The reconciling save is
377
+ // removal-aware — it subtracts removed hashes from the latest on-disk state
378
+ // rather than blindly unioning the pre-removal contents back in.
379
+ await withMemoryDir(async (dir) => {
380
+ const stateDir = path.join(dir, "state");
381
+ await mkdir(stateDir, { recursive: true });
382
+ const idx = new ContentHashIndex(stateDir);
383
+ await idx.load();
384
+ idx.add("archived fact body");
385
+ await idx.save();
386
+ const mid = new ContentHashIndex(stateDir);
387
+ await mid.load();
388
+ assert.ok(mid.has("archived fact body"), "hash is on disk before archival");
389
+
390
+ // Archival: remove + reconciling save (the production saveContentHashIndexes path).
391
+ idx.remove("archived fact body");
392
+ await idx.saveMergingWithDisk();
393
+
394
+ const fresh = new ContentHashIndex(stateDir);
395
+ await fresh.load();
396
+ assert.equal(
397
+ fresh.has("archived fact body"),
398
+ false,
399
+ "removed hash stays gone → re-extraction of that content is allowed",
400
+ );
401
+ });
402
+ });
403
+
404
+ test("#1909: reconciling removal drops the removed hash AND preserves a concurrent append", async () => {
405
+ // Review round 8 thread 3: removal and append batches serialize under the same
406
+ // per-file lock and both reconcile with the latest on-disk state — so a removal
407
+ // neither resurrects its own hash nor clobbers a concurrent extraction's append.
408
+ await withMemoryDir(async (dir) => {
409
+ const stateDir = path.join(dir, "state");
410
+ await mkdir(stateDir, { recursive: true });
411
+ const seed = new ContentHashIndex(stateDir);
412
+ await seed.load();
413
+ seed.add("to be archived");
414
+ await seed.save();
415
+
416
+ // Remover loads {archived} and marks it removed (not yet published).
417
+ const remover = new ContentHashIndex(stateDir);
418
+ await remover.load();
419
+ remover.remove("to be archived");
420
+
421
+ // Concurrent appender (another instance) adds a new hash and publishes first.
422
+ const appender = new ContentHashIndex(stateDir);
423
+ await appender.load();
424
+ appender.add("freshly appended");
425
+ await appender.saveMergingWithDisk();
426
+
427
+ // Remover reconciles against the now-updated disk: drops its removal, keeps
428
+ // the concurrent append.
429
+ await remover.saveMergingWithDisk();
430
+
431
+ const fresh = new ContentHashIndex(stateDir);
432
+ await fresh.load();
433
+ assert.equal(fresh.has("to be archived"), false, "removed hash dropped — no resurrection");
434
+ assert.ok(fresh.has("freshly appended"), "concurrent append preserved — no clobber");
435
+ });
436
+ });
437
+
438
+ test("#1909: saveMergingWithDisk is a no-op when the index is not dirty (dirty short-circuit)", async () => {
439
+ // Review round 7 finding 3: a no-fact/deduped run must not re-read+rewrite the
440
+ // whole fact-hashes.txt. An unmutated index skips the O(file) work entirely.
441
+ await withMemoryDir(async (dir) => {
442
+ const stateDir = path.join(dir, "state");
443
+ await mkdir(stateDir, { recursive: true });
444
+ const seed = new ContentHashIndex(stateDir);
445
+ await seed.load();
446
+ seed.add("seed fact");
447
+ await seed.save();
448
+
449
+ // A fresh instance that only LOADS (adds nothing) is not dirty → merge-save
450
+ // writes nothing. Prove it by making a foreign writer append, then a clean
451
+ // merge-save must NOT clobber that foreign row.
452
+ const clean = new ContentHashIndex(stateDir);
453
+ await clean.load();
454
+ const foreign = new ContentHashIndex(stateDir);
455
+ await foreign.load();
456
+ foreign.add("foreign fact");
457
+ await foreign.save();
458
+
459
+ await clean.saveMergingWithDisk(); // not dirty → must be a no-op
460
+
461
+ const fresh = new ContentHashIndex(stateDir);
462
+ await fresh.load();
463
+ assert.ok(fresh.has("seed fact"));
464
+ assert.ok(fresh.has("foreign fact"), "a non-dirty merge-save did not rewrite/clobber the file");
465
+ });
466
+ });
467
+
468
+ // Issue #1909 review round 14: the markerless authoritative rebuild
469
+ // (ensureFactHashIndexAuthoritative) used to read ONLY the hot tier
470
+ // (readAllMemories). A fact or procedure demoted to the cold tier is still
471
+ // active, so a restart dropped its content-hash from the dedup index and the
472
+ // next extraction re-created the demoted memory. The rebuild now unions the hot
473
+ // AND cold tiers (readAllColdMemories), so hashes survive a hot/cold restart.
474
+
475
+ test("#1909: rebuild indexes ACTIVE cold-tier facts; a demoted fact's hash survives restart", async () => {
476
+ await withMemoryDir(async (dir) => {
477
+ StorageManager.clearAllStaticCaches();
478
+ const storage = new StorageManager(dir);
479
+
480
+ // Two facts: one stays hot, one is demoted to cold.
481
+ await storage.writeMemory("fact", "hot tier fact stays put", { source: "manual" });
482
+ await storage.writeMemory("fact", "cold tier fact was demoted", { source: "manual" });
483
+
484
+ storage.invalidateAllMemoriesCacheForDir();
485
+ const demoted = (await storage.readAllMemories()).find(
486
+ (m) => m.content.includes("cold tier fact was demoted"),
487
+ );
488
+ assert.ok(demoted, "the fact to demote must be readable in hot before migration");
489
+ await storage.migrateMemoryToTier(demoted!, "cold");
490
+
491
+ // Simulate a process restart: drop every static/in-memory cache and open a
492
+ // fresh StorageManager over the same baseDir. Its first dedup lookup forces
493
+ // a full corpus rebuild with no marker to trust.
494
+ StorageManager.clearAllStaticCaches();
495
+ const reopened = new StorageManager(dir);
496
+
497
+ // The cold-demoted fact must be confirmed present in the rebuilt index (this
498
+ // FAILS on a hot-only rebuild), and the hot fact must still be present too.
499
+ assert.equal(
500
+ await reopened.hasFactContentHash("cold tier fact was demoted"),
501
+ true,
502
+ "a fact demoted to cold must survive the corpus rebuild (hot+cold union)",
503
+ );
504
+ assert.equal(
505
+ await reopened.hasFactContentHash("hot tier fact stays put"),
506
+ true,
507
+ "the hot-tier fact must also survive the rebuild",
508
+ );
509
+ });
510
+ });
511
+
512
+ // PR #2016: when saveMergingWithDisk cannot acquire the cross-process lock in
513
+ // time, it used to resolve "successfully" while retaining the added hash ONLY
514
+ // in this process's memory — a silent gap. A long-lived peer that already built
515
+ // its authoritative in-memory index would never see the write, and no later
516
+ // save was guaranteed. The fix schedules a bounded, durable background retry
517
+ // that keeps re-attempting the locked publish until the addition lands.
518
+ //
519
+ // These tests hold the advisory lock deterministically (write the <index>.lock
520
+ // file with a fresh mtime so it is neither acquirable nor stale-breakable within
521
+ // the short test window) and use small injected lock/retry timings.
522
+ //
523
+ // ts-no-test-timers exception (same rationale as serialize-mutations.test.ts):
524
+ // the retry is driven by a REAL setTimeout coupled to withHeldFileLock's REAL
525
+ // filesystem mtime-staleness + poll loop — fake timers cannot advance the fs
526
+ // lock's platform clock, so time control here must be real. We never guess a
527
+ // duration: each test awaits the code's own `whenReconcileRetrySettled()`
528
+ // signal. The retry timer is `unref`'d in production so a pending retry never
529
+ // keeps a daemon alive, which means an awaiting test needs a ref'd keep-alive
530
+ // interval to hold the event loop open until that signal fires; it is cleared
531
+ // the instant the retry chain settles.
532
+
533
+ async function withRetryLoopAlive(run: () => Promise<void>): Promise<void> {
534
+ const keepAlive = setInterval(() => {}, 1000);
535
+ try {
536
+ await run();
537
+ } finally {
538
+ clearInterval(keepAlive);
539
+ }
540
+ }
541
+
542
+ test("#2016: a lock-timed-out append is NOT silently dropped and eventually persists via durable retry", async () => {
543
+ await withMemoryDir(async (dir) => {
544
+ const stateDir = path.join(dir, "state");
545
+ await mkdir(stateDir, { recursive: true });
546
+ const lockPath = path.join(stateDir, "fact-hashes.txt.lock");
547
+
548
+ // A peer holds the lock: a fresh, non-stale lock file the acquirer cannot take.
549
+ await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
550
+
551
+ const idx = new ContentHashIndex(stateDir, undefined, undefined, undefined, {
552
+ maxWaitMs: 50,
553
+ pollMs: 10,
554
+ retryBaseMs: 15,
555
+ retryMaxAttempts: 5,
556
+ });
557
+ await idx.load();
558
+ idx.add("deferred-under-contended-lock");
559
+
560
+ // The batch save times out on the lock. It must NOT publish (peer's lock is
561
+ // held) but MUST arm a durable retry instead of resolving as "complete".
562
+ await idx.saveMergingWithDisk();
563
+ assert.equal(idx.hasPendingReconcileRetry, true, "a durable retry must be armed after the lock timeout");
564
+
565
+ // Disk is untouched while the lock is held — proves it did not write unlocked.
566
+ const early = new ContentHashIndex(stateDir);
567
+ await early.load();
568
+ assert.equal(early.has("deferred-under-contended-lock"), false, "no unlocked publish while the peer holds the lock");
569
+ // In-memory dedup still works this session (dirty addition retained).
570
+ assert.equal(idx.has("deferred-under-contended-lock"), true, "addition stays live in-memory");
571
+
572
+ // The peer releases the lock; the background retry must then land the write.
573
+ await rm(lockPath, { force: true });
574
+ await withRetryLoopAlive(() => idx.whenReconcileRetrySettled());
575
+
576
+ assert.equal(idx.hasPendingReconcileRetry, false, "retry chain settled after publishing");
577
+ const fresh = new ContentHashIndex(stateDir);
578
+ await fresh.load();
579
+ assert.ok(
580
+ fresh.has("deferred-under-contended-lock"),
581
+ "the deferred addition eventually reached disk via the durable retry",
582
+ );
583
+ });
584
+ });
585
+
586
+ test("#2016: exhausting retries under a permanently held lock is best-effort, never a fatal append failure", async () => {
587
+ await withMemoryDir(async (dir) => {
588
+ const stateDir = path.join(dir, "state");
589
+ await mkdir(stateDir, { recursive: true });
590
+ const lockPath = path.join(stateDir, "fact-hashes.txt.lock");
591
+ await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
592
+
593
+ const idx = new ContentHashIndex(stateDir, undefined, undefined, undefined, {
594
+ maxWaitMs: 25,
595
+ pollMs: 10,
596
+ retryBaseMs: 10,
597
+ retryMaxAttempts: 2,
598
+ });
599
+ await idx.load();
600
+ idx.add("never-persistable-while-locked");
601
+
602
+ // The save resolves (best-effort) — a busy lock must never throw and turn a
603
+ // durable-.md append into a fatal failure.
604
+ await assert.doesNotReject(idx.saveMergingWithDisk());
605
+
606
+ // Let the bounded retry chain run to exhaustion (lock is never released).
607
+ await withRetryLoopAlive(() => idx.whenReconcileRetrySettled());
608
+
609
+ assert.equal(idx.hasPendingReconcileRetry, false, "retries are bounded — the chain gives up, it does not spin forever");
610
+ // The addition is still held in-memory (dedup stays correct this session);
611
+ // disk stays empty, so the corpus-rebuild-on-restart safety net covers it.
612
+ assert.equal(idx.has("never-persistable-while-locked"), true, "addition retained in-memory after giving up");
613
+ const fresh = new ContentHashIndex(stateDir);
614
+ await fresh.load();
615
+ assert.equal(fresh.has("never-persistable-while-locked"), false, "nothing written unlocked while the lock stayed held");
616
+ });
617
+ });
618
+
619
+ test("#2016: successive lock-timed-out saves do not stack duplicate retries (reentrancy guard)", async () => {
620
+ await withMemoryDir(async (dir) => {
621
+ const stateDir = path.join(dir, "state");
622
+ await mkdir(stateDir, { recursive: true });
623
+ const lockPath = path.join(stateDir, "fact-hashes.txt.lock");
624
+ await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
625
+
626
+ const idx = new ContentHashIndex(stateDir, undefined, undefined, undefined, {
627
+ maxWaitMs: 40,
628
+ pollMs: 10,
629
+ retryBaseMs: 500, // long enough that neither timed-out save fires the retry mid-test
630
+ retryMaxAttempts: 5,
631
+ });
632
+ await idx.load();
633
+ idx.add("first-deferred");
634
+ await idx.saveMergingWithDisk();
635
+ assert.equal(idx.hasPendingReconcileRetry, true, "first timeout arms exactly one retry");
636
+
637
+ // A second timed-out save while a retry is already armed must be a no-op for
638
+ // scheduling — the single armed timer is the reentrancy guard.
639
+ idx.add("second-deferred");
640
+ await idx.saveMergingWithDisk();
641
+ assert.equal(idx.hasPendingReconcileRetry, true, "still exactly one retry armed (no duplicate/parallel retries)");
642
+
643
+ // Release the lock and let the single retry drain BOTH deferred additions in
644
+ // one locked publish.
645
+ await rm(lockPath, { force: true });
646
+ await withRetryLoopAlive(() => idx.whenReconcileRetrySettled());
647
+
648
+ const fresh = new ContentHashIndex(stateDir);
649
+ await fresh.load();
650
+ assert.ok(fresh.has("first-deferred"), "first deferred addition persisted");
651
+ assert.ok(fresh.has("second-deferred"), "second deferred addition persisted by the same single retry");
652
+ });
653
+ });
654
+
655
+ // PR #2016 (startup hash-index rebuild race): ensureFactHashIndexAuthoritative
656
+ // used to run its corpus scan and publish with an UNLOCKED clear→scan→save().
657
+ // A peer process that committed a newer hash to fact-hashes.txt under the
658
+ // per-file lock — or landed a deferred reconcile-retry — in the window between
659
+ // the rebuild's corpus scan and its overwrite was silently clobbered, and the
660
+ // in-process authoritative flag then suppressed any further rebuild that
661
+ // session. The rebuild now runs its scan + publish under the SAME per-file lock
662
+ // the reconciling saves use (ContentHashIndex.rebuildUnderLock): a concurrent
663
+ // locked writer serializes against it and its addition is preserved (reconciled
664
+ // on top), and a contended lock defers the rebuild rather than publishing an
665
+ // unlocked overwrite. Same real-timer rationale as the retry tests above.
666
+
667
+ test("#2016: rebuildUnderLock publishes the rebuilt set under an uncontended lock", async () => {
668
+ await withMemoryDir(async (dir) => {
669
+ const stateDir = path.join(dir, "state");
670
+ await mkdir(stateDir, { recursive: true });
671
+ const idx = new ContentHashIndex(stateDir);
672
+ await idx.load();
673
+
674
+ const published = await idx.rebuildUnderLock(async () => {
675
+ idx.clear();
676
+ idx.addByHash(ContentHashIndex.computeHash("rebuilt-fact"));
677
+ });
678
+ assert.equal(published, true, "rebuild published under an uncontended lock");
679
+
680
+ const fresh = new ContentHashIndex(stateDir);
681
+ await fresh.load();
682
+ assert.ok(fresh.has("rebuilt-fact"), "the rebuilt hash reached disk under the lock");
683
+ });
684
+ });
685
+
686
+ test("#2016: rebuildUnderLock refuses to publish while a peer holds the lock (no unlocked overwrite)", async () => {
687
+ await withMemoryDir(async (dir) => {
688
+ const stateDir = path.join(dir, "state");
689
+ await mkdir(stateDir, { recursive: true });
690
+ // A peer already committed a hash to disk.
691
+ const peerHash = ContentHashIndex.computeHash("peer-committed-fact");
692
+ await writeFile(path.join(stateDir, "fact-hashes.txt"), `${peerHash}\n`);
693
+ // The peer holds the advisory lock (fresh, non-stale, not stale-breakable).
694
+ const lockPath = path.join(stateDir, "fact-hashes.txt.lock");
695
+ await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
696
+
697
+ const idx = new ContentHashIndex(stateDir, undefined, undefined, undefined, {
698
+ maxWaitMs: 50,
699
+ pollMs: 10,
700
+ });
701
+ await idx.load();
702
+ let populateRan = false;
703
+ const published = await idx.rebuildUnderLock(async () => {
704
+ populateRan = true;
705
+ idx.clear();
706
+ idx.addByHash(ContentHashIndex.computeHash("rebuild-would-clobber"));
707
+ });
708
+ assert.equal(published, false, "rebuild does not publish when the lock is contended");
709
+ assert.equal(populateRan, false, "populate never runs without the lock — no half-cleared state");
710
+
711
+ // Disk is untouched: the peer's committed hash is preserved and the
712
+ // rebuild's set was NOT written unlocked.
713
+ const disk = new ContentHashIndex(stateDir);
714
+ await disk.load();
715
+ assert.ok(disk.has("peer-committed-fact"), "peer's committed hash preserved (not clobbered)");
716
+ assert.equal(disk.has("rebuild-would-clobber"), false, "no unlocked overwrite while the lock is held");
717
+ // The rebuilder's in-memory index is non-destructive (still the loaded set).
718
+ assert.ok(idx.has("peer-committed-fact"), "loaded set retained in-memory after a deferred rebuild");
719
+ });
720
+ });
721
+
722
+ test("#2016: a rebuild serializes with a concurrent locked writer — the peer's addition is preserved, not clobbered", async () => {
723
+ await withMemoryDir(async (dir) => {
724
+ const stateDir = path.join(dir, "state");
725
+ await mkdir(stateDir, { recursive: true });
726
+
727
+ const rebuilder = new ContentHashIndex(stateDir);
728
+ await rebuilder.load();
729
+ const peer = new ContentHashIndex(stateDir, undefined, undefined, undefined, {
730
+ maxWaitMs: 50,
731
+ pollMs: 10,
732
+ retryBaseMs: 15,
733
+ retryMaxAttempts: 5,
734
+ });
735
+ await peer.load();
736
+ peer.add("peer-append-during-rebuild");
737
+
738
+ let peerBlockedDuringRebuild = false;
739
+ const published = await rebuilder.rebuildUnderLock(async () => {
740
+ rebuilder.clear();
741
+ rebuilder.addByHash(ContentHashIndex.computeHash("rebuilt-corpus-fact"));
742
+ // While the rebuild holds the lock the peer's locked publish must NOT be
743
+ // able to interleave: it times out and arms a durable retry instead of
744
+ // writing. This is also the deadlock-freedom proof — the non-reentrant
745
+ // file lock yields acquired=false rather than blocking forever.
746
+ await peer.saveMergingWithDisk();
747
+ peerBlockedDuringRebuild = peer.hasPendingReconcileRetry;
748
+ });
749
+ assert.equal(published, true, "the rebuild published under the lock");
750
+ assert.equal(
751
+ peerBlockedDuringRebuild,
752
+ true,
753
+ "the peer could not acquire the lock while the rebuild held it (serialized)",
754
+ );
755
+
756
+ // The peer's deferred retry lands after the rebuild releases the lock and
757
+ // reconciles ON TOP of the freshly-published set — losing neither hash.
758
+ await withRetryLoopAlive(() => peer.whenReconcileRetrySettled());
759
+
760
+ const fresh = new ContentHashIndex(stateDir);
761
+ await fresh.load();
762
+ assert.ok(fresh.has("rebuilt-corpus-fact"), "the rebuild's set was published");
763
+ assert.ok(
764
+ fresh.has("peer-append-during-rebuild"),
765
+ "the peer's concurrent addition survived — reconciled on top of the rebuild, never clobbered",
766
+ );
767
+ });
768
+ });
769
+
770
+ test("#2016: a peer's committed fact survives a fresh-session authoritative rebuild (multi-process)", async () => {
771
+ await withMemoryDir(async (dir) => {
772
+ // Process A: writes a fact (real .md + committed hash).
773
+ const peer = new StorageManager(dir);
774
+ await peer.writeMemory("fact", "durable multiprocess fact", { source: "extraction" });
775
+
776
+ // Process B: a fresh session rebuilds authoritatively under the lock and
777
+ // still dedups the peer's fact (corpus scan + locked publish through
778
+ // ensureFactHashIndexAuthoritative → rebuildUnderLock).
779
+ const rebuilder = new StorageManager(dir);
780
+ assert.equal(
781
+ await rebuilder.hasFactContentHash("durable multiprocess fact"),
782
+ true,
783
+ "the peer's fact is deduped after a fresh-session locked rebuild",
784
+ );
785
+ });
786
+ });
787
+
788
+ // PR #2016 findings 1-2 + 3: when the authoritative rebuild lock cannot be
789
+ // acquired, hasFactContentHash()/getAuthoritativeFactHashIndex() must NOT answer
790
+ // from the stale loaded snapshot as if it were authoritative; and a deferred
791
+ // lock-timeout append must be drainable inline at a short-lived writer's
792
+ // shutdown boundary rather than relying on the unref'd background timer.
793
+ //
794
+ // These tests hold the advisory lock deterministically (a fresh, non-stale
795
+ // <index>.lock the acquirer cannot take within the short window) and inject
796
+ // tight lock/retry budgets so the miss path is exercised without real
797
+ // multi-second waits. Same real-timer rationale as the retry tests above.
798
+
799
+ test("#2016 finding 1: a lock-contended miss is verified against the corpus, never a stale-snapshot false miss", async () => {
800
+ await withMemoryDir(async (dir) => {
801
+ // Process A writes a durable fact (.md corpus + fact-hashes.txt).
802
+ const writer = new StorageManager(dir);
803
+ await writer.writeMemory("fact", "durable fact under contention", { source: "extraction" });
804
+
805
+ // Drop the on-disk hash index so a fresh session's LOADED snapshot is EMPTY,
806
+ // but keep the durable .md — the corpus is now the only source of truth.
807
+ await rm(path.join(dir, "state", "fact-hashes.txt"), { force: true });
808
+
809
+ // A peer holds the rebuild lock so process B cannot become authoritative.
810
+ const lockPath = path.join(dir, "state", "fact-hashes.txt.lock");
811
+ await mkdir(path.dirname(lockPath), { recursive: true });
812
+ await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
813
+
814
+ try {
815
+ const reader = new StorageManager(dir);
816
+ reader.factHashIndexLockOptions = { maxWaitMs: 40, pollMs: 10, retryMaxAttempts: 2, retryBaseMs: 10 };
817
+
818
+ assert.equal(
819
+ await reader.isFactContentHashAuthoritative(),
820
+ false,
821
+ "index cannot be authoritative while the peer holds the rebuild lock",
822
+ );
823
+ assert.equal(
824
+ await reader.hasFactContentHash("durable fact under contention"),
825
+ true,
826
+ "a miss on the empty loaded snapshot is verified against the durable corpus — no false dedup miss",
827
+ );
828
+ assert.equal(
829
+ await reader.hasFactContentHash("was never written anywhere"),
830
+ false,
831
+ "a genuine miss is still a miss under contention (corpus confirms absence)",
832
+ );
833
+ } finally {
834
+ await rm(lockPath, { force: true });
835
+ }
836
+ });
837
+ });
838
+
839
+ test("#2016 finding 2: getAuthoritativeFactHashIndex fails explicitly instead of returning a non-authoritative index", async () => {
840
+ await withMemoryDir(async (dir) => {
841
+ const storage = new StorageManager(dir);
842
+ storage.factHashIndexLockOptions = { maxWaitMs: 40, pollMs: 10, retryMaxAttempts: 2, retryBaseMs: 10 };
843
+ const lockPath = path.join(dir, "state", "fact-hashes.txt.lock");
844
+ await mkdir(path.dirname(lockPath), { recursive: true });
845
+ await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
846
+
847
+ try {
848
+ await assert.rejects(
849
+ () => storage.getAuthoritativeFactHashIndex(),
850
+ (err: unknown) => err instanceof FactHashIndexNotAuthoritativeError,
851
+ "must throw rather than return a stale/non-authoritative index",
852
+ );
853
+ assert.equal(
854
+ await storage.isFactContentHashAuthoritative(),
855
+ false,
856
+ "the non-authoritative state is propagated, not masked as authoritative",
857
+ );
858
+ } finally {
859
+ await rm(lockPath, { force: true });
860
+ }
861
+
862
+ // Once the lock is free the rebuild publishes and the accessor returns.
863
+ const idx = await storage.getAuthoritativeFactHashIndex();
864
+ assert.ok(idx, "returns the authoritative index after the lock clears");
865
+ assert.equal(await storage.isFactContentHashAuthoritative(), true);
866
+ });
867
+ });
868
+
869
+ test("#2016 finding 3: flushReconcileRetry drains a deferred lock-timeout append to disk at shutdown", async () => {
870
+ await withMemoryDir(async (dir) => {
871
+ const stateDir = path.join(dir, "state");
872
+ await mkdir(stateDir, { recursive: true });
873
+ const lockPath = path.join(stateDir, "fact-hashes.txt.lock");
874
+ await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
875
+
876
+ const idx = new ContentHashIndex(stateDir, undefined, undefined, undefined, {
877
+ maxWaitMs: 30,
878
+ pollMs: 10,
879
+ retryBaseMs: 10,
880
+ retryMaxAttempts: 5,
881
+ });
882
+ await idx.load();
883
+ idx.add("deferred-drained-on-shutdown");
884
+ await idx.saveMergingWithDisk();
885
+ assert.equal(idx.hasPendingReconcileRetry, true, "the lock timeout armed a background retry");
886
+
887
+ // The peer releases the lock; a short-lived writer drains INLINE at its
888
+ // shutdown boundary instead of relying on the unref'd background timer.
889
+ await rm(lockPath, { force: true });
890
+ await idx.flushReconcileRetry();
891
+
892
+ assert.equal(idx.hasPendingReconcileRetry, false, "no lingering retry after the inline drain");
893
+ const fresh = new ContentHashIndex(stateDir);
894
+ await fresh.load();
895
+ assert.ok(
896
+ fresh.has("deferred-drained-on-shutdown"),
897
+ "the deferred hash reached disk via the inline shutdown drain",
898
+ );
899
+ });
900
+ });
901
+
902
+ // PR #2016 thread SDzOP: the storage-owned removal
903
+ // (removeFactContentHashesForMemories) and the approval re-add
904
+ // (addActiveFactContentHash) used to publish fact-hashes.txt with the unlocked
905
+ // whole-file save(). That republishes THIS instance's cached in-memory set, so a
906
+ // hash a peer appended to disk after this instance last synced — the exact
907
+ // concurrent-index window the reconcile path exists to close — is silently
908
+ // clobbered. Both paths now flush through the SAME cross-process locked,
909
+ // removal-aware reconcile (saveMergingWithDisk) the write/batch/rebuild paths
910
+ // use. This test reproduces the concurrent-append race deterministically: it
911
+ // re-baselines the cached index freshness fingerprint to the peer-advanced file
912
+ // so the removal keeps the authoritative fast path (no corpus rebuild) and its
913
+ // SAVE step alone is exercised — an unlocked save() drops the peer append, the
914
+ // locked reconcile keeps it.
915
+ test("#2016 thread SDzOP: a storage fact-hash removal reconciles under the lock, preserving a concurrent peer append", async () => {
916
+ await withMemoryDir(async (dir) => {
917
+ const stateDir = path.join(dir, "state");
918
+ const storage = new StorageManager(dir);
919
+
920
+ await storage.writeMemory("fact", "alpha stays active", { source: "manual" });
921
+ await storage.writeMemory("fact", "beta gets archived", { source: "manual" });
922
+
923
+ // Make the shared index authoritative and cached (the production hot path).
924
+ assert.equal(await storage.hasFactContentHash("alpha stays active"), true);
925
+ // Test-only access to storage/index internals: the shape is structurally
926
+ // known and a runtime check would be meaningless here (private members).
927
+ const storageInternals = storage as unknown as {
928
+ getFactHashIndex: () => Promise<ContentHashIndex>;
929
+ readAllMemories: () => Promise<MemoryFile[]>;
930
+ };
931
+ const idx = await storageInternals.getFactHashIndex.call(storage);
932
+
933
+ // Archive beta off disk so the removal actually drops its hash (it is no
934
+ // longer owned by an active corpus fact).
935
+ const all = await storageInternals.readAllMemories.call(storage);
936
+ const beta = all.find((m) => (m.content ?? "").includes("beta gets archived"));
937
+ assert.ok(beta, "beta fact must exist in the corpus");
938
+ await rm(beta!.path, { force: true });
939
+ storage.invalidateAllMemoriesCacheForDir();
940
+
941
+ // Concurrent index activity: a peer instance appends a NEW hash under the
942
+ // cross-process lock. The storage's cached index does not know about it.
943
+ const peer = new ContentHashIndex(stateDir);
944
+ await peer.load();
945
+ peer.add("peer concurrent append");
946
+ await peer.saveMergingWithDisk();
947
+
948
+ // Re-baseline the cached index freshness fingerprint to the peer-advanced
949
+ // file so ensureFactHashIndexAuthoritative keeps the fast path (no rebuild),
950
+ // isolating the removal's SAVE step — the exact code the reviewer flagged.
951
+ const idxInternals = idx as unknown as {
952
+ captureSyncedFingerprint: () => Promise<void>;
953
+ };
954
+ await idxInternals.captureSyncedFingerprint.call(idx);
955
+
956
+ const saveSpy = mock.method(ContentHashIndex.prototype, "save");
957
+ const reconcileSpy = mock.method(ContentHashIndex.prototype, "saveMergingWithDisk");
958
+ try {
959
+ await storage.removeFactContentHashesForMemories([beta!]);
960
+ assert.ok(
961
+ reconcileSpy.mock.callCount() >= 1,
962
+ "the removal flushes via the locked reconcile (saveMergingWithDisk)",
963
+ );
964
+ assert.equal(
965
+ saveSpy.mock.callCount(),
966
+ 0,
967
+ "the removal must NOT use the unlocked whole-file save()",
968
+ );
969
+ } finally {
970
+ saveSpy.mock.restore();
971
+ reconcileSpy.mock.restore();
972
+ }
973
+
974
+ const fresh = new ContentHashIndex(stateDir);
975
+ await fresh.load();
976
+ assert.ok(fresh.has("alpha stays active"), "the surviving fact's hash is intact");
977
+ assert.equal(
978
+ fresh.has("beta gets archived"),
979
+ false,
980
+ "the archived fact's hash is dropped — no resurrection",
981
+ );
982
+ assert.ok(
983
+ fresh.has("peer concurrent append"),
984
+ "the concurrent peer append survives the removal (locked reconcile, not unlocked overwrite)",
985
+ );
986
+ });
987
+ });
988
+
989
+ // PR #2016 thread SD7Tj: the reconcile publish() snapshots this.added at its
990
+ // start, then read+rewrites fact-hashes.txt under the lock. A concurrent
991
+ // add()/remove() that lands during those disk awaits was silently dropped: the
992
+ // old publish cleared this.added/this.removed WHOLESALE and set dirty=false, so
993
+ // the mid-flight delta vanished from memory AND was never persisted. publish now
994
+ // consumes ONLY the deltas it published; late arrivals stay pending (dirty
995
+ // retained) and a bounded durable retry is re-armed to land them.
996
+ test("#2016 thread SD7Tj: a hash added while a reconcile save awaits disk is not lost", async () => {
997
+ await withMemoryDir(async (dir) => {
998
+ const stateDir = path.join(dir, "state");
999
+ await mkdir(stateDir, { recursive: true });
1000
+
1001
+ // The write-key provider is invoked synchronously at publish's write step —
1002
+ // AFTER publish has snapshotted this.added and BEFORE it consumes/clears it.
1003
+ // Injecting an add() there deterministically reproduces a concurrent add
1004
+ // landing in publish's mid-flight window, with no module mocking.
1005
+ let idx!: ContentHashIndex;
1006
+ let injected = false;
1007
+ const writeKeyProvider = (): Buffer | null => {
1008
+ if (!injected) {
1009
+ injected = true;
1010
+ idx.add("added-during-save");
1011
+ }
1012
+ return null;
1013
+ };
1014
+ idx = new ContentHashIndex(stateDir, () => null, writeKeyProvider, undefined, {
1015
+ maxWaitMs: 50,
1016
+ pollMs: 10,
1017
+ retryBaseMs: 10,
1018
+ retryMaxAttempts: 5,
1019
+ });
1020
+ await idx.load();
1021
+ idx.add("added-before-save");
1022
+
1023
+ await idx.saveMergingWithDisk();
1024
+
1025
+ // The pre-save addition published as expected.
1026
+ const afterFirst = new ContentHashIndex(stateDir);
1027
+ await afterFirst.load();
1028
+ assert.equal(afterFirst.has("added-before-save"), true, "the pre-save addition is on disk");
1029
+
1030
+ // The mid-save addition must survive in-memory (old code wiped it via
1031
+ // `this.hashes = merged` + `this.added.clear()`).
1032
+ assert.equal(idx.has("added-during-save"), true, "mid-save addition retained in-memory");
1033
+
1034
+ // ...and it must still be durably persistable — old code set dirty=false so
1035
+ // the drain below would be a no-op and the delta would be lost forever.
1036
+ await withRetryLoopAlive(() => idx.flushReconcileRetry());
1037
+ const fresh = new ContentHashIndex(stateDir);
1038
+ await fresh.load();
1039
+ assert.ok(fresh.has("added-before-save"), "pre-save addition still durable after drain");
1040
+ assert.ok(
1041
+ fresh.has("added-during-save"),
1042
+ "the mid-save addition reached disk — the mid-flight delta was preserved, not dropped",
1043
+ );
1044
+ });
1045
+ });
1046
+
1047
+ // PR #2016 thread SD7Tk: a direct (non-deferred) writeMemory("fact") awaited
1048
+ // saveMergingWithDisk, which on a lock timeout defers to an UNREF'd background
1049
+ // retry and returns without publishing. writeMemory then returned as if the
1050
+ // index were durable while the addition lived only in memory + a timer that a
1051
+ // short-lived process can exit before it fires. writeMemory now drains that
1052
+ // deferred retry inline (flushReconcileRetry) before returning, so a caller can
1053
+ // never observe a lingering, exit-losable retry.
1054
+ test("#2016 thread SD7Tk: a direct fact write drains its deferred hash retry before returning", async () => {
1055
+ await withMemoryDir(async (dir) => {
1056
+ const flushSpy = mock.method(ContentHashIndex.prototype, "flushReconcileRetry");
1057
+ const lockPath = path.join(dir, "state", "fact-hashes.txt.lock");
1058
+ await mkdir(path.dirname(lockPath), { recursive: true });
1059
+ // A peer holds the lock so the direct write's reconcile save times out and
1060
+ // defers to a background retry instead of publishing.
1061
+ await writeFile(lockPath, `99999 peer-owner ${new Date().toISOString()}\n`);
1062
+ try {
1063
+ const storage = new StorageManager(dir);
1064
+ storage.factHashIndexLockOptions = { maxWaitMs: 30, pollMs: 10, retryMaxAttempts: 2, retryBaseMs: 10 };
1065
+
1066
+ await withRetryLoopAlive(async () => {
1067
+ await storage.writeMemory("fact", "direct-write-under-contended-lock", { source: "manual" });
1068
+ });
1069
+
1070
+ // The direct write must have drained the deferred retry inline before
1071
+ // returning. Under the old code writeMemory awaited only
1072
+ // saveMergingWithDisk (which returned with the retry still armed) and never
1073
+ // called flushReconcileRetry — so this count was 0.
1074
+ assert.ok(
1075
+ flushSpy.mock.callCount() >= 1,
1076
+ "direct writeMemory drains the deferred hash retry (flushReconcileRetry) before returning",
1077
+ );
1078
+ } finally {
1079
+ flushSpy.mock.restore();
1080
+ await rm(lockPath, { force: true });
1081
+ }
1082
+ });
1083
+ });
1084
+ test("#2016 thread SD-nG: a direct re-add of a hash the local snapshot still holds but a peer removed republishes it durably", async () => {
1085
+ await withMemoryDir(async (dir) => {
1086
+ const stateDir = path.join(dir, "state");
1087
+ await mkdir(stateDir, { recursive: true });
1088
+ const CONTENT = "the reintroduced fact content";
1089
+
1090
+ // Peer A registers the hash and publishes it to the shared on-disk index.
1091
+ const peerA = new ContentHashIndex(stateDir);
1092
+ await peerA.load();
1093
+ peerA.add(CONTENT);
1094
+ await peerA.saveMergingWithDisk();
1095
+
1096
+ // Peer B loads the same on-disk index — its in-memory snapshot HOLDS the hash.
1097
+ const peerB = new ContentHashIndex(stateDir);
1098
+ await peerB.load();
1099
+ assert.equal(peerB.has(CONTENT), true, "B loaded the hash from disk");
1100
+
1101
+ // Peer A removes the hash and publishes the removal — disk no longer holds
1102
+ // it, but B's snapshot is now STALE (still holds the hash).
1103
+ peerA.remove(CONTENT);
1104
+ await peerA.saveMergingWithDisk();
1105
+
1106
+ // B directly re-adds the same content. Pre-fix this was a no-op: the local
1107
+ // snapshot already held the hash, so add() recorded no delta and left the
1108
+ // instance not dirty, so B's reconcile short-circuited and the reintroduced
1109
+ // hash never reached disk — a peer view permanently lost it.
1110
+ peerB.add(CONTENT);
1111
+ await peerB.saveMergingWithDisk();
1112
+
1113
+ // A fresh reader (another peer) must now see the reintroduced hash.
1114
+ const reader = new ContentHashIndex(stateDir);
1115
+ await reader.load();
1116
+ assert.equal(
1117
+ reader.has(CONTENT),
1118
+ true,
1119
+ "the reintroduced hash is durably republished for peers (stale re-add recorded as a delta)",
1120
+ );
1121
+ });
1122
+ });
1123
+ test("#2016 thread PRRT_kwDORJXyws6SEHve: addByHash re-add of a hash the local snapshot still holds but a peer removed republishes it durably", async () => {
1124
+ // Parallel to thread SD-nG for add(): the reactivation path
1125
+ // (StorageManager.addActiveFactContentHash) re-registers a hash via addByHash
1126
+ // OUTSIDE a rebuild. Pre-fix, when this instance's in-memory set still held
1127
+ // the hash but a peer had REMOVED it on disk, addByHash no-op'd (hash present
1128
+ // in `hashes`), recording no delta and leaving the instance not dirty. The
1129
+ // subsequent reconcile then short-circuited (not dirty), so the reintroduced
1130
+ // hash never reached disk and the peer view lost it permanently.
1131
+ await withMemoryDir(async (dir) => {
1132
+ const stateDir = path.join(dir, "state");
1133
+ await mkdir(stateDir, { recursive: true });
1134
+ const hash = ContentHashIndex.computeHash("reactivated-fact-body");
1135
+
1136
+ // Peer A publishes the hash to the shared on-disk index.
1137
+ const peerA = new ContentHashIndex(stateDir);
1138
+ await peerA.load();
1139
+ peerA.addByHash(hash);
1140
+ await peerA.saveMergingWithDisk();
1141
+
1142
+ // Peer B loads the same index — its in-memory snapshot HOLDS the hash.
1143
+ const peerB = new ContentHashIndex(stateDir);
1144
+ await peerB.load();
1145
+ assert.equal(peerB.has("reactivated-fact-body"), true, "B loaded the hash from disk");
1146
+
1147
+ // Peer A removes the hash and publishes the removal — disk no longer holds
1148
+ // it, but B's snapshot is now STALE (still holds the hash).
1149
+ peerA.removeByHash(hash);
1150
+ await peerA.saveMergingWithDisk();
1151
+
1152
+ // B re-registers the same hash via addByHash (the reactivation path). Pre-fix
1153
+ // a no-op; fixed, it records the durable delta.
1154
+ peerB.addByHash(hash);
1155
+ await peerB.saveMergingWithDisk();
1156
+
1157
+ const reader = new ContentHashIndex(stateDir);
1158
+ await reader.load();
1159
+ assert.equal(
1160
+ reader.has("reactivated-fact-body"),
1161
+ true,
1162
+ "addByHash on a stale local hit must republish the hash durably for peers",
1163
+ );
1164
+ });
1165
+ });
1166
+
1167
+ test("#2016 thread PRRT_kwDORJXyws6SEHvh: reactivation drains the deferred reconcile retry inline like writeMemory", async () => {
1168
+ // restoreFactHashAfterApproval -> addActiveFactContentHash used to publish the
1169
+ // reintroduced hash with a bare saveMergingWithDisk() and return. On a lock
1170
+ // timeout that call DEFERS to an unref'd background retry WITHOUT publishing,
1171
+ // so a short-lived caller could exit before the hash reached disk. writeMemory
1172
+ // guards this by draining the deferred retry inline (flushReconcileRetry); the
1173
+ // reactivation path is the same lifecycle boundary and must do the same. This
1174
+ // asserts the wiring — the drain's durability semantics are covered by the
1175
+ // ContentHashIndex lock-timeout tests above.
1176
+ await withMemoryDir(async (dir) => {
1177
+ const storage = new StorageManager(dir);
1178
+ const { id } = await storage.writeMemory("fact", "reactivated fact body", { source: "manual" });
1179
+ assert.ok(id, "wrote the fact to reactivate");
1180
+
1181
+ // Reset spies AFTER the write so we only measure the reactivation path
1182
+ // (writeMemory itself also flushes).
1183
+ const reconcileSpy = mock.method(ContentHashIndex.prototype, "saveMergingWithDisk");
1184
+ const flushSpy = mock.method(ContentHashIndex.prototype, "flushReconcileRetry");
1185
+ try {
1186
+ await storage.restoreFactHashAfterApproval(id);
1187
+ assert.ok(
1188
+ reconcileSpy.mock.callCount() >= 1,
1189
+ "reactivation publishes via the locked reconcile (saveMergingWithDisk)",
1190
+ );
1191
+ assert.ok(
1192
+ flushSpy.mock.callCount() >= 1,
1193
+ "reactivation drains the deferred lock-timeout retry inline (flushReconcileRetry) — same durability guarantee as writeMemory",
1194
+ );
1195
+ } finally {
1196
+ reconcileSpy.mock.restore();
1197
+ flushSpy.mock.restore();
1198
+ }
1199
+ });
1200
+ });