@remnic/core 10.0.60 → 10.0.62

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 (289) hide show
  1. package/dist/access-admin-ops-surface.js +25 -24
  2. package/dist/access-authorization-probe.js +26 -25
  3. package/dist/access-boundary.js +25 -24
  4. package/dist/access-cli.js +73 -72
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-extraction-force-flush.js +25 -24
  7. package/dist/access-http-briefing.js +26 -25
  8. package/dist/access-http-query.js +27 -26
  9. package/dist/access-http.js +44 -43
  10. package/dist/access-identity-continuity-surface.js +25 -24
  11. package/dist/access-lcm-surface.js +25 -24
  12. package/dist/access-mcp.js +36 -35
  13. package/dist/access-namespace-preflight.js +25 -24
  14. package/dist/access-observe-write-category.js +4 -4
  15. package/dist/access-observe-write-surface.js +25 -24
  16. package/dist/access-operations-batch.js +27 -26
  17. package/dist/access-operations.js +34 -33
  18. package/dist/access-recall-concurrency.js +25 -24
  19. package/dist/access-recall-diagnostics.js +2 -2
  20. package/dist/access-recall-response.js +25 -24
  21. package/dist/access-recall-surface.js +25 -24
  22. package/dist/access-schema.js +3 -3
  23. package/dist/access-service.js +25 -24
  24. package/dist/access-surface-catalog.js +27 -26
  25. package/dist/access-surface-catalog.js.map +1 -1
  26. package/dist/active-recall.js +8 -6
  27. package/dist/active-recall.js.map +1 -1
  28. package/dist/briefing.js +8 -6
  29. package/dist/buffer-persistence.d.ts +14 -3
  30. package/dist/buffer-persistence.d.ts.map +1 -1
  31. package/dist/buffer-persistence.js +1 -1
  32. package/dist/buffer-turn-helpers.d.ts +7 -0
  33. package/dist/buffer-turn-helpers.d.ts.map +1 -1
  34. package/dist/buffer-turn-helpers.js +5 -3
  35. package/dist/buffer.d.ts +5 -1
  36. package/dist/buffer.d.ts.map +1 -1
  37. package/dist/buffer.js +3 -3
  38. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-GWVG7LGC.js} +2 -2
  39. package/dist/causal-consolidation.js +9 -7
  40. package/dist/causal-consolidation.js.map +1 -1
  41. package/dist/{chunk-VHBGJAYC.js → chunk-25QK3LR2.js} +2 -2
  42. package/dist/{chunk-X2TZWVCC.js → chunk-3HLWLYRD.js} +2 -2
  43. package/dist/{chunk-TIS6T6UP.js → chunk-3VNRDRBM.js} +2 -2
  44. package/dist/{chunk-4O4VAEWH.js → chunk-4NLHEE2Z.js} +2 -2
  45. package/dist/{chunk-3DG2YAAX.js → chunk-6EZPYZT3.js} +62 -35
  46. package/dist/chunk-6EZPYZT3.js.map +1 -0
  47. package/dist/{chunk-I6UTOUBK.js → chunk-6VBKU3FY.js} +150 -345
  48. package/dist/chunk-6VBKU3FY.js.map +1 -0
  49. package/dist/{chunk-66CC6L6R.js → chunk-76LKZ42K.js} +2 -2
  50. package/dist/{chunk-LR7RW4T6.js → chunk-7ETWIBTO.js} +3 -3
  51. package/dist/{chunk-JM2WJBP7.js → chunk-7JFXXA57.js} +2 -2
  52. package/dist/{chunk-PIVSUPF2.js → chunk-A6JV3WHI.js} +2 -2
  53. package/dist/{chunk-MOB4JHG6.js → chunk-BDNPW3KM.js} +2 -2
  54. package/dist/{chunk-IJFX4JRI.js → chunk-BQOOUKEN.js} +7 -7
  55. package/dist/{chunk-PRSF7DWT.js → chunk-BU4W2ISH.js} +2 -2
  56. package/dist/{chunk-KQAFEZQX.js → chunk-BUKK5SWA.js} +2 -2
  57. package/dist/{chunk-JRY55I6Z.js → chunk-C2AYUXEF.js} +2 -2
  58. package/dist/{chunk-DF4SYU4Z.js → chunk-C5OW2FFB.js} +2 -2
  59. package/dist/{chunk-74Z2BYEK.js → chunk-CYEEVWQ6.js} +2 -2
  60. package/dist/{chunk-M7KAJBO2.js → chunk-EADUOZWH.js} +3 -3
  61. package/dist/{chunk-N5H22V75.js → chunk-ECT5ME6X.js} +2 -2
  62. package/dist/{chunk-M3GOWG2P.js → chunk-FMOUUWQN.js} +2 -2
  63. package/dist/{chunk-2EJAPJ5G.js → chunk-FYPKSR6Z.js} +2 -2
  64. package/dist/{chunk-L5RMURVI.js → chunk-GDCKOER4.js} +41 -13
  65. package/dist/chunk-GDCKOER4.js.map +1 -0
  66. package/dist/{chunk-LGLLCWA2.js → chunk-GNEB3WXR.js} +2 -2
  67. package/dist/{chunk-Z4EH5TSO.js → chunk-HNZH57JK.js} +2 -2
  68. package/dist/{chunk-PTJWUN4H.js → chunk-IJKBIOWL.js} +30 -30
  69. package/dist/{chunk-C5GTIRIO.js → chunk-IMNJJKHW.js} +22 -22
  70. package/dist/{chunk-ADK5TMQL.js → chunk-IVJI5FDA.js} +2 -2
  71. package/dist/{chunk-F3GX76LV.js → chunk-IWAB7JVH.js} +2 -2
  72. package/dist/{chunk-JFRO5HJY.js → chunk-J62O2UW5.js} +2 -2
  73. package/dist/chunk-JCL7RO2G.js +674 -0
  74. package/dist/chunk-JCL7RO2G.js.map +1 -0
  75. package/dist/{chunk-A3UFV4K3.js → chunk-JIYJIH5N.js} +2 -2
  76. package/dist/{chunk-UIX4JLVV.js → chunk-JVFYP2LD.js} +2 -2
  77. package/dist/{chunk-KJ3MDX4V.js → chunk-KXY4HW2Y.js} +2 -2
  78. package/dist/{chunk-VQ6H4DL6.js → chunk-L334PSLW.js} +4 -4
  79. package/dist/{chunk-IA3XZG4V.js → chunk-L74VVEJA.js} +2 -2
  80. package/dist/{chunk-UY67YP6Q.js → chunk-LIUI46BX.js} +2 -2
  81. package/dist/{chunk-4OFXLQJD.js → chunk-MPDLLLOG.js} +8 -3
  82. package/dist/chunk-MPDLLLOG.js.map +1 -0
  83. package/dist/{chunk-UP2Q7U4G.js → chunk-MTVC5S5N.js} +2 -2
  84. package/dist/{chunk-QWURVT5Z.js → chunk-N7CATBYH.js} +2 -2
  85. package/dist/{chunk-JEDS2QYV.js → chunk-OH55F76C.js} +2 -2
  86. package/dist/{chunk-2NLLXCJG.js → chunk-OWHERGF2.js} +2 -2
  87. package/dist/{chunk-G6GMZJQ4.js → chunk-PA6ZNFB7.js} +2 -2
  88. package/dist/{chunk-6BKSUA6P.js → chunk-PWEIZCPS.js} +2 -2
  89. package/dist/{chunk-T4SMMTGY.js → chunk-Q3DAWUAV.js} +2 -2
  90. package/dist/{chunk-AWKADEMO.js → chunk-QCAJMIAE.js} +3 -3
  91. package/dist/{chunk-ZRDEOC4X.js → chunk-QI7LLSWX.js} +2 -2
  92. package/dist/{chunk-7DHNBABU.js → chunk-QJ6IR35B.js} +3 -3
  93. package/dist/{chunk-J53IBVMF.js → chunk-R25JMO5O.js} +2 -2
  94. package/dist/{chunk-PARM6OYL.js → chunk-SEA56G6S.js} +1 -1
  95. package/dist/chunk-SEA56G6S.js.map +1 -0
  96. package/dist/{chunk-HL643H2F.js → chunk-TMWSCE2L.js} +8 -8
  97. package/dist/{chunk-FLDXEJFX.js → chunk-TPCRDPUY.js} +2 -2
  98. package/dist/{chunk-KZMRMKC7.js → chunk-U7MIMS2Y.js} +11 -1
  99. package/dist/chunk-U7MIMS2Y.js.map +1 -0
  100. package/dist/{chunk-WJC5KGYC.js → chunk-UFQB5VQ5.js} +6 -6
  101. package/dist/{chunk-64WHWIPL.js → chunk-UVHXLVFJ.js} +2 -2
  102. package/dist/{chunk-X7Y7WX73.js → chunk-UVYI6VIX.js} +1 -1
  103. package/dist/{chunk-5XY46Z2W.js → chunk-UXRCORZX.js} +3 -3
  104. package/dist/{chunk-WUL6LDJ4.js → chunk-VPEA63FC.js} +2 -2
  105. package/dist/{chunk-MHSUEA32.js → chunk-WA6XRNWE.js} +3 -3
  106. package/dist/{chunk-GGHX4YHM.js → chunk-WB3OFPM2.js} +2 -2
  107. package/dist/{chunk-RSYFSCKL.js → chunk-WZEDFDJ6.js} +14 -14
  108. package/dist/{chunk-2HVCTFKG.js → chunk-X2ZLHUEO.js} +2 -2
  109. package/dist/{chunk-PJLJN23N.js → chunk-XICJL56Z.js} +2 -2
  110. package/dist/{chunk-ZXEPLZUQ.js → chunk-YGM47S7M.js} +7 -7
  111. package/dist/{chunk-XWXBXBOX.js → chunk-YLB7TW2B.js} +2 -1
  112. package/dist/chunk-YLB7TW2B.js.map +1 -0
  113. package/dist/{chunk-KXDUSGPM.js → chunk-YRL4PGPP.js} +2 -2
  114. package/dist/{chunk-GJF337YM.js → chunk-Z45353ML.js} +2 -2
  115. package/dist/{chunk-NJZY5LOC.js → chunk-ZARHK7PT.js} +2 -2
  116. package/dist/{chunk-NF5EHQTJ.js → chunk-ZFU2B2GM.js} +5 -5
  117. package/dist/{chunk-W37MS6SX.js → chunk-ZRSXXWIX.js} +2 -2
  118. package/dist/cli.js +60 -59
  119. package/dist/coding/export-okf-codegraph.js +7 -5
  120. package/dist/coding/export-okf-codegraph.js.map +1 -1
  121. package/dist/compounding/engine.js +9 -7
  122. package/dist/config.d.ts.map +1 -1
  123. package/dist/config.js +8 -6
  124. package/dist/connectors/codex-materialize-runner.js +8 -6
  125. package/dist/connectors/index.js +8 -6
  126. package/dist/convergence-refresh.js +10 -8
  127. package/dist/corpus-watermark.js +10 -8
  128. package/dist/deep-recall-http-glue.js +4 -4
  129. package/dist/entity-retrieval.js +8 -6
  130. package/dist/external-wiki-access.js +26 -25
  131. package/dist/extraction.js +8 -6
  132. package/dist/harmonic-retrieval.js +8 -6
  133. package/dist/index.d.ts +1 -1
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +75 -72
  136. package/dist/location/index.js +10 -8
  137. package/dist/maintenance/memory-governance.js +8 -6
  138. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -6
  139. package/dist/maintenance/rebuild-memory-projection-runner.js +10 -8
  140. package/dist/maintenance/rebuild-memory-projection.js +9 -7
  141. package/dist/memory-browse-operations.js +26 -25
  142. package/dist/namespaces/migrate.js +9 -7
  143. package/dist/namespaces/search.js +3 -3
  144. package/dist/namespaces/storage.js +8 -6
  145. package/dist/operator-doctor-corpus.js +11 -9
  146. package/dist/operator-toolkit.js +17 -15
  147. package/dist/orchestration/buffer-drain-gate.d.ts +21 -0
  148. package/dist/orchestration/buffer-drain-gate.d.ts.map +1 -0
  149. package/dist/orchestration/buffer-drain-runner.d.ts +79 -0
  150. package/dist/orchestration/buffer-drain-runner.d.ts.map +1 -0
  151. package/dist/orchestration/buffer-drain-runner.js +780 -0
  152. package/dist/orchestration/buffer-drain-runner.js.map +1 -0
  153. package/dist/orchestration/buffer-swap.d.ts +12 -0
  154. package/dist/orchestration/buffer-swap.d.ts.map +1 -0
  155. package/dist/orchestration/maintenance.js +18 -16
  156. package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
  157. package/dist/orchestrator.d.ts +4 -1
  158. package/dist/orchestrator.d.ts.map +1 -1
  159. package/dist/orchestrator.js +73 -72
  160. package/dist/recall-explain-renderer.js +3 -3
  161. package/dist/recall-navigate-access.js +2 -2
  162. package/dist/recall-navigate-disclosure.js +2 -2
  163. package/dist/recall-navigate.js +2 -2
  164. package/dist/recall-navigation-operations.js +26 -25
  165. package/dist/recall-navigation.js +25 -24
  166. package/dist/recall-xray-cli.js +4 -4
  167. package/dist/recall-xray-renderer.js +3 -3
  168. package/dist/recall-xray.js +2 -2
  169. package/dist/resume-bundles.js +9 -7
  170. package/dist/search/factory.js +2 -2
  171. package/dist/search/index.js +4 -4
  172. package/dist/semantic-consolidation.js +9 -7
  173. package/dist/semantic-rule-promotion.js +8 -6
  174. package/dist/semantic-rule-verifier.js +8 -6
  175. package/dist/source-agent-qualifier.js +8 -6
  176. package/dist/standup/build.js +8 -6
  177. package/dist/storage/buffer-drain-claims.d.ts +159 -0
  178. package/dist/storage/buffer-drain-claims.d.ts.map +1 -0
  179. package/dist/storage/buffer-drain-claims.js +18 -0
  180. package/dist/storage/buffer-drain-claims.js.map +1 -0
  181. package/dist/storage/buffer-session-files.d.ts +14 -2
  182. package/dist/storage/buffer-session-files.d.ts.map +1 -1
  183. package/dist/storage/buffer-session-files.js +22 -0
  184. package/dist/storage/buffer-session-files.js.map +1 -0
  185. package/dist/storage.d.ts +6 -1
  186. package/dist/storage.d.ts.map +1 -1
  187. package/dist/storage.js +7 -5
  188. package/dist/support-passport/index.js +28 -27
  189. package/dist/transfer/backup.js +2 -2
  190. package/dist/transfer/capsule-export.js +2 -2
  191. package/dist/transfer/capsule-import.js +2 -2
  192. package/dist/transfer/export-okf.js +8 -6
  193. package/dist/types.d.ts +2 -0
  194. package/dist/types.d.ts.map +1 -1
  195. package/dist/types.js +1 -1
  196. package/dist/verified-recall.js +8 -6
  197. package/dist/who-knows-cli.js +9 -7
  198. package/dist/who-knows.js +8 -6
  199. package/package.json +32 -2
  200. package/src/access-observe-replay-durability.test.ts +1 -0
  201. package/src/buffer-drain-handover.test.ts +307 -0
  202. package/src/buffer-incremental-persistence.test.ts +19 -1
  203. package/src/buffer-persistence.ts +27 -5
  204. package/src/buffer-session.test.ts +3 -3
  205. package/src/buffer-turn-helpers.ts +16 -0
  206. package/src/buffer.ts +50 -10
  207. package/src/config.ts +1 -0
  208. package/src/index.ts +1 -1
  209. package/src/orchestration/buffer-drain-gate.test.ts +40 -0
  210. package/src/orchestration/buffer-drain-gate.ts +38 -0
  211. package/src/orchestration/buffer-drain-runner.ts +312 -0
  212. package/src/orchestration/buffer-swap.ts +27 -0
  213. package/src/orchestration/turn-ingestion.ts +2 -0
  214. package/src/orchestrator.ts +9 -6
  215. package/src/storage/buffer-drain-claims.test.ts +448 -0
  216. package/src/storage/buffer-drain-claims.ts +593 -0
  217. package/src/storage/buffer-session-files.ts +50 -3
  218. package/src/storage.ts +8 -11
  219. package/src/testing/subjects/extraction-lifecycle.test.ts +34 -0
  220. package/src/types.ts +2 -0
  221. package/dist/chunk-3DG2YAAX.js.map +0 -1
  222. package/dist/chunk-4OFXLQJD.js.map +0 -1
  223. package/dist/chunk-I6UTOUBK.js.map +0 -1
  224. package/dist/chunk-KZMRMKC7.js.map +0 -1
  225. package/dist/chunk-L5RMURVI.js.map +0 -1
  226. package/dist/chunk-PARM6OYL.js.map +0 -1
  227. package/dist/chunk-XWXBXBOX.js.map +0 -1
  228. /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
  229. /package/dist/{chunk-VHBGJAYC.js.map → chunk-25QK3LR2.js.map} +0 -0
  230. /package/dist/{chunk-X2TZWVCC.js.map → chunk-3HLWLYRD.js.map} +0 -0
  231. /package/dist/{chunk-TIS6T6UP.js.map → chunk-3VNRDRBM.js.map} +0 -0
  232. /package/dist/{chunk-4O4VAEWH.js.map → chunk-4NLHEE2Z.js.map} +0 -0
  233. /package/dist/{chunk-66CC6L6R.js.map → chunk-76LKZ42K.js.map} +0 -0
  234. /package/dist/{chunk-LR7RW4T6.js.map → chunk-7ETWIBTO.js.map} +0 -0
  235. /package/dist/{chunk-JM2WJBP7.js.map → chunk-7JFXXA57.js.map} +0 -0
  236. /package/dist/{chunk-PIVSUPF2.js.map → chunk-A6JV3WHI.js.map} +0 -0
  237. /package/dist/{chunk-MOB4JHG6.js.map → chunk-BDNPW3KM.js.map} +0 -0
  238. /package/dist/{chunk-IJFX4JRI.js.map → chunk-BQOOUKEN.js.map} +0 -0
  239. /package/dist/{chunk-PRSF7DWT.js.map → chunk-BU4W2ISH.js.map} +0 -0
  240. /package/dist/{chunk-KQAFEZQX.js.map → chunk-BUKK5SWA.js.map} +0 -0
  241. /package/dist/{chunk-JRY55I6Z.js.map → chunk-C2AYUXEF.js.map} +0 -0
  242. /package/dist/{chunk-DF4SYU4Z.js.map → chunk-C5OW2FFB.js.map} +0 -0
  243. /package/dist/{chunk-74Z2BYEK.js.map → chunk-CYEEVWQ6.js.map} +0 -0
  244. /package/dist/{chunk-M7KAJBO2.js.map → chunk-EADUOZWH.js.map} +0 -0
  245. /package/dist/{chunk-N5H22V75.js.map → chunk-ECT5ME6X.js.map} +0 -0
  246. /package/dist/{chunk-M3GOWG2P.js.map → chunk-FMOUUWQN.js.map} +0 -0
  247. /package/dist/{chunk-2EJAPJ5G.js.map → chunk-FYPKSR6Z.js.map} +0 -0
  248. /package/dist/{chunk-LGLLCWA2.js.map → chunk-GNEB3WXR.js.map} +0 -0
  249. /package/dist/{chunk-Z4EH5TSO.js.map → chunk-HNZH57JK.js.map} +0 -0
  250. /package/dist/{chunk-PTJWUN4H.js.map → chunk-IJKBIOWL.js.map} +0 -0
  251. /package/dist/{chunk-C5GTIRIO.js.map → chunk-IMNJJKHW.js.map} +0 -0
  252. /package/dist/{chunk-ADK5TMQL.js.map → chunk-IVJI5FDA.js.map} +0 -0
  253. /package/dist/{chunk-F3GX76LV.js.map → chunk-IWAB7JVH.js.map} +0 -0
  254. /package/dist/{chunk-JFRO5HJY.js.map → chunk-J62O2UW5.js.map} +0 -0
  255. /package/dist/{chunk-A3UFV4K3.js.map → chunk-JIYJIH5N.js.map} +0 -0
  256. /package/dist/{chunk-UIX4JLVV.js.map → chunk-JVFYP2LD.js.map} +0 -0
  257. /package/dist/{chunk-KJ3MDX4V.js.map → chunk-KXY4HW2Y.js.map} +0 -0
  258. /package/dist/{chunk-VQ6H4DL6.js.map → chunk-L334PSLW.js.map} +0 -0
  259. /package/dist/{chunk-IA3XZG4V.js.map → chunk-L74VVEJA.js.map} +0 -0
  260. /package/dist/{chunk-UY67YP6Q.js.map → chunk-LIUI46BX.js.map} +0 -0
  261. /package/dist/{chunk-UP2Q7U4G.js.map → chunk-MTVC5S5N.js.map} +0 -0
  262. /package/dist/{chunk-QWURVT5Z.js.map → chunk-N7CATBYH.js.map} +0 -0
  263. /package/dist/{chunk-JEDS2QYV.js.map → chunk-OH55F76C.js.map} +0 -0
  264. /package/dist/{chunk-2NLLXCJG.js.map → chunk-OWHERGF2.js.map} +0 -0
  265. /package/dist/{chunk-G6GMZJQ4.js.map → chunk-PA6ZNFB7.js.map} +0 -0
  266. /package/dist/{chunk-6BKSUA6P.js.map → chunk-PWEIZCPS.js.map} +0 -0
  267. /package/dist/{chunk-T4SMMTGY.js.map → chunk-Q3DAWUAV.js.map} +0 -0
  268. /package/dist/{chunk-AWKADEMO.js.map → chunk-QCAJMIAE.js.map} +0 -0
  269. /package/dist/{chunk-ZRDEOC4X.js.map → chunk-QI7LLSWX.js.map} +0 -0
  270. /package/dist/{chunk-7DHNBABU.js.map → chunk-QJ6IR35B.js.map} +0 -0
  271. /package/dist/{chunk-J53IBVMF.js.map → chunk-R25JMO5O.js.map} +0 -0
  272. /package/dist/{chunk-HL643H2F.js.map → chunk-TMWSCE2L.js.map} +0 -0
  273. /package/dist/{chunk-FLDXEJFX.js.map → chunk-TPCRDPUY.js.map} +0 -0
  274. /package/dist/{chunk-WJC5KGYC.js.map → chunk-UFQB5VQ5.js.map} +0 -0
  275. /package/dist/{chunk-64WHWIPL.js.map → chunk-UVHXLVFJ.js.map} +0 -0
  276. /package/dist/{chunk-X7Y7WX73.js.map → chunk-UVYI6VIX.js.map} +0 -0
  277. /package/dist/{chunk-5XY46Z2W.js.map → chunk-UXRCORZX.js.map} +0 -0
  278. /package/dist/{chunk-WUL6LDJ4.js.map → chunk-VPEA63FC.js.map} +0 -0
  279. /package/dist/{chunk-MHSUEA32.js.map → chunk-WA6XRNWE.js.map} +0 -0
  280. /package/dist/{chunk-GGHX4YHM.js.map → chunk-WB3OFPM2.js.map} +0 -0
  281. /package/dist/{chunk-RSYFSCKL.js.map → chunk-WZEDFDJ6.js.map} +0 -0
  282. /package/dist/{chunk-2HVCTFKG.js.map → chunk-X2ZLHUEO.js.map} +0 -0
  283. /package/dist/{chunk-PJLJN23N.js.map → chunk-XICJL56Z.js.map} +0 -0
  284. /package/dist/{chunk-ZXEPLZUQ.js.map → chunk-YGM47S7M.js.map} +0 -0
  285. /package/dist/{chunk-KXDUSGPM.js.map → chunk-YRL4PGPP.js.map} +0 -0
  286. /package/dist/{chunk-GJF337YM.js.map → chunk-Z45353ML.js.map} +0 -0
  287. /package/dist/{chunk-NJZY5LOC.js.map → chunk-ZARHK7PT.js.map} +0 -0
  288. /package/dist/{chunk-NF5EHQTJ.js.map → chunk-ZFU2B2GM.js.map} +0 -0
  289. /package/dist/{chunk-W37MS6SX.js.map → chunk-ZRSXXWIX.js.map} +0 -0
@@ -0,0 +1,307 @@
1
+ // Buffer-drain handover protocol: a live buffer owner (the daemon) must hand
2
+ // a claimed session's turns INTO the drain claim instead of writing a session
3
+ // file, so the out-of-process `remnic buffer drain` can extract them without
4
+ // the daemon resurrecting or losing content. Covers:
5
+ // 1. persist path: claimed key saves hand over + evict the in-memory entry,
6
+ // merging the owner's newest turns into the claim (identity-deduped).
7
+ // 2. no resurrection: after handover the session file stays gone.
8
+ // 3. extraction gate: a claimed session's queued extraction settles skipped
9
+ // (claimed_by_buffer_drain) without touching the LLM; turns stay in the
10
+ // claim for the drain worker.
11
+ // 4. default-key mirror: handing over "default" clears the legacy mirror.
12
+ import assert from "node:assert/strict";
13
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
14
+ import os from "node:os";
15
+ import { hostname } from "node:os";
16
+ import path from "node:path";
17
+ import test from "node:test";
18
+
19
+ import { SmartBuffer } from "./buffer.js";
20
+ import { BufferSessionFiles } from "./storage/buffer-session-files.js";
21
+ import { BufferDrainClaims, type DrainLease } from "./storage/buffer-drain-claims.js";
22
+ import type { BufferStorage } from "./buffer-persistence.js";
23
+ import type { BufferState } from "./types.js";
24
+ import { TurnIngestionCoordinator, type TurnIngestionDeps } from "./orchestration/turn-ingestion.js";
25
+ import type { BufferEntryState, BufferTurn, PluginConfig } from "./types.js";
26
+ import { parseConfig } from "./config.js";
27
+ import type { ExtractionRunResult } from "./orchestration/extraction-run.js";
28
+
29
+ function plainIo() {
30
+ return {
31
+ read: async (filePath: string) => readFileSync(filePath, "utf8"),
32
+ write: async (filePath: string, content: string) => {
33
+ mkdirSync(path.dirname(filePath), { recursive: true });
34
+ writeFileSync(filePath, content);
35
+ },
36
+ };
37
+ }
38
+
39
+ function turn(content: string, sessionKey = "sess-1", timestamp = "2026-11-01T00:00:00.000Z"): BufferTurn {
40
+ return { role: "user", content, timestamp, sessionKey };
41
+ }
42
+
43
+ function entry(turns: ReturnType<typeof turn>[]): BufferEntryState {
44
+ return { turns, lastExtractionAt: null, extractionCount: 0 };
45
+ }
46
+
47
+ function makeConfig(): PluginConfig {
48
+ return parseConfig({
49
+ bufferSaveDebounceMs: 0,
50
+ triggerMode: "smart",
51
+ bufferMaxTurns: 1000,
52
+ bufferMaxMinutes: 100000,
53
+ highSignalPatterns: [],
54
+ extractionMinUserTurns: 1,
55
+ extractionMinChars: 0,
56
+ });
57
+ }
58
+
59
+ interface Harness {
60
+ stateDir: string;
61
+ buffer: SmartBuffer;
62
+ claims: BufferDrainClaims;
63
+ storage: BufferStorage;
64
+ lease: DrainLease;
65
+ cleanup(): void;
66
+ }
67
+
68
+ /** A SmartBuffer wired exactly like the daemon wires it: storage methods backed by BufferSessionFiles. */
69
+ function makeHarness(): Harness {
70
+ const stateDir = mkdtempSync(path.join(os.tmpdir(), "remnic-handover-"));
71
+ const files = new BufferSessionFiles(stateDir, plainIo());
72
+ const claims = new BufferDrainClaims(stateDir, plainIo());
73
+ const storage: BufferStorage = {
74
+ loadBuffer: () => files.load(),
75
+ saveBuffer: (state: BufferState) => files.reconcile(state),
76
+ saveBufferSession: (key: string, entryState: BufferEntryState) => files.save(key, entryState),
77
+ deleteBufferSession: (key: string) => files.remove(key),
78
+ isBufferSessionClaimed: (key: string) => files.isClaimed(key),
79
+ };
80
+ const buffer = new SmartBuffer(makeConfig(), storage);
81
+ const now = new Date().toISOString();
82
+ return {
83
+ stateDir,
84
+ buffer,
85
+ claims,
86
+ storage,
87
+ lease: { ownerId: "worker-1", pid: process.pid, host: hostname(), acquiredAt: now, heartbeatAt: now, leaseMs: 60_000 },
88
+ cleanup: () => rmSync(stateDir, { recursive: true, force: true }),
89
+ };
90
+ }
91
+
92
+ test("a claimed session's save hands over and evicts instead of writing a session file", async () => {
93
+ const h = makeHarness();
94
+ try {
95
+ await h.buffer.load();
96
+ await h.buffer.addTurn("sess-1", turn("old-1"));
97
+ await h.buffer.addTurn("sess-1", turn("old-2"));
98
+ assert.ok(readdirSync(path.join(h.stateDir, "buffer")).some((name) => name.endsWith(".json")), "seeded turns persisted as a session file");
99
+
100
+ const outcome = await h.claims.claimFromSessionFile("sess-1", h.lease);
101
+ assert.equal(outcome, "claimed");
102
+
103
+ // The daemon owner buffers one more turn for the claimed session and its
104
+ // debounced-off save fires: this is the resurrection path, and it must
105
+ // hand over instead.
106
+ await h.buffer.addTurn("sess-1", turn("new-3"));
107
+ await h.buffer.save();
108
+
109
+ const claim = await h.claims.load("sess-1");
110
+ assert.ok(claim, "claim must exist after handover");
111
+ assert.deepEqual(
112
+ claim.entry.turns.map((t) => t.content),
113
+ ["old-1", "old-2", "new-3"],
114
+ "the owner's newest turn merges into the claim; nothing duplicates",
115
+ );
116
+ const topLevel = readdirSync(path.join(h.stateDir, "buffer")).filter((name) => name.endsWith(".json"));
117
+ assert.deepEqual(topLevel, [], "a claimed session must never be written back as a session file");
118
+
119
+ // The in-memory entry was evicted with the handover: the next observe
120
+ // starts fresh (its turns flow into the claim at the next handover).
121
+ await h.buffer.addTurn("sess-1", turn("post-eviction", "sess-1", "2026-11-01T00:05:00.000Z"));
122
+ await h.buffer.save();
123
+ const claimAfter = await h.claims.load("sess-1");
124
+ assert.ok(claimAfter);
125
+ assert.deepEqual(
126
+ claimAfter.entry.turns.map((t) => t.content),
127
+ ["old-1", "old-2", "new-3", "post-eviction"],
128
+ "post-handover observes keep flowing into the claim",
129
+ );
130
+ assert.deepEqual(
131
+ readdirSync(path.join(h.stateDir, "buffer")).filter((name) => name.endsWith(".json")),
132
+ [],
133
+ );
134
+ } finally {
135
+ h.cleanup();
136
+ }
137
+ });
138
+
139
+ test("handover of the default key clears the legacy top-level mirror", async () => {
140
+ const h = makeHarness();
141
+ try {
142
+ await h.buffer.load();
143
+ await h.buffer.addTurn("default", turn("legacy", "legacy-session"));
144
+ const outcome = await h.claims.claimFromSessionFile("default", h.lease);
145
+ assert.equal(outcome, "claimed");
146
+ await h.buffer.addTurn("default", turn("legacy-2", "legacy-session"));
147
+ await h.buffer.save();
148
+ const claim = await h.claims.load("default");
149
+ assert.ok(claim);
150
+ assert.deepEqual(claim.entry.turns.map((t) => t.content), ["legacy", "legacy-2"]);
151
+ // No duplicate writes: the snapshot must settle empty after handover.
152
+ assert.deepEqual(
153
+ readdirSync(path.join(h.stateDir, "buffer")).filter((name) => name.endsWith(".json")),
154
+ [],
155
+ );
156
+ } finally {
157
+ h.cleanup();
158
+ }
159
+ });
160
+
161
+ /**
162
+ * Narrow a test double to the dependency contract. The fixture provides the
163
+ * members the drain path reads; the rest are unused by that path.
164
+ */
165
+ /** The dependency members the drain path reads. */
166
+ interface DrainDepsFixture {
167
+ buffer: unknown;
168
+ config: unknown;
169
+ extractionQueueCoordinator: unknown;
170
+ extractionWindowBudgetChars: unknown;
171
+ getStorage: unknown;
172
+ shouldQueueExtraction: unknown;
173
+ runExtraction: unknown;
174
+ }
175
+
176
+ function asDeps<T extends object>(value: DrainDepsFixture): T {
177
+ // SAFETY: the fixture provides the members the drain path reads; the
178
+ // remaining contract members are unused by that path.
179
+ return value as T;
180
+ }
181
+
182
+ function gateFixture(isClaimed: boolean) {
183
+ const runs: { turns: BufferTurn[] }[] = [];
184
+ const config = {
185
+ extractionChunkDropAfterFailures: 3,
186
+ extractionMinUserTurns: 1,
187
+ extractionMinChars: 0,
188
+ extractionMaxTurnChars: 100_000,
189
+ };
190
+ const deps = {
191
+ buffer: {
192
+ isSessionClaimed: () => isClaimed,
193
+ getTurns: () => [],
194
+ dropExtractionChunk: async () => 0,
195
+ },
196
+ config,
197
+ extractionQueueCoordinator: {
198
+ enqueue: (fn: () => Promise<void>) => {
199
+ void fn().catch(() => {});
200
+ return true;
201
+ },
202
+ runPreemptable: (_preemptable: boolean, parent: AbortSignal | undefined, fn: (signal: AbortSignal) => Promise<void>) => fn(parent ?? new AbortController().signal),
203
+ beginPreemptable: () => {},
204
+ },
205
+ extractionWindowBudgetChars: () => 500,
206
+ getStorage: async () => ({ saveDroppedExtractionChunk: async () => "/dropped" }),
207
+ shouldQueueExtraction: () => true,
208
+ runExtraction: (async (turns: BufferTurn[]) => {
209
+ runs.push({ turns });
210
+ return { status: "completed", persistedCount: 0, durableOutputCount: 0 };
211
+ }),
212
+ // SAFETY: the fixture provides exactly the deps the coordinator reads
213
+ // for the drain path (buffer, queue coordinator, budget, storage, dedupe
214
+ // gate and runExtraction); the cast only fills the unused members.
215
+ };
216
+ const coordinator = new TurnIngestionCoordinator(asDeps<TurnIngestionDeps>(deps));
217
+ return { coordinator, runs };
218
+ }
219
+
220
+ test("a claimed session's queued extraction settles skipped without an LLM call", async () => {
221
+ const claimed = gateFixture(true);
222
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
223
+ void claimed.coordinator.queueBufferedExtraction([turn("blocked")], "trigger_mode", {
224
+ bufferKey: "sess-1",
225
+ clearBufferAfterExtraction: true,
226
+ onTaskSettled: (error, result) => {
227
+ try {
228
+ assert.equal(error, undefined);
229
+ assert.equal(result?.status, "skipped");
230
+ assert.equal(result?.reason, "claimed_by_buffer_drain");
231
+ assert.equal(claimed.runs.length, 0, "the LLM must not run for a claimed session");
232
+ resolve();
233
+ } catch (err) {
234
+ reject(err instanceof Error ? err : new Error(String(err)));
235
+ }
236
+ },
237
+ });
238
+ await promise;
239
+ });
240
+
241
+ test("an unclaimed session still extracts normally (gate is claim-only)", async () => {
242
+ const unclaimed = gateFixture(false);
243
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
244
+ void unclaimed.coordinator.queueBufferedExtraction([turn("flows")], "trigger_mode", {
245
+ bufferKey: "sess-2",
246
+ clearBufferAfterExtraction: true,
247
+ onTaskSettled: (error, result) => {
248
+ try {
249
+ assert.equal(error, undefined);
250
+ assert.notEqual(result?.reason, "claimed_by_buffer_drain");
251
+ assert.equal(unclaimed.runs.length, 1);
252
+ resolve();
253
+ } catch (err) {
254
+ reject(err instanceof Error ? err : new Error(String(err)));
255
+ }
256
+ },
257
+ });
258
+ await promise;
259
+ });
260
+
261
+ test("the owner keeps the claim while a drain is still running on it", async () => {
262
+ const h = makeHarness();
263
+ try {
264
+ await h.buffer.load();
265
+ await h.buffer.addTurn("sess-1", turn("old-1"));
266
+ await h.buffer.addTurn("sess-1", turn("old-2"));
267
+ const claimOutcome = await h.claims.claimFromSessionFile("sess-1", h.lease);
268
+ assert.equal(claimOutcome, "claimed");
269
+
270
+ // The drainer extracts old-1 (progress write moves it to `drained`) but
271
+ // old-2 is still pending, and the drainer holds the lease.
272
+ const loaded = await h.claims.load("sess-1");
273
+ assert.ok(loaded);
274
+ await h.claims.writeEntry("sess-1", h.lease.ownerId, entry([turn("old-2")]), loaded.entry);
275
+
276
+ // The owner saves while the drain is mid-flight: this must hand over into
277
+ // the claim (keeping the claim AND the lease), not consume the tombstone.
278
+ await h.buffer.addTurn("sess-1", turn("new-3"));
279
+ await h.buffer.save();
280
+ const saveSession = h.storage.saveBufferSession;
281
+ assert.ok(saveSession, "the harness wires saveBufferSession");
282
+ const saveOutcome = await saveSession("sess-1", {
283
+ turns: [turn("old-1"), turn("old-2"), turn("new-3")],
284
+ lastExtractionAt: null,
285
+ extractionCount: 0,
286
+ });
287
+ assert.equal(saveOutcome?.status, "handed_over", "a running drain keeps its claim");
288
+ assert.equal(await h.claims.load("sess-1") !== null, true, "the claim survives the owner's mid-drain save");
289
+ assert.equal(existsSync(h.claims.leasePathForTest("sess-1")), true, "the lease survives the owner's mid-drain save");
290
+
291
+ // The drain finishes: everything extracted, the drainer acks (drops its
292
+ // lease, leaving the tombstone). Only now does the owner's save consume it.
293
+ const current = await h.claims.load("sess-1");
294
+ assert.ok(current);
295
+ await h.claims.writeEntry("sess-1", h.lease.ownerId, entry([]), current.entry);
296
+ await h.claims.releaseLease("sess-1", h.lease.ownerId);
297
+ const finalOutcome = await saveSession("sess-1", {
298
+ turns: [turn("old-1"), turn("old-2"), turn("new-3")],
299
+ lastExtractionAt: null,
300
+ extractionCount: 0,
301
+ });
302
+ assert.equal(finalOutcome?.status, "saved", "the acked tombstone is consumed");
303
+ assert.equal(await h.claims.load("sess-1"), null, "the tombstone is gone after the consume");
304
+ } finally {
305
+ h.cleanup();
306
+ }
307
+ });
@@ -163,7 +163,7 @@ test("clearAfterExtraction persists the cleared session state", async () => {
163
163
 
164
164
  test("evicted sessions get their files deleted", async () => {
165
165
  const storage = new IncrementalStorage(emptyState());
166
- const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
166
+ const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100, bufferMaxSessions: 200 }), storage);
167
167
 
168
168
  // normalizeState always materializes a "default" entry, so 198 seeded
169
169
  // sessions + default = 199; new-session reaches exactly 200 (the
@@ -191,6 +191,24 @@ test("evicted sessions get their files deleted", async () => {
191
191
  }
192
192
  });
193
193
 
194
+ test("the default entry cap leaves headroom for live sessions during backlog drains (#3303)", async () => {
195
+ // w9C: live session captures were dropped when the extraction worker was busy
196
+ // on a backlog doc and the 200-session default cap refused new sessions. The
197
+ // default must leave enough headroom that live captures survive a backlog.
198
+ const storage = new IncrementalStorage(emptyState());
199
+ const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
200
+
201
+ for (let i = 0; i < 200; i++) {
202
+ await buffer.addTurnBounded(`backlog-${i}`, makeTurn(`backlog-${i}`, `backlog doc ${i}`));
203
+ }
204
+ await buffer.flushPendingSave();
205
+
206
+ // A live session arriving while the backlog occupies 200 entries must be
207
+ // admitted, not dropped.
208
+ const outcome = await buffer.addTurnBounded("live-session", makeTurn("live-session", "live turn"));
209
+ assert.notEqual(outcome, null, "a live session at the default cap must not be dropped");
210
+ });
211
+
194
212
  test("resetToEmpty drops every session file on the next persist", async () => {
195
213
  const storage = new IncrementalStorage(emptyState());
196
214
  const buffer = new SmartBuffer(parseConfig({ triggerMode: "every_n", bufferMaxTurns: 100 }), storage);
@@ -1,4 +1,5 @@
1
- import type { BufferEntryState, BufferState, BufferSurpriseEvent } from "./types.js";
1
+ import type { BufferEntryState, BufferState, BufferSurpriseEvent, BufferTurn } from "./types.js";
2
+ import type { BufferSessionSaveOutcome } from "./storage/buffer-drain-claims.js";
2
3
 
3
4
  /**
4
5
  * The buffer persistence surface SmartBuffer depends on.
@@ -12,12 +13,23 @@ import type { BufferEntryState, BufferState, BufferSurpriseEvent } from "./types
12
13
  export interface BufferStorage {
13
14
  loadBuffer(): Promise<BufferState>;
14
15
  saveBuffer(state: BufferState): Promise<void>;
15
- saveBufferSession?(key: string, entry: BufferEntryState): Promise<void>;
16
+ /** Reports "handed_over" when the session is claimed by `remnic buffer drain` and the entry moved into the claim instead of a session file. Legacy doubles may return void (treated as "saved"). */
17
+ saveBufferSession?(key: string, entry: BufferEntryState): Promise<BufferSessionSaveOutcome | void>;
16
18
  deleteBufferSession?(key: string): Promise<void>;
19
+ /** Sync claim check for the extraction gate; omitted when the backend has no drain-claim store. */
20
+ isBufferSessionClaimed?(key: string): boolean;
17
21
  /** Optional surprise-telemetry sink; older storage and test doubles omit it. */
18
22
  appendBufferSurpriseEvents?(events: BufferSurpriseEvent[]): Promise<number>;
19
23
  }
20
24
 
25
+ /** What one persist pass changed beyond plain writes. */
26
+ export interface BufferPersistOutcome {
27
+ /** Sessions whose in-memory entry moved into a drain claim: the caller must evict them (a claimed session no longer lives in this process). */
28
+ handedOver: string[];
29
+ /** Per session, the turns a drain already extracted: the caller subtracts them from its in-memory copy. */
30
+ drainedTurns: Map<string, BufferTurn[]>;
31
+ }
32
+
21
33
  /**
22
34
  * Tracks which sessions' persisted files are stale and writes only those.
23
35
  *
@@ -61,15 +73,17 @@ export class BufferSessionPersistence {
61
73
  storage: BufferStorage,
62
74
  state: BufferState,
63
75
  peekEntry: (key: string) => BufferEntryState | undefined,
64
- ): Promise<void> {
76
+ ): Promise<BufferPersistOutcome> {
65
77
  const saveSession = storage.saveBufferSession?.bind(storage);
66
78
  const deleteSession = storage.deleteBufferSession?.bind(storage);
67
79
  if (!saveSession || !deleteSession) {
68
80
  await storage.saveBuffer(state);
69
81
  this.dirty.clear();
70
82
  this.deleted.clear();
71
- return;
83
+ return { handedOver: [], drainedTurns: new Map() };
72
84
  }
85
+ const handedOver: string[] = [];
86
+ const drainedTurns = new Map<string, BufferTurn[]>();
73
87
  for (const key of [...this.deleted]) {
74
88
  await deleteSession(key);
75
89
  this.deleted.delete(key);
@@ -82,8 +96,16 @@ export class BufferSessionPersistence {
82
96
  this.markDeleted(key);
83
97
  continue;
84
98
  }
85
- await saveSession(key, entry);
99
+ const outcome = await saveSession(key, entry);
86
100
  this.dirty.delete(key);
101
+ // "handed_over": the entry moved into a drain claim. Content is durable
102
+ // in the claim, so this key's pending state is settled — the caller
103
+ // evicts the in-memory entry on its side.
104
+ if (outcome?.status === "handed_over") handedOver.push(key);
105
+ // A drain tombstone: the turns it extracted must leave this process's
106
+ // copy too, or the owner would extract them a second time.
107
+ if (outcome?.drainedTurns && outcome.drainedTurns.length > 0) drainedTurns.set(key, outcome.drainedTurns);
87
108
  }
109
+ return { handedOver, drainedTurns };
88
110
  }
89
111
  }
@@ -428,7 +428,7 @@ test("SmartBuffer prunes stale logical session buffers to a bounded entry set",
428
428
  ...entries,
429
429
  },
430
430
  });
431
- const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
431
+ const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0, bufferMaxSessions: 200 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
432
432
 
433
433
  await buffer.addTurn("active-thread", makeTurn("active-thread", "pending memory"));
434
434
 
@@ -672,7 +672,7 @@ test("SmartBuffer prefers pruning empty entries and bounds pending-turn buffers
672
672
  ...entries,
673
673
  },
674
674
  });
675
- const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
675
+ const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0, bufferMaxSessions: 200 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
676
676
 
677
677
  await buffer.addTurn("active-thread", makeTurn("active-thread", "pending memory"));
678
678
 
@@ -699,7 +699,7 @@ test("pruneEntries evicts oldest NON-empty session entries past the cap (#1908 r
699
699
  };
700
700
  }
701
701
  const storage = new FakeStorage({ turns: [], lastExtractionAt: null, extractionCount: 0, entries });
702
- const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
702
+ const buffer = new SmartBuffer(parseConfig({ bufferSaveDebounceMs: 0, bufferMaxSessions: 200 }), storage as unknown as ConstructorParameters<typeof SmartBuffer>[1]);
703
703
  await buffer.load();
704
704
  await buffer.addTurn("session-newest", makeTurn("session-newest", "new turn"));
705
705
 
@@ -104,6 +104,22 @@ export function copyBufferTurn(turn: BufferTurn): BufferTurn {
104
104
  return copy;
105
105
  }
106
106
 
107
+ /**
108
+ * Remove the turns that a drain already extracted (`drained`) from `turns`.
109
+ * Identity-based (role/content/timestamp/session/...): the same comparison
110
+ * the buffer uses to match live turns, so a stale owner can subtract exactly
111
+ * what the drain took and keep everything else.
112
+ */
113
+ export function subtractBufferTurns(turns: readonly BufferTurn[], drained: readonly BufferTurn[]): BufferTurn[] {
114
+ if (drained.length === 0) return [...turns];
115
+ const kept = [...turns];
116
+ for (const gone of drained) {
117
+ const at = kept.findIndex((live) => bufferTurnsEqual(live, gone));
118
+ if (at >= 0) kept.splice(at, 1);
119
+ }
120
+ return kept;
121
+ }
122
+
107
123
  export function bufferTurnsEqual(left: BufferTurn | undefined, right: BufferTurn): boolean {
108
124
  if (!left) return false;
109
125
  return (
package/src/buffer.ts CHANGED
@@ -13,7 +13,7 @@ import type {
13
13
  } from "./types.js";
14
14
  import { resolvePresentationCapabilities, resolveNamespaceCapabilities } from "./capabilities.js";
15
15
  import { resolvePrincipal } from "./namespaces/principal.js";
16
- import { scopeProfilePlanIdentity } from "./buffer-turn-helpers.js";
16
+ import { scopeProfilePlanIdentity, subtractBufferTurns } from "./buffer-turn-helpers.js";
17
17
  import { ExtractionDeadlineError } from "./orchestration/extraction-run.js";
18
18
  import type { ExtractionBufferSnapshot } from "./extraction-liveness.js";
19
19
  import {
@@ -49,7 +49,9 @@ export interface RetainedTurnCleanupOptions {
49
49
  codingContext?: import("./types.js").CodingContext;
50
50
  }
51
51
 
52
- const MAX_BUFFER_ENTRY_COUNT = 200;
52
+ // Raised from 200 in #3303: live captures were being dropped at busy hours
53
+ // while the single extraction worker drained a backlog.
54
+ const FALLBACK_MAX_BUFFER_SESSIONS = 1000;
53
55
  const isAcceptedTurn = (t: { acceptedCapture?: boolean; writeNamespaceOverride?: string }): boolean =>
54
56
  t.acceptedCapture === true || t.writeNamespaceOverride !== undefined;
55
57
 
@@ -129,6 +131,14 @@ export class SmartBuffer {
129
131
  this.surpriseProbe = surpriseProbe;
130
132
  }
131
133
 
134
+ /** Distinct sessions admitted before backpressure; configurable (#3303, raised from 200). */
135
+ private get maxSessionEntries(): number {
136
+ // parseConfig defaults, validates, and floors bufferMaxSessions at the I/O
137
+ // boundary; FALLBACK_MAX_BUFFER_SESSIONS covers direct constructions with
138
+ // configs from before the knob existed.
139
+ return this.config.bufferMaxSessions ?? FALLBACK_MAX_BUFFER_SESSIONS;
140
+ }
141
+
132
142
  private enqueueMutation<T>(op: () => Promise<T>): Promise<T> {
133
143
  const run = this.mutationChain.catch(() => {}).then(op);
134
144
  this.mutationChain = run.catch(() => {});
@@ -206,11 +216,11 @@ export class SmartBuffer {
206
216
  );
207
217
  }
208
218
 
209
- /** Evict oldest entries past MAX_BUFFER_ENTRY_COUNT and queue their session files for deletion. */
219
+ /** Evict oldest entries past this.maxSessionEntries and queue their session files for deletion. */
210
220
  private pruneEntries(retainKeys: string[]): void {
211
221
  const entries = this.state.entries;
212
222
  if (!entries) return;
213
- const evicted = selectEvictedBufferKeys(entries, retainKeys, MAX_BUFFER_ENTRY_COUNT, {
223
+ const evicted = selectEvictedBufferKeys(entries, retainKeys, this.maxSessionEntries, {
214
224
  activityAt: (entry) => this.entryActivityAt(entry),
215
225
  holdsAcceptedCapture: (entry) => this.entryHoldsAcceptedCapture(entry),
216
226
  });
@@ -222,7 +232,7 @@ export class SmartBuffer {
222
232
  // be observable, not silent.
223
233
  log.warn(
224
234
  `buffer: pruned ${evicted.length} oldest session entr${evicted.length === 1 ? "y" : "ies"} ` +
225
- `past MAX_BUFFER_ENTRY_COUNT=${MAX_BUFFER_ENTRY_COUNT}; buffered turn data for those sessions was dropped`,
235
+ `past the session-entry cap (${this.maxSessionEntries}); buffered turn data for those sessions was dropped`,
226
236
  );
227
237
  }
228
238
  }
@@ -289,7 +299,37 @@ export class SmartBuffer {
289
299
  }
290
300
 
291
301
  private async saveUnlocked(): Promise<void> {
292
- await this.persistence.persist(this.storage, this.state, (key) => this.peekEntry(key) ?? undefined);
302
+ const { handedOver, drainedTurns } = await this.persistence.persist(this.storage, this.state, (key) => this.peekEntry(key) ?? undefined);
303
+ // A drain tombstone: the turns the drain extracted leave this copy too,
304
+ // so a stale owner never extracts (or persists) them a second time.
305
+ for (const [key, drained] of drainedTurns) {
306
+ const entry = this.peekEntry(key);
307
+ if (!entry) continue;
308
+ entry.turns = subtractBufferTurns(entry.turns, drained);
309
+ if (entry.retainedTurns && entry.retainedTurns.length > 0) {
310
+ entry.retainedTurns = subtractBufferTurns(entry.retainedTurns, drained);
311
+ if (entry.retainedTurns.length === 0) delete entry.retainedTurns;
312
+ }
313
+ if (key === "default") this.state.turns = entry.turns;
314
+ }
315
+ if (handedOver.length === 0) return;
316
+ // A drain claim now owns these sessions' turns (durable in the claim
317
+ // file): drop the in-memory entries so this process cannot resurrect
318
+ // them. A later observe for the same key starts a fresh entry whose
319
+ // turns flow into the claim at the next handover.
320
+ const entries = this.state.entries;
321
+ if (!entries) return;
322
+ for (const key of handedOver) {
323
+ delete entries[key];
324
+ if (key === "default") {
325
+ this.state.turns = [];
326
+ }
327
+ }
328
+ }
329
+
330
+ /** Sync gate for the extraction queue: is this session claimed by a buffer drain? */
331
+ isSessionClaimed(key: string): boolean {
332
+ return this.storage.isBufferSessionClaimed?.(key) === true;
293
333
  }
294
334
 
295
335
  async save(): Promise<void> {
@@ -457,9 +497,9 @@ export class SmartBuffer {
457
497
  await this.loadUnlocked();
458
498
  if (this.peekEntry(bufferKey) === null) {
459
499
  this.makeRoomForEntryUnlocked();
460
- if (Object.keys(this.state.entries ?? {}).length >= MAX_BUFFER_ENTRY_COUNT) {
500
+ if (Object.keys(this.state.entries ?? {}).length >= this.maxSessionEntries) {
461
501
  log.warn(
462
- `buffer[${bufferKey}]: durable capture refused at MAX_BUFFER_ENTRY_COUNT=${MAX_BUFFER_ENTRY_COUNT}`,
502
+ `buffer[${bufferKey}]: durable capture refused at the session-entry cap (${this.maxSessionEntries})`,
463
503
  );
464
504
  return null;
465
505
  }
@@ -475,7 +515,7 @@ export class SmartBuffer {
475
515
  const entries = this.state.entries;
476
516
  if (!entries) return;
477
517
  const keys = Object.keys(entries);
478
- if (keys.length < MAX_BUFFER_ENTRY_COUNT) return;
518
+ if (keys.length < this.maxSessionEntries) return;
479
519
  const byOldestActivity = (left: string, right: string): number =>
480
520
  this.entryActivityAt(entries[left] ?? { turns: [], lastExtractionAt: null, extractionCount: 0 }) -
481
521
  this.entryActivityAt(entries[right] ?? { turns: [], lastExtractionAt: null, extractionCount: 0 });
@@ -488,7 +528,7 @@ export class SmartBuffer {
488
528
  )
489
529
  .sort(byOldestActivity);
490
530
  for (const key of emptyRemovable) {
491
- if (Object.keys(entries).length < MAX_BUFFER_ENTRY_COUNT) break;
531
+ if (Object.keys(entries).length < this.maxSessionEntries) break;
492
532
  delete entries[key];
493
533
  this.persistence.markDeleted(key);
494
534
  }
package/src/config.ts CHANGED
@@ -1591,6 +1591,7 @@ export function parseConfig(
1591
1591
  ...parseSecurityConfig(cfg, rawOperatorConfig),
1592
1592
  bufferMaxTurns:
1593
1593
  typeof cfg.bufferMaxTurns === "number" ? cfg.bufferMaxTurns : 5,
1594
+ bufferMaxSessions: parsePositiveInteger(cfg.bufferMaxSessions, "bufferMaxSessions") ?? 1000,
1594
1595
  bufferMaxMinutes:
1595
1596
  typeof cfg.bufferMaxMinutes === "number" ? cfg.bufferMaxMinutes : 15,
1596
1597
  // Surprise-gated buffer flush (issue #563, D-MEM). See types.ts for
package/src/index.ts CHANGED
@@ -346,7 +346,7 @@ export { MeilisearchBackend } from "./search/meilisearch-backend.js";
346
346
  // ---------------------------------------------------------------------------
347
347
 
348
348
  export { buildEntityRecallSection } from "./entity-retrieval.js";
349
- export { isNamespacePolicyCovered, resolvePrincipal } from "./namespaces/principal.js";
349
+ export { defaultNamespaceForPrincipal, isNamespacePolicyCovered, resolvePrincipal } from "./namespaces/principal.js";
350
350
  export {
351
351
  NamespaceCatalog,
352
352
  type NamespaceRecord,
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `skipClaimedSession` gate: a buffer without a claim registry never reports a
3
+ * claim (the legacy/test-double shape), and a claimed session settles as
4
+ * skipped without running an extraction.
5
+ */
6
+ import assert from "node:assert/strict";
7
+ import test from "node:test";
8
+ import { skipClaimedSession, type ClaimAwareBuffer } from "./buffer-drain-gate.js";
9
+ import type { ExtractionRunResult } from "./extraction-run.js";
10
+
11
+ test("a buffer without isSessionClaimed is never treated as claimed", () => {
12
+ const settled: ExtractionRunResult[] = [];
13
+ const buffer: ClaimAwareBuffer = {};
14
+ const result = skipClaimedSession(buffer, "sess-1", (r) => settled.push(r));
15
+ assert.equal(result, false);
16
+ assert.deepEqual(settled, []);
17
+ });
18
+
19
+ test("an unclaimed session with the method proceeds", () => {
20
+ const settled: ExtractionRunResult[] = [];
21
+ const result = skipClaimedSession({ isSessionClaimed: () => false }, "sess-1", (r) => settled.push(r));
22
+ assert.equal(result, false);
23
+ assert.deepEqual(settled, []);
24
+ });
25
+
26
+ test("a claimed session settles as skipped without extracting", () => {
27
+ const settled: ExtractionRunResult[] = [];
28
+ const result = skipClaimedSession({ isSessionClaimed: () => true }, "sess-1", (r) => settled.push(r));
29
+ assert.equal(result, true);
30
+ assert.equal(settled.length, 1);
31
+ assert.equal(settled[0].status, "skipped");
32
+ assert.equal(settled[0].reason, "claimed_by_buffer_drain");
33
+ });
34
+
35
+ test("a buffer-less deps bundle (older host or test double) is never claimed", () => {
36
+ const settled: ExtractionRunResult[] = [];
37
+ const result = skipClaimedSession(undefined, "sess-1", (r) => settled.push(r));
38
+ assert.equal(result, false);
39
+ assert.deepEqual(settled, []);
40
+ });