@remnic/core 10.0.69 → 10.0.71

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 (412) hide show
  1. package/dist/access-admin-ops-surface.js +82 -79
  2. package/dist/access-authorization-probe.js +83 -80
  3. package/dist/access-boundary.js +82 -79
  4. package/dist/access-cli.js +196 -193
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-extraction-force-flush.js +82 -79
  7. package/dist/access-http-briefing.js +83 -80
  8. package/dist/access-http-query.js +84 -81
  9. package/dist/access-http.js +106 -103
  10. package/dist/access-identity-continuity-surface.js +82 -79
  11. package/dist/access-lcm-surface.js +82 -79
  12. package/dist/access-mcp.js +95 -92
  13. package/dist/access-namespace-preflight.js +82 -79
  14. package/dist/access-observe-lcm-archive.d.ts +5 -0
  15. package/dist/access-observe-lcm-archive.d.ts.map +1 -0
  16. package/dist/access-observe-lcm-archive.js +9 -0
  17. package/dist/access-observe-objective-state.d.ts +8 -0
  18. package/dist/access-observe-objective-state.d.ts.map +1 -0
  19. package/dist/access-observe-objective-state.js +17 -0
  20. package/dist/access-observe-stage-timing.d.ts +11 -0
  21. package/dist/access-observe-stage-timing.d.ts.map +1 -0
  22. package/dist/access-observe-stage-timing.js +9 -0
  23. package/dist/access-observe-stage-timing.js.map +1 -0
  24. package/dist/access-observe-write-category.js +13 -13
  25. package/dist/access-observe-write-surface.d.ts.map +1 -1
  26. package/dist/access-observe-write-surface.js +82 -79
  27. package/dist/access-offline-manifest.js +8 -8
  28. package/dist/access-operations-batch.js +85 -82
  29. package/dist/access-operations.js +92 -89
  30. package/dist/access-recall-concurrency.js +82 -79
  31. package/dist/access-recall-response.js +82 -79
  32. package/dist/access-recall-surface.js +82 -79
  33. package/dist/access-schema.js +12 -12
  34. package/dist/access-service.d.ts +1 -0
  35. package/dist/access-service.d.ts.map +1 -1
  36. package/dist/access-service.js +82 -79
  37. package/dist/access-surface-catalog.js +84 -81
  38. package/dist/access-surface-catalog.js.map +1 -1
  39. package/dist/active-recall.js +26 -26
  40. package/dist/adapters/index.js +4 -4
  41. package/dist/adapters/registry.js +2 -2
  42. package/dist/briefing.js +17 -17
  43. package/dist/buffer-turn-helpers.d.ts +10 -1
  44. package/dist/buffer-turn-helpers.d.ts.map +1 -1
  45. package/dist/buffer-turn-helpers.js +3 -1
  46. package/dist/buffer.d.ts +4 -0
  47. package/dist/buffer.d.ts.map +1 -1
  48. package/dist/buffer.js +9 -9
  49. package/dist/calibration.js +4 -4
  50. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
  51. package/dist/capsule-crypto-7FJQINUR.js.map +1 -0
  52. package/dist/causal-behavior.js +2 -2
  53. package/dist/causal-chain.js +2 -2
  54. package/dist/causal-consolidation.js +22 -22
  55. package/dist/causal-retrieval.js +2 -2
  56. package/dist/causal-trajectory-graph.js +3 -3
  57. package/dist/causal-trajectory.js +1 -1
  58. package/dist/{chunk-6WKWL53V.js → chunk-23BA3USG.js} +128 -122
  59. package/dist/chunk-23BA3USG.js.map +1 -0
  60. package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  61. package/dist/{chunk-CI3MAAIY.js → chunk-2R44BBPK.js} +2 -2
  62. package/dist/{chunk-QDZWQBLU.js → chunk-2R7OCBRR.js} +2 -2
  63. package/dist/{chunk-B3MWELZ3.js → chunk-2WPWEVAH.js} +2 -2
  64. package/dist/{chunk-PLCYKAWH.js → chunk-3ICAWZUU.js} +4 -4
  65. package/dist/{chunk-R4ZHF4K2.js → chunk-43FLJLT2.js} +4 -4
  66. package/dist/{chunk-PMDOGXA2.js → chunk-4PMFSYH7.js} +2 -2
  67. package/dist/{chunk-HXFIILGF.js → chunk-4RASUAN3.js} +4 -4
  68. package/dist/{chunk-EI6M5NVK.js → chunk-5JBVPJCT.js} +2 -2
  69. package/dist/{chunk-4NV7O76Q.js → chunk-5ORJHH43.js} +2 -2
  70. package/dist/{chunk-74AY3S7U.js → chunk-5RW247F5.js} +10 -10
  71. package/dist/{chunk-4XINO77L.js → chunk-66JOLMWS.js} +2 -2
  72. package/dist/{chunk-AYJQ644F.js → chunk-6KBCUIZP.js} +2 -2
  73. package/dist/{chunk-GKL6QYZO.js → chunk-6L63IN3M.js} +1 -1
  74. package/dist/{chunk-OPFVXM2D.js → chunk-7P5VBRBS.js} +2 -2
  75. package/dist/{chunk-4SOWMA55.js → chunk-7WFY2L5J.js} +3 -3
  76. package/dist/{chunk-GJ4HU6LK.js → chunk-AGRMXXVA.js} +2 -2
  77. package/dist/{chunk-KOLJJN5G.js → chunk-AQNHEDXP.js} +11 -4
  78. package/dist/chunk-AQNHEDXP.js.map +1 -0
  79. package/dist/{chunk-IVUVQPTQ.js → chunk-AZNE2H6D.js} +2 -2
  80. package/dist/{chunk-HPB5TF55.js → chunk-BCBY5GYZ.js} +3 -3
  81. package/dist/{chunk-M4IEZGCK.js → chunk-BFBS7OFQ.js} +7 -7
  82. package/dist/{chunk-4PG5R6BJ.js → chunk-BNU5TQLR.js} +2 -2
  83. package/dist/{chunk-TVE7W67N.js → chunk-BTU77RB5.js} +2 -2
  84. package/dist/{chunk-H6VJSITJ.js → chunk-CIL4GHMD.js} +3 -3
  85. package/dist/{chunk-KZZ5EZ7O.js → chunk-DC3DWF2M.js} +2 -2
  86. package/dist/{chunk-R2BIVU2Z.js → chunk-DE4U5PTV.js} +50 -46
  87. package/dist/chunk-DE4U5PTV.js.map +1 -0
  88. package/dist/{chunk-DDBB55L2.js → chunk-DSSTOV5M.js} +534 -361
  89. package/dist/chunk-DSSTOV5M.js.map +1 -0
  90. package/dist/{chunk-4MPAKYZU.js → chunk-EGKDONTC.js} +12 -6
  91. package/dist/chunk-EGKDONTC.js.map +1 -0
  92. package/dist/{chunk-AUFPPBC3.js → chunk-ENPJX3BB.js} +3 -3
  93. package/dist/{chunk-CTH2CQCB.js → chunk-EQ6EMJRG.js} +2 -2
  94. package/dist/{chunk-63A2BU2B.js → chunk-F4ZHKDBW.js} +5 -5
  95. package/dist/{chunk-3LBDEGQA.js → chunk-FDUCIUQ5.js} +2 -2
  96. package/dist/{chunk-OCK4D5PC.js → chunk-FGGLVCHI.js} +2 -2
  97. package/dist/{chunk-XVZA6VXC.js → chunk-FZVROZKC.js} +4 -4
  98. package/dist/{chunk-3RHU5MD7.js → chunk-GXAZ4Q2S.js} +2 -2
  99. package/dist/{chunk-UVVFPCPX.js → chunk-H2X32F2K.js} +2 -2
  100. package/dist/{chunk-645WOU5I.js → chunk-H7BZBSP6.js} +4 -4
  101. package/dist/{chunk-W7K5BP3M.js → chunk-HQPFACXO.js} +2 -2
  102. package/dist/{chunk-TECK5IVY.js → chunk-HZNURRDS.js} +2 -2
  103. package/dist/{chunk-U2CAGMUD.js → chunk-ICBUTKLL.js} +2 -2
  104. package/dist/{chunk-B4DXREWC.js → chunk-IQXQ3KWR.js} +2 -2
  105. package/dist/{chunk-AL3TCNSJ.js → chunk-J6GZ2254.js} +4 -4
  106. package/dist/{chunk-5MGKGEL7.js → chunk-J72ZY7NU.js} +2 -2
  107. package/dist/{chunk-OH55F76C.js → chunk-JEX2JL3U.js} +2 -2
  108. package/dist/{chunk-Y3XPSGNF.js → chunk-JGLZ46JS.js} +2 -2
  109. package/dist/{chunk-EXQJMFRF.js → chunk-JY4LXMZR.js} +2 -2
  110. package/dist/{chunk-6P7U3HWG.js → chunk-K36RMINQ.js} +3 -3
  111. package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
  112. package/dist/{chunk-LN4X2NDA.js → chunk-KY3KJUNN.js} +10 -10
  113. package/dist/chunk-L3E7MLMK.js +20 -0
  114. package/dist/chunk-L3E7MLMK.js.map +1 -0
  115. package/dist/{chunk-H5DIRCZP.js → chunk-LHMCZ6VW.js} +18 -18
  116. package/dist/{chunk-GGD2YDS6.js → chunk-LN7CWLAU.js} +2 -2
  117. package/dist/{chunk-GHHQLSSY.js → chunk-LYOCAUI6.js} +3 -3
  118. package/dist/{chunk-VA3GNAWQ.js → chunk-LZIOY7ZU.js} +4 -5
  119. package/dist/chunk-LZIOY7ZU.js.map +1 -0
  120. package/dist/{chunk-W26D2USW.js → chunk-MUH7RR6T.js} +6 -6
  121. package/dist/{chunk-IAGVM7EY.js → chunk-N2Y6COEM.js} +2 -2
  122. package/dist/{chunk-64JLBFCQ.js → chunk-N7NAGECH.js} +2 -2
  123. package/dist/{chunk-5Y6VA6X2.js → chunk-OGQGDOYK.js} +2 -2
  124. package/dist/{chunk-U7MIMS2Y.js → chunk-OUBEJWVI.js} +17 -1
  125. package/dist/chunk-OUBEJWVI.js.map +1 -0
  126. package/dist/{chunk-JLMOH5JA.js → chunk-OWZXDBV4.js} +2 -2
  127. package/dist/{chunk-ZORS5BG5.js → chunk-PJAMFIDE.js} +10 -10
  128. package/dist/chunk-QCFZDYYG.js +34 -0
  129. package/dist/chunk-QCFZDYYG.js.map +1 -0
  130. package/dist/{chunk-SXTVZ6JL.js → chunk-QIOQI7QZ.js} +2 -2
  131. package/dist/{chunk-MMCO4SRG.js → chunk-QNN4FYBD.js} +19 -11
  132. package/dist/chunk-QNN4FYBD.js.map +1 -0
  133. package/dist/{chunk-75DCOS6I.js → chunk-QUPTDKSX.js} +67 -67
  134. package/dist/{chunk-HH73M65H.js → chunk-QVZZE6VH.js} +3 -3
  135. package/dist/{chunk-XWE3EOWX.js → chunk-RZED7FAU.js} +2 -2
  136. package/dist/{chunk-IZBIDPMY.js → chunk-S37O4S6E.js} +2 -2
  137. package/dist/{chunk-NHWDTOBN.js → chunk-S73ZVU37.js} +21 -21
  138. package/dist/{chunk-YSATDEBY.js → chunk-SBRUTD5O.js} +5 -5
  139. package/dist/{chunk-WVP2I6C5.js → chunk-SDL22RRW.js} +50 -11
  140. package/dist/chunk-SDL22RRW.js.map +1 -0
  141. package/dist/{chunk-PRKNAIGZ.js → chunk-T6T2RMWW.js} +5 -5
  142. package/dist/{chunk-CIEEYVUB.js → chunk-TPX7XXY4.js} +2 -2
  143. package/dist/{chunk-L334PSLW.js → chunk-U2A56LLF.js} +8 -8
  144. package/dist/{chunk-P4TV3TQ2.js → chunk-U6IL4724.js} +5 -5
  145. package/dist/{chunk-5UM4H64E.js → chunk-UAOJVQXC.js} +2 -2
  146. package/dist/{chunk-MOIJWWNA.js → chunk-UE5JYPU6.js} +13 -13
  147. package/dist/{chunk-TGQW3AMK.js → chunk-UIFWZMO7.js} +23 -23
  148. package/dist/{chunk-UW6LBFYN.js → chunk-USX5YIKB.js} +6 -6
  149. package/dist/{chunk-QGFR73SN.js → chunk-VK5HKVQ7.js} +2 -2
  150. package/dist/{chunk-ZOBYTRBU.js → chunk-VNTQU6KB.js} +47 -2
  151. package/dist/chunk-VNTQU6KB.js.map +1 -0
  152. package/dist/{chunk-IHCXMYSE.js → chunk-VPEIWMUT.js} +2 -2
  153. package/dist/{chunk-O5ZINWW3.js → chunk-VSJ3ARFS.js} +2 -2
  154. package/dist/{chunk-RLVI2M5Q.js → chunk-W4V7P5HP.js} +2 -2
  155. package/dist/{chunk-ELHJK7LM.js → chunk-WBN66HPI.js} +6 -6
  156. package/dist/{chunk-BJLCV2P3.js → chunk-WGRAB3KB.js} +2 -2
  157. package/dist/{chunk-5KDDAQ6K.js → chunk-WN2DASTS.js} +10 -10
  158. package/dist/{chunk-IG3EZ3TF.js → chunk-WNQ3B3Q5.js} +2 -2
  159. package/dist/{chunk-V6SBA5O7.js → chunk-WYOHQJKC.js} +7 -7
  160. package/dist/chunk-WZU5AGO6.js +38 -0
  161. package/dist/chunk-WZU5AGO6.js.map +1 -0
  162. package/dist/{chunk-WHROVQ5Q.js → chunk-X4DHAXFO.js} +3 -3
  163. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  164. package/dist/{chunk-JLGBJ4AX.js → chunk-XPZM7GP6.js} +10 -10
  165. package/dist/{chunk-64S6X4IU.js → chunk-XTN54NML.js} +4 -4
  166. package/dist/{chunk-UYUFDTIX.js → chunk-YIXWY4OZ.js} +2 -2
  167. package/dist/{chunk-NCZGGGWI.js → chunk-YPYSC3TX.js} +12 -12
  168. package/dist/{chunk-AVGDYYMA.js → chunk-ZBET3W26.js} +3 -3
  169. package/dist/{chunk-Y7427SL7.js → chunk-ZG4GNPRE.js} +216 -50
  170. package/dist/chunk-ZG4GNPRE.js.map +1 -0
  171. package/dist/chunk-ZODKV27J.js +443 -0
  172. package/dist/chunk-ZODKV27J.js.map +1 -0
  173. package/dist/cli.js +133 -130
  174. package/dist/coding/export-okf-codegraph.js +16 -16
  175. package/dist/coding/pre-action-gate.js +2 -2
  176. package/dist/compounding/engine.js +18 -18
  177. package/dist/config.js +26 -26
  178. package/dist/connectors/codex-materialize-runner.js +19 -19
  179. package/dist/connectors/codex-materialize.js +2 -2
  180. package/dist/connectors/index.js +20 -20
  181. package/dist/consolidation-provenance-check.js +2 -2
  182. package/dist/convergence-refresh.js +20 -20
  183. package/dist/corpus-watermark.js +20 -20
  184. package/dist/deep-recall-http-glue.js +13 -13
  185. package/dist/deep-recall.js +2 -2
  186. package/dist/embedding-fallback.js +2 -2
  187. package/dist/entity-retrieval.js +20 -20
  188. package/dist/event-order-recall.js +2 -2
  189. package/dist/external-wiki-access.js +83 -80
  190. package/dist/extraction-span-materialize.js +2 -2
  191. package/dist/extraction-window-drain.d.ts.map +1 -1
  192. package/dist/extraction-window-drain.js +1 -1
  193. package/dist/extraction.js +32 -32
  194. package/dist/{graph-edge-decay-OEX5OGMI.js → graph-edge-decay-CXTNJ7IQ.js} +6 -6
  195. package/dist/graph-edge-decay-CXTNJ7IQ.js.map +1 -0
  196. package/dist/graph-jsonl.js +4 -4
  197. package/dist/graph-snapshot.js +4 -4
  198. package/dist/graph.js +3 -3
  199. package/dist/harmonic-retrieval.js +17 -17
  200. package/dist/index.js +281 -278
  201. package/dist/lcm/archive.d.ts +12 -2
  202. package/dist/lcm/archive.d.ts.map +1 -1
  203. package/dist/lcm/archive.js +2 -2
  204. package/dist/lcm/engine.d.ts +59 -8
  205. package/dist/lcm/engine.d.ts.map +1 -1
  206. package/dist/lcm/engine.js +6 -6
  207. package/dist/lcm/index.js +16 -16
  208. package/dist/lcm/queue.d.ts +7 -3
  209. package/dist/lcm/queue.d.ts.map +1 -1
  210. package/dist/lcm/queue.js +1 -1
  211. package/dist/lcm/summarizer.d.ts +8 -1
  212. package/dist/lcm/summarizer.d.ts.map +1 -1
  213. package/dist/lcm/summarizer.js +3 -3
  214. package/dist/local-llm.js +4 -4
  215. package/dist/location/index.js +19 -19
  216. package/dist/maintenance/memory-governance.js +18 -18
  217. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +17 -17
  218. package/dist/maintenance/rebuild-memory-projection-runner.js +20 -20
  219. package/dist/maintenance/rebuild-memory-projection.js +19 -19
  220. package/dist/memory-browse-operations.js +83 -80
  221. package/dist/memory-browse.js +6 -6
  222. package/dist/namespaces/migrate.js +18 -18
  223. package/dist/namespaces/search.js +7 -7
  224. package/dist/namespaces/storage.js +17 -17
  225. package/dist/offline-sync-changeset-generations.js +5 -5
  226. package/dist/offline-sync-embedding-generation.js +4 -4
  227. package/dist/offline-sync-snapshot-stream.js +7 -7
  228. package/dist/offline-sync.js +9 -9
  229. package/dist/operator-doctor-corpus.js +21 -21
  230. package/dist/operator-toolkit.js +41 -41
  231. package/dist/orchestration/backlog-scheduler.d.ts +151 -0
  232. package/dist/orchestration/backlog-scheduler.d.ts.map +1 -0
  233. package/dist/orchestration/backlog-scheduler.js +891 -0
  234. package/dist/orchestration/backlog-scheduler.js.map +1 -0
  235. package/dist/orchestration/buffer-drain-runner.d.ts +31 -27
  236. package/dist/orchestration/buffer-drain-runner.d.ts.map +1 -1
  237. package/dist/orchestration/buffer-drain-runner.js +202 -404
  238. package/dist/orchestration/buffer-drain-runner.js.map +1 -1
  239. package/dist/orchestration/extraction-drain.d.ts +50 -17
  240. package/dist/orchestration/extraction-drain.d.ts.map +1 -1
  241. package/dist/orchestration/extraction-queue-coordinator.d.ts +19 -1
  242. package/dist/orchestration/extraction-queue-coordinator.d.ts.map +1 -1
  243. package/dist/orchestration/extraction-run.d.ts +8 -0
  244. package/dist/orchestration/extraction-run.d.ts.map +1 -1
  245. package/dist/orchestration/extraction-task-queueing.d.ts +44 -0
  246. package/dist/orchestration/extraction-task-queueing.d.ts.map +1 -0
  247. package/dist/orchestration/maintenance.js +33 -33
  248. package/dist/orchestration/turn-ingestion.d.ts +11 -5
  249. package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
  250. package/dist/orchestrator.d.ts +13 -0
  251. package/dist/orchestrator.d.ts.map +1 -1
  252. package/dist/orchestrator.js +196 -193
  253. package/dist/qmd.js +4 -4
  254. package/dist/recall-navigation-operations.js +83 -80
  255. package/dist/recall-navigation.js +82 -79
  256. package/dist/recall-state-view-admission.js +6 -6
  257. package/dist/recall-why-service.js +6 -6
  258. package/dist/resume-bundles.js +29 -29
  259. package/dist/search/factory.js +6 -6
  260. package/dist/search/index.js +12 -12
  261. package/dist/semantic-consolidation.js +25 -25
  262. package/dist/semantic-rule-promotion.js +17 -17
  263. package/dist/semantic-rule-verifier.js +17 -17
  264. package/dist/source-agent-qualifier.js +24 -24
  265. package/dist/standup/build.js +17 -17
  266. package/dist/storage/buffer-drain-claims.d.ts +15 -0
  267. package/dist/storage/buffer-drain-claims.d.ts.map +1 -1
  268. package/dist/storage/buffer-drain-claims.js +2 -2
  269. package/dist/storage/buffer-session-files.js +2 -2
  270. package/dist/storage.js +16 -16
  271. package/dist/summarizer.js +6 -6
  272. package/dist/support-passport/index.js +85 -82
  273. package/dist/transfer/backup.js +2 -2
  274. package/dist/transfer/capsule-export.js +2 -2
  275. package/dist/transfer/capsule-import.js +2 -2
  276. package/dist/transfer/export-okf.js +17 -17
  277. package/dist/verified-recall.js +17 -17
  278. package/dist/who-knows-cli.js +18 -18
  279. package/dist/who-knows.js +17 -17
  280. package/package.json +12 -2
  281. package/src/access-observe-lcm-archive.ts +30 -0
  282. package/src/access-observe-lcm-fail-closed.test.ts +118 -0
  283. package/src/access-observe-objective-state.ts +40 -0
  284. package/src/access-observe-stage-timing.test.ts +246 -0
  285. package/src/access-observe-stage-timing.ts +58 -0
  286. package/src/access-observe-write-surface.ts +45 -45
  287. package/src/access-service.ts +1 -0
  288. package/src/backlog-scheduler.test.ts +806 -0
  289. package/src/buffer-drain-handover.test.ts +363 -18
  290. package/src/buffer-drain-inprocess.test.ts +355 -0
  291. package/src/buffer-drain-model-boundary.test.ts +1062 -0
  292. package/src/buffer-drop-extraction-chunk.test.ts +237 -22
  293. package/src/buffer-turn-helpers.ts +26 -1
  294. package/src/buffer.ts +8 -0
  295. package/src/extraction-window-drain.ts +7 -7
  296. package/src/lcm/archive.ts +59 -7
  297. package/src/lcm/engine.ts +256 -53
  298. package/src/lcm/queue.test.ts +32 -14
  299. package/src/lcm/queue.ts +24 -16
  300. package/src/lcm/summarizer.ts +15 -2
  301. package/src/lcm-durability.test.ts +856 -0
  302. package/src/lcm-engine.test.ts +12 -3
  303. package/src/orchestration/backlog-scheduler.ts +481 -0
  304. package/src/orchestration/buffer-drain-runner.ts +545 -180
  305. package/src/orchestration/extraction-drain.ts +287 -108
  306. package/src/orchestration/extraction-queue-coordinator.ts +51 -3
  307. package/src/orchestration/extraction-run.test.ts +41 -0
  308. package/src/orchestration/extraction-run.ts +24 -5
  309. package/src/orchestration/extraction-task-queueing.ts +87 -0
  310. package/src/orchestration/turn-ingestion.ts +46 -59
  311. package/src/orchestrator-extraction-queue.test.ts +46 -2
  312. package/src/orchestrator-flush.test.ts +4 -1
  313. package/src/orchestrator.ts +47 -21
  314. package/src/storage/buffer-drain-claims.ts +55 -0
  315. package/dist/chunk-4MPAKYZU.js.map +0 -1
  316. package/dist/chunk-6WKWL53V.js.map +0 -1
  317. package/dist/chunk-DDBB55L2.js.map +0 -1
  318. package/dist/chunk-KOLJJN5G.js.map +0 -1
  319. package/dist/chunk-MMCO4SRG.js.map +0 -1
  320. package/dist/chunk-R2BIVU2Z.js.map +0 -1
  321. package/dist/chunk-U7MIMS2Y.js.map +0 -1
  322. package/dist/chunk-VA3GNAWQ.js.map +0 -1
  323. package/dist/chunk-WVP2I6C5.js.map +0 -1
  324. package/dist/chunk-Y7427SL7.js.map +0 -1
  325. package/dist/chunk-ZOBYTRBU.js.map +0 -1
  326. /package/dist/{capsule-crypto-GWVG7LGC.js.map → access-observe-lcm-archive.js.map} +0 -0
  327. /package/dist/{graph-edge-decay-OEX5OGMI.js.map → access-observe-objective-state.js.map} +0 -0
  328. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  329. /package/dist/{chunk-CI3MAAIY.js.map → chunk-2R44BBPK.js.map} +0 -0
  330. /package/dist/{chunk-QDZWQBLU.js.map → chunk-2R7OCBRR.js.map} +0 -0
  331. /package/dist/{chunk-B3MWELZ3.js.map → chunk-2WPWEVAH.js.map} +0 -0
  332. /package/dist/{chunk-PLCYKAWH.js.map → chunk-3ICAWZUU.js.map} +0 -0
  333. /package/dist/{chunk-R4ZHF4K2.js.map → chunk-43FLJLT2.js.map} +0 -0
  334. /package/dist/{chunk-PMDOGXA2.js.map → chunk-4PMFSYH7.js.map} +0 -0
  335. /package/dist/{chunk-HXFIILGF.js.map → chunk-4RASUAN3.js.map} +0 -0
  336. /package/dist/{chunk-EI6M5NVK.js.map → chunk-5JBVPJCT.js.map} +0 -0
  337. /package/dist/{chunk-4NV7O76Q.js.map → chunk-5ORJHH43.js.map} +0 -0
  338. /package/dist/{chunk-74AY3S7U.js.map → chunk-5RW247F5.js.map} +0 -0
  339. /package/dist/{chunk-4XINO77L.js.map → chunk-66JOLMWS.js.map} +0 -0
  340. /package/dist/{chunk-AYJQ644F.js.map → chunk-6KBCUIZP.js.map} +0 -0
  341. /package/dist/{chunk-GKL6QYZO.js.map → chunk-6L63IN3M.js.map} +0 -0
  342. /package/dist/{chunk-OPFVXM2D.js.map → chunk-7P5VBRBS.js.map} +0 -0
  343. /package/dist/{chunk-4SOWMA55.js.map → chunk-7WFY2L5J.js.map} +0 -0
  344. /package/dist/{chunk-GJ4HU6LK.js.map → chunk-AGRMXXVA.js.map} +0 -0
  345. /package/dist/{chunk-IVUVQPTQ.js.map → chunk-AZNE2H6D.js.map} +0 -0
  346. /package/dist/{chunk-HPB5TF55.js.map → chunk-BCBY5GYZ.js.map} +0 -0
  347. /package/dist/{chunk-M4IEZGCK.js.map → chunk-BFBS7OFQ.js.map} +0 -0
  348. /package/dist/{chunk-4PG5R6BJ.js.map → chunk-BNU5TQLR.js.map} +0 -0
  349. /package/dist/{chunk-TVE7W67N.js.map → chunk-BTU77RB5.js.map} +0 -0
  350. /package/dist/{chunk-H6VJSITJ.js.map → chunk-CIL4GHMD.js.map} +0 -0
  351. /package/dist/{chunk-KZZ5EZ7O.js.map → chunk-DC3DWF2M.js.map} +0 -0
  352. /package/dist/{chunk-AUFPPBC3.js.map → chunk-ENPJX3BB.js.map} +0 -0
  353. /package/dist/{chunk-CTH2CQCB.js.map → chunk-EQ6EMJRG.js.map} +0 -0
  354. /package/dist/{chunk-63A2BU2B.js.map → chunk-F4ZHKDBW.js.map} +0 -0
  355. /package/dist/{chunk-3LBDEGQA.js.map → chunk-FDUCIUQ5.js.map} +0 -0
  356. /package/dist/{chunk-OCK4D5PC.js.map → chunk-FGGLVCHI.js.map} +0 -0
  357. /package/dist/{chunk-XVZA6VXC.js.map → chunk-FZVROZKC.js.map} +0 -0
  358. /package/dist/{chunk-3RHU5MD7.js.map → chunk-GXAZ4Q2S.js.map} +0 -0
  359. /package/dist/{chunk-UVVFPCPX.js.map → chunk-H2X32F2K.js.map} +0 -0
  360. /package/dist/{chunk-645WOU5I.js.map → chunk-H7BZBSP6.js.map} +0 -0
  361. /package/dist/{chunk-W7K5BP3M.js.map → chunk-HQPFACXO.js.map} +0 -0
  362. /package/dist/{chunk-TECK5IVY.js.map → chunk-HZNURRDS.js.map} +0 -0
  363. /package/dist/{chunk-U2CAGMUD.js.map → chunk-ICBUTKLL.js.map} +0 -0
  364. /package/dist/{chunk-B4DXREWC.js.map → chunk-IQXQ3KWR.js.map} +0 -0
  365. /package/dist/{chunk-AL3TCNSJ.js.map → chunk-J6GZ2254.js.map} +0 -0
  366. /package/dist/{chunk-5MGKGEL7.js.map → chunk-J72ZY7NU.js.map} +0 -0
  367. /package/dist/{chunk-OH55F76C.js.map → chunk-JEX2JL3U.js.map} +0 -0
  368. /package/dist/{chunk-Y3XPSGNF.js.map → chunk-JGLZ46JS.js.map} +0 -0
  369. /package/dist/{chunk-EXQJMFRF.js.map → chunk-JY4LXMZR.js.map} +0 -0
  370. /package/dist/{chunk-6P7U3HWG.js.map → chunk-K36RMINQ.js.map} +0 -0
  371. /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
  372. /package/dist/{chunk-LN4X2NDA.js.map → chunk-KY3KJUNN.js.map} +0 -0
  373. /package/dist/{chunk-H5DIRCZP.js.map → chunk-LHMCZ6VW.js.map} +0 -0
  374. /package/dist/{chunk-GGD2YDS6.js.map → chunk-LN7CWLAU.js.map} +0 -0
  375. /package/dist/{chunk-GHHQLSSY.js.map → chunk-LYOCAUI6.js.map} +0 -0
  376. /package/dist/{chunk-W26D2USW.js.map → chunk-MUH7RR6T.js.map} +0 -0
  377. /package/dist/{chunk-IAGVM7EY.js.map → chunk-N2Y6COEM.js.map} +0 -0
  378. /package/dist/{chunk-64JLBFCQ.js.map → chunk-N7NAGECH.js.map} +0 -0
  379. /package/dist/{chunk-5Y6VA6X2.js.map → chunk-OGQGDOYK.js.map} +0 -0
  380. /package/dist/{chunk-JLMOH5JA.js.map → chunk-OWZXDBV4.js.map} +0 -0
  381. /package/dist/{chunk-ZORS5BG5.js.map → chunk-PJAMFIDE.js.map} +0 -0
  382. /package/dist/{chunk-SXTVZ6JL.js.map → chunk-QIOQI7QZ.js.map} +0 -0
  383. /package/dist/{chunk-75DCOS6I.js.map → chunk-QUPTDKSX.js.map} +0 -0
  384. /package/dist/{chunk-HH73M65H.js.map → chunk-QVZZE6VH.js.map} +0 -0
  385. /package/dist/{chunk-XWE3EOWX.js.map → chunk-RZED7FAU.js.map} +0 -0
  386. /package/dist/{chunk-IZBIDPMY.js.map → chunk-S37O4S6E.js.map} +0 -0
  387. /package/dist/{chunk-NHWDTOBN.js.map → chunk-S73ZVU37.js.map} +0 -0
  388. /package/dist/{chunk-YSATDEBY.js.map → chunk-SBRUTD5O.js.map} +0 -0
  389. /package/dist/{chunk-PRKNAIGZ.js.map → chunk-T6T2RMWW.js.map} +0 -0
  390. /package/dist/{chunk-CIEEYVUB.js.map → chunk-TPX7XXY4.js.map} +0 -0
  391. /package/dist/{chunk-L334PSLW.js.map → chunk-U2A56LLF.js.map} +0 -0
  392. /package/dist/{chunk-P4TV3TQ2.js.map → chunk-U6IL4724.js.map} +0 -0
  393. /package/dist/{chunk-5UM4H64E.js.map → chunk-UAOJVQXC.js.map} +0 -0
  394. /package/dist/{chunk-MOIJWWNA.js.map → chunk-UE5JYPU6.js.map} +0 -0
  395. /package/dist/{chunk-TGQW3AMK.js.map → chunk-UIFWZMO7.js.map} +0 -0
  396. /package/dist/{chunk-UW6LBFYN.js.map → chunk-USX5YIKB.js.map} +0 -0
  397. /package/dist/{chunk-QGFR73SN.js.map → chunk-VK5HKVQ7.js.map} +0 -0
  398. /package/dist/{chunk-IHCXMYSE.js.map → chunk-VPEIWMUT.js.map} +0 -0
  399. /package/dist/{chunk-O5ZINWW3.js.map → chunk-VSJ3ARFS.js.map} +0 -0
  400. /package/dist/{chunk-RLVI2M5Q.js.map → chunk-W4V7P5HP.js.map} +0 -0
  401. /package/dist/{chunk-ELHJK7LM.js.map → chunk-WBN66HPI.js.map} +0 -0
  402. /package/dist/{chunk-BJLCV2P3.js.map → chunk-WGRAB3KB.js.map} +0 -0
  403. /package/dist/{chunk-5KDDAQ6K.js.map → chunk-WN2DASTS.js.map} +0 -0
  404. /package/dist/{chunk-IG3EZ3TF.js.map → chunk-WNQ3B3Q5.js.map} +0 -0
  405. /package/dist/{chunk-V6SBA5O7.js.map → chunk-WYOHQJKC.js.map} +0 -0
  406. /package/dist/{chunk-WHROVQ5Q.js.map → chunk-X4DHAXFO.js.map} +0 -0
  407. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  408. /package/dist/{chunk-JLGBJ4AX.js.map → chunk-XPZM7GP6.js.map} +0 -0
  409. /package/dist/{chunk-64S6X4IU.js.map → chunk-XTN54NML.js.map} +0 -0
  410. /package/dist/{chunk-UYUFDTIX.js.map → chunk-YIXWY4OZ.js.map} +0 -0
  411. /package/dist/{chunk-NCZGGGWI.js.map → chunk-YPYSC3TX.js.map} +0 -0
  412. /package/dist/{chunk-AVGDYYMA.js.map → chunk-ZBET3W26.js.map} +0 -0
@@ -0,0 +1,806 @@
1
+ // Daemon-owned backlog scheduler: ONE bounded in-process loop that claims and
2
+ // drains ALL persisted extraction backlog — startup (existing claims + buffered
3
+ // sessions), new persisted work (ticks), retry-while-pending (backoff, no busy
4
+ // loops) — with bounded memory (counts only, never payload lists) and the
5
+ // queue's own concurrency as the in-flight bound.
6
+ //
7
+ // Claim authority: an existing claim (including an EMPTY ack tombstone) is
8
+ // authoritative over the live buffer's in-memory copy; a tombstoned key is
9
+ // never re-claimed or counted as work, and new ingress consumes the tombstone
10
+ // through the native save path before the key becomes a candidate again.
11
+ import assert from "node:assert/strict";
12
+ import test from "node:test";
13
+
14
+ import {
15
+ BacklogScheduler,
16
+ type BacklogSchedulerDeps,
17
+ type BacklogSchedulerOptions,
18
+ wireBacklogScheduler,
19
+ } from "./orchestration/backlog-scheduler.js";
20
+ import type { DrainSessionOutcome } from "./orchestration/buffer-drain-runner.js";
21
+ import { ExtractionQueueCoordinator } from "./orchestration/extraction-queue-coordinator.js";
22
+ import { Orchestrator } from "./orchestrator.js";
23
+ import {
24
+ cleanupDir,
25
+ makeHarmonicLifecycleConfig,
26
+ mkTempMemoryDir,
27
+ singleFactResult,
28
+ stubExtraction,
29
+ } from "./testing/orchestrator-lite.js";
30
+ import type { BufferTurn } from "./types.js";
31
+
32
+ interface StubState {
33
+ /** Live pending counts per key, as the scheduler's bounded scan sees them. */
34
+ counts: Map<string, { turns: number; retainedTurns: number }>;
35
+ /** Keys the claim store already holds (including empty tombstones). */
36
+ claimed: Set<string>;
37
+ /** Pending turns per existing claim (0 = drained tombstone). */
38
+ claimTurns: Map<string, number>;
39
+ /** Keys the scheduler claimed via the native rename path. */
40
+ claimedThisRun: Set<string>;
41
+ /** Drain outcomes to hand back, in submission order. */
42
+ outcomes: DrainSessionOutcome[];
43
+ /** Settled drain submissions (key, outcome). */
44
+ drained: { key: string; outcome: DrainSessionOutcome }[];
45
+ }
46
+
47
+ function makeDeps(state: StubState, queue: ExtractionQueueCoordinator): BacklogSchedulerDeps {
48
+ return {
49
+ queue,
50
+ claims: {
51
+ list: async () =>
52
+ [...state.claimed].map((key) => ({
53
+ key,
54
+ turnCount: state.claimTurns.get(key) ?? 0,
55
+ leased: false,
56
+ leaseExpired: true,
57
+ })),
58
+ claimFromSessionFile: async (key) => {
59
+ if (state.claimed.has(key) || state.claimedThisRun.has(key)) return "already_claimed";
60
+ const turns = state.counts.get(key)?.turns ?? 0;
61
+ if (turns === 0) return "empty";
62
+ state.claimedThisRun.add(key);
63
+ // The renamed claim holds the session's pending turns.
64
+ state.claimed.add(key);
65
+ state.claimTurns.set(key, turns);
66
+ return "claimed";
67
+ },
68
+ isClaimed: (key) => state.claimed.has(key) || state.claimedThisRun.has(key),
69
+ listPage: async (after, limit) => {
70
+ // Contract: listPage returns claims with pending turns only.
71
+ const names = [...state.claimed].filter((key) => (state.claimTurns.get(key) ?? 0) > 0).sort();
72
+ const start = after === undefined ? 0 : names.findIndex((n) => n > after);
73
+ const begin = start < 0 ? names.length : start;
74
+ const out = names
75
+ .slice(begin, begin + limit)
76
+ .map((key) => ({ key, turnCount: state.claimTurns.get(key) ?? 0, leased: false, leaseExpired: true }));
77
+ const consumed = begin + out.length;
78
+ return { entries: out, nextCursor: consumed < names.length ? names[consumed] : undefined };
79
+ },
80
+ },
81
+ liveBufferCounts: () => state.counts,
82
+ lease: () => ({
83
+ ownerId: "daemon-drain-test",
84
+ pid: process.pid,
85
+ host: "test",
86
+ acquiredAt: new Date().toISOString(),
87
+ heartbeatAt: new Date().toISOString(),
88
+ leaseMs: 60_000,
89
+ }),
90
+ drainKey: async (key) => {
91
+ const outcome = state.outcomes.shift() ?? emptyOutcome(key);
92
+ state.drained.push({ key, outcome });
93
+ // A drained claim is an empty tombstone: authoritative, no pending turns.
94
+ if (outcome.status === "drained") state.claimTurns.set(key, 0);
95
+ return outcome;
96
+ },
97
+ };
98
+ }
99
+
100
+ function emptyOutcome(key: string, overrides: Partial<DrainSessionOutcome> = {}): DrainSessionOutcome {
101
+ return {
102
+ key,
103
+ status: "drained",
104
+ turnsAtStart: 0,
105
+ turnsRemaining: 0,
106
+ persistedCount: 0,
107
+ durableOutputCount: 0,
108
+ ...overrides,
109
+ };
110
+ }
111
+
112
+ function turn(content: string, sessionKey: string, timestamp = "2026-11-01T00:00:00.000Z"): BufferTurn {
113
+ return { role: "user", content, timestamp, sessionKey };
114
+ }
115
+
116
+ test("startup drains existing claims and newly buffered sessions within the queue's concurrency", async () => {
117
+ const queue = new ExtractionQueueCoordinator(2);
118
+ const state: StubState = {
119
+ counts: new Map([
120
+ ["live-1", { turns: 3, retainedTurns: 0 }],
121
+ ["live-2", { turns: 1, retainedTurns: 2 }],
122
+ ]),
123
+ claimed: new Set(["existing-claim"]),
124
+ claimTurns: new Map([["existing-claim", 5]]),
125
+ claimedThisRun: new Set(),
126
+ outcomes: [
127
+ emptyOutcome("existing-claim", { turnsAtStart: 5 }),
128
+ emptyOutcome("live-1", { turnsAtStart: 3 }),
129
+ emptyOutcome("live-2", { turnsAtStart: 3 }),
130
+ ],
131
+ drained: [],
132
+ };
133
+ const scheduler = new BacklogScheduler(makeDeps(state, queue), { pollMs: 3_600_000 });
134
+ scheduler.start();
135
+ try {
136
+ await scheduler.tickForTest();
137
+ await scheduler.settleForTest();
138
+ // The first tick filled the concurrency bound; the next drains the rest.
139
+ await scheduler.tickForTest();
140
+ await scheduler.settleForTest();
141
+ assert.deepEqual(
142
+ state.drained.map((d) => d.key).sort(),
143
+ ["existing-claim", "live-1", "live-2"],
144
+ "startup covers existing claims AND unclaimed buffered work"
145
+ );
146
+ assert.ok(
147
+ state.claimedThisRun.has("live-1") && state.claimedThisRun.has("live-2"),
148
+ "unclaimed work went through the native claim"
149
+ );
150
+ const snap = scheduler.snapshot();
151
+ assert.equal(snap.counters.processed, 3);
152
+ assert.equal(snap.counters.claimed, 3);
153
+ } finally {
154
+ await scheduler.stop();
155
+ }
156
+ });
157
+
158
+ test("an empty ack tombstone is authoritative: no resubmit, no recount", async () => {
159
+ const queue = new ExtractionQueueCoordinator(1);
160
+ const state: StubState = {
161
+ // The live buffer still holds the OLD snapshot (no dirty save since the
162
+ // drain acked) and the claim store holds the EMPTY tombstone.
163
+ counts: new Map([["sess-a", { turns: 2, retainedTurns: 0 }]]),
164
+ claimed: new Set(["sess-a"]),
165
+ claimTurns: new Map([["sess-a", 0]]),
166
+ claimedThisRun: new Set(),
167
+ outcomes: [],
168
+ drained: [],
169
+ };
170
+ const scheduler = new BacklogScheduler(makeDeps(state, queue), { pollMs: 3_600_000 });
171
+ scheduler.start();
172
+ try {
173
+ await scheduler.tickForTest();
174
+ await scheduler.settleForTest();
175
+ assert.equal(state.drained.length, 0, "a tombstoned key is never resubmitted");
176
+ assert.equal(scheduler.snapshot().counters.processed, 0);
177
+ // No ingress: still nothing on the next tick.
178
+ await scheduler.tickForTest();
179
+ await scheduler.settleForTest();
180
+ assert.equal(state.drained.length, 0);
181
+ } finally {
182
+ await scheduler.stop();
183
+ }
184
+ });
185
+
186
+ test("new ingress consumes the tombstone and the key resumes draining", async () => {
187
+ const queue = new ExtractionQueueCoordinator(1);
188
+ const state: StubState = {
189
+ counts: new Map([["sess-a", { turns: 2, retainedTurns: 0 }]]),
190
+ claimed: new Set(["sess-a"]),
191
+ claimTurns: new Map([["sess-a", 0]]),
192
+ claimedThisRun: new Set(),
193
+ outcomes: [emptyOutcome("sess-a", { turnsAtStart: 1, durableOutputCount: 1 })],
194
+ drained: [],
195
+ };
196
+ const scheduler = new BacklogScheduler(makeDeps(state, queue), { pollMs: 3_600_000 });
197
+ scheduler.start();
198
+ try {
199
+ await scheduler.tickForTest();
200
+ await scheduler.settleForTest();
201
+ assert.equal(state.drained.length, 0);
202
+
203
+ // New ingress: the live owner's save consumed the tombstone (native
204
+ // handover path), so the key is unclaimed again with genuinely new work.
205
+ state.claimed.delete("sess-a");
206
+ state.claimTurns.delete("sess-a");
207
+ state.counts.set("sess-a", { turns: 1, retainedTurns: 0 });
208
+ await scheduler.tickForTest();
209
+ await scheduler.settleForTest();
210
+ assert.deepEqual(
211
+ state.drained.map((d) => d.key),
212
+ ["sess-a"],
213
+ "the key resumes and drains exactly once"
214
+ );
215
+ assert.equal(scheduler.snapshot().counters.processed, 1);
216
+ } finally {
217
+ await scheduler.stop();
218
+ }
219
+ });
220
+
221
+ test("failed and denied outcomes back off instead of busy-retrying, and are surfaced", async () => {
222
+ const queue = new ExtractionQueueCoordinator(1);
223
+ const state: StubState = {
224
+ counts: new Map([["bad", { turns: 4, retainedTurns: 0 }]]),
225
+ claimed: new Set(),
226
+ claimTurns: new Map(),
227
+ claimedThisRun: new Set(),
228
+ outcomes: [
229
+ emptyOutcome("bad", { status: "failed", turnsRemaining: 4, reason: "provider down" }),
230
+ emptyOutcome("bad", {
231
+ status: "deferred",
232
+ turnsRemaining: 1,
233
+ reason: "pinned_namespace_unavailable",
234
+ denied: [{ reason: "pinned_namespace_unavailable", turns: 3 }],
235
+ }),
236
+ ],
237
+ drained: [],
238
+ };
239
+ const deps = makeDeps(state, queue);
240
+ // Frozen clock: backoff windows never elapse unless the test moves it.
241
+ let nowMs = 1_000_000;
242
+ const options: BacklogSchedulerOptions = {
243
+ pollMs: 3_600_000,
244
+ retryBaseMs: 60_000,
245
+ retryMaxMs: 900_000,
246
+ now: () => nowMs,
247
+ };
248
+ const scheduler = new BacklogScheduler(deps, options);
249
+ scheduler.start();
250
+ try {
251
+ await scheduler.tickForTest();
252
+ await scheduler.settleForTest();
253
+ const snap1 = scheduler.snapshot();
254
+ assert.equal(snap1.counters.failed, 1, "the failure is counted");
255
+ assert.equal(snap1.keys.find((k) => k.key === "bad")?.state, "backoff");
256
+ // Backoff: an immediate re-tick must NOT resubmit (no busy retry).
257
+ await scheduler.tickForTest();
258
+ await scheduler.settleForTest();
259
+ assert.equal(state.drained.length, 1, "no busy retry inside the backoff window");
260
+
261
+ // Second submission (clock advanced past the backoff window) reports the
262
+ // denial and flips the key state.
263
+ nowMs = 2_000_000;
264
+ await scheduler.tickForTest();
265
+ await scheduler.settleForTest();
266
+ const snap2 = scheduler.snapshot();
267
+ assert.deepEqual(snap2.counters.denied, { keys: 1, turns: 3 }, "denials are surfaced in counters");
268
+ assert.equal(snap2.keys.find((k) => k.key === "bad")?.state, "denied");
269
+ assert.equal(snap2.counters.processed, 0, "denied backlog is never counted processed");
270
+ assert.deepEqual(snap2.keys.find((k) => k.key === "bad")?.reason, "pinned_namespace_unavailable");
271
+ } finally {
272
+ await scheduler.stop();
273
+ }
274
+ });
275
+
276
+ test("the in-flight bound is the queue's own maxConcurrency", async () => {
277
+ const queue = new ExtractionQueueCoordinator(2);
278
+ const state: StubState = {
279
+ counts: new Map([
280
+ ["k1", { turns: 1, retainedTurns: 0 }],
281
+ ["k2", { turns: 1, retainedTurns: 0 }],
282
+ ["k3", { turns: 1, retainedTurns: 0 }],
283
+ ]),
284
+ claimed: new Set(),
285
+ claimTurns: new Map(),
286
+ claimedThisRun: new Set(),
287
+ outcomes: [],
288
+ drained: [],
289
+ };
290
+ const deps = makeDeps(state, queue);
291
+ const gates = new Map<string, () => void>();
292
+ const started: string[] = [];
293
+ const enteredWaiters = new Map<string, Promise<void>>();
294
+ const enteredResolve = new Map<string, () => void>();
295
+ const whenEntered = (key: string): Promise<void> => {
296
+ const existing = enteredWaiters.get(key);
297
+ if (existing) return existing;
298
+ const { promise, resolve } = Promise.withResolvers<void>();
299
+ enteredWaiters.set(key, promise);
300
+ enteredResolve.set(key, resolve);
301
+ return promise;
302
+ };
303
+ const markEntered = (key: string): void => {
304
+ whenEntered(key);
305
+ enteredResolve.get(key)?.();
306
+ };
307
+ const scheduler = new BacklogScheduler(
308
+ {
309
+ ...deps,
310
+ drainKey: async (key) => {
311
+ started.push(key);
312
+ markEntered(key);
313
+ await new Promise<void>((resolve) => gates.set(key, resolve));
314
+ const outcome = emptyOutcome(key);
315
+ state.drained.push({ key, outcome });
316
+ // A drained claim tombstones (no pending turns left to list).
317
+ state.claimTurns.set(key, 0);
318
+ return outcome;
319
+ },
320
+ },
321
+ { pollMs: 3_600_000 }
322
+ );
323
+ scheduler.start();
324
+ try {
325
+ // start()'s initial tick submits up to the bound; drainKey entry is the
326
+ // deterministic signal (no wall-clock waits).
327
+ await whenEntered("k1");
328
+ await whenEntered("k2");
329
+ assert.equal(started.length, 2, "only maxConcurrency submissions are outstanding");
330
+ assert.deepEqual(scheduler.snapshot().inFlight.sort(), ["k1", "k2"]);
331
+ gates.get("k1")?.();
332
+ // Settle k1's SLOT (k2 is still gated and excluded), then the freed slot
333
+ // lets the next candidate submit.
334
+ await scheduler.settleForTest("k2");
335
+ await scheduler.tickForTest();
336
+ await whenEntered("k3");
337
+ assert.equal(started.length, 3, "a settled slot lets the next candidate submit");
338
+ gates.get("k2")?.();
339
+ gates.get("k3")?.();
340
+ await scheduler.settleForTest();
341
+ } finally {
342
+ await scheduler.stop();
343
+ }
344
+ });
345
+
346
+ test("stop aborts in-flight drains; a fresh start resumes the still-claimed backlog", async () => {
347
+ const queue = new ExtractionQueueCoordinator(1);
348
+ const state: StubState = {
349
+ counts: new Map([["slow", { turns: 9, retainedTurns: 0 }]]),
350
+ claimed: new Set(),
351
+ claimTurns: new Map(),
352
+ claimedThisRun: new Set(),
353
+ outcomes: [],
354
+ drained: [],
355
+ };
356
+ const deps = makeDeps(state, queue);
357
+ const observedSignals: AbortSignal[] = [];
358
+ const entered = Promise.withResolvers<void>();
359
+ let release!: () => void;
360
+ const gate = new Promise<void>((resolve) => {
361
+ release = resolve;
362
+ });
363
+ const scheduler = new BacklogScheduler(
364
+ {
365
+ ...deps,
366
+ drainKey: async (key, options) => {
367
+ observedSignals.push(options.signal);
368
+ entered.resolve();
369
+ await gate;
370
+ return emptyOutcome(key, { status: "failed", turnsRemaining: 9, reason: "aborted" });
371
+ },
372
+ },
373
+ { pollMs: 3_600_000 }
374
+ );
375
+ scheduler.start();
376
+ await scheduler.tickForTest();
377
+ await entered.promise;
378
+ const stopping = scheduler.stop();
379
+ release();
380
+ await stopping;
381
+ assert.equal(observedSignals[0]?.aborted, true, "stop aborts in-flight drains");
382
+ assert.equal(scheduler.snapshot().inFlight.length, 0);
383
+ // A fresh scheduler over the same (still-claimed) backlog resumes it.
384
+ const second = new BacklogScheduler(
385
+ {
386
+ ...deps,
387
+ drainKey: async (key) => {
388
+ const outcome = emptyOutcome(key, { turnsAtStart: 9 });
389
+ state.drained.push({ key, outcome });
390
+ // A drained claim tombstones (no pending turns left to list).
391
+ state.claimTurns.set(key, 0);
392
+ return outcome;
393
+ },
394
+ },
395
+ { pollMs: 3_600_000 }
396
+ );
397
+ second.start();
398
+ try {
399
+ await second.tickForTest();
400
+ await second.settleForTest();
401
+ assert.deepEqual(
402
+ state.drained.map((d) => d.key),
403
+ ["slow"],
404
+ "a restart resumes the backlog"
405
+ );
406
+ } finally {
407
+ await second.stop();
408
+ }
409
+ });
410
+
411
+ // ── Real wiring: scheduler over a real orchestrator (native claim path) ────
412
+
413
+ test("a wired scheduler drains real backlog and resumes after tombstone-consuming ingress", async () => {
414
+ const memoryDir = await mkTempMemoryDir("backlog-sched-wired");
415
+ let orchestrator: Orchestrator | undefined;
416
+ try {
417
+ orchestrator = new Orchestrator(
418
+ makeHarmonicLifecycleConfig(memoryDir, { extractionMinChars: 0, extractionMinUserTurns: 1 })
419
+ );
420
+ const calls = stubExtraction(orchestrator, (turns) => singleFactResult(`fact for ${turns[0]?.content ?? "empty"}`));
421
+ const scheduler = wireBacklogScheduler(orchestrator, { pollMs: 3_600_000 });
422
+ try {
423
+ await orchestrator.buffer.addTurn("wired-1", turn("wired one", "wired-1"));
424
+ await orchestrator.buffer.addTurn("wired-2", turn("wired two", "wired-2"));
425
+ await orchestrator.buffer.save();
426
+
427
+ scheduler.start();
428
+ // Concurrency 1: tick/settle until both sessions drained (bounded loop,
429
+ // each iteration an await — no wall-clock waits).
430
+ for (let attempt = 0; attempt < 4 && scheduler.snapshot().counters.processed < 2; attempt += 1) {
431
+ await scheduler.tickForTest();
432
+ await scheduler.settleForTest();
433
+ }
434
+ assert.equal(scheduler.snapshot().counters.processed, 2, "both persisted sessions drained");
435
+ assert.equal(calls.length, 2);
436
+ // The native claim path: session files gone, empty tombstones remain.
437
+ const liveFiles = await orchestrator.storage.loadBuffer();
438
+ assert.equal(liveFiles.entries?.["wired-1"]?.turns.length ?? 0, 0, "the drained session file is consumed");
439
+ const claims = orchestrator.storage.bufferDrainClaims();
440
+ const listed = await claims.list();
441
+ assert.deepEqual(listed.map((l) => l.turnCount).sort(), [0, 0], "both claims are empty tombstones");
442
+
443
+ // New ingress: the live save consumes the tombstone natively; the
444
+ // scheduler resumes the key and drains exactly the new turn.
445
+ await orchestrator.buffer.addTurn("wired-1", turn("wired three", "wired-1", "2026-11-01T00:05:00.000Z"));
446
+ await orchestrator.buffer.save();
447
+ for (let attempt = 0; attempt < 4 && scheduler.snapshot().counters.processed < 3; attempt += 1) {
448
+ await scheduler.tickForTest();
449
+ await scheduler.settleForTest();
450
+ }
451
+ assert.equal(scheduler.snapshot().counters.processed, 3, "ingress after the tombstone resumes draining");
452
+ assert.equal(calls.length, 3);
453
+ assert.deepEqual(
454
+ calls[2]?.map((t) => t.content),
455
+ ["wired three"],
456
+ "only the genuinely new turn extracts — drained turns never resurrect"
457
+ );
458
+ } finally {
459
+ await scheduler.stop();
460
+ }
461
+ } finally {
462
+ await orchestrator?.destroy().catch(() => undefined);
463
+ await cleanupDir(memoryDir);
464
+ }
465
+ });
466
+
467
+ test("a claim decision unsettled mid-rename keeps the key reserved until it settles", async () => {
468
+ const queue = new ExtractionQueueCoordinator(1);
469
+ const state: StubState = {
470
+ counts: new Map([["racy", { turns: 2, retainedTurns: 0 }]]),
471
+ claimed: new Set(),
472
+ claimTurns: new Map(),
473
+ claimedThisRun: new Set(),
474
+ outcomes: [],
475
+ drained: [],
476
+ };
477
+ const deps = makeDeps(state, queue);
478
+ let releaseClaim!: () => void;
479
+ const claimGate = new Promise<void>((resolve) => {
480
+ releaseClaim = resolve;
481
+ });
482
+ const decisionSettled = Promise.withResolvers<void>();
483
+ const liveRuns: string[] = [];
484
+ const scheduler = new BacklogScheduler(
485
+ {
486
+ ...deps,
487
+ claims: {
488
+ ...deps.claims,
489
+ // Pause the claim decision AFTER the synchronous reservation.
490
+ claimFromSessionFile: async (key) => {
491
+ await claimGate;
492
+ state.claimedThisRun.add(key);
493
+ decisionSettled.resolve();
494
+ return "claimed";
495
+ },
496
+ },
497
+ drainKey: async (key) => {
498
+ liveRuns.push(`drain:${key}`);
499
+ const outcome = emptyOutcome(key, { turnsAtStart: 2 });
500
+ state.drained.push({ key, outcome });
501
+ return outcome;
502
+ },
503
+ },
504
+ { pollMs: 3_600_000 }
505
+ );
506
+ scheduler.start();
507
+ try {
508
+ await scheduler.tickForTest();
509
+ // The decision is unsettled: the reservation is held and visible.
510
+ assert.deepEqual(scheduler.snapshot().reservedKeys, ["racy"]);
511
+ // A live task enqueued for the same key cannot start past the gate while
512
+ // the reservation is held (selection skips reserved keys — proven in the
513
+ // queue suite; here the fence is observable through the live stub too).
514
+ assert.equal(
515
+ queue.enqueue(
516
+ async () => {
517
+ liveRuns.push("live:racy");
518
+ },
519
+ { key: "racy" }
520
+ ),
521
+ true
522
+ );
523
+ assert.equal(liveRuns.length, 0, "no live or drain work runs before the decision settles");
524
+ // The decision settles: reservation released, drain runs; the live task
525
+ // (a real pipeline's gate self-skips claimed keys) settles before it.
526
+ releaseClaim();
527
+ await decisionSettled.promise;
528
+ await scheduler.settleForTest();
529
+ assert.equal(state.claimedThisRun.has("racy"), true, "the claim decision completed");
530
+ assert.ok(liveRuns.includes("drain:racy"), "the drain ran after the decision");
531
+ assert.deepEqual(scheduler.snapshot().reservedKeys, [], "the reservation was released with the decision");
532
+ } finally {
533
+ releaseClaim();
534
+ await scheduler.stop();
535
+ }
536
+ });
537
+
538
+ test("stop during a pending discovery submit: no drain runs, restart handles it", async () => {
539
+ const queue = new ExtractionQueueCoordinator(1);
540
+ const state: StubState = {
541
+ counts: new Map([["late", { turns: 1, retainedTurns: 0 }]]),
542
+ claimed: new Set(),
543
+ claimTurns: new Map(),
544
+ claimedThisRun: new Set(),
545
+ outcomes: [],
546
+ drained: [],
547
+ };
548
+ const deps = makeDeps(state, queue);
549
+ let releaseList!: () => void;
550
+ const listGate = new Promise<void>((resolve) => {
551
+ releaseList = resolve;
552
+ });
553
+ const entered = Promise.withResolvers<void>();
554
+ const scheduler = new BacklogScheduler(
555
+ {
556
+ ...deps,
557
+ claims: {
558
+ ...deps.claims,
559
+ list: async () => {
560
+ await listGate;
561
+ return deps.claims.list();
562
+ },
563
+ },
564
+ drainKey: async (key) => {
565
+ entered.resolve();
566
+ state.drained.push({ key, outcome: emptyOutcome(key) });
567
+ return emptyOutcome(key);
568
+ },
569
+ },
570
+ { pollMs: 3_600_000 }
571
+ );
572
+ scheduler.start();
573
+ const stopping = scheduler.stop();
574
+ releaseList();
575
+ await stopping;
576
+ await Promise.race([entered.promise, new Promise((r) => setTimeout(r, 0))]);
577
+ assert.equal(state.drained.length, 0, "no drain may run after stop, even when discovery was pending");
578
+ assert.equal(scheduler.snapshot().inFlight.length, 0);
579
+ void entered;
580
+ });
581
+
582
+ test("a discovery failure must not reject into the detached tick; the next poll retries", async () => {
583
+ const queue = new ExtractionQueueCoordinator(2);
584
+ const state: StubState = {
585
+ counts: new Map([["live-1", { turns: 1, retainedTurns: 0 }]]),
586
+ claimed: new Set(),
587
+ claimTurns: new Map(),
588
+ claimedThisRun: new Set(),
589
+ outcomes: [emptyOutcome("live-1", { turnsAtStart: 1 })],
590
+ drained: [],
591
+ };
592
+ const deps = makeDeps(state, queue);
593
+ let failures = 0;
594
+ deps.claims.listPage = async () => {
595
+ failures += 1;
596
+ if (failures === 1) throw Object.assign(new Error("EACCES: claims dir"), { code: "EACCES" });
597
+ return { entries: [] };
598
+ };
599
+ const scheduler = new BacklogScheduler(deps, { pollMs: 3_600_000 });
600
+ scheduler.start();
601
+ try {
602
+ // start() detaches the first tick: a discovery failure must be caught and
603
+ // logged, never surface as an unhandled rejection that kills the server.
604
+ await scheduler.tickForTest();
605
+ await scheduler.settleForTest();
606
+ // The scheduler is still alive: the next poll discovers and drains.
607
+ await scheduler.tickForTest();
608
+ await scheduler.settleForTest();
609
+ assert.equal(state.drained.length, 1, "the scheduler recovers on the next poll");
610
+ } finally {
611
+ await scheduler.stop();
612
+ }
613
+ });
614
+
615
+ test("a thrown drain leaves the claim intact, counts failed, and backs off", async () => {
616
+ const queue = new ExtractionQueueCoordinator(2);
617
+ const state: StubState = {
618
+ counts: new Map([["throwing-1", { turns: 2, retainedTurns: 0 }]]),
619
+ claimed: new Set(),
620
+ claimTurns: new Map(),
621
+ claimedThisRun: new Set(),
622
+ outcomes: [],
623
+ drained: [],
624
+ };
625
+ const deps = makeDeps(state, queue);
626
+ deps.drainKey = async (key) => {
627
+ state.drained.push({ key, outcome: emptyOutcome(key) });
628
+ throw new Error("provider exploded");
629
+ };
630
+ const scheduler = new BacklogScheduler(deps, { pollMs: 3_600_000 });
631
+ scheduler.start();
632
+ try {
633
+ await scheduler.tickForTest();
634
+ await scheduler.settleForTest();
635
+ assert.equal(state.drained.length, 1, "the drain attempt ran");
636
+ const snapshot = scheduler.snapshot();
637
+ assert.equal(snapshot.counters.failed, 1, "a thrown drain counts failed");
638
+ const keyState = snapshot.keys.find((entry) => entry.key === "throwing-1");
639
+ assert.equal(keyState?.state, "backoff", "the key backs off for a later attempt");
640
+ assert.equal(state.claimTurns.get("throwing-1"), 2, "the claim content is intact in the store");
641
+ } finally {
642
+ await scheduler.stop();
643
+ }
644
+ });
645
+
646
+ test("a deferred outcome without a denial report backs off but never counts denied", async () => {
647
+ const queue = new ExtractionQueueCoordinator(2);
648
+ const state: StubState = {
649
+ counts: new Map([["deferred-1", { turns: 1, retainedTurns: 0 }]]),
650
+ claimed: new Set(),
651
+ claimTurns: new Map(),
652
+ claimedThisRun: new Set(),
653
+ outcomes: [
654
+ {
655
+ key: "deferred-1",
656
+ status: "deferred",
657
+ turnsAtStart: 1,
658
+ turnsRemaining: 1,
659
+ persistedCount: 0,
660
+ durableOutputCount: 0,
661
+ reason: "provider_circuit_open",
662
+ },
663
+ ],
664
+ drained: [],
665
+ };
666
+ const scheduler = new BacklogScheduler(makeDeps(state, queue), { pollMs: 3_600_000 });
667
+ scheduler.start();
668
+ try {
669
+ await scheduler.tickForTest();
670
+ await scheduler.settleForTest();
671
+ const snapshot = scheduler.snapshot();
672
+ const keyState = snapshot.keys.find((entry) => entry.key === "deferred-1");
673
+ assert.equal(keyState?.state, "backoff", "a provider deferral is a backoff, not a denial");
674
+ assert.deepEqual(snapshot.counters.denied, { keys: 0, turns: 0 }, "no denied turns without a denial report");
675
+ assert.equal(snapshot.counters.failed, 0);
676
+ } finally {
677
+ await scheduler.stop();
678
+ }
679
+ });
680
+
681
+ test("a claim creation failure is caught: failed counter, backoff, no crash", async () => {
682
+ const queue = new ExtractionQueueCoordinator(2);
683
+ const state: StubState = {
684
+ counts: new Map([["enospc-1", { turns: 1, retainedTurns: 0 }]]),
685
+ claimed: new Set(),
686
+ claimTurns: new Map(),
687
+ claimedThisRun: new Set(),
688
+ outcomes: [],
689
+ drained: [],
690
+ };
691
+ const deps = makeDeps(state, queue);
692
+ deps.claims.claimFromSessionFile = async () => {
693
+ throw Object.assign(new Error("ENOSPC: no space left on device"), { code: "ENOSPC" });
694
+ };
695
+ const scheduler = new BacklogScheduler(deps, { pollMs: 3_600_000 });
696
+ scheduler.start();
697
+ try {
698
+ // The detached submission must swallow the claim failure: no unhandled
699
+ // rejection, the scheduler stays alive.
700
+ await scheduler.tickForTest();
701
+ await scheduler.settleForTest();
702
+ const snapshot = scheduler.snapshot();
703
+ assert.equal(snapshot.counters.failed, 1, "the claim failure counts failed");
704
+ const keyState = snapshot.keys.find((entry) => entry.key === "enospc-1");
705
+ assert.equal(keyState?.state, "backoff", "the key backs off for a later attempt");
706
+ assert.equal(state.drained.length, 0, "nothing drained without a claim");
707
+ } finally {
708
+ await scheduler.stop();
709
+ }
710
+ });
711
+
712
+ test("a zero concurrency limit disables submissions (the documented '0 disables' contract)", async () => {
713
+ const queue = new ExtractionQueueCoordinator(1);
714
+ const state: StubState = {
715
+ counts: new Map([["live-1", { turns: 1, retainedTurns: 0 }]]),
716
+ claimed: new Set(["live-1"]),
717
+ claimTurns: new Map([["live-1", 1]]),
718
+ claimedThisRun: new Set(),
719
+ outcomes: [emptyOutcome("live-1", { turnsAtStart: 1 })],
720
+ drained: [],
721
+ };
722
+ // A queue that reports a zero limit: the scheduler must submit nothing
723
+ // rather than treat the limit as "one slot".
724
+ Object.defineProperty(queue, "maxConcurrency", { get: () => 0 });
725
+ const scheduler = new BacklogScheduler(makeDeps(state, queue), { pollMs: 3_600_000 });
726
+ scheduler.start();
727
+ try {
728
+ await scheduler.tickForTest();
729
+ await scheduler.settleForTest();
730
+ assert.equal(state.drained.length, 0, "a zero limit submits nothing");
731
+ assert.equal(state.outcomes.length, 1, "the pending outcome is untouched");
732
+ } finally {
733
+ await scheduler.stop();
734
+ }
735
+ });
736
+
737
+ test("a discovery failure after the page cursor moved restores the cursor so the page is re-listed", async () => {
738
+ const queue = new ExtractionQueueCoordinator(1);
739
+ const claimKeys: string[] = [];
740
+ for (let i = 1; i <= 17; i += 1) claimKeys.push("claim-" + String(i).padStart(2, "0"));
741
+ const state: StubState = {
742
+ counts: new Map(claimKeys.map((key) => [key, { turns: 1, retainedTurns: 0 }])),
743
+ claimed: new Set(claimKeys),
744
+ claimTurns: new Map(claimKeys.map((key) => [key, 1])),
745
+ claimedThisRun: new Set(),
746
+ outcomes: claimKeys.map((key) => emptyOutcome(key, { turnsAtStart: 1 })),
747
+ drained: [],
748
+ };
749
+ const deps = makeDeps(state, queue);
750
+ let failLiveCounts = true;
751
+ deps.liveBufferCounts = () => {
752
+ // The page is already listed when this read fails — the cursor moved.
753
+ if (failLiveCounts) {
754
+ failLiveCounts = false;
755
+ throw new Error("EIO: buffer counts");
756
+ }
757
+ return state.counts;
758
+ };
759
+ const scheduler = new BacklogScheduler(deps, { pollMs: 3_600_000 });
760
+ scheduler.start();
761
+ try {
762
+ // start() detaches the first tick: it lists the page (cursor moves) and
763
+ // then the live-count read fails.
764
+ await scheduler.tickForTest();
765
+ await scheduler.settleForTest();
766
+ // The cursor was restored, so the SAME page is listed again and its
767
+ // claims drain — otherwise they wait for the cursor to wrap.
768
+ assert.equal(state.drained.length, 1, "the re-listed page is submitted on the next tick");
769
+ assert.equal(state.drained[0]?.key, claimKeys[0], "the first claim of the page drains first");
770
+ } finally {
771
+ await scheduler.stop();
772
+ }
773
+ });
774
+
775
+ test("claims past the concurrency bound are re-listed on the next tick, in page order", async () => {
776
+ const queue = new ExtractionQueueCoordinator(1);
777
+ const claimKeys: string[] = [];
778
+ for (let i = 1; i <= 17; i += 1) claimKeys.push("claim-" + String(i).padStart(2, "0"));
779
+ const state: StubState = {
780
+ counts: new Map(claimKeys.map((key) => [key, { turns: 1, retainedTurns: 0 }])),
781
+ claimed: new Set(),
782
+ claimTurns: new Map(),
783
+ claimedThisRun: new Set(),
784
+ outcomes: claimKeys.map((key) => emptyOutcome(key, { turnsAtStart: 1 })),
785
+ drained: [],
786
+ };
787
+ const scheduler = new BacklogScheduler(makeDeps(state, queue), { pollMs: 3_600_000 });
788
+ scheduler.start();
789
+ try {
790
+ // Two ticks with concurrency 1: page order must be preserved — the
791
+ // first two claims drain, one per tick, in claim order.
792
+ for (let tick = 1; tick <= 2; tick += 1) {
793
+ await scheduler.tickForTest();
794
+ await scheduler.settleForTest();
795
+ }
796
+ const drainedKeys = state.drained.map((entry) => entry.key);
797
+ assert.deepEqual(drainedKeys.slice(0, 2), [claimKeys[0], claimKeys[1]], "page-order fairness: claims 1 then 2 drain before claim 17");
798
+ for (let tick = 3; tick <= 20; tick += 1) {
799
+ await scheduler.tickForTest();
800
+ await scheduler.settleForTest();
801
+ }
802
+ assert.equal(new Set(state.drained.map((entry) => entry.key)).size, 17, "every claim drains exactly once");
803
+ } finally {
804
+ await scheduler.stop();
805
+ }
806
+ });