@remnic/core 10.0.59 → 10.0.61

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 (370) hide show
  1. package/dist/access-admin-ops-surface.js +53 -50
  2. package/dist/access-authorization-probe.js +54 -51
  3. package/dist/access-boundary.js +53 -50
  4. package/dist/access-cli.js +143 -140
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-extraction-force-flush.js +53 -50
  7. package/dist/access-http-briefing.js +54 -51
  8. package/dist/access-http-query.js +55 -52
  9. package/dist/access-http.js +76 -73
  10. package/dist/access-identity-continuity-surface.js +53 -50
  11. package/dist/access-lcm-surface.js +53 -50
  12. package/dist/access-mcp.js +64 -61
  13. package/dist/access-namespace-preflight.js +53 -50
  14. package/dist/access-observe-write-category.js +10 -10
  15. package/dist/access-observe-write-surface.js +53 -50
  16. package/dist/access-offline-manifest.js +7 -7
  17. package/dist/access-operations-batch.js +56 -53
  18. package/dist/access-operations.js +63 -60
  19. package/dist/access-recall-concurrency.js +53 -50
  20. package/dist/access-recall-response.js +53 -50
  21. package/dist/access-recall-surface.js +53 -50
  22. package/dist/access-schema.js +9 -9
  23. package/dist/access-service.js +53 -50
  24. package/dist/access-surface-catalog.js +55 -52
  25. package/dist/access-surface-catalog.js.map +1 -1
  26. package/dist/active-recall.js +24 -20
  27. package/dist/active-recall.js.map +1 -1
  28. package/dist/adapters/index.js +7 -7
  29. package/dist/adapters/registry.js +3 -3
  30. package/dist/briefing.js +16 -12
  31. package/dist/buffer-persistence.d.ts +14 -3
  32. package/dist/buffer-persistence.d.ts.map +1 -1
  33. package/dist/buffer-persistence.js +1 -1
  34. package/dist/buffer-turn-helpers.d.ts +7 -0
  35. package/dist/buffer-turn-helpers.d.ts.map +1 -1
  36. package/dist/buffer-turn-helpers.js +5 -3
  37. package/dist/buffer.d.ts +2 -0
  38. package/dist/buffer.d.ts.map +1 -1
  39. package/dist/buffer.js +7 -7
  40. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-GWVG7LGC.js} +2 -2
  41. package/dist/causal-behavior.js +2 -2
  42. package/dist/causal-chain.js +2 -2
  43. package/dist/causal-consolidation.js +19 -15
  44. package/dist/causal-consolidation.js.map +1 -1
  45. package/dist/causal-retrieval.js +2 -2
  46. package/dist/causal-trajectory-graph.js +2 -2
  47. package/dist/causal-trajectory.js +1 -1
  48. package/dist/{chunk-ZAZZOQAQ.js → chunk-23OEPU3K.js} +2 -2
  49. package/dist/{chunk-XDKEHZUC.js → chunk-264B57GV.js} +2 -2
  50. package/dist/{chunk-5U2BBF7E.js → chunk-46U6L5DM.js} +20 -10
  51. package/dist/chunk-46U6L5DM.js.map +1 -0
  52. package/dist/chunk-4LZ7SVKM.js +211 -0
  53. package/dist/chunk-4LZ7SVKM.js.map +1 -0
  54. package/dist/{chunk-NRGQ5HJO.js → chunk-4P4VLPGB.js} +1 -1
  55. package/dist/{chunk-GIRWNTMV.js → chunk-4PG5R6BJ.js} +2 -2
  56. package/dist/{chunk-52II7PB6.js → chunk-645WOU5I.js} +4 -4
  57. package/dist/{chunk-RG3I2663.js → chunk-64JLBFCQ.js} +2 -2
  58. package/dist/{chunk-G3T6YMBO.js → chunk-64S6X4IU.js} +7 -7
  59. package/dist/{chunk-WASVDS6G.js → chunk-6HTEPYOI.js} +4 -4
  60. package/dist/{chunk-6AEKSOWV.js → chunk-6I6DFULH.js} +8 -8
  61. package/dist/{chunk-DJR37Q37.js → chunk-6T4CMKNP.js} +2 -2
  62. package/dist/{chunk-RHJ5VGB3.js → chunk-73A2DDDR.js} +2 -2
  63. package/dist/{chunk-4B73TQQV.js → chunk-7AKPLEJV.js} +2 -2
  64. package/dist/{chunk-UHH67NLU.js → chunk-7OKYO2UC.js} +2 -2
  65. package/dist/{chunk-KAL5SXPP.js → chunk-7ULSP7AU.js} +6 -6
  66. package/dist/{chunk-USNU3BEQ.js → chunk-A4FZLM6Q.js} +2 -2
  67. package/dist/{chunk-VV73XT3A.js → chunk-BJLCV2P3.js} +2 -2
  68. package/dist/{chunk-KQAFEZQX.js → chunk-BUKK5SWA.js} +2 -2
  69. package/dist/{chunk-DJFULNXK.js → chunk-C2AYUXEF.js} +2 -2
  70. package/dist/{chunk-QY24FSXT.js → chunk-D3BYJSHX.js} +51 -51
  71. package/dist/{chunk-JZRQY7ZQ.js → chunk-D4WR56FR.js} +2 -2
  72. package/dist/{chunk-3E3Z6UA3.js → chunk-DMYNM3WG.js} +124 -97
  73. package/dist/chunk-DMYNM3WG.js.map +1 -0
  74. package/dist/{chunk-FB4JQEFS.js → chunk-DP22K7IC.js} +2 -2
  75. package/dist/{chunk-EJFOGYF3.js → chunk-DTQJ7BRN.js} +3 -3
  76. package/dist/{chunk-MLUPSZFK.js → chunk-DZL5PBEJ.js} +2 -2
  77. package/dist/{chunk-JD5F5MYN.js → chunk-EGCC5OJE.js} +3 -3
  78. package/dist/{chunk-IQSRVR6Z.js → chunk-EJ4RS7I7.js} +2 -2
  79. package/dist/{chunk-WBN66HPI.js → chunk-ELHJK7LM.js} +6 -6
  80. package/dist/{chunk-EH46DHBU.js → chunk-ENAPZQA6.js} +2 -2
  81. package/dist/{chunk-LLG5VNK7.js → chunk-F2UAAI6B.js} +2 -2
  82. package/dist/{chunk-YZNTAOPZ.js → chunk-FKNNWH7P.js} +2 -2
  83. package/dist/{chunk-AAANTXCQ.js → chunk-FZVZYU5C.js} +2 -2
  84. package/dist/{chunk-ON77FQXH.js → chunk-G6GMZJQ4.js} +2 -2
  85. package/dist/{chunk-OL7A7NKH.js → chunk-GNP4BV4Z.js} +2 -2
  86. package/dist/{chunk-GDYLZ6FR.js → chunk-HJ3UTGIW.js} +3 -3
  87. package/dist/{chunk-CCL2OXFO.js → chunk-HSHLDSPX.js} +3 -3
  88. package/dist/{chunk-R4BR62UO.js → chunk-HWMGQDZI.js} +2 -2
  89. package/dist/{chunk-BPIAXMGC.js → chunk-IIUEHIQ2.js} +2 -2
  90. package/dist/{chunk-RTJJPW3C.js → chunk-ILMB3X4X.js} +5 -5
  91. package/dist/{chunk-RFT4OPYI.js → chunk-IPN67KVR.js} +2 -2
  92. package/dist/chunk-JCL7RO2G.js +674 -0
  93. package/dist/chunk-JCL7RO2G.js.map +1 -0
  94. package/dist/{chunk-IEW6NDHX.js → chunk-JGSWWD33.js} +9 -9
  95. package/dist/{chunk-A3UFV4K3.js → chunk-JIYJIH5N.js} +2 -2
  96. package/dist/{chunk-4G2A7MH3.js → chunk-JKUVPVKR.js} +3 -3
  97. package/dist/{chunk-YJHOVA42.js → chunk-JL5FHZ7O.js} +30 -6
  98. package/dist/chunk-JL5FHZ7O.js.map +1 -0
  99. package/dist/{chunk-VCLNU6MY.js → chunk-L2HSSVVD.js} +2 -2
  100. package/dist/{chunk-U2A56LLF.js → chunk-L334PSLW.js} +8 -8
  101. package/dist/{chunk-HQUYENKK.js → chunk-LIUI46BX.js} +3 -3
  102. package/dist/{chunk-ERMT5WQM.js → chunk-MKE5DP3S.js} +2 -2
  103. package/dist/{chunk-4OFXLQJD.js → chunk-MPDLLLOG.js} +8 -3
  104. package/dist/chunk-MPDLLLOG.js.map +1 -0
  105. package/dist/{chunk-6BO27AWQ.js → chunk-MPJ7VHHA.js} +2 -2
  106. package/dist/{chunk-WXBVGKS6.js → chunk-N7RAABFM.js} +182 -421
  107. package/dist/chunk-N7RAABFM.js.map +1 -0
  108. package/dist/{chunk-KMJDH4KX.js → chunk-NSUGAIIB.js} +16 -16
  109. package/dist/{chunk-7NT3FKAF.js → chunk-NXHBQC2O.js} +2 -2
  110. package/dist/{chunk-JEX2JL3U.js → chunk-OH55F76C.js} +2 -2
  111. package/dist/{chunk-37VTMUAL.js → chunk-OUJ4DVR5.js} +2 -2
  112. package/dist/{chunk-2NLLXCJG.js → chunk-OWHERGF2.js} +2 -2
  113. package/dist/{chunk-2IVK5IDY.js → chunk-P5F7ISLV.js} +2 -2
  114. package/dist/{chunk-PM4PKC3Y.js → chunk-PNVQNAJM.js} +3 -3
  115. package/dist/{chunk-FXWX7VD3.js → chunk-PRYDNBCO.js} +12 -12
  116. package/dist/{chunk-YDDLSGYE.js → chunk-PUTH5REB.js} +2 -2
  117. package/dist/{chunk-GFAEKSFR.js → chunk-QBK3IM6W.js} +5 -5
  118. package/dist/{chunk-KRAMCBIW.js → chunk-QKXPOLQD.js} +4 -4
  119. package/dist/{chunk-YDT3OEYF.js → chunk-R4ZHF4K2.js} +2 -2
  120. package/dist/{chunk-ISNPYOXR.js → chunk-RBUR6USI.js} +5 -5
  121. package/dist/{chunk-WZYAXOGO.js → chunk-RZSHIJYY.js} +2 -2
  122. package/dist/{chunk-TYSD7UQO.js → chunk-SVTIWBSA.js} +2 -2
  123. package/dist/{chunk-AMKIIRLL.js → chunk-TBFAQHQZ.js} +2 -2
  124. package/dist/{chunk-ZE5N4JSY.js → chunk-TKCJI7WA.js} +2 -2
  125. package/dist/{chunk-RYB553SS.js → chunk-TLOAAOT3.js} +2 -2
  126. package/dist/{chunk-RKRX4C4H.js → chunk-TPKB7T6Z.js} +2 -2
  127. package/dist/{chunk-JZCBMKR5.js → chunk-TUCEP2LS.js} +5 -5
  128. package/dist/{chunk-KZMRMKC7.js → chunk-U7MIMS2Y.js} +11 -1
  129. package/dist/chunk-U7MIMS2Y.js.map +1 -0
  130. package/dist/{chunk-OMWQO7H6.js → chunk-UEDC2VGP.js} +2 -2
  131. package/dist/{chunk-X7Y7WX73.js → chunk-UVYI6VIX.js} +1 -1
  132. package/dist/{chunk-JY2BGCYN.js → chunk-VCYH2XUY.js} +2 -2
  133. package/dist/{chunk-NGH74LDV.js → chunk-WD2QH3JM.js} +9 -9
  134. package/dist/{chunk-G6A3SGCE.js → chunk-WVEEVYMH.js} +8 -25
  135. package/dist/chunk-WVEEVYMH.js.map +1 -0
  136. package/dist/{chunk-UGTYADB4.js → chunk-WWSJQ7MP.js} +3 -3
  137. package/dist/{chunk-UQBOPCDK.js → chunk-X4PAATLA.js} +2 -2
  138. package/dist/{chunk-KBOQQEVC.js → chunk-XAFSZ4DP.js} +2 -2
  139. package/dist/{chunk-2S6JADBB.js → chunk-XICJL56Z.js} +2 -2
  140. package/dist/{chunk-Y5KESB7T.js → chunk-XJDSZFBZ.js} +45 -45
  141. package/dist/{chunk-24SIB3EW.js → chunk-XWE3EOWX.js} +2 -2
  142. package/dist/{chunk-Y77FXULH.js → chunk-XWXBXBOX.js} +2 -2
  143. package/dist/{chunk-VKQI47DJ.js → chunk-Y7427SL7.js} +4 -4
  144. package/dist/{chunk-TEH5MDX5.js → chunk-YH7HJNWV.js} +22 -22
  145. package/dist/chunk-YHTG64XV.js +117 -0
  146. package/dist/chunk-YHTG64XV.js.map +1 -0
  147. package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
  148. package/dist/{chunk-2M36PR7R.js → chunk-Z2T5BPOZ.js} +2 -2
  149. package/dist/cli.js +100 -97
  150. package/dist/coding/export-okf-codegraph.js +15 -11
  151. package/dist/coding/export-okf-codegraph.js.map +1 -1
  152. package/dist/coding/pre-action-gate.js +2 -2
  153. package/dist/compounding/engine.js +17 -13
  154. package/dist/config.js +24 -20
  155. package/dist/connectors/codex-materialize-runner.js +16 -12
  156. package/dist/connectors/index.js +16 -12
  157. package/dist/convergence-refresh.js +18 -14
  158. package/dist/corpus-scan-worker-host.d.ts +109 -0
  159. package/dist/corpus-scan-worker-host.d.ts.map +1 -0
  160. package/dist/corpus-scan-worker-host.js +15 -0
  161. package/dist/corpus-scan-worker.entry.d.ts +21 -0
  162. package/dist/corpus-scan-worker.entry.d.ts.map +1 -0
  163. package/dist/corpus-scan-worker.entry.js +60 -0
  164. package/dist/corpus-scan-worker.entry.js.map +1 -0
  165. package/dist/corpus-scan-worker.entry.mjs +176 -0
  166. package/dist/corpus-scan.d.ts +71 -0
  167. package/dist/corpus-scan.d.ts.map +1 -0
  168. package/dist/corpus-scan.js +16 -0
  169. package/dist/corpus-watermark.d.ts.map +1 -1
  170. package/dist/corpus-watermark.js +18 -14
  171. package/dist/dashboard-runtime.js +2 -2
  172. package/dist/deep-recall-http-glue.js +10 -10
  173. package/dist/embedding-fallback.js +2 -2
  174. package/dist/{entity-canonical-id-migration-F3JVG6K6.js → entity-canonical-id-migration-CUDCKWPF.js} +4 -2
  175. package/dist/entity-canonical-id-migration-CUDCKWPF.js.map +1 -0
  176. package/dist/entity-retrieval.js +18 -14
  177. package/dist/event-order-recall.js +3 -3
  178. package/dist/external-wiki-access.js +54 -51
  179. package/dist/extraction.js +21 -17
  180. package/dist/{graph-edge-decay-IU75NV42.js → graph-edge-decay-2BZTY2IM.js} +5 -5
  181. package/dist/graph-edge-decay-2BZTY2IM.js.map +1 -0
  182. package/dist/graph-jsonl.js +3 -3
  183. package/dist/graph-snapshot.js +3 -3
  184. package/dist/graph.js +2 -2
  185. package/dist/harmonic-retrieval.js +16 -12
  186. package/dist/index.d.ts +1 -1
  187. package/dist/index.d.ts.map +1 -1
  188. package/dist/index.js +174 -169
  189. package/dist/lcm/engine.js +2 -2
  190. package/dist/lcm/index.js +5 -5
  191. package/dist/location/index.js +18 -14
  192. package/dist/maintenance/memory-governance.js +16 -12
  193. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +16 -12
  194. package/dist/maintenance/rebuild-memory-projection-runner.js +18 -14
  195. package/dist/maintenance/rebuild-memory-projection.js +17 -13
  196. package/dist/memory-browse-operations.js +54 -51
  197. package/dist/memory-browse.js +5 -5
  198. package/dist/namespaces/migrate.js +17 -13
  199. package/dist/namespaces/search.js +7 -7
  200. package/dist/namespaces/storage.js +16 -12
  201. package/dist/offline-sync-changeset-generations.js +4 -4
  202. package/dist/offline-sync-embedding-generation.js +3 -3
  203. package/dist/offline-sync-snapshot-stream.js +7 -7
  204. package/dist/offline-sync.js +6 -6
  205. package/dist/operator-doctor-corpus.js +19 -15
  206. package/dist/operator-toolkit.js +35 -31
  207. package/dist/orchestration/buffer-drain-gate.d.ts +21 -0
  208. package/dist/orchestration/buffer-drain-gate.d.ts.map +1 -0
  209. package/dist/orchestration/buffer-drain-runner.d.ts +79 -0
  210. package/dist/orchestration/buffer-drain-runner.d.ts.map +1 -0
  211. package/dist/orchestration/buffer-drain-runner.js +780 -0
  212. package/dist/orchestration/buffer-drain-runner.js.map +1 -0
  213. package/dist/orchestration/buffer-swap.d.ts +12 -0
  214. package/dist/orchestration/buffer-swap.d.ts.map +1 -0
  215. package/dist/orchestration/maintenance.js +26 -22
  216. package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
  217. package/dist/orchestrator.d.ts +4 -1
  218. package/dist/orchestrator.d.ts.map +1 -1
  219. package/dist/orchestrator.js +143 -140
  220. package/dist/qmd.js +4 -4
  221. package/dist/recall-navigation-operations.js +54 -51
  222. package/dist/recall-navigation.js +53 -50
  223. package/dist/recall-state-view-admission.js +5 -5
  224. package/dist/recall-why-service.js +5 -5
  225. package/dist/resume-bundles.js +25 -21
  226. package/dist/search/factory.js +6 -6
  227. package/dist/search/index.js +12 -12
  228. package/dist/semantic-consolidation.js +17 -13
  229. package/dist/semantic-rule-promotion.js +16 -12
  230. package/dist/semantic-rule-verifier.js +16 -12
  231. package/dist/source-agent-qualifier.js +19 -15
  232. package/dist/standup/build.js +16 -12
  233. package/dist/storage/buffer-drain-claims.d.ts +159 -0
  234. package/dist/storage/buffer-drain-claims.d.ts.map +1 -0
  235. package/dist/storage/buffer-drain-claims.js +18 -0
  236. package/dist/storage/buffer-drain-claims.js.map +1 -0
  237. package/dist/storage/buffer-session-files.d.ts +14 -2
  238. package/dist/storage/buffer-session-files.d.ts.map +1 -1
  239. package/dist/storage/buffer-session-files.js +22 -0
  240. package/dist/storage/buffer-session-files.js.map +1 -0
  241. package/dist/storage/entity-canonical-id-migration.d.ts.map +1 -1
  242. package/dist/storage/memory-read-store.d.ts +5 -9
  243. package/dist/storage/memory-read-store.d.ts.map +1 -1
  244. package/dist/storage.d.ts +6 -1
  245. package/dist/storage.d.ts.map +1 -1
  246. package/dist/storage.js +17 -13
  247. package/dist/support-passport/index.js +56 -53
  248. package/dist/transfer/backup.js +2 -2
  249. package/dist/transfer/capsule-export.js +2 -2
  250. package/dist/transfer/capsule-import.js +2 -2
  251. package/dist/transfer/export-okf.js +16 -12
  252. package/dist/verified-recall.js +16 -12
  253. package/dist/who-knows-cli.js +17 -13
  254. package/dist/who-knows.js +16 -12
  255. package/package.json +32 -2
  256. package/scripts/build-with-heap.mjs +8 -0
  257. package/src/buffer-drain-handover.test.ts +307 -0
  258. package/src/buffer-persistence.ts +27 -5
  259. package/src/buffer-turn-helpers.ts +16 -0
  260. package/src/buffer.ts +32 -2
  261. package/src/corpus-scan-worker-host.ts +314 -0
  262. package/src/corpus-scan-worker.entry.mjs +176 -0
  263. package/src/corpus-scan-worker.entry.ts +95 -0
  264. package/src/corpus-scan.ts +215 -0
  265. package/src/corpus-watermark.ts +28 -12
  266. package/src/index.ts +1 -1
  267. package/src/orchestration/buffer-drain-gate.test.ts +40 -0
  268. package/src/orchestration/buffer-drain-gate.ts +38 -0
  269. package/src/orchestration/buffer-drain-runner.ts +312 -0
  270. package/src/orchestration/buffer-swap.ts +27 -0
  271. package/src/orchestration/turn-ingestion.ts +2 -0
  272. package/src/orchestrator.ts +9 -6
  273. package/src/storage/buffer-drain-claims.test.ts +448 -0
  274. package/src/storage/buffer-drain-claims.ts +593 -0
  275. package/src/storage/buffer-session-files.ts +50 -3
  276. package/src/storage/entity-canonical-id-migration.ts +4 -26
  277. package/src/storage/memory-read-store.ts +17 -99
  278. package/src/storage.ts +8 -11
  279. package/src/testing/subjects/extraction-lifecycle.test.ts +34 -0
  280. package/dist/chunk-3E3Z6UA3.js.map +0 -1
  281. package/dist/chunk-4OFXLQJD.js.map +0 -1
  282. package/dist/chunk-5U2BBF7E.js.map +0 -1
  283. package/dist/chunk-G6A3SGCE.js.map +0 -1
  284. package/dist/chunk-KZMRMKC7.js.map +0 -1
  285. package/dist/chunk-WXBVGKS6.js.map +0 -1
  286. package/dist/chunk-YJHOVA42.js.map +0 -1
  287. /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
  288. /package/dist/{chunk-ZAZZOQAQ.js.map → chunk-23OEPU3K.js.map} +0 -0
  289. /package/dist/{chunk-XDKEHZUC.js.map → chunk-264B57GV.js.map} +0 -0
  290. /package/dist/{chunk-NRGQ5HJO.js.map → chunk-4P4VLPGB.js.map} +0 -0
  291. /package/dist/{chunk-GIRWNTMV.js.map → chunk-4PG5R6BJ.js.map} +0 -0
  292. /package/dist/{chunk-52II7PB6.js.map → chunk-645WOU5I.js.map} +0 -0
  293. /package/dist/{chunk-RG3I2663.js.map → chunk-64JLBFCQ.js.map} +0 -0
  294. /package/dist/{chunk-G3T6YMBO.js.map → chunk-64S6X4IU.js.map} +0 -0
  295. /package/dist/{chunk-WASVDS6G.js.map → chunk-6HTEPYOI.js.map} +0 -0
  296. /package/dist/{chunk-6AEKSOWV.js.map → chunk-6I6DFULH.js.map} +0 -0
  297. /package/dist/{chunk-DJR37Q37.js.map → chunk-6T4CMKNP.js.map} +0 -0
  298. /package/dist/{chunk-RHJ5VGB3.js.map → chunk-73A2DDDR.js.map} +0 -0
  299. /package/dist/{chunk-4B73TQQV.js.map → chunk-7AKPLEJV.js.map} +0 -0
  300. /package/dist/{chunk-UHH67NLU.js.map → chunk-7OKYO2UC.js.map} +0 -0
  301. /package/dist/{chunk-KAL5SXPP.js.map → chunk-7ULSP7AU.js.map} +0 -0
  302. /package/dist/{chunk-USNU3BEQ.js.map → chunk-A4FZLM6Q.js.map} +0 -0
  303. /package/dist/{chunk-VV73XT3A.js.map → chunk-BJLCV2P3.js.map} +0 -0
  304. /package/dist/{chunk-KQAFEZQX.js.map → chunk-BUKK5SWA.js.map} +0 -0
  305. /package/dist/{chunk-DJFULNXK.js.map → chunk-C2AYUXEF.js.map} +0 -0
  306. /package/dist/{chunk-QY24FSXT.js.map → chunk-D3BYJSHX.js.map} +0 -0
  307. /package/dist/{chunk-JZRQY7ZQ.js.map → chunk-D4WR56FR.js.map} +0 -0
  308. /package/dist/{chunk-FB4JQEFS.js.map → chunk-DP22K7IC.js.map} +0 -0
  309. /package/dist/{chunk-EJFOGYF3.js.map → chunk-DTQJ7BRN.js.map} +0 -0
  310. /package/dist/{chunk-MLUPSZFK.js.map → chunk-DZL5PBEJ.js.map} +0 -0
  311. /package/dist/{chunk-JD5F5MYN.js.map → chunk-EGCC5OJE.js.map} +0 -0
  312. /package/dist/{chunk-IQSRVR6Z.js.map → chunk-EJ4RS7I7.js.map} +0 -0
  313. /package/dist/{chunk-WBN66HPI.js.map → chunk-ELHJK7LM.js.map} +0 -0
  314. /package/dist/{chunk-EH46DHBU.js.map → chunk-ENAPZQA6.js.map} +0 -0
  315. /package/dist/{chunk-LLG5VNK7.js.map → chunk-F2UAAI6B.js.map} +0 -0
  316. /package/dist/{chunk-YZNTAOPZ.js.map → chunk-FKNNWH7P.js.map} +0 -0
  317. /package/dist/{chunk-AAANTXCQ.js.map → chunk-FZVZYU5C.js.map} +0 -0
  318. /package/dist/{chunk-ON77FQXH.js.map → chunk-G6GMZJQ4.js.map} +0 -0
  319. /package/dist/{chunk-OL7A7NKH.js.map → chunk-GNP4BV4Z.js.map} +0 -0
  320. /package/dist/{chunk-GDYLZ6FR.js.map → chunk-HJ3UTGIW.js.map} +0 -0
  321. /package/dist/{chunk-CCL2OXFO.js.map → chunk-HSHLDSPX.js.map} +0 -0
  322. /package/dist/{chunk-R4BR62UO.js.map → chunk-HWMGQDZI.js.map} +0 -0
  323. /package/dist/{chunk-BPIAXMGC.js.map → chunk-IIUEHIQ2.js.map} +0 -0
  324. /package/dist/{chunk-RTJJPW3C.js.map → chunk-ILMB3X4X.js.map} +0 -0
  325. /package/dist/{chunk-RFT4OPYI.js.map → chunk-IPN67KVR.js.map} +0 -0
  326. /package/dist/{chunk-IEW6NDHX.js.map → chunk-JGSWWD33.js.map} +0 -0
  327. /package/dist/{chunk-A3UFV4K3.js.map → chunk-JIYJIH5N.js.map} +0 -0
  328. /package/dist/{chunk-4G2A7MH3.js.map → chunk-JKUVPVKR.js.map} +0 -0
  329. /package/dist/{chunk-VCLNU6MY.js.map → chunk-L2HSSVVD.js.map} +0 -0
  330. /package/dist/{chunk-U2A56LLF.js.map → chunk-L334PSLW.js.map} +0 -0
  331. /package/dist/{chunk-HQUYENKK.js.map → chunk-LIUI46BX.js.map} +0 -0
  332. /package/dist/{chunk-ERMT5WQM.js.map → chunk-MKE5DP3S.js.map} +0 -0
  333. /package/dist/{chunk-6BO27AWQ.js.map → chunk-MPJ7VHHA.js.map} +0 -0
  334. /package/dist/{chunk-KMJDH4KX.js.map → chunk-NSUGAIIB.js.map} +0 -0
  335. /package/dist/{chunk-7NT3FKAF.js.map → chunk-NXHBQC2O.js.map} +0 -0
  336. /package/dist/{chunk-JEX2JL3U.js.map → chunk-OH55F76C.js.map} +0 -0
  337. /package/dist/{chunk-37VTMUAL.js.map → chunk-OUJ4DVR5.js.map} +0 -0
  338. /package/dist/{chunk-2NLLXCJG.js.map → chunk-OWHERGF2.js.map} +0 -0
  339. /package/dist/{chunk-2IVK5IDY.js.map → chunk-P5F7ISLV.js.map} +0 -0
  340. /package/dist/{chunk-PM4PKC3Y.js.map → chunk-PNVQNAJM.js.map} +0 -0
  341. /package/dist/{chunk-FXWX7VD3.js.map → chunk-PRYDNBCO.js.map} +0 -0
  342. /package/dist/{chunk-YDDLSGYE.js.map → chunk-PUTH5REB.js.map} +0 -0
  343. /package/dist/{chunk-GFAEKSFR.js.map → chunk-QBK3IM6W.js.map} +0 -0
  344. /package/dist/{chunk-KRAMCBIW.js.map → chunk-QKXPOLQD.js.map} +0 -0
  345. /package/dist/{chunk-YDT3OEYF.js.map → chunk-R4ZHF4K2.js.map} +0 -0
  346. /package/dist/{chunk-ISNPYOXR.js.map → chunk-RBUR6USI.js.map} +0 -0
  347. /package/dist/{chunk-WZYAXOGO.js.map → chunk-RZSHIJYY.js.map} +0 -0
  348. /package/dist/{chunk-TYSD7UQO.js.map → chunk-SVTIWBSA.js.map} +0 -0
  349. /package/dist/{chunk-AMKIIRLL.js.map → chunk-TBFAQHQZ.js.map} +0 -0
  350. /package/dist/{chunk-ZE5N4JSY.js.map → chunk-TKCJI7WA.js.map} +0 -0
  351. /package/dist/{chunk-RYB553SS.js.map → chunk-TLOAAOT3.js.map} +0 -0
  352. /package/dist/{chunk-RKRX4C4H.js.map → chunk-TPKB7T6Z.js.map} +0 -0
  353. /package/dist/{chunk-JZCBMKR5.js.map → chunk-TUCEP2LS.js.map} +0 -0
  354. /package/dist/{chunk-OMWQO7H6.js.map → chunk-UEDC2VGP.js.map} +0 -0
  355. /package/dist/{chunk-X7Y7WX73.js.map → chunk-UVYI6VIX.js.map} +0 -0
  356. /package/dist/{chunk-JY2BGCYN.js.map → chunk-VCYH2XUY.js.map} +0 -0
  357. /package/dist/{chunk-NGH74LDV.js.map → chunk-WD2QH3JM.js.map} +0 -0
  358. /package/dist/{chunk-UGTYADB4.js.map → chunk-WWSJQ7MP.js.map} +0 -0
  359. /package/dist/{chunk-UQBOPCDK.js.map → chunk-X4PAATLA.js.map} +0 -0
  360. /package/dist/{chunk-KBOQQEVC.js.map → chunk-XAFSZ4DP.js.map} +0 -0
  361. /package/dist/{chunk-2S6JADBB.js.map → chunk-XICJL56Z.js.map} +0 -0
  362. /package/dist/{chunk-Y5KESB7T.js.map → chunk-XJDSZFBZ.js.map} +0 -0
  363. /package/dist/{chunk-24SIB3EW.js.map → chunk-XWE3EOWX.js.map} +0 -0
  364. /package/dist/{chunk-Y77FXULH.js.map → chunk-XWXBXBOX.js.map} +0 -0
  365. /package/dist/{chunk-VKQI47DJ.js.map → chunk-Y7427SL7.js.map} +0 -0
  366. /package/dist/{chunk-TEH5MDX5.js.map → chunk-YH7HJNWV.js.map} +0 -0
  367. /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
  368. /package/dist/{chunk-2M36PR7R.js.map → chunk-Z2T5BPOZ.js.map} +0 -0
  369. /package/dist/{entity-canonical-id-migration-F3JVG6K6.js.map → corpus-scan-worker-host.js.map} +0 -0
  370. /package/dist/{graph-edge-decay-IU75NV42.js.map → corpus-scan.js.map} +0 -0
@@ -0,0 +1,448 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readdirSync, existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, utimesSync } from "node:fs";
3
+ import os from "node:os";
4
+ import { hostname } from "node:os";
5
+ import path from "node:path";
6
+ import test from "node:test";
7
+
8
+ import { BufferDrainClaims, type DrainLease } from "./buffer-drain-claims.js";
9
+ import { bufferSessionFileName } from "./buffer-session-files.js";
10
+ import type { BufferEntryState } from "../types.js";
11
+
12
+ /** Plain-file BufferFileIo double: claims logic is file-atomic, not crypto-aware. */
13
+ function plainIo() {
14
+ return {
15
+ read: async (filePath: string) => readFileSync(filePath, "utf8"),
16
+ write: async (filePath: string, content: string) => {
17
+ mkdirSync(path.dirname(filePath), { recursive: true });
18
+ const tmp = `${filePath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
19
+ writeFileSync(tmp, content);
20
+ // Write-to-temp then rename: readers never observe a partial claim.
21
+ rmSync(filePath, { force: true });
22
+ writeFileSync(filePath, content);
23
+ rmSync(tmp, { force: true });
24
+ },
25
+ };
26
+ }
27
+
28
+ function turn(content: string, timestamp = "2026-11-01T00:00:00.000Z") {
29
+ return { role: "user" as const, content, timestamp, sessionKey: "sess-1" };
30
+ }
31
+
32
+ function entry(turns: ReturnType<typeof turn>[]): BufferEntryState {
33
+ return { turns, lastExtractionAt: null, extractionCount: 0 };
34
+ }
35
+
36
+ function setup() {
37
+ const stateDir = mkdtempSync(path.join(os.tmpdir(), "remnic-drain-claims-"));
38
+ const claims = new BufferDrainClaims(stateDir, plainIo());
39
+ const sessionFile = (key: string) => path.join(stateDir, "buffer", bufferSessionFileName(key));
40
+ const seedSession = (key: string, entryState: BufferEntryState) => {
41
+ mkdirSync(path.dirname(sessionFile(key)), { recursive: true });
42
+ writeFileSync(sessionFile(key), JSON.stringify({ key, ...entryState }));
43
+ };
44
+ return { stateDir, claims, sessionFile, seedSession, cleanup: () => rmSync(stateDir, { recursive: true, force: true }) };
45
+ }
46
+
47
+ function makeLease(ownerId = "worker-1"): DrainLease {
48
+ const now = new Date().toISOString();
49
+ return { ownerId, pid: process.pid, host: hostname(), acquiredAt: now, heartbeatAt: now, leaseMs: 60_000 };
50
+ }
51
+
52
+ test("claimFromSessionFile moves a session file into the drain dir atomically", async () => {
53
+ const s = setup();
54
+ try {
55
+ s.seedSession("sess-1", entry([turn("hello")]));
56
+ const outcome = await s.claims.claimFromSessionFile("sess-1", makeLease());
57
+ assert.equal(outcome, "claimed");
58
+ assert.equal(existsSync(s.sessionFile("sess-1")), false, "session file must be gone");
59
+ assert.equal(s.claims.isClaimed("sess-1"), true);
60
+ const claim = await s.claims.load("sess-1");
61
+ assert.ok(claim);
62
+ assert.equal(claim.key, "sess-1");
63
+ assert.equal(claim.entry.turns.length, 1);
64
+ assert.equal(claim.entry.turns[0]?.content, "hello");
65
+ } finally {
66
+ s.cleanup();
67
+ }
68
+ });
69
+
70
+ test("claimFromSessionFile reports empty for a missing or empty session and leaves no claim", async () => {
71
+ const s = setup();
72
+ try {
73
+ assert.equal(await s.claims.claimFromSessionFile("missing", makeLease()), "empty");
74
+ assert.equal(s.claims.isClaimed("missing"), false);
75
+ s.seedSession("blank", entry([]));
76
+ assert.equal(await s.claims.claimFromSessionFile("blank", makeLease()), "empty");
77
+ assert.equal(s.claims.isClaimed("blank"), false);
78
+ assert.equal(existsSync(s.sessionFile("blank")), true, "an empty live session belongs to its owner, not the drain");
79
+ } finally {
80
+ s.cleanup();
81
+ }
82
+ });
83
+
84
+ test("claimFromSessionFile refuses a key that is already claimed", async () => {
85
+ const s = setup();
86
+ try {
87
+ s.seedSession("sess-1", entry([turn("a")]));
88
+ assert.equal(await s.claims.claimFromSessionFile("sess-1", makeLease()), "claimed");
89
+ assert.equal(await s.claims.claimFromSessionFile("sess-1", makeLease()), "already_claimed");
90
+ const claim = await s.claims.load("sess-1");
91
+ assert.equal(claim?.entry.turns.length, 1, "the losing claim must not corrupt the winner's content");
92
+ } finally {
93
+ s.cleanup();
94
+ }
95
+ });
96
+
97
+ test("claimFromSessionFile cleans up an unleased orphan claim it re-claims", async () => {
98
+ const s = setup();
99
+ try {
100
+ s.seedSession("sess-1", entry([turn("a")]));
101
+ await s.claims.claimFromSessionFile("sess-1", makeLease());
102
+ await s.claims.releaseLease("sess-1", "worker-1");
103
+ // A claim file with no lease is an orphan from a killed drainer: the next
104
+ // claim call must hand the SAME durable content to the new owner.
105
+ assert.equal(await s.claims.claimFromSessionFile("sess-1", makeLease()), "already_claimed");
106
+ const adopted = await s.claims.adopt("sess-1", { ...makeLease("worker-2") });
107
+ assert.equal(adopted, "adopted");
108
+ const claim = await s.claims.load("sess-1");
109
+ assert.equal(claim?.entry.turns.length, 1, "orphaned content survives the takeover");
110
+ } finally {
111
+ s.cleanup();
112
+ }
113
+ });
114
+
115
+ test("adopt takes over an expired lease and refuses a live one", async () => {
116
+ const s = setup();
117
+ try {
118
+ s.seedSession("sess-1", entry([turn("a")]));
119
+ await s.claims.claimFromSessionFile("sess-1", makeLease());
120
+ assert.equal(await s.claims.adopt("sess-1", { ...makeLease("worker-2") }), "busy");
121
+ // Expire the lease by backdating its heartbeat beyond leaseMs.
122
+ const leasePath = s.claims.leasePathForTest("sess-1");
123
+ const stale = { ...makeLease(), heartbeatAt: new Date(Date.now() - 10 * makeLease().leaseMs).toISOString() };
124
+ writeFileSync(leasePath, JSON.stringify(stale));
125
+ assert.equal(await s.claims.adopt("sess-1", { ...makeLease("worker-2") }), "adopted");
126
+ assert.equal(await s.claims.heartbeat("sess-1", "worker-1", makeLease().leaseMs), "lost", "the previous owner must detect the takeover");
127
+ assert.equal(await s.claims.heartbeat("sess-1", "worker-2", makeLease().leaseMs), "ok");
128
+ } finally {
129
+ s.cleanup();
130
+ }
131
+ });
132
+
133
+ test("mergeIntoClaim appends unseen turns and dedupes turns already claimed", async () => {
134
+ const s = setup();
135
+ try {
136
+ s.seedSession("sess-1", entry([turn("a"), turn("b", "2026-11-01T00:01:00.000Z")]));
137
+ await s.claims.claimFromSessionFile("sess-1", makeLease());
138
+ // A live writer (the daemon) hands over its memory entry after the claim:
139
+ // the same old turns PLUS a new one. Nothing may duplicate.
140
+ await s.claims.mergeIntoClaim("sess-1", entry([turn("a"), turn("b", "2026-11-01T00:01:00.000Z"), turn("c", "2026-11-01T00:02:00.000Z")]));
141
+ const claim = await s.claims.load("sess-1");
142
+ assert.deepEqual(claim?.entry.turns.map((t) => t.content), ["a", "b", "c"]);
143
+ assert.equal(await s.claims.ack("sess-1", "worker-1"), "not_empty");
144
+ } finally {
145
+ s.cleanup();
146
+ }
147
+ });
148
+
149
+ test("mergeIntoClaim also dedupes retainedTurns", async () => {
150
+ const s = setup();
151
+ try {
152
+ s.seedSession("sess-1", { ...entry([turn("a")]), retainedTurns: [turn("kept")] });
153
+ await s.claims.claimFromSessionFile("sess-1", makeLease());
154
+ await s.claims.mergeIntoClaim("sess-1", { ...entry([]), retainedTurns: [turn("kept"), turn("kept2")] });
155
+ const claim = await s.claims.load("sess-1");
156
+ assert.deepEqual(claim?.entry.retainedTurns?.map((t) => t.content), ["kept", "kept2"]);
157
+ assert.deepEqual(claim?.entry.turns.map((t) => t.content), ["a"]);
158
+ } finally {
159
+ s.cleanup();
160
+ }
161
+ });
162
+
163
+ test("writeEntry replaces the claim content; ack keeps a tombstone the owner consumes", async () => {
164
+ const s = setup();
165
+ try {
166
+ s.seedSession("sess-1", entry([turn("a"), turn("b")]));
167
+ await s.claims.claimFromSessionFile("sess-1", makeLease());
168
+ await s.claims.writeEntry("sess-1", "worker-1", entry([turn("b")]));
169
+ assert.deepEqual((await s.claims.load("sess-1"))?.entry.turns.map((t) => t.content), ["b"]);
170
+ assert.deepEqual((await s.claims.drainedTurns("sess-1")).map((t) => t.content), ["a"], "extracted turns are recorded");
171
+ assert.equal(await s.claims.ack("sess-1", "not-the-owner"), "busy");
172
+ assert.equal(await s.claims.ack("sess-1", "worker-1"), "not_empty");
173
+ await s.claims.writeEntry("sess-1", "worker-1", entry([]));
174
+ assert.equal(await s.claims.ack("sess-1", "worker-1"), "acked");
175
+ // The claim file stays as a tombstone: a live owner with a stale copy
176
+ // subtracts the drained turns instead of writing them back.
177
+ assert.equal(s.claims.isClaimed("sess-1"), true);
178
+ assert.deepEqual((await s.claims.drainedTurns("sess-1")).map((t) => t.content), ["a", "b"]);
179
+ assert.equal(existsSync(s.claims.leasePathForTest("sess-1")), false, "the lease is released");
180
+ await s.claims.release("sess-1");
181
+ assert.equal(s.claims.isClaimed("sess-1"), false);
182
+ } finally {
183
+ s.cleanup();
184
+ }
185
+ });
186
+
187
+ test("releaseLease keeps the claim content so a later run resumes it", async () => {
188
+ const s = setup();
189
+ try {
190
+ s.seedSession("sess-1", entry([turn("a")]));
191
+ await s.claims.claimFromSessionFile("sess-1", makeLease());
192
+ await s.claims.releaseLease("sess-1", "worker-1");
193
+ assert.equal(s.claims.isClaimed("sess-1"), true, "content must survive the lease release");
194
+ assert.equal(existsSync(s.claims.leasePathForTest("sess-1")), false);
195
+ const listed = await s.claims.list();
196
+ assert.equal(listed.length, 1);
197
+ assert.equal(listed[0]?.key, "sess-1");
198
+ assert.equal(listed[0]?.leased, false);
199
+ } finally {
200
+ s.cleanup();
201
+ }
202
+ });
203
+
204
+ test("list reports lease liveness and expiry", async () => {
205
+ const s = setup();
206
+ try {
207
+ s.seedSession("a", entry([turn("1")]));
208
+ s.seedSession("b", entry([turn("2")]));
209
+ await s.claims.claimFromSessionFile("a", makeLease());
210
+ await s.claims.claimFromSessionFile("b", { ...makeLease("worker-2") });
211
+ const stale = { ...makeLease(), heartbeatAt: new Date(Date.now() - 10 * makeLease().leaseMs).toISOString() };
212
+ writeFileSync(s.claims.leasePathForTest("b"), JSON.stringify(stale));
213
+ const listed = await s.claims.list();
214
+ assert.deepEqual(listed.map((c) => c.key).sort(), ["a", "b"]);
215
+ const a = listed.find((c) => c.key === "a");
216
+ const b = listed.find((c) => c.key === "b");
217
+ assert.equal(a?.leased, true);
218
+ assert.equal(a?.leaseExpired, false);
219
+ assert.equal(b?.leased, true);
220
+ assert.equal(b?.leaseExpired, true);
221
+ } finally {
222
+ s.cleanup();
223
+ }
224
+ });
225
+
226
+ test("claims live in their own directory and never collide with session files", async () => {
227
+ const s = setup();
228
+ try {
229
+ s.seedSession("sess-1", entry([turn("a")]));
230
+ await s.claims.claimFromSessionFile("sess-1", makeLease());
231
+ const bufferDir = path.join(s.stateDir, "buffer");
232
+ const topLevel = readdirSync(bufferDir, { withFileTypes: true }).filter((d) => d.isFile());
233
+ // Only the drain subdirectory plus nothing else: the loader's isFile scan
234
+ // must never see claim content as a session.
235
+ assert.deepEqual(topLevel.map((d) => d.name), []);
236
+ } finally {
237
+ s.cleanup();
238
+ }
239
+ });
240
+
241
+ test("claimFromSessionFile never replaces an existing claim", async () => {
242
+ const s = setup();
243
+ try {
244
+ // First drainer claims the session; then a session file reappears beside
245
+ // the claim (an owner TOCTOU write, or a crash between the tombstone
246
+ // consume and its release). The next claim attempt must keep the claim
247
+ // and its lease exactly as they are.
248
+ s.seedSession("sess-1", entry([turn("claim content")]));
249
+ assert.equal(await s.claims.claimFromSessionFile("sess-1", makeLease("worker-original")), "claimed");
250
+ s.seedSession("sess-1", entry([turn("fresh owner copy")]));
251
+ const outcome = await s.claims.claimFromSessionFile("sess-1", makeLease("worker-two"));
252
+ assert.equal(outcome, "already_claimed");
253
+ const claim = await s.claims.load("sess-1");
254
+ assert.ok(claim);
255
+ assert.deepEqual(
256
+ claim.entry.turns.map((t) => t.content),
257
+ ["claim content"],
258
+ "the existing claim content survives the second claim attempt",
259
+ );
260
+ // SAFETY: the lease body is written by this test / the claims module with an ownerId field.
261
+ const lease = JSON.parse(readFileSync(s.claims.leasePathForTest("sess-1"), "utf8")) as { ownerId: string };
262
+ assert.equal(lease.ownerId, "worker-original", "the original lease is untouched");
263
+ } finally {
264
+ s.cleanup();
265
+ }
266
+ });
267
+
268
+ test("adopt takes over a dead owner's claim in one call and keeps a lease", async () => {
269
+ const s = setup();
270
+ try {
271
+ s.seedSession("sess-1", entry([turn("hello")]));
272
+ assert.equal(await s.claims.claimFromSessionFile("sess-1", makeLease("worker-dead")), "claimed");
273
+ // Simulate the killed worker: its lease is unexpired but its pid is gone.
274
+ const deadLease = { ...makeLease("worker-dead"), pid: 4_500_000 };
275
+ writeFileSync(s.claims.leasePathForTest("sess-1"), JSON.stringify(deadLease));
276
+ const outcome = await s.claims.adopt("sess-1", makeLease("worker-new"));
277
+ assert.equal(outcome, "adopted", "a dead owner's claim is adoptable without waiting for expiry");
278
+ // SAFETY: the lease body is written by this test / the claims module with an ownerId field.
279
+ const lease = JSON.parse(readFileSync(s.claims.leasePathForTest("sess-1"), "utf8")) as { ownerId: string };
280
+ assert.equal(lease.ownerId, "worker-new", "the takeover leaves a lease behind");
281
+ } finally {
282
+ s.cleanup();
283
+ }
284
+ });
285
+
286
+ test("concurrent adopters of an expired lease resolve to exactly one winner", async () => {
287
+ const s = setup();
288
+ try {
289
+ s.seedSession("sess-1", entry([turn("hello")]));
290
+ assert.equal(await s.claims.claimFromSessionFile("sess-1", makeLease("worker-first")), "claimed");
291
+ const expired = { ...makeLease("worker-first"), heartbeatAt: new Date(Date.now() - 10 * makeLease().leaseMs).toISOString() };
292
+ writeFileSync(s.claims.leasePathForTest("sess-1"), JSON.stringify(expired));
293
+ const outcomes = await Promise.all([
294
+ s.claims.adopt("sess-1", makeLease("worker-a")),
295
+ s.claims.adopt("sess-1", makeLease("worker-b")),
296
+ s.claims.adopt("sess-1", makeLease("worker-c")),
297
+ ]);
298
+ assert.equal(outcomes.filter((o) => o === "adopted").length, 1, "exactly one adopter wins");
299
+ // SAFETY: the lease body is written by this test / the claims module with an ownerId field.
300
+ const lease = JSON.parse(readFileSync(s.claims.leasePathForTest("sess-1"), "utf8")) as { ownerId: string };
301
+ const winner = ["worker-a", "worker-b", "worker-c"][outcomes.indexOf("adopted")];
302
+ assert.equal(lease.ownerId, winner, "the surviving lease belongs to the winner");
303
+ } finally {
304
+ s.cleanup();
305
+ }
306
+ });
307
+
308
+ test("writeEntry keeps turns merged into the claim after the drainer loaded it", async () => {
309
+ const s = setup();
310
+ try {
311
+ s.seedSession("sess-1", entry([turn("t1")]));
312
+ assert.equal(await s.claims.claimFromSessionFile("sess-1", makeLease()), "claimed");
313
+ const loaded = await s.claims.load("sess-1");
314
+ assert.ok(loaded, "the drainer loaded the claim");
315
+ // The daemon appends t2 while the drainer works; the drainer then reports
316
+ // its snapshot fully extracted.
317
+ await s.claims.mergeIntoClaim("sess-1", entry([turn("t2")]));
318
+ await s.claims.writeEntry("sess-1", "worker-1", entry([]), loaded.entry);
319
+ const claim = await s.claims.load("sess-1");
320
+ assert.ok(claim);
321
+ assert.deepEqual(
322
+ claim.entry.turns.map((t) => t.content),
323
+ ["t2"],
324
+ "the late-merged turn stays pending instead of being marked drained",
325
+ );
326
+ assert.deepEqual(
327
+ (claim.drained ?? []).map((t) => t.content),
328
+ ["t1"],
329
+ "only the turn the drainer actually saw is recorded as drained",
330
+ );
331
+ } finally {
332
+ s.cleanup();
333
+ }
334
+ });
335
+
336
+ test("mergeIntoClaim does not resurrect turns the drain already extracted", async () => {
337
+ const s = setup();
338
+ try {
339
+ s.seedSession("sess-1", entry([turn("t1")]));
340
+ assert.equal(await s.claims.claimFromSessionFile("sess-1", makeLease()), "claimed");
341
+ const loaded = await s.claims.load("sess-1");
342
+ assert.ok(loaded);
343
+ await s.claims.writeEntry("sess-1", "worker-1", entry([]), loaded.entry); // t1 extracted
344
+ // The owner still holds a stale in-memory copy of t1 plus a genuinely new
345
+ // t2: merging must keep t1 out of the pending entry.
346
+ await s.claims.mergeIntoClaim("sess-1", entry([turn("t1"), turn("t2")]));
347
+ const claim = await s.claims.load("sess-1");
348
+ assert.ok(claim);
349
+ assert.deepEqual(
350
+ claim.entry.turns.map((t) => t.content),
351
+ ["t2"],
352
+ "the extracted turn never re-enters the pending entry",
353
+ );
354
+ assert.deepEqual((claim.drained ?? []).map((t) => t.content), ["t1"]);
355
+ } finally {
356
+ s.cleanup();
357
+ }
358
+ });
359
+
360
+ test("a claim counts as drained only when the acked tombstone is complete", async () => {
361
+ const s = setup();
362
+ try {
363
+ s.seedSession("sess-1", entry([turn("t1"), turn("t2")]));
364
+ assert.equal(await s.claims.claimFromSessionFile("sess-1", makeLease()), "claimed");
365
+ const loaded = await s.claims.load("sess-1");
366
+ assert.ok(loaded);
367
+ // Mid-drain: t2 extracted, t1 pending, lease alive.
368
+ await s.claims.writeEntry("sess-1", "worker-1", entry([turn("t1")]), loaded.entry);
369
+ assert.equal(await s.claims.isDrained("sess-1"), false, "partial drained is not finished");
370
+ // Everything extracted but the drainer still holds the lease.
371
+ await s.claims.writeEntry("sess-1", "worker-1", entry([]), loaded.entry);
372
+ assert.equal(await s.claims.isDrained("sess-1"), false, "an unacked claim is not finished");
373
+ // Ack: the drainer drops its lease and leaves the tombstone.
374
+ await s.claims.releaseLease("sess-1", makeLease().ownerId);
375
+ assert.equal(await s.claims.isDrained("sess-1"), true, "the acked tombstone is finished");
376
+ } finally {
377
+ s.cleanup();
378
+ }
379
+ });
380
+
381
+ test("writeEntry rejects progress from a drainer that lost the lease", async () => {
382
+ const s = setup();
383
+ try {
384
+ s.seedSession("sess-1", entry([turn("t1")]));
385
+ assert.equal(await s.claims.claimFromSessionFile("sess-1", makeLease("worker-old")), "claimed");
386
+ // Expire worker-old's lease and let worker-two adopt the claim.
387
+ const expired = { ...makeLease("worker-old"), heartbeatAt: new Date(Date.now() - 10 * makeLease().leaseMs).toISOString() };
388
+ writeFileSync(s.claims.leasePathForTest("sess-1"), JSON.stringify(expired));
389
+ assert.equal(await s.claims.adopt("sess-1", makeLease("worker-two")), "adopted");
390
+ // The superseded drainer must not write progress: it could restore turns
391
+ // the new owner already extracted.
392
+ const loaded = await s.claims.load("sess-1");
393
+ assert.ok(loaded);
394
+ await assert.rejects(
395
+ () => s.claims.writeEntry("sess-1", "worker-old", entry([]), loaded.entry),
396
+ /no longer owned/,
397
+ );
398
+ await s.claims.writeEntry("sess-1", "worker-two", entry([]), loaded.entry);
399
+ assert.deepEqual((await s.claims.drainedTurns("sess-1")).map((t) => t.content), ["t1"], "the new owner's progress lands");
400
+ } finally {
401
+ s.cleanup();
402
+ }
403
+ });
404
+
405
+ test("a retained turn the drainer extracted is tombstoned, not dropped", async () => {
406
+ const s = setup();
407
+ try {
408
+ const claimEntry: BufferEntryState = { turns: [turn("t1")], retainedTurns: [turn("r1")], lastExtractionAt: null, extractionCount: 0 };
409
+ s.seedSession("sess-1", claimEntry);
410
+ assert.equal(await s.claims.claimFromSessionFile("sess-1", makeLease()), "claimed");
411
+ const loaded = await s.claims.load("sess-1");
412
+ assert.ok(loaded);
413
+ // The drainer extracts everything, including the retained copy.
414
+ await s.claims.writeEntry("sess-1", "worker-1", entry([]), loaded.entry);
415
+ const claim = await s.claims.load("sess-1");
416
+ assert.ok(claim);
417
+ assert.deepEqual((claim.drainedRetained ?? []).map((t) => t.content), ["r1"], "the extracted retained copy is recorded");
418
+ // A live owner still holding a stale retained copy cannot resurrect it.
419
+ await s.claims.mergeIntoClaim("sess-1", { turns: [], retainedTurns: [turn("r1")], lastExtractionAt: null, extractionCount: 0 });
420
+ const after = await s.claims.load("sess-1");
421
+ assert.ok(after);
422
+ assert.deepEqual(after.entry.retainedTurns ?? [], [], "the stale retained copy never re-enters the claim");
423
+ // A retained-only tombstone counts as a finished drain.
424
+ await s.claims.releaseLease("sess-1", "worker-1");
425
+ assert.equal(await s.claims.isDrained("sess-1"), true, "a claim with only extracted retained turns is finished");
426
+ } finally {
427
+ s.cleanup();
428
+ }
429
+ });
430
+
431
+ test("a turn the drainer retained for retry is not recorded as drained", async () => {
432
+ const s = setup();
433
+ try {
434
+ s.seedSession("sess-1", entry([turn("t1")]));
435
+ assert.equal(await s.claims.claimFromSessionFile("sess-1", makeLease()), "claimed");
436
+ const loaded = await s.claims.load("sess-1");
437
+ assert.ok(loaded);
438
+ // The drainer moved t1 into its retained list (deferred retry): it is
439
+ // still pending, not extracted.
440
+ await s.claims.writeEntry("sess-1", "worker-1", { turns: [], retainedTurns: [turn("t1")], lastExtractionAt: null, extractionCount: 0 }, loaded.entry);
441
+ const claim = await s.claims.load("sess-1");
442
+ assert.ok(claim);
443
+ assert.deepEqual((claim.drained ?? []).map((t) => t.content), [], "a retained-for-retry turn stays out of drained");
444
+ assert.deepEqual((claim.entry.retainedTurns ?? []).map((t) => t.content), ["t1"], "the retained copy stays pending in the claim");
445
+ } finally {
446
+ s.cleanup();
447
+ }
448
+ });