@remnic/core 10.0.35 → 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 (385) hide show
  1. package/dist/access-admin-ops-surface.js +81 -81
  2. package/dist/access-authorization-probe.js +82 -82
  3. package/dist/access-boundary.js +81 -81
  4. package/dist/access-citation-namespace.js +2 -2
  5. package/dist/access-cli.js +172 -170
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-extraction-force-flush.js +81 -81
  8. package/dist/access-http-briefing.js +82 -82
  9. package/dist/access-http-query.js +83 -83
  10. package/dist/access-http.js +104 -104
  11. package/dist/access-identity-continuity-surface.js +81 -81
  12. package/dist/access-lcm-surface.js +81 -81
  13. package/dist/access-mcp.js +93 -93
  14. package/dist/access-namespace-preflight.js +81 -81
  15. package/dist/access-observe-write-category.js +13 -13
  16. package/dist/access-observe-write-surface.js +81 -81
  17. package/dist/access-offline-manifest.js +12 -12
  18. package/dist/access-operations-batch.js +85 -85
  19. package/dist/access-operations.js +92 -92
  20. package/dist/access-recall-concurrency.js +81 -81
  21. package/dist/access-recall-response.js +81 -81
  22. package/dist/access-recall-surface.js +81 -81
  23. package/dist/access-schema.js +8 -8
  24. package/dist/access-service.js +81 -81
  25. package/dist/access-surface-catalog.js +83 -83
  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/calibration.js +3 -3
  41. package/dist/causal-behavior.js +2 -2
  42. package/dist/causal-chain.js +2 -2
  43. package/dist/causal-consolidation.js +32 -32
  44. package/dist/causal-retrieval.js +2 -2
  45. package/dist/causal-trajectory-graph.js +2 -2
  46. package/dist/causal-trajectory.js +1 -1
  47. package/dist/{chunk-HT6IEMQ2.js → chunk-2RJO7VS4.js} +2 -2
  48. package/dist/{chunk-YLF42JQ7.js → chunk-3BPMYMPX.js} +2 -2
  49. package/dist/{chunk-GAARPWMN.js → chunk-3L5WH6CV.js} +2 -2
  50. package/dist/{chunk-XVZA6VXC.js → chunk-3SL52JKD.js} +4 -4
  51. package/dist/chunk-4OFXLQJD.js +58 -0
  52. package/dist/chunk-4OFXLQJD.js.map +1 -0
  53. package/dist/{chunk-S2PDFSXS.js → chunk-4P4VLPGB.js} +1 -1
  54. package/dist/{chunk-2ZYIAUYU.js → chunk-5CWH2LKL.js} +4 -4
  55. package/dist/{chunk-G2PC6IZB.js → chunk-5U3NUHMA.js} +86 -86
  56. package/dist/{chunk-4LOMATQA.js → chunk-5UWP5X3T.js} +2 -2
  57. package/dist/{chunk-JUXOTY6J.js → chunk-5YQXWVCP.js} +9 -2
  58. package/dist/chunk-5YQXWVCP.js.map +1 -0
  59. package/dist/{chunk-G3T6YMBO.js → chunk-64S6X4IU.js} +7 -7
  60. package/dist/{chunk-NHB4RC3G.js → chunk-6QOL4G76.js} +2 -2
  61. package/dist/{chunk-GRBQO56D.js → chunk-6R6CU3MR.js} +51 -51
  62. package/dist/{chunk-SQ554O3K.js → chunk-74AY3S7U.js} +3 -3
  63. package/dist/{chunk-4ISHIPOC.js → chunk-7PG7QBWD.js} +2 -2
  64. package/dist/{chunk-A4B7WL5N.js → chunk-7ZBANI3M.js} +2 -2
  65. package/dist/{chunk-DG5UKN2Y.js → chunk-A23DPFFI.js} +2 -2
  66. package/dist/{chunk-77ZK533I.js → chunk-ARFT6ZBE.js} +2 -2
  67. package/dist/{chunk-V44DWTFA.js → chunk-AYXRK264.js} +6 -6
  68. package/dist/{chunk-GRZX3RHY.js → chunk-BPBUUGAO.js} +2 -2
  69. package/dist/{chunk-DFP37GUN.js → chunk-CCYXCT4B.js} +2 -2
  70. package/dist/{chunk-JJWOKRZA.js → chunk-CNQQNYJ2.js} +2 -2
  71. package/dist/{chunk-QZP3RMYP.js → chunk-CU7WDDPV.js} +4 -4
  72. package/dist/{chunk-GY4R4P2A.js → chunk-CUWOSRCS.js} +2 -2
  73. package/dist/{chunk-CJKHLACF.js → chunk-D2AKUKAQ.js} +2 -2
  74. package/dist/{chunk-BRZRP5WD.js → chunk-DK5VMJL3.js} +2 -2
  75. package/dist/{chunk-77IXBNV6.js → chunk-E24RUQEN.js} +2 -2
  76. package/dist/{chunk-PTQZPVRQ.js → chunk-E5PVMB3I.js} +2 -2
  77. package/dist/{chunk-AHS3QEEA.js → chunk-EEOYF3IB.js} +2 -2
  78. package/dist/{chunk-PH33R7GN.js → chunk-EHIKY2IA.js} +2 -2
  79. package/dist/{chunk-NLQBTHBV.js → chunk-EVVCE7S2.js} +2 -2
  80. package/dist/{chunk-FQ52AJXX.js → chunk-FBAHBFT5.js} +2 -2
  81. package/dist/{chunk-D75ET36O.js → chunk-FL6SLVAJ.js} +2 -2
  82. package/dist/{chunk-6NAN4WFY.js → chunk-FO5AFCRI.js} +5 -5
  83. package/dist/{chunk-5LWODP5R.js → chunk-FRAL3EU4.js} +2 -2
  84. package/dist/{chunk-AAANTXCQ.js → chunk-FZVZYU5C.js} +2 -2
  85. package/dist/{chunk-GQVTALUJ.js → chunk-G2SRB62M.js} +2 -2
  86. package/dist/{chunk-ORWDCX5Q.js → chunk-G6PPA4TH.js} +11 -11
  87. package/dist/{chunk-VNAIUNXI.js → chunk-GCVUBVEN.js} +10 -10
  88. package/dist/{chunk-TGAXRT7N.js → chunk-GFYFKXWZ.js} +2 -2
  89. package/dist/{chunk-ZABEDRGQ.js → chunk-HIMXDRSQ.js} +2 -2
  90. package/dist/{chunk-GDYLZ6FR.js → chunk-HJ3UTGIW.js} +3 -3
  91. package/dist/{chunk-MGXLLL5L.js → chunk-HKEK27M4.js} +2 -2
  92. package/dist/{chunk-X3IFRQKS.js → chunk-I73ZSORJ.js} +4 -4
  93. package/dist/{chunk-XUB4I2N6.js → chunk-JFI3ATK4.js} +2 -2
  94. package/dist/{chunk-R4SATHCV.js → chunk-JH2ALMPR.js} +2 -2
  95. package/dist/{chunk-TKBW4YLS.js → chunk-JH4FCVWJ.js} +2 -2
  96. package/dist/{chunk-UAYA4NJN.js → chunk-JWTUJVIF.js} +28 -36
  97. package/dist/chunk-JWTUJVIF.js.map +1 -0
  98. package/dist/chunk-KASJJGRG.js +29 -0
  99. package/dist/chunk-KASJJGRG.js.map +1 -0
  100. package/dist/{chunk-X73SCHKR.js → chunk-KC5SVCUJ.js} +2 -2
  101. package/dist/{chunk-6NXHX6MI.js → chunk-KOY25HUB.js} +2 -2
  102. package/dist/{chunk-TRW7N4FS.js → chunk-L2JQSG5M.js} +2 -2
  103. package/dist/{chunk-SIIWHIBK.js → chunk-L46Y5FPE.js} +2 -2
  104. package/dist/{chunk-CIJRM6SQ.js → chunk-LAQS5HYU.js} +2 -2
  105. package/dist/{chunk-TYT4TEQV.js → chunk-LDWE7OZS.js} +6 -2
  106. package/dist/chunk-LDWE7OZS.js.map +1 -0
  107. package/dist/{chunk-23VGZ65V.js → chunk-LHBOFROT.js} +2 -2
  108. package/dist/{chunk-LO775P3S.js → chunk-LRJKHXU4.js} +2 -2
  109. package/dist/{chunk-QISYYK4L.js → chunk-LSSCMZQ2.js} +3 -3
  110. package/dist/{chunk-PCIIKQXW.js → chunk-MBMT5FUQ.js} +2 -2
  111. package/dist/{chunk-XFDHL252.js → chunk-MGXFJOUR.js} +3 -3
  112. package/dist/{chunk-7WTTE5FW.js → chunk-MH3LC7KB.js} +2 -2
  113. package/dist/{chunk-ERMT5WQM.js → chunk-MKE5DP3S.js} +2 -2
  114. package/dist/{chunk-DJMC4W6N.js → chunk-MNDHKZZQ.js} +2 -2
  115. package/dist/{chunk-7FG2JSU6.js → chunk-NMKETOAX.js} +2 -2
  116. package/dist/{chunk-TVM47JQB.js → chunk-NQOM6PHO.js} +6 -6
  117. package/dist/{chunk-AI2VXOSO.js → chunk-NSQSOLCE.js} +2 -2
  118. package/dist/{chunk-UR25IQVT.js → chunk-P5CUEB2Q.js} +8 -8
  119. package/dist/{chunk-FXO43NBW.js → chunk-PSQ2P2T3.js} +3 -3
  120. package/dist/{chunk-KVNOHMVE.js → chunk-PTRPOJ7U.js} +2 -2
  121. package/dist/{chunk-SXRX6K35.js → chunk-QEGE5QCL.js} +47 -47
  122. package/dist/{chunk-CM6JDFCN.js → chunk-QIC7YZ3D.js} +16 -16
  123. package/dist/{chunk-KRAMCBIW.js → chunk-QKXPOLQD.js} +4 -4
  124. package/dist/{chunk-O3VKQMWC.js → chunk-R6TCM47S.js} +364 -166
  125. package/dist/chunk-R6TCM47S.js.map +1 -0
  126. package/dist/{chunk-G5UR4DXQ.js → chunk-RBC4GIZU.js} +2 -2
  127. package/dist/{chunk-GK56GNNS.js → chunk-RVYXSS3Q.js} +8 -8
  128. package/dist/{chunk-G7XAZU2L.js → chunk-RWWYBE7E.js} +3 -3
  129. package/dist/{chunk-HJM23E65.js → chunk-S53WZWGL.js} +2 -2
  130. package/dist/{chunk-MLED254J.js → chunk-T6OZW2WR.js} +8 -2
  131. package/dist/chunk-T6OZW2WR.js.map +1 -0
  132. package/dist/{chunk-3ODEGTVV.js → chunk-TDCR6PQL.js} +2 -2
  133. package/dist/{chunk-QCP75JGO.js → chunk-TFA6HPOA.js} +5 -5
  134. package/dist/{chunk-SE3JAD22.js → chunk-TPKB7T6Z.js} +2 -2
  135. package/dist/{chunk-6VYY2XFK.js → chunk-TVIGN3XZ.js} +19 -19
  136. package/dist/{chunk-ZG3HZZ6K.js → chunk-TYGLV6ZG.js} +5 -1
  137. package/dist/{chunk-ZG3HZZ6K.js.map → chunk-TYGLV6ZG.js.map} +1 -1
  138. package/dist/{chunk-K567EIW3.js → chunk-U4JXQCJ3.js} +2 -2
  139. package/dist/{chunk-WI6MIJYK.js → chunk-U7HJBODU.js} +5 -5
  140. package/dist/{chunk-IS4HKNWP.js → chunk-UBN5AOLT.js} +2 -2
  141. package/dist/{chunk-7VJEZQYF.js → chunk-UFBIOF7C.js} +3 -3
  142. package/dist/{chunk-JNELS2TW.js → chunk-UJDTXWMT.js} +2 -2
  143. package/dist/{chunk-M3GKSCFN.js → chunk-ULUX6Q7R.js} +4 -4
  144. package/dist/{chunk-ZRU7636R.js → chunk-UMFBHYPW.js} +12 -12
  145. package/dist/{chunk-GKNX4QO3.js → chunk-UQWA5QDT.js} +3 -3
  146. package/dist/{chunk-4Z7O2QVJ.js → chunk-USOXPN5H.js} +2 -2
  147. package/dist/{chunk-XA32GZ6B.js → chunk-UXXVOA4D.js} +3 -3
  148. package/dist/{chunk-OLAQWYNF.js → chunk-V5RJCXLG.js} +6 -6
  149. package/dist/{chunk-A2D3WICT.js → chunk-VGFNTY3Y.js} +2 -2
  150. package/dist/{chunk-EH6KLYIE.js → chunk-VKTP3TZB.js} +4 -4
  151. package/dist/{chunk-CSGFGQBI.js → chunk-VPO53QMG.js} +2 -2
  152. package/dist/{chunk-P2RCFQA6.js → chunk-WADPRAZE.js} +2 -2
  153. package/dist/{chunk-IVYAPAHG.js → chunk-WRDFYSUC.js} +2 -2
  154. package/dist/{chunk-FW7KWZBI.js → chunk-WSLNC5AL.js} +8 -8
  155. package/dist/{chunk-C6S2Y7HE.js → chunk-XKDQTDXN.js} +4 -4
  156. package/dist/{chunk-4AHKNZW7.js → chunk-XLEP7SS3.js} +2 -2
  157. package/dist/{chunk-B6L7BKA7.js → chunk-XRFLR3XF.js} +4 -4
  158. package/dist/{chunk-H7QC7WCU.js → chunk-XTBEXZFN.js} +4 -4
  159. package/dist/{chunk-S2S4U4G3.js → chunk-Y6Y3TRBN.js} +4 -4
  160. package/dist/{chunk-H6SKBCNI.js → chunk-YC2C67F7.js} +2 -2
  161. package/dist/{chunk-6FIFEXFF.js → chunk-YGYT7S3G.js} +7 -7
  162. package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
  163. package/dist/{chunk-YOL56HY5.js → chunk-YQ6SLFNZ.js} +3 -3
  164. package/dist/{chunk-G22A6WJQ.js → chunk-Z2T5BPOZ.js} +2 -2
  165. package/dist/{chunk-IHTKBQBS.js → chunk-Z5AQZRHF.js} +2 -2
  166. package/dist/{chunk-QQ4T622E.js → chunk-Z5QWUR3Y.js} +2 -2
  167. package/dist/cli.js +131 -131
  168. package/dist/coding/export-okf-codegraph.js +30 -30
  169. package/dist/coding/pre-action-gate.js +2 -2
  170. package/dist/compounding/engine.js +31 -31
  171. package/dist/config.js +40 -40
  172. package/dist/connectors/codex-materialize-runner.js +29 -29
  173. package/dist/connectors/index.js +29 -29
  174. package/dist/contradiction/index.js +5 -5
  175. package/dist/convergence-refresh.js +31 -31
  176. package/dist/conversation-index/backend.js +2 -2
  177. package/dist/corpus-watermark.js +31 -31
  178. package/dist/dashboard-runtime.js +2 -2
  179. package/dist/deep-recall-http-glue.js +9 -9
  180. package/dist/entity-retrieval.js +31 -31
  181. package/dist/explicit-capture.js +7 -7
  182. package/dist/external-wiki-access.js +82 -82
  183. package/dist/extraction-span-materialize.js +2 -2
  184. package/dist/extraction.js +44 -44
  185. package/dist/{graph-edge-decay-IU75NV42.js → graph-edge-decay-2BZTY2IM.js} +5 -5
  186. package/dist/graph-edge-decay-2BZTY2IM.js.map +1 -0
  187. package/dist/graph-jsonl.js +3 -3
  188. package/dist/graph-snapshot.js +3 -3
  189. package/dist/graph.js +2 -2
  190. package/dist/harmonic-retrieval.js +30 -30
  191. package/dist/harmonic-source-lookup.js +2 -2
  192. package/dist/index.js +226 -224
  193. package/dist/lcm/index.js +3 -3
  194. package/dist/local-llm-helpers.d.ts.map +1 -1
  195. package/dist/local-llm-helpers.js +2 -1
  196. package/dist/local-llm-transport.d.ts +1 -0
  197. package/dist/local-llm-transport.d.ts.map +1 -1
  198. package/dist/local-llm-transport.js +5 -3
  199. package/dist/local-llm.js +3 -3
  200. package/dist/location/index.js +32 -32
  201. package/dist/maintenance/memory-governance.js +29 -29
  202. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +29 -29
  203. package/dist/maintenance/rebuild-memory-projection-runner.js +31 -31
  204. package/dist/maintenance/rebuild-memory-projection.js +30 -30
  205. package/dist/memory-browse-operations.js +82 -82
  206. package/dist/memory-browse.js +5 -5
  207. package/dist/memory-subject.js +3 -3
  208. package/dist/namespaces/migrate.js +30 -30
  209. package/dist/namespaces/search.js +11 -11
  210. package/dist/namespaces/storage.js +29 -29
  211. package/dist/offline-sync-changeset-generations.js +3 -3
  212. package/dist/offline-sync-embedding-generation.js +2 -2
  213. package/dist/offline-sync-file-io.js +2 -2
  214. package/dist/offline-sync-impression-drain.js +3 -3
  215. package/dist/offline-sync-runtime-state.d.ts.map +1 -1
  216. package/dist/offline-sync-runtime-state.js +1 -1
  217. package/dist/offline-sync-snapshot-stream.js +8 -8
  218. package/dist/offline-sync-upload-staging.js +3 -3
  219. package/dist/offline-sync.js +8 -8
  220. package/dist/operator-doctor-corpus.js +32 -32
  221. package/dist/operator-toolkit.js +51 -51
  222. package/dist/orchestration/maintenance.js +44 -44
  223. package/dist/orchestrator.js +174 -172
  224. package/dist/qmd.js +4 -4
  225. package/dist/recall-navigation-operations.js +82 -82
  226. package/dist/recall-navigation.js +81 -81
  227. package/dist/recall-state-view-admission.js +5 -5
  228. package/dist/recall-why-service.js +5 -5
  229. package/dist/reconcile/manifest.js +3 -3
  230. package/dist/{resolution-V6JBMW7U.js → resolution-4ZEQPXUZ.js} +6 -6
  231. package/dist/resolution-4ZEQPXUZ.js.map +1 -0
  232. package/dist/resume-bundles.js +41 -41
  233. package/dist/retrieval-agents.js +2 -2
  234. package/dist/salvage-envelope.js +5 -5
  235. package/dist/search/factory.js +10 -10
  236. package/dist/search/index.js +14 -14
  237. package/dist/search/lancedb-backend.js +2 -2
  238. package/dist/search/meilisearch-backend.js +2 -2
  239. package/dist/search/orama-backend.js +2 -2
  240. package/dist/secure-store/index.js +2 -2
  241. package/dist/semantic-consolidation.js +30 -30
  242. package/dist/semantic-rule-promotion.js +29 -29
  243. package/dist/semantic-rule-verifier.js +29 -29
  244. package/dist/source-agent-qualifier.js +34 -34
  245. package/dist/standup/build.js +29 -29
  246. package/dist/storage/buffer-session-files.d.ts +69 -0
  247. package/dist/storage/buffer-session-files.d.ts.map +1 -0
  248. package/dist/storage.d.ts +5 -1
  249. package/dist/storage.d.ts.map +1 -1
  250. package/dist/storage.js +28 -28
  251. package/dist/subject-config.js +4 -4
  252. package/dist/summarizer.js +4 -4
  253. package/dist/support-passport/index.js +84 -84
  254. package/dist/transfer/export-okf.js +30 -30
  255. package/dist/verified-recall.js +29 -29
  256. package/dist/who-knows-cli.js +30 -30
  257. package/dist/who-knows.js +29 -29
  258. package/dist/write-envelope.js +4 -4
  259. package/package.json +2 -2
  260. package/src/buffer-eviction.ts +51 -0
  261. package/src/buffer-incremental-persistence.test.ts +256 -0
  262. package/src/buffer-persistence.ts +89 -0
  263. package/src/buffer.ts +32 -61
  264. package/src/local-llm-headers-timeout.test.ts +64 -0
  265. package/src/local-llm-helpers.ts +6 -1
  266. package/src/local-llm-transport.ts +14 -1
  267. package/src/offline-sync-runtime-state.ts +24 -2
  268. package/src/offline-sync.test.ts +9 -0
  269. package/src/secure-store/secure-fs.ts +11 -0
  270. package/src/secure-store/secure-store.test.ts +22 -0
  271. package/src/storage/buffer-session-files.ts +249 -0
  272. package/src/storage-buffer-sessions.test.ts +369 -0
  273. package/src/storage.ts +11 -16
  274. package/dist/chunk-JUXOTY6J.js.map +0 -1
  275. package/dist/chunk-MLED254J.js.map +0 -1
  276. package/dist/chunk-O3VKQMWC.js.map +0 -1
  277. package/dist/chunk-TYT4TEQV.js.map +0 -1
  278. package/dist/chunk-UAYA4NJN.js.map +0 -1
  279. /package/dist/{auto-sync-VPHR3FBI.js.map → auto-sync-R3XGJL5I.js.map} +0 -0
  280. /package/dist/{graph-edge-decay-IU75NV42.js.map → buffer-eviction.js.map} +0 -0
  281. /package/dist/{resolution-V6JBMW7U.js.map → buffer-persistence.js.map} +0 -0
  282. /package/dist/{chunk-HT6IEMQ2.js.map → chunk-2RJO7VS4.js.map} +0 -0
  283. /package/dist/{chunk-YLF42JQ7.js.map → chunk-3BPMYMPX.js.map} +0 -0
  284. /package/dist/{chunk-GAARPWMN.js.map → chunk-3L5WH6CV.js.map} +0 -0
  285. /package/dist/{chunk-XVZA6VXC.js.map → chunk-3SL52JKD.js.map} +0 -0
  286. /package/dist/{chunk-S2PDFSXS.js.map → chunk-4P4VLPGB.js.map} +0 -0
  287. /package/dist/{chunk-2ZYIAUYU.js.map → chunk-5CWH2LKL.js.map} +0 -0
  288. /package/dist/{chunk-G2PC6IZB.js.map → chunk-5U3NUHMA.js.map} +0 -0
  289. /package/dist/{chunk-4LOMATQA.js.map → chunk-5UWP5X3T.js.map} +0 -0
  290. /package/dist/{chunk-G3T6YMBO.js.map → chunk-64S6X4IU.js.map} +0 -0
  291. /package/dist/{chunk-NHB4RC3G.js.map → chunk-6QOL4G76.js.map} +0 -0
  292. /package/dist/{chunk-GRBQO56D.js.map → chunk-6R6CU3MR.js.map} +0 -0
  293. /package/dist/{chunk-SQ554O3K.js.map → chunk-74AY3S7U.js.map} +0 -0
  294. /package/dist/{chunk-4ISHIPOC.js.map → chunk-7PG7QBWD.js.map} +0 -0
  295. /package/dist/{chunk-A4B7WL5N.js.map → chunk-7ZBANI3M.js.map} +0 -0
  296. /package/dist/{chunk-DG5UKN2Y.js.map → chunk-A23DPFFI.js.map} +0 -0
  297. /package/dist/{chunk-77ZK533I.js.map → chunk-ARFT6ZBE.js.map} +0 -0
  298. /package/dist/{chunk-V44DWTFA.js.map → chunk-AYXRK264.js.map} +0 -0
  299. /package/dist/{chunk-GRZX3RHY.js.map → chunk-BPBUUGAO.js.map} +0 -0
  300. /package/dist/{chunk-DFP37GUN.js.map → chunk-CCYXCT4B.js.map} +0 -0
  301. /package/dist/{chunk-JJWOKRZA.js.map → chunk-CNQQNYJ2.js.map} +0 -0
  302. /package/dist/{chunk-QZP3RMYP.js.map → chunk-CU7WDDPV.js.map} +0 -0
  303. /package/dist/{chunk-GY4R4P2A.js.map → chunk-CUWOSRCS.js.map} +0 -0
  304. /package/dist/{chunk-CJKHLACF.js.map → chunk-D2AKUKAQ.js.map} +0 -0
  305. /package/dist/{chunk-BRZRP5WD.js.map → chunk-DK5VMJL3.js.map} +0 -0
  306. /package/dist/{chunk-77IXBNV6.js.map → chunk-E24RUQEN.js.map} +0 -0
  307. /package/dist/{chunk-PTQZPVRQ.js.map → chunk-E5PVMB3I.js.map} +0 -0
  308. /package/dist/{chunk-AHS3QEEA.js.map → chunk-EEOYF3IB.js.map} +0 -0
  309. /package/dist/{chunk-PH33R7GN.js.map → chunk-EHIKY2IA.js.map} +0 -0
  310. /package/dist/{chunk-NLQBTHBV.js.map → chunk-EVVCE7S2.js.map} +0 -0
  311. /package/dist/{chunk-FQ52AJXX.js.map → chunk-FBAHBFT5.js.map} +0 -0
  312. /package/dist/{chunk-D75ET36O.js.map → chunk-FL6SLVAJ.js.map} +0 -0
  313. /package/dist/{chunk-6NAN4WFY.js.map → chunk-FO5AFCRI.js.map} +0 -0
  314. /package/dist/{chunk-5LWODP5R.js.map → chunk-FRAL3EU4.js.map} +0 -0
  315. /package/dist/{chunk-AAANTXCQ.js.map → chunk-FZVZYU5C.js.map} +0 -0
  316. /package/dist/{chunk-GQVTALUJ.js.map → chunk-G2SRB62M.js.map} +0 -0
  317. /package/dist/{chunk-ORWDCX5Q.js.map → chunk-G6PPA4TH.js.map} +0 -0
  318. /package/dist/{chunk-VNAIUNXI.js.map → chunk-GCVUBVEN.js.map} +0 -0
  319. /package/dist/{chunk-TGAXRT7N.js.map → chunk-GFYFKXWZ.js.map} +0 -0
  320. /package/dist/{chunk-ZABEDRGQ.js.map → chunk-HIMXDRSQ.js.map} +0 -0
  321. /package/dist/{chunk-GDYLZ6FR.js.map → chunk-HJ3UTGIW.js.map} +0 -0
  322. /package/dist/{chunk-MGXLLL5L.js.map → chunk-HKEK27M4.js.map} +0 -0
  323. /package/dist/{chunk-X3IFRQKS.js.map → chunk-I73ZSORJ.js.map} +0 -0
  324. /package/dist/{chunk-XUB4I2N6.js.map → chunk-JFI3ATK4.js.map} +0 -0
  325. /package/dist/{chunk-R4SATHCV.js.map → chunk-JH2ALMPR.js.map} +0 -0
  326. /package/dist/{chunk-TKBW4YLS.js.map → chunk-JH4FCVWJ.js.map} +0 -0
  327. /package/dist/{chunk-X73SCHKR.js.map → chunk-KC5SVCUJ.js.map} +0 -0
  328. /package/dist/{chunk-6NXHX6MI.js.map → chunk-KOY25HUB.js.map} +0 -0
  329. /package/dist/{chunk-TRW7N4FS.js.map → chunk-L2JQSG5M.js.map} +0 -0
  330. /package/dist/{chunk-SIIWHIBK.js.map → chunk-L46Y5FPE.js.map} +0 -0
  331. /package/dist/{chunk-CIJRM6SQ.js.map → chunk-LAQS5HYU.js.map} +0 -0
  332. /package/dist/{chunk-23VGZ65V.js.map → chunk-LHBOFROT.js.map} +0 -0
  333. /package/dist/{chunk-LO775P3S.js.map → chunk-LRJKHXU4.js.map} +0 -0
  334. /package/dist/{chunk-QISYYK4L.js.map → chunk-LSSCMZQ2.js.map} +0 -0
  335. /package/dist/{chunk-PCIIKQXW.js.map → chunk-MBMT5FUQ.js.map} +0 -0
  336. /package/dist/{chunk-XFDHL252.js.map → chunk-MGXFJOUR.js.map} +0 -0
  337. /package/dist/{chunk-7WTTE5FW.js.map → chunk-MH3LC7KB.js.map} +0 -0
  338. /package/dist/{chunk-ERMT5WQM.js.map → chunk-MKE5DP3S.js.map} +0 -0
  339. /package/dist/{chunk-DJMC4W6N.js.map → chunk-MNDHKZZQ.js.map} +0 -0
  340. /package/dist/{chunk-7FG2JSU6.js.map → chunk-NMKETOAX.js.map} +0 -0
  341. /package/dist/{chunk-TVM47JQB.js.map → chunk-NQOM6PHO.js.map} +0 -0
  342. /package/dist/{chunk-AI2VXOSO.js.map → chunk-NSQSOLCE.js.map} +0 -0
  343. /package/dist/{chunk-UR25IQVT.js.map → chunk-P5CUEB2Q.js.map} +0 -0
  344. /package/dist/{chunk-FXO43NBW.js.map → chunk-PSQ2P2T3.js.map} +0 -0
  345. /package/dist/{chunk-KVNOHMVE.js.map → chunk-PTRPOJ7U.js.map} +0 -0
  346. /package/dist/{chunk-SXRX6K35.js.map → chunk-QEGE5QCL.js.map} +0 -0
  347. /package/dist/{chunk-CM6JDFCN.js.map → chunk-QIC7YZ3D.js.map} +0 -0
  348. /package/dist/{chunk-KRAMCBIW.js.map → chunk-QKXPOLQD.js.map} +0 -0
  349. /package/dist/{chunk-G5UR4DXQ.js.map → chunk-RBC4GIZU.js.map} +0 -0
  350. /package/dist/{chunk-GK56GNNS.js.map → chunk-RVYXSS3Q.js.map} +0 -0
  351. /package/dist/{chunk-G7XAZU2L.js.map → chunk-RWWYBE7E.js.map} +0 -0
  352. /package/dist/{chunk-HJM23E65.js.map → chunk-S53WZWGL.js.map} +0 -0
  353. /package/dist/{chunk-3ODEGTVV.js.map → chunk-TDCR6PQL.js.map} +0 -0
  354. /package/dist/{chunk-QCP75JGO.js.map → chunk-TFA6HPOA.js.map} +0 -0
  355. /package/dist/{chunk-SE3JAD22.js.map → chunk-TPKB7T6Z.js.map} +0 -0
  356. /package/dist/{chunk-6VYY2XFK.js.map → chunk-TVIGN3XZ.js.map} +0 -0
  357. /package/dist/{chunk-K567EIW3.js.map → chunk-U4JXQCJ3.js.map} +0 -0
  358. /package/dist/{chunk-WI6MIJYK.js.map → chunk-U7HJBODU.js.map} +0 -0
  359. /package/dist/{chunk-IS4HKNWP.js.map → chunk-UBN5AOLT.js.map} +0 -0
  360. /package/dist/{chunk-7VJEZQYF.js.map → chunk-UFBIOF7C.js.map} +0 -0
  361. /package/dist/{chunk-JNELS2TW.js.map → chunk-UJDTXWMT.js.map} +0 -0
  362. /package/dist/{chunk-M3GKSCFN.js.map → chunk-ULUX6Q7R.js.map} +0 -0
  363. /package/dist/{chunk-ZRU7636R.js.map → chunk-UMFBHYPW.js.map} +0 -0
  364. /package/dist/{chunk-GKNX4QO3.js.map → chunk-UQWA5QDT.js.map} +0 -0
  365. /package/dist/{chunk-4Z7O2QVJ.js.map → chunk-USOXPN5H.js.map} +0 -0
  366. /package/dist/{chunk-XA32GZ6B.js.map → chunk-UXXVOA4D.js.map} +0 -0
  367. /package/dist/{chunk-OLAQWYNF.js.map → chunk-V5RJCXLG.js.map} +0 -0
  368. /package/dist/{chunk-A2D3WICT.js.map → chunk-VGFNTY3Y.js.map} +0 -0
  369. /package/dist/{chunk-EH6KLYIE.js.map → chunk-VKTP3TZB.js.map} +0 -0
  370. /package/dist/{chunk-CSGFGQBI.js.map → chunk-VPO53QMG.js.map} +0 -0
  371. /package/dist/{chunk-P2RCFQA6.js.map → chunk-WADPRAZE.js.map} +0 -0
  372. /package/dist/{chunk-IVYAPAHG.js.map → chunk-WRDFYSUC.js.map} +0 -0
  373. /package/dist/{chunk-FW7KWZBI.js.map → chunk-WSLNC5AL.js.map} +0 -0
  374. /package/dist/{chunk-C6S2Y7HE.js.map → chunk-XKDQTDXN.js.map} +0 -0
  375. /package/dist/{chunk-4AHKNZW7.js.map → chunk-XLEP7SS3.js.map} +0 -0
  376. /package/dist/{chunk-B6L7BKA7.js.map → chunk-XRFLR3XF.js.map} +0 -0
  377. /package/dist/{chunk-H7QC7WCU.js.map → chunk-XTBEXZFN.js.map} +0 -0
  378. /package/dist/{chunk-S2S4U4G3.js.map → chunk-Y6Y3TRBN.js.map} +0 -0
  379. /package/dist/{chunk-H6SKBCNI.js.map → chunk-YC2C67F7.js.map} +0 -0
  380. /package/dist/{chunk-6FIFEXFF.js.map → chunk-YGYT7S3G.js.map} +0 -0
  381. /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
  382. /package/dist/{chunk-YOL56HY5.js.map → chunk-YQ6SLFNZ.js.map} +0 -0
  383. /package/dist/{chunk-G22A6WJQ.js.map → chunk-Z2T5BPOZ.js.map} +0 -0
  384. /package/dist/{chunk-IHTKBQBS.js.map → chunk-Z5AQZRHF.js.map} +0 -0
  385. /package/dist/{chunk-QQ4T622E.js.map → chunk-Z5QWUR3Y.js.map} +0 -0
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Per-session incremental buffer persistence (SmartBuffer layer).
3
+ *
4
+ * Pins the hot-path contract on top of the storage primitives tested in
5
+ * storage-buffer-sessions.test.ts:
6
+ *
7
+ * - a storage that exposes saveBufferSession/deleteBufferSession gets
8
+ * INCREMENTAL saves: only the sessions whose state actually changed are
9
+ * written, only on boundaries that need durability;
10
+ * - sessions whose state did not change are never rewritten;
11
+ * - eviction and resetToEmpty delete the corresponding session files;
12
+ * - a legacy storage (only loadBuffer/saveBuffer) keeps the exact old
13
+ * whole-state behavior — existing deployments and test doubles are
14
+ * unaffected.
15
+ *
16
+ * All saves flow through SmartBuffer's mutation chain, so there is exactly
17
+ * one writer per session file.
18
+ */
19
+ import test from "node:test";
20
+ import assert from "node:assert/strict";
21
+ import { mkdtemp, rm } from "node:fs/promises";
22
+ import os from "node:os";
23
+ import path from "node:path";
24
+
25
+ import { SmartBuffer } from "./buffer.js";
26
+ import { parseConfig } from "./config.js";
27
+ import { StorageManager } from "./storage.js";
28
+ import type { BufferEntryState, BufferState, BufferTurn } from "./types.js";
29
+
30
+ interface SessionFileRecord {
31
+ entry: BufferEntryState;
32
+ writeCount: number;
33
+ }
34
+
35
+ /**
36
+ * Storage double implementing the incremental per-session surface. Records
37
+ * per-session writes/deletes and whole-state saves separately so tests can
38
+ * assert exactly which files the hot path touched.
39
+ */
40
+ class IncrementalStorage {
41
+ readonly sessions = new Map<string, SessionFileRecord>();
42
+ readonly deleted: string[] = [];
43
+ wholeStateSaves = 0;
44
+ private initial: BufferState;
45
+
46
+ constructor(initial: BufferState) {
47
+ this.initial = initial;
48
+ }
49
+
50
+ async loadBuffer(): Promise<BufferState> {
51
+ return structuredClone(this.initial);
52
+ }
53
+
54
+ async saveBuffer(state: BufferState): Promise<void> {
55
+ this.wholeStateSaves += 1;
56
+ this.initial = structuredClone(state);
57
+ }
58
+
59
+ async saveBufferSession(key: string, entry: BufferEntryState): Promise<void> {
60
+ const record = this.sessions.get(key) ?? { entry, writeCount: 0 };
61
+ record.entry = structuredClone(entry);
62
+ record.writeCount += 1;
63
+ this.sessions.set(key, record);
64
+ }
65
+
66
+ async deleteBufferSession(key: string): Promise<void> {
67
+ this.deleted.push(key);
68
+ this.sessions.delete(key);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Storage double implementing ONLY the legacy whole-state surface — every
74
+ * pre-existing test double in the repo looks like this from SmartBuffer's
75
+ * point of view, and must keep working unchanged.
76
+ */
77
+ class LegacyOnlyStorage {
78
+ wholeStateSaves = 0;
79
+ lastSaved: BufferState | null = null;
80
+
81
+ constructor(private initial: BufferState) {}
82
+
83
+ async loadBuffer(): Promise<BufferState> {
84
+ return structuredClone(this.initial);
85
+ }
86
+
87
+ async saveBuffer(state: BufferState): Promise<void> {
88
+ this.wholeStateSaves += 1;
89
+ this.lastSaved = structuredClone(state);
90
+ }
91
+ }
92
+
93
+ function makeTurn(sessionKey: string, content: string): BufferTurn {
94
+ return {
95
+ role: "user",
96
+ content,
97
+ timestamp: "2026-10-29T12:00:00.000Z",
98
+ sessionKey,
99
+ };
100
+ }
101
+
102
+ function emptyState(): BufferState {
103
+ return { turns: [], lastExtractionAt: null, extractionCount: 0 };
104
+ }
105
+
106
+ test("steady-state observe persists only the active session's file", async () => {
107
+ const storage = new IncrementalStorage(emptyState());
108
+ const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
109
+
110
+ await buffer.addTurn("thread-a", makeTurn("thread-a", "alpha"));
111
+ await buffer.flushPendingSave();
112
+ assert.equal(storage.wholeStateSaves, 0, "hot path fell back to whole-state save");
113
+ assert.equal(storage.sessions.get("thread-a")?.writeCount, 1);
114
+ assert.equal(storage.sessions.get("thread-a")?.entry.turns[0]?.content, "alpha");
115
+
116
+ await buffer.addTurn("thread-b", makeTurn("thread-b", "beta"));
117
+ await buffer.flushPendingSave();
118
+ assert.equal(storage.sessions.get("thread-b")?.writeCount, 1);
119
+ // Session A's turn did NOT touch session A's file again.
120
+ assert.equal(storage.sessions.get("thread-a")?.writeCount, 1, "unchanged session file was rewritten");
121
+ });
122
+
123
+ test("a second turn in the same session rewrites only that session's file", async () => {
124
+ const storage = new IncrementalStorage(emptyState());
125
+ const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
126
+
127
+ await buffer.addTurn("thread-a", makeTurn("thread-a", "one"));
128
+ await buffer.flushPendingSave();
129
+ await buffer.addTurn("thread-a", makeTurn("thread-a", "two"));
130
+ await buffer.flushPendingSave();
131
+
132
+ const record = storage.sessions.get("thread-a");
133
+ assert.equal(record?.writeCount, 2);
134
+ assert.equal(record?.entry.turns.length, 2);
135
+ });
136
+
137
+ test("extraction-boundary turn persists inline and durably", async () => {
138
+ const storage = new IncrementalStorage(emptyState());
139
+ // bufferMaxTurns: 1 → the very first turn triggers extract_batch.
140
+ const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 1 }), storage);
141
+
142
+ const outcome = await buffer.addTurnWithOutcome("thread-a", makeTurn("thread-a", "trigger"));
143
+ assert.equal(outcome.decision, "extract_batch");
144
+ // Inline save already ran inside the mutation — no flush needed.
145
+ assert.equal(storage.sessions.get("thread-a")?.writeCount, 1);
146
+ assert.equal(storage.wholeStateSaves, 0, "extraction boundary fell back to whole-state save");
147
+ });
148
+
149
+ test("clearAfterExtraction persists the cleared session state", async () => {
150
+ const storage = new IncrementalStorage(emptyState());
151
+ const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
152
+
153
+ await buffer.addTurn("thread-a", makeTurn("thread-a", "alpha"));
154
+ await buffer.flushPendingSave();
155
+ const before = storage.sessions.get("thread-a")?.writeCount ?? 0;
156
+
157
+ await buffer.clearAfterExtraction("thread-a");
158
+ const record = storage.sessions.get("thread-a");
159
+ assert.equal(record?.writeCount, before + 1, "clearAfterExtraction did not persist the session");
160
+ assert.equal(record?.entry.turns.length, 0);
161
+ assert.equal(record?.entry.extractionCount, 1);
162
+ });
163
+
164
+ test("evicted sessions get their files deleted", async () => {
165
+ const storage = new IncrementalStorage(emptyState());
166
+ const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
167
+
168
+ // normalizeState always materializes a "default" entry, so 198 seeded
169
+ // sessions + default = 199; new-session reaches exactly 200 (the
170
+ // addTurnBounded cap). One more UNBOUNDED addTurn pushes to 201 and
171
+ // pruneEntries evicts the oldest non-default session (s0).
172
+ for (let i = 0; i < 198; i++) {
173
+ const key = `s${i}`;
174
+ await buffer.addTurnBounded(key, makeTurn(key, `content ${i}`));
175
+ }
176
+ await buffer.flushPendingSave();
177
+
178
+ const outcome = await buffer.addTurnBounded("new-session", makeTurn("new-session", "fresh"));
179
+ assert.notEqual(outcome, null, "addTurnBounded refused below capacity");
180
+ await buffer.addTurn("overflow", makeTurn("overflow", "one too many"));
181
+ // The evicted key's file deletion lands on the next persist, like any
182
+ // other buffered state change.
183
+ await buffer.flushPendingSave();
184
+ assert.ok(storage.deleted.includes("s0"), "oldest session was not evicted under capacity pressure");
185
+ assert.ok(
186
+ storage.deleted.length > 0,
187
+ "capacity pressure evicted entries without deleting their session files",
188
+ );
189
+ for (const key of storage.deleted) {
190
+ assert.equal(storage.sessions.has(key), false, `evicted session ${key} still has a file record`);
191
+ }
192
+ });
193
+
194
+ test("resetToEmpty drops every session file on the next persist", async () => {
195
+ const storage = new IncrementalStorage(emptyState());
196
+ const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
197
+
198
+ await buffer.addTurn("thread-a", makeTurn("thread-a", "alpha"));
199
+ await buffer.flushPendingSave();
200
+ buffer.resetToEmpty();
201
+ await buffer.flushPendingSave();
202
+ // resetToEmpty marks every known key for deletion; deleting a key whose
203
+ // file never existed is an ENOENT no-op on real storage (covered in the
204
+ // storage contract tests). The session that HAD a file must be gone.
205
+ assert.ok(storage.deleted.includes("thread-a"));
206
+ assert.equal(storage.sessions.size, 0);
207
+ });
208
+
209
+ test("retained turns survive the per-session persist", async () => {
210
+ const storage = new IncrementalStorage(emptyState());
211
+ const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
212
+
213
+ await buffer.addTurn("thread-a", makeTurn("thread-a", "alpha"));
214
+ await buffer.flushPendingSave();
215
+ await buffer.retainDeferredTurns("thread-a", [makeTurn("thread-a", "alpha")], 5);
216
+ await buffer.flushPendingSave();
217
+
218
+ const record = storage.sessions.get("thread-a");
219
+ assert.equal(record?.entry.retainedTurns?.length, 1, "retained turns were not persisted");
220
+ assert.equal(record?.entry.retainedTurns?.[0]?.content, "alpha");
221
+ });
222
+
223
+ test("buffer state survives a restart through per-session files", async () => {
224
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-bufio-restart-"));
225
+ try {
226
+ const storage = new StorageManager(root);
227
+ const config = parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 });
228
+ const first = new SmartBuffer(config, storage);
229
+ await first.addTurn("thread-a", makeTurn("thread-a", "alpha"));
230
+ await first.addTurn("thread-b", makeTurn("thread-b", "beta"));
231
+ await first.flushPendingSave();
232
+
233
+ // A fresh process reads the buffer back from the per-session files.
234
+ const second = new SmartBuffer(config, storage);
235
+ await second.load();
236
+ assert.equal(second.getTurns("thread-a").length, 1);
237
+ assert.equal(second.getTurns("thread-a")[0]?.content, "alpha");
238
+ assert.equal(second.getTurns("thread-b")[0]?.content, "beta");
239
+ } finally {
240
+ await rm(root, { recursive: true, force: true });
241
+ }
242
+ });
243
+
244
+ test("legacy-only storage keeps the whole-state saveBuffer behavior", async () => {
245
+ const storage = new LegacyOnlyStorage(emptyState());
246
+ const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
247
+
248
+ await buffer.addTurn("thread-a", makeTurn("thread-a", "alpha"));
249
+ await buffer.flushPendingSave();
250
+ await buffer.addTurn("thread-b", makeTurn("thread-b", "beta"));
251
+ await buffer.flushPendingSave();
252
+
253
+ assert.ok(storage.wholeStateSaves >= 2, "legacy storage did not receive whole-state saves");
254
+ assert.equal(storage.lastSaved?.entries?.["thread-a"]?.turns[0]?.content, "alpha");
255
+ assert.equal(storage.lastSaved?.entries?.["thread-b"]?.turns[0]?.content, "beta");
256
+ });
@@ -0,0 +1,89 @@
1
+ import type { BufferEntryState, BufferState, BufferSurpriseEvent } from "./types.js";
2
+
3
+ /**
4
+ * The buffer persistence surface SmartBuffer depends on.
5
+ *
6
+ * `saveBufferSession` / `deleteBufferSession` are the per-session incremental
7
+ * writes (one file per session); when a storage backend does not provide
8
+ * them, the buffer falls back to the whole-state `saveBuffer`. Both are
9
+ * optional in the type so a storage double only has to implement what it
10
+ * actually supports.
11
+ */
12
+ export interface BufferStorage {
13
+ loadBuffer(): Promise<BufferState>;
14
+ saveBuffer(state: BufferState): Promise<void>;
15
+ saveBufferSession?(key: string, entry: BufferEntryState): Promise<void>;
16
+ deleteBufferSession?(key: string): Promise<void>;
17
+ /** Optional surprise-telemetry sink; older storage and test doubles omit it. */
18
+ appendBufferSurpriseEvents?(events: BufferSurpriseEvent[]): Promise<number>;
19
+ }
20
+
21
+ /**
22
+ * Tracks which sessions' persisted files are stale and writes only those.
23
+ *
24
+ * Marks are cleared only after a durable write of that session. All calls
25
+ * happen under SmartBuffer's serialized mutation chain: one writer.
26
+ */
27
+ export class BufferSessionPersistence {
28
+ private readonly dirty = new Set<string>();
29
+ private readonly deleted = new Set<string>();
30
+
31
+ markDirty(key: string): void {
32
+ this.dirty.add(key);
33
+ this.deleted.delete(key);
34
+ }
35
+
36
+ markDeleted(key: string): void {
37
+ this.deleted.add(key);
38
+ this.dirty.delete(key);
39
+ }
40
+
41
+ /** Eviction removed `key` from memory: its session file must go, any pending write is moot. */
42
+ markEvicted(keys: readonly string[]): void {
43
+ for (const key of keys) this.markDeleted(key);
44
+ }
45
+
46
+ hasPending(): boolean {
47
+ return this.dirty.size > 0 || this.deleted.size > 0;
48
+ }
49
+
50
+ /**
51
+ * Persist buffer state. On session-capable storage this writes ONLY the
52
+ * sessions whose state changed since the last successful persist and
53
+ * deletes files for evicted/reset sessions, so the observe path never
54
+ * re-serializes other sessions' turns. On legacy storage it falls back to
55
+ * the whole-state saveBuffer. Files are deleted before dirty writes: a
56
+ * crash mid-persist can then only resurrect already-evicted data (the same
57
+ * exposure as a whole-file rewrite), never drop sessions the state says
58
+ * exist.
59
+ */
60
+ async persist(
61
+ storage: BufferStorage,
62
+ state: BufferState,
63
+ peekEntry: (key: string) => BufferEntryState | undefined,
64
+ ): Promise<void> {
65
+ const saveSession = storage.saveBufferSession?.bind(storage);
66
+ const deleteSession = storage.deleteBufferSession?.bind(storage);
67
+ if (!saveSession || !deleteSession) {
68
+ await storage.saveBuffer(state);
69
+ this.dirty.clear();
70
+ this.deleted.clear();
71
+ return;
72
+ }
73
+ for (const key of [...this.deleted]) {
74
+ await deleteSession(key);
75
+ this.deleted.delete(key);
76
+ }
77
+ for (const key of [...this.dirty]) {
78
+ const entry = peekEntry(key);
79
+ if (!entry) {
80
+ // A dirty key without state must not be written from a fabricated
81
+ // empty entry: treat it as a deletion instead.
82
+ this.markDeleted(key);
83
+ continue;
84
+ }
85
+ await saveSession(key, entry);
86
+ this.dirty.delete(key);
87
+ }
88
+ }
89
+ }
package/src/buffer.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { log } from "./logger.js";
2
2
  import { throwIfAborted } from "./abort-error.js";
3
3
  import { scanSignals } from "./signal.js";
4
- import type { StorageManager } from "./storage.js";
4
+ import { BufferSessionPersistence, type BufferStorage } from "./buffer-persistence.js";
5
+ import { selectEvictedBufferKeys } from "./buffer-eviction.js";
5
6
  import type {
6
7
  BufferEntryState,
7
8
  BufferState,
@@ -175,9 +176,12 @@ export class SmartBuffer {
175
176
  * activity (see BUFFER_SAVE_MAX_DEFER_MULTIPLIER). */
176
177
  private firstPendingAtMs: number | null = null;
177
178
 
179
+ /** Which sessions' persisted files are stale (per-session incremental saves). */
180
+ private readonly persistence = new BufferSessionPersistence();
181
+
178
182
  constructor(
179
183
  private readonly config: PluginConfig,
180
- private readonly storage: StorageManager,
184
+ private readonly storage: BufferStorage,
181
185
  surpriseProbe: BufferSurpriseProbe | null = null,
182
186
  ) {
183
187
  this.state = { turns: [], lastExtractionAt: null, extractionCount: 0 };
@@ -261,60 +265,20 @@ export class SmartBuffer {
261
265
  );
262
266
  }
263
267
 
268
+ /** Evict oldest entries past MAX_BUFFER_ENTRY_COUNT and queue their session files for deletion. */
264
269
  private pruneEntries(retainKeys: string[]): void {
265
270
  const entries = this.state.entries;
266
271
  if (!entries) return;
267
- const keys = Object.keys(entries);
268
- if (keys.length <= MAX_BUFFER_ENTRY_COUNT) return;
269
-
270
- const insertionOrder = new Map(keys.map((key, index) => [key, index]));
271
- const byOldestActivity = (left: string, right: string): number => {
272
- const leftAt = this.entryActivityAt(entries[left] ?? {
273
- turns: [],
274
- lastExtractionAt: null,
275
- extractionCount: 0,
276
- });
277
- const rightAt = this.entryActivityAt(entries[right] ?? {
278
- turns: [],
279
- lastExtractionAt: null,
280
- extractionCount: 0,
281
- });
282
- if (leftAt !== rightAt) return leftAt - rightAt;
283
- return (insertionOrder.get(left) ?? 0) - (insertionOrder.get(right) ?? 0);
284
- };
285
- const candidates = keys.filter(
286
- (key) =>
287
- key !== "default" &&
288
- !retainKeys.includes(key) &&
289
- // #3251: accepted captures exit via refusal/backpressure, never eviction.
290
- !this.entryHoldsAcceptedCapture(entries[key]),
291
- );
292
- const emptyRemovable = candidates
293
- .filter((key) => (entries[key]?.turns.length ?? 0) === 0)
294
- .sort(byOldestActivity);
295
-
296
- const removableCount = Math.max(0, keys.length - MAX_BUFFER_ENTRY_COUNT);
297
- let evicted = emptyRemovable.slice(0, removableCount);
298
- if (evicted.length < removableCount) {
299
- // Retained failed extractions (#1908 retry retention) keep their turns in
300
- // the buffer, so empty entries alone may not bring the map back under the
301
- // cap during a provider outage. The cap is the documented bound — evict
302
- // the oldest NON-empty sessions too rather than growing unboundedly
303
- // (codex review).
304
- const nonEmptyRemovable = candidates
305
- .filter((key) => (entries[key]?.turns.length ?? 0) > 0)
306
- .sort(byOldestActivity);
307
- evicted = evicted.concat(nonEmptyRemovable.slice(0, removableCount - evicted.length));
308
- }
309
- for (const key of evicted) {
310
- delete entries[key];
311
- }
272
+ const evicted = selectEvictedBufferKeys(entries, retainKeys, MAX_BUFFER_ENTRY_COUNT, {
273
+ activityAt: (entry) => this.entryActivityAt(entry),
274
+ holdsAcceptedCapture: (entry) => this.entryHoldsAcceptedCapture(entry),
275
+ });
276
+ for (const key of evicted) delete entries[key];
277
+ this.persistence.markEvicted(evicted);
312
278
  if (evicted.length > 0) {
313
- // Loud degradation (extraction hot-loop hardening, Step 7): during a
314
- // prolonged provider outage the breaker holds extraction off and buffered
315
- // sessions accumulate. Eviction of the oldest empty session entries past
316
- // MAX_BUFFER_ENTRY_COUNT is bounded and expected, but must be observable —
317
- // not silent — so operators can see buffer pressure.
279
+ // Loud degradation (extraction hot-loop hardening, Step 7): buffer
280
+ // pressure during a provider outage is bounded and expected, but must
281
+ // be observable, not silent.
318
282
  log.warn(
319
283
  `buffer: pruned ${evicted.length} oldest session entr${evicted.length === 1 ? "y" : "ies"} ` +
320
284
  `past MAX_BUFFER_ENTRY_COUNT=${MAX_BUFFER_ENTRY_COUNT}; buffered turn data for those sessions was dropped`,
@@ -376,12 +340,15 @@ export class SmartBuffer {
376
340
  * so the buffer can still accept new turns for the rest of the session.
377
341
  */
378
342
  resetToEmpty(): void {
343
+ for (const key of Object.keys(this.state.entries ?? {})) {
344
+ this.persistence.markDeleted(key);
345
+ }
379
346
  this.state = { turns: [], lastExtractionAt: null, extractionCount: 0 };
380
347
  this.loaded = true;
381
348
  }
382
349
 
383
350
  private async saveUnlocked(): Promise<void> {
384
- await this.storage.saveBuffer(this.state);
351
+ await this.persistence.persist(this.storage, this.state, (key) => this.peekEntry(key) ?? undefined);
385
352
  }
386
353
 
387
354
  async save(): Promise<void> {
@@ -495,13 +462,16 @@ export class SmartBuffer {
495
462
  clearTimeout(this.saveTimer);
496
463
  this.saveTimer = null;
497
464
  }
498
- if (!this.pendingSave) return;
465
+ // resetToEmpty-only deletions are pending work too, even with no
466
+ // pending-save flag set.
467
+ if (!this.pendingSave && !this.persistence.hasPending()) return;
499
468
  try {
500
469
  await this.enqueueMutation(async () => {
501
470
  // Re-check under the serializer: a concurrent flush may have already
502
471
  // persisted this pending save. Skip the redundant write (no phantom
503
- // writes).
504
- if (!this.pendingSave) return;
472
+ // writes). resetToEmpty-only deletions also land here without a
473
+ // pending-save flag.
474
+ if (!this.pendingSave && !this.persistence.hasPending()) return;
505
475
  await this.saveUnlocked();
506
476
  // Clear the pending flag INSIDE the same serialized mutation as the
507
477
  // write (issue #1909 review). Clearing it after the enqueue await
@@ -579,6 +549,7 @@ export class SmartBuffer {
579
549
  for (const key of emptyRemovable) {
580
550
  if (Object.keys(entries).length < MAX_BUFFER_ENTRY_COUNT) break;
581
551
  delete entries[key];
552
+ this.persistence.markDeleted(key);
582
553
  }
583
554
  }
584
555
 
@@ -714,6 +685,7 @@ export class SmartBuffer {
714
685
  const decision = this.evaluate(entry, signal.level);
715
686
  const turnCountInWindow = entry.turns.length;
716
687
 
688
+ this.persistence.markDirty(bufferKey);
717
689
  this.pruneEntries([bufferKey]);
718
690
  if (decision === "keep_buffering" && this.config.bufferSaveDebounceMs > 0) {
719
691
  // Steady-state buffering: coalesce the whole-state serialize onto a
@@ -792,11 +764,7 @@ export class SmartBuffer {
792
764
  private async emitSurpriseEventSafe(
793
765
  params: SurpriseTelemetryQueueEntry,
794
766
  ): Promise<void> {
795
- const storage = this.storage as StorageManager & {
796
- appendBufferSurpriseEvents?: (
797
- events: BufferSurpriseEvent[],
798
- ) => Promise<number>;
799
- };
767
+ const storage = this.storage;
800
768
  if (typeof storage.appendBufferSurpriseEvents !== "function") {
801
769
  // Older StorageManager / test double without the telemetry sink.
802
770
  // Silently skip — core path is still covered by the log line above.
@@ -955,6 +923,7 @@ export class SmartBuffer {
955
923
  );
956
924
  if (retained.length > 0) entry.retainedTurns = retained;
957
925
  else delete entry.retainedTurns;
926
+ this.persistence.markDirty(bufferKey);
958
927
  await this.saveUnlocked();
959
928
  });
960
929
  }
@@ -1062,6 +1031,7 @@ export class SmartBuffer {
1062
1031
  if (remainingTurns.length > 0) entry.retainedTurns = remainingTurns;
1063
1032
  else delete entry.retainedTurns;
1064
1033
  if (bufferKey === "default") this.state.turns = entry.turns;
1034
+ this.persistence.markDirty(bufferKey);
1065
1035
  }
1066
1036
  if (changed || hadPendingSave) {
1067
1037
  assertLifecycle();
@@ -1172,6 +1142,7 @@ export class SmartBuffer {
1172
1142
  this.state.lastExtractionAt = entry.lastExtractionAt;
1173
1143
  this.state.extractionCount = entry.extractionCount;
1174
1144
  }
1145
+ this.persistence.markDirty(bufferKey);
1175
1146
  this.pruneEntries([bufferKey]);
1176
1147
  await this.saveNowRetainingPendingOnFailure("clearAfterExtraction");
1177
1148
  });
@@ -12,6 +12,7 @@ import {
12
12
  } from "undici";
13
13
 
14
14
  import { LocalLlmClient } from "./local-llm.js";
15
+ import { probeFetch } from "./local-llm-helpers.js";
15
16
  import { classifyAnalysisProviderError } from "./activity/timeline/analysis-provider.js";
16
17
  import { ChatTransport } from "./local-llm-transport.js";
17
18
  import { initLogger, resetLogger } from "./logger.js";
@@ -466,6 +467,69 @@ test("a process-wide custom dispatcher is left in place", async () => {
466
467
  await restored.close();
467
468
  });
468
469
 
470
+ test("an explicit opt-in widens the pool even when a custom global dispatcher is installed", async () => {
471
+ // Some hosts install a process-wide dispatcher (for example an agent runtime's
472
+ // managed proxy) that is unrelated to the LAN model server. There the 300s
473
+ // header cap stood silently and every slow completion died with `fetch failed`
474
+ // at 301s. REMNIC_LOCAL_LLM_DIRECT=1 declares that the local endpoint is
475
+ // reached directly, so the transport uses its own pool. Without the flag the
476
+ // previous behavior (leave the custom dispatcher alone) is unchanged.
477
+ const original = getGlobalDispatcher();
478
+ const proxy = new ProxyAgent("http://127.0.0.1:9");
479
+ const priorFlag = process.env.REMNIC_LOCAL_LLM_DIRECT;
480
+ try {
481
+ setGlobalDispatcher(proxy);
482
+ delete process.env.REMNIC_LOCAL_LLM_DIRECT;
483
+ assert.equal(new ChatTransport().dispatcherFor(900_000), undefined, "no flag: custom dispatcher stays");
484
+
485
+ process.env.REMNIC_LOCAL_LLM_DIRECT = "1";
486
+ const transport = new ChatTransport();
487
+ const agent = transport.dispatcherFor(900_000);
488
+ assert.ok(agent instanceof Agent, "flag set: the widened pool must be used");
489
+ assert.ok(!(agent instanceof ProxyAgent), "flag set: the proxy dispatcher must not be used");
490
+ await agent.close();
491
+
492
+ process.env.REMNIC_LOCAL_LLM_DIRECT = "0";
493
+ assert.equal(new ChatTransport().dispatcherFor(900_000), undefined, "flag 0: custom dispatcher stays");
494
+ } finally {
495
+ if (priorFlag === undefined) delete process.env.REMNIC_LOCAL_LLM_DIRECT;
496
+ else process.env.REMNIC_LOCAL_LLM_DIRECT = priorFlag;
497
+ setGlobalDispatcher(original);
498
+ await proxy.close();
499
+ }
500
+ });
501
+
502
+ test("REMNIC_LOCAL_LLM_DIRECT=1 also sends availability probes around a global proxy dispatcher", async () => {
503
+ // A host whose global dispatcher cannot reach the LAN model server would fail
504
+ // checkAvailability() before any chat request runs, so direct mode must cover
505
+ // the probe as well as ChatTransport.post.
506
+ const server = http.createServer((_req, res) => {
507
+ res.setHeader("content-type", "application/json");
508
+ res.end('{"data":[]}');
509
+ });
510
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
511
+ // SAFETY: listen(0, "127.0.0.1") above binds a TCP port, so address() is an AddressInfo, never a pipe string or null.
512
+ const { port } = server.address() as AddressInfo;
513
+ const url = `http://127.0.0.1:${port}/v1/models`;
514
+ const original = getGlobalDispatcher();
515
+ const priorFlag = process.env.REMNIC_LOCAL_LLM_DIRECT;
516
+ try {
517
+ setGlobalDispatcher(new ProxyAgent("http://127.0.0.1:9"));
518
+ delete process.env.REMNIC_LOCAL_LLM_DIRECT;
519
+ const viaProxy = await probeFetch(url, { timeoutMs: 2_000, headers: {} });
520
+ assert.equal(viaProxy.ok, false, "no flag: the probe goes through the (unreachable) proxy");
521
+
522
+ process.env.REMNIC_LOCAL_LLM_DIRECT = "1";
523
+ const direct = await probeFetch(url, { timeoutMs: 2_000, headers: {} });
524
+ assert.equal(direct.ok, true, "flag set: the probe reaches the endpoint directly");
525
+ } finally {
526
+ if (priorFlag === undefined) delete process.env.REMNIC_LOCAL_LLM_DIRECT;
527
+ else process.env.REMNIC_LOCAL_LLM_DIRECT = priorFlag;
528
+ setGlobalDispatcher(original);
529
+ await new Promise<void>((resolve) => server.close(() => resolve()));
530
+ }
531
+ });
532
+
469
533
  test("aborting during a retry backoff stops the local request lane", async () => {
470
534
  const original = globalThis.fetch;
471
535
  const abortController = new AbortController();
@@ -1,5 +1,7 @@
1
1
  import type { ResponseFormatJSONSchema } from "openai/resources/shared";
2
+ import type { Agent } from "undici";
2
3
  import { log } from "./logger.js";
4
+ import { directProbeDispatcher } from "./local-llm-transport.js";
3
5
  import type { LocalLlmRequestPriority } from "./local-llm.js";
4
6
 
5
7
  export interface LocalLlmChatCompletionOptions {
@@ -167,7 +169,10 @@ export async function probeFetch(
167
169
  : controller.signal;
168
170
  const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
169
171
  try {
170
- const response = await fetch(url, { signal: requestSignal, headers: options.headers });
172
+ const init: RequestInit & { dispatcher?: Agent } = { signal: requestSignal, headers: options.headers };
173
+ const dispatcher = directProbeDispatcher();
174
+ if (dispatcher) init.dispatcher = dispatcher;
175
+ const response = await fetch(url, init);
171
176
  clearTimeout(timeout);
172
177
  if (!response.ok) {
173
178
  log.debug(`local LLM probe: ${url} returned HTTP ${response.status}`);
@@ -40,6 +40,18 @@ function isPlainPoolingAgent(dispatcher: unknown): boolean {
40
40
  return ctor.name === "Agent";
41
41
  }
42
42
 
43
+ /**
44
+ * REMNIC_LOCAL_LLM_DIRECT=1 declares that the local endpoint is reached
45
+ * directly. Availability probes use this shared plain pool in that mode so they
46
+ * take the same route as chat requests; `undefined` keeps the global dispatcher.
47
+ */
48
+ let directProbeAgent: Agent | null = null;
49
+ export function directProbeDispatcher(): Agent | undefined {
50
+ if (process.env.REMNIC_LOCAL_LLM_DIRECT !== "1") return undefined;
51
+ directProbeAgent ??= new Agent();
52
+ return directProbeAgent;
53
+ }
54
+
43
55
  /**
44
56
  * Connection pool whose header/body inactivity budgets track the configured
45
57
  * request budget.
@@ -72,7 +84,8 @@ export class ChatTransport {
72
84
  * because only the dispatcher's owner knows how to rebuild it.
73
85
  */
74
86
  dispatcherFor(budgetMs: number): Agent | undefined {
75
- if (!isPlainPoolingAgent(getGlobalDispatcher())) {
87
+ // REMNIC_LOCAL_LLM_DIRECT=1: the operator declares the local endpoint is reached directly even though the process installed a global dispatcher for other traffic (an agent runtime's managed proxy). Without it the 300s header cap stands silently on such hosts and every completion slower than five minutes dies with `fetch failed`. Off by default so deployments that must use a proxy keep it.
88
+ if (!isPlainPoolingAgent(getGlobalDispatcher()) && process.env.REMNIC_LOCAL_LLM_DIRECT !== "1") {
76
89
  if (!this.warnedAboutCustomDispatcher) {
77
90
  this.warnedAboutCustomDispatcher = true;
78
91
  log.debug(