@remnic/core 10.0.36 → 10.0.37

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 (364) hide show
  1. package/dist/access-admin-ops-surface.js +77 -77
  2. package/dist/access-authorization-probe.js +78 -78
  3. package/dist/access-boundary.js +77 -77
  4. package/dist/access-citation-namespace.js +2 -2
  5. package/dist/access-cli.js +168 -166
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-extraction-force-flush.js +77 -77
  8. package/dist/access-http-briefing.js +78 -78
  9. package/dist/access-http-query.js +79 -79
  10. package/dist/access-http.js +100 -100
  11. package/dist/access-identity-continuity-surface.js +77 -77
  12. package/dist/access-lcm-surface.js +77 -77
  13. package/dist/access-mcp.js +89 -89
  14. package/dist/access-namespace-preflight.js +77 -77
  15. package/dist/access-observe-write-category.js +13 -13
  16. package/dist/access-observe-write-surface.js +77 -77
  17. package/dist/access-offline-manifest.js +12 -12
  18. package/dist/access-operations-batch.js +81 -81
  19. package/dist/access-operations.js +88 -88
  20. package/dist/access-recall-concurrency.js +77 -77
  21. package/dist/access-recall-response.js +77 -77
  22. package/dist/access-recall-surface.js +77 -77
  23. package/dist/access-schema.js +8 -8
  24. package/dist/access-service.js +77 -77
  25. package/dist/access-surface-catalog.js +79 -79
  26. package/dist/active-recall.js +40 -40
  27. package/dist/adapters/index.js +7 -7
  28. package/dist/adapters/registry.js +3 -3
  29. package/dist/{auto-sync-VPHR3FBI.js → auto-sync-R3XGJL5I.js} +7 -7
  30. package/dist/briefing.js +29 -29
  31. package/dist/buffer-eviction.d.ts +16 -0
  32. package/dist/buffer-eviction.d.ts.map +1 -0
  33. package/dist/buffer-eviction.js +8 -0
  34. package/dist/buffer-persistence.d.ts +45 -0
  35. package/dist/buffer-persistence.d.ts.map +1 -0
  36. package/dist/buffer-persistence.js +8 -0
  37. package/dist/buffer.d.ts +5 -2
  38. package/dist/buffer.d.ts.map +1 -1
  39. package/dist/buffer.js +13 -11
  40. package/dist/causal-behavior.js +2 -2
  41. package/dist/causal-chain.js +2 -2
  42. package/dist/causal-consolidation.js +32 -32
  43. package/dist/causal-retrieval.js +2 -2
  44. package/dist/causal-trajectory-graph.js +2 -2
  45. package/dist/causal-trajectory.js +1 -1
  46. package/dist/{chunk-NIANATGL.js → chunk-2RJO7VS4.js} +2 -2
  47. package/dist/{chunk-YLF42JQ7.js → chunk-3BPMYMPX.js} +2 -2
  48. package/dist/{chunk-GAARPWMN.js → chunk-3L5WH6CV.js} +2 -2
  49. package/dist/{chunk-XVZA6VXC.js → chunk-3SL52JKD.js} +4 -4
  50. package/dist/chunk-4OFXLQJD.js +58 -0
  51. package/dist/chunk-4OFXLQJD.js.map +1 -0
  52. package/dist/{chunk-S2PDFSXS.js → chunk-4P4VLPGB.js} +1 -1
  53. package/dist/{chunk-2ZYIAUYU.js → chunk-5CWH2LKL.js} +4 -4
  54. package/dist/{chunk-VKB2FGNW.js → chunk-5U3NUHMA.js} +85 -85
  55. package/dist/{chunk-PDYP7SUC.js → chunk-5UWP5X3T.js} +2 -2
  56. package/dist/{chunk-G3T6YMBO.js → chunk-64S6X4IU.js} +7 -7
  57. package/dist/{chunk-NHB4RC3G.js → chunk-6QOL4G76.js} +2 -2
  58. package/dist/{chunk-NBH7IRME.js → chunk-6R6CU3MR.js} +51 -51
  59. package/dist/{chunk-4ISHIPOC.js → chunk-7PG7QBWD.js} +2 -2
  60. package/dist/{chunk-A4B7WL5N.js → chunk-7ZBANI3M.js} +2 -2
  61. package/dist/{chunk-DG5UKN2Y.js → chunk-A23DPFFI.js} +2 -2
  62. package/dist/{chunk-77ZK533I.js → chunk-ARFT6ZBE.js} +2 -2
  63. package/dist/{chunk-V44DWTFA.js → chunk-AYXRK264.js} +6 -6
  64. package/dist/{chunk-XO4HOBM2.js → chunk-BPBUUGAO.js} +2 -2
  65. package/dist/{chunk-DFP37GUN.js → chunk-CCYXCT4B.js} +2 -2
  66. package/dist/{chunk-IKCGNTGS.js → chunk-CNQQNYJ2.js} +2 -2
  67. package/dist/{chunk-QZP3RMYP.js → chunk-CU7WDDPV.js} +4 -4
  68. package/dist/{chunk-GY4R4P2A.js → chunk-CUWOSRCS.js} +2 -2
  69. package/dist/{chunk-CJKHLACF.js → chunk-D2AKUKAQ.js} +2 -2
  70. package/dist/{chunk-BRZRP5WD.js → chunk-DK5VMJL3.js} +2 -2
  71. package/dist/{chunk-77IXBNV6.js → chunk-E24RUQEN.js} +2 -2
  72. package/dist/{chunk-PTQZPVRQ.js → chunk-E5PVMB3I.js} +2 -2
  73. package/dist/{chunk-AHS3QEEA.js → chunk-EEOYF3IB.js} +2 -2
  74. package/dist/{chunk-PH33R7GN.js → chunk-EHIKY2IA.js} +2 -2
  75. package/dist/{chunk-LJSBF6PW.js → chunk-EVVCE7S2.js} +2 -2
  76. package/dist/{chunk-FQ52AJXX.js → chunk-FBAHBFT5.js} +2 -2
  77. package/dist/{chunk-NDJ42GKB.js → chunk-FO5AFCRI.js} +5 -5
  78. package/dist/{chunk-5LWODP5R.js → chunk-FRAL3EU4.js} +2 -2
  79. package/dist/{chunk-AAANTXCQ.js → chunk-FZVZYU5C.js} +2 -2
  80. package/dist/{chunk-OHMGR6QN.js → chunk-G2SRB62M.js} +2 -2
  81. package/dist/{chunk-ORWDCX5Q.js → chunk-G6PPA4TH.js} +11 -11
  82. package/dist/{chunk-VNAIUNXI.js → chunk-GCVUBVEN.js} +10 -10
  83. package/dist/{chunk-TGAXRT7N.js → chunk-GFYFKXWZ.js} +2 -2
  84. package/dist/{chunk-ZABEDRGQ.js → chunk-HIMXDRSQ.js} +2 -2
  85. package/dist/{chunk-GDYLZ6FR.js → chunk-HJ3UTGIW.js} +3 -3
  86. package/dist/{chunk-AD3ZB72W.js → chunk-HKEK27M4.js} +2 -2
  87. package/dist/{chunk-X3IFRQKS.js → chunk-I73ZSORJ.js} +4 -4
  88. package/dist/{chunk-XUB4I2N6.js → chunk-JFI3ATK4.js} +2 -2
  89. package/dist/{chunk-R4SATHCV.js → chunk-JH2ALMPR.js} +2 -2
  90. package/dist/{chunk-TKBW4YLS.js → chunk-JH4FCVWJ.js} +2 -2
  91. package/dist/{chunk-UAYA4NJN.js → chunk-JWTUJVIF.js} +28 -36
  92. package/dist/chunk-JWTUJVIF.js.map +1 -0
  93. package/dist/chunk-KASJJGRG.js +29 -0
  94. package/dist/chunk-KASJJGRG.js.map +1 -0
  95. package/dist/{chunk-X73SCHKR.js → chunk-KC5SVCUJ.js} +2 -2
  96. package/dist/{chunk-6NXHX6MI.js → chunk-KOY25HUB.js} +2 -2
  97. package/dist/{chunk-TRW7N4FS.js → chunk-L2JQSG5M.js} +2 -2
  98. package/dist/{chunk-SIIWHIBK.js → chunk-L46Y5FPE.js} +2 -2
  99. package/dist/{chunk-CIJRM6SQ.js → chunk-LAQS5HYU.js} +2 -2
  100. package/dist/{chunk-TYT4TEQV.js → chunk-LDWE7OZS.js} +6 -2
  101. package/dist/chunk-LDWE7OZS.js.map +1 -0
  102. package/dist/{chunk-23VGZ65V.js → chunk-LHBOFROT.js} +2 -2
  103. package/dist/{chunk-LO775P3S.js → chunk-LRJKHXU4.js} +2 -2
  104. package/dist/{chunk-QISYYK4L.js → chunk-LSSCMZQ2.js} +3 -3
  105. package/dist/{chunk-PCIIKQXW.js → chunk-MBMT5FUQ.js} +2 -2
  106. package/dist/{chunk-XFDHL252.js → chunk-MGXFJOUR.js} +3 -3
  107. package/dist/{chunk-7WTTE5FW.js → chunk-MH3LC7KB.js} +2 -2
  108. package/dist/{chunk-ERMT5WQM.js → chunk-MKE5DP3S.js} +2 -2
  109. package/dist/{chunk-DJMC4W6N.js → chunk-MNDHKZZQ.js} +2 -2
  110. package/dist/{chunk-7FG2JSU6.js → chunk-NMKETOAX.js} +2 -2
  111. package/dist/{chunk-RYUSWAAU.js → chunk-NQOM6PHO.js} +6 -6
  112. package/dist/{chunk-AI2VXOSO.js → chunk-NSQSOLCE.js} +2 -2
  113. package/dist/{chunk-UR25IQVT.js → chunk-P5CUEB2Q.js} +8 -8
  114. package/dist/{chunk-FXO43NBW.js → chunk-PSQ2P2T3.js} +3 -3
  115. package/dist/{chunk-X544TW54.js → chunk-PTRPOJ7U.js} +2 -2
  116. package/dist/{chunk-4YYIZ475.js → chunk-QEGE5QCL.js} +45 -45
  117. package/dist/{chunk-KQUN4RSX.js → chunk-QIC7YZ3D.js} +16 -16
  118. package/dist/{chunk-KRAMCBIW.js → chunk-QKXPOLQD.js} +4 -4
  119. package/dist/{chunk-O3VKQMWC.js → chunk-R6TCM47S.js} +364 -166
  120. package/dist/chunk-R6TCM47S.js.map +1 -0
  121. package/dist/{chunk-G5UR4DXQ.js → chunk-RBC4GIZU.js} +2 -2
  122. package/dist/{chunk-65JLKIVA.js → chunk-RVYXSS3Q.js} +7 -7
  123. package/dist/{chunk-G7XAZU2L.js → chunk-RWWYBE7E.js} +3 -3
  124. package/dist/{chunk-HJM23E65.js → chunk-S53WZWGL.js} +2 -2
  125. package/dist/{chunk-3ODEGTVV.js → chunk-TDCR6PQL.js} +2 -2
  126. package/dist/{chunk-QCP75JGO.js → chunk-TFA6HPOA.js} +5 -5
  127. package/dist/{chunk-SE3JAD22.js → chunk-TPKB7T6Z.js} +2 -2
  128. package/dist/{chunk-6VYY2XFK.js → chunk-TVIGN3XZ.js} +19 -19
  129. package/dist/{chunk-ZG3HZZ6K.js → chunk-TYGLV6ZG.js} +5 -1
  130. package/dist/{chunk-ZG3HZZ6K.js.map → chunk-TYGLV6ZG.js.map} +1 -1
  131. package/dist/{chunk-K567EIW3.js → chunk-U4JXQCJ3.js} +2 -2
  132. package/dist/{chunk-WI6MIJYK.js → chunk-U7HJBODU.js} +5 -5
  133. package/dist/{chunk-IS4HKNWP.js → chunk-UBN5AOLT.js} +2 -2
  134. package/dist/{chunk-7VJEZQYF.js → chunk-UFBIOF7C.js} +3 -3
  135. package/dist/{chunk-JNELS2TW.js → chunk-UJDTXWMT.js} +2 -2
  136. package/dist/{chunk-M3GKSCFN.js → chunk-ULUX6Q7R.js} +4 -4
  137. package/dist/{chunk-V3JZZJ2E.js → chunk-UMFBHYPW.js} +11 -11
  138. package/dist/{chunk-GKNX4QO3.js → chunk-UQWA5QDT.js} +3 -3
  139. package/dist/{chunk-4Z7O2QVJ.js → chunk-USOXPN5H.js} +2 -2
  140. package/dist/{chunk-XA32GZ6B.js → chunk-UXXVOA4D.js} +3 -3
  141. package/dist/{chunk-5XTSIZ3L.js → chunk-V5RJCXLG.js} +6 -6
  142. package/dist/{chunk-A2D3WICT.js → chunk-VGFNTY3Y.js} +2 -2
  143. package/dist/{chunk-EH6KLYIE.js → chunk-VKTP3TZB.js} +4 -4
  144. package/dist/{chunk-CSGFGQBI.js → chunk-VPO53QMG.js} +2 -2
  145. package/dist/{chunk-ZSAQSLKL.js → chunk-WADPRAZE.js} +2 -2
  146. package/dist/{chunk-IVYAPAHG.js → chunk-WRDFYSUC.js} +2 -2
  147. package/dist/{chunk-F2JLBXMG.js → chunk-WSLNC5AL.js} +8 -8
  148. package/dist/{chunk-C6S2Y7HE.js → chunk-XKDQTDXN.js} +4 -4
  149. package/dist/{chunk-4AHKNZW7.js → chunk-XLEP7SS3.js} +2 -2
  150. package/dist/{chunk-B6L7BKA7.js → chunk-XRFLR3XF.js} +4 -4
  151. package/dist/{chunk-H7QC7WCU.js → chunk-XTBEXZFN.js} +4 -4
  152. package/dist/{chunk-S2S4U4G3.js → chunk-Y6Y3TRBN.js} +4 -4
  153. package/dist/{chunk-H6SKBCNI.js → chunk-YC2C67F7.js} +2 -2
  154. package/dist/{chunk-6FIFEXFF.js → chunk-YGYT7S3G.js} +7 -7
  155. package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
  156. package/dist/{chunk-YOL56HY5.js → chunk-YQ6SLFNZ.js} +3 -3
  157. package/dist/{chunk-G22A6WJQ.js → chunk-Z2T5BPOZ.js} +2 -2
  158. package/dist/{chunk-IHTKBQBS.js → chunk-Z5AQZRHF.js} +2 -2
  159. package/dist/{chunk-QQ4T622E.js → chunk-Z5QWUR3Y.js} +2 -2
  160. package/dist/cli.js +127 -127
  161. package/dist/coding/export-okf-codegraph.js +30 -30
  162. package/dist/coding/pre-action-gate.js +2 -2
  163. package/dist/compounding/engine.js +31 -31
  164. package/dist/config.js +40 -40
  165. package/dist/connectors/codex-materialize-runner.js +29 -29
  166. package/dist/connectors/index.js +29 -29
  167. package/dist/contradiction/index.js +5 -5
  168. package/dist/convergence-refresh.js +31 -31
  169. package/dist/conversation-index/backend.js +2 -2
  170. package/dist/corpus-watermark.js +31 -31
  171. package/dist/dashboard-runtime.js +2 -2
  172. package/dist/deep-recall-http-glue.js +9 -9
  173. package/dist/entity-retrieval.js +31 -31
  174. package/dist/explicit-capture.js +7 -7
  175. package/dist/external-wiki-access.js +78 -78
  176. package/dist/extraction-span-materialize.js +2 -2
  177. package/dist/extraction.js +44 -44
  178. package/dist/{graph-edge-decay-IU75NV42.js → graph-edge-decay-2BZTY2IM.js} +5 -5
  179. package/dist/graph-edge-decay-2BZTY2IM.js.map +1 -0
  180. package/dist/graph-jsonl.js +3 -3
  181. package/dist/graph-snapshot.js +3 -3
  182. package/dist/graph.js +2 -2
  183. package/dist/harmonic-retrieval.js +30 -30
  184. package/dist/harmonic-source-lookup.js +2 -2
  185. package/dist/index.js +225 -223
  186. package/dist/lcm/index.js +3 -3
  187. package/dist/location/index.js +32 -32
  188. package/dist/maintenance/memory-governance.js +29 -29
  189. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +29 -29
  190. package/dist/maintenance/rebuild-memory-projection-runner.js +31 -31
  191. package/dist/maintenance/rebuild-memory-projection.js +30 -30
  192. package/dist/memory-browse-operations.js +78 -78
  193. package/dist/memory-browse.js +5 -5
  194. package/dist/memory-subject.js +3 -3
  195. package/dist/namespaces/migrate.js +30 -30
  196. package/dist/namespaces/search.js +11 -11
  197. package/dist/namespaces/storage.js +29 -29
  198. package/dist/offline-sync-changeset-generations.js +3 -3
  199. package/dist/offline-sync-embedding-generation.js +2 -2
  200. package/dist/offline-sync-file-io.js +2 -2
  201. package/dist/offline-sync-impression-drain.js +3 -3
  202. package/dist/offline-sync-runtime-state.d.ts.map +1 -1
  203. package/dist/offline-sync-runtime-state.js +1 -1
  204. package/dist/offline-sync-snapshot-stream.js +8 -8
  205. package/dist/offline-sync-upload-staging.js +3 -3
  206. package/dist/offline-sync.js +8 -8
  207. package/dist/operator-doctor-corpus.js +32 -32
  208. package/dist/operator-toolkit.js +51 -51
  209. package/dist/orchestration/maintenance.js +41 -41
  210. package/dist/orchestrator.js +173 -171
  211. package/dist/qmd.js +4 -4
  212. package/dist/recall-navigation-operations.js +78 -78
  213. package/dist/recall-navigation.js +77 -77
  214. package/dist/recall-state-view-admission.js +5 -5
  215. package/dist/recall-why-service.js +5 -5
  216. package/dist/reconcile/manifest.js +3 -3
  217. package/dist/{resolution-V6JBMW7U.js → resolution-4ZEQPXUZ.js} +6 -6
  218. package/dist/resolution-4ZEQPXUZ.js.map +1 -0
  219. package/dist/resume-bundles.js +41 -41
  220. package/dist/retrieval-agents.js +2 -2
  221. package/dist/salvage-envelope.js +5 -5
  222. package/dist/search/factory.js +10 -10
  223. package/dist/search/index.js +14 -14
  224. package/dist/search/lancedb-backend.js +2 -2
  225. package/dist/search/meilisearch-backend.js +2 -2
  226. package/dist/search/orama-backend.js +2 -2
  227. package/dist/secure-store/index.js +2 -2
  228. package/dist/semantic-consolidation.js +30 -30
  229. package/dist/semantic-rule-promotion.js +29 -29
  230. package/dist/semantic-rule-verifier.js +29 -29
  231. package/dist/source-agent-qualifier.js +34 -34
  232. package/dist/standup/build.js +29 -29
  233. package/dist/storage/buffer-session-files.d.ts +69 -0
  234. package/dist/storage/buffer-session-files.d.ts.map +1 -0
  235. package/dist/storage.d.ts +5 -1
  236. package/dist/storage.d.ts.map +1 -1
  237. package/dist/storage.js +28 -28
  238. package/dist/subject-config.js +4 -4
  239. package/dist/support-passport/index.js +80 -80
  240. package/dist/transfer/export-okf.js +30 -30
  241. package/dist/verified-recall.js +29 -29
  242. package/dist/who-knows-cli.js +30 -30
  243. package/dist/who-knows.js +29 -29
  244. package/dist/write-envelope.js +4 -4
  245. package/package.json +2 -2
  246. package/src/buffer-eviction.ts +51 -0
  247. package/src/buffer-incremental-persistence.test.ts +256 -0
  248. package/src/buffer-persistence.ts +89 -0
  249. package/src/buffer.ts +32 -61
  250. package/src/offline-sync-runtime-state.ts +24 -2
  251. package/src/offline-sync.test.ts +9 -0
  252. package/src/secure-store/secure-fs.ts +11 -0
  253. package/src/secure-store/secure-store.test.ts +22 -0
  254. package/src/storage/buffer-session-files.ts +249 -0
  255. package/src/storage-buffer-sessions.test.ts +369 -0
  256. package/src/storage.ts +11 -16
  257. package/dist/chunk-O3VKQMWC.js.map +0 -1
  258. package/dist/chunk-TYT4TEQV.js.map +0 -1
  259. package/dist/chunk-UAYA4NJN.js.map +0 -1
  260. /package/dist/{auto-sync-VPHR3FBI.js.map → auto-sync-R3XGJL5I.js.map} +0 -0
  261. /package/dist/{graph-edge-decay-IU75NV42.js.map → buffer-eviction.js.map} +0 -0
  262. /package/dist/{resolution-V6JBMW7U.js.map → buffer-persistence.js.map} +0 -0
  263. /package/dist/{chunk-NIANATGL.js.map → chunk-2RJO7VS4.js.map} +0 -0
  264. /package/dist/{chunk-YLF42JQ7.js.map → chunk-3BPMYMPX.js.map} +0 -0
  265. /package/dist/{chunk-GAARPWMN.js.map → chunk-3L5WH6CV.js.map} +0 -0
  266. /package/dist/{chunk-XVZA6VXC.js.map → chunk-3SL52JKD.js.map} +0 -0
  267. /package/dist/{chunk-S2PDFSXS.js.map → chunk-4P4VLPGB.js.map} +0 -0
  268. /package/dist/{chunk-2ZYIAUYU.js.map → chunk-5CWH2LKL.js.map} +0 -0
  269. /package/dist/{chunk-VKB2FGNW.js.map → chunk-5U3NUHMA.js.map} +0 -0
  270. /package/dist/{chunk-PDYP7SUC.js.map → chunk-5UWP5X3T.js.map} +0 -0
  271. /package/dist/{chunk-G3T6YMBO.js.map → chunk-64S6X4IU.js.map} +0 -0
  272. /package/dist/{chunk-NHB4RC3G.js.map → chunk-6QOL4G76.js.map} +0 -0
  273. /package/dist/{chunk-NBH7IRME.js.map → chunk-6R6CU3MR.js.map} +0 -0
  274. /package/dist/{chunk-4ISHIPOC.js.map → chunk-7PG7QBWD.js.map} +0 -0
  275. /package/dist/{chunk-A4B7WL5N.js.map → chunk-7ZBANI3M.js.map} +0 -0
  276. /package/dist/{chunk-DG5UKN2Y.js.map → chunk-A23DPFFI.js.map} +0 -0
  277. /package/dist/{chunk-77ZK533I.js.map → chunk-ARFT6ZBE.js.map} +0 -0
  278. /package/dist/{chunk-V44DWTFA.js.map → chunk-AYXRK264.js.map} +0 -0
  279. /package/dist/{chunk-XO4HOBM2.js.map → chunk-BPBUUGAO.js.map} +0 -0
  280. /package/dist/{chunk-DFP37GUN.js.map → chunk-CCYXCT4B.js.map} +0 -0
  281. /package/dist/{chunk-IKCGNTGS.js.map → chunk-CNQQNYJ2.js.map} +0 -0
  282. /package/dist/{chunk-QZP3RMYP.js.map → chunk-CU7WDDPV.js.map} +0 -0
  283. /package/dist/{chunk-GY4R4P2A.js.map → chunk-CUWOSRCS.js.map} +0 -0
  284. /package/dist/{chunk-CJKHLACF.js.map → chunk-D2AKUKAQ.js.map} +0 -0
  285. /package/dist/{chunk-BRZRP5WD.js.map → chunk-DK5VMJL3.js.map} +0 -0
  286. /package/dist/{chunk-77IXBNV6.js.map → chunk-E24RUQEN.js.map} +0 -0
  287. /package/dist/{chunk-PTQZPVRQ.js.map → chunk-E5PVMB3I.js.map} +0 -0
  288. /package/dist/{chunk-AHS3QEEA.js.map → chunk-EEOYF3IB.js.map} +0 -0
  289. /package/dist/{chunk-PH33R7GN.js.map → chunk-EHIKY2IA.js.map} +0 -0
  290. /package/dist/{chunk-LJSBF6PW.js.map → chunk-EVVCE7S2.js.map} +0 -0
  291. /package/dist/{chunk-FQ52AJXX.js.map → chunk-FBAHBFT5.js.map} +0 -0
  292. /package/dist/{chunk-NDJ42GKB.js.map → chunk-FO5AFCRI.js.map} +0 -0
  293. /package/dist/{chunk-5LWODP5R.js.map → chunk-FRAL3EU4.js.map} +0 -0
  294. /package/dist/{chunk-AAANTXCQ.js.map → chunk-FZVZYU5C.js.map} +0 -0
  295. /package/dist/{chunk-OHMGR6QN.js.map → chunk-G2SRB62M.js.map} +0 -0
  296. /package/dist/{chunk-ORWDCX5Q.js.map → chunk-G6PPA4TH.js.map} +0 -0
  297. /package/dist/{chunk-VNAIUNXI.js.map → chunk-GCVUBVEN.js.map} +0 -0
  298. /package/dist/{chunk-TGAXRT7N.js.map → chunk-GFYFKXWZ.js.map} +0 -0
  299. /package/dist/{chunk-ZABEDRGQ.js.map → chunk-HIMXDRSQ.js.map} +0 -0
  300. /package/dist/{chunk-GDYLZ6FR.js.map → chunk-HJ3UTGIW.js.map} +0 -0
  301. /package/dist/{chunk-AD3ZB72W.js.map → chunk-HKEK27M4.js.map} +0 -0
  302. /package/dist/{chunk-X3IFRQKS.js.map → chunk-I73ZSORJ.js.map} +0 -0
  303. /package/dist/{chunk-XUB4I2N6.js.map → chunk-JFI3ATK4.js.map} +0 -0
  304. /package/dist/{chunk-R4SATHCV.js.map → chunk-JH2ALMPR.js.map} +0 -0
  305. /package/dist/{chunk-TKBW4YLS.js.map → chunk-JH4FCVWJ.js.map} +0 -0
  306. /package/dist/{chunk-X73SCHKR.js.map → chunk-KC5SVCUJ.js.map} +0 -0
  307. /package/dist/{chunk-6NXHX6MI.js.map → chunk-KOY25HUB.js.map} +0 -0
  308. /package/dist/{chunk-TRW7N4FS.js.map → chunk-L2JQSG5M.js.map} +0 -0
  309. /package/dist/{chunk-SIIWHIBK.js.map → chunk-L46Y5FPE.js.map} +0 -0
  310. /package/dist/{chunk-CIJRM6SQ.js.map → chunk-LAQS5HYU.js.map} +0 -0
  311. /package/dist/{chunk-23VGZ65V.js.map → chunk-LHBOFROT.js.map} +0 -0
  312. /package/dist/{chunk-LO775P3S.js.map → chunk-LRJKHXU4.js.map} +0 -0
  313. /package/dist/{chunk-QISYYK4L.js.map → chunk-LSSCMZQ2.js.map} +0 -0
  314. /package/dist/{chunk-PCIIKQXW.js.map → chunk-MBMT5FUQ.js.map} +0 -0
  315. /package/dist/{chunk-XFDHL252.js.map → chunk-MGXFJOUR.js.map} +0 -0
  316. /package/dist/{chunk-7WTTE5FW.js.map → chunk-MH3LC7KB.js.map} +0 -0
  317. /package/dist/{chunk-ERMT5WQM.js.map → chunk-MKE5DP3S.js.map} +0 -0
  318. /package/dist/{chunk-DJMC4W6N.js.map → chunk-MNDHKZZQ.js.map} +0 -0
  319. /package/dist/{chunk-7FG2JSU6.js.map → chunk-NMKETOAX.js.map} +0 -0
  320. /package/dist/{chunk-RYUSWAAU.js.map → chunk-NQOM6PHO.js.map} +0 -0
  321. /package/dist/{chunk-AI2VXOSO.js.map → chunk-NSQSOLCE.js.map} +0 -0
  322. /package/dist/{chunk-UR25IQVT.js.map → chunk-P5CUEB2Q.js.map} +0 -0
  323. /package/dist/{chunk-FXO43NBW.js.map → chunk-PSQ2P2T3.js.map} +0 -0
  324. /package/dist/{chunk-X544TW54.js.map → chunk-PTRPOJ7U.js.map} +0 -0
  325. /package/dist/{chunk-4YYIZ475.js.map → chunk-QEGE5QCL.js.map} +0 -0
  326. /package/dist/{chunk-KQUN4RSX.js.map → chunk-QIC7YZ3D.js.map} +0 -0
  327. /package/dist/{chunk-KRAMCBIW.js.map → chunk-QKXPOLQD.js.map} +0 -0
  328. /package/dist/{chunk-G5UR4DXQ.js.map → chunk-RBC4GIZU.js.map} +0 -0
  329. /package/dist/{chunk-65JLKIVA.js.map → chunk-RVYXSS3Q.js.map} +0 -0
  330. /package/dist/{chunk-G7XAZU2L.js.map → chunk-RWWYBE7E.js.map} +0 -0
  331. /package/dist/{chunk-HJM23E65.js.map → chunk-S53WZWGL.js.map} +0 -0
  332. /package/dist/{chunk-3ODEGTVV.js.map → chunk-TDCR6PQL.js.map} +0 -0
  333. /package/dist/{chunk-QCP75JGO.js.map → chunk-TFA6HPOA.js.map} +0 -0
  334. /package/dist/{chunk-SE3JAD22.js.map → chunk-TPKB7T6Z.js.map} +0 -0
  335. /package/dist/{chunk-6VYY2XFK.js.map → chunk-TVIGN3XZ.js.map} +0 -0
  336. /package/dist/{chunk-K567EIW3.js.map → chunk-U4JXQCJ3.js.map} +0 -0
  337. /package/dist/{chunk-WI6MIJYK.js.map → chunk-U7HJBODU.js.map} +0 -0
  338. /package/dist/{chunk-IS4HKNWP.js.map → chunk-UBN5AOLT.js.map} +0 -0
  339. /package/dist/{chunk-7VJEZQYF.js.map → chunk-UFBIOF7C.js.map} +0 -0
  340. /package/dist/{chunk-JNELS2TW.js.map → chunk-UJDTXWMT.js.map} +0 -0
  341. /package/dist/{chunk-M3GKSCFN.js.map → chunk-ULUX6Q7R.js.map} +0 -0
  342. /package/dist/{chunk-V3JZZJ2E.js.map → chunk-UMFBHYPW.js.map} +0 -0
  343. /package/dist/{chunk-GKNX4QO3.js.map → chunk-UQWA5QDT.js.map} +0 -0
  344. /package/dist/{chunk-4Z7O2QVJ.js.map → chunk-USOXPN5H.js.map} +0 -0
  345. /package/dist/{chunk-XA32GZ6B.js.map → chunk-UXXVOA4D.js.map} +0 -0
  346. /package/dist/{chunk-5XTSIZ3L.js.map → chunk-V5RJCXLG.js.map} +0 -0
  347. /package/dist/{chunk-A2D3WICT.js.map → chunk-VGFNTY3Y.js.map} +0 -0
  348. /package/dist/{chunk-EH6KLYIE.js.map → chunk-VKTP3TZB.js.map} +0 -0
  349. /package/dist/{chunk-CSGFGQBI.js.map → chunk-VPO53QMG.js.map} +0 -0
  350. /package/dist/{chunk-ZSAQSLKL.js.map → chunk-WADPRAZE.js.map} +0 -0
  351. /package/dist/{chunk-IVYAPAHG.js.map → chunk-WRDFYSUC.js.map} +0 -0
  352. /package/dist/{chunk-F2JLBXMG.js.map → chunk-WSLNC5AL.js.map} +0 -0
  353. /package/dist/{chunk-C6S2Y7HE.js.map → chunk-XKDQTDXN.js.map} +0 -0
  354. /package/dist/{chunk-4AHKNZW7.js.map → chunk-XLEP7SS3.js.map} +0 -0
  355. /package/dist/{chunk-B6L7BKA7.js.map → chunk-XRFLR3XF.js.map} +0 -0
  356. /package/dist/{chunk-H7QC7WCU.js.map → chunk-XTBEXZFN.js.map} +0 -0
  357. /package/dist/{chunk-S2S4U4G3.js.map → chunk-Y6Y3TRBN.js.map} +0 -0
  358. /package/dist/{chunk-H6SKBCNI.js.map → chunk-YC2C67F7.js.map} +0 -0
  359. /package/dist/{chunk-6FIFEXFF.js.map → chunk-YGYT7S3G.js.map} +0 -0
  360. /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
  361. /package/dist/{chunk-YOL56HY5.js.map → chunk-YQ6SLFNZ.js.map} +0 -0
  362. /package/dist/{chunk-G22A6WJQ.js.map → chunk-Z2T5BPOZ.js.map} +0 -0
  363. /package/dist/{chunk-IHTKBQBS.js.map → chunk-Z5AQZRHF.js.map} +0 -0
  364. /package/dist/{chunk-QQ4T622E.js.map → chunk-Z5QWUR3Y.js.map} +0 -0
@@ -0,0 +1,249 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { Stats } from "node:fs";
3
+ import { lstat, readdir, unlink } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { log } from "../logger.js";
6
+ import { SecureStoreDecryptError, SecureStoreLockedError } from "../secure-store/secure-fs.js";
7
+ import type { BufferEntryState, BufferState, BufferTurn } from "../types.js";
8
+
9
+ /** The secure-store file I/O the owning StorageManager already provides. */
10
+ export interface BufferFileIo {
11
+ read(filePath: string): Promise<string>;
12
+ write(filePath: string, content: string): Promise<void>;
13
+ }
14
+
15
+ type FsError = NodeJS.ErrnoException;
16
+
17
+ function isMissing(err: FsError | null): boolean {
18
+ return err?.code === "ENOENT";
19
+ }
20
+
21
+ function asFsError(err: Error): FsError {
22
+ return err;
23
+ }
24
+
25
+ /**
26
+ * Case-independent, collision-free file name for a buffer key: the lowercase
27
+ * hex SHA-256 of the key under a fixed prefix. Hex has no case and no path
28
+ * characters, so `Session-A` and `session-a` get different names even on
29
+ * case-insensitive filesystems, every key (short or long) lives in the same
30
+ * naming domain, and the key itself is stored inside the file (loading never
31
+ * inverts the hash).
32
+ */
33
+ export function bufferSessionFileName(key: string): string {
34
+ return `s-${createHash("sha256").update(key).digest("hex")}.json`;
35
+ }
36
+
37
+ /** Flatten a legacy BufferState into per-session entries (top-level fields
38
+ * are the legacy mirror of the "default" entry). */
39
+ export function legacyStateToEntries(state: BufferState): Map<string, BufferEntryState> {
40
+ const entries = new Map<string, BufferEntryState>();
41
+ for (const [key, value] of Object.entries(state.entries ?? {})) {
42
+ if (value && Array.isArray(value.turns)) entries.set(key, value);
43
+ }
44
+ if (!entries.has("default")) {
45
+ entries.set("default", {
46
+ turns: Array.isArray(state.turns) ? state.turns : [],
47
+ lastExtractionAt: state.lastExtractionAt ?? null,
48
+ extractionCount: Number.isFinite(state.extractionCount) ? state.extractionCount : 0,
49
+ });
50
+ }
51
+ return entries;
52
+ }
53
+
54
+ /** What a per-session file contains once parsed (written only by this module). */
55
+ interface SessionFileBody {
56
+ key?: string;
57
+ turns?: BufferTurn[];
58
+ retainedTurns?: BufferTurn[];
59
+ lastExtractionAt?: string | null;
60
+ extractionCount?: number;
61
+ }
62
+
63
+ function entryFromBody(body: SessionFileBody): { key: string; entry: BufferEntryState } | null {
64
+ if (!body || !Array.isArray(body.turns) || (body.key?.length ?? 0) === 0) return null;
65
+ const entry: BufferEntryState = {
66
+ turns: body.turns,
67
+ lastExtractionAt: body.lastExtractionAt ?? null,
68
+ extractionCount: Number.isFinite(body.extractionCount) ? Number(body.extractionCount) : 0,
69
+ };
70
+ if (Array.isArray(body.retainedTurns)) entry.retainedTurns = body.retainedTurns;
71
+ return { key: String(body.key), entry };
72
+ }
73
+
74
+ /**
75
+ * One file per buffer session under `<stateDir>/buffer/`, so the observe hot
76
+ * path persists only the session that changed. All writes happen under
77
+ * SmartBuffer's serialized mutation chain (one writer per process).
78
+ */
79
+ export class BufferSessionFiles {
80
+ private dirVerified = false;
81
+
82
+ constructor(
83
+ private readonly stateDir: string,
84
+ private readonly io: BufferFileIo,
85
+ ) {}
86
+
87
+ private get dir(): string {
88
+ return path.join(this.stateDir, "buffer");
89
+ }
90
+
91
+ private fileFor(key: string): string {
92
+ return path.join(this.dir, bufferSessionFileName(key));
93
+ }
94
+
95
+ /**
96
+ * Refuse a symlinked buffer directory: following it would send buffered
97
+ * conversation data (and deletions) outside the memory root. One lstat of
98
+ * one directory, cached after it passes, so the observe path pays nothing
99
+ * per write; the cache is dropped on any failure.
100
+ */
101
+ private async assertDirSafe(): Promise<void> {
102
+ if (this.dirVerified) return;
103
+ let stat: Stats | null = null;
104
+ try {
105
+ stat = await lstat(this.dir);
106
+ } catch (err) {
107
+ if (!(err instanceof Error) || !isMissing(asFsError(err))) throw err;
108
+ }
109
+ if (stat && (stat.isSymbolicLink() || !stat.isDirectory())) {
110
+ throw new Error(`buffer session directory must be a real directory, not a symlink or file: ${this.dir}`);
111
+ }
112
+ this.dirVerified = stat !== null;
113
+ }
114
+
115
+ /** Persist ONE session atomically. Compact JSON: machine-read state, and
116
+ * pretty-printing a large buffer costs CPU and bytes on every observe. */
117
+ async save(key: string, entry: BufferEntryState): Promise<void> {
118
+ await this.assertDirSafe();
119
+ await this.io.write(this.fileFor(key), JSON.stringify({ key, ...entry }));
120
+ }
121
+
122
+ /** Remove one session's file. Idempotent: an absent file is fine. */
123
+ async remove(key: string): Promise<void> {
124
+ await this.assertDirSafe();
125
+ try {
126
+ await unlink(this.fileFor(key));
127
+ } catch (err) {
128
+ if (!(err instanceof Error) || !isMissing(asFsError(err))) throw err;
129
+ }
130
+ }
131
+
132
+ private async listFiles(): Promise<string[]> {
133
+ await this.assertDirSafe();
134
+ try {
135
+ const dirents = await readdir(this.dir, { withFileTypes: true });
136
+ // Regular files only: a symlink inside the directory is never followed.
137
+ return dirents.filter((d) => d.isFile() && d.name.endsWith(".json")).map((d) => d.name);
138
+ } catch (err) {
139
+ if (err instanceof Error && isMissing(asFsError(err))) return [];
140
+ throw err;
141
+ }
142
+ }
143
+
144
+ /** Read every per-session file. Corrupt files are skipped (and logged) so
145
+ * one bad file cannot nuke the other sessions' buffered turns. */
146
+ private async readAll(): Promise<{ entries: Map<string, BufferEntryState>; fileCount: number }> {
147
+ const names = await this.listFiles();
148
+ const entries = new Map<string, BufferEntryState>();
149
+ for (const name of names) {
150
+ try {
151
+ const body: SessionFileBody = JSON.parse(await this.io.read(path.join(this.dir, name)));
152
+ const parsed = entryFromBody(body);
153
+ if (!parsed) throw new Error("invalid buffer session file shape");
154
+ if (entries.has(parsed.key)) {
155
+ log.warn(`buffer: duplicate session file for key ${parsed.key} (${name}); keeping the first`);
156
+ continue;
157
+ }
158
+ entries.set(parsed.key, parsed.entry);
159
+ } catch (err) {
160
+ // A locked store or a wrong key is not a corrupt file: reading it as empty would let
161
+ // later saves replace the real encrypted session.
162
+ if (err instanceof SecureStoreLockedError || err instanceof SecureStoreDecryptError) throw err;
163
+ log.warn(`buffer: skipping unreadable per-session buffer file ${name}: ${err instanceof Error ? err.message : String(err)}`);
164
+ }
165
+ }
166
+ return { entries, fileCount: names.length };
167
+ }
168
+
169
+ /**
170
+ * Read the legacy single-file buffer.json for migration. Null when absent,
171
+ * or unreadable while session files exist (they are authoritative; a
172
+ * corrupt leftover must not block startup). With no session files a corrupt
173
+ * legacy file THROWS, preserving the old contract so callers can reset it.
174
+ */
175
+ private async readLegacy(hasSessionFiles: boolean): Promise<BufferState | null> {
176
+ let raw: string;
177
+ try {
178
+ raw = await this.io.read(path.join(this.stateDir, "buffer.json"));
179
+ } catch (err) {
180
+ if (err instanceof SecureStoreLockedError) throw err;
181
+ if (err instanceof Error && isMissing(asFsError(err))) return null;
182
+ throw err;
183
+ }
184
+ try {
185
+ const legacy: BufferState = JSON.parse(raw);
186
+ return legacy;
187
+ } catch (err) {
188
+ if (!hasSessionFiles) throw err;
189
+ log.warn(
190
+ `buffer: legacy buffer.json is unreadable but per-session buffer files exist; ` +
191
+ `ignoring the legacy file (${err instanceof Error ? err.message : String(err)})`,
192
+ );
193
+ return null;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Load the whole buffer, migrating a legacy buffer.json once. A per-session
199
+ * file that parsed wins over the legacy snapshot (a crash mid-migration
200
+ * leaves exactly that pair); legacy entries without a usable file are
201
+ * written out, then the legacy file is retired. Both steps are idempotent.
202
+ */
203
+ async load(): Promise<BufferState> {
204
+ const { entries, fileCount } = await this.readAll();
205
+ const legacy = await this.readLegacy(fileCount > 0);
206
+ if (legacy !== null) {
207
+ for (const [key, entry] of legacyStateToEntries(legacy)) {
208
+ if (entries.has(key)) continue;
209
+ entries.set(key, entry);
210
+ await this.save(key, entry);
211
+ }
212
+ try {
213
+ await unlink(path.join(this.stateDir, "buffer.json"));
214
+ } catch (err) {
215
+ if (!(err instanceof Error) || !isMissing(asFsError(err))) throw err;
216
+ }
217
+ }
218
+ const out: NonNullable<BufferState["entries"]> = Object.create(null);
219
+ for (const [key, entry] of entries) out[key] = entry;
220
+ const def = entries.get("default");
221
+ return {
222
+ turns: def?.turns ?? [],
223
+ lastExtractionAt: def?.lastExtractionAt ?? null,
224
+ extractionCount: def?.extractionCount ?? 0,
225
+ entries: out,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Reconcile the files with a WHOLE buffer state: write every entry, then
231
+ * delete files for keys no longer present. Writes first, deletes last: a
232
+ * crash leaves stale extra files (removed by the next reconcile) but never
233
+ * loses sessions the state says exist. Not on the observe hot path.
234
+ */
235
+ async reconcile(state: BufferState): Promise<void> {
236
+ const entries = legacyStateToEntries(state);
237
+ const existing = await this.listFiles();
238
+ for (const [key, entry] of entries) await this.save(key, entry);
239
+ const live = new Set([...entries.keys()].map((key) => bufferSessionFileName(key)));
240
+ for (const name of existing) {
241
+ if (live.has(name)) continue;
242
+ try {
243
+ await unlink(path.join(this.dir, name));
244
+ } catch (err) {
245
+ if (!(err instanceof Error) || !isMissing(asFsError(err))) throw err;
246
+ }
247
+ }
248
+ }
249
+ }
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Per-session buffer persistence (storage layer).
3
+ *
4
+ * The legacy buffer persistence rewrote the WHOLE state/buffer.json — every
5
+ * session's turns — on every buffer save, which on long-running daemons meant
6
+ * serializing 100+ MB per observe. These tests pin the replacement contract:
7
+ *
8
+ * - one file per session under `<memoryDir>/state/buffer/`,
9
+ * - incremental `saveBufferSession` / `deleteBufferSession` for the hot path,
10
+ * - whole-state `saveBuffer` as a full reconcile (used at correctness
11
+ * boundaries and by legacy callers),
12
+ * - one-time migration from the legacy single-file format, safe when a crash
13
+ * lands mid-migration (per-session file wins over the legacy snapshot),
14
+ * - per-session writes do NOT run the heavyweight `ensureDirectories()`
15
+ * pass (the realpath/lstat storm) — they mkdir only what the write needs.
16
+ */
17
+ import assert from "node:assert/strict";
18
+ import { mkdir, mkdtemp, readdir, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
19
+ import os from "node:os";
20
+ import path from "node:path";
21
+ import test from "node:test";
22
+
23
+ import { SecureStoreDecryptError } from "./secure-store/secure-fs.js";
24
+ import { StorageManager } from "./storage.js";
25
+ import { bufferSessionFileName } from "./storage/buffer-session-files.js";
26
+ import type { BufferEntryState, BufferState } from "./types.js";
27
+
28
+ async function tempRoot(prefix: string): Promise<string> {
29
+ return mkdtemp(path.join(os.tmpdir(), `${prefix}-`));
30
+ }
31
+
32
+ function makeEntry(turnContent: string, extractionCount = 0): BufferEntryState {
33
+ return {
34
+ turns: [
35
+ {
36
+ role: "user",
37
+ content: turnContent,
38
+ timestamp: "2026-10-29T12:00:00.000Z",
39
+ sessionKey: "session-a",
40
+ },
41
+ ],
42
+ lastExtractionAt: extractionCount > 0 ? "2026-10-29T12:01:00.000Z" : null,
43
+ extractionCount,
44
+ };
45
+ }
46
+
47
+ test("saveBufferSession writes one file per session and loadBuffer reads it back", async () => {
48
+ const root = await tempRoot("bufio-sessions");
49
+ try {
50
+ const storage = new StorageManager(root);
51
+ await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
52
+
53
+ const bufferDir = path.join(root, "state", "buffer");
54
+ const names = await readdir(bufferDir);
55
+ assert.equal(names.length, 1, `expected exactly one session file, got ${names.join(",")}`);
56
+
57
+ const state = await storage.loadBuffer();
58
+ assert.equal(state.entries?.["session-a"]?.turns.length, 1);
59
+ assert.equal(state.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
60
+ } finally {
61
+ await rm(root, { recursive: true, force: true });
62
+ }
63
+ });
64
+
65
+ test("saving session B leaves session A's file byte-identical", async () => {
66
+ const root = await tempRoot("bufio-isolation");
67
+ try {
68
+ const storage = new StorageManager(root);
69
+ await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
70
+ const dir = path.join(root, "state", "buffer");
71
+ const [aName] = await readdir(dir);
72
+ const before = await readFile(path.join(dir, aName));
73
+ const beforeStat = await stat(path.join(dir, aName));
74
+
75
+ await storage.saveBufferSession("session-b", makeEntry("beta fact"));
76
+ const after = await readFile(path.join(dir, aName));
77
+ const afterStat = await stat(path.join(dir, aName));
78
+ assert.deepEqual(after, before, "session A file content changed when session B was saved");
79
+ assert.equal(afterStat.mtimeMs, beforeStat.mtimeMs, "session A file was rewritten when session B was saved");
80
+ } finally {
81
+ await rm(root, { recursive: true, force: true });
82
+ }
83
+ });
84
+
85
+ test("deleteBufferSession removes the file and loadBuffer drops the session", async () => {
86
+ const root = await tempRoot("bufio-delete");
87
+ try {
88
+ const storage = new StorageManager(root);
89
+ await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
90
+ await storage.deleteBufferSession("session-a");
91
+ const state = await storage.loadBuffer();
92
+ assert.equal(state.entries?.["session-a"], undefined);
93
+ // Deleting an absent session is a no-op, not a throw (idempotent cleanup).
94
+ await storage.deleteBufferSession("session-a");
95
+ } finally {
96
+ await rm(root, { recursive: true, force: true });
97
+ }
98
+ });
99
+
100
+ test("legacy state/buffer.json migrates to per-session files exactly once", async () => {
101
+ const root = await tempRoot("bufio-migrate");
102
+ try {
103
+ const stateDir = path.join(root, "state");
104
+ await mkdir(stateDir, { recursive: true });
105
+ const legacy: BufferState = {
106
+ turns: [{ role: "user", content: "default turn", timestamp: "2026-10-29T12:00:00.000Z", sessionKey: "default" }],
107
+ lastExtractionAt: null,
108
+ extractionCount: 2,
109
+ entries: {
110
+ default: {
111
+ turns: [{ role: "user", content: "default turn", timestamp: "2026-10-29T12:00:00.000Z", sessionKey: "default" }],
112
+ lastExtractionAt: null,
113
+ extractionCount: 2,
114
+ },
115
+ "session-a": makeEntry("alpha fact"),
116
+ },
117
+ };
118
+ await writeFile(path.join(stateDir, "buffer.json"), JSON.stringify(legacy), "utf8");
119
+
120
+ const storage = new StorageManager(root);
121
+ const loaded = await storage.loadBuffer();
122
+ assert.equal(loaded.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
123
+ assert.equal(loaded.entries?.default?.extractionCount, 2);
124
+
125
+ // Migration is materialized: per-session files exist, the legacy file is gone.
126
+ const names = await readdir(path.join(stateDir, "buffer"));
127
+ assert.equal(names.filter((n) => n.endsWith(".json")).length, 2, "expected one file per legacy entry");
128
+ await assert.rejects(stat(path.join(stateDir, "buffer.json")), { code: "ENOENT" });
129
+
130
+ // Second load reads purely from the per-session files.
131
+ const reloaded = await storage.loadBuffer();
132
+ assert.equal(reloaded.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
133
+ } finally {
134
+ await rm(root, { recursive: true, force: true });
135
+ }
136
+ });
137
+
138
+ test("crash mid-migration: existing per-session file wins over the legacy snapshot", async () => {
139
+ const root = await tempRoot("bufio-migrate-crash");
140
+ try {
141
+ const stateDir = path.join(root, "state");
142
+ await mkdir(path.join(stateDir, "buffer"), { recursive: true });
143
+ // Session A was already migrated (with a NEWER turn) before the crash;
144
+ // the legacy file still holds the older snapshot for both sessions.
145
+ await storage_writeSession(stateDir, "session-a", makeEntry("alpha fact NEW"));
146
+ const legacy: BufferState = {
147
+ turns: [],
148
+ lastExtractionAt: null,
149
+ extractionCount: 0,
150
+ entries: {
151
+ "session-a": makeEntry("alpha fact OLD"),
152
+ "session-b": makeEntry("beta fact"),
153
+ },
154
+ };
155
+ await writeFile(path.join(stateDir, "buffer.json"), JSON.stringify(legacy), "utf8");
156
+
157
+ const storage = new StorageManager(root);
158
+ const loaded = await storage.loadBuffer();
159
+ assert.equal(loaded.entries?.["session-a"]?.turns[0]?.content, "alpha fact NEW");
160
+ assert.equal(loaded.entries?.["session-b"]?.turns[0]?.content, "beta fact");
161
+ await assert.rejects(stat(path.join(stateDir, "buffer.json")), { code: "ENOENT" });
162
+ } finally {
163
+ await rm(root, { recursive: true, force: true });
164
+ }
165
+ });
166
+
167
+ async function storage_writeSession(stateDir: string, key: string, entry: BufferEntryState): Promise<void> {
168
+ // Written directly (pre-migration crash simulation) using the same file naming
169
+ // the storage layer derives; the storage instance is created after setup.
170
+ await writeFile(
171
+ path.join(stateDir, "buffer", bufferSessionFileName(key)),
172
+ JSON.stringify({ key, ...entry }),
173
+ "utf8",
174
+ );
175
+ }
176
+
177
+ test("whole-state saveBuffer reconciles per-session files (writes, deletes stale)", async () => {
178
+ const root = await tempRoot("bufio-reconcile");
179
+ try {
180
+ const storage = new StorageManager(root);
181
+ await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
182
+ await storage.saveBufferSession("session-gone", makeEntry("stale fact"));
183
+
184
+ await storage.saveBuffer({
185
+ turns: [],
186
+ lastExtractionAt: null,
187
+ extractionCount: 0,
188
+ entries: { "session-a": makeEntry("alpha fact v2") },
189
+ });
190
+
191
+ const state = await storage.loadBuffer();
192
+ assert.equal(state.entries?.["session-a"]?.turns[0]?.content, "alpha fact v2");
193
+ assert.equal(state.entries?.["session-gone"], undefined, "stale session file survived a whole-state reconcile");
194
+ const names = await readdir(path.join(root, "state", "buffer"));
195
+ // Reconcile materializes the "default" entry too (legacy semantics: the
196
+ // top-level state IS the default session), so expect exactly those two.
197
+ assert.deepEqual(
198
+ names.filter((n) => n.endsWith(".json")).sort(),
199
+ [bufferSessionFileName("session-a"), bufferSessionFileName("default")].sort(),
200
+ );
201
+ } finally {
202
+ await rm(root, { recursive: true, force: true });
203
+ }
204
+ });
205
+
206
+ test("a corrupt per-session file is skipped; the rest of the buffer loads", async () => {
207
+ const root = await tempRoot("bufio-corrupt-file");
208
+ try {
209
+ const storage = new StorageManager(root);
210
+ await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
211
+ await storage.saveBufferSession("session-b", makeEntry("beta fact"));
212
+ const dir = path.join(root, "state", "buffer");
213
+ const names = (await readdir(dir)).filter((n) => n === bufferSessionFileName("session-b"));
214
+ await writeFile(path.join(dir, names[0]), "{not json", "utf8");
215
+
216
+ const state = await storage.loadBuffer();
217
+ assert.equal(state.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
218
+ assert.equal(state.entries?.["session-b"], undefined);
219
+ } finally {
220
+ await rm(root, { recursive: true, force: true });
221
+ }
222
+ });
223
+
224
+ test("a corrupt session file is replaced by the legacy snapshot during migration", async () => {
225
+ const root = await tempRoot("bufio-corrupt-plus-legacy");
226
+ try {
227
+ const stateDir = path.join(root, "state");
228
+ await mkdir(path.join(stateDir, "buffer"), { recursive: true });
229
+ await writeFile(
230
+ path.join(stateDir, "buffer", bufferSessionFileName("session-a")),
231
+ "{not json",
232
+ "utf8",
233
+ );
234
+ const legacy: BufferState = {
235
+ turns: [],
236
+ lastExtractionAt: null,
237
+ extractionCount: 0,
238
+ entries: { "session-a": makeEntry("alpha fact") },
239
+ };
240
+ await writeFile(path.join(stateDir, "buffer.json"), JSON.stringify(legacy), "utf8");
241
+
242
+ const storage = new StorageManager(root);
243
+ const loaded = await storage.loadBuffer();
244
+ assert.equal(loaded.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
245
+ await assert.rejects(stat(path.join(stateDir, "buffer.json")), { code: "ENOENT" });
246
+ } finally {
247
+ await rm(root, { recursive: true, force: true });
248
+ }
249
+ });
250
+
251
+ test("corrupt legacy buffer.json with no session files still throws (reset path preserved)", async () => {
252
+ const root = await tempRoot("bufio-corrupt-legacy");
253
+ try {
254
+ const stateDir = path.join(root, "state");
255
+ await mkdir(stateDir, { recursive: true });
256
+ await writeFile(path.join(stateDir, "buffer.json"), "{not json", "utf8");
257
+ const storage = new StorageManager(root);
258
+ await assert.rejects(() => storage.loadBuffer());
259
+ } finally {
260
+ await rm(root, { recursive: true, force: true });
261
+ }
262
+ });
263
+
264
+ test("per-session save does not run the heavyweight ensureDirectories pass", async () => {
265
+ const root = await tempRoot("bufio-light-save");
266
+ try {
267
+ const storage = new StorageManager(root);
268
+ await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
269
+ // ensureDirectories() materializes facts/, entities/, questions/, ... — a
270
+ // per-session buffer write must only need state/buffer/.
271
+ assert.equal(await exists(path.join(root, "entities")), false, "entities dir created by a buffer session save");
272
+ assert.equal(await exists(path.join(root, "questions")), false, "questions dir created by a buffer session save");
273
+ const state = await storage.loadBuffer();
274
+ assert.equal(state.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
275
+ } finally {
276
+ await rm(root, { recursive: true, force: true });
277
+ }
278
+ });
279
+
280
+ async function exists(p: string): Promise<boolean> {
281
+ try {
282
+ await stat(p);
283
+ return true;
284
+ } catch {
285
+ return false;
286
+ }
287
+ }
288
+
289
+ test("session file names round-trip awkward buffer keys", async () => {
290
+ const root = await tempRoot("bufio-awkward-keys");
291
+ try {
292
+ const storage = new StorageManager(root);
293
+ const keys = ["default", "agent:main:abc123", "weird /slash/key", "__proto__", "unicode-ключ-🔑"];
294
+ for (const [i, key] of keys.entries()) {
295
+ await storage.saveBufferSession(key, makeEntry(`fact-${i}`));
296
+ }
297
+ const state = await storage.loadBuffer();
298
+ for (const [i, key] of keys.entries()) {
299
+ assert.equal(state.entries?.[key]?.turns[0]?.content, `fact-${i}`, `key ${key} did not round-trip`);
300
+ }
301
+ const names = await readdir(path.join(root, "state", "buffer"));
302
+ assert.equal(names.filter((n) => n.endsWith(".json")).length, keys.length, "two keys collided into one file");
303
+ } finally {
304
+ await rm(root, { recursive: true, force: true });
305
+ }
306
+ });
307
+
308
+ test("session file names do not collide on case-insensitive filesystems", () => {
309
+ const keys = ["Session-A", "session-a", "SESSION-A", "h" + "0".repeat(64), "x".repeat(400), "x".repeat(401)];
310
+ const folded = new Set(keys.map((key) => bufferSessionFileName(key).toLowerCase()));
311
+ assert.equal(folded.size, keys.length, "two keys fold to the same file name");
312
+ });
313
+
314
+ test("a symlinked buffer directory is refused and nothing is written through it", async () => {
315
+ const root = await tempRoot("bufio-symlink-dir");
316
+ const outside = await tempRoot("bufio-outside");
317
+ try {
318
+ const storage = new StorageManager(root);
319
+ await mkdir(path.join(root, "state"), { recursive: true });
320
+ await symlink(outside, path.join(root, "state", "buffer"));
321
+
322
+ await assert.rejects(storage.saveBufferSession("session-a", makeEntry("secret")), /symlink/);
323
+ await assert.rejects(storage.deleteBufferSession("session-a"), /symlink/);
324
+ await assert.rejects(storage.loadBuffer(), /symlink/);
325
+ assert.deepEqual(await readdir(outside), [], "nothing may be written outside the memory root");
326
+ } finally {
327
+ await rm(root, { recursive: true, force: true });
328
+ await rm(outside, { recursive: true, force: true });
329
+ }
330
+ });
331
+
332
+ test("a symlinked session file is never followed when loading", async () => {
333
+ const root = await tempRoot("bufio-symlink-file");
334
+ const outside = await tempRoot("bufio-outside-file");
335
+ try {
336
+ const storage = new StorageManager(root);
337
+ await storage.saveBufferSession("session-a", makeEntry("alpha fact"));
338
+ const target = path.join(outside, "stolen.json");
339
+ await writeFile(target, JSON.stringify({ key: "session-evil", ...makeEntry("evil fact") }), "utf8");
340
+ await symlink(target, path.join(root, "state", "buffer", "s-planted.json"));
341
+
342
+ const state = await storage.loadBuffer();
343
+
344
+ assert.equal(state.entries?.["session-a"]?.turns[0]?.content, "alpha fact");
345
+ assert.equal(state.entries?.["session-evil"], undefined, "the symlink target must not be read");
346
+ } finally {
347
+ await rm(root, { recursive: true, force: true });
348
+ await rm(outside, { recursive: true, force: true });
349
+ }
350
+ });
351
+
352
+ test("loading with the wrong secure-store key rejects instead of reading an empty buffer", async () => {
353
+ const root = await tempRoot("bufio-wrong-key");
354
+ try {
355
+ const writer = new StorageManager(root);
356
+ writer.setSecureStoreKey(Buffer.alloc(32, 1), true);
357
+ await writer.saveBufferSession("session-a", makeEntry("sealed turn"));
358
+
359
+ const reader = new StorageManager(root);
360
+ reader.setSecureStoreKey(Buffer.alloc(32, 2), true);
361
+
362
+ await assert.rejects(reader.loadBuffer(), SecureStoreDecryptError);
363
+ // The encrypted file must survive: a rejected load must not let later saves replace it.
364
+ const names = await readdir(path.join(root, "state", "buffer"));
365
+ assert.deepEqual(names, [bufferSessionFileName("session-a")]);
366
+ } finally {
367
+ await rm(root, { recursive: true, force: true });
368
+ }
369
+ });
package/src/storage.ts CHANGED
@@ -142,9 +142,11 @@ import {
142
142
  type TombstoneStats,
143
143
  } from "./lifecycle/tombstones.js";
144
144
  import { supersessionKeysForFact } from "./temporal-supersession.js";
145
+ import { BufferSessionFiles } from "./storage/buffer-session-files.js";
145
146
  import { runSupersessionSideEffects, type SupersessionAuditOptions } from "./storage/supersession-side-effects.js";
146
147
  import type {
147
148
  AccessTrackingEntry,
149
+ BufferEntryState,
148
150
  BufferState,
149
151
  ConfidenceTier,
150
152
  ContinuityIncidentCloseInput,
@@ -5508,23 +5510,16 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
5508
5510
  return deleted;
5509
5511
  }
5510
5512
 
5511
- async loadBuffer(): Promise<BufferState> {
5512
- const bufferPath = path.join(this.stateDir, "buffer.json");
5513
- try {
5514
- const raw = await this.readStorageSecureFile(bufferPath);
5515
- return JSON.parse(raw) as BufferState;
5516
- } catch (err) {
5517
- if (err instanceof SecureStoreLockedError) throw err;
5518
- if (!isErrnoCode(err, "ENOENT")) throw err;
5519
- return { turns: [], lastExtractionAt: null, extractionCount: 0 };
5520
- }
5521
- }
5522
-
5523
- async saveBuffer(state: BufferState): Promise<void> {
5524
- await this.ensureDirectories();
5525
- const bufferPath = path.join(this.stateDir, "buffer.json");
5526
- await this.writeStorageSecureFile(bufferPath, JSON.stringify(state, null, 2));
5513
+ // Buffer persistence: one file per session (storage/buffer-session-files.ts).
5514
+ private bufferFiles?: BufferSessionFiles;
5515
+ private bufferSessionFiles(): BufferSessionFiles {
5516
+ const io = { read: (p: string) => this.readStorageSecureFile(p), write: (p: string, c: string) => this.writeStorageSecureFile(p, c) };
5517
+ return (this.bufferFiles ??= new BufferSessionFiles(this.stateDir, io));
5527
5518
  }
5519
+ async loadBuffer(): Promise<BufferState> { return this.bufferSessionFiles().load(); }
5520
+ async saveBuffer(state: BufferState): Promise<void> { await this.bufferSessionFiles().reconcile(state); }
5521
+ async saveBufferSession(key: string, entry: BufferEntryState): Promise<void> { await this.bufferSessionFiles().save(key, entry); }
5522
+ async deleteBufferSession(key: string): Promise<void> { await this.bufferSessionFiles().remove(key); }
5528
5523
 
5529
5524
  async loadMeta(): Promise<MetaState> {
5530
5525
  const metaPath = path.join(this.stateDir, "meta.json");