@remnic/core 10.0.84 → 10.0.85

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 (399) hide show
  1. package/dist/access-admin-ops-surface.js +67 -66
  2. package/dist/access-authorization-probe.js +68 -67
  3. package/dist/access-boundary.js +67 -66
  4. package/dist/access-cli.js +146 -144
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-extraction-force-flush.js +67 -66
  7. package/dist/access-http-briefing.js +68 -67
  8. package/dist/access-http-query.js +69 -68
  9. package/dist/access-http.js +91 -89
  10. package/dist/access-identity-continuity-surface.js +67 -66
  11. package/dist/access-lcm-surface.js +67 -66
  12. package/dist/access-mcp-frames.d.ts +29 -0
  13. package/dist/access-mcp-frames.d.ts.map +1 -0
  14. package/dist/access-mcp-frames.js +10 -0
  15. package/dist/access-mcp.d.ts +1 -1
  16. package/dist/access-mcp.d.ts.map +1 -1
  17. package/dist/access-mcp.js +82 -80
  18. package/dist/access-namespace-preflight.js +67 -66
  19. package/dist/access-observe-write-category.js +9 -9
  20. package/dist/access-observe-write-surface.d.ts.map +1 -1
  21. package/dist/access-observe-write-surface.js +67 -66
  22. package/dist/access-offline-manifest.js +3 -3
  23. package/dist/access-operations-batch.js +71 -70
  24. package/dist/access-operations.js +78 -77
  25. package/dist/access-recall-concurrency.js +67 -66
  26. package/dist/access-recall-response.js +67 -66
  27. package/dist/access-recall-surface.js +67 -66
  28. package/dist/access-schema.js +4 -4
  29. package/dist/access-service.js +67 -66
  30. package/dist/access-surface-catalog.js +69 -68
  31. package/dist/access-surface-catalog.js.map +1 -1
  32. package/dist/active-recall.js +31 -31
  33. package/dist/{auto-sync-C7E6G7Y2.js → auto-sync-3AZW7MXI.js} +6 -6
  34. package/dist/briefing.js +25 -25
  35. package/dist/buffer-persistence.d.ts +8 -4
  36. package/dist/buffer-persistence.d.ts.map +1 -1
  37. package/dist/buffer-persistence.js +1 -1
  38. package/dist/buffer-turn-helpers.d.ts +5 -0
  39. package/dist/buffer-turn-helpers.d.ts.map +1 -1
  40. package/dist/buffer-turn-helpers.js +1 -1
  41. package/dist/buffer.d.ts +4 -1
  42. package/dist/buffer.d.ts.map +1 -1
  43. package/dist/buffer.js +9 -9
  44. package/dist/calibration.js +2 -2
  45. package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-GWVG7LGC.js} +2 -2
  46. package/dist/capsule-gunzip-KGBDGF7P.js +11 -0
  47. package/dist/{capsule-merge-XVTQJJ6R.js → capsule-merge-2IIXFXN5.js} +5 -11
  48. package/dist/capsule-merge-2IIXFXN5.js.map +1 -0
  49. package/dist/causal-behavior.js +2 -2
  50. package/dist/causal-chain.js +2 -2
  51. package/dist/causal-consolidation.js +32 -32
  52. package/dist/causal-retrieval.js +2 -2
  53. package/dist/causal-trajectory-graph.js +2 -2
  54. package/dist/causal-trajectory.js +1 -1
  55. package/dist/{chunk-T6T2RMWW.js → chunk-23B4HLWG.js} +5 -5
  56. package/dist/{chunk-LECSS7EE.js → chunk-2ASEDFOA.js} +2 -2
  57. package/dist/{chunk-TPX7XXY4.js → chunk-2JAT4S7N.js} +2 -2
  58. package/dist/{chunk-XRBHIR7Q.js → chunk-2URKYLMK.js} +2 -2
  59. package/dist/{chunk-IN4OFAHX.js → chunk-2XYDL2FG.js} +2 -2
  60. package/dist/{chunk-HQGATKG2.js → chunk-3KGCHTTK.js} +2 -2
  61. package/dist/{chunk-YAUHSNPB.js → chunk-3LHPTBWG.js} +2 -2
  62. package/dist/{chunk-7PMOCQOL.js → chunk-3ODZLCHC.js} +192 -63
  63. package/dist/chunk-3ODZLCHC.js.map +1 -0
  64. package/dist/{chunk-XT45WCKX.js → chunk-3VU2FDTU.js} +8 -8
  65. package/dist/{chunk-CIIHIAJ2.js → chunk-433EXTC4.js} +3 -3
  66. package/dist/{chunk-QZSIDSB2.js → chunk-44FQCC5D.js} +2 -2
  67. package/dist/{chunk-YUTT4ZTI.js → chunk-45VUHDV2.js} +2 -2
  68. package/dist/{chunk-54FDG7K7.js → chunk-4DPLCYEK.js} +2 -2
  69. package/dist/{chunk-KO3YBHOV.js → chunk-4IWEPQEV.js} +2 -2
  70. package/dist/{chunk-H7BZBSP6.js → chunk-52II7PB6.js} +4 -4
  71. package/dist/{chunk-K6RTQZA4.js → chunk-6DANGCMO.js} +2 -2
  72. package/dist/{chunk-2IW5PT7U.js → chunk-6KFFRMPQ.js} +2 -2
  73. package/dist/{chunk-MSIRVXQ3.js → chunk-73YVMZ7D.js} +4 -4
  74. package/dist/{chunk-A7BM2JTH.js → chunk-AJHNXIQU.js} +2 -2
  75. package/dist/{chunk-2D7BTSDW.js → chunk-ANZZLRUX.js} +2 -2
  76. package/dist/{chunk-YOEYL6CH.js → chunk-AP3TSV6S.js} +2 -2
  77. package/dist/{chunk-GQZSGXR6.js → chunk-BATPQNRG.js} +2 -2
  78. package/dist/{chunk-D5SSBCYY.js → chunk-BLGQUZFB.js} +2 -2
  79. package/dist/{chunk-BDN3TDGV.js → chunk-BLMTOIBK.js} +2 -2
  80. package/dist/{chunk-B74NPUHT.js → chunk-BMIM6ESC.js} +43 -43
  81. package/dist/chunk-BMIM6ESC.js.map +1 -0
  82. package/dist/{chunk-R45L4TV6.js → chunk-BUKK5SWA.js} +6 -6
  83. package/dist/{chunk-TFLPDNQV.js → chunk-CCU7URRP.js} +2 -2
  84. package/dist/{chunk-2XJZLSXE.js → chunk-CMRHXPLQ.js} +20 -20
  85. package/dist/{chunk-BK4FBHZ3.js → chunk-D6THW7PA.js} +3 -3
  86. package/dist/{chunk-BFBS7OFQ.js → chunk-DC55RIEZ.js} +4 -4
  87. package/dist/{chunk-VKDAJJQP.js → chunk-DI5ET2IE.js} +5 -5
  88. package/dist/{chunk-QZBTIYKD.js → chunk-DM4RRTN7.js} +3 -3
  89. package/dist/{chunk-EBOHFZI4.js → chunk-E5QEF3JO.js} +2 -2
  90. package/dist/{chunk-K3B72DKJ.js → chunk-E7V5AK7I.js} +2 -2
  91. package/dist/{chunk-WAD32CZG.js → chunk-ELWI2R6H.js} +26 -13
  92. package/dist/chunk-ELWI2R6H.js.map +1 -0
  93. package/dist/{chunk-BV2BRHD4.js → chunk-FHGWKODN.js} +2 -2
  94. package/dist/{chunk-HPFVZPRH.js → chunk-FK4WC6VK.js} +3 -3
  95. package/dist/{chunk-VLMBSGQF.js → chunk-FRM4UTDR.js} +6 -6
  96. package/dist/{chunk-LTDLJCMQ.js → chunk-FZPDRCOB.js} +2 -2
  97. package/dist/{chunk-ZJJFUEPO.js → chunk-GBOYOL5L.js} +2 -2
  98. package/dist/{chunk-YUTG35M4.js → chunk-GJVGFKOU.js} +2 -2
  99. package/dist/{chunk-77R7BQZ2.js → chunk-GKYIRTNX.js} +33 -11
  100. package/dist/{chunk-77R7BQZ2.js.map → chunk-GKYIRTNX.js.map} +1 -1
  101. package/dist/{chunk-K36RMINQ.js → chunk-GOZXQNIM.js} +3 -3
  102. package/dist/{chunk-HWHNQVRV.js → chunk-GYNRNKMW.js} +2 -2
  103. package/dist/{chunk-LU66I56Q.js → chunk-H34INI3P.js} +29 -3
  104. package/dist/chunk-H34INI3P.js.map +1 -0
  105. package/dist/{chunk-HLKUZW42.js → chunk-H4D5YURJ.js} +80 -94
  106. package/dist/chunk-H4D5YURJ.js.map +1 -0
  107. package/dist/{chunk-6LCWKS62.js → chunk-HVFX2N2G.js} +60 -59
  108. package/dist/chunk-HVFX2N2G.js.map +1 -0
  109. package/dist/chunk-IGLJNHOX.js +62 -0
  110. package/dist/chunk-IGLJNHOX.js.map +1 -0
  111. package/dist/{chunk-KFLD7ZZG.js → chunk-IUKYFTGW.js} +3 -3
  112. package/dist/{chunk-YK7GOPIE.js → chunk-JFH6JAGY.js} +6 -4
  113. package/dist/chunk-JFH6JAGY.js.map +1 -0
  114. package/dist/{chunk-BNQJKWWZ.js → chunk-K3FH3LAQ.js} +4 -4
  115. package/dist/{chunk-FJ2VJTGA.js → chunk-KHZVAISH.js} +2 -2
  116. package/dist/{chunk-WHQXWFH7.js → chunk-LT3CMO65.js} +2 -2
  117. package/dist/{chunk-NUSAAN75.js → chunk-MHSBOEH7.js} +2 -2
  118. package/dist/{chunk-KJCL2FZO.js → chunk-MNYQVYJ4.js} +2 -2
  119. package/dist/{chunk-RZH5RVJC.js → chunk-MTTYIXSU.js} +2 -2
  120. package/dist/{chunk-FVRTCYZ3.js → chunk-MVOJDLXS.js} +2 -2
  121. package/dist/{chunk-KESJ5XX3.js → chunk-MYCCO6AA.js} +2 -2
  122. package/dist/{chunk-66JOLMWS.js → chunk-MZCGHPIJ.js} +2 -2
  123. package/dist/{chunk-B4ZU34JQ.js → chunk-N6II66E5.js} +3 -3
  124. package/dist/{chunk-BK7AJUWB.js → chunk-NIV7RPWQ.js} +6 -6
  125. package/dist/{chunk-TLHF75OR.js → chunk-O55AGDTI.js} +10 -10
  126. package/dist/{chunk-GGJFRVRD.js → chunk-OMYRPMQC.js} +2 -2
  127. package/dist/{chunk-3JPTUCD7.js → chunk-ONN2E5QD.js} +32 -23
  128. package/dist/chunk-ONN2E5QD.js.map +1 -0
  129. package/dist/{chunk-XHGYZRVW.js → chunk-OSTFDWZJ.js} +4 -4
  130. package/dist/{chunk-BXLOS5AJ.js → chunk-OWHERGF2.js} +2 -2
  131. package/dist/{chunk-WC475JOG.js → chunk-P225W4RQ.js} +6 -6
  132. package/dist/{chunk-TFA6HPOA.js → chunk-QCP75JGO.js} +5 -5
  133. package/dist/{chunk-JRVMCZE3.js → chunk-QEYTRFR3.js} +2 -2
  134. package/dist/chunk-QV2VZKD5.js +31 -0
  135. package/dist/chunk-QV2VZKD5.js.map +1 -0
  136. package/dist/{chunk-IZJQNDGI.js → chunk-R6HMAIKM.js} +2 -2
  137. package/dist/{chunk-VUROWXFL.js → chunk-RNGDKFY2.js} +2 -2
  138. package/dist/{chunk-FF6N7BXC.js → chunk-S45INSV3.js} +4 -4
  139. package/dist/{chunk-2RUTN7PR.js → chunk-S7GJNA3K.js} +2 -2
  140. package/dist/{chunk-XYP5WBDO.js → chunk-SGHV6LZ7.js} +13 -13
  141. package/dist/chunk-SGHV6LZ7.js.map +1 -0
  142. package/dist/{chunk-RJWL3RHB.js → chunk-TFEHSPGQ.js} +2 -2
  143. package/dist/{chunk-T77AX6DG.js → chunk-TQVECGUQ.js} +4 -4
  144. package/dist/{chunk-OQSSQOKQ.js → chunk-TV65NR7L.js} +2 -2
  145. package/dist/{chunk-KQUSP6LB.js → chunk-UDKWFDV2.js} +1 -1
  146. package/dist/chunk-UDKWFDV2.js.map +1 -0
  147. package/dist/{chunk-HQPFACXO.js → chunk-UH3FPX3L.js} +2 -2
  148. package/dist/{chunk-4EACCIPO.js → chunk-UHPHH74F.js} +2 -2
  149. package/dist/{chunk-DQEMWVMT.js → chunk-UVYI6VIX.js} +1 -1
  150. package/dist/{chunk-6QYPL6NI.js → chunk-V45TEVDA.js} +3 -3
  151. package/dist/{chunk-GCAQ3VUJ.js → chunk-VCVEMQJK.js} +2 -2
  152. package/dist/{chunk-6L63IN3M.js → chunk-VEFGHRKV.js} +1 -1
  153. package/dist/{chunk-E33UJHY3.js → chunk-WFDWLWDK.js} +2 -2
  154. package/dist/{chunk-4KLQI3L2.js → chunk-WLNWFHX3.js} +1 -1
  155. package/dist/chunk-WLNWFHX3.js.map +1 -0
  156. package/dist/{chunk-KQLBXUUQ.js → chunk-WYG5UNDY.js} +2 -2
  157. package/dist/{chunk-MPGLM2VU.js → chunk-X7REO4CH.js} +2 -2
  158. package/dist/{chunk-MPDLLLOG.js → chunk-XO23OKMK.js} +9 -5
  159. package/dist/chunk-XO23OKMK.js.map +1 -0
  160. package/dist/{chunk-Z3DDQXM4.js → chunk-XSFQN7TP.js} +35 -36
  161. package/dist/chunk-XSFQN7TP.js.map +1 -0
  162. package/dist/{chunk-FZVROZKC.js → chunk-XVZA6VXC.js} +4 -4
  163. package/dist/{chunk-WV254W4V.js → chunk-Y3IYKNBY.js} +2 -2
  164. package/dist/{chunk-FUOAIJKQ.js → chunk-Y43X3E24.js} +2 -2
  165. package/dist/{chunk-3T3VHXUH.js → chunk-YAAKVTZA.js} +2 -2
  166. package/dist/{chunk-VPEIWMUT.js → chunk-YLTU2DC4.js} +2 -2
  167. package/dist/{chunk-65SKM4BF.js → chunk-YNBMBL4C.js} +2 -2
  168. package/dist/{chunk-PJYK7HH2.js → chunk-ZWTHVUT5.js} +2 -2
  169. package/dist/cli.js +110 -108
  170. package/dist/coding/export-okf-codegraph.js +25 -25
  171. package/dist/coding/pre-action-gate.js +2 -2
  172. package/dist/compounding/engine.js +27 -27
  173. package/dist/config.js +28 -28
  174. package/dist/connectors/codex-materialize-runner.js +25 -25
  175. package/dist/connectors/index.js +25 -25
  176. package/dist/contradiction/index.js +4 -4
  177. package/dist/convergence-refresh.js +27 -27
  178. package/dist/conversation-index/backend.js +2 -2
  179. package/dist/corpus-watermark.js +27 -27
  180. package/dist/deep-recall-http-glue.js +5 -5
  181. package/dist/entity-retrieval.js +25 -25
  182. package/dist/event-order-recall.js +2 -2
  183. package/dist/explicit-capture.js +5 -5
  184. package/dist/external-wiki-access.js +68 -67
  185. package/dist/extraction-evidence-class.js +2 -2
  186. package/dist/extraction-quote-grounding.d.ts.map +1 -1
  187. package/dist/extraction-quote-grounding.js +1 -1
  188. package/dist/extraction-source-grounding-rules.js +2 -2
  189. package/dist/extraction-source-grounding.js +5 -5
  190. package/dist/extraction-span-materialize.js +2 -2
  191. package/dist/extraction.js +40 -40
  192. package/dist/{first-start-migration-QVD22WWS.js → first-start-migration-A6PHIBUQ.js} +4 -4
  193. package/dist/{graph-edge-decay-CXTNJ7IQ.js → graph-edge-decay-EKWXRBWF.js} +5 -5
  194. package/dist/graph-edge-decay-EKWXRBWF.js.map +1 -0
  195. package/dist/graph-jsonl.js +3 -3
  196. package/dist/graph-snapshot.js +3 -3
  197. package/dist/graph.js +2 -2
  198. package/dist/harmonic-retrieval.js +25 -25
  199. package/dist/index.js +195 -193
  200. package/dist/lcm/engine.js +2 -2
  201. package/dist/lcm/index.js +2 -2
  202. package/dist/local-llm.js +2 -2
  203. package/dist/location/index.js +28 -28
  204. package/dist/maintenance/memory-governance.js +25 -25
  205. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +25 -25
  206. package/dist/maintenance/rebuild-memory-projection-runner.js +27 -27
  207. package/dist/maintenance/rebuild-memory-projection.js +26 -26
  208. package/dist/memory-browse-operations.js +68 -67
  209. package/dist/memory-subject.js +2 -2
  210. package/dist/namespaces/migrate.js +26 -26
  211. package/dist/namespaces/search.d.ts +8 -0
  212. package/dist/namespaces/search.d.ts.map +1 -1
  213. package/dist/namespaces/search.js +10 -6
  214. package/dist/namespaces/storage.js +25 -25
  215. package/dist/operator-doctor-corpus.js +28 -28
  216. package/dist/operator-toolkit.js +39 -39
  217. package/dist/orchestration/backlog-scheduler.d.ts +3 -2
  218. package/dist/orchestration/backlog-scheduler.d.ts.map +1 -1
  219. package/dist/orchestration/backlog-scheduler.js +157 -155
  220. package/dist/orchestration/backlog-scheduler.js.map +1 -1
  221. package/dist/orchestration/buffer-drain-runner.d.ts +6 -1
  222. package/dist/orchestration/buffer-drain-runner.d.ts.map +1 -1
  223. package/dist/orchestration/buffer-drain-runner.js +147 -145
  224. package/dist/orchestration/extraction-persist.d.ts.map +1 -1
  225. package/dist/orchestration/fact-dedup-window.d.ts +1 -4
  226. package/dist/orchestration/fact-dedup-window.d.ts.map +1 -1
  227. package/dist/orchestration/maintenance.js +39 -39
  228. package/dist/orchestrator.js +146 -144
  229. package/dist/provenance.js +2 -2
  230. package/dist/recall-navigation-operations.js +68 -67
  231. package/dist/recall-navigation.js +67 -66
  232. package/dist/reconcile/manifest.js +2 -2
  233. package/dist/{resolution-BNWE5Y25.js → resolution-23ZBDM6A.js} +5 -5
  234. package/dist/resolution-23ZBDM6A.js.map +1 -0
  235. package/dist/resume-bundles.js +29 -29
  236. package/dist/salvage-envelope.js +4 -4
  237. package/dist/search/factory.js +5 -5
  238. package/dist/search/index.js +9 -9
  239. package/dist/semantic-consolidation.js +26 -26
  240. package/dist/semantic-rule-promotion.js +25 -25
  241. package/dist/semantic-rule-verifier.js +25 -25
  242. package/dist/source-agent-qualifier.js +31 -31
  243. package/dist/standup/build.js +25 -25
  244. package/dist/storage/buffer-drain-claims.d.ts +18 -6
  245. package/dist/storage/buffer-drain-claims.d.ts.map +1 -1
  246. package/dist/storage/buffer-drain-claims.js +2 -2
  247. package/dist/storage/buffer-session-files.js +2 -2
  248. package/dist/storage/content-hash-index.d.ts +8 -0
  249. package/dist/storage/content-hash-index.d.ts.map +1 -1
  250. package/dist/storage.d.ts +10 -1
  251. package/dist/storage.d.ts.map +1 -1
  252. package/dist/storage.js +24 -24
  253. package/dist/subject-config.js +3 -3
  254. package/dist/summarizer.js +3 -3
  255. package/dist/support-passport/index.js +70 -69
  256. package/dist/transfer/autodetect.js +1 -1
  257. package/dist/transfer/backup.js +2 -2
  258. package/dist/transfer/capsule-export.js +3 -3
  259. package/dist/transfer/capsule-gunzip.d.ts +14 -0
  260. package/dist/transfer/capsule-gunzip.d.ts.map +1 -0
  261. package/dist/transfer/capsule-import.d.ts.map +1 -1
  262. package/dist/transfer/capsule-import.js +3 -2
  263. package/dist/transfer/capsule-merge.d.ts.map +1 -1
  264. package/dist/transfer/export-okf.js +25 -25
  265. package/dist/verified-recall.js +25 -25
  266. package/dist/who-knows-cli.js +26 -26
  267. package/dist/who-knows.js +25 -25
  268. package/dist/write-envelope.js +3 -3
  269. package/package.json +2 -2
  270. package/src/access-mcp-frames.test.ts +47 -0
  271. package/src/access-mcp-frames.ts +74 -0
  272. package/src/access-mcp.test.ts +146 -0
  273. package/src/access-mcp.ts +30 -30
  274. package/src/access-observe-write-surface.ts +4 -4
  275. package/src/backlog-scheduler.test.ts +443 -3
  276. package/src/buffer-persistence.ts +15 -6
  277. package/src/buffer-turn-helpers.ts +6 -0
  278. package/src/buffer.ts +6 -6
  279. package/src/cli.ts +4 -4
  280. package/src/extraction-quote-grounding.test.ts +49 -0
  281. package/src/extraction-quote-grounding.ts +50 -12
  282. package/src/fact-dedup-window.test.ts +103 -20
  283. package/src/namespaces/search.test.ts +339 -0
  284. package/src/namespaces/search.ts +54 -1
  285. package/src/orchestration/backlog-scheduler.ts +13 -12
  286. package/src/orchestration/buffer-drain-runner.ts +40 -16
  287. package/src/orchestration/extraction-persist.ts +2 -10
  288. package/src/orchestration/fact-dedup-window.ts +5 -12
  289. package/src/storage/buffer-drain-claims.test.ts +330 -6
  290. package/src/storage/buffer-drain-claims.ts +226 -80
  291. package/src/storage/buffer-session-files.ts +2 -2
  292. package/src/storage/content-hash-index.ts +9 -0
  293. package/src/storage.ts +15 -0
  294. package/src/transfer/capsule-gunzip.ts +39 -0
  295. package/src/transfer/capsule-import.test.ts +73 -0
  296. package/src/transfer/capsule-import.ts +2 -2
  297. package/src/transfer/capsule-merge.test.ts +34 -0
  298. package/src/transfer/capsule-merge.ts +2 -10
  299. package/dist/capsule-merge-XVTQJJ6R.js.map +0 -1
  300. package/dist/chunk-3JPTUCD7.js.map +0 -1
  301. package/dist/chunk-4KLQI3L2.js.map +0 -1
  302. package/dist/chunk-6LCWKS62.js.map +0 -1
  303. package/dist/chunk-7PMOCQOL.js.map +0 -1
  304. package/dist/chunk-B74NPUHT.js.map +0 -1
  305. package/dist/chunk-HLKUZW42.js.map +0 -1
  306. package/dist/chunk-KQUSP6LB.js.map +0 -1
  307. package/dist/chunk-LU66I56Q.js.map +0 -1
  308. package/dist/chunk-MPDLLLOG.js.map +0 -1
  309. package/dist/chunk-WAD32CZG.js.map +0 -1
  310. package/dist/chunk-XYP5WBDO.js.map +0 -1
  311. package/dist/chunk-YK7GOPIE.js.map +0 -1
  312. package/dist/chunk-Z3DDQXM4.js.map +0 -1
  313. /package/dist/{capsule-crypto-YO5QJ6L3.js.map → access-mcp-frames.js.map} +0 -0
  314. /package/dist/{auto-sync-C7E6G7Y2.js.map → auto-sync-3AZW7MXI.js.map} +0 -0
  315. /package/dist/{graph-edge-decay-CXTNJ7IQ.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
  316. /package/dist/{resolution-BNWE5Y25.js.map → capsule-gunzip-KGBDGF7P.js.map} +0 -0
  317. /package/dist/{chunk-T6T2RMWW.js.map → chunk-23B4HLWG.js.map} +0 -0
  318. /package/dist/{chunk-LECSS7EE.js.map → chunk-2ASEDFOA.js.map} +0 -0
  319. /package/dist/{chunk-TPX7XXY4.js.map → chunk-2JAT4S7N.js.map} +0 -0
  320. /package/dist/{chunk-XRBHIR7Q.js.map → chunk-2URKYLMK.js.map} +0 -0
  321. /package/dist/{chunk-IN4OFAHX.js.map → chunk-2XYDL2FG.js.map} +0 -0
  322. /package/dist/{chunk-HQGATKG2.js.map → chunk-3KGCHTTK.js.map} +0 -0
  323. /package/dist/{chunk-YAUHSNPB.js.map → chunk-3LHPTBWG.js.map} +0 -0
  324. /package/dist/{chunk-XT45WCKX.js.map → chunk-3VU2FDTU.js.map} +0 -0
  325. /package/dist/{chunk-CIIHIAJ2.js.map → chunk-433EXTC4.js.map} +0 -0
  326. /package/dist/{chunk-QZSIDSB2.js.map → chunk-44FQCC5D.js.map} +0 -0
  327. /package/dist/{chunk-YUTT4ZTI.js.map → chunk-45VUHDV2.js.map} +0 -0
  328. /package/dist/{chunk-54FDG7K7.js.map → chunk-4DPLCYEK.js.map} +0 -0
  329. /package/dist/{chunk-KO3YBHOV.js.map → chunk-4IWEPQEV.js.map} +0 -0
  330. /package/dist/{chunk-H7BZBSP6.js.map → chunk-52II7PB6.js.map} +0 -0
  331. /package/dist/{chunk-K6RTQZA4.js.map → chunk-6DANGCMO.js.map} +0 -0
  332. /package/dist/{chunk-2IW5PT7U.js.map → chunk-6KFFRMPQ.js.map} +0 -0
  333. /package/dist/{chunk-MSIRVXQ3.js.map → chunk-73YVMZ7D.js.map} +0 -0
  334. /package/dist/{chunk-A7BM2JTH.js.map → chunk-AJHNXIQU.js.map} +0 -0
  335. /package/dist/{chunk-2D7BTSDW.js.map → chunk-ANZZLRUX.js.map} +0 -0
  336. /package/dist/{chunk-YOEYL6CH.js.map → chunk-AP3TSV6S.js.map} +0 -0
  337. /package/dist/{chunk-GQZSGXR6.js.map → chunk-BATPQNRG.js.map} +0 -0
  338. /package/dist/{chunk-D5SSBCYY.js.map → chunk-BLGQUZFB.js.map} +0 -0
  339. /package/dist/{chunk-BDN3TDGV.js.map → chunk-BLMTOIBK.js.map} +0 -0
  340. /package/dist/{chunk-R45L4TV6.js.map → chunk-BUKK5SWA.js.map} +0 -0
  341. /package/dist/{chunk-TFLPDNQV.js.map → chunk-CCU7URRP.js.map} +0 -0
  342. /package/dist/{chunk-2XJZLSXE.js.map → chunk-CMRHXPLQ.js.map} +0 -0
  343. /package/dist/{chunk-BK4FBHZ3.js.map → chunk-D6THW7PA.js.map} +0 -0
  344. /package/dist/{chunk-BFBS7OFQ.js.map → chunk-DC55RIEZ.js.map} +0 -0
  345. /package/dist/{chunk-VKDAJJQP.js.map → chunk-DI5ET2IE.js.map} +0 -0
  346. /package/dist/{chunk-QZBTIYKD.js.map → chunk-DM4RRTN7.js.map} +0 -0
  347. /package/dist/{chunk-EBOHFZI4.js.map → chunk-E5QEF3JO.js.map} +0 -0
  348. /package/dist/{chunk-K3B72DKJ.js.map → chunk-E7V5AK7I.js.map} +0 -0
  349. /package/dist/{chunk-BV2BRHD4.js.map → chunk-FHGWKODN.js.map} +0 -0
  350. /package/dist/{chunk-HPFVZPRH.js.map → chunk-FK4WC6VK.js.map} +0 -0
  351. /package/dist/{chunk-VLMBSGQF.js.map → chunk-FRM4UTDR.js.map} +0 -0
  352. /package/dist/{chunk-LTDLJCMQ.js.map → chunk-FZPDRCOB.js.map} +0 -0
  353. /package/dist/{chunk-ZJJFUEPO.js.map → chunk-GBOYOL5L.js.map} +0 -0
  354. /package/dist/{chunk-YUTG35M4.js.map → chunk-GJVGFKOU.js.map} +0 -0
  355. /package/dist/{chunk-K36RMINQ.js.map → chunk-GOZXQNIM.js.map} +0 -0
  356. /package/dist/{chunk-HWHNQVRV.js.map → chunk-GYNRNKMW.js.map} +0 -0
  357. /package/dist/{chunk-KFLD7ZZG.js.map → chunk-IUKYFTGW.js.map} +0 -0
  358. /package/dist/{chunk-BNQJKWWZ.js.map → chunk-K3FH3LAQ.js.map} +0 -0
  359. /package/dist/{chunk-FJ2VJTGA.js.map → chunk-KHZVAISH.js.map} +0 -0
  360. /package/dist/{chunk-WHQXWFH7.js.map → chunk-LT3CMO65.js.map} +0 -0
  361. /package/dist/{chunk-NUSAAN75.js.map → chunk-MHSBOEH7.js.map} +0 -0
  362. /package/dist/{chunk-KJCL2FZO.js.map → chunk-MNYQVYJ4.js.map} +0 -0
  363. /package/dist/{chunk-RZH5RVJC.js.map → chunk-MTTYIXSU.js.map} +0 -0
  364. /package/dist/{chunk-FVRTCYZ3.js.map → chunk-MVOJDLXS.js.map} +0 -0
  365. /package/dist/{chunk-KESJ5XX3.js.map → chunk-MYCCO6AA.js.map} +0 -0
  366. /package/dist/{chunk-66JOLMWS.js.map → chunk-MZCGHPIJ.js.map} +0 -0
  367. /package/dist/{chunk-B4ZU34JQ.js.map → chunk-N6II66E5.js.map} +0 -0
  368. /package/dist/{chunk-BK7AJUWB.js.map → chunk-NIV7RPWQ.js.map} +0 -0
  369. /package/dist/{chunk-TLHF75OR.js.map → chunk-O55AGDTI.js.map} +0 -0
  370. /package/dist/{chunk-GGJFRVRD.js.map → chunk-OMYRPMQC.js.map} +0 -0
  371. /package/dist/{chunk-XHGYZRVW.js.map → chunk-OSTFDWZJ.js.map} +0 -0
  372. /package/dist/{chunk-BXLOS5AJ.js.map → chunk-OWHERGF2.js.map} +0 -0
  373. /package/dist/{chunk-WC475JOG.js.map → chunk-P225W4RQ.js.map} +0 -0
  374. /package/dist/{chunk-TFA6HPOA.js.map → chunk-QCP75JGO.js.map} +0 -0
  375. /package/dist/{chunk-JRVMCZE3.js.map → chunk-QEYTRFR3.js.map} +0 -0
  376. /package/dist/{chunk-IZJQNDGI.js.map → chunk-R6HMAIKM.js.map} +0 -0
  377. /package/dist/{chunk-VUROWXFL.js.map → chunk-RNGDKFY2.js.map} +0 -0
  378. /package/dist/{chunk-FF6N7BXC.js.map → chunk-S45INSV3.js.map} +0 -0
  379. /package/dist/{chunk-2RUTN7PR.js.map → chunk-S7GJNA3K.js.map} +0 -0
  380. /package/dist/{chunk-RJWL3RHB.js.map → chunk-TFEHSPGQ.js.map} +0 -0
  381. /package/dist/{chunk-T77AX6DG.js.map → chunk-TQVECGUQ.js.map} +0 -0
  382. /package/dist/{chunk-OQSSQOKQ.js.map → chunk-TV65NR7L.js.map} +0 -0
  383. /package/dist/{chunk-HQPFACXO.js.map → chunk-UH3FPX3L.js.map} +0 -0
  384. /package/dist/{chunk-4EACCIPO.js.map → chunk-UHPHH74F.js.map} +0 -0
  385. /package/dist/{chunk-DQEMWVMT.js.map → chunk-UVYI6VIX.js.map} +0 -0
  386. /package/dist/{chunk-6QYPL6NI.js.map → chunk-V45TEVDA.js.map} +0 -0
  387. /package/dist/{chunk-GCAQ3VUJ.js.map → chunk-VCVEMQJK.js.map} +0 -0
  388. /package/dist/{chunk-6L63IN3M.js.map → chunk-VEFGHRKV.js.map} +0 -0
  389. /package/dist/{chunk-E33UJHY3.js.map → chunk-WFDWLWDK.js.map} +0 -0
  390. /package/dist/{chunk-KQLBXUUQ.js.map → chunk-WYG5UNDY.js.map} +0 -0
  391. /package/dist/{chunk-MPGLM2VU.js.map → chunk-X7REO4CH.js.map} +0 -0
  392. /package/dist/{chunk-FZVROZKC.js.map → chunk-XVZA6VXC.js.map} +0 -0
  393. /package/dist/{chunk-WV254W4V.js.map → chunk-Y3IYKNBY.js.map} +0 -0
  394. /package/dist/{chunk-FUOAIJKQ.js.map → chunk-Y43X3E24.js.map} +0 -0
  395. /package/dist/{chunk-3T3VHXUH.js.map → chunk-YAAKVTZA.js.map} +0 -0
  396. /package/dist/{chunk-VPEIWMUT.js.map → chunk-YLTU2DC4.js.map} +0 -0
  397. /package/dist/{chunk-65SKM4BF.js.map → chunk-YNBMBL4C.js.map} +0 -0
  398. /package/dist/{chunk-PJYK7HH2.js.map → chunk-ZWTHVUT5.js.map} +0 -0
  399. /package/dist/{first-start-migration-QVD22WWS.js.map → first-start-migration-A6PHIBUQ.js.map} +0 -0
@@ -20,8 +20,9 @@
20
20
  * extraction is not durably finished, so a kill at any point loses
21
21
  * nothing and the next run resumes.
22
22
  * - A live buffer owner that must persist a claimed session hands its
23
- * in-memory entry INTO the claim (merge, identity-deduped) instead of
24
- * writing a session file — it can never resurrect claimed turns.
23
+ * in-memory entry INTO the claim (identity-aware, occurrence-preserving
24
+ * merge) instead of writing a session file — it can never resurrect
25
+ * turns the drain already finished.
25
26
  * - Claims are idempotent: re-claiming, double-draining, and two
26
27
  * concurrent drainers are all safe (per-key mutation lock + exclusive
27
28
  * lease; the lock also fences lease takeover and progress writes).
@@ -38,7 +39,7 @@ import { mkdir, readdir, rename, unlink, writeFile } from "node:fs/promises";
38
39
  import path from "node:path";
39
40
 
40
41
  import { bufferSessionFileName, type BufferFileIo } from "./buffer-session-files.js";
41
- import { bufferTurnsEqual, mergeBufferTurnsByIdentity, subtractBufferTurns } from "../buffer-turn-helpers.js";
42
+ import { bufferTurnIdentityKey, subtractBufferTurns, type DrainedTurnSets } from "../buffer-turn-helpers.js";
42
43
  import { log } from "../logger.js";
43
44
  import { withHeldFileLock } from "../utils/serialize-mutations.js";
44
45
  import type { BufferEntryState, BufferTurn } from "../types.js";
@@ -47,11 +48,12 @@ import type { BufferEntryState, BufferTurn } from "../types.js";
47
48
  export interface BufferSessionSaveOutcome {
48
49
  /** "saved": the session file holds the entry. "handed_over": the entry moved into a drain claim. */
49
50
  status: "saved" | "handed_over";
50
- /** Turns a drain already extracted (claim tombstone): the owner subtracts them from its copy. */
51
- drainedTurns?: BufferTurn[];
51
+ /** Tombstoned turn identities by their original active/retained list. */
52
+ drainedTurns?: DrainedTurnSets;
52
53
  }
53
54
 
54
55
  export interface DrainLease {
56
+ /** Unique fencing token for one scheduler/process identity; never reuse it after restart. */
55
57
  ownerId: string;
56
58
  pid: number;
57
59
  /** Host the owner runs on: a takeover may only judge liveness on that host. */
@@ -74,6 +76,13 @@ export interface DrainClaimFile {
74
76
  drained?: BufferTurn[];
75
77
  /** Same tombstone for turns the drain extracted from `retainedTurns`. */
76
78
  drainedRetained?: BufferTurn[];
79
+ /**
80
+ * Pending turns whose only copy sits in the other list than the one they
81
+ * were claimed into (active to retained or back). When such a turn is
82
+ * extracted, its tombstone goes to BOTH lists: a live owner still holds
83
+ * the stale copy in the list the turn started in.
84
+ */
85
+ movedTurns?: BufferTurn[];
77
86
  }
78
87
 
79
88
  export type ClaimFromSessionOutcome = "claimed" | "empty" | "already_claimed";
@@ -95,29 +104,80 @@ function emptyEntry(): BufferEntryState {
95
104
  return { turns: [], lastExtractionAt: null, extractionCount: 0 };
96
105
  }
97
106
 
98
- /** Identity-dedupe `incoming` turns into `existing` (they are the same turns a
99
- * live owner still held after claiming; only genuinely NEW turns append). */
107
+ function turnCounts(turns: readonly BufferTurn[]): Map<string, number> {
108
+ const counts = new Map<string, number>();
109
+ for (const turn of turns) {
110
+ const key = bufferTurnIdentityKey(turn);
111
+ counts.set(key, (counts.get(key) ?? 0) + 1);
112
+ }
113
+ return counts;
114
+ }
115
+
100
116
  function mergeTurns(existing: BufferTurn[], incoming: readonly BufferTurn[]): BufferTurn[] {
101
- return mergeBufferTurnsByIdentity(existing, incoming);
117
+ const merged = [...existing];
118
+ const existingCounts = turnCounts(existing);
119
+ const incomingCounts = new Map<string, number>();
120
+ for (const turn of incoming) {
121
+ const key = bufferTurnIdentityKey(turn);
122
+ const count = (incomingCounts.get(key) ?? 0) + 1;
123
+ incomingCounts.set(key, count);
124
+ if (count > (existingCounts.get(key) ?? 0)) merged.push(turn);
125
+ }
126
+ return merged;
127
+ }
128
+
129
+ function preserveSourceOccurrences(
130
+ target: BufferTurn[],
131
+ source: readonly BufferTurn[],
132
+ identity: string,
133
+ ): void {
134
+ const targetCount = turnCounts(target).get(identity) ?? 0;
135
+ let sourceCount = 0;
136
+ for (const turn of source) {
137
+ if (bufferTurnIdentityKey(turn) !== identity) continue;
138
+ sourceCount += 1;
139
+ if (sourceCount > targetCount) target.push(turn);
140
+ }
141
+ }
142
+
143
+ function appendUnmatched(
144
+ snapshot: readonly BufferTurn[],
145
+ matchedCounts: ReadonlyMap<string, number>,
146
+ unmatched: BufferTurn[],
147
+ ): void {
148
+ const remaining = new Map(matchedCounts);
149
+ for (const turn of snapshot) {
150
+ const key = bufferTurnIdentityKey(turn);
151
+ const matched = remaining.get(key) ?? 0;
152
+ if (matched > 0) remaining.set(key, matched - 1);
153
+ else unmatched.push(turn);
154
+ }
155
+ }
156
+
157
+ function removeOneByIdentity(turns: BufferTurn[], identity: string): boolean {
158
+ const index = turns.findIndex((turn) => bufferTurnIdentityKey(turn) === identity);
159
+ if (index < 0) return false;
160
+ turns.splice(index, 1);
161
+ return true;
162
+ }
163
+
164
+ function takeByIdentity(source: readonly BufferTurn[], identity: string, count: number): BufferTurn[] {
165
+ return source.filter((turn) => bufferTurnIdentityKey(turn) === identity).slice(0, count);
102
166
  }
103
167
 
104
168
  function mergeEntries(claim: DrainClaimFile, incoming: BufferEntryState): DrainClaimFile {
105
169
  const entry = emptyEntry();
106
- // Turns already extracted stay out of the pending entry even when the
107
- // owner's incoming copy is stale and still carries them (either tombstone
108
- // list counts — a turn in either one left the claim for good).
109
- const gone = [...(claim.drained ?? []), ...(claim.drainedRetained ?? [])];
170
+ // The claim's own lists already exclude what was extracted; each tombstone
171
+ // applies only to the incoming copy in the list the turn was extracted from.
110
172
  entry.turns = mergeTurns(
111
- subtractBufferTurns(claim.entry.turns, gone),
112
- subtractBufferTurns(incoming.turns, gone),
173
+ claim.entry.turns,
174
+ subtractBufferTurns(incoming.turns, claim.drained ?? []),
113
175
  );
114
- const retained = [
115
- ...subtractBufferTurns(claim.entry.retainedTurns ?? [], gone),
116
- ...subtractBufferTurns(incoming.retainedTurns ?? [], gone),
117
- ];
118
- // SAFETY: an empty accumulator of the same element type.
119
- const dedupedRetained = mergeTurns([], retained);
120
- if (dedupedRetained.length > 0) entry.retainedTurns = dedupedRetained;
176
+ entry.retainedTurns = mergeTurns(
177
+ claim.entry.retainedTurns ?? [],
178
+ subtractBufferTurns(incoming.retainedTurns ?? [], claim.drainedRetained ?? []),
179
+ );
180
+ if (entry.retainedTurns.length === 0) delete entry.retainedTurns;
121
181
  const stamps = [claim.entry.lastExtractionAt, incoming.lastExtractionAt].filter(
122
182
  (value): value is string => value !== null && value !== undefined,
123
183
  );
@@ -163,6 +223,7 @@ const claimBodySchema = z.object({
163
223
  claimedAt: z.string().optional(),
164
224
  drained: z.array(z.unknown()).optional(),
165
225
  drainedRetained: z.array(z.unknown()).optional(),
226
+ movedTurns: z.array(z.unknown()).optional(),
166
227
  });
167
228
 
168
229
  function parseBody(raw: string): DrainClaimFile | null {
@@ -204,6 +265,10 @@ function parseBody(raw: string): DrainClaimFile | null {
204
265
  // SAFETY: same boundary as the turns above (this module writes the file).
205
266
  claim.drainedRetained = body.drainedRetained as BufferTurn[];
206
267
  }
268
+ if (Array.isArray(body.movedTurns)) {
269
+ // SAFETY: same boundary as the turns above (this module writes the file).
270
+ claim.movedTurns = body.movedTurns as BufferTurn[];
271
+ }
207
272
  return claim;
208
273
  }
209
274
 
@@ -232,6 +297,10 @@ function parseLease(raw: string): DrainLease | null {
232
297
  leaseMs: lease.leaseMs,
233
298
  };
234
299
  }
300
+ function leaseBelongsToProcess(lease: DrainLease | null, ownerId: string): boolean {
301
+ return lease !== null && lease.ownerId === ownerId && lease.pid === process.pid && lease.host === hostname();
302
+ }
303
+
235
304
 
236
305
  /** Claim bodies `listPage` may read per requested entry: bounds one scan's
237
306
  * I/O even when the directory is mostly empty tombstones. */
@@ -375,20 +444,19 @@ export class BufferDrainClaims {
375
444
  async (acquired) => {
376
445
  if (!acquired) return "busy";
377
446
  const existing = await this.readLease(key);
447
+ if (existing?.ownerId === lease.ownerId && existing.pid === lease.pid && existing.host === lease.host) {
448
+ // Reuse the token only from the same process identity.
449
+ await this.writeLease(key, { ...existing, heartbeatAt: new Date().toISOString(), leaseMs: lease.leaseMs });
450
+ return "adopted";
451
+ }
378
452
  if (existing && !isLeaseExpired(existing)) {
379
- if (existing.ownerId === lease.ownerId) {
380
- // The same drainer already holds it: renew the lease.
381
- await this.writeLease(key, { ...existing, heartbeatAt: new Date().toISOString(), leaseMs: lease.leaseMs });
382
- return "adopted";
383
- }
384
453
  if (isOwnerAlive(existing)) return "busy";
385
454
  // The owner died without releasing the claim (killed worker):
386
455
  // take it over now instead of waiting for the lease to expire.
387
456
  await unlink(this.leaseFileFor(key)).catch(() => {});
388
457
  } else if (existing) {
389
458
  // Expired lease: clear it before the exclusive create. We hold the
390
- // lock, so no concurrent adopter can interleave its own
391
- // unlink/create between these steps.
459
+ // lock, so no concurrent adopter can interleave its own unlink/create.
392
460
  await unlink(this.leaseFileFor(key)).catch(() => {});
393
461
  }
394
462
  try {
@@ -444,7 +512,7 @@ export class BufferDrainClaims {
444
512
  await withHeldFileLock(this.lockFileFor(key), { staleMs: CLAIM_LOCK_STALE_MS, maxWaitMs: CLAIM_LOCK_MAX_WAIT_MS }, async (acquired) => {
445
513
  if (!acquired) throw new Error(`buffer drain claim for ${key} is busy; progress write skipped`);
446
514
  const lease = await this.readLease(key);
447
- if (!lease || lease.ownerId !== ownerId) {
515
+ if (!leaseBelongsToProcess(lease, ownerId)) {
448
516
  throw new Error(`buffer drain claim for ${key} is no longer owned by ${ownerId}; progress write skipped`);
449
517
  }
450
518
  const claim = await this.readClaimBody(key);
@@ -452,39 +520,87 @@ export class BufferDrainClaims {
452
520
  const seenTurns = seen?.turns ?? claim.entry.turns;
453
521
  const seenRetained = seen?.retainedTurns ?? claim.entry.retainedTurns ?? [];
454
522
  const entryRetained = entry.retainedTurns ?? [];
455
- // Still held by the drainer: in its remaining turns OR retained for retry.
456
- const kept = (turn: BufferTurn): boolean =>
457
- entry.turns.some((held) => bufferTurnsEqual(held, turn)) || entryRetained.some((held) => bufferTurnsEqual(held, turn));
458
- // Progress: the snapshot turns that vanished from the entry were extracted.
459
- const drained = [...(claim.drained ?? [])];
460
- for (const turn of claim.entry.turns) {
461
- const extracted =
462
- seenTurns.some((snapshot) => bufferTurnsEqual(snapshot, turn)) && !kept(turn);
463
- if (!extracted) continue;
464
- if (drained.some((gone) => bufferTurnsEqual(gone, turn))) continue;
465
- drained.push(turn);
523
+ const seenActiveCounts = turnCounts(seenTurns);
524
+ const seenRetainedCounts = turnCounts(seenRetained);
525
+ const activeOutputCounts = turnCounts(entry.turns);
526
+ const retainedOutputCounts = turnCounts(entryRetained);
527
+ const matchedActiveCounts = new Map<string, number>();
528
+ const matchedRetainedCounts = new Map<string, number>();
529
+ const ambiguousIdentities = new Set<string>();
530
+ const identityKeys = new Set([
531
+ ...seenActiveCounts.keys(),
532
+ ...seenRetainedCounts.keys(),
533
+ ]);
534
+ const crossedCounts = new Map<string, number>();
535
+ for (const identity of identityKeys) {
536
+ const seenActive = seenActiveCounts.get(identity) ?? 0;
537
+ const seenRetainedCount = seenRetainedCounts.get(identity) ?? 0;
538
+ const activeOutput = activeOutputCounts.get(identity) ?? 0;
539
+ const retainedOutput = retainedOutputCounts.get(identity) ?? 0;
540
+ if (seenActive > 0 && seenRetainedCount > 0 && activeOutput + retainedOutput > 0) {
541
+ // Equal identities across source lists cannot reveal which original
542
+ // occurrence survived a cross-list move. Keep both source lists
543
+ // pending until a later progress write removes every copy.
544
+ matchedActiveCounts.set(identity, seenActive);
545
+ matchedRetainedCounts.set(identity, seenRetainedCount);
546
+ ambiguousIdentities.add(identity);
547
+ continue;
548
+ }
549
+ const matchedActive = Math.min(seenActive, activeOutput);
550
+ const matchedRetained = Math.min(seenRetainedCount, retainedOutput);
551
+ const activeMovedToRetained = Math.min(
552
+ seenActive - matchedActive,
553
+ retainedOutput - matchedRetained,
554
+ );
555
+ const retainedMovedToActive = Math.min(
556
+ seenRetainedCount - matchedRetained,
557
+ activeOutput - matchedActive,
558
+ );
559
+ matchedActiveCounts.set(identity, matchedActive + activeMovedToRetained);
560
+ matchedRetainedCounts.set(identity, matchedRetained + retainedMovedToActive);
561
+ const crossed = activeMovedToRetained + retainedMovedToActive;
562
+ if (crossed > 0) crossedCounts.set(identity, crossed);
563
+ }
564
+ // A pending copy that crossed lists stays tombstoned in its ORIGINAL
565
+ // list too once it is extracted. A copy that crosses back is home again.
566
+ const moved = [...(claim.movedTurns ?? [])];
567
+ for (const [identity, crossed] of crossedCounts) {
568
+ let returned = 0;
569
+ while (returned < crossed && removeOneByIdentity(moved, identity)) returned += 1;
570
+ moved.push(...takeByIdentity([...seenTurns, ...seenRetained], identity, crossed - returned));
466
571
  }
467
- const drainedRetained = [...(claim.drainedRetained ?? [])];
468
- for (const turn of claim.entry.retainedTurns ?? []) {
469
- const extracted =
470
- seenRetained.some((snapshot) => bufferTurnsEqual(snapshot, turn)) && !kept(turn);
471
- if (!extracted) continue;
472
- if (drained.some((gone) => bufferTurnsEqual(gone, turn)) || drainedRetained.some((gone) => bufferTurnsEqual(gone, turn))) continue;
473
- drainedRetained.push(turn);
572
+ const extractedActive: BufferTurn[] = [];
573
+ appendUnmatched(seenTurns, matchedActiveCounts, extractedActive);
574
+ const extractedRetained: BufferTurn[] = [];
575
+ appendUnmatched(seenRetained, matchedRetainedCounts, extractedRetained);
576
+ const drained = [...(claim.drained ?? []), ...extractedActive];
577
+ const drainedRetained = [...(claim.drainedRetained ?? []), ...extractedRetained];
578
+ for (const turn of extractedActive) {
579
+ if (removeOneByIdentity(moved, bufferTurnIdentityKey(turn))) drainedRetained.push(turn);
474
580
  }
475
- // Turns merged in after the drainer loaded stay pending, in extraction
476
- // order behind the drainer's own remainder.
477
- const late = claim.entry.turns.filter(
478
- (turn) =>
479
- !seenTurns.some((snapshot) => bufferTurnsEqual(snapshot, turn)) && !kept(turn),
480
- );
481
- const next: BufferEntryState = { ...entry, turns: [...entry.turns, ...late] };
482
- const lateRetained = (claim.entry.retainedTurns ?? []).filter(
483
- (turn) =>
484
- !seenRetained.some((snapshot) => bufferTurnsEqual(snapshot, turn)) && !kept(turn),
485
- );
486
- if (lateRetained.length > 0) next.retainedTurns = mergeTurns(entryRetained, lateRetained);
487
- await this.writeClaimBody(key, { ...claim, entry: next, drained, drainedRetained });
581
+ for (const turn of extractedRetained) {
582
+ if (removeOneByIdentity(moved, bufferTurnIdentityKey(turn))) drained.push(turn);
583
+ }
584
+ // Source occurrences of an ambiguous identity are restored BEFORE late
585
+ // arrivals are appended, so a late equal turn cannot stand in for them.
586
+ const keptTurns = [...entry.turns];
587
+ const keptRetained = [...entryRetained];
588
+ for (const identity of ambiguousIdentities) {
589
+ preserveSourceOccurrences(keptTurns, seenTurns, identity);
590
+ preserveSourceOccurrences(keptRetained, seenRetained, identity);
591
+ }
592
+ const late = subtractBufferTurns(claim.entry.turns, seenTurns);
593
+ const lateRetained = subtractBufferTurns(claim.entry.retainedTurns ?? [], seenRetained);
594
+ const next: BufferEntryState = { ...entry, turns: [...keptTurns, ...late] };
595
+ const nextRetained = [...keptRetained, ...lateRetained];
596
+ if (nextRetained.length > 0) next.retainedTurns = nextRetained;
597
+ await this.writeClaimBody(key, {
598
+ ...claim,
599
+ entry: next,
600
+ drained,
601
+ drainedRetained,
602
+ movedTurns: moved.length > 0 ? moved : undefined,
603
+ });
488
604
  });
489
605
  }
490
606
 
@@ -496,7 +612,7 @@ export class BufferDrainClaims {
496
612
  return await withHeldFileLock(this.lockFileFor(key), { staleMs: CLAIM_LOCK_STALE_MS, maxWaitMs: CLAIM_LOCK_MAX_WAIT_MS }, async (acquired) => {
497
613
  if (!acquired) return "ok";
498
614
  const lease = await this.readLease(key);
499
- if (!lease || lease.ownerId !== ownerId) return "lost";
615
+ if (!lease || !leaseBelongsToProcess(lease, ownerId)) return "lost";
500
616
  await this.writeLease(key, { ...lease, heartbeatAt: new Date().toISOString(), leaseMs });
501
617
  return "ok";
502
618
  });
@@ -504,9 +620,16 @@ export class BufferDrainClaims {
504
620
 
505
621
  /** Give the key back (content stays; the next run resumes it). */
506
622
  async releaseLease(key: string, ownerId: string): Promise<void> {
507
- const lease = await this.readLease(key);
508
- if (!lease || lease.ownerId !== ownerId) return;
509
- await unlink(this.leaseFileFor(key)).catch(() => {});
623
+ await withHeldFileLock(
624
+ this.lockFileFor(key),
625
+ { staleMs: CLAIM_LOCK_STALE_MS, maxWaitMs: CLAIM_LOCK_MAX_WAIT_MS },
626
+ async (acquired) => {
627
+ if (!acquired) return;
628
+ const lease = await this.readLease(key);
629
+ if (!leaseBelongsToProcess(lease, ownerId)) return;
630
+ await unlink(this.leaseFileFor(key)).catch(() => {});
631
+ },
632
+ );
510
633
  }
511
634
 
512
635
  /**
@@ -514,25 +637,48 @@ export class BufferDrainClaims {
514
637
  * extract. Non-empty claims stay (deferred content is never dropped here).
515
638
  */
516
639
  async ack(key: string, ownerId: string): Promise<AckOutcome> {
517
- const lease = await this.readLease(key);
518
- if (!lease || lease.ownerId !== ownerId) return "busy";
519
- const claim = await this.readClaimBody(key);
520
- if (!claim) {
521
- await unlink(this.leaseFileFor(key)).catch(() => {});
522
- return "acked";
523
- }
524
- if (claim.entry.turns.length > 0 || (claim.entry.retainedTurns?.length ?? 0) > 0) return "not_empty";
525
- // Keep the (empty) claim file as a TOMBSTONE: a live buffer owner that
526
- // still holds a stale copy subtracts the drained turns instead of
527
- // writing them back. The owner consumes the tombstone on its next save.
528
- await unlink(this.leaseFileFor(key)).catch(() => {});
529
- return "acked";
640
+ return await withHeldFileLock(
641
+ this.lockFileFor(key),
642
+ { staleMs: CLAIM_LOCK_STALE_MS, maxWaitMs: CLAIM_LOCK_MAX_WAIT_MS },
643
+ async (acquired) => {
644
+ if (!acquired) return "busy";
645
+ const lease = await this.readLease(key);
646
+ if (!leaseBelongsToProcess(lease, ownerId)) return "busy";
647
+ const claim = await this.readClaimBody(key);
648
+ if (!claim) {
649
+ await unlink(this.leaseFileFor(key)).catch(() => {});
650
+ return "acked";
651
+ }
652
+ if (claim.entry.turns.length > 0 || (claim.entry.retainedTurns?.length ?? 0) > 0) return "not_empty";
653
+ // Keep the empty claim as a tombstone until its live owner consumes it.
654
+ await unlink(this.leaseFileFor(key)).catch(() => {});
655
+ return "acked";
656
+ },
657
+ );
530
658
  }
531
659
 
532
- /** The turns a drain already extracted for `key` (tombstone content). */
533
- async drainedTurns(key: string): Promise<BufferTurn[]> {
660
+ /** Return extracted identities with their active/retained provenance intact. */
661
+ async drainedTurns(key: string): Promise<DrainedTurnSets> {
534
662
  const claim = await this.readClaimBody(key);
535
- return claim?.drained ?? [];
663
+ return {
664
+ turns: claim?.drained ?? [],
665
+ retainedTurns: claim?.drainedRetained ?? [],
666
+ };
667
+ }
668
+
669
+ /** Return this lease owner's progress, fenced against claim takeover. */
670
+ async drainedTurnsForOwner(key: string, ownerId: string): Promise<DrainedTurnSets | null> {
671
+ return await withHeldFileLock(
672
+ this.lockFileFor(key),
673
+ { staleMs: CLAIM_LOCK_STALE_MS, maxWaitMs: CLAIM_LOCK_MAX_WAIT_MS },
674
+ async (acquired) => {
675
+ if (!acquired) throw new Error(`buffer drain claim for ${key} is busy; cannot reconcile progress`);
676
+ const lease = await this.readLease(key);
677
+ if (!leaseBelongsToProcess(lease, ownerId)) return null;
678
+ const claim = await this.readClaimBody(key);
679
+ return claim ? { turns: claim.drained ?? [], retainedTurns: claim.drainedRetained ?? [] } : null;
680
+ }
681
+ );
536
682
  }
537
683
 
538
684
  /**
@@ -148,9 +148,9 @@ export class BufferSessionFiles {
148
148
  // the owner's fresh remainder back as a session file (the drained
149
149
  // turns are subtracted here too, so a caller that did not subtract
150
150
  // them cannot resurrect them) and release the tombstone.
151
- const fresh: BufferEntryState = { ...entry, turns: subtractBufferTurns(entry.turns, drained) };
151
+ const fresh: BufferEntryState = { ...entry, turns: subtractBufferTurns(entry.turns, drained.turns) };
152
152
  if (entry.retainedTurns && entry.retainedTurns.length > 0) {
153
- fresh.retainedTurns = subtractBufferTurns(entry.retainedTurns, drained);
153
+ fresh.retainedTurns = subtractBufferTurns(entry.retainedTurns, drained.retainedTurns);
154
154
  if (fresh.retainedTurns.length === 0) delete fresh.retainedTurns;
155
155
  }
156
156
  await this.assertDirSafe();
@@ -99,6 +99,15 @@ export class FactHashIndexNotAuthoritativeError extends Error {
99
99
  }
100
100
  }
101
101
 
102
+ /**
103
+ * One dedup lookup: whether the shared index holds the hash, and whether that
104
+ * answer can be trusted as a clean MISS. Both fields describe the same instant.
105
+ */
106
+ export interface ContentHashLookup {
107
+ readonly present: boolean;
108
+ readonly authoritative: boolean;
109
+ }
110
+
102
111
  type DiskFingerprint = {
103
112
  mtimeMs: number;
104
113
  size: number;
package/src/storage.ts CHANGED
@@ -215,6 +215,7 @@ import {
215
215
  FactHashIndexNotAuthoritativeError,
216
216
  CONTENT_HASH_INDEX_RETRY_MAX_DELAY_MS,
217
217
  type ContentHashIndexLockOptions,
218
+ type ContentHashLookup,
218
219
  } from "./storage/content-hash-index.js";
219
220
  import {
220
221
  TombstoneBlockedCaptureIndexHost,
@@ -4172,6 +4173,20 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
4172
4173
  return await this.ensureFactHashIndexAuthoritative();
4173
4174
  }
4174
4175
 
4176
+ /**
4177
+ * Shared-index membership of `content` plus the authority of that answer,
4178
+ * read in one synchronous step after the freshness check or locked rebuild.
4179
+ * A separate probe and `isFactContentHashAuthoritative()` call can pair a
4180
+ * stale MISS with authority from another moment, because each one may rebuild
4181
+ * the index (#3312). Trust a MISS only when `authoritative`; otherwise
4182
+ * confirm against the corpus.
4183
+ */
4184
+ async lookupContentHash(content: string): Promise<ContentHashLookup> {
4185
+ const index = await this.getFactHashIndex();
4186
+ await this.ensureFactHashIndexAuthoritative();
4187
+ return { present: index.has(content), authoritative: this.factHashIndexAuthoritative === true };
4188
+ }
4189
+
4175
4190
  async writeArtifact(
4176
4191
  quote: string,
4177
4192
  options: {
@@ -0,0 +1,39 @@
1
+ import { gunzipSync } from "node:zlib";
2
+
3
+ import { isErrnoCode } from "../utils/errno.js";
4
+
5
+ /**
6
+ * Hard cap on decompressed capsule JSON for import, merge and inspect. `gunzipSync` without
7
+ * `maxOutputLength` allocates whatever the stream claims, so a small
8
+ * highly-compressible gzip can OOM the process. 64 MiB is enough for ordinary
9
+ * capsules and stops classic bomb shapes at the zlib edge.
10
+ */
11
+ export const CAPSULE_IMPORT_MAX_GUNZIP_BYTES = 64 * 1024 * 1024;
12
+
13
+ /**
14
+ * Gunzip a capsule archive buffer with a hard decompressed-size cap.
15
+ * `caller` is the public API name used in error messages (`importCapsule`,
16
+ * `mergeCapsule`, `capsule inspect`).
17
+ */
18
+ export function gunzipCapsuleArchive(
19
+ raw: Buffer,
20
+ caller: string,
21
+ archiveAbs: string,
22
+ ): string {
23
+ try {
24
+ return gunzipSync(raw, {
25
+ maxOutputLength: CAPSULE_IMPORT_MAX_GUNZIP_BYTES,
26
+ }).toString("utf-8");
27
+ } catch (cause) {
28
+ if (isErrnoCode(cause, "ERR_BUFFER_TOO_LARGE")) {
29
+ throw new Error(
30
+ `${caller}: archive decompressed size exceeds limit of ${CAPSULE_IMPORT_MAX_GUNZIP_BYTES} bytes`,
31
+ cause instanceof Error ? { cause } : undefined,
32
+ );
33
+ }
34
+ throw new Error(
35
+ `${caller}: archive is not a valid gzip file: ${archiveAbs}`,
36
+ cause instanceof Error ? { cause } : undefined,
37
+ );
38
+ }
39
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * capsule gunzip bounds — Finding C3 (gzip bomb / unbounded gunzip).
3
+ *
4
+ * importCapsule/mergeCapsule previously called gunzipSync with no
5
+ * maxOutputLength, so a small highly-compressible archive could expand into
6
+ * multi-gigabyte buffers. These tests pin the refusal of a real gzip-bomb
7
+ * shape and keep a normal capsule import green.
8
+ */
9
+
10
+ import assert from "node:assert/strict";
11
+ import test from "node:test";
12
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
13
+ import path from "node:path";
14
+ import { gzipSync } from "node:zlib";
15
+
16
+ import { makeTempDir } from "../testing/tmp-dir.js";
17
+ import { exportCapsule } from "./capsule-export.js";
18
+ import { CAPSULE_IMPORT_MAX_GUNZIP_BYTES } from "./capsule-gunzip.js";
19
+ import { importCapsule } from "./capsule-import.js";
20
+
21
+ async function writeFact(dir: string, id: string, body: string): Promise<void> {
22
+ await mkdir(path.join(dir, "facts"), { recursive: true });
23
+ await writeFile(
24
+ path.join(dir, "facts", `${id}.md`),
25
+ `---\nid: ${id}\n---\n${body}\n`,
26
+ );
27
+ }
28
+
29
+ test("importCapsule refuses a small gzip that expands past the output bound", async () => {
30
+ const dir = await makeTempDir("cap-gunzip-bomb-");
31
+ // Highly compressible payload one byte over the decompressed cap. gzip of
32
+ // 64 MiB of identical bytes stays ~65 KiB — a classic bomb shape.
33
+ const bombPayload = Buffer.alloc(CAPSULE_IMPORT_MAX_GUNZIP_BYTES + 1, 0x41);
34
+ const archivePath = path.join(dir, "bomb.capsule.json.gz");
35
+ await writeFile(archivePath, gzipSync(bombPayload));
36
+
37
+ const root = await makeTempDir("cap-gunzip-bomb-root-");
38
+ await assert.rejects(
39
+ () =>
40
+ importCapsule({
41
+ archivePath,
42
+ root,
43
+ mode: "skip",
44
+ }),
45
+ /importCapsule: archive decompressed size exceeds limit of \d+ bytes/,
46
+ );
47
+ });
48
+
49
+ test("importCapsule still imports a normal capsule archive", async () => {
50
+ const srcDir = await makeTempDir("cap-gunzip-ok-src-");
51
+ const dstDir = await makeTempDir("cap-gunzip-ok-dst-");
52
+ const outDir = await makeTempDir("cap-gunzip-ok-out-");
53
+
54
+ await writeFact(srcDir, "fact-ok", "Normal capsule fact.");
55
+ const exported = await exportCapsule({
56
+ name: "gunzip-ok-capsule",
57
+ root: srcDir,
58
+ outDir,
59
+ pluginVersion: "0.0.0-test",
60
+ now: 1_700_000_000_000,
61
+ });
62
+
63
+ const result = await importCapsule({
64
+ archivePath: exported.archivePath,
65
+ root: dstDir,
66
+ mode: "skip",
67
+ });
68
+
69
+ assert.equal(result.imported.length, 1);
70
+ assert.equal(result.skipped.length, 0);
71
+ const content = await readFile(path.join(dstDir, "facts", "fact-ok.md"), "utf-8");
72
+ assert.match(content, /Normal capsule fact\./);
73
+ });
@@ -1,7 +1,6 @@
1
1
  import { lstat, mkdir, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- import { gunzipSync } from "node:zlib";
5
4
  import { bumpMemoryCorpusVersionForDir } from "../memory-corpus-version.js";
6
5
  import {
7
6
  HistoricalEntityCanonicalIdCache,
@@ -31,6 +30,7 @@ import {
31
30
  type ExportMemoryRecordV1,
32
31
  } from "./types.js";
33
32
  import { isEncryptedCapsuleFile, decryptCapsuleFileInMemory } from "./capsule-crypto.js";
33
+ import { gunzipCapsuleArchive } from "./capsule-gunzip.js";
34
34
 
35
35
  /**
36
36
  * Conflict-resolution mode for {@link importCapsule}. Inverse of the
@@ -197,7 +197,7 @@ export async function importCapsule(
197
197
  } else {
198
198
  raw = await readFile(archiveAbs);
199
199
  }
200
- const json = gunzipSync(raw).toString("utf-8");
200
+ const json = gunzipCapsuleArchive(raw, "importCapsule", archiveAbs);
201
201
  let parsedJson: unknown;
202
202
  try {
203
203
  parsedJson = JSON.parse(json);
@@ -0,0 +1,34 @@
1
+ /**
2
+ * capsule-merge gunzip bounds — Finding C3 companion to capsule-import.test.ts.
3
+ *
4
+ * mergeCapsule reads the same gzip archive shape as importCapsule; the shared
5
+ * helper must refuse a real bomb with a clear oversize error.
6
+ */
7
+
8
+ import assert from "node:assert/strict";
9
+ import test from "node:test";
10
+ import { writeFile } from "node:fs/promises";
11
+ import path from "node:path";
12
+ import { gzipSync } from "node:zlib";
13
+
14
+ import { makeTempDir } from "../testing/tmp-dir.js";
15
+ import { CAPSULE_IMPORT_MAX_GUNZIP_BYTES } from "./capsule-gunzip.js";
16
+ import { mergeCapsule } from "./capsule-merge.js";
17
+
18
+ test("mergeCapsule refuses a small gzip that expands past the output bound", async () => {
19
+ const dir = await makeTempDir("cap-merge-gunzip-bomb-");
20
+ const bombPayload = Buffer.alloc(CAPSULE_IMPORT_MAX_GUNZIP_BYTES + 1, 0x41);
21
+ const archivePath = path.join(dir, "bomb.capsule.json.gz");
22
+ await writeFile(archivePath, gzipSync(bombPayload));
23
+
24
+ const root = await makeTempDir("cap-merge-gunzip-bomb-root-");
25
+ await assert.rejects(
26
+ () =>
27
+ mergeCapsule({
28
+ sourceArchive: archivePath,
29
+ targetRoot: root,
30
+ conflictMode: "skip-conflicts",
31
+ }),
32
+ /mergeCapsule: archive decompressed size exceeds limit of \d+ bytes/,
33
+ );
34
+ });