@remnic/core 9.7.14 → 9.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (950) hide show
  1. package/dist/access-admin-ops-surface.d.ts +13 -6
  2. package/dist/access-admin-ops-surface.js +88 -74
  3. package/dist/access-audit.js +2 -2
  4. package/dist/access-boundary.d.ts +35 -17
  5. package/dist/access-boundary.js +89 -77
  6. package/dist/access-citation.d.ts +28 -0
  7. package/dist/access-citation.js +9 -0
  8. package/dist/access-cli.js +197 -181
  9. package/dist/access-cli.js.map +1 -1
  10. package/dist/access-errors.d.ts +16 -1
  11. package/dist/access-errors.js +7 -3
  12. package/dist/access-http.d.ts +25 -19
  13. package/dist/access-http.js +103 -90
  14. package/dist/access-identity-continuity-surface.d.ts +13 -8
  15. package/dist/access-identity-continuity-surface.js +88 -74
  16. package/dist/access-lcm-surface.d.ts +16 -9
  17. package/dist/access-lcm-surface.js +88 -74
  18. package/dist/access-mcp.d.ts +23 -16
  19. package/dist/access-mcp.js +99 -87
  20. package/dist/access-memory-search-fanout.d.ts +93 -0
  21. package/dist/access-memory-search-fanout.js +18 -0
  22. package/dist/access-observe-write-surface.d.ts +26 -8
  23. package/dist/access-observe-write-surface.js +88 -74
  24. package/dist/access-operations-batch.js +95 -83
  25. package/dist/access-operations.d.ts +26 -19
  26. package/dist/access-operations.js +97 -85
  27. package/dist/access-recall-concurrency.d.ts +250 -0
  28. package/dist/access-recall-concurrency.js +157 -0
  29. package/dist/access-recall-response.d.ts +130 -0
  30. package/dist/access-recall-response.js +147 -0
  31. package/dist/access-recall-surface.d.ts +15 -7
  32. package/dist/access-recall-surface.js +88 -74
  33. package/dist/access-schema.d.ts +34 -34
  34. package/dist/access-schema.js +8 -6
  35. package/dist/{access-service-CPhSnLOH.d.ts → access-service-DEL4qCrc.d.ts} +25 -32
  36. package/dist/access-service.d.ts +13 -6
  37. package/dist/access-service.js +92 -75
  38. package/dist/access-surface-catalog.d.ts +23 -16
  39. package/dist/access-surface-catalog.js +1 -0
  40. package/dist/access-surface-catalog.js.map +1 -1
  41. package/dist/access-token-capabilities.d.ts +2 -1
  42. package/dist/access-token-capabilities.js +4 -2
  43. package/dist/action-confidence.d.ts +3 -1
  44. package/dist/active-memory-bridge.d.ts +3 -1
  45. package/dist/active-memory-bridge.js +4 -4
  46. package/dist/active-recall.d.ts +4 -1
  47. package/dist/active-recall.js +23 -13
  48. package/dist/active-recall.js.map +1 -1
  49. package/dist/adapters/index.js +4 -4
  50. package/dist/adapters/registry.js +2 -2
  51. package/dist/{auto-sync-AAMTVZMK.js → auto-sync-UZG3XQZ5.js} +14 -8
  52. package/dist/{auto-sync-AAMTVZMK.js.map → auto-sync-UZG3XQZ5.js.map} +1 -1
  53. package/dist/behavior-learner.d.ts +3 -1
  54. package/dist/behavior-signals.d.ts +3 -1
  55. package/dist/bootstrap.d.ts +12 -7
  56. package/dist/bootstrap.js +2 -2
  57. package/dist/bounded-jsonl-state.d.ts +54 -0
  58. package/dist/bounded-jsonl-state.js +10 -0
  59. package/dist/boxes.js +2 -2
  60. package/dist/briefing.d.ts +5 -2
  61. package/dist/briefing.js +37 -27
  62. package/dist/buffer-surprise-report.d.ts +3 -1
  63. package/dist/buffer.d.ts +60 -2
  64. package/dist/buffer.js +3 -3
  65. package/dist/bulk-import/index.d.ts +5 -3
  66. package/dist/calibration.d.ts +3 -1
  67. package/dist/calibration.js +6 -6
  68. package/dist/capabilities.d.ts +7 -7
  69. package/dist/capabilities.js +1 -1
  70. package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-YO5QJ6L3.js} +3 -3
  71. package/dist/capsule-crypto-YO5QJ6L3.js.map +1 -0
  72. package/dist/{capsule-merge-Z2HUO6IK.js → capsule-merge-5KKJQBWQ.js} +4 -4
  73. package/dist/{catalog-uX9sT9ws.d.ts → catalog-BWqHb5HT.d.ts} +1 -1
  74. package/dist/causal-behavior.d.ts +3 -1
  75. package/dist/causal-behavior.js +6 -6
  76. package/dist/causal-chain.js +4 -4
  77. package/dist/causal-consolidation.d.ts +3 -1
  78. package/dist/causal-consolidation.js +46 -37
  79. package/dist/causal-consolidation.js.map +1 -1
  80. package/dist/causal-retrieval.js +6 -6
  81. package/dist/causal-trajectory-graph.d.ts +3 -1
  82. package/dist/causal-trajectory-graph.js +1 -1
  83. package/dist/causal-trajectory.js +1 -1
  84. package/dist/{chunk-A3GOMRJG.js → chunk-23L3IXFD.js} +2 -2
  85. package/dist/chunk-2CGLBUW3.js +15 -0
  86. package/dist/chunk-2CGLBUW3.js.map +1 -0
  87. package/dist/{chunk-MRHFJV44.js → chunk-2DQ2XREU.js} +2 -2
  88. package/dist/{chunk-ASKEM4AR.js → chunk-2EJOEICD.js} +1823 -1483
  89. package/dist/chunk-2EJOEICD.js.map +1 -0
  90. package/dist/{chunk-L2B3EE7B.js → chunk-2QMKMVHH.js} +2 -2
  91. package/dist/{chunk-6CALL5VN.js → chunk-2UFO7X4Q.js} +7 -7
  92. package/dist/{chunk-2MR3MFQB.js → chunk-2VEOKUPP.js} +3 -3
  93. package/dist/chunk-2Z24E5W2.js +22 -0
  94. package/dist/chunk-2Z24E5W2.js.map +1 -0
  95. package/dist/{chunk-RHOZ5LLO.js → chunk-347WD2LE.js} +2 -2
  96. package/dist/{chunk-5S7Z5SHA.js → chunk-34RC5RT7.js} +2 -2
  97. package/dist/{chunk-KGWXHWAH.js → chunk-3QAH2S33.js} +3 -3
  98. package/dist/{chunk-37CTGERM.js → chunk-3WCASNQZ.js} +12 -1
  99. package/dist/chunk-3WCASNQZ.js.map +1 -0
  100. package/dist/{chunk-ZIDSD6VC.js → chunk-3Y224NYC.js} +3 -3
  101. package/dist/{chunk-RBQLJSJ7.js → chunk-44XZ75OQ.js} +46 -10
  102. package/dist/chunk-44XZ75OQ.js.map +1 -0
  103. package/dist/{chunk-VFSP73LP.js → chunk-45GF7Y3V.js} +2 -2
  104. package/dist/{chunk-7ICYQUOL.js → chunk-4BJQODZF.js} +2514 -1526
  105. package/dist/chunk-4BJQODZF.js.map +1 -0
  106. package/dist/{chunk-JMBVCAR4.js → chunk-57OWKFUQ.js} +17 -6
  107. package/dist/chunk-57OWKFUQ.js.map +1 -0
  108. package/dist/{chunk-OEHCMALU.js → chunk-5I4M26Q7.js} +5 -5
  109. package/dist/{chunk-2E6G7FCE.js → chunk-5Q7EVV4X.js} +7 -7
  110. package/dist/{chunk-LC7OODWZ.js → chunk-5SB7SC2G.js} +80 -49
  111. package/dist/chunk-5SB7SC2G.js.map +1 -0
  112. package/dist/{chunk-VZ5OJE4X.js → chunk-5VQJBH2G.js} +2 -2
  113. package/dist/{chunk-PUBTHW5O.js → chunk-6DRPPQEJ.js} +2 -2
  114. package/dist/{chunk-NWTGD4AN.js → chunk-6KIQT7RG.js} +10 -9
  115. package/dist/chunk-6KIQT7RG.js.map +1 -0
  116. package/dist/{chunk-4LEF6XWJ.js → chunk-6O22TZSB.js} +3 -3
  117. package/dist/{chunk-ZQ7UBUZ6.js → chunk-6QYST5XN.js} +6 -6
  118. package/dist/{chunk-W4PGTAS2.js → chunk-6WAFYNVT.js} +3 -3
  119. package/dist/chunk-6ZMAMSHS.js +20 -0
  120. package/dist/chunk-6ZMAMSHS.js.map +1 -0
  121. package/dist/{chunk-R5VAVVHP.js → chunk-72A4L5JJ.js} +2 -2
  122. package/dist/{chunk-X64YLC24.js → chunk-72VAXGEI.js} +5 -13
  123. package/dist/chunk-72VAXGEI.js.map +1 -0
  124. package/dist/{chunk-IKQTFQMI.js → chunk-755Q3DGF.js} +22 -3
  125. package/dist/chunk-755Q3DGF.js.map +1 -0
  126. package/dist/{chunk-F7AU34FC.js → chunk-76GLCM57.js} +160 -127
  127. package/dist/chunk-76GLCM57.js.map +1 -0
  128. package/dist/{chunk-D7FOKIY5.js → chunk-7AMFUATK.js} +12 -9
  129. package/dist/{chunk-D7FOKIY5.js.map → chunk-7AMFUATK.js.map} +1 -1
  130. package/dist/{chunk-BFVEEB7W.js → chunk-7ON2JW73.js} +57 -22
  131. package/dist/chunk-7ON2JW73.js.map +1 -0
  132. package/dist/chunk-7RNBVZZO.js +270 -0
  133. package/dist/chunk-7RNBVZZO.js.map +1 -0
  134. package/dist/{chunk-3FAMU5TX.js → chunk-A6USBZTO.js} +2 -2
  135. package/dist/{chunk-TXVD7PLT.js → chunk-A72SWTWW.js} +7 -7
  136. package/dist/{chunk-FC77VUX5.js → chunk-A7Q43EUZ.js} +2 -2
  137. package/dist/{chunk-UJBESW6X.js → chunk-AE7WMV4V.js} +7 -7
  138. package/dist/{chunk-J2HSAU72.js → chunk-AIK5W3IW.js} +29 -1
  139. package/dist/chunk-AIK5W3IW.js.map +1 -0
  140. package/dist/{chunk-OKUEZOGW.js → chunk-AITCS2RC.js} +2 -2
  141. package/dist/{chunk-OC35KQIN.js → chunk-AJ7QCSIS.js} +4 -4
  142. package/dist/{chunk-FNMFJWBY.js → chunk-ANEUPF6O.js} +5 -5
  143. package/dist/chunk-AREGMX2J.js +50 -0
  144. package/dist/chunk-AREGMX2J.js.map +1 -0
  145. package/dist/chunk-AVPXXV6J.js +448 -0
  146. package/dist/chunk-AVPXXV6J.js.map +1 -0
  147. package/dist/{chunk-2A3OXAP2.js → chunk-B7RWBZXW.js} +2 -2
  148. package/dist/{chunk-GWTFLF2P.js → chunk-BJMBJZ2Y.js} +59 -59
  149. package/dist/chunk-BJMBJZ2Y.js.map +1 -0
  150. package/dist/{chunk-N3JDIRBW.js → chunk-BLAKS3CP.js} +4 -4
  151. package/dist/{chunk-M7VGG3ID.js → chunk-BV5WMSIJ.js} +9 -9
  152. package/dist/{chunk-2BFLDH5F.js → chunk-BXLOS5AJ.js} +2 -2
  153. package/dist/{chunk-UXKMMNJD.js → chunk-C23BYTJV.js} +6 -6
  154. package/dist/{chunk-JZHHREBL.js → chunk-C3AKM55W.js} +2 -2
  155. package/dist/{chunk-RP7YXA67.js → chunk-C5JF6EZ7.js} +2 -2
  156. package/dist/{chunk-FBNWOMCE.js → chunk-C6XBRYVD.js} +16 -15
  157. package/dist/chunk-C6XBRYVD.js.map +1 -0
  158. package/dist/{chunk-FTYDWKO6.js → chunk-CEN6DW6X.js} +12 -2
  159. package/dist/chunk-CEN6DW6X.js.map +1 -0
  160. package/dist/chunk-CGASUJVP.js +421 -0
  161. package/dist/chunk-CGASUJVP.js.map +1 -0
  162. package/dist/{chunk-DDGV7NAX.js → chunk-CQMEM5CC.js} +2 -2
  163. package/dist/{chunk-A7B4RMQG.js → chunk-CXZQOVIE.js} +12 -12
  164. package/dist/chunk-D3V3UMET.js +44 -0
  165. package/dist/chunk-D3V3UMET.js.map +1 -0
  166. package/dist/{chunk-7N2RUBAP.js → chunk-DDA47TES.js} +2 -2
  167. package/dist/{chunk-64SE4X5G.js → chunk-DEBHXHJR.js} +3 -3
  168. package/dist/{chunk-EHK6KZHK.js → chunk-DNHVPZOD.js} +2 -2
  169. package/dist/{chunk-437S3G37.js → chunk-DQEMWVMT.js} +2 -2
  170. package/dist/{chunk-LZZNTPLR.js → chunk-DX3XRGLH.js} +2 -2
  171. package/dist/chunk-EA2H5LML.js +72 -0
  172. package/dist/chunk-EA2H5LML.js.map +1 -0
  173. package/dist/{chunk-IFJ23BHU.js → chunk-EEC7UJPX.js} +112 -106
  174. package/dist/chunk-EEC7UJPX.js.map +1 -0
  175. package/dist/{chunk-KSNVDPDF.js → chunk-EKBNDKDY.js} +21 -20
  176. package/dist/chunk-EKBNDKDY.js.map +1 -0
  177. package/dist/{chunk-YF6FMLBT.js → chunk-EQWEIIKQ.js} +64 -34
  178. package/dist/chunk-EQWEIIKQ.js.map +1 -0
  179. package/dist/{chunk-7MW3CVLD.js → chunk-ES36HTSC.js} +4 -4
  180. package/dist/{chunk-TN7THW5U.js → chunk-ET356KSB.js} +2 -2
  181. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  182. package/dist/{chunk-7LFXDGLH.js → chunk-F7GAIWFA.js} +20 -20
  183. package/dist/{chunk-IKSELOIV.js → chunk-FFPJINJE.js} +7 -7
  184. package/dist/chunk-FMLKOL2Z.js +636 -0
  185. package/dist/chunk-FMLKOL2Z.js.map +1 -0
  186. package/dist/chunk-FQC3DHH2.js +20 -0
  187. package/dist/chunk-FQC3DHH2.js.map +1 -0
  188. package/dist/{chunk-V53GJUSL.js → chunk-FUTF72MJ.js} +15 -15
  189. package/dist/chunk-GCASGYIO.js +71 -0
  190. package/dist/chunk-GCASGYIO.js.map +1 -0
  191. package/dist/{chunk-5UNWK7KU.js → chunk-GFPZS3F5.js} +8 -8
  192. package/dist/{chunk-BCDKBDSY.js → chunk-GNVZQ5JN.js} +99 -41
  193. package/dist/chunk-GNVZQ5JN.js.map +1 -0
  194. package/dist/{chunk-G6NKALRN.js → chunk-H4WG3UQZ.js} +2 -2
  195. package/dist/{chunk-HLTAPHZZ.js → chunk-HNZKVLAN.js} +2 -2
  196. package/dist/{chunk-5EZXHU36.js → chunk-HYQJQGD3.js} +2 -2
  197. package/dist/{chunk-ANADNYWK.js → chunk-IDZFBSHN.js} +8 -6
  198. package/dist/{chunk-ANADNYWK.js.map → chunk-IDZFBSHN.js.map} +1 -1
  199. package/dist/{chunk-XVLEGMJI.js → chunk-IFH6LWTO.js} +50 -8
  200. package/dist/chunk-IFH6LWTO.js.map +1 -0
  201. package/dist/{chunk-RULE4VG5.js → chunk-ILGC6JH6.js} +21 -6
  202. package/dist/chunk-ILGC6JH6.js.map +1 -0
  203. package/dist/chunk-IUM3VSHI.js +113 -0
  204. package/dist/chunk-IUM3VSHI.js.map +1 -0
  205. package/dist/{chunk-ND5M64JJ.js → chunk-IYNGIMVG.js} +101 -91
  206. package/dist/chunk-IYNGIMVG.js.map +1 -0
  207. package/dist/{chunk-TRWPCR6P.js → chunk-JECBQBEX.js} +3 -3
  208. package/dist/{chunk-HG32URJJ.js → chunk-JYIFGGEM.js} +2 -2
  209. package/dist/{chunk-VA4WSW54.js → chunk-JYIJNNN3.js} +3 -3
  210. package/dist/{chunk-L5HJQC36.js → chunk-K2TDK7GG.js} +3 -3
  211. package/dist/{chunk-MKDRQAA3.js → chunk-KAVBJSBQ.js} +4 -4
  212. package/dist/{chunk-EEARRVZ2.js → chunk-KF5EUVKQ.js} +16 -6
  213. package/dist/chunk-KF5EUVKQ.js.map +1 -0
  214. package/dist/chunk-KFZY2OFH.js +42 -0
  215. package/dist/chunk-KFZY2OFH.js.map +1 -0
  216. package/dist/{chunk-4OKLXWDZ.js → chunk-KNB7IBBU.js} +2 -2
  217. package/dist/{chunk-QLULYKWZ.js → chunk-KSXJOC4F.js} +2 -2
  218. package/dist/{chunk-OBMR4VIX.js → chunk-L2DPF4DF.js} +186 -15
  219. package/dist/chunk-L2DPF4DF.js.map +1 -0
  220. package/dist/{chunk-JI5AVXYO.js → chunk-LEPOP46W.js} +6 -6
  221. package/dist/{chunk-O7BBFJLP.js → chunk-LTAVSOZW.js} +2 -2
  222. package/dist/{chunk-E3VODCC3.js → chunk-LUDXM7MD.js} +14 -2
  223. package/dist/{chunk-E3VODCC3.js.map → chunk-LUDXM7MD.js.map} +1 -1
  224. package/dist/{chunk-2IZAGAJR.js → chunk-LULR4IUW.js} +3 -3
  225. package/dist/{chunk-NUWYOKS6.js → chunk-MC5Q3J7S.js} +398 -7
  226. package/dist/chunk-MC5Q3J7S.js.map +1 -0
  227. package/dist/{chunk-WMWVO45V.js → chunk-MC6XO7T4.js} +2 -2
  228. package/dist/chunk-MKQBXQKU.js +107 -0
  229. package/dist/chunk-MKQBXQKU.js.map +1 -0
  230. package/dist/{chunk-7PND4PUC.js → chunk-MMGGVZ4Q.js} +29 -9
  231. package/dist/{chunk-7PND4PUC.js.map → chunk-MMGGVZ4Q.js.map} +1 -1
  232. package/dist/{chunk-D6RIFG4O.js → chunk-MNB5RMAY.js} +2 -2
  233. package/dist/{chunk-67ZCJLJY.js → chunk-MYFQ3ED4.js} +8 -8
  234. package/dist/{chunk-V3UE5I5F.js → chunk-MYVPLJOO.js} +2 -2
  235. package/dist/chunk-NINSR6LX.js +83 -0
  236. package/dist/chunk-NINSR6LX.js.map +1 -0
  237. package/dist/{chunk-MTWKDOVO.js → chunk-NLE6TSLG.js} +2 -2
  238. package/dist/{chunk-DBRYEIGY.js → chunk-OJP3N7UK.js} +2 -2
  239. package/dist/{chunk-AD6AMF2N.js → chunk-OQJIB32H.js} +28 -23
  240. package/dist/chunk-OQJIB32H.js.map +1 -0
  241. package/dist/{chunk-SMX5267P.js → chunk-OZKOPQCM.js} +2 -2
  242. package/dist/{chunk-JCK7V6SN.js → chunk-P65YJEEL.js} +2 -2
  243. package/dist/{chunk-MTR7BZQN.js → chunk-PBGWYM3H.js} +3 -3
  244. package/dist/chunk-PIG4WJFQ.js +87 -0
  245. package/dist/chunk-PIG4WJFQ.js.map +1 -0
  246. package/dist/chunk-PQVCH2AE.js +76 -0
  247. package/dist/chunk-PQVCH2AE.js.map +1 -0
  248. package/dist/{chunk-AXTXN4T6.js → chunk-PWFCHOB3.js} +2 -2
  249. package/dist/{chunk-FDPZVY6D.js → chunk-QFN4U7AG.js} +2 -2
  250. package/dist/{chunk-K2ZLHESQ.js → chunk-QIEUU4UK.js} +2 -2
  251. package/dist/{chunk-AHLPZ2SF.js → chunk-QJAQNXWI.js} +44 -6
  252. package/dist/chunk-QJAQNXWI.js.map +1 -0
  253. package/dist/{chunk-3D6AFGQR.js → chunk-QVT3E4CE.js} +7 -7
  254. package/dist/{chunk-VYOFVVNK.js → chunk-R26ZMXR2.js} +24 -24
  255. package/dist/{chunk-VYOFVVNK.js.map → chunk-R26ZMXR2.js.map} +1 -1
  256. package/dist/chunk-R3DJVOSM.js +34 -0
  257. package/dist/chunk-R3DJVOSM.js.map +1 -0
  258. package/dist/{chunk-SFQ6QNL7.js → chunk-R6YJI3HE.js} +2 -1
  259. package/dist/{chunk-5CJVLG3H.js → chunk-RFDD3B7W.js} +5 -5
  260. package/dist/{chunk-JBPKEARU.js → chunk-RKNJBZ55.js} +4 -4
  261. package/dist/chunk-ROI7BK7H.js +88 -0
  262. package/dist/chunk-ROI7BK7H.js.map +1 -0
  263. package/dist/{chunk-UTRJFDP2.js → chunk-S2BBISHU.js} +4 -4
  264. package/dist/{chunk-IALLX7EE.js → chunk-S2FSBU4H.js} +3 -3
  265. package/dist/{chunk-BFPETA3S.js → chunk-SCCO46KZ.js} +3 -2
  266. package/dist/chunk-SCCO46KZ.js.map +1 -0
  267. package/dist/chunk-SICXL4MK.js +165 -0
  268. package/dist/chunk-SICXL4MK.js.map +1 -0
  269. package/dist/chunk-SK7AMF2J.js +168 -0
  270. package/dist/chunk-SK7AMF2J.js.map +1 -0
  271. package/dist/{chunk-GMSDQUCR.js → chunk-TDTXKOAJ.js} +6475 -5748
  272. package/dist/chunk-TDTXKOAJ.js.map +1 -0
  273. package/dist/{chunk-KSBEEMQF.js → chunk-TG6IHVE2.js} +151 -6
  274. package/dist/chunk-TG6IHVE2.js.map +1 -0
  275. package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
  276. package/dist/{chunk-PK5XBDA6.js → chunk-U3ZAVRP3.js} +2 -2
  277. package/dist/{chunk-ODPLEWB6.js → chunk-UFGFMWAD.js} +47 -17
  278. package/dist/chunk-UFGFMWAD.js.map +1 -0
  279. package/dist/{chunk-4KJLYTLS.js → chunk-V57NLO5P.js} +2 -2
  280. package/dist/{chunk-FLVL4L3L.js → chunk-VDX2J7OX.js} +2 -2
  281. package/dist/{chunk-7XGSIUP5.js → chunk-W4FBGYGK.js} +1 -1
  282. package/dist/{chunk-TXLGHDHS.js → chunk-W4XJFLAL.js} +53 -32
  283. package/dist/chunk-W4XJFLAL.js.map +1 -0
  284. package/dist/{chunk-6SXVCD7W.js → chunk-W7IGLX4G.js} +2 -2
  285. package/dist/{chunk-KQDNAXOM.js → chunk-WG5HSQ4P.js} +19 -6
  286. package/dist/chunk-WG5HSQ4P.js.map +1 -0
  287. package/dist/{chunk-LTJAMRGI.js → chunk-WL5EBRDH.js} +17 -3
  288. package/dist/chunk-WL5EBRDH.js.map +1 -0
  289. package/dist/{chunk-KAWTOYAW.js → chunk-WLZYGLJ4.js} +2 -2
  290. package/dist/{chunk-W5NKDU24.js → chunk-WON4APQY.js} +4 -4
  291. package/dist/{chunk-NKCPUHCI.js → chunk-WZFAPHIK.js} +6 -6
  292. package/dist/{chunk-KQPJLK3X.js → chunk-XL24QQ63.js} +2 -2
  293. package/dist/{chunk-X47LWMKA.js → chunk-XMMESCPD.js} +26 -1
  294. package/dist/chunk-XMMESCPD.js.map +1 -0
  295. package/dist/{chunk-4B4MUS5D.js → chunk-YROFNEG5.js} +7 -7
  296. package/dist/{chunk-WU46YPY6.js → chunk-YV32N67O.js} +1 -1
  297. package/dist/chunk-YV32N67O.js.map +1 -0
  298. package/dist/{chunk-B3F4CWQR.js → chunk-YVKF3HEW.js} +1 -1
  299. package/dist/chunk-YVKF3HEW.js.map +1 -0
  300. package/dist/{chunk-VRAD2FGO.js → chunk-Z54EM5YZ.js} +12 -12
  301. package/dist/{chunk-2VN4JAX7.js → chunk-ZOKZIR5K.js} +95 -33
  302. package/dist/chunk-ZOKZIR5K.js.map +1 -0
  303. package/dist/{cli-_Y2Bmt6A.d.ts → cli-C_pqqTCw.d.ts} +28 -12
  304. package/dist/cli.d.ts +16 -9
  305. package/dist/cli.js +145 -130
  306. package/dist/compounding/engine.d.ts +5 -2
  307. package/dist/compounding/engine.js +39 -29
  308. package/dist/compounding/preference-consolidator.d.ts +3 -1
  309. package/dist/compounding/preference-consolidator.js +1 -1
  310. package/dist/compression-optimizer.d.ts +3 -1
  311. package/dist/config-qmd.d.ts +8 -0
  312. package/dist/config-qmd.js +20 -0
  313. package/dist/config-qmd.js.map +1 -0
  314. package/dist/config.d.ts +3 -1
  315. package/dist/config.js +15 -10
  316. package/dist/connectors/codex-materialize-runner.d.ts +3 -1
  317. package/dist/connectors/codex-materialize-runner.js +39 -29
  318. package/dist/connectors/codex-materialize.d.ts +3 -1
  319. package/dist/connectors/codex-materialize.js +2 -2
  320. package/dist/connectors/index.d.ts +3 -1
  321. package/dist/connectors/index.js +44 -35
  322. package/dist/consolidation-provenance-check.d.ts +5 -2
  323. package/dist/consolidation-provenance-check.js +4 -4
  324. package/dist/consolidation-undo.d.ts +5 -2
  325. package/dist/consolidation-undo.js +3 -3
  326. package/dist/contradiction/index.d.ts +5 -2
  327. package/dist/contradiction/index.js +10 -4
  328. package/dist/{contradiction-scan-J57T5QV7.js → contradiction-scan-RJBABNRH.js} +4 -4
  329. package/dist/contradiction-scan-RJBABNRH.js.map +1 -0
  330. package/dist/conversation-index/backend.d.ts +3 -1
  331. package/dist/conversation-index/backend.js +5 -5
  332. package/dist/conversation-index/chunker.d.ts +3 -1
  333. package/dist/conversation-index/cleanup.js +2 -2
  334. package/dist/conversation-index/faiss-adapter.d.ts +3 -1
  335. package/dist/conversation-index/faiss-adapter.js +2 -2
  336. package/dist/conversation-index/indexer.d.ts +3 -1
  337. package/dist/conversation-index/indexer.js +2 -2
  338. package/dist/conversation-index/search.d.ts +3 -1
  339. package/dist/conversation-index/search.js +2 -2
  340. package/dist/cross-namespace-budget.d.ts +45 -1
  341. package/dist/cross-namespace-budget.js +5 -3
  342. package/dist/dashboard-runtime.js +2 -2
  343. package/dist/day-summary.d.ts +3 -1
  344. package/dist/day-summary.js +3 -3
  345. package/dist/delinearize.d.ts +3 -1
  346. package/dist/direct-answer-wiring.d.ts +3 -1
  347. package/dist/direct-answer.d.ts +3 -1
  348. package/dist/{dreams-ledger-6UODQOX6.js → dreams-ledger-567VKVOU.js} +4 -2
  349. package/dist/dreams-ledger-567VKVOU.js.map +1 -0
  350. package/dist/embedding-fallback.d.ts +3 -1
  351. package/dist/embedding-fallback.js +3 -3
  352. package/dist/enrichment/index.d.ts +3 -1
  353. package/dist/entity-retrieval.d.ts +5 -2
  354. package/dist/entity-retrieval.js +38 -29
  355. package/dist/entity-schema.d.ts +3 -1
  356. package/dist/explicit-capture.d.ts +13 -8
  357. package/dist/explicit-capture.js +7 -3
  358. package/dist/extraction-faithfulness.d.ts +3 -1
  359. package/dist/extraction-faithfulness.js +5 -5
  360. package/dist/extraction-judge-telemetry.d.ts +3 -1
  361. package/dist/extraction-judge-telemetry.js +2 -2
  362. package/dist/extraction-judge-training.d.ts +3 -1
  363. package/dist/extraction-judge-training.js +2 -2
  364. package/dist/extraction-judge.d.ts +3 -1
  365. package/dist/extraction-judge.js +5 -5
  366. package/dist/extraction.d.ts +3 -1
  367. package/dist/extraction.js +13 -12
  368. package/dist/faithfulness-config.js +3 -2
  369. package/dist/fallback-llm.d.ts +3 -1
  370. package/dist/fallback-llm.js +4 -4
  371. package/dist/{first-start-migration-TI67YMLG.js → first-start-migration-CSRHCJIQ.js} +2 -2
  372. package/dist/{forget-CQCLB5LB.js → forget-HRPLVB3W.js} +5 -5
  373. package/dist/graph-dashboard-diff.d.ts +3 -1
  374. package/dist/graph-dashboard-key.d.ts +3 -1
  375. package/dist/graph-dashboard-parser.d.ts +3 -1
  376. package/dist/{graph-edge-decay-FO7AM4UR.js → graph-edge-decay-Z6ENALU2.js} +3 -3
  377. package/dist/graph-edge-decay-Z6ENALU2.js.map +1 -0
  378. package/dist/graph-edge-reinforcement.d.ts +3 -1
  379. package/dist/graph-snapshot.d.ts +3 -1
  380. package/dist/graph-snapshot.js +2 -2
  381. package/dist/graph.d.ts +57 -10
  382. package/dist/graph.js +1 -1
  383. package/dist/identity-continuity.d.ts +1 -2
  384. package/dist/importance.d.ts +3 -1
  385. package/dist/importers/index.d.ts +3 -1
  386. package/dist/in-flight-reads.d.ts +3 -1
  387. package/dist/index.d.ts +101 -92
  388. package/dist/index.js +290 -242
  389. package/dist/intent.d.ts +3 -1
  390. package/dist/lcm/archive.js +2 -2
  391. package/dist/lcm/engine.d.ts +3 -1
  392. package/dist/lcm/engine.js +6 -6
  393. package/dist/lcm/index.d.ts +3 -1
  394. package/dist/lcm/index.js +13 -13
  395. package/dist/lcm/schema.js +2 -2
  396. package/dist/lcm/summarizer.js +3 -3
  397. package/dist/lcm/tools.d.ts +3 -1
  398. package/dist/lifecycle.d.ts +3 -1
  399. package/dist/live-connectors-runner.d.ts +3 -1
  400. package/dist/local-llm.d.ts +5 -2
  401. package/dist/local-llm.js +3 -3
  402. package/dist/local-model-endpoint.d.ts +3 -1
  403. package/dist/logger.d.ts +3 -1
  404. package/dist/logger.js +7 -3
  405. package/dist/maintenance/archive-observations.js +2 -2
  406. package/dist/maintenance/memory-governance.d.ts +3 -1
  407. package/dist/maintenance/memory-governance.js +38 -28
  408. package/dist/maintenance/migrate-observations.js +3 -3
  409. package/dist/maintenance/observation-ledger-utils.js +2 -2
  410. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +102 -1
  411. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +40 -28
  412. package/dist/maintenance/rebuild-memory-projection.js +41 -31
  413. package/dist/maintenance/rebuild-observations.js +3 -3
  414. package/dist/mcp-memory-inspector-app.d.ts +22 -15
  415. package/dist/memory-action-policy.d.ts +3 -1
  416. package/dist/memory-cache.d.ts +46 -2
  417. package/dist/memory-cache.js +7 -1
  418. package/dist/memory-corpus-version.js +2 -2
  419. package/dist/memory-lifecycle-ledger-utils.d.ts +25 -2
  420. package/dist/memory-lifecycle-ledger-utils.js +11 -1
  421. package/dist/memory-projection-store.d.ts +3 -1
  422. package/dist/memory-projection-store.js +2 -2
  423. package/dist/memory-provenance.d.ts +3 -1
  424. package/dist/memory-worth-outcomes.d.ts +5 -2
  425. package/dist/model-registry.js +2 -2
  426. package/dist/models-json.d.ts +3 -1
  427. package/dist/models-json.js +2 -2
  428. package/dist/namespaces/migrate.d.ts +6 -3
  429. package/dist/namespaces/migrate.js +54 -45
  430. package/dist/namespaces/principal.d.ts +27 -2
  431. package/dist/namespaces/principal.js +6 -2
  432. package/dist/namespaces/search.d.ts +16 -2
  433. package/dist/namespaces/search.js +25 -21
  434. package/dist/namespaces/storage.d.ts +24 -3
  435. package/dist/namespaces/storage.js +38 -30
  436. package/dist/native-knowledge.d.ts +3 -1
  437. package/dist/native-knowledge.js +2 -2
  438. package/dist/negative.js +2 -2
  439. package/dist/offline-sync-exclude-globs.d.ts +13 -0
  440. package/dist/offline-sync-exclude-globs.js +10 -0
  441. package/dist/offline-sync-exclude-globs.js.map +1 -0
  442. package/dist/offline-sync-impression-drain.d.ts +78 -0
  443. package/dist/offline-sync-impression-drain.js +37 -0
  444. package/dist/offline-sync-impression-drain.js.map +1 -0
  445. package/dist/offline-sync.js +4 -2
  446. package/dist/operator-toolkit.d.ts +5 -2
  447. package/dist/operator-toolkit.js +73 -58
  448. package/dist/orchestration/compression-guideline-coordinator.d.ts +5 -2
  449. package/dist/orchestration/compression-guideline-coordinator.js +3 -3
  450. package/dist/orchestration/maintenance.d.ts +121 -2
  451. package/dist/orchestration/maintenance.js +47 -34
  452. package/dist/{orchestrator-BbZ_dQSU.d.ts → orchestrator-CQJ8U7En.d.ts} +85 -79
  453. package/dist/orchestrator.d.ts +13 -8
  454. package/dist/orchestrator.js +197 -180
  455. package/dist/page-versioning.js +3 -3
  456. package/dist/patterns-cli.d.ts +3 -1
  457. package/dist/{pipeline-FRMDb1fn.d.ts → pipeline-Cn-kvYeV.d.ts} +1 -1
  458. package/dist/policy-runtime.d.ts +3 -1
  459. package/dist/prioritized-embed.d.ts +19 -0
  460. package/dist/prioritized-embed.js +8 -0
  461. package/dist/prioritized-embed.js.map +1 -0
  462. package/dist/proactive-contention.d.ts +28 -0
  463. package/dist/proactive-contention.js +8 -0
  464. package/dist/proactive-contention.js.map +1 -0
  465. package/dist/profiling.js +2 -2
  466. package/dist/provenance.d.ts +3 -1
  467. package/dist/provenance.js +3 -2
  468. package/dist/{purge-NDWV6WYD.js → purge-IRF47ZTX.js} +3 -3
  469. package/dist/{qmd-xJRrHFbA.d.ts → qmd-oh9VXz1d.d.ts} +11 -1
  470. package/dist/qmd-preflight.d.ts +5 -2
  471. package/dist/qmd-recall-cache.d.ts +3 -1
  472. package/dist/qmd-status.d.ts +57 -0
  473. package/dist/qmd-status.js +18 -0
  474. package/dist/qmd-status.js.map +1 -0
  475. package/dist/qmd.d.ts +5 -2
  476. package/dist/qmd.js +8 -4
  477. package/dist/recall-concurrency-config.d.ts +16 -0
  478. package/dist/recall-concurrency-config.js +10 -0
  479. package/dist/recall-concurrency-config.js.map +1 -0
  480. package/dist/recall-disclosure-escalation.d.ts +3 -1
  481. package/dist/recall-explain-renderer.d.ts +4 -1
  482. package/dist/recall-explain-renderer.js +3 -3
  483. package/dist/recall-memory-map.d.ts +26 -0
  484. package/dist/recall-memory-map.js +18 -0
  485. package/dist/recall-memory-map.js.map +1 -0
  486. package/dist/recall-planner-llm.d.ts +3 -1
  487. package/dist/recall-planner-llm.js +4 -4
  488. package/dist/recall-qos.js +2 -2
  489. package/dist/recall-state.d.ts +163 -2
  490. package/dist/recall-state.js +6 -3
  491. package/dist/recall-tag-filter.d.ts +3 -1
  492. package/dist/recall-timings.d.ts +11 -2
  493. package/dist/recall-timings.js +3 -1
  494. package/dist/recall-xray-cli.d.ts +3 -1
  495. package/dist/recall-xray-cli.js +4 -4
  496. package/dist/recall-xray-renderer.d.ts +3 -1
  497. package/dist/recall-xray-renderer.js +3 -3
  498. package/dist/recall-xray.d.ts +3 -1
  499. package/dist/recall-xray.js +2 -2
  500. package/dist/relevance.js +3 -3
  501. package/dist/replay/normalizers/chatgpt.d.ts +3 -1
  502. package/dist/replay/normalizers/claude.d.ts +3 -1
  503. package/dist/replay/normalizers/openclaw.d.ts +3 -1
  504. package/dist/replay/normalizers/shared.d.ts +3 -1
  505. package/dist/replay/runner.d.ts +3 -1
  506. package/dist/replay/types.d.ts +3 -1
  507. package/dist/rerank.d.ts +8 -1
  508. package/dist/rerank.js +3 -1
  509. package/dist/resolution-3K27T4VO.js +18 -0
  510. package/dist/resolution-3K27T4VO.js.map +1 -0
  511. package/dist/resolve-auth-token.d.ts +3 -1
  512. package/dist/resolve-provider-secret.js +2 -2
  513. package/dist/resume-bundles.js +19 -14
  514. package/dist/retrieval-agents.d.ts +19 -16
  515. package/dist/retrieval-agents.js +3 -3
  516. package/dist/retrieval-tiers.d.ts +3 -1
  517. package/dist/routing/engine.d.ts +3 -1
  518. package/dist/routing/store.d.ts +3 -1
  519. package/dist/routing/store.js +2 -2
  520. package/dist/salvage-envelope.d.ts +18 -0
  521. package/dist/salvage-envelope.js +15 -0
  522. package/dist/salvage-envelope.js.map +1 -0
  523. package/dist/schemas.d.ts +24 -24
  524. package/dist/scope-profiles-EpJG7yKP.d.ts +30 -0
  525. package/dist/search/document-scanner.js +3 -2
  526. package/dist/search/embed-helper.d.ts +3 -1
  527. package/dist/search/embed-helper.js +3 -3
  528. package/dist/search/factory.d.ts +3 -1
  529. package/dist/search/factory.js +19 -17
  530. package/dist/search/index.d.ts +3 -1
  531. package/dist/search/index.js +23 -21
  532. package/dist/search/lancedb-backend.d.ts +3 -1
  533. package/dist/search/lancedb-backend.js +6 -5
  534. package/dist/search/meilisearch-backend.d.ts +3 -1
  535. package/dist/search/meilisearch-backend.js +6 -5
  536. package/dist/search/noop-backend.d.ts +3 -1
  537. package/dist/search/orama-backend.d.ts +3 -1
  538. package/dist/search/orama-backend.js +6 -5
  539. package/dist/search/port.d.ts +27 -2
  540. package/dist/search/port.js +1 -1
  541. package/dist/search/remote-backend.d.ts +3 -1
  542. package/dist/search/remote-backend.js +3 -3
  543. package/dist/secure-store/index.d.ts +12 -1
  544. package/dist/secure-store/index.js +6 -3
  545. package/dist/{semantic-consolidation-fvdZRa5Z.d.ts → semantic-consolidation-DILZEQq3.d.ts} +1 -1
  546. package/dist/semantic-consolidation.d.ts +4 -2
  547. package/dist/semantic-consolidation.js +41 -32
  548. package/dist/semantic-rule-promotion.js +37 -27
  549. package/dist/semantic-rule-verifier.d.ts +3 -1
  550. package/dist/semantic-rule-verifier.js +38 -28
  551. package/dist/session-observer-bands.d.ts +3 -1
  552. package/dist/session-observer-state.d.ts +3 -1
  553. package/dist/session-observer-state.js +2 -2
  554. package/dist/session-transcript-migration.js +2 -2
  555. package/dist/shared-context/manager.d.ts +3 -1
  556. package/dist/shared-context/manager.js +3 -3
  557. package/dist/signal.d.ts +3 -1
  558. package/dist/{storage-CrvzaM9C.d.ts → storage-NIyKYD8g.d.ts} +505 -110
  559. package/dist/storage-guards.js +2 -2
  560. package/dist/storage.d.ts +6 -3
  561. package/dist/storage.js +41 -29
  562. package/dist/structured-attributes.d.ts +23 -1
  563. package/dist/structured-attributes.js +7 -1
  564. package/dist/summarizer.d.ts +3 -1
  565. package/dist/summarizer.js +11 -11
  566. package/dist/summary-snapshot.d.ts +3 -1
  567. package/dist/summary-snapshot.js +2 -2
  568. package/dist/temporal-index-batch.d.ts +1 -0
  569. package/dist/temporal-index-batch.js +10 -0
  570. package/dist/temporal-index-batch.js.map +1 -0
  571. package/dist/temporal-index-compat.d.ts +279 -1
  572. package/dist/temporal-index-compat.js +1 -1
  573. package/dist/temporal-index.d.ts +1 -225
  574. package/dist/temporal-index.js +35 -3
  575. package/dist/temporal-supersession.d.ts +5 -2
  576. package/dist/temporal-supersession.js +2 -2
  577. package/dist/temporal-timeline-recall.d.ts +3 -1
  578. package/dist/temporal-validity.d.ts +3 -1
  579. package/dist/threading.d.ts +3 -1
  580. package/dist/threading.js +2 -2
  581. package/dist/tier-migration.d.ts +5 -2
  582. package/dist/tier-routing.d.ts +3 -1
  583. package/dist/{tier-stats-KV7IWCJR.js → tier-stats-QEJSPFV2.js} +2 -2
  584. package/dist/tokens.js +3 -3
  585. package/dist/topics.d.ts +3 -1
  586. package/dist/transcript.d.ts +3 -1
  587. package/dist/transcript.js +2 -2
  588. package/dist/transfer/backup.js +3 -3
  589. package/dist/transfer/capsule-export.js +3 -3
  590. package/dist/transfer/capsule-import.js +6 -6
  591. package/dist/transfer/types.d.ts +24 -24
  592. package/dist/trust-score-stage.d.ts +75 -38
  593. package/dist/trust-score-stage.js +11 -3
  594. package/dist/trust-score.d.ts +3 -1
  595. package/dist/{types-Bl4BWDM8.d.ts → types-D_fG2ix8.d.ts} +49 -63
  596. package/dist/types-continuity.d.ts +52 -0
  597. package/dist/types-continuity.js +1 -0
  598. package/dist/types-continuity.js.map +1 -0
  599. package/dist/types.d.ts +3 -1
  600. package/dist/types.js +1 -1
  601. package/dist/utility-runtime.d.ts +3 -1
  602. package/dist/utils/serialize-mutations.d.ts +31 -2
  603. package/dist/utils/serialize-mutations.js +1 -1
  604. package/dist/verified-recall.js +39 -29
  605. package/dist/write-envelope.d.ts +282 -0
  606. package/dist/write-envelope.js +35 -0
  607. package/dist/write-envelope.js.map +1 -0
  608. package/dist/write-quarantine.d.ts +79 -0
  609. package/dist/write-quarantine.js +11 -0
  610. package/dist/write-quarantine.js.map +1 -0
  611. package/dist/write-rate-limiter.d.ts +45 -0
  612. package/dist/write-rate-limiter.js +9 -0
  613. package/dist/write-rate-limiter.js.map +1 -0
  614. package/package.json +37 -2
  615. package/src/access-admin-ops-surface.ts +17 -7
  616. package/src/access-boundary.test.ts +28 -0
  617. package/src/access-boundary.ts +31 -2
  618. package/src/access-citation.test.ts +72 -0
  619. package/src/access-citation.ts +139 -0
  620. package/src/access-errors.ts +21 -0
  621. package/src/access-fingerprint-parity.test.ts +211 -0
  622. package/src/access-http-cancellation.test.ts +59 -21
  623. package/src/access-http.test.ts +90 -0
  624. package/src/access-http.ts +79 -79
  625. package/src/access-mcp-cancellation.test.ts +16 -7
  626. package/src/access-memory-search-fanout.test.ts +220 -0
  627. package/src/access-memory-search-fanout.ts +168 -0
  628. package/src/access-observe-quarantine.test.ts +120 -0
  629. package/src/access-observe-write-surface.ts +130 -11
  630. package/src/access-operations-batch.ts +2 -0
  631. package/src/access-recall-concurrency.ts +773 -0
  632. package/src/access-recall-response.ts +307 -0
  633. package/src/access-recall-surface.ts +100 -252
  634. package/src/access-service-coding-write.test.ts +22 -4
  635. package/src/access-service-health.test.ts +49 -0
  636. package/src/access-service-namespace.test.ts +486 -8
  637. package/src/access-service-observe-scope.test.ts +4 -0
  638. package/src/access-service-recall-concurrency.test.ts +1990 -0
  639. package/src/access-service.ts +182 -175
  640. package/src/access-surface-catalog.ts +1 -0
  641. package/src/access-token-capabilities.ts +16 -9
  642. package/src/active-recall.test.ts +35 -0
  643. package/src/active-recall.ts +7 -0
  644. package/src/bounded-jsonl-state.ts +93 -0
  645. package/src/buffer-session.test.ts +422 -5
  646. package/src/buffer-surprise-trigger.test.ts +41 -0
  647. package/src/buffer.ts +218 -3
  648. package/src/capabilities-cluster-b.test.ts +1 -0
  649. package/src/capabilities.ts +5 -6
  650. package/src/cli.ts +21 -22
  651. package/src/coding/architecture-surfaces.test.ts +6 -3
  652. package/src/coding/architecture-surfaces.ts +36 -25
  653. package/src/coding/decision-surfaces.ts +42 -34
  654. package/src/compounding/engine.test.ts +5 -1
  655. package/src/compounding/engine.ts +7 -7
  656. package/src/config-qmd.ts +89 -0
  657. package/src/config.test.ts +136 -0
  658. package/src/config.ts +122 -118
  659. package/src/connector-provenance-lifecycle.test.ts +4 -4
  660. package/src/connectors/coerce.test.ts +131 -0
  661. package/src/connectors/coerce.ts +80 -7
  662. package/src/contradiction/contradiction.test.ts +10 -6
  663. package/src/contradiction/resolution.ts +17 -4
  664. package/src/correction/correction-access-wiring.ts +62 -18
  665. package/src/cross-namespace-budget.test.ts +35 -0
  666. package/src/cross-namespace-budget.ts +86 -19
  667. package/src/explicit-capture.ts +54 -17
  668. package/src/extraction.ts +2 -3
  669. package/src/faithfulness-graph-guard.test.ts +16 -69
  670. package/src/graph-recall.test.ts +31 -0
  671. package/src/graph.test.ts +137 -1
  672. package/src/graph.ts +274 -17
  673. package/src/index.ts +22 -2
  674. package/src/local-llm.ts +4 -6
  675. package/src/logger.ts +16 -0
  676. package/src/maintenance/dreams-ledger.ts +100 -49
  677. package/src/maintenance/rebuild-memory-lifecycle-ledger-cli.ts +77 -0
  678. package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +373 -9
  679. package/src/memory-cache.ts +70 -0
  680. package/src/memory-lifecycle-ledger-utils.ts +51 -5
  681. package/src/namespaces/principal.test.ts +37 -1
  682. package/src/namespaces/principal.ts +48 -0
  683. package/src/namespaces/search.test.ts +79 -5
  684. package/src/namespaces/search.ts +59 -6
  685. package/src/namespaces/storage.ts +45 -1
  686. package/src/offline-sync-exclude-globs.ts +58 -0
  687. package/src/offline-sync-impression-drain.test.ts +316 -0
  688. package/src/offline-sync-impression-drain.ts +260 -0
  689. package/src/offline-sync.test.ts +32 -5
  690. package/src/offline-sync.ts +1 -14
  691. package/src/operator-toolkit.ts +1 -1
  692. package/src/orchestration/batch-deindex-flush.test.ts +146 -0
  693. package/src/orchestration/consolidation-run.ts +96 -85
  694. package/src/orchestration/extraction-envelope.ts +94 -0
  695. package/src/orchestration/extraction-persist-promotion.ts +134 -0
  696. package/src/orchestration/extraction-persist.ts +325 -260
  697. package/src/orchestration/extraction-run.test.ts +1 -1
  698. package/src/orchestration/extraction-run.ts +2 -2
  699. package/src/orchestration/graph-recall-coordinator.ts +18 -7
  700. package/src/orchestration/lifecycle-policy-coordinator.ts +63 -48
  701. package/src/orchestration/maintenance.test.ts +2103 -3
  702. package/src/orchestration/maintenance.ts +581 -2
  703. package/src/orchestration/namespace-read-fanout.ts +14 -8
  704. package/src/orchestration/orchestrator-helpers.ts +8 -1
  705. package/src/orchestration/orchestrator-init.ts +22 -6
  706. package/src/orchestration/orchestrator-namespace-scope.ts +71 -0
  707. package/src/orchestration/persistence-index.ts +64 -9
  708. package/src/orchestration/qmd-result-resolver.ts +27 -0
  709. package/src/orchestration/recall-entry.ts +34 -19
  710. package/src/orchestration/recall-internal.ts +207 -79
  711. package/src/orchestration/recall-rerank-coordinator.ts +229 -82
  712. package/src/orchestration/recall-result-formatter.ts +76 -20
  713. package/src/orchestration/recall-search-pipeline.test.ts +70 -0
  714. package/src/orchestration/recall-search-pipeline.ts +102 -129
  715. package/src/orchestration/recall-section-coordinator.ts +315 -85
  716. package/src/orchestration/semantic-consolidation-coordinator.ts +69 -37
  717. package/src/orchestration/session-context.ts +21 -0
  718. package/src/orchestration/workspace-ops.test.ts +348 -0
  719. package/src/orchestration/workspace-ops.ts +172 -34
  720. package/src/orchestrator-chunked-hash-durability.test.ts +126 -0
  721. package/src/orchestrator-connector-dedup.test.ts +1061 -40
  722. package/src/orchestrator-flush.test.ts +155 -39
  723. package/src/orchestrator-nonchunked-hash-durability.test.ts +121 -0
  724. package/src/orchestrator-source-attribution.test.ts +9 -9
  725. package/src/orchestrator.ts +166 -150
  726. package/src/prioritized-embed.ts +123 -0
  727. package/src/proactive-contention.test.ts +80 -0
  728. package/src/proactive-contention.ts +39 -0
  729. package/src/procedural/procedure-miner.test.ts +19 -14
  730. package/src/procedural/procedure-miner.ts +44 -13
  731. package/src/qmd-client.test.ts +111 -1
  732. package/src/qmd-status.ts +186 -0
  733. package/src/qmd.test.ts +22 -0
  734. package/src/qmd.ts +60 -48
  735. package/src/recall-budget-config.test.ts +30 -0
  736. package/src/recall-cold-deadline.test.ts +149 -0
  737. package/src/recall-concurrency-config.ts +28 -0
  738. package/src/recall-memory-map.ts +74 -0
  739. package/src/recall-rerank-coordinator.test.ts +508 -0
  740. package/src/recall-state.test.ts +825 -1
  741. package/src/recall-state.ts +406 -7
  742. package/src/recall-timings-access.test.ts +41 -0
  743. package/src/recall-timings.ts +16 -0
  744. package/src/relay/mission-access.test.ts +5 -5
  745. package/src/rerank.ts +22 -0
  746. package/src/retrieval-agents.test.ts +79 -0
  747. package/src/retrieval-agents.ts +71 -19
  748. package/src/salvage-envelope.ts +28 -0
  749. package/src/scopes/scope-plan.test.ts +42 -0
  750. package/src/scopes/scope-plan.ts +45 -0
  751. package/src/search/port.ts +24 -0
  752. package/src/secure-store/index.ts +1 -0
  753. package/src/secure-store/secure-fs.ts +48 -0
  754. package/src/secure-store/secure-store.test.ts +40 -0
  755. package/src/semantic-rule-promotion.ts +13 -4
  756. package/src/storage/content-hash-index.ts +655 -0
  757. package/src/storage/entity-timeline.ts +473 -0
  758. package/src/storage/memory-lifecycle-ledger-access.ts +603 -0
  759. package/src/storage/secure-line-reader.test.ts +197 -0
  760. package/src/storage/secure-line-reader.ts +181 -3
  761. package/src/storage-append-header-sniff.test.ts +195 -0
  762. package/src/storage-behavior-signals-cache.test.ts +237 -0
  763. package/src/storage-contract/cache-coherence.test.ts +96 -1
  764. package/src/storage-contract/content-hash.test.ts +47 -2
  765. package/src/storage-fact-hash-index-defer.test.ts +1200 -0
  766. package/src/storage-memory-lifecycle-ledger.test.ts +1146 -3
  767. package/src/storage.ts +942 -903
  768. package/src/structured-attributes.ts +52 -0
  769. package/src/temporal-index-batch.ts +112 -0
  770. package/src/temporal-index.test.ts +193 -0
  771. package/src/temporal-index.ts +49 -53
  772. package/src/testing/lifecycle-matrix.test.ts +212 -0
  773. package/src/testing/lifecycle-matrix.ts +271 -0
  774. package/src/testing/orchestrator-lite.ts +236 -0
  775. package/src/testing/subjects/dreams-ledger.test.ts +67 -0
  776. package/src/testing/subjects/extraction-lifecycle.test.ts +442 -0
  777. package/src/testing/subjects/lifecycle-ledger.test.ts +93 -0
  778. package/src/testing/subjects/recall-budget.test.ts +461 -0
  779. package/src/testing/subjects/serialized-write-chain.test.ts +224 -0
  780. package/src/trust-score-stage.test.ts +154 -0
  781. package/src/trust-score-stage.ts +216 -53
  782. package/src/types-continuity.ts +57 -0
  783. package/src/types.ts +66 -77
  784. package/src/utils/path-containment.test.ts +75 -0
  785. package/src/utils/path-containment.ts +76 -0
  786. package/src/utils/serialize-mutations.test.ts +122 -0
  787. package/src/utils/serialize-mutations.ts +68 -3
  788. package/src/wearables/memory-gen.test.ts +31 -9
  789. package/src/wearables/memory-gen.ts +95 -55
  790. package/src/wearables/pipeline.test.ts +10 -7
  791. package/src/wearables/service.test.ts +6 -8
  792. package/src/wearables/service.ts +2 -2
  793. package/src/write-envelope.extension.test.ts +111 -0
  794. package/src/write-envelope.test.ts +683 -0
  795. package/src/write-envelope.ts +902 -0
  796. package/src/write-quarantine.test.ts +193 -0
  797. package/src/write-quarantine.ts +241 -0
  798. package/src/write-rate-limiter.test.ts +105 -0
  799. package/src/write-rate-limiter.ts +135 -0
  800. package/src/write-sealed-parity.test.ts +204 -0
  801. package/dist/chunk-2VN4JAX7.js.map +0 -1
  802. package/dist/chunk-37CTGERM.js.map +0 -1
  803. package/dist/chunk-7ICYQUOL.js.map +0 -1
  804. package/dist/chunk-AD6AMF2N.js.map +0 -1
  805. package/dist/chunk-AHLPZ2SF.js.map +0 -1
  806. package/dist/chunk-ASKEM4AR.js.map +0 -1
  807. package/dist/chunk-B3F4CWQR.js.map +0 -1
  808. package/dist/chunk-BCDKBDSY.js.map +0 -1
  809. package/dist/chunk-BFPETA3S.js.map +0 -1
  810. package/dist/chunk-BFVEEB7W.js.map +0 -1
  811. package/dist/chunk-E5BK6CGV.js +0 -335
  812. package/dist/chunk-E5BK6CGV.js.map +0 -1
  813. package/dist/chunk-EEARRVZ2.js.map +0 -1
  814. package/dist/chunk-F7AU34FC.js.map +0 -1
  815. package/dist/chunk-FBNWOMCE.js.map +0 -1
  816. package/dist/chunk-FTYDWKO6.js.map +0 -1
  817. package/dist/chunk-GMSDQUCR.js.map +0 -1
  818. package/dist/chunk-GWTFLF2P.js.map +0 -1
  819. package/dist/chunk-IFJ23BHU.js.map +0 -1
  820. package/dist/chunk-IKQTFQMI.js.map +0 -1
  821. package/dist/chunk-J2HSAU72.js.map +0 -1
  822. package/dist/chunk-JMBVCAR4.js.map +0 -1
  823. package/dist/chunk-KQDNAXOM.js.map +0 -1
  824. package/dist/chunk-KSBEEMQF.js.map +0 -1
  825. package/dist/chunk-KSNVDPDF.js.map +0 -1
  826. package/dist/chunk-LC7OODWZ.js.map +0 -1
  827. package/dist/chunk-LTJAMRGI.js.map +0 -1
  828. package/dist/chunk-ND5M64JJ.js.map +0 -1
  829. package/dist/chunk-NUWYOKS6.js.map +0 -1
  830. package/dist/chunk-NWTGD4AN.js.map +0 -1
  831. package/dist/chunk-OBMR4VIX.js.map +0 -1
  832. package/dist/chunk-ODPLEWB6.js.map +0 -1
  833. package/dist/chunk-PHK3HARR.js +0 -32
  834. package/dist/chunk-PHK3HARR.js.map +0 -1
  835. package/dist/chunk-RANHFI3P.js +0 -8
  836. package/dist/chunk-RANHFI3P.js.map +0 -1
  837. package/dist/chunk-RBQLJSJ7.js.map +0 -1
  838. package/dist/chunk-RULE4VG5.js.map +0 -1
  839. package/dist/chunk-SVOZFLIQ.js +0 -16
  840. package/dist/chunk-SVOZFLIQ.js.map +0 -1
  841. package/dist/chunk-TXLGHDHS.js.map +0 -1
  842. package/dist/chunk-UN3CH3CO.js +0 -118
  843. package/dist/chunk-UN3CH3CO.js.map +0 -1
  844. package/dist/chunk-VSMRDBFT.js +0 -17
  845. package/dist/chunk-VSMRDBFT.js.map +0 -1
  846. package/dist/chunk-WU46YPY6.js.map +0 -1
  847. package/dist/chunk-X47LWMKA.js.map +0 -1
  848. package/dist/chunk-X64YLC24.js.map +0 -1
  849. package/dist/chunk-XVLEGMJI.js.map +0 -1
  850. package/dist/chunk-YF6FMLBT.js.map +0 -1
  851. package/dist/resolution-VH4NGENZ.js +0 -12
  852. /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-citation.js.map} +0 -0
  853. /package/dist/{contradiction-scan-J57T5QV7.js.map → access-memory-search-fanout.js.map} +0 -0
  854. /package/dist/{dreams-ledger-6UODQOX6.js.map → access-recall-concurrency.js.map} +0 -0
  855. /package/dist/{graph-edge-decay-FO7AM4UR.js.map → access-recall-response.js.map} +0 -0
  856. /package/dist/{resolution-VH4NGENZ.js.map → bounded-jsonl-state.js.map} +0 -0
  857. /package/dist/{capsule-merge-Z2HUO6IK.js.map → capsule-merge-5KKJQBWQ.js.map} +0 -0
  858. /package/dist/{chunk-A3GOMRJG.js.map → chunk-23L3IXFD.js.map} +0 -0
  859. /package/dist/{chunk-MRHFJV44.js.map → chunk-2DQ2XREU.js.map} +0 -0
  860. /package/dist/{chunk-L2B3EE7B.js.map → chunk-2QMKMVHH.js.map} +0 -0
  861. /package/dist/{chunk-6CALL5VN.js.map → chunk-2UFO7X4Q.js.map} +0 -0
  862. /package/dist/{chunk-2MR3MFQB.js.map → chunk-2VEOKUPP.js.map} +0 -0
  863. /package/dist/{chunk-RHOZ5LLO.js.map → chunk-347WD2LE.js.map} +0 -0
  864. /package/dist/{chunk-5S7Z5SHA.js.map → chunk-34RC5RT7.js.map} +0 -0
  865. /package/dist/{chunk-KGWXHWAH.js.map → chunk-3QAH2S33.js.map} +0 -0
  866. /package/dist/{chunk-ZIDSD6VC.js.map → chunk-3Y224NYC.js.map} +0 -0
  867. /package/dist/{chunk-VFSP73LP.js.map → chunk-45GF7Y3V.js.map} +0 -0
  868. /package/dist/{chunk-OEHCMALU.js.map → chunk-5I4M26Q7.js.map} +0 -0
  869. /package/dist/{chunk-2E6G7FCE.js.map → chunk-5Q7EVV4X.js.map} +0 -0
  870. /package/dist/{chunk-VZ5OJE4X.js.map → chunk-5VQJBH2G.js.map} +0 -0
  871. /package/dist/{chunk-PUBTHW5O.js.map → chunk-6DRPPQEJ.js.map} +0 -0
  872. /package/dist/{chunk-4LEF6XWJ.js.map → chunk-6O22TZSB.js.map} +0 -0
  873. /package/dist/{chunk-ZQ7UBUZ6.js.map → chunk-6QYST5XN.js.map} +0 -0
  874. /package/dist/{chunk-W4PGTAS2.js.map → chunk-6WAFYNVT.js.map} +0 -0
  875. /package/dist/{chunk-R5VAVVHP.js.map → chunk-72A4L5JJ.js.map} +0 -0
  876. /package/dist/{chunk-3FAMU5TX.js.map → chunk-A6USBZTO.js.map} +0 -0
  877. /package/dist/{chunk-TXVD7PLT.js.map → chunk-A72SWTWW.js.map} +0 -0
  878. /package/dist/{chunk-FC77VUX5.js.map → chunk-A7Q43EUZ.js.map} +0 -0
  879. /package/dist/{chunk-UJBESW6X.js.map → chunk-AE7WMV4V.js.map} +0 -0
  880. /package/dist/{chunk-OKUEZOGW.js.map → chunk-AITCS2RC.js.map} +0 -0
  881. /package/dist/{chunk-OC35KQIN.js.map → chunk-AJ7QCSIS.js.map} +0 -0
  882. /package/dist/{chunk-FNMFJWBY.js.map → chunk-ANEUPF6O.js.map} +0 -0
  883. /package/dist/{chunk-2A3OXAP2.js.map → chunk-B7RWBZXW.js.map} +0 -0
  884. /package/dist/{chunk-N3JDIRBW.js.map → chunk-BLAKS3CP.js.map} +0 -0
  885. /package/dist/{chunk-M7VGG3ID.js.map → chunk-BV5WMSIJ.js.map} +0 -0
  886. /package/dist/{chunk-2BFLDH5F.js.map → chunk-BXLOS5AJ.js.map} +0 -0
  887. /package/dist/{chunk-UXKMMNJD.js.map → chunk-C23BYTJV.js.map} +0 -0
  888. /package/dist/{chunk-JZHHREBL.js.map → chunk-C3AKM55W.js.map} +0 -0
  889. /package/dist/{chunk-RP7YXA67.js.map → chunk-C5JF6EZ7.js.map} +0 -0
  890. /package/dist/{chunk-DDGV7NAX.js.map → chunk-CQMEM5CC.js.map} +0 -0
  891. /package/dist/{chunk-A7B4RMQG.js.map → chunk-CXZQOVIE.js.map} +0 -0
  892. /package/dist/{chunk-7N2RUBAP.js.map → chunk-DDA47TES.js.map} +0 -0
  893. /package/dist/{chunk-64SE4X5G.js.map → chunk-DEBHXHJR.js.map} +0 -0
  894. /package/dist/{chunk-EHK6KZHK.js.map → chunk-DNHVPZOD.js.map} +0 -0
  895. /package/dist/{chunk-437S3G37.js.map → chunk-DQEMWVMT.js.map} +0 -0
  896. /package/dist/{chunk-LZZNTPLR.js.map → chunk-DX3XRGLH.js.map} +0 -0
  897. /package/dist/{chunk-7MW3CVLD.js.map → chunk-ES36HTSC.js.map} +0 -0
  898. /package/dist/{chunk-TN7THW5U.js.map → chunk-ET356KSB.js.map} +0 -0
  899. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  900. /package/dist/{chunk-7LFXDGLH.js.map → chunk-F7GAIWFA.js.map} +0 -0
  901. /package/dist/{chunk-IKSELOIV.js.map → chunk-FFPJINJE.js.map} +0 -0
  902. /package/dist/{chunk-V53GJUSL.js.map → chunk-FUTF72MJ.js.map} +0 -0
  903. /package/dist/{chunk-5UNWK7KU.js.map → chunk-GFPZS3F5.js.map} +0 -0
  904. /package/dist/{chunk-G6NKALRN.js.map → chunk-H4WG3UQZ.js.map} +0 -0
  905. /package/dist/{chunk-HLTAPHZZ.js.map → chunk-HNZKVLAN.js.map} +0 -0
  906. /package/dist/{chunk-5EZXHU36.js.map → chunk-HYQJQGD3.js.map} +0 -0
  907. /package/dist/{chunk-TRWPCR6P.js.map → chunk-JECBQBEX.js.map} +0 -0
  908. /package/dist/{chunk-HG32URJJ.js.map → chunk-JYIFGGEM.js.map} +0 -0
  909. /package/dist/{chunk-VA4WSW54.js.map → chunk-JYIJNNN3.js.map} +0 -0
  910. /package/dist/{chunk-L5HJQC36.js.map → chunk-K2TDK7GG.js.map} +0 -0
  911. /package/dist/{chunk-MKDRQAA3.js.map → chunk-KAVBJSBQ.js.map} +0 -0
  912. /package/dist/{chunk-4OKLXWDZ.js.map → chunk-KNB7IBBU.js.map} +0 -0
  913. /package/dist/{chunk-QLULYKWZ.js.map → chunk-KSXJOC4F.js.map} +0 -0
  914. /package/dist/{chunk-JI5AVXYO.js.map → chunk-LEPOP46W.js.map} +0 -0
  915. /package/dist/{chunk-O7BBFJLP.js.map → chunk-LTAVSOZW.js.map} +0 -0
  916. /package/dist/{chunk-2IZAGAJR.js.map → chunk-LULR4IUW.js.map} +0 -0
  917. /package/dist/{chunk-WMWVO45V.js.map → chunk-MC6XO7T4.js.map} +0 -0
  918. /package/dist/{chunk-D6RIFG4O.js.map → chunk-MNB5RMAY.js.map} +0 -0
  919. /package/dist/{chunk-67ZCJLJY.js.map → chunk-MYFQ3ED4.js.map} +0 -0
  920. /package/dist/{chunk-V3UE5I5F.js.map → chunk-MYVPLJOO.js.map} +0 -0
  921. /package/dist/{chunk-MTWKDOVO.js.map → chunk-NLE6TSLG.js.map} +0 -0
  922. /package/dist/{chunk-DBRYEIGY.js.map → chunk-OJP3N7UK.js.map} +0 -0
  923. /package/dist/{chunk-SMX5267P.js.map → chunk-OZKOPQCM.js.map} +0 -0
  924. /package/dist/{chunk-JCK7V6SN.js.map → chunk-P65YJEEL.js.map} +0 -0
  925. /package/dist/{chunk-MTR7BZQN.js.map → chunk-PBGWYM3H.js.map} +0 -0
  926. /package/dist/{chunk-AXTXN4T6.js.map → chunk-PWFCHOB3.js.map} +0 -0
  927. /package/dist/{chunk-FDPZVY6D.js.map → chunk-QFN4U7AG.js.map} +0 -0
  928. /package/dist/{chunk-K2ZLHESQ.js.map → chunk-QIEUU4UK.js.map} +0 -0
  929. /package/dist/{chunk-3D6AFGQR.js.map → chunk-QVT3E4CE.js.map} +0 -0
  930. /package/dist/{chunk-SFQ6QNL7.js.map → chunk-R6YJI3HE.js.map} +0 -0
  931. /package/dist/{chunk-5CJVLG3H.js.map → chunk-RFDD3B7W.js.map} +0 -0
  932. /package/dist/{chunk-JBPKEARU.js.map → chunk-RKNJBZ55.js.map} +0 -0
  933. /package/dist/{chunk-UTRJFDP2.js.map → chunk-S2BBISHU.js.map} +0 -0
  934. /package/dist/{chunk-IALLX7EE.js.map → chunk-S2FSBU4H.js.map} +0 -0
  935. /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
  936. /package/dist/{chunk-PK5XBDA6.js.map → chunk-U3ZAVRP3.js.map} +0 -0
  937. /package/dist/{chunk-4KJLYTLS.js.map → chunk-V57NLO5P.js.map} +0 -0
  938. /package/dist/{chunk-FLVL4L3L.js.map → chunk-VDX2J7OX.js.map} +0 -0
  939. /package/dist/{chunk-7XGSIUP5.js.map → chunk-W4FBGYGK.js.map} +0 -0
  940. /package/dist/{chunk-6SXVCD7W.js.map → chunk-W7IGLX4G.js.map} +0 -0
  941. /package/dist/{chunk-KAWTOYAW.js.map → chunk-WLZYGLJ4.js.map} +0 -0
  942. /package/dist/{chunk-W5NKDU24.js.map → chunk-WON4APQY.js.map} +0 -0
  943. /package/dist/{chunk-NKCPUHCI.js.map → chunk-WZFAPHIK.js.map} +0 -0
  944. /package/dist/{chunk-KQPJLK3X.js.map → chunk-XL24QQ63.js.map} +0 -0
  945. /package/dist/{chunk-4B4MUS5D.js.map → chunk-YROFNEG5.js.map} +0 -0
  946. /package/dist/{chunk-VRAD2FGO.js.map → chunk-Z54EM5YZ.js.map} +0 -0
  947. /package/dist/{first-start-migration-TI67YMLG.js.map → first-start-migration-CSRHCJIQ.js.map} +0 -0
  948. /package/dist/{forget-CQCLB5LB.js.map → forget-HRPLVB3W.js.map} +0 -0
  949. /package/dist/{purge-NDWV6WYD.js.map → purge-IRF47ZTX.js.map} +0 -0
  950. /package/dist/{tier-stats-KV7IWCJR.js.map → tier-stats-QEJSPFV2.js.map} +0 -0
@@ -18,7 +18,8 @@ import test from "node:test";
18
18
  import assert from "node:assert/strict";
19
19
  import os from "node:os";
20
20
  import path from "node:path";
21
- import { mkdir, mkdtemp, rm, symlink } from "node:fs/promises";
21
+ import { lstat, mkdir, mkdtemp, readdir, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
22
+ import { execFileSync } from "node:child_process";
22
23
 
23
24
  import { MaintenanceScheduler } from "./maintenance.js";
24
25
  import type { PluginConfig } from "../types.js";
@@ -27,9 +28,22 @@ import type {
27
28
  NamespaceSearchRouter,
28
29
  NamespaceUpdateResult,
29
30
  } from "../namespaces/search.js";
30
- import type { NamespaceCatalog } from "../namespaces/catalog.js";
31
- import { namespaceIdentityToken } from "../namespaces/identity.js";
31
+ import type { NamespaceCatalog, NamespaceRecord } from "../namespaces/catalog.js";
32
+ import { namespaceIdentityFromToken, namespaceIdentityToken } from "../namespaces/identity.js";
33
+ import { isSafeRouteNamespace } from "../routing/engine.js";
32
34
  import { readNamespaceMaintenanceStatuses } from "../maintenance/namespace-planner.js";
35
+ import { StorageManager } from "../storage.js";
36
+ import {
37
+ decryptFileBody,
38
+ encryptFileBody,
39
+ filePathAad,
40
+ isEncryptedFile,
41
+ SECURE_STORE_ENVELOPE_OVERHEAD_BYTES,
42
+ } from "../secure-store/secure-fs.js";
43
+ import { pendingLifecycleLedgerDir } from "../storage/memory-lifecycle-ledger-access.js";
44
+ import { listContainedSpillFiles } from "../utils/path-containment.js";
45
+ import { rebuildMemoryLifecycleLedger } from "../maintenance/rebuild-memory-lifecycle-ledger.js";
46
+ import { resolveNamespaceStorageRoot } from "../namespaces/storage.js";
33
47
 
34
48
  /** Build a fixture PluginConfig. Cast bridges a partial fixture to the full
35
49
  * contract — only the fields the scheduler/planner read are populated. */
@@ -736,3 +750,2089 @@ test("MaintenanceScheduler reads a runtime-swapped qmd backend via getQmd (regre
736
750
  scheduler.dispose();
737
751
  }
738
752
  });
753
+
754
+ // ───────────────────────────────────────────────────────────────────────────
755
+ // Lifecycle-ledger auto-compaction (issue #1910)
756
+ // ───────────────────────────────────────────────────────────────────────────
757
+
758
+ /** Access the private size-gated compaction trigger for focused testing. The
759
+ * `lifecycleLedgerMaxBytes` seam lets a test inject a tiny bound so the
760
+ * bounding + post-write verification path is exercised without a 400MB fixture
761
+ * (#2033). */
762
+ interface CompactableScheduler {
763
+ maybeCompactMemoryLifecycleLedger(): Promise<void>;
764
+ lifecycleLedgerMaxBytes: number;
765
+ // Arming this to `Date.now()` puts a focused test inside the min-interval
766
+ // window so it can prove the throttle path (#2033).
767
+ lastLifecycleCompactionAtMs: number;
768
+ dispose(): void;
769
+ }
770
+
771
+ async function seedMemoryDirWithOversizedLedger(
772
+ padBytes: number,
773
+ ): Promise<{ memoryDir: string; ledgerPath: string }> {
774
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-"));
775
+ await mkdir(path.join(memoryDir, "facts", "2026-03-08"), { recursive: true });
776
+ await writeFile(
777
+ path.join(memoryDir, "facts", "2026-03-08", "fact-1.md"),
778
+ `---
779
+ id: fact-1
780
+ category: fact
781
+ created: 2026-03-08T00:00:00.000Z
782
+ updated: 2026-03-08T01:00:00.000Z
783
+ source: test
784
+ confidence: 0.8
785
+ confidenceTier: implied
786
+ tags: ["alpha"]
787
+ ---
788
+
789
+ alpha
790
+ `,
791
+ "utf-8",
792
+ );
793
+ const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
794
+ await mkdir(path.dirname(ledgerPath), { recursive: true });
795
+ // Oversized legacy ledger: many junk rows the rebuild will discard (it
796
+ // reconstructs from frontmatter), so the compacted output is far smaller.
797
+ const line = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
798
+ const rows = Math.ceil(padBytes / line.length);
799
+ await writeFile(ledgerPath, line.repeat(rows), "utf-8");
800
+ return { memoryDir, ledgerPath };
801
+ }
802
+
803
+ function buildCompactionScheduler(config: PluginConfig): CompactableScheduler {
804
+ return new MaintenanceScheduler({
805
+ config,
806
+ getQmd: () => stubQmd(),
807
+ // The compaction path never touches the router/catalog.
808
+ namespaceSearchRouter: {} as unknown as NamespaceSearchRouter,
809
+ namespaceCatalog: {} as unknown as NamespaceCatalog,
810
+ }) as unknown as CompactableScheduler;
811
+ }
812
+
813
+ test("auto-compaction shrinks an oversized ledger and writes a verbatim backup", async () => {
814
+ const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(4096);
815
+ try {
816
+ const before = await readFile(ledgerPath, "utf-8");
817
+ const beforeSize = (await stat(ledgerPath)).size;
818
+ const scheduler = buildCompactionScheduler(
819
+ fixtureConfig({
820
+ memoryDir,
821
+ memoryLifecycleLedgerCompactBytes: 1024,
822
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
823
+ }),
824
+ );
825
+ try {
826
+ await scheduler.maybeCompactMemoryLifecycleLedger();
827
+ } finally {
828
+ scheduler.dispose();
829
+ }
830
+
831
+ const afterSize = (await stat(ledgerPath)).size;
832
+ assert.ok(afterSize < beforeSize, "compacted ledger must be smaller");
833
+ const rebuilt = (await readFile(ledgerPath, "utf-8")).trim().split("\n").map((l) => JSON.parse(l));
834
+ assert.deepEqual(rebuilt.map((r) => r.eventType), ["created", "updated"]);
835
+
836
+ // A verbatim backup of the original ledger must exist under archive/.
837
+ const archiveRoot = path.join(memoryDir, "archive", "memory-lifecycle-ledger");
838
+ const stamps = await readdir(archiveRoot);
839
+ assert.equal(stamps.length, 1);
840
+ const backup = await readFile(
841
+ path.join(archiveRoot, stamps[0]!, "state", "memory-lifecycle-ledger.jsonl"),
842
+ "utf-8",
843
+ );
844
+ assert.equal(backup, before, "backup must be the verbatim original ledger");
845
+ } finally {
846
+ await rm(memoryDir, { recursive: true, force: true });
847
+ }
848
+ });
849
+
850
+ test("auto-compaction preserves append-only lifecycle events with no frontmatter equivalent (#1910)", async () => {
851
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-preserve-"));
852
+ try {
853
+ await mkdir(path.join(memoryDir, "facts", "2026-03-08"), { recursive: true });
854
+ await writeFile(
855
+ path.join(memoryDir, "facts", "2026-03-08", "fact-1.md"),
856
+ `---
857
+ id: fact-1
858
+ category: fact
859
+ created: 2026-03-08T00:00:00.000Z
860
+ updated: 2026-03-08T01:00:00.000Z
861
+ source: test
862
+ confidence: 0.8
863
+ confidenceTier: implied
864
+ tags: ["alpha"]
865
+ ---
866
+
867
+ alpha
868
+ `,
869
+ "utf-8",
870
+ );
871
+ const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
872
+ await mkdir(path.dirname(ledgerPath), { recursive: true });
873
+ const appendOnly = {
874
+ eventId: "capture-1",
875
+ memoryId: "fact-1",
876
+ eventType: "explicit_capture_accepted",
877
+ timestamp: "2026-03-08T02:00:00.000Z",
878
+ actor: "explicit-capture",
879
+ ruleVersion: "memory-lifecycle-ledger.v1",
880
+ };
881
+ // Oversized: junk pad rows the rebuild discards + one valid append-only
882
+ // event frontmatter cannot reconstruct and MUST survive compaction.
883
+ const pad = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
884
+ await writeFile(
885
+ ledgerPath,
886
+ pad.repeat(Math.ceil(4096 / pad.length)) + `${JSON.stringify(appendOnly)}\n`,
887
+ "utf-8",
888
+ );
889
+
890
+ const scheduler = buildCompactionScheduler(
891
+ fixtureConfig({
892
+ memoryDir,
893
+ memoryLifecycleLedgerCompactBytes: 1024,
894
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
895
+ }),
896
+ );
897
+ try {
898
+ await scheduler.maybeCompactMemoryLifecycleLedger();
899
+ } finally {
900
+ scheduler.dispose();
901
+ }
902
+
903
+ const rebuilt = (await readFile(ledgerPath, "utf-8"))
904
+ .trim()
905
+ .split("\n")
906
+ .map((l) => JSON.parse(l) as { eventId: string; eventType: string; legacy?: boolean });
907
+ const eventTypes = rebuilt.map((r) => r.eventType);
908
+ // Frontmatter-derived rows are reconstructed AND the append-only capture row
909
+ // is carried over (a bare rebuild dropped it silently before this fix).
910
+ assert.ok(eventTypes.includes("created"));
911
+ assert.ok(eventTypes.includes("updated"));
912
+ assert.ok(
913
+ rebuilt.some((r) => r.eventId === "capture-1" && r.eventType === "explicit_capture_accepted"),
914
+ "append-only capture event must survive background compaction",
915
+ );
916
+ // Junk padding rows are still discarded — compaction shrank the ledger.
917
+ assert.ok(!rebuilt.some((r) => r.legacy === true), "junk rows discarded");
918
+ } finally {
919
+ await rm(memoryDir, { recursive: true, force: true });
920
+ }
921
+ });
922
+
923
+ test("auto-compaction is disabled when memoryLifecycleLedgerCompactBytes is 0", async () => {
924
+ const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(4096);
925
+ try {
926
+ const before = await readFile(ledgerPath, "utf-8");
927
+ const scheduler = buildCompactionScheduler(
928
+ fixtureConfig({
929
+ memoryDir,
930
+ memoryLifecycleLedgerCompactBytes: 0,
931
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
932
+ }),
933
+ );
934
+ try {
935
+ await scheduler.maybeCompactMemoryLifecycleLedger();
936
+ } finally {
937
+ scheduler.dispose();
938
+ }
939
+ assert.equal(await readFile(ledgerPath, "utf-8"), before, "disabled: ledger untouched");
940
+ await assert.rejects(() => readdir(path.join(memoryDir, "archive", "memory-lifecycle-ledger")));
941
+ } finally {
942
+ await rm(memoryDir, { recursive: true, force: true });
943
+ }
944
+ });
945
+
946
+ test("auto-compaction min-interval throttle prevents a second run within the window", async () => {
947
+ const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(4096);
948
+ try {
949
+ const scheduler = buildCompactionScheduler(
950
+ fixtureConfig({
951
+ memoryDir,
952
+ memoryLifecycleLedgerCompactBytes: 1024,
953
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
954
+ }),
955
+ );
956
+ try {
957
+ await scheduler.maybeCompactMemoryLifecycleLedger();
958
+ const compactedSize = (await stat(ledgerPath)).size;
959
+
960
+ // Re-grow the ledger past the threshold; a second immediate call is
961
+ // inside the min-interval window and must be throttled (no recompaction).
962
+ const line = '{"legacy":true,"pad":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"}\n';
963
+ await writeFile(ledgerPath, line.repeat(Math.ceil(4096 / line.length)), "utf-8");
964
+ const regrownSize = (await stat(ledgerPath)).size;
965
+
966
+ await scheduler.maybeCompactMemoryLifecycleLedger();
967
+ assert.equal((await stat(ledgerPath)).size, regrownSize, "throttled: ledger not recompacted");
968
+ assert.ok(regrownSize > compactedSize);
969
+ } finally {
970
+ scheduler.dispose();
971
+ }
972
+ } finally {
973
+ await rm(memoryDir, { recursive: true, force: true });
974
+ }
975
+ });
976
+
977
+ test("auto-compaction is a no-op when the ledger is below the threshold", async () => {
978
+ const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(256);
979
+ try {
980
+ const before = await readFile(ledgerPath, "utf-8");
981
+ const scheduler = buildCompactionScheduler(
982
+ fixtureConfig({
983
+ memoryDir,
984
+ memoryLifecycleLedgerCompactBytes: 1024 * 1024,
985
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
986
+ }),
987
+ );
988
+ try {
989
+ await scheduler.maybeCompactMemoryLifecycleLedger();
990
+ } finally {
991
+ scheduler.dispose();
992
+ }
993
+ assert.equal(await readFile(ledgerPath, "utf-8"), before, "under threshold: ledger untouched");
994
+ } finally {
995
+ await rm(memoryDir, { recursive: true, force: true });
996
+ }
997
+ });
998
+
999
+ test("auto-compaction singleflight: two requests racing through the over-cap probe compact once (#2033)", async () => {
1000
+ const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(4096);
1001
+ try {
1002
+ const scheduler = buildCompactionScheduler(
1003
+ fixtureConfig({
1004
+ memoryDir,
1005
+ memoryLifecycleLedgerCompactBytes: 1024,
1006
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
1007
+ }),
1008
+ ) as CompactableScheduler & {
1009
+ hasOverCapEncryptedLifecycleLedger: (targets: unknown) => Promise<boolean>;
1010
+ compactLifecycleLedgerTarget: (
1011
+ target: unknown,
1012
+ threshold: number,
1013
+ ) => Promise<"skipped" | "compacted" | "failed" | "deferred">;
1014
+ };
1015
+ try {
1016
+ // Inside the min-interval window so the awaited over-cap probe — the ONLY
1017
+ // await between the singleflight guard and claiming it — is exercised.
1018
+ scheduler.lastLifecycleCompactionAtMs = Date.now();
1019
+
1020
+ // Deterministically open the race window the reviewer flagged: gate the
1021
+ // over-cap probe so BOTH callers pass the early `lifecycleCompactionInFlight`
1022
+ // guard and park inside the same await, then release them together. An
1023
+ // over-cap encrypted ledger (probe => true) bypasses the throttle, so both
1024
+ // fall through toward claiming the guard.
1025
+ const { promise: probeGate, resolve: releaseProbe } = Promise.withResolvers<void>();
1026
+ // Resolves the instant the SECOND caller enters the probe, so the test
1027
+ // releases the gate on a real signal rather than a timed poll.
1028
+ const { promise: bothArrived, resolve: signalBothArrived } =
1029
+ Promise.withResolvers<void>();
1030
+ let probeArrivals = 0;
1031
+ scheduler.hasOverCapEncryptedLifecycleLedger = async () => {
1032
+ probeArrivals += 1;
1033
+ if (probeArrivals === 2) signalBothArrived();
1034
+ await probeGate;
1035
+ return true;
1036
+ };
1037
+
1038
+ // Count real compaction attempts. Each one rewrites the ledger and writes
1039
+ // a verbatim (400MB-class in production) backup, so a second call here is
1040
+ // exactly the duplicate expensive work singleflight must prevent.
1041
+ let compactions = 0;
1042
+ const realCompact = scheduler.compactLifecycleLedgerTarget.bind(scheduler);
1043
+ scheduler.compactLifecycleLedgerTarget = async (target, threshold) => {
1044
+ compactions += 1;
1045
+ return realCompact(target, threshold);
1046
+ };
1047
+
1048
+ const a = scheduler.maybeCompactMemoryLifecycleLedger();
1049
+ const b = scheduler.maybeCompactMemoryLifecycleLedger();
1050
+ // Both callers must reach the gated probe before either can claim the guard.
1051
+ await bothArrived;
1052
+ releaseProbe();
1053
+ await Promise.all([a, b]);
1054
+
1055
+ assert.equal(
1056
+ compactions,
1057
+ 1,
1058
+ "exactly one caller compacts; the racing caller must re-observe the singleflight guard after the probe",
1059
+ );
1060
+
1061
+ // Exactly one verbatim backup — proof no duplicate backup ran back-to-back.
1062
+ const archiveRoot = path.join(memoryDir, "archive", "memory-lifecycle-ledger");
1063
+ const stamps = await readdir(archiveRoot);
1064
+ assert.equal(stamps.length, 1, "only one compaction backup exists");
1065
+ assert.ok((await stat(ledgerPath)).size < 4096, "ledger was compacted once");
1066
+ } finally {
1067
+ scheduler.dispose();
1068
+ }
1069
+ } finally {
1070
+ await rm(memoryDir, { recursive: true, force: true });
1071
+ }
1072
+ });
1073
+
1074
+ /** Scheduler wired with a live secure-store-configured root storage (issue
1075
+ * #1910, Cursor Medium / Codex P2). */
1076
+ function buildCompactionSchedulerWithStorage(
1077
+ config: PluginConfig,
1078
+ getStorage: () => StorageManager,
1079
+ storageForNamespace?: (namespace: string) => Promise<StorageManager>,
1080
+ namespaceCatalog?: NamespaceCatalog,
1081
+ ): CompactableScheduler {
1082
+ return new MaintenanceScheduler({
1083
+ config,
1084
+ getQmd: () => stubQmd(),
1085
+ namespaceSearchRouter: {} as unknown as NamespaceSearchRouter,
1086
+ namespaceCatalog: (namespaceCatalog ?? ({} as unknown as NamespaceCatalog)),
1087
+ getStorage,
1088
+ storageForNamespace,
1089
+ }) as unknown as CompactableScheduler;
1090
+ }
1091
+
1092
+ test("auto-compaction leaves the throttle un-advanced after a failed run so it retries next pass", async () => {
1093
+ const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(4096);
1094
+ try {
1095
+ const beforeSize = (await stat(ledgerPath)).size;
1096
+ // A required-but-locked secure store makes the rewrite throw
1097
+ // SecureStoreLockedError, so the first compaction fails.
1098
+ const storage = new StorageManager(memoryDir);
1099
+ storage.setSecureStoreRequired(true);
1100
+ const scheduler = buildCompactionSchedulerWithStorage(
1101
+ fixtureConfig({
1102
+ memoryDir,
1103
+ memoryLifecycleLedgerCompactBytes: 1024,
1104
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
1105
+ }),
1106
+ () => storage,
1107
+ );
1108
+ try {
1109
+ await scheduler.maybeCompactMemoryLifecycleLedger();
1110
+ // Failed run must NOT shrink the ledger and must NOT arm the throttle.
1111
+ assert.equal((await stat(ledgerPath)).size, beforeSize, "failed run leaves ledger intact");
1112
+
1113
+ // Unlock the store, then call again INSIDE the min-interval window. If the
1114
+ // failed run had armed the throttle this second call would be skipped and
1115
+ // the ledger would stay oversized. Because it did not, the ledger compacts.
1116
+ storage.setSecureStoreKey(Buffer.alloc(32, 9));
1117
+ await scheduler.maybeCompactMemoryLifecycleLedger();
1118
+ assert.ok(
1119
+ (await stat(ledgerPath)).size < beforeSize,
1120
+ "still-eligible retry compacts once the failure clears",
1121
+ );
1122
+ } finally {
1123
+ scheduler.dispose();
1124
+ }
1125
+ } finally {
1126
+ await rm(memoryDir, { recursive: true, force: true });
1127
+ }
1128
+ });
1129
+
1130
+ test("auto-compaction rewrites the ledger encrypted at rest under a secure store", async () => {
1131
+ const { memoryDir, ledgerPath } = await seedMemoryDirWithOversizedLedger(4096);
1132
+ try {
1133
+ const before = await readFile(ledgerPath, "utf-8");
1134
+ const beforeSize = before.length;
1135
+ const key = Buffer.alloc(32, 5);
1136
+ const storage = new StorageManager(memoryDir);
1137
+ storage.setSecureStoreRequired(true);
1138
+ storage.setSecureStoreKey(key);
1139
+ const scheduler = buildCompactionSchedulerWithStorage(
1140
+ fixtureConfig({
1141
+ memoryDir,
1142
+ memoryLifecycleLedgerCompactBytes: 1024,
1143
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
1144
+ }),
1145
+ () => storage,
1146
+ );
1147
+ try {
1148
+ await scheduler.maybeCompactMemoryLifecycleLedger();
1149
+ } finally {
1150
+ scheduler.dispose();
1151
+ }
1152
+ // The rewritten ledger is encrypted at rest and smaller than the plaintext
1153
+ // original, and decrypts back to the rebuilt events via the same key.
1154
+ assert.ok(isEncryptedFile(await readFile(ledgerPath)), "compacted ledger must be encrypted");
1155
+ assert.ok((await stat(ledgerPath)).size < beforeSize);
1156
+ const rebuilt = await storage.readAllMemoryLifecycleEvents();
1157
+ assert.deepEqual(rebuilt.map((e) => e.eventType), ["created", "updated"]);
1158
+ // The existing ledger here is PLAINTEXT (only the rewrite encrypts), so its
1159
+ // backup is a directly-readable verbatim copy — the source-AAD re-encrypt only
1160
+ // applies when the EXISTING ledger is itself encrypted (#2033).
1161
+ const archiveRoot = path.join(memoryDir, "archive", "memory-lifecycle-ledger");
1162
+ const stamps = await readdir(archiveRoot);
1163
+ assert.equal(stamps.length, 1);
1164
+ assert.equal(
1165
+ await readFile(
1166
+ path.join(archiveRoot, stamps[0]!, "state", "memory-lifecycle-ledger.jsonl"),
1167
+ "utf-8",
1168
+ ),
1169
+ before,
1170
+ );
1171
+ } finally {
1172
+ await rm(memoryDir, { recursive: true, force: true });
1173
+ }
1174
+ });
1175
+
1176
+ test("auto-compaction re-encrypts an ENCRYPTED ledger's backup for the archive path so it decrypts there, not with the source AAD (#2033)", async () => {
1177
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-enc-backup-"));
1178
+ try {
1179
+ // A memory file so the rebuild reconstructs a small active ledger.
1180
+ await mkdir(path.join(memoryDir, "facts", "2026-03-08"), { recursive: true });
1181
+ await writeFile(
1182
+ path.join(memoryDir, "facts", "2026-03-08", "fact-1.md"),
1183
+ `---\nid: fact-1\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
1184
+ + `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
1185
+ + `confidenceTier: implied\ntags: ["alpha"]\n---\n\nalpha\n`,
1186
+ "utf-8",
1187
+ );
1188
+ const key = Buffer.alloc(32, 6);
1189
+ const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
1190
+ await mkdir(path.dirname(ledgerPath), { recursive: true });
1191
+ // An oversized ENCRYPTED existing ledger of valid legacy events. Its bytes
1192
+ // are path-bound to the SOURCE via AAD, so a byte copy to the archive path
1193
+ // would not decrypt there — the exact orphaning the fix prevents.
1194
+ const legacyIds = Array.from({ length: 80 }, (_, i) => `evt-legacy-${i}`);
1195
+ const plaintextLedger = legacyIds
1196
+ .map((id) => JSON.stringify({
1197
+ schemaVersion: 1, eventId: id, memoryId: "m-legacy", eventType: "note",
1198
+ timestamp: "2026-03-08T00:00:00.000Z", actor: "t", ruleVersion: "1",
1199
+ }))
1200
+ .join("\n") + "\n";
1201
+ await writeFile(ledgerPath, encryptFileBody(plaintextLedger, key, filePathAad(ledgerPath, memoryDir)));
1202
+ assert.ok(isEncryptedFile(await readFile(ledgerPath)), "precondition: source ledger encrypted");
1203
+
1204
+ const storage = new StorageManager(memoryDir);
1205
+ storage.setSecureStoreRequired(true);
1206
+ storage.setSecureStoreKey(key);
1207
+ const scheduler = buildCompactionSchedulerWithStorage(
1208
+ fixtureConfig({
1209
+ memoryDir,
1210
+ memoryLifecycleLedgerCompactBytes: 1024,
1211
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
1212
+ }),
1213
+ () => storage,
1214
+ );
1215
+ try {
1216
+ await scheduler.maybeCompactMemoryLifecycleLedger();
1217
+ } finally {
1218
+ scheduler.dispose();
1219
+ }
1220
+
1221
+ assert.ok(isEncryptedFile(await readFile(ledgerPath)), "compacted ledger stays encrypted at rest");
1222
+
1223
+ const archiveRoot = path.join(memoryDir, "archive", "memory-lifecycle-ledger");
1224
+ const stamps = await readdir(archiveRoot);
1225
+ assert.equal(stamps.length, 1);
1226
+ const backupPath = path.join(archiveRoot, stamps[0]!, "state", "memory-lifecycle-ledger.jsonl");
1227
+ const backupBytes = await readFile(backupPath);
1228
+ assert.ok(isEncryptedFile(backupBytes), "backup encrypted at rest (no plaintext leak)");
1229
+ // Decrypts AT THE BACKUP PATH — a directly readable recovery ledger.
1230
+ const decrypted = decryptFileBody(backupBytes, key, filePathAad(backupPath, memoryDir)).toString("utf8");
1231
+ const decryptedIds = decrypted
1232
+ .trim().split("\n").filter(Boolean)
1233
+ .map((l) => JSON.parse(l).eventId as string)
1234
+ .sort();
1235
+ assert.deepEqual(
1236
+ decryptedIds,
1237
+ [...legacyIds].sort(),
1238
+ "backup preserves exactly the prior ledger's events, decryptable at the archive path",
1239
+ );
1240
+ // A byte-for-byte copy would carry the SOURCE path's AAD and fail here.
1241
+ assert.throws(
1242
+ () => decryptFileBody(backupBytes, key, filePathAad(ledgerPath, memoryDir)),
1243
+ "backup must NOT decrypt under the SOURCE-path AAD (proves re-encrypt, not byte copy)",
1244
+ );
1245
+ } finally {
1246
+ await rm(memoryDir, { recursive: true, force: true });
1247
+ }
1248
+ });
1249
+
1250
+ test("mixed pass: a deferred encrypted target keeps the throttle un-armed so untouched ledgers retry (#2033)", async () => {
1251
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-mixed-"));
1252
+ try {
1253
+ const junk = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
1254
+ const junkBlob = junk.repeat(Math.ceil(4096 / junk.length));
1255
+
1256
+ // Root: plaintext oversized ledger with a reconstructable memory → compacts.
1257
+ await mkdir(path.join(root, "facts", "2026-03-08"), { recursive: true });
1258
+ await writeFile(
1259
+ path.join(root, "facts", "2026-03-08", "fact-root.md"),
1260
+ `---\nid: fact-root\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
1261
+ + `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
1262
+ + `confidenceTier: implied\ntags: ["alpha"]\n---\n\nalpha\n`,
1263
+ "utf-8",
1264
+ );
1265
+ const rootLedger = path.join(root, "state", "memory-lifecycle-ledger.jsonl");
1266
+ await mkdir(path.dirname(rootLedger), { recursive: true });
1267
+ await writeFile(rootLedger, junkBlob, "utf-8");
1268
+
1269
+ // Namespace: encrypted-at-rest oversized ledger whose secure store is LOCKED
1270
+ // (required, no key). Compaction must DEFER it (not skip) so this untouched,
1271
+ // still-oversized ledger keeps its retry eligibility.
1272
+ const key = Buffer.alloc(32, 5);
1273
+ const nsDir = path.join(root, "namespaces", namespaceIdentityToken("locked-ns"));
1274
+ const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
1275
+ await mkdir(path.dirname(nsLedger), { recursive: true });
1276
+ const encrypted = encryptFileBody(junkBlob, key, filePathAad(nsLedger, nsDir));
1277
+ await writeFile(nsLedger, encrypted);
1278
+
1279
+ const catalog = {
1280
+ enabled: true,
1281
+ listNamespaces: async (): Promise<NamespaceRecord[]> =>
1282
+ [{ namespace: "locked-ns", storageDir: nsDir } as unknown as NamespaceRecord],
1283
+ } as unknown as NamespaceCatalog;
1284
+ const nsStorage = new StorageManager(nsDir);
1285
+ nsStorage.setSecureStoreRequired(true); // required + no key → locked (deferred).
1286
+ const scheduler = buildCompactionSchedulerWithStorage(
1287
+ fixtureConfig({
1288
+ memoryDir: root,
1289
+ memoryLifecycleLedgerCompactBytes: 1024,
1290
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
1291
+ }),
1292
+ () => new StorageManager(root),
1293
+ async () => nsStorage,
1294
+ catalog,
1295
+ );
1296
+ try {
1297
+ const rootBefore = (await stat(rootLedger)).size;
1298
+ await scheduler.maybeCompactMemoryLifecycleLedger();
1299
+ assert.ok((await stat(rootLedger)).size < rootBefore, "root ledger compacted");
1300
+ assert.deepEqual(
1301
+ await readFile(nsLedger),
1302
+ encrypted,
1303
+ "encrypted+locked namespace ledger deferred, left byte-for-byte intact",
1304
+ );
1305
+
1306
+ // Re-grow the root within the min-interval window. Because a target was
1307
+ // DEFERRED (not a genuine no-op), the throttle must NOT have armed, so this
1308
+ // second in-window pass still recompacts — the #2033 throttle fix.
1309
+ await writeFile(rootLedger, junkBlob, "utf-8");
1310
+ const regrown = (await stat(rootLedger)).size;
1311
+ await scheduler.maybeCompactMemoryLifecycleLedger();
1312
+ assert.ok(
1313
+ (await stat(rootLedger)).size < regrown,
1314
+ "deferred target left the throttle un-armed: the second in-window pass recompacts",
1315
+ );
1316
+ } finally {
1317
+ scheduler.dispose();
1318
+ }
1319
+ } finally {
1320
+ await rm(root, { recursive: true, force: true });
1321
+ }
1322
+ });
1323
+
1324
+ test("maintenance drains a pending lifecycle-append spill even when compaction is disabled (#2033)", async () => {
1325
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-drain-"));
1326
+ try {
1327
+ const key = Buffer.alloc(32, 7);
1328
+ const storage = new StorageManager(memoryDir);
1329
+ storage.setSecureStoreKey(key);
1330
+ const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
1331
+ await mkdir(path.dirname(ledgerPath), { recursive: true });
1332
+ // An encrypted pending spill, exactly as a lock-timed-out append leaves it.
1333
+ const spillDir = pendingLifecycleLedgerDir(ledgerPath);
1334
+ await mkdir(spillDir, { recursive: true });
1335
+ const spillPath = path.join(spillDir, "spill.jsonl");
1336
+ const row =
1337
+ `{"eventId":"evt-spilled","memoryId":"m","eventType":"created",`
1338
+ + `"timestamp":"2026-03-08T00:00:00.000Z","actor":"t","ruleVersion":"1"}\n`;
1339
+ await writeFile(spillPath, encryptFileBody(row, key, filePathAad(spillPath, memoryDir)));
1340
+
1341
+ // Compaction disabled (threshold 0): the unconditional root drain must still
1342
+ // fold the spill back into the ledger so no lifecycle row is lost.
1343
+ const scheduler = buildCompactionSchedulerWithStorage(
1344
+ fixtureConfig({
1345
+ memoryDir,
1346
+ memoryLifecycleLedgerCompactBytes: 0,
1347
+ memoryLifecycleLedgerCompactMinIntervalMs: 1,
1348
+ }),
1349
+ () => storage,
1350
+ );
1351
+ try {
1352
+ await scheduler.maybeCompactMemoryLifecycleLedger();
1353
+ } finally {
1354
+ scheduler.dispose();
1355
+ }
1356
+
1357
+ const ids = (await storage.readAllMemoryLifecycleEvents()).map((e) => e.eventId);
1358
+ assert.ok(ids.includes("evt-spilled"), "spill drained into the ledger with compaction disabled");
1359
+ await assert.rejects(() => stat(spillPath), /ENOENT/, "spill file removed after drain");
1360
+ } finally {
1361
+ await rm(memoryDir, { recursive: true, force: true });
1362
+ }
1363
+ });
1364
+
1365
+ test("maintenance drains a namespace pending spill created after the throttle armed, before returning on the min-interval throttle (#2033)", async () => {
1366
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-drain-throttled-"));
1367
+ try {
1368
+ // Root: oversized plaintext ledger with a reconstructable memory so the
1369
+ // FIRST pass compacts and arms the min-interval throttle.
1370
+ const junk = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
1371
+ const junkBlob = junk.repeat(Math.ceil(4096 / junk.length));
1372
+ await mkdir(path.join(root, "facts", "2026-03-08"), { recursive: true });
1373
+ await writeFile(
1374
+ path.join(root, "facts", "2026-03-08", "fact-root.md"),
1375
+ `---\nid: fact-root\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
1376
+ + `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
1377
+ + `confidenceTier: implied\ntags: ["alpha"]\n---\n\nalpha\n`,
1378
+ "utf-8",
1379
+ );
1380
+ const rootLedger = path.join(root, "state", "memory-lifecycle-ledger.jsonl");
1381
+ await mkdir(path.dirname(rootLedger), { recursive: true });
1382
+ await writeFile(rootLedger, junkBlob, "utf-8");
1383
+
1384
+ // Namespace: a keyed (unlocked) secure store, no oversized ledger — so the
1385
+ // first pass SKIPS it (skip never blocks arming the throttle).
1386
+ const key = Buffer.alloc(32, 7);
1387
+ const token = namespaceIdentityToken("project-alpha");
1388
+ const nsDir = path.join(root, "namespaces", token);
1389
+ const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
1390
+ await mkdir(path.dirname(nsLedger), { recursive: true });
1391
+ const nsStorage = new StorageManager(nsDir);
1392
+ nsStorage.setSecureStoreKey(key);
1393
+ const catalog = {
1394
+ enabled: true,
1395
+ listNamespaces: async (): Promise<NamespaceRecord[]> =>
1396
+ [{ namespace: "project-alpha", storageDir: nsDir } as unknown as NamespaceRecord],
1397
+ } as unknown as NamespaceCatalog;
1398
+ const scheduler = buildCompactionSchedulerWithStorage(
1399
+ fixtureConfig({
1400
+ memoryDir: root,
1401
+ memoryLifecycleLedgerCompactBytes: 1024,
1402
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
1403
+ }),
1404
+ () => new StorageManager(root),
1405
+ async () => nsStorage,
1406
+ catalog,
1407
+ );
1408
+ try {
1409
+ // Pass 1 compacts the root and arms the throttle for the whole interval.
1410
+ const rootBefore = (await stat(rootLedger)).size;
1411
+ await scheduler.maybeCompactMemoryLifecycleLedger();
1412
+ assert.ok((await stat(rootLedger)).size < rootBefore, "root ledger compacted on the first pass");
1413
+
1414
+ // AFTER the throttle armed, a namespace impression spills (exactly as a
1415
+ // lock-timed-out append leaves it) — encrypted for the namespace store.
1416
+ const spillDir = pendingLifecycleLedgerDir(nsLedger);
1417
+ await mkdir(spillDir, { recursive: true });
1418
+ const spillPath = path.join(spillDir, "spill.jsonl");
1419
+ const row =
1420
+ `{"eventId":"evt-ns-spilled","memoryId":"m","eventType":"created",`
1421
+ + `"timestamp":"2026-03-08T00:00:00.000Z","actor":"t","ruleVersion":"1"}\n`;
1422
+ await writeFile(spillPath, encryptFileBody(row, key, filePathAad(spillPath, nsDir)));
1423
+
1424
+ // Pass 2 is inside the min-interval window: it WILL return on the throttle,
1425
+ // but it must first drain the namespace pending spill. Before the #2033
1426
+ // fix the throttle return preceded namespace draining and the spill was
1427
+ // stranded for the whole interval.
1428
+ await scheduler.maybeCompactMemoryLifecycleLedger();
1429
+ const ids = (await nsStorage.readAllMemoryLifecycleEvents()).map((e) => e.eventId);
1430
+ assert.ok(
1431
+ ids.includes("evt-ns-spilled"),
1432
+ "namespace spill drained before the throttle return, not stranded until the interval elapses",
1433
+ );
1434
+ await assert.rejects(() => stat(spillPath), /ENOENT/, "namespace spill file removed after drain");
1435
+ } finally {
1436
+ scheduler.dispose();
1437
+ }
1438
+ } finally {
1439
+ await rm(root, { recursive: true, force: true });
1440
+ }
1441
+ });
1442
+
1443
+ test("auto-compaction bounds per-namespace ledgers, not just the root state path", async () => {
1444
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-"));
1445
+ try {
1446
+ // Root ledger stays absent (nothing to compact); a namespace under
1447
+ // memoryDir/namespaces/<token>/ carries the oversized ledger.
1448
+ const token = namespaceIdentityToken("project-alpha");
1449
+ const nsDir = path.join(root, "namespaces", token);
1450
+ await mkdir(path.join(nsDir, "facts", "2026-03-08"), { recursive: true });
1451
+ await writeFile(
1452
+ path.join(nsDir, "facts", "2026-03-08", "fact-ns.md"),
1453
+ `---\nid: fact-ns\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
1454
+ + `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
1455
+ + `confidenceTier: implied\ntags: ["alpha"]\n---\n\nalpha\n`,
1456
+ "utf-8",
1457
+ );
1458
+ const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
1459
+ await mkdir(path.dirname(nsLedger), { recursive: true });
1460
+ const line = '{"legacy":true,"pad":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}\n';
1461
+ await writeFile(nsLedger, line.repeat(Math.ceil(4096 / line.length)), "utf-8");
1462
+ const beforeSize = (await stat(nsLedger)).size;
1463
+
1464
+ const catalog = {
1465
+ enabled: true,
1466
+ listNamespaces: async (): Promise<NamespaceRecord[]> =>
1467
+ [{ namespace: "project-alpha", storageDir: nsDir } as unknown as NamespaceRecord],
1468
+ } as unknown as NamespaceCatalog;
1469
+ const nsStorages = new Map<string, StorageManager>();
1470
+ const scheduler = buildCompactionSchedulerWithStorage(
1471
+ fixtureConfig({
1472
+ memoryDir: root,
1473
+ memoryLifecycleLedgerCompactBytes: 1024,
1474
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
1475
+ }),
1476
+ () => new StorageManager(root),
1477
+ async (namespace) => {
1478
+ let sm = nsStorages.get(namespace);
1479
+ if (!sm) {
1480
+ sm = new StorageManager(nsDir);
1481
+ nsStorages.set(namespace, sm);
1482
+ }
1483
+ return sm;
1484
+ },
1485
+ catalog,
1486
+ );
1487
+ try {
1488
+ await scheduler.maybeCompactMemoryLifecycleLedger();
1489
+ } finally {
1490
+ scheduler.dispose();
1491
+ }
1492
+ assert.ok((await stat(nsLedger)).size < beforeSize, "namespace ledger must be compacted");
1493
+ const rebuilt = (await readFile(nsLedger, "utf-8")).trim().split("\n").map((l) => JSON.parse(l));
1494
+ assert.deepEqual(rebuilt.map((r) => r.eventType), ["created", "updated"]);
1495
+ } finally {
1496
+ await rm(root, { recursive: true, force: true });
1497
+ }
1498
+ });
1499
+
1500
+ test("catalog-disabled fallback never downgrades an encrypted namespace ledger to plaintext (#2033)", async () => {
1501
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-secure-"));
1502
+ try {
1503
+ // Namespaces enabled but the catalog is inactive, so only the filesystem
1504
+ // fallback finds this per-namespace ledger. It is encrypted at rest and
1505
+ // oversized, but the fallback has no secure StorageManager for a
1506
+ // catalog-disabled namespace. The rebuild-chokepoint guard must REFUSE to
1507
+ // rebuild it through a plaintext StorageManager — leaving it untouched and
1508
+ // still encrypted — rather than downgrade it to plaintext.
1509
+ const key = Buffer.alloc(32, 5);
1510
+ const token = namespaceIdentityToken("project-secure");
1511
+ const nsDir = path.join(root, "namespaces", token);
1512
+ const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
1513
+ await mkdir(path.dirname(nsLedger), { recursive: true });
1514
+ const junk = '{"legacy":true,"pad":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}\n';
1515
+ const encrypted = encryptFileBody(
1516
+ junk.repeat(Math.ceil(4096 / junk.length)), key, filePathAad(nsLedger, nsDir),
1517
+ );
1518
+ await writeFile(nsLedger, encrypted);
1519
+ assert.ok(isEncryptedFile(await readFile(nsLedger)), "precondition: ledger encrypted");
1520
+
1521
+ const scheduler = buildCompactionScheduler(
1522
+ fixtureConfig({
1523
+ memoryDir: root,
1524
+ namespacesEnabled: true,
1525
+ memoryLifecycleLedgerCompactBytes: 1024,
1526
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
1527
+ }),
1528
+ );
1529
+ try {
1530
+ await scheduler.maybeCompactMemoryLifecycleLedger();
1531
+ } finally {
1532
+ scheduler.dispose();
1533
+ }
1534
+ // Skipped, not rewritten: the encrypted ledger must be byte-for-byte intact.
1535
+ assert.deepEqual(
1536
+ await readFile(nsLedger),
1537
+ encrypted,
1538
+ "encrypted namespace ledger must be left untouched, never plaintext-rewritten",
1539
+ );
1540
+ } finally {
1541
+ await rm(root, { recursive: true, force: true });
1542
+ }
1543
+ });
1544
+
1545
+ test("catalog-disabled fallback compacts an encrypted over-cap namespace ledger through the keyed store (#2033 thread PRRT_kwDORJXyws6SEvWo)", async () => {
1546
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-secure-keyed-"));
1547
+ try {
1548
+ // Catalog disabled but namespaces + secure-store enabled: only the
1549
+ // filesystem fallback finds this per-namespace ledger. With a namespace
1550
+ // storage resolver wired (the production orchestrator wiring), the fallback
1551
+ // must resolve the namespace's root-keyed secure StorageManager so the
1552
+ // encrypted, over-cap ledger COMPACTS — instead of deferring forever behind
1553
+ // a keyless plaintext target (the pre-fix behavior asserted by the sibling
1554
+ // "never downgrades" test, which wires no resolver).
1555
+ const key = Buffer.alloc(32, 5);
1556
+ const nsName = "project-secure-keyed";
1557
+ const token = namespaceIdentityToken(nsName);
1558
+ const nsDir = path.join(root, "namespaces", token);
1559
+ // A plaintext frontmatter memory the rebuild reconstructs into created +
1560
+ // updated events (read fine under a keyed store — the reader probes per
1561
+ // file), so the compacted ledger has real, bounded content.
1562
+ await mkdir(path.join(nsDir, "facts", "2026-03-08"), { recursive: true });
1563
+ await writeFile(
1564
+ path.join(nsDir, "facts", "2026-03-08", "fact-ns.md"),
1565
+ `---\nid: fact-ns\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
1566
+ + `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
1567
+ + `confidenceTier: implied\ntags: ["secure"]\n---\n\nsecure\n`,
1568
+ "utf-8",
1569
+ );
1570
+ const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
1571
+ await mkdir(path.dirname(nsLedger), { recursive: true });
1572
+ const junk = '{"legacy":true,"pad":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}\n';
1573
+ const encrypted = encryptFileBody(
1574
+ junk.repeat(Math.ceil(4096 / junk.length)), key, filePathAad(nsLedger, nsDir),
1575
+ );
1576
+ await writeFile(nsLedger, encrypted);
1577
+ assert.ok(isEncryptedFile(await readFile(nsLedger)), "precondition: ledger encrypted");
1578
+ const beforeSize = (await stat(nsLedger)).size;
1579
+
1580
+ // The resolver is consulted with the DECODED namespace (the on-disk token is
1581
+ // decoded back to its canonical namespace) and returns the namespace's
1582
+ // root-keyed, unlocked secure storage rooted AT the fallback dir — exactly
1583
+ // what the live router's storageFor(namespace) yields.
1584
+ const nsStorage = new StorageManager(nsDir);
1585
+ nsStorage.setSecureStoreRequired(true);
1586
+ nsStorage.setSecureStoreKey(key); // unlocked + encrypt-on-write ⇒ rewrite stays encrypted.
1587
+ let resolverCalls = 0;
1588
+ const scheduler = buildCompactionSchedulerWithStorage(
1589
+ fixtureConfig({
1590
+ memoryDir: root,
1591
+ namespacesEnabled: true,
1592
+ secureStoreEnabled: true,
1593
+ secureStoreEncryptOnWrite: true,
1594
+ memoryLifecycleLedgerCompactBytes: 1024,
1595
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
1596
+ }),
1597
+ () => new StorageManager(root), // root has no ledger ⇒ skipped.
1598
+ async (namespace) => {
1599
+ resolverCalls += 1;
1600
+ assert.equal(namespace, nsName, "fallback resolves by the DECODED namespace, not the raw token");
1601
+ return nsStorage;
1602
+ },
1603
+ undefined, // catalog disabled.
1604
+ );
1605
+ // Named seam cast (reason: assert the resolved target and its compaction
1606
+ // outcome directly), mirroring the extended casts used by the singleflight
1607
+ // and request-wiring tests above.
1608
+ const seam = scheduler as unknown as CompactableScheduler & {
1609
+ resolveLifecycleCompactionTargets: () => Promise<
1610
+ Array<{ memoryDir: string; storage?: StorageManager }>
1611
+ >;
1612
+ compactLifecycleLedgerTarget: (
1613
+ target: unknown,
1614
+ threshold: number,
1615
+ ) => Promise<"skipped" | "compacted" | "failed" | "deferred">;
1616
+ };
1617
+ try {
1618
+ const targets = await seam.resolveLifecycleCompactionTargets();
1619
+ assert.ok(resolverCalls > 0, "the fallback consulted the namespace storage resolver");
1620
+ const nsTarget = targets.find(
1621
+ (t) => path.resolve(t.memoryDir) === path.resolve(nsDir),
1622
+ );
1623
+ assert.ok(nsTarget, "fallback discovered the per-namespace ledger dir");
1624
+ assert.ok(nsTarget?.storage, "fallback target carries a keyed StorageManager, not a keyless dir");
1625
+ assert.ok(
1626
+ nsTarget?.storage?.isSecureStoreUnlocked(),
1627
+ "fallback target storage is unlocked so the encrypted ledger can be rewritten",
1628
+ );
1629
+
1630
+ // The core proof: the encrypted over-cap ledger COMPACTS, it does not defer.
1631
+ const outcome = await seam.compactLifecycleLedgerTarget(nsTarget, 1024);
1632
+ assert.equal(
1633
+ outcome,
1634
+ "compacted",
1635
+ "encrypted fallback ledger compacts through the keyed store, never deferred forever",
1636
+ );
1637
+ } finally {
1638
+ scheduler.dispose();
1639
+ }
1640
+
1641
+ // On disk: rewritten, still encrypted at rest (no plaintext downgrade), and
1642
+ // decrypts back to the reconstructed events via the same key.
1643
+ assert.notDeepEqual(
1644
+ await readFile(nsLedger),
1645
+ encrypted,
1646
+ "ledger was rewritten (not left byte-for-byte intact, i.e. not deferred)",
1647
+ );
1648
+ assert.ok((await stat(nsLedger)).size < beforeSize, "compacted ledger is smaller");
1649
+ assert.ok(
1650
+ isEncryptedFile(await readFile(nsLedger)),
1651
+ "compacted ledger stays encrypted at rest (namespace isolation + at-rest format preserved)",
1652
+ );
1653
+ const rebuilt = await nsStorage.readAllMemoryLifecycleEvents();
1654
+ assert.deepEqual(rebuilt.map((e) => e.eventType), ["created", "updated"]);
1655
+ } finally {
1656
+ await rm(root, { recursive: true, force: true });
1657
+ }
1658
+ });
1659
+
1660
+ test("catalog-disabled fallback decodes a long tokenized namespace dir so an encrypted over-cap ledger compacts instead of deferring forever (#2033 thread PRRT_kwDORJXyws6SE5fv)", async () => {
1661
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-longtoken-"));
1662
+ try {
1663
+ // A VALID namespace (passes isSafeRouteNamespace) that is long enough that
1664
+ // its identity token — the on-disk dir name — exceeds the router's 64-char
1665
+ // route-namespace limit. Passing that raw token back to the resolver (the
1666
+ // pre-fix behavior) makes the router reject it, dropping the fallback to a
1667
+ // keyless target that defers the encrypted over-cap ledger forever. The fix
1668
+ // decodes the token to the canonical namespace before resolving.
1669
+ const nsName = "project-origin-with-a-verylong-namespace";
1670
+ const token = namespaceIdentityToken(nsName);
1671
+ assert.ok(nsName.length >= 31, "precondition: namespace long enough to overflow the routed token");
1672
+ assert.ok(isSafeRouteNamespace(nsName), "precondition: the namespace itself is a valid route namespace");
1673
+ assert.ok(token.length > 64, "precondition: the tokenized dir name exceeds the 64-char route limit");
1674
+ assert.ok(!isSafeRouteNamespace(token), "precondition: the raw token would be REJECTED as a route namespace");
1675
+ assert.equal(namespaceIdentityFromToken(token), nsName, "precondition: the token decodes back to the namespace");
1676
+
1677
+ const key = Buffer.alloc(32, 7);
1678
+ const nsDir = path.join(root, "namespaces", token);
1679
+ // Plaintext frontmatter memory the rebuild reconstructs into created+updated.
1680
+ await mkdir(path.join(nsDir, "facts", "2026-03-08"), { recursive: true });
1681
+ await writeFile(
1682
+ path.join(nsDir, "facts", "2026-03-08", "fact-ns.md"),
1683
+ `---\nid: fact-ns\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
1684
+ + `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
1685
+ + `confidenceTier: implied\ntags: ["long"]\n---\n\nlong\n`,
1686
+ "utf-8",
1687
+ );
1688
+ const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
1689
+ await mkdir(path.dirname(nsLedger), { recursive: true });
1690
+ const junk = '{"legacy":true,"pad":"wwwwwwwwwwwwwwwwwwwwwwwwwwwwww"}\n';
1691
+ const encrypted = encryptFileBody(
1692
+ junk.repeat(Math.ceil(4096 / junk.length)), key, filePathAad(nsLedger, nsDir),
1693
+ );
1694
+ await writeFile(nsLedger, encrypted);
1695
+ assert.ok(isEncryptedFile(await readFile(nsLedger)), "precondition: ledger encrypted");
1696
+ const beforeSize = (await stat(nsLedger)).size;
1697
+
1698
+ const nsStorage = new StorageManager(nsDir);
1699
+ nsStorage.setSecureStoreRequired(true);
1700
+ nsStorage.setSecureStoreKey(key); // unlocked + encrypt-on-write ⇒ rewrite stays encrypted.
1701
+ let resolvedWith: string | null = null;
1702
+ const scheduler = buildCompactionSchedulerWithStorage(
1703
+ fixtureConfig({
1704
+ memoryDir: root,
1705
+ namespacesEnabled: true,
1706
+ secureStoreEnabled: true,
1707
+ secureStoreEncryptOnWrite: true,
1708
+ memoryLifecycleLedgerCompactBytes: 1024,
1709
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
1710
+ }),
1711
+ () => new StorageManager(root), // root has no ledger ⇒ skipped.
1712
+ // Mirror the production router: reject anything that is not a safe route
1713
+ // namespace, exactly how getWritableStorageForNamespace throws. With the
1714
+ // pre-fix behavior the fallback passed the over-64 token and this resolver
1715
+ // would throw, stranding the ledger on a keyless deferred target.
1716
+ async (namespace) => {
1717
+ resolvedWith = namespace;
1718
+ if (!isSafeRouteNamespace(namespace)) {
1719
+ throw new Error(`invalid namespace: ${namespace}`);
1720
+ }
1721
+ return nsStorage;
1722
+ },
1723
+ undefined, // catalog disabled.
1724
+ );
1725
+ const seam = scheduler as unknown as CompactableScheduler & {
1726
+ resolveLifecycleCompactionTargets: () => Promise<
1727
+ Array<{ memoryDir: string; storage?: StorageManager }>
1728
+ >;
1729
+ compactLifecycleLedgerTarget: (
1730
+ target: unknown,
1731
+ threshold: number,
1732
+ ) => Promise<"skipped" | "compacted" | "failed" | "deferred">;
1733
+ };
1734
+ try {
1735
+ const targets = await seam.resolveLifecycleCompactionTargets();
1736
+ assert.equal(resolvedWith, nsName, "fallback resolves by the DECODED namespace, never the over-64 token");
1737
+ const nsTarget = targets.find(
1738
+ (t) => path.resolve(t.memoryDir) === path.resolve(nsDir),
1739
+ );
1740
+ assert.ok(nsTarget, "fallback discovered the per-namespace ledger dir");
1741
+ assert.ok(
1742
+ nsTarget?.storage,
1743
+ "long-namespace fallback target carries a keyed StorageManager, not a keyless dir",
1744
+ );
1745
+ assert.ok(
1746
+ nsTarget?.storage?.isSecureStoreUnlocked(),
1747
+ "keyed target is unlocked so the encrypted ledger can be rewritten",
1748
+ );
1749
+
1750
+ // The core proof: the encrypted over-cap ledger COMPACTS, it does not defer.
1751
+ const outcome = await seam.compactLifecycleLedgerTarget(nsTarget, 1024);
1752
+ assert.equal(
1753
+ outcome,
1754
+ "compacted",
1755
+ "encrypted over-cap ledger for a long namespace compacts through the keyed store, never deferred forever",
1756
+ );
1757
+ } finally {
1758
+ scheduler.dispose();
1759
+ }
1760
+
1761
+ assert.notDeepEqual(
1762
+ await readFile(nsLedger),
1763
+ encrypted,
1764
+ "ledger was rewritten (not left byte-for-byte intact, i.e. not deferred)",
1765
+ );
1766
+ assert.ok((await stat(nsLedger)).size < beforeSize, "compacted ledger is smaller");
1767
+ assert.ok(
1768
+ isEncryptedFile(await readFile(nsLedger)),
1769
+ "compacted ledger stays encrypted at rest (namespace isolation + at-rest format preserved)",
1770
+ );
1771
+ const rebuilt = await nsStorage.readAllMemoryLifecycleEvents();
1772
+ assert.deepEqual(rebuilt.map((e) => e.eventType), ["created", "updated"]);
1773
+ } finally {
1774
+ await rm(root, { recursive: true, force: true });
1775
+ }
1776
+ });
1777
+
1778
+ test("catalog-disabled fallback resolves a LEGACY RAW namespace dir (not a ns- token) through the keyed store so its encrypted over-cap ledger compacts (#2033)", async () => {
1779
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-legacyraw-"));
1780
+ try {
1781
+ // A legacy deployment wrote per-namespace data under the RAW namespace name
1782
+ // (namespaces/<name>/), before identity tokenization existed. Such a dir name
1783
+ // is NOT a ns-<hex> token, so namespaceIdentityFromToken returns null; the
1784
+ // pre-fix fallback then only offered ns- token dirs to the keyed resolver and
1785
+ // left this legacy dir on a keyless target, deferring its encrypted over-cap
1786
+ // ledger forever. The fix treats a non-token dir as a raw namespace when it
1787
+ // is a safe route namespace and the keyed store roots back at THIS dir.
1788
+ const nsName = "legacy-raw-ns";
1789
+ assert.equal(namespaceIdentityFromToken(nsName), null, "precondition: dir name is NOT a ns- identity token");
1790
+ assert.ok(isSafeRouteNamespace(nsName), "precondition: dir name is a safe route namespace");
1791
+
1792
+ const key = Buffer.alloc(32, 5);
1793
+ const nsDir = path.join(root, "namespaces", nsName);
1794
+ await mkdir(path.join(nsDir, "facts", "2026-03-08"), { recursive: true });
1795
+ await writeFile(
1796
+ path.join(nsDir, "facts", "2026-03-08", "fact-legacy.md"),
1797
+ `---\nid: fact-legacy\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
1798
+ + `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
1799
+ + `confidenceTier: implied\ntags: ["legacy"]\n---\n\nlegacy\n`,
1800
+ "utf-8",
1801
+ );
1802
+ const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
1803
+ await mkdir(path.dirname(nsLedger), { recursive: true });
1804
+ const junk = '{"legacy":true,"pad":"wwwwwwwwwwwwwwwwwwwwwwwwwwwwww"}\n';
1805
+ const encrypted = encryptFileBody(
1806
+ junk.repeat(Math.ceil(4096 / junk.length)), key, filePathAad(nsLedger, nsDir),
1807
+ );
1808
+ await writeFile(nsLedger, encrypted);
1809
+ assert.ok(isEncryptedFile(await readFile(nsLedger)), "precondition: ledger encrypted");
1810
+ const beforeSize = (await stat(nsLedger)).size;
1811
+
1812
+ const nsStorage = new StorageManager(nsDir);
1813
+ nsStorage.setSecureStoreRequired(true);
1814
+ nsStorage.setSecureStoreKey(key); // unlocked + encrypt-on-write ⇒ rewrite stays encrypted.
1815
+ let resolvedWith: string | null = null;
1816
+ const scheduler = buildCompactionSchedulerWithStorage(
1817
+ fixtureConfig({
1818
+ memoryDir: root,
1819
+ namespacesEnabled: true,
1820
+ secureStoreEnabled: true,
1821
+ secureStoreEncryptOnWrite: true,
1822
+ memoryLifecycleLedgerCompactBytes: 1024,
1823
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
1824
+ }),
1825
+ () => new StorageManager(root), // root has no ledger ⇒ skipped.
1826
+ // The production router routes an existing legacy raw-name dir back to its
1827
+ // namespaces/<name>/ root; mirror that by returning the keyed nsStorage for
1828
+ // the raw name and rejecting anything unsafe.
1829
+ async (namespace) => {
1830
+ resolvedWith = namespace;
1831
+ if (!isSafeRouteNamespace(namespace)) {
1832
+ throw new Error(`invalid namespace: ${namespace}`);
1833
+ }
1834
+ return nsStorage;
1835
+ },
1836
+ undefined, // catalog disabled.
1837
+ );
1838
+ const seam = scheduler as unknown as CompactableScheduler & {
1839
+ resolveLifecycleCompactionTargets: () => Promise<
1840
+ Array<{ memoryDir: string; storage?: StorageManager }>
1841
+ >;
1842
+ compactLifecycleLedgerTarget: (
1843
+ target: unknown,
1844
+ threshold: number,
1845
+ ) => Promise<"skipped" | "compacted" | "failed" | "deferred">;
1846
+ };
1847
+ try {
1848
+ const targets = await seam.resolveLifecycleCompactionTargets();
1849
+ assert.equal(resolvedWith, nsName, "fallback resolves by the RAW legacy namespace name");
1850
+ const nsTarget = targets.find(
1851
+ (t) => path.resolve(t.memoryDir) === path.resolve(nsDir),
1852
+ );
1853
+ assert.ok(nsTarget, "fallback discovered the legacy raw namespace ledger dir");
1854
+ assert.ok(
1855
+ nsTarget?.storage,
1856
+ "legacy raw fallback target carries a keyed StorageManager, not a keyless dir",
1857
+ );
1858
+ assert.ok(
1859
+ nsTarget?.storage?.isSecureStoreUnlocked(),
1860
+ "keyed target is unlocked so the encrypted ledger can be rewritten",
1861
+ );
1862
+
1863
+ const outcome = await seam.compactLifecycleLedgerTarget(nsTarget, 1024);
1864
+ assert.equal(
1865
+ outcome,
1866
+ "compacted",
1867
+ "encrypted over-cap ledger under a legacy raw namespace dir compacts through the keyed store, never deferred forever",
1868
+ );
1869
+ } finally {
1870
+ scheduler.dispose();
1871
+ }
1872
+
1873
+ assert.notDeepEqual(await readFile(nsLedger), encrypted, "ledger was rewritten (not deferred)");
1874
+ assert.ok((await stat(nsLedger)).size < beforeSize, "compacted ledger is smaller");
1875
+ assert.ok(
1876
+ isEncryptedFile(await readFile(nsLedger)),
1877
+ "compacted ledger stays encrypted at rest (namespace isolation + at-rest format preserved)",
1878
+ );
1879
+ const rebuilt = await nsStorage.readAllMemoryLifecycleEvents();
1880
+ assert.deepEqual(rebuilt.map((e) => e.eventType), ["created", "updated"]);
1881
+ } finally {
1882
+ await rm(root, { recursive: true, force: true });
1883
+ }
1884
+ });
1885
+
1886
+ test("catalog-disabled fallback routes a LEGACY RAW namespace dir through the PRODUCTION resolver (resolveNamespaceStorageRoot) so its encrypted over-cap ledger compacts, not defers (#2033)", async () => {
1887
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-legacyraw-prod-"));
1888
+ try {
1889
+ // Higher-fidelity companion to the hand-rolled-mock legacy-raw test above.
1890
+ // The resolver here is the LIVE production routing helper
1891
+ // `resolveNamespaceStorageRoot` (exactly the root NamespaceStorageRouter.
1892
+ // storageFor resolves through), not a stub returning a fixed StorageManager.
1893
+ // A legacy deployment wrote per-namespace data under the RAW namespace name
1894
+ // (namespaces/<name>/) before tokenization, with NO catalog. The fallback
1895
+ // must (1) offer the raw dir name to the keyed resolver AND (2) the production
1896
+ // resolver must route that raw name BACK to its legacy namespaces/<name>/ root
1897
+ // (returning the legacy dir when the tokenized dir has no marker), so the keyed
1898
+ // guard `storage.dir === childPath` holds and the encrypted over-cap ledger
1899
+ // compacts through the keyed store. Were resolveNamespaceStorageRoot ever to
1900
+ // route a legacy raw name to the tokenized dir, the guard would fail and the
1901
+ // ledger would defer forever — a regression the mock-based test cannot catch.
1902
+ const nsName = "legacy-raw-prod-ns";
1903
+ assert.equal(namespaceIdentityFromToken(nsName), null, "precondition: dir name is NOT a ns- identity token");
1904
+ assert.ok(isSafeRouteNamespace(nsName), "precondition: dir name is a safe route namespace");
1905
+
1906
+ const config = fixtureConfig({
1907
+ memoryDir: root,
1908
+ defaultNamespace: "primary",
1909
+ namespacesEnabled: true,
1910
+ secureStoreEnabled: true,
1911
+ secureStoreEncryptOnWrite: true,
1912
+ memoryLifecycleLedgerCompactBytes: 1024,
1913
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
1914
+ });
1915
+
1916
+ const key = Buffer.alloc(32, 7);
1917
+ const nsDir = path.join(root, "namespaces", nsName);
1918
+ await mkdir(path.join(nsDir, "facts", "2026-03-08"), { recursive: true });
1919
+ await writeFile(
1920
+ path.join(nsDir, "facts", "2026-03-08", "fact-legacy.md"),
1921
+ `---\nid: fact-legacy\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
1922
+ + `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
1923
+ + `confidenceTier: implied\ntags: ["legacy"]\n---\n\nlegacy\n`,
1924
+ "utf-8",
1925
+ );
1926
+ const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
1927
+ await mkdir(path.dirname(nsLedger), { recursive: true });
1928
+ const junk = '{"legacy":true,"pad":"vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"}\n';
1929
+ const encrypted = encryptFileBody(
1930
+ junk.repeat(Math.ceil(4096 / junk.length)), key, filePathAad(nsLedger, nsDir),
1931
+ );
1932
+ await writeFile(nsLedger, encrypted);
1933
+ assert.ok(isEncryptedFile(await readFile(nsLedger)), "precondition: ledger encrypted");
1934
+ const beforeSize = (await stat(nsLedger)).size;
1935
+
1936
+ // Precondition on the PRODUCTION resolver: the live routing helper resolves
1937
+ // the legacy raw name back to the legacy namespaces/<name>/ dir (no tokenized
1938
+ // dir with markers exists), so the keyed target roots at THIS childPath.
1939
+ assert.equal(
1940
+ path.resolve(await resolveNamespaceStorageRoot(config, nsName)),
1941
+ path.resolve(nsDir),
1942
+ "production resolver routes the legacy raw namespace name to its legacy dir",
1943
+ );
1944
+
1945
+ let resolvedWith: string | null = null;
1946
+ const scheduler = buildCompactionSchedulerWithStorage(
1947
+ config,
1948
+ () => new StorageManager(root), // root has no ledger, so it is skipped.
1949
+ // Mirror NamespaceStorageRouter.storageFor: resolve the root through the
1950
+ // LIVE resolver, then build the root-keyed secure StorageManager the router
1951
+ // would hand back (applySecureStoreConfig sets the required flag + key).
1952
+ async (namespace) => {
1953
+ resolvedWith = namespace;
1954
+ const resolvedRoot = await resolveNamespaceStorageRoot(config, namespace);
1955
+ const sm = new StorageManager(resolvedRoot);
1956
+ sm.setSecureStoreRequired(true);
1957
+ sm.setSecureStoreKey(key);
1958
+ return sm;
1959
+ },
1960
+ undefined, // catalog disabled.
1961
+ );
1962
+ const seam = scheduler as unknown as CompactableScheduler & {
1963
+ resolveLifecycleCompactionTargets: () => Promise<
1964
+ Array<{ memoryDir: string; storage?: StorageManager }>
1965
+ >;
1966
+ compactLifecycleLedgerTarget: (
1967
+ target: unknown,
1968
+ threshold: number,
1969
+ ) => Promise<"skipped" | "compacted" | "failed" | "deferred">;
1970
+ };
1971
+ try {
1972
+ const targets = await seam.resolveLifecycleCompactionTargets();
1973
+ assert.equal(resolvedWith, nsName, "fallback resolves by the RAW legacy namespace name");
1974
+ const nsTarget = targets.find(
1975
+ (t) => path.resolve(t.memoryDir) === path.resolve(nsDir),
1976
+ );
1977
+ assert.ok(nsTarget, "fallback discovered the legacy raw namespace ledger dir");
1978
+ assert.ok(
1979
+ nsTarget?.storage,
1980
+ "legacy raw fallback target carries a keyed StorageManager (production resolver), not a keyless dir",
1981
+ );
1982
+ assert.equal(
1983
+ path.resolve(nsTarget!.storage!.dir),
1984
+ path.resolve(nsDir),
1985
+ "the keyed target roots at the legacy raw dir, so the keyed guard held",
1986
+ );
1987
+ assert.ok(
1988
+ nsTarget?.storage?.isSecureStoreUnlocked(),
1989
+ "keyed target is unlocked so the encrypted ledger can be rewritten",
1990
+ );
1991
+
1992
+ const outcome = await seam.compactLifecycleLedgerTarget(nsTarget, 1024);
1993
+ assert.equal(
1994
+ outcome,
1995
+ "compacted",
1996
+ "encrypted over-cap ledger under a legacy raw namespace dir compacts through the production-resolved keyed store, never deferred forever",
1997
+ );
1998
+ } finally {
1999
+ scheduler.dispose();
2000
+ }
2001
+
2002
+ assert.notDeepEqual(await readFile(nsLedger), encrypted, "ledger was rewritten (not deferred)");
2003
+ assert.ok((await stat(nsLedger)).size < beforeSize, "compacted ledger is smaller");
2004
+ assert.ok(
2005
+ isEncryptedFile(await readFile(nsLedger)),
2006
+ "compacted ledger stays encrypted at rest (namespace isolation + at-rest format preserved)",
2007
+ );
2008
+ const verifyStorage = new StorageManager(nsDir);
2009
+ verifyStorage.setSecureStoreRequired(true);
2010
+ verifyStorage.setSecureStoreKey(key);
2011
+ const rebuilt = await verifyStorage.readAllMemoryLifecycleEvents();
2012
+ assert.deepEqual(rebuilt.map((e) => e.eventType), ["created", "updated"]);
2013
+ } finally {
2014
+ await rm(root, { recursive: true, force: true });
2015
+ }
2016
+ });
2017
+
2018
+ test("preserving rebuild skips the backup+rewrite for an already-compact ledger, ending the periodic re-archive churn (#2033 thread PRRT_kwDORJXyws6SExst)", async () => {
2019
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-noop-"));
2020
+ try {
2021
+ const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
2022
+ await mkdir(path.dirname(ledgerPath), { recursive: true });
2023
+ // Valid append-only history (no frontmatter equivalent) that stays well below
2024
+ // the read/decrypt cap, plus junk rows the first rebuild discards. The first
2025
+ // rebuild therefore genuinely rewrites (drops the junk) and archives a backup;
2026
+ // every later rebuild would reproduce the now-canonical ledger byte-for-byte.
2027
+ // Pre-fix that reproduced identical content and archived a NEW backup every
2028
+ // interval, growing archive/memory-lifecycle-ledger without reducing the
2029
+ // active file. The no-op skip must leave both the ledger and the archive
2030
+ // untouched on the second pass.
2031
+ const events = Array.from({ length: 30 }, (_unused, i) => ({
2032
+ eventId: `noop-${String(i).padStart(3, "0")}`,
2033
+ memoryId: "mem-a",
2034
+ eventType: "explicit_capture_accepted",
2035
+ timestamp: new Date(Date.UTC(2026, 2, 8, 0, 0, 0, 0) + i * 1000).toISOString(),
2036
+ actor: "explicit-capture",
2037
+ ruleVersion: "memory-lifecycle-ledger.v1",
2038
+ }));
2039
+ const junk = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
2040
+ await writeFile(
2041
+ ledgerPath,
2042
+ junk.repeat(4) + `${events.map((e) => JSON.stringify(e)).join("\n")}\n`,
2043
+ "utf-8",
2044
+ );
2045
+
2046
+ const storage = new StorageManager(memoryDir);
2047
+ const bigCap = 10 * 1024 * 1024; // far above the tiny ledger: nothing is bounded away.
2048
+ const archiveRoot = path.join(memoryDir, "archive", "memory-lifecycle-ledger");
2049
+
2050
+ // First rebuild: discards the junk, canonicalizes, and archives one backup.
2051
+ const first = await rebuildMemoryLifecycleLedger({
2052
+ memoryDir,
2053
+ dryRun: false,
2054
+ storage,
2055
+ preserveExistingEvents: true,
2056
+ maxLedgerBytes: bigCap,
2057
+ now: new Date("2026-03-08T00:00:00.000Z"),
2058
+ });
2059
+ assert.equal(first.rewritten, true, "first rebuild rewrites (junk discarded)");
2060
+ assert.ok(first.backupPath, "first rebuild archives a backup");
2061
+ const afterFirst = await readFile(ledgerPath);
2062
+ assert.equal(
2063
+ (await readdir(archiveRoot)).length,
2064
+ 1,
2065
+ "exactly one backup after the first (effective) rebuild",
2066
+ );
2067
+
2068
+ // Second rebuild at a DISTINCT timestamp: a real re-archive would create a
2069
+ // second stamped backup dir. The no-op skip must create none and leave the
2070
+ // ledger byte-for-byte.
2071
+ const second = await rebuildMemoryLifecycleLedger({
2072
+ memoryDir,
2073
+ dryRun: false,
2074
+ storage,
2075
+ preserveExistingEvents: true,
2076
+ maxLedgerBytes: bigCap,
2077
+ now: new Date("2026-03-08T01:00:00.000Z"),
2078
+ });
2079
+ assert.equal(second.rewritten, false, "already-compact ledger: rebuild is a no-op");
2080
+ assert.equal(second.backupPath, undefined, "no-op rebuild archives no backup");
2081
+ assert.deepEqual(await readFile(ledgerPath), afterFirst, "no-op leaves the ledger byte-for-byte");
2082
+ assert.equal(
2083
+ (await readdir(archiveRoot)).length,
2084
+ 1,
2085
+ "no second backup archived across intervals (churn eliminated)",
2086
+ );
2087
+
2088
+ // A third pass at yet another timestamp stays a no-op: the archive never grows.
2089
+ const third = await rebuildMemoryLifecycleLedger({
2090
+ memoryDir,
2091
+ dryRun: false,
2092
+ storage,
2093
+ preserveExistingEvents: true,
2094
+ maxLedgerBytes: bigCap,
2095
+ now: new Date("2026-03-08T02:00:00.000Z"),
2096
+ });
2097
+ assert.equal(third.rewritten, false, "still a no-op on the third interval");
2098
+ assert.equal((await readdir(archiveRoot)).length, 1, "archive still holds exactly one backup");
2099
+ } finally {
2100
+ await rm(memoryDir, { recursive: true, force: true });
2101
+ }
2102
+ });
2103
+
2104
+ test("catalog-disabled fallback refuses a symlinked namespaces scan root (#2033)", async () => {
2105
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-symlink-"));
2106
+ const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-outside-"));
2107
+ try {
2108
+ // <root>/namespaces is a symlink pointing OUTSIDE the memory dir. Following
2109
+ // it would let compaction read/rewrite ledgers outside memoryDir, so the
2110
+ // scan root must be rejected and the external ledger left untouched.
2111
+ const nsLedger = path.join(
2112
+ outside, namespaceIdentityToken("evil"), "state", "memory-lifecycle-ledger.jsonl",
2113
+ );
2114
+ await mkdir(path.dirname(nsLedger), { recursive: true });
2115
+ const junk = '{"legacy":true,"pad":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}\n';
2116
+ const original = junk.repeat(Math.ceil(4096 / junk.length));
2117
+ await writeFile(nsLedger, original, "utf-8");
2118
+ await symlink(outside, path.join(root, "namespaces"));
2119
+
2120
+ const scheduler = buildCompactionScheduler(
2121
+ fixtureConfig({
2122
+ memoryDir: root,
2123
+ namespacesEnabled: true,
2124
+ memoryLifecycleLedgerCompactBytes: 1024,
2125
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2126
+ }),
2127
+ );
2128
+ try {
2129
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2130
+ } finally {
2131
+ scheduler.dispose();
2132
+ }
2133
+ assert.equal(
2134
+ await readFile(nsLedger, "utf-8"),
2135
+ original,
2136
+ "ledger behind a symlinked namespaces root must be untouched",
2137
+ );
2138
+ } finally {
2139
+ await rm(root, { recursive: true, force: true });
2140
+ await rm(outside, { recursive: true, force: true });
2141
+ }
2142
+ });
2143
+
2144
+ test("maintenance request triggers lifecycle compaction even when QMD is unavailable (#1910)", async () => {
2145
+ const { memoryDir } = await seedMemoryDirWithOversizedLedger(4096);
2146
+ try {
2147
+ // QMD reports unavailable, so requestQmdMaintenance() short-circuits and
2148
+ // runQmdMaintenance() never fires. The compaction check must still be wired
2149
+ // off the request entrypoint so a QMD-off deployment keeps bounding the
2150
+ // ledger. requestQmdMaintenanceForTool invokes the check synchronously
2151
+ // (fire-and-forget), so a spy on the private method observes the wiring
2152
+ // deterministically without waiting on the async filesystem work.
2153
+ const scheduler = new MaintenanceScheduler({
2154
+ config: fixtureConfig({
2155
+ memoryDir,
2156
+ memoryLifecycleLedgerCompactBytes: 1024,
2157
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2158
+ }),
2159
+ getQmd: () => ({ isAvailable: () => false }) as unknown as SearchBackend,
2160
+ namespaceSearchRouter: {} as unknown as NamespaceSearchRouter,
2161
+ namespaceCatalog: {} as unknown as NamespaceCatalog,
2162
+ });
2163
+ let compactionInvoked = false;
2164
+ // Named seam cast (reason: reach the private compaction method to observe
2165
+ // the request→compaction wiring), mirroring the CompactableScheduler cast
2166
+ // used by the direct compaction tests above.
2167
+ const seam = scheduler as unknown as CompactableScheduler;
2168
+ seam.maybeCompactMemoryLifecycleLedger = async () => {
2169
+ compactionInvoked = true;
2170
+ };
2171
+ try {
2172
+ scheduler.requestQmdMaintenanceForTool("test");
2173
+ assert.ok(
2174
+ compactionInvoked,
2175
+ "lifecycle compaction must be triggered from the maintenance request when QMD is unavailable",
2176
+ );
2177
+ } finally {
2178
+ scheduler.dispose();
2179
+ }
2180
+ } finally {
2181
+ await rm(memoryDir, { recursive: true, force: true });
2182
+ }
2183
+ });
2184
+
2185
+ test("auto-compaction bounds per-namespace ledgers via filesystem fallback when the catalog is disabled (#1910)", async () => {
2186
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-ns-nocatalog-"));
2187
+ try {
2188
+ const token = namespaceIdentityToken("project-beta");
2189
+ const nsDir = path.join(root, "namespaces", token);
2190
+ await mkdir(path.join(nsDir, "facts", "2026-03-08"), { recursive: true });
2191
+ await writeFile(
2192
+ path.join(nsDir, "facts", "2026-03-08", "fact-ns.md"),
2193
+ `---\nid: fact-ns\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
2194
+ + `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
2195
+ + `confidenceTier: implied\ntags: ["beta"]\n---\n\nbeta\n`,
2196
+ "utf-8",
2197
+ );
2198
+ const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
2199
+ await mkdir(path.dirname(nsLedger), { recursive: true });
2200
+ const line = '{"legacy":true,"pad":"wwwwwwwwwwwwwwwwwwwwwwwwwwwwww"}\n';
2201
+ await writeFile(nsLedger, line.repeat(Math.ceil(4096 / line.length)), "utf-8");
2202
+ const beforeSize = (await stat(nsLedger)).size;
2203
+
2204
+ // namespacesEnabled=true but NO catalog wired (the namespaceCatalogEnabled=false
2205
+ // deployment). The catalog walk finds nothing; the filesystem fallback must
2206
+ // still discover and bound the per-namespace ledger (codex P2).
2207
+ const scheduler = buildCompactionScheduler(
2208
+ fixtureConfig({
2209
+ memoryDir: root,
2210
+ namespacesEnabled: true,
2211
+ memoryLifecycleLedgerCompactBytes: 1024,
2212
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2213
+ }),
2214
+ );
2215
+ try {
2216
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2217
+ } finally {
2218
+ scheduler.dispose();
2219
+ }
2220
+ assert.ok(
2221
+ (await stat(nsLedger)).size < beforeSize,
2222
+ "namespace ledger must be compacted via the filesystem fallback",
2223
+ );
2224
+ const rebuilt = (await readFile(nsLedger, "utf-8")).trim().split("\n").map((l) => JSON.parse(l));
2225
+ assert.deepEqual(rebuilt.map((r) => r.eventType), ["created", "updated"]);
2226
+ } finally {
2227
+ await rm(root, { recursive: true, force: true });
2228
+ }
2229
+ });
2230
+
2231
+ test("auto-compaction bounds a large append-only history under the read/decrypt cap and arms the throttle (#2033)", async () => {
2232
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-bound-"));
2233
+ try {
2234
+ const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
2235
+ await mkdir(path.dirname(ledgerPath), { recursive: true });
2236
+ // Many VALID append-only events with no frontmatter equivalent: a preserving
2237
+ // compaction must keep them, so bounding — not junk-discard — is what keeps
2238
+ // the ledger under the cap. Distinct ascending timestamps make "newest"
2239
+ // precise.
2240
+ const total = 200;
2241
+ const events = Array.from({ length: total }, (_unused, i) => ({
2242
+ eventId: `cap-${String(i).padStart(3, "0")}`,
2243
+ memoryId: "mem-a",
2244
+ eventType: "explicit_capture_accepted",
2245
+ timestamp: new Date(Date.UTC(2026, 2, 8, 0, 0, 0, 0) + i * 1000).toISOString(),
2246
+ actor: "explicit-capture",
2247
+ ruleVersion: "memory-lifecycle-ledger.v1",
2248
+ }));
2249
+ const original = `${events.map((e) => JSON.stringify(e)).join("\n")}\n`;
2250
+ await writeFile(ledgerPath, original, "utf-8");
2251
+ const rowBytes = Buffer.byteLength(`${JSON.stringify(events[0])}\n`, "utf8");
2252
+ const cap = rowBytes * 20 + Math.floor(rowBytes / 2); // admits ~20 rows.
2253
+
2254
+ const scheduler = buildCompactionScheduler(
2255
+ fixtureConfig({
2256
+ memoryDir,
2257
+ memoryLifecycleLedgerCompactBytes: rowBytes * 5, // threshold << ledger size.
2258
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2259
+ }),
2260
+ );
2261
+ scheduler.lifecycleLedgerMaxBytes = cap;
2262
+ try {
2263
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2264
+
2265
+ const rewritten = await readFile(ledgerPath, "utf-8");
2266
+ assert.ok(
2267
+ Buffer.byteLength(rewritten, "utf8") < cap,
2268
+ "compacted ledger must be bounded under the read/decrypt cap",
2269
+ );
2270
+ const keptIds = rewritten.trim().split("\n").map((l) => JSON.parse(l).eventId);
2271
+ assert.ok(keptIds.includes(`cap-${String(total - 1).padStart(3, "0")}`), "newest append-only event survives");
2272
+ assert.ok(!keptIds.includes("cap-000"), "oldest append-only event archived out of the active ledger");
2273
+
2274
+ // The bounding archive lives in the timestamped backup, so nothing is lost.
2275
+ const backups = await readdir(path.join(memoryDir, "archive", "memory-lifecycle-ledger")).catch(() => []);
2276
+ assert.ok(backups.length > 0, "a backup archive was written for the overflow");
2277
+
2278
+ // Effective (under cap) compaction armed the throttle: a second in-window
2279
+ // pass after re-growing the ledger must NOT recompact.
2280
+ await writeFile(ledgerPath, original, "utf-8");
2281
+ const regrown = (await stat(ledgerPath)).size;
2282
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2283
+ assert.equal(
2284
+ (await stat(ledgerPath)).size,
2285
+ regrown,
2286
+ "throttle armed after an effective bounded compaction: second in-window pass is a no-op",
2287
+ );
2288
+ } finally {
2289
+ scheduler.dispose();
2290
+ }
2291
+ } finally {
2292
+ await rm(memoryDir, { recursive: true, force: true });
2293
+ }
2294
+ });
2295
+
2296
+ test("filesystem-fallback compaction drains a pending lifecycle append even when no storage is wired (#2033)", async () => {
2297
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-fallback-drain-"));
2298
+ try {
2299
+ // Namespaces enabled but NO catalog storage wired: only the filesystem
2300
+ // fallback finds this per-namespace ledger, so target.storage is absent.
2301
+ const token = namespaceIdentityToken("project-fallback");
2302
+ const nsDir = path.join(root, "namespaces", token);
2303
+ const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
2304
+ await mkdir(path.dirname(nsLedger), { recursive: true });
2305
+ await writeFile(nsLedger, "", "utf-8");
2306
+ // A plaintext pending spill, exactly as a lock-timed-out append leaves it.
2307
+ const spillDir = pendingLifecycleLedgerDir(nsLedger);
2308
+ await mkdir(spillDir, { recursive: true });
2309
+ const spillPath = path.join(spillDir, "spill.jsonl");
2310
+ const spilled = {
2311
+ eventId: "evt-fallback-spilled",
2312
+ memoryId: "mem-ns",
2313
+ eventType: "explicit_capture_accepted",
2314
+ timestamp: "2026-03-08T00:00:00.000Z",
2315
+ actor: "explicit-capture",
2316
+ ruleVersion: "memory-lifecycle-ledger.v1",
2317
+ };
2318
+ await writeFile(spillPath, `${JSON.stringify(spilled)}\n`, "utf-8");
2319
+
2320
+ const scheduler = buildCompactionScheduler(
2321
+ fixtureConfig({
2322
+ memoryDir: root,
2323
+ namespacesEnabled: true,
2324
+ memoryLifecycleLedgerCompactBytes: 1024,
2325
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2326
+ }),
2327
+ );
2328
+ try {
2329
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2330
+ } finally {
2331
+ scheduler.dispose();
2332
+ }
2333
+
2334
+ const ledger = await readFile(nsLedger, "utf-8");
2335
+ assert.ok(
2336
+ ledger.includes("evt-fallback-spilled"),
2337
+ "the pending spill must be drained into the namespace ledger via the fallback path",
2338
+ );
2339
+ await assert.rejects(() => stat(spillPath), /ENOENT/, "spill file removed after the fallback drain");
2340
+ } finally {
2341
+ await rm(root, { recursive: true, force: true });
2342
+ }
2343
+ });
2344
+
2345
+ test("filesystem-fallback compaction recovers a crash-orphaned *.claimed spill when no live spill remains (#2033)", async () => {
2346
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-fallback-claimed-"));
2347
+ try {
2348
+ // Namespaces enabled but NO catalog storage wired: only the filesystem
2349
+ // fallback finds this per-namespace ledger, so target.storage is absent.
2350
+ const token = namespaceIdentityToken("project-fallback-claimed");
2351
+ const nsDir = path.join(root, "namespaces", token);
2352
+ const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
2353
+ await mkdir(path.dirname(nsLedger), { recursive: true });
2354
+ await writeFile(nsLedger, "", "utf-8");
2355
+ // A drain that died between claiming a spill (rename to `.claimed`) and
2356
+ // committing it leaves the ONLY copy of those rows as a crash-orphaned
2357
+ // `*.jsonl.claimed` file — NO live `*.jsonl` spill remains. The pre-scan
2358
+ // must still reach the recovery drain so recoverOrphanedClaims restores and
2359
+ // re-commits it; gating solely on live spills stranded these rows forever.
2360
+ const spillDir = pendingLifecycleLedgerDir(nsLedger);
2361
+ await mkdir(spillDir, { recursive: true });
2362
+ const claimedPath = path.join(spillDir, "spill.jsonl.claimed");
2363
+ const orphaned = {
2364
+ eventId: "evt-claimed-orphan",
2365
+ memoryId: "mem-ns",
2366
+ eventType: "explicit_capture_accepted",
2367
+ timestamp: "2026-03-08T00:00:00.000Z",
2368
+ actor: "explicit-capture",
2369
+ ruleVersion: "memory-lifecycle-ledger.v1",
2370
+ };
2371
+ await writeFile(claimedPath, `${JSON.stringify(orphaned)}\n`, "utf-8");
2372
+ // Precondition: no live spill exists — only the claimed orphan.
2373
+ assert.equal((await listContainedSpillFiles(spillDir)).length, 0, "precondition: no live *.jsonl spill");
2374
+ assert.equal(
2375
+ (await listContainedSpillFiles(spillDir, ".jsonl.claimed")).length,
2376
+ 1,
2377
+ "precondition: exactly one crash-orphaned *.jsonl.claimed file",
2378
+ );
2379
+
2380
+ const scheduler = buildCompactionScheduler(
2381
+ fixtureConfig({
2382
+ memoryDir: root,
2383
+ namespacesEnabled: true,
2384
+ memoryLifecycleLedgerCompactBytes: 1024,
2385
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2386
+ }),
2387
+ );
2388
+ try {
2389
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2390
+ } finally {
2391
+ scheduler.dispose();
2392
+ }
2393
+
2394
+ const ledger = await readFile(nsLedger, "utf-8");
2395
+ assert.ok(
2396
+ ledger.includes("evt-claimed-orphan"),
2397
+ "the crash-orphaned claimed spill must be recovered and folded into the namespace ledger",
2398
+ );
2399
+ await assert.rejects(() => stat(claimedPath), /ENOENT/, "claimed orphan removed after recovery + commit");
2400
+ assert.equal(
2401
+ (await listContainedSpillFiles(spillDir, ".jsonl.claimed")).length,
2402
+ 0,
2403
+ "no claimed orphan left stranded",
2404
+ );
2405
+ } finally {
2406
+ await rm(root, { recursive: true, force: true });
2407
+ }
2408
+ });
2409
+
2410
+ test("catalog-disabled fallback drain re-probes encryption UNDER the lock and defers ciphertext instead of plaintext-corrupting it (#2033)", async () => {
2411
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-fallback-secure-drain-"));
2412
+ try {
2413
+ const key = Buffer.alloc(32, 4);
2414
+ const validRow = (id: string): string =>
2415
+ `{"eventId":"${id}","memoryId":"m","eventType":"created",`
2416
+ + `"timestamp":"2026-03-08T00:00:00.000Z","actor":"t","ruleVersion":"1"}\n`;
2417
+
2418
+ // nsA: plaintext (empty) ledger + an ENCRYPTED pending spill, exactly as a
2419
+ // secure-store writer's lock-timeout leaves it. A keyless plaintext readFile
2420
+ // would fold ciphertext into the ledger; the under-lock probe must refuse it.
2421
+ const tokenA = namespaceIdentityToken("ns-enc-spill");
2422
+ const nsDirA = path.join(root, "namespaces", tokenA);
2423
+ const nsLedgerA = path.join(nsDirA, "state", "memory-lifecycle-ledger.jsonl");
2424
+ await mkdir(path.dirname(nsLedgerA), { recursive: true });
2425
+ await writeFile(nsLedgerA, "", "utf-8");
2426
+ const spillDirA = pendingLifecycleLedgerDir(nsLedgerA);
2427
+ await mkdir(spillDirA, { recursive: true });
2428
+ const spillPathA = path.join(spillDirA, "spill.jsonl");
2429
+ const encryptedSpillA = encryptFileBody(validRow("evt-enc-spill"), key, filePathAad(spillPathA, nsDirA));
2430
+ await writeFile(spillPathA, encryptedSpillA);
2431
+
2432
+ // nsB: ENCRYPTED ledger + a PLAINTEXT pending spill. A keyless plaintext
2433
+ // appendFile would append plaintext onto the encrypted ledger; the under-lock
2434
+ // append probe must refuse it and roll the claim back.
2435
+ const tokenB = namespaceIdentityToken("ns-enc-ledger");
2436
+ const nsDirB = path.join(root, "namespaces", tokenB);
2437
+ const nsLedgerB = path.join(nsDirB, "state", "memory-lifecycle-ledger.jsonl");
2438
+ await mkdir(path.dirname(nsLedgerB), { recursive: true });
2439
+ const encryptedLedgerB = encryptFileBody(validRow("evt-existing"), key, filePathAad(nsLedgerB, nsDirB));
2440
+ await writeFile(nsLedgerB, encryptedLedgerB);
2441
+ const spillDirB = pendingLifecycleLedgerDir(nsLedgerB);
2442
+ await mkdir(spillDirB, { recursive: true });
2443
+ const spillPathB = path.join(spillDirB, "spill.jsonl");
2444
+ await writeFile(spillPathB, validRow("evt-plain-spill"), "utf-8");
2445
+
2446
+ const scheduler = buildCompactionScheduler(
2447
+ fixtureConfig({
2448
+ memoryDir: root,
2449
+ namespacesEnabled: true,
2450
+ memoryLifecycleLedgerCompactBytes: 0, // drain still runs unconditionally
2451
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2452
+ }),
2453
+ );
2454
+ try {
2455
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2456
+ } finally {
2457
+ scheduler.dispose();
2458
+ }
2459
+
2460
+ // nsA: encrypted spill deferred — byte-for-byte intact, the ledger never got
2461
+ // ciphertext folded into it.
2462
+ assert.deepEqual(await readFile(spillPathA), encryptedSpillA, "encrypted spill left intact for the keyed path");
2463
+ assert.equal(await readFile(nsLedgerA, "utf-8"), "", "plaintext ledger never received ciphertext");
2464
+ assert.ok(isEncryptedFile(await readFile(spillPathA)), "spill stays encrypted at rest");
2465
+
2466
+ // nsB: plaintext spill deferred (not appended onto the encrypted ledger), and
2467
+ // the encrypted ledger left byte-for-byte intact.
2468
+ assert.deepEqual(await readFile(nsLedgerB), encryptedLedgerB, "encrypted ledger left byte-for-byte intact");
2469
+ assert.equal(
2470
+ await readFile(spillPathB, "utf-8"),
2471
+ validRow("evt-plain-spill"),
2472
+ "plaintext spill rolled back to unclaimed for a later keyed pass",
2473
+ );
2474
+ } finally {
2475
+ await rm(root, { recursive: true, force: true });
2476
+ }
2477
+ });
2478
+
2479
+ test("auto-compaction triggers on an encrypted ledger at/above the decrypt cap even when the configured threshold is larger (#2033)", async () => {
2480
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-cap-clamp-"));
2481
+ try {
2482
+ await mkdir(path.join(memoryDir, "facts", "2026-03-08"), { recursive: true });
2483
+ await writeFile(
2484
+ path.join(memoryDir, "facts", "2026-03-08", "fact-1.md"),
2485
+ `---\nid: fact-1\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
2486
+ + `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
2487
+ + `confidenceTier: implied\ntags: ["alpha"]\n---\n\nalpha\n`,
2488
+ "utf-8",
2489
+ );
2490
+ const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
2491
+ await mkdir(path.dirname(ledgerPath), { recursive: true });
2492
+ // Encrypted-at-rest oversized ledger of junk rows the rebuild discards.
2493
+ const key = Buffer.alloc(32, 7);
2494
+ const junk = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
2495
+ const junkBlob = junk.repeat(Math.ceil(4096 / junk.length));
2496
+ await writeFile(ledgerPath, encryptFileBody(junkBlob, key, filePathAad(ledgerPath, memoryDir)));
2497
+ const beforeSize = (await stat(ledgerPath)).size;
2498
+
2499
+ const storage = new StorageManager(memoryDir);
2500
+ storage.setSecureStoreRequired(true);
2501
+ storage.setSecureStoreKey(key); // unlocked, so an encrypted target is not deferred.
2502
+ const scheduler = buildCompactionSchedulerWithStorage(
2503
+ fixtureConfig({
2504
+ memoryDir,
2505
+ // Threshold FAR above both the ledger AND the (seam) decrypt cap. A
2506
+ // pre-clamp trigger keyed purely off this threshold would skip the ledger
2507
+ // forever and let it grow permanently unreadable past the cap.
2508
+ memoryLifecycleLedgerCompactBytes: 10 * 1024 * 1024,
2509
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2510
+ }),
2511
+ () => storage,
2512
+ );
2513
+ scheduler.lifecycleLedgerMaxBytes = 2048; // tiny cap: the ~4KB ledger is "at/above" it.
2514
+ try {
2515
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2516
+ } finally {
2517
+ scheduler.dispose();
2518
+ }
2519
+ const afterSize = (await stat(ledgerPath)).size;
2520
+ assert.ok(afterSize < beforeSize, "over-cap encrypted ledger must compact despite the larger threshold");
2521
+ assert.ok(afterSize < 2048, "compacted encrypted ledger must land below the decrypt cap");
2522
+ assert.ok(isEncryptedFile(await readFile(ledgerPath)), "rewritten ledger stays encrypted at rest");
2523
+ const rebuilt = await storage.readAllMemoryLifecycleEvents();
2524
+ assert.deepEqual(rebuilt.map((e) => e.eventType), ["created", "updated"]);
2525
+ } finally {
2526
+ await rm(memoryDir, { recursive: true, force: true });
2527
+ }
2528
+ });
2529
+
2530
+ test("auto-compaction bypasses the min-interval throttle for an over-cap encrypted ledger, but still throttles a below-cap target (#2033)", async () => {
2531
+ // Bypass arm: an encrypted, over-cap ledger with the throttle freshly armed.
2532
+ // Without the bypass the min-interval return fires before the compaction path
2533
+ // can clamp it, leaving the unreadable ledger to wait out the whole interval.
2534
+ const bypassDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-throttle-bypass-"));
2535
+ // Throttle arm: an ordinary below-cap target with the throttle armed must NOT
2536
+ // recompact — the bypass is scoped to over-cap encrypted ledgers only.
2537
+ const throttledDir = (await seedMemoryDirWithOversizedLedger(4096)).memoryDir;
2538
+ try {
2539
+ await mkdir(path.join(bypassDir, "facts", "2026-03-08"), { recursive: true });
2540
+ await writeFile(
2541
+ path.join(bypassDir, "facts", "2026-03-08", "fact-1.md"),
2542
+ `---\nid: fact-1\ncategory: fact\ncreated: 2026-03-08T00:00:00.000Z\n`
2543
+ + `updated: 2026-03-08T01:00:00.000Z\nsource: test\nconfidence: 0.8\n`
2544
+ + `confidenceTier: implied\ntags: ["alpha"]\n---\n\nalpha\n`,
2545
+ "utf-8",
2546
+ );
2547
+ const bypassLedger = path.join(bypassDir, "state", "memory-lifecycle-ledger.jsonl");
2548
+ await mkdir(path.dirname(bypassLedger), { recursive: true });
2549
+ const key = Buffer.alloc(32, 7);
2550
+ const junk = '{"legacy":true,"pad":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}\n';
2551
+ const junkBlob = junk.repeat(Math.ceil(4096 / junk.length));
2552
+ await writeFile(bypassLedger, encryptFileBody(junkBlob, key, filePathAad(bypassLedger, bypassDir)));
2553
+ const bypassBefore = (await stat(bypassLedger)).size;
2554
+
2555
+ const storage = new StorageManager(bypassDir);
2556
+ storage.setSecureStoreRequired(true);
2557
+ storage.setSecureStoreKey(key); // unlocked, so the encrypted target is not deferred.
2558
+ const bypassScheduler = buildCompactionSchedulerWithStorage(
2559
+ fixtureConfig({
2560
+ memoryDir: bypassDir,
2561
+ // Threshold far above both the ledger and the (seam) cap: only the
2562
+ // cap-clamp — not the configured threshold — makes this target eligible.
2563
+ memoryLifecycleLedgerCompactBytes: 10 * 1024 * 1024,
2564
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2565
+ }),
2566
+ () => storage,
2567
+ );
2568
+ bypassScheduler.lifecycleLedgerMaxBytes = 2048; // tiny cap: the ~4KB ledger is over it.
2569
+ // Arm the throttle: WITHOUT the #2033 bypass this in-window pass returns
2570
+ // before compaction and the over-cap encrypted ledger is never clamped.
2571
+ bypassScheduler.lastLifecycleCompactionAtMs = Date.now();
2572
+ try {
2573
+ await bypassScheduler.maybeCompactMemoryLifecycleLedger();
2574
+ } finally {
2575
+ bypassScheduler.dispose();
2576
+ }
2577
+ const bypassAfter = (await stat(bypassLedger)).size;
2578
+ assert.ok(
2579
+ bypassAfter < bypassBefore,
2580
+ "over-cap encrypted ledger must compact despite the armed throttle (#2033 bypass)",
2581
+ );
2582
+ assert.ok(bypassAfter < 2048, "compacted encrypted ledger must land below the decrypt cap");
2583
+ assert.ok(isEncryptedFile(await readFile(bypassLedger)), "rewritten ledger stays encrypted at rest");
2584
+
2585
+ // Below-cap control: same armed throttle, ordinary plaintext oversized
2586
+ // ledger (over threshold, under the default cap). The bypass must NOT fire,
2587
+ // so the throttle still holds and the ledger is left untouched.
2588
+ const throttledLedger = path.join(throttledDir, "state", "memory-lifecycle-ledger.jsonl");
2589
+ const throttledBefore = (await stat(throttledLedger)).size;
2590
+ const throttledScheduler = buildCompactionScheduler(
2591
+ fixtureConfig({
2592
+ memoryDir: throttledDir,
2593
+ memoryLifecycleLedgerCompactBytes: 1024, // ledger is over threshold — eligible if unthrottled.
2594
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2595
+ }),
2596
+ );
2597
+ throttledScheduler.lastLifecycleCompactionAtMs = Date.now();
2598
+ try {
2599
+ await throttledScheduler.maybeCompactMemoryLifecycleLedger();
2600
+ } finally {
2601
+ throttledScheduler.dispose();
2602
+ }
2603
+ assert.equal(
2604
+ (await stat(throttledLedger)).size,
2605
+ throttledBefore,
2606
+ "below-cap target stays throttled: the armed throttle prevents recompaction",
2607
+ );
2608
+ } finally {
2609
+ await rm(bypassDir, { recursive: true, force: true });
2610
+ await rm(throttledDir, { recursive: true, force: true });
2611
+ }
2612
+ });
2613
+
2614
+ test("auto-compaction reserves the secure-store envelope so an encrypted rewrite lands below the cap and does not retry forever (#2033)", async () => {
2615
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-envelope-"));
2616
+ try {
2617
+ const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
2618
+ await mkdir(path.dirname(ledgerPath), { recursive: true });
2619
+ const total = 200;
2620
+ const events = Array.from({ length: total }, (_unused, i) => ({
2621
+ eventId: `cap-${String(i).padStart(3, "0")}`,
2622
+ memoryId: "mem-a",
2623
+ eventType: "explicit_capture_accepted",
2624
+ timestamp: new Date(Date.UTC(2026, 2, 8, 0, 0, 0, 0) + i * 1000).toISOString(),
2625
+ actor: "explicit-capture",
2626
+ ruleVersion: "memory-lifecycle-ledger.v1",
2627
+ }));
2628
+ const plaintext = `${events.map((e) => JSON.stringify(e)).join("\n")}\n`;
2629
+ const key = Buffer.alloc(32, 3);
2630
+ await writeFile(ledgerPath, encryptFileBody(plaintext, key, filePathAad(ledgerPath, memoryDir)));
2631
+ const rowBytes = Buffer.byteLength(`${JSON.stringify(events[0])}\n`, "utf8");
2632
+ // Cap == exactly 20 plaintext rows. The reserving budget (cap − envelope − 1)
2633
+ // admits 19 rows, so the encrypted output (19 rows + envelope) is below the
2634
+ // cap. WITHOUT the reserve the old budget (== cap) would keep 20 rows and the
2635
+ // encrypted file (20 rows + envelope) would be at/over the cap — the endless
2636
+ // "ineffective compaction" retry this fix prevents.
2637
+ const cap = rowBytes * 20;
2638
+ assert.ok(
2639
+ rowBytes * 20 + SECURE_STORE_ENVELOPE_OVERHEAD_BYTES > cap,
2640
+ "sanity: cap chosen so an unreserved budget would push the encrypted file over the cap",
2641
+ );
2642
+
2643
+ const storage = new StorageManager(memoryDir);
2644
+ storage.setSecureStoreRequired(true);
2645
+ storage.setSecureStoreKey(key);
2646
+ const scheduler = buildCompactionSchedulerWithStorage(
2647
+ fixtureConfig({
2648
+ memoryDir,
2649
+ memoryLifecycleLedgerCompactBytes: rowBytes * 5,
2650
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2651
+ }),
2652
+ () => storage,
2653
+ );
2654
+ scheduler.lifecycleLedgerMaxBytes = cap;
2655
+ try {
2656
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2657
+ const onDisk = (await stat(ledgerPath)).size;
2658
+ assert.ok(isEncryptedFile(await readFile(ledgerPath)), "rewritten ledger encrypted at rest");
2659
+ assert.ok(onDisk < cap, `encrypted on-disk ledger (${onDisk}B) must be strictly below the cap (${cap}B)`);
2660
+ const keptIds = (await storage.readAllMemoryLifecycleEvents()).map((e) => e.eventId);
2661
+ assert.ok(keptIds.includes(`cap-${String(total - 1).padStart(3, "0")}`), "newest event survives");
2662
+ assert.ok(!keptIds.includes("cap-000"), "oldest event archived out of the active ledger");
2663
+
2664
+ const backups = await readdir(path.join(memoryDir, "archive", "memory-lifecycle-ledger")).catch(() => []);
2665
+ assert.ok(backups.length > 0, "overflow archived to a verbatim backup");
2666
+
2667
+ // The compaction was judged effective (throttle armed): re-grow within the
2668
+ // window; a broken budget would have reported "failed" (throttle un-armed)
2669
+ // and this second pass would recompact.
2670
+ // Re-grow OVER the threshold but strictly BELOW the decrypt cap, so this
2671
+ // pass is governed by the armed throttle — not the over-cap encrypted
2672
+ // bypass (#2033), which only fires for a ledger at/over the cap.
2673
+ const regrownPlain = `${events.slice(0, 10).map((e) => JSON.stringify(e)).join("\n")}\n`;
2674
+ await writeFile(ledgerPath, encryptFileBody(regrownPlain, key, filePathAad(ledgerPath, memoryDir)));
2675
+ const regrown = (await stat(ledgerPath)).size;
2676
+ assert.ok(
2677
+ regrown > rowBytes * 5 && regrown < cap,
2678
+ "regrown ledger is over threshold but below the cap (throttle, not bypass, applies)",
2679
+ );
2680
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2681
+ assert.equal(
2682
+ (await stat(ledgerPath)).size,
2683
+ regrown,
2684
+ "throttle armed after an effective encrypted compaction: second in-window pass is a no-op",
2685
+ );
2686
+ } finally {
2687
+ scheduler.dispose();
2688
+ }
2689
+ } finally {
2690
+ await rm(memoryDir, { recursive: true, force: true });
2691
+ }
2692
+ });
2693
+
2694
+ test("auto-compaction reserves the envelope when a PLAINTEXT ledger will be rewritten encrypted (write-mode budget, #2033)", async () => {
2695
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-plaintext-encrypt-"));
2696
+ try {
2697
+ const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
2698
+ await mkdir(path.dirname(ledgerPath), { recursive: true });
2699
+ const total = 200;
2700
+ const events = Array.from({ length: total }, (_unused, i) => ({
2701
+ eventId: `cap-${String(i).padStart(3, "0")}`,
2702
+ memoryId: "mem-a",
2703
+ eventType: "explicit_capture_accepted",
2704
+ timestamp: new Date(Date.UTC(2026, 2, 8, 0, 0, 0, 0) + i * 1000).toISOString(),
2705
+ actor: "explicit-capture",
2706
+ ruleVersion: "memory-lifecycle-ledger.v1",
2707
+ }));
2708
+ const plaintext = `${events.map((e) => JSON.stringify(e)).join("\n")}\n`;
2709
+ // The on-disk ledger is PLAINTEXT (header not encrypted). The pre-fix budget
2710
+ // keyed off this header and skipped the reserve, but the rewrite below
2711
+ // encrypts (secureStoreEncryptOnWrite + unlocked key), so the envelope must
2712
+ // still be reserved.
2713
+ await writeFile(ledgerPath, plaintext, "utf-8");
2714
+ assert.ok(!isEncryptedFile(await readFile(ledgerPath)), "precondition: existing ledger is plaintext");
2715
+
2716
+ const rowBytes = Buffer.byteLength(`${JSON.stringify(events[0])}\n`, "utf8");
2717
+ // Cap so the UNRESERVED budget (cap − 1) keeps 20 rows whose encrypted size
2718
+ // (20 rows + envelope) reaches the cap, while the reserving budget
2719
+ // (cap − envelope − 1) keeps 19 rows that land strictly below it.
2720
+ const cap = rowBytes * 20 + SECURE_STORE_ENVELOPE_OVERHEAD_BYTES;
2721
+ assert.ok(
2722
+ 20 * rowBytes + SECURE_STORE_ENVELOPE_OVERHEAD_BYTES >= cap,
2723
+ "sanity: an unreserved 20-row plaintext budget encrypts to at/over the cap",
2724
+ );
2725
+ assert.ok(
2726
+ 19 * rowBytes + SECURE_STORE_ENVELOPE_OVERHEAD_BYTES < cap,
2727
+ "sanity: a reserved 19-row budget encrypts to strictly below the cap",
2728
+ );
2729
+
2730
+ const key = Buffer.alloc(32, 5);
2731
+ const storage = new StorageManager(memoryDir);
2732
+ storage.setSecureStoreRequired(true);
2733
+ storage.setSecureStoreKey(key); // unlocked + encrypt-on-write ⇒ the rewrite encrypts.
2734
+ assert.ok(storage.willEncryptStateWrites(), "precondition: writes will be encrypted at rest");
2735
+
2736
+ const scheduler = buildCompactionSchedulerWithStorage(
2737
+ fixtureConfig({
2738
+ memoryDir,
2739
+ memoryLifecycleLedgerCompactBytes: rowBytes * 5,
2740
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2741
+ }),
2742
+ () => storage,
2743
+ );
2744
+ scheduler.lifecycleLedgerMaxBytes = cap;
2745
+ try {
2746
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2747
+ const onDisk = (await stat(ledgerPath)).size;
2748
+ assert.ok(isEncryptedFile(await readFile(ledgerPath)), "rewritten ledger encrypted at rest");
2749
+ assert.ok(onDisk < cap, `encrypted on-disk ledger (${onDisk}B) must be strictly below the cap (${cap}B)`);
2750
+ const keptIds = (await storage.readAllMemoryLifecycleEvents()).map((e) => e.eventId);
2751
+ assert.ok(keptIds.includes(`cap-${String(total - 1).padStart(3, "0")}`), "newest event survives");
2752
+ assert.ok(!keptIds.includes("cap-000"), "oldest event archived out of the active ledger");
2753
+
2754
+ // Throttle armed only after an EFFECTIVE compaction: an unreserved budget
2755
+ // would have reported "failed" (over-cap) and recompacted on this pass.
2756
+ await writeFile(ledgerPath, plaintext, "utf-8");
2757
+ const regrown = (await stat(ledgerPath)).size;
2758
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2759
+ assert.equal(
2760
+ (await stat(ledgerPath)).size,
2761
+ regrown,
2762
+ "throttle armed after an effective compaction: second in-window pass is a no-op",
2763
+ );
2764
+ } finally {
2765
+ scheduler.dispose();
2766
+ }
2767
+ } finally {
2768
+ await rm(memoryDir, { recursive: true, force: true });
2769
+ }
2770
+ });
2771
+
2772
+ test("catalog-disabled fallback pending drain skips unsafe spill entries (symlink/FIFO/dir) without following or blocking (#2033)", { timeout: 30_000 }, async () => {
2773
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-fallback-unsafe-"));
2774
+ const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-autocompact-fallback-outside-"));
2775
+ try {
2776
+ const token = namespaceIdentityToken("project-unsafe");
2777
+ const nsDir = path.join(root, "namespaces", token);
2778
+ const nsLedger = path.join(nsDir, "state", "memory-lifecycle-ledger.jsonl");
2779
+ await mkdir(path.dirname(nsLedger), { recursive: true });
2780
+ await writeFile(nsLedger, "", "utf-8");
2781
+ const spillDir = pendingLifecycleLedgerDir(nsLedger);
2782
+ await mkdir(spillDir, { recursive: true });
2783
+
2784
+ // A legitimate plaintext spill that MUST drain.
2785
+ const goodPath = path.join(spillDir, "good.jsonl");
2786
+ const spilled = {
2787
+ eventId: "evt-good-spilled",
2788
+ memoryId: "mem-ns",
2789
+ eventType: "explicit_capture_accepted",
2790
+ timestamp: "2026-03-08T00:00:00.000Z",
2791
+ actor: "explicit-capture",
2792
+ ruleVersion: "memory-lifecycle-ledger.v1",
2793
+ };
2794
+ await writeFile(goodPath, `${JSON.stringify(spilled)}\n`, "utf-8");
2795
+
2796
+ // Unsafe entries the guarded lister MUST skip WITHOUT opening/following:
2797
+ // - a symlink to an OUTSIDE file (following it would escape the spill dir),
2798
+ // - a FIFO (opening it for read would block until a writer appears),
2799
+ // - a subdirectory (not a regular file).
2800
+ const outsideFile = path.join(outside, "target.jsonl");
2801
+ await writeFile(outsideFile, '{"eventId":"evt-outside"}\n', "utf-8");
2802
+ const linkPath = path.join(spillDir, "link.jsonl");
2803
+ await symlink(outsideFile, linkPath);
2804
+ const fifoPath = path.join(spillDir, "pipe.jsonl");
2805
+ execFileSync("mkfifo", [fifoPath]);
2806
+ const dirPath = path.join(spillDir, "dir.jsonl");
2807
+ await mkdir(dirPath);
2808
+
2809
+ const scheduler = buildCompactionScheduler(
2810
+ fixtureConfig({
2811
+ memoryDir: root,
2812
+ namespacesEnabled: true,
2813
+ memoryLifecycleLedgerCompactBytes: 1024,
2814
+ memoryLifecycleLedgerCompactMinIntervalMs: 60 * 60 * 1000,
2815
+ }),
2816
+ );
2817
+ try {
2818
+ // Completing at all is part of the assertion: opening the FIFO would hang
2819
+ // (caught by the test timeout) and following the symlink would escape.
2820
+ await scheduler.maybeCompactMemoryLifecycleLedger();
2821
+ } finally {
2822
+ scheduler.dispose();
2823
+ }
2824
+
2825
+ const ledger = await readFile(nsLedger, "utf-8");
2826
+ assert.ok(ledger.includes("evt-good-spilled"), "the safe plaintext spill drains into the ledger");
2827
+ assert.ok(!ledger.includes("evt-outside"), "the symlink target outside the spill dir is never followed");
2828
+ await assert.rejects(() => stat(goodPath), /ENOENT/, "drained safe spill removed");
2829
+ // Unsafe entries are left untouched (skipped, not read or deleted).
2830
+ assert.ok((await lstat(linkPath)).isSymbolicLink(), "symlink entry left in place");
2831
+ assert.ok((await lstat(fifoPath)).isFIFO(), "FIFO entry left in place");
2832
+ assert.ok((await lstat(dirPath)).isDirectory(), "directory entry left in place");
2833
+ assert.equal(await readFile(outsideFile, "utf-8"), '{"eventId":"evt-outside"}\n', "outside target intact and unread");
2834
+ } finally {
2835
+ await rm(root, { recursive: true, force: true });
2836
+ await rm(outside, { recursive: true, force: true });
2837
+ }
2838
+ });