@remnic/core 10.0.68 → 10.0.70

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 (356) hide show
  1. package/dist/access-admin-ops-surface.js +80 -77
  2. package/dist/access-authorization-probe.js +81 -78
  3. package/dist/access-boundary.js +80 -77
  4. package/dist/access-cli.js +194 -191
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-extraction-force-flush.js +80 -77
  7. package/dist/access-http-briefing.js +81 -78
  8. package/dist/access-http-query.js +82 -79
  9. package/dist/access-http.js +104 -101
  10. package/dist/access-identity-continuity-surface.js +80 -77
  11. package/dist/access-lcm-surface.js +80 -77
  12. package/dist/access-mcp.js +93 -90
  13. package/dist/access-namespace-preflight.js +80 -77
  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 +80 -77
  27. package/dist/access-offline-manifest.js +8 -8
  28. package/dist/access-operations-batch.js +83 -80
  29. package/dist/access-operations.js +90 -87
  30. package/dist/access-recall-concurrency.js +80 -77
  31. package/dist/access-recall-response.js +80 -77
  32. package/dist/access-recall-surface.js +80 -77
  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 +80 -77
  37. package/dist/access-surface-catalog.js +82 -79
  38. package/dist/access-surface-catalog.js.map +1 -1
  39. package/dist/active-recall.js +24 -24
  40. package/dist/adapters/index.js +4 -4
  41. package/dist/adapters/registry.js +2 -2
  42. package/dist/briefing.js +15 -15
  43. package/dist/buffer.js +8 -8
  44. package/dist/calibration.js +4 -4
  45. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
  46. package/dist/capsule-crypto-7FJQINUR.js.map +1 -0
  47. package/dist/causal-behavior.js +2 -2
  48. package/dist/causal-chain.js +2 -2
  49. package/dist/causal-consolidation.js +20 -20
  50. package/dist/causal-retrieval.js +2 -2
  51. package/dist/causal-trajectory-graph.js +3 -3
  52. package/dist/causal-trajectory.js +1 -1
  53. package/dist/{chunk-NCZGGGWI.js → chunk-2BBMCMJS.js} +12 -12
  54. package/dist/{chunk-UVVFPCPX.js → chunk-2GV24KFD.js} +2 -2
  55. package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  56. package/dist/{chunk-QDZWQBLU.js → chunk-2R7OCBRR.js} +2 -2
  57. package/dist/{chunk-U2CAGMUD.js → chunk-2YPIFJHL.js} +2 -2
  58. package/dist/{chunk-4SOWMA55.js → chunk-3AASGFR4.js} +3 -3
  59. package/dist/{chunk-CI3MAAIY.js → chunk-3F7N4I5I.js} +2 -2
  60. package/dist/{chunk-PLCYKAWH.js → chunk-3ICAWZUU.js} +4 -4
  61. package/dist/{chunk-B4DXREWC.js → chunk-3W37NITP.js} +2 -2
  62. package/dist/{chunk-R4ZHF4K2.js → chunk-43FLJLT2.js} +4 -4
  63. package/dist/{chunk-RLVI2M5Q.js → chunk-4BOBW6H4.js} +2 -2
  64. package/dist/{chunk-HXFIILGF.js → chunk-4RASUAN3.js} +4 -4
  65. package/dist/{chunk-JLMOH5JA.js → chunk-5FMGPV34.js} +2 -2
  66. package/dist/{chunk-EI6M5NVK.js → chunk-5JBVPJCT.js} +2 -2
  67. package/dist/{chunk-74AY3S7U.js → chunk-5RW247F5.js} +10 -10
  68. package/dist/{chunk-P4TV3TQ2.js → chunk-5VQSKBNF.js} +5 -5
  69. package/dist/{chunk-H5DIRCZP.js → chunk-64U42JL5.js} +18 -18
  70. package/dist/{chunk-4XINO77L.js → chunk-66JOLMWS.js} +2 -2
  71. package/dist/{chunk-WHROVQ5Q.js → chunk-66PEACZP.js} +3 -3
  72. package/dist/{chunk-GKL6QYZO.js → chunk-6L63IN3M.js} +1 -1
  73. package/dist/{chunk-Y3XPSGNF.js → chunk-6QS5NQJH.js} +2 -2
  74. package/dist/{chunk-KOLJJN5G.js → chunk-AQNHEDXP.js} +11 -4
  75. package/dist/chunk-AQNHEDXP.js.map +1 -0
  76. package/dist/{chunk-6WKWL53V.js → chunk-B7YAIHAV.js} +127 -121
  77. package/dist/chunk-B7YAIHAV.js.map +1 -0
  78. package/dist/{chunk-M4IEZGCK.js → chunk-BFBS7OFQ.js} +7 -7
  79. package/dist/{chunk-4PG5R6BJ.js → chunk-BNU5TQLR.js} +2 -2
  80. package/dist/{chunk-B3MWELZ3.js → chunk-BW33YTF3.js} +2 -2
  81. package/dist/{chunk-H6VJSITJ.js → chunk-CIL4GHMD.js} +3 -3
  82. package/dist/{chunk-5UM4H64E.js → chunk-D3AGBN6G.js} +2 -2
  83. package/dist/{chunk-IG3EZ3TF.js → chunk-DM3UZUCN.js} +2 -2
  84. package/dist/{chunk-PMDOGXA2.js → chunk-EF3HNVL4.js} +2 -2
  85. package/dist/{chunk-TVE7W67N.js → chunk-EIZ4MK33.js} +2 -2
  86. package/dist/{chunk-AUFPPBC3.js → chunk-ENPJX3BB.js} +3 -3
  87. package/dist/{chunk-63A2BU2B.js → chunk-F4ZHKDBW.js} +5 -5
  88. package/dist/{chunk-3LBDEGQA.js → chunk-FDUCIUQ5.js} +2 -2
  89. package/dist/{chunk-CTH2CQCB.js → chunk-FM6UKZAE.js} +2 -2
  90. package/dist/{chunk-XVZA6VXC.js → chunk-FZVROZKC.js} +4 -4
  91. package/dist/{chunk-IAGVM7EY.js → chunk-GMSRLDVR.js} +2 -2
  92. package/dist/{chunk-HPB5TF55.js → chunk-GT4G2S2Y.js} +3 -3
  93. package/dist/{chunk-645WOU5I.js → chunk-H7BZBSP6.js} +4 -4
  94. package/dist/{chunk-5MGKGEL7.js → chunk-HNF7IMYQ.js} +2 -2
  95. package/dist/{chunk-W7K5BP3M.js → chunk-HQPFACXO.js} +2 -2
  96. package/dist/{chunk-TECK5IVY.js → chunk-HZNURRDS.js} +2 -2
  97. package/dist/{chunk-SXTVZ6JL.js → chunk-I6HRHJRA.js} +2 -2
  98. package/dist/{chunk-NHWDTOBN.js → chunk-ILD7N3XC.js} +20 -20
  99. package/dist/{chunk-4NV7O76Q.js → chunk-J5TVHCDG.js} +2 -2
  100. package/dist/{chunk-AL3TCNSJ.js → chunk-J6GZ2254.js} +4 -4
  101. package/dist/{chunk-OH55F76C.js → chunk-JEX2JL3U.js} +2 -2
  102. package/dist/{chunk-AVGDYYMA.js → chunk-JGQEF33K.js} +3 -3
  103. package/dist/{chunk-6P7U3HWG.js → chunk-K36RMINQ.js} +3 -3
  104. package/dist/{chunk-UW6LBFYN.js → chunk-K47G2CBP.js} +6 -6
  105. package/dist/{chunk-3RHU5MD7.js → chunk-K7LLGCPH.js} +2 -2
  106. package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
  107. package/dist/chunk-L3E7MLMK.js +20 -0
  108. package/dist/chunk-L3E7MLMK.js.map +1 -0
  109. package/dist/{chunk-R2BIVU2Z.js → chunk-LTST4R7U.js} +2 -2
  110. package/dist/{chunk-IZBIDPMY.js → chunk-LYTKNK5O.js} +2 -2
  111. package/dist/{chunk-HH73M65H.js → chunk-MCFI6G7U.js} +3 -3
  112. package/dist/{chunk-V6SBA5O7.js → chunk-MFKBYXBY.js} +5 -5
  113. package/dist/{chunk-AYJQ644F.js → chunk-MK7HXGM6.js} +2 -2
  114. package/dist/{chunk-MOIJWWNA.js → chunk-MKMIY3E7.js} +13 -13
  115. package/dist/{chunk-EXQJMFRF.js → chunk-MRN2MKB7.js} +2 -2
  116. package/dist/{chunk-W26D2USW.js → chunk-MUH7RR6T.js} +6 -6
  117. package/dist/{chunk-64JLBFCQ.js → chunk-N7NAGECH.js} +2 -2
  118. package/dist/{chunk-75DCOS6I.js → chunk-OAKOAOEG.js} +67 -67
  119. package/dist/{chunk-FAF4A76Q.js → chunk-OAQO5AZJ.js} +512 -378
  120. package/dist/chunk-OAQO5AZJ.js.map +1 -0
  121. package/dist/{chunk-OPFVXM2D.js → chunk-OX6GY22Q.js} +2 -2
  122. package/dist/chunk-QCFZDYYG.js +34 -0
  123. package/dist/chunk-QCFZDYYG.js.map +1 -0
  124. package/dist/{chunk-MMCO4SRG.js → chunk-QNN4FYBD.js} +19 -11
  125. package/dist/chunk-QNN4FYBD.js.map +1 -0
  126. package/dist/{chunk-XWE3EOWX.js → chunk-RZED7FAU.js} +2 -2
  127. package/dist/{chunk-YSATDEBY.js → chunk-SBRUTD5O.js} +5 -5
  128. package/dist/{chunk-WVP2I6C5.js → chunk-SDL22RRW.js} +50 -11
  129. package/dist/chunk-SDL22RRW.js.map +1 -0
  130. package/dist/{chunk-ZORS5BG5.js → chunk-SQO5SEKA.js} +10 -10
  131. package/dist/{chunk-PRKNAIGZ.js → chunk-T6T2RMWW.js} +5 -5
  132. package/dist/{chunk-O5ZINWW3.js → chunk-THFSCUFC.js} +2 -2
  133. package/dist/{chunk-CIEEYVUB.js → chunk-TPX7XXY4.js} +2 -2
  134. package/dist/{chunk-OCK4D5PC.js → chunk-TSZP7GGJ.js} +2 -2
  135. package/dist/{chunk-L334PSLW.js → chunk-U2A56LLF.js} +8 -8
  136. package/dist/{chunk-TGQW3AMK.js → chunk-UIFWZMO7.js} +23 -23
  137. package/dist/{chunk-LN4X2NDA.js → chunk-UJ2FP2RV.js} +10 -10
  138. package/dist/{chunk-4MPAKYZU.js → chunk-VDYRXI3E.js} +5 -5
  139. package/dist/{chunk-IHCXMYSE.js → chunk-VPEIWMUT.js} +2 -2
  140. package/dist/{chunk-ELHJK7LM.js → chunk-WBN66HPI.js} +6 -6
  141. package/dist/{chunk-BJLCV2P3.js → chunk-WGRAB3KB.js} +2 -2
  142. package/dist/{chunk-IVUVQPTQ.js → chunk-WIW4LOZL.js} +2 -2
  143. package/dist/{chunk-5KDDAQ6K.js → chunk-WN2DASTS.js} +10 -10
  144. package/dist/chunk-WZU5AGO6.js +38 -0
  145. package/dist/chunk-WZU5AGO6.js.map +1 -0
  146. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  147. package/dist/{chunk-5Y6VA6X2.js → chunk-XDUP6SWG.js} +2 -2
  148. package/dist/{chunk-GHHQLSSY.js → chunk-XR4QR3JT.js} +3 -3
  149. package/dist/{chunk-64S6X4IU.js → chunk-XTN54NML.js} +4 -4
  150. package/dist/{chunk-QGFR73SN.js → chunk-YIG4HPY4.js} +2 -2
  151. package/dist/{chunk-UYUFDTIX.js → chunk-YIXWY4OZ.js} +2 -2
  152. package/dist/{chunk-GJ4HU6LK.js → chunk-YLMTFRZR.js} +2 -2
  153. package/dist/{chunk-GGD2YDS6.js → chunk-YRMSHA7H.js} +2 -2
  154. package/dist/{chunk-Y7427SL7.js → chunk-ZG4GNPRE.js} +216 -50
  155. package/dist/chunk-ZG4GNPRE.js.map +1 -0
  156. package/dist/{chunk-KZZ5EZ7O.js → chunk-ZS5LREUZ.js} +2 -2
  157. package/dist/{chunk-JLGBJ4AX.js → chunk-ZYEWTVNQ.js} +10 -10
  158. package/dist/cli.js +131 -128
  159. package/dist/coding/export-okf-codegraph.js +14 -14
  160. package/dist/coding/pre-action-gate.js +2 -2
  161. package/dist/compounding/engine.js +16 -16
  162. package/dist/config.js +24 -24
  163. package/dist/connectors/codex-materialize-runner.js +17 -17
  164. package/dist/connectors/codex-materialize.js +2 -2
  165. package/dist/connectors/index.js +18 -18
  166. package/dist/consolidation-provenance-check.js +2 -2
  167. package/dist/convergence-refresh.js +18 -18
  168. package/dist/corpus-watermark.js +18 -18
  169. package/dist/deep-recall-http-glue.js +13 -13
  170. package/dist/deep-recall.js +2 -2
  171. package/dist/embedding-fallback.js +2 -2
  172. package/dist/entity-retrieval.js +18 -18
  173. package/dist/event-order-recall.js +2 -2
  174. package/dist/external-wiki-access.js +81 -78
  175. package/dist/extraction-span-materialize.js +2 -2
  176. package/dist/extraction.js +30 -30
  177. package/dist/{graph-edge-decay-OEX5OGMI.js → graph-edge-decay-CXTNJ7IQ.js} +6 -6
  178. package/dist/graph-edge-decay-CXTNJ7IQ.js.map +1 -0
  179. package/dist/graph-jsonl.js +4 -4
  180. package/dist/graph-snapshot.js +4 -4
  181. package/dist/graph.js +3 -3
  182. package/dist/harmonic-retrieval.js +15 -15
  183. package/dist/index.js +279 -276
  184. package/dist/lcm/archive.d.ts +12 -2
  185. package/dist/lcm/archive.d.ts.map +1 -1
  186. package/dist/lcm/archive.js +2 -2
  187. package/dist/lcm/engine.d.ts +59 -8
  188. package/dist/lcm/engine.d.ts.map +1 -1
  189. package/dist/lcm/engine.js +6 -6
  190. package/dist/lcm/index.js +16 -16
  191. package/dist/lcm/queue.d.ts +7 -3
  192. package/dist/lcm/queue.d.ts.map +1 -1
  193. package/dist/lcm/queue.js +1 -1
  194. package/dist/lcm/summarizer.d.ts +8 -1
  195. package/dist/lcm/summarizer.d.ts.map +1 -1
  196. package/dist/lcm/summarizer.js +3 -3
  197. package/dist/local-llm.js +4 -4
  198. package/dist/location/index.js +17 -17
  199. package/dist/maintenance/memory-governance.js +16 -16
  200. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +15 -15
  201. package/dist/maintenance/rebuild-memory-projection-runner.js +18 -18
  202. package/dist/maintenance/rebuild-memory-projection.js +17 -17
  203. package/dist/memory-browse-operations.js +81 -78
  204. package/dist/memory-browse.js +6 -6
  205. package/dist/namespaces/migrate.js +16 -16
  206. package/dist/namespaces/search.js +7 -7
  207. package/dist/namespaces/storage.js +15 -15
  208. package/dist/offline-sync-changeset-generations.js +5 -5
  209. package/dist/offline-sync-embedding-generation.js +4 -4
  210. package/dist/offline-sync-snapshot-stream.js +7 -7
  211. package/dist/offline-sync.js +9 -9
  212. package/dist/operator-doctor-corpus.js +19 -19
  213. package/dist/operator-toolkit.js +39 -39
  214. package/dist/orchestration/buffer-drain-runner.js +194 -191
  215. package/dist/orchestration/buffer-drain-runner.js.map +1 -1
  216. package/dist/orchestration/extraction-persist.d.ts.map +1 -1
  217. package/dist/orchestration/maintenance.js +31 -31
  218. package/dist/orchestration/promotion-dedup-window.d.ts +118 -0
  219. package/dist/orchestration/promotion-dedup-window.d.ts.map +1 -0
  220. package/dist/orchestrator.js +195 -192
  221. package/dist/qmd.js +4 -4
  222. package/dist/recall-navigation-operations.js +81 -78
  223. package/dist/recall-navigation.js +80 -77
  224. package/dist/recall-state-view-admission.js +6 -6
  225. package/dist/recall-why-service.js +6 -6
  226. package/dist/resume-bundles.js +27 -27
  227. package/dist/search/factory.js +6 -6
  228. package/dist/search/index.js +12 -12
  229. package/dist/semantic-consolidation.js +23 -23
  230. package/dist/semantic-rule-promotion.js +15 -15
  231. package/dist/semantic-rule-verifier.js +15 -15
  232. package/dist/source-agent-qualifier.js +22 -22
  233. package/dist/standup/build.js +15 -15
  234. package/dist/storage.js +14 -14
  235. package/dist/summarizer.js +6 -6
  236. package/dist/support-passport/index.js +83 -80
  237. package/dist/transfer/backup.js +2 -2
  238. package/dist/transfer/capsule-export.js +2 -2
  239. package/dist/transfer/capsule-import.js +2 -2
  240. package/dist/transfer/export-okf.js +15 -15
  241. package/dist/verified-recall.js +15 -15
  242. package/dist/who-knows-cli.js +16 -16
  243. package/dist/who-knows.js +15 -15
  244. package/package.json +2 -2
  245. package/src/access-observe-lcm-archive.ts +30 -0
  246. package/src/access-observe-lcm-fail-closed.test.ts +118 -0
  247. package/src/access-observe-objective-state.ts +40 -0
  248. package/src/access-observe-stage-timing.test.ts +246 -0
  249. package/src/access-observe-stage-timing.ts +58 -0
  250. package/src/access-observe-write-surface.ts +45 -45
  251. package/src/access-service.ts +1 -0
  252. package/src/extraction-shared-promotion-parallel-dedup.test.ts +212 -0
  253. package/src/lcm/archive.ts +59 -7
  254. package/src/lcm/engine.ts +256 -53
  255. package/src/lcm/queue.test.ts +32 -14
  256. package/src/lcm/queue.ts +24 -16
  257. package/src/lcm/summarizer.ts +15 -2
  258. package/src/lcm-durability.test.ts +856 -0
  259. package/src/lcm-engine.test.ts +12 -3
  260. package/src/orchestration/extraction-persist.ts +40 -35
  261. package/src/orchestration/promotion-dedup-window.ts +319 -0
  262. package/dist/chunk-6WKWL53V.js.map +0 -1
  263. package/dist/chunk-FAF4A76Q.js.map +0 -1
  264. package/dist/chunk-KOLJJN5G.js.map +0 -1
  265. package/dist/chunk-MMCO4SRG.js.map +0 -1
  266. package/dist/chunk-WVP2I6C5.js.map +0 -1
  267. package/dist/chunk-Y7427SL7.js.map +0 -1
  268. /package/dist/{capsule-crypto-GWVG7LGC.js.map → access-observe-lcm-archive.js.map} +0 -0
  269. /package/dist/{graph-edge-decay-OEX5OGMI.js.map → access-observe-objective-state.js.map} +0 -0
  270. /package/dist/{chunk-NCZGGGWI.js.map → chunk-2BBMCMJS.js.map} +0 -0
  271. /package/dist/{chunk-UVVFPCPX.js.map → chunk-2GV24KFD.js.map} +0 -0
  272. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  273. /package/dist/{chunk-QDZWQBLU.js.map → chunk-2R7OCBRR.js.map} +0 -0
  274. /package/dist/{chunk-U2CAGMUD.js.map → chunk-2YPIFJHL.js.map} +0 -0
  275. /package/dist/{chunk-4SOWMA55.js.map → chunk-3AASGFR4.js.map} +0 -0
  276. /package/dist/{chunk-CI3MAAIY.js.map → chunk-3F7N4I5I.js.map} +0 -0
  277. /package/dist/{chunk-PLCYKAWH.js.map → chunk-3ICAWZUU.js.map} +0 -0
  278. /package/dist/{chunk-B4DXREWC.js.map → chunk-3W37NITP.js.map} +0 -0
  279. /package/dist/{chunk-R4ZHF4K2.js.map → chunk-43FLJLT2.js.map} +0 -0
  280. /package/dist/{chunk-RLVI2M5Q.js.map → chunk-4BOBW6H4.js.map} +0 -0
  281. /package/dist/{chunk-HXFIILGF.js.map → chunk-4RASUAN3.js.map} +0 -0
  282. /package/dist/{chunk-JLMOH5JA.js.map → chunk-5FMGPV34.js.map} +0 -0
  283. /package/dist/{chunk-EI6M5NVK.js.map → chunk-5JBVPJCT.js.map} +0 -0
  284. /package/dist/{chunk-74AY3S7U.js.map → chunk-5RW247F5.js.map} +0 -0
  285. /package/dist/{chunk-P4TV3TQ2.js.map → chunk-5VQSKBNF.js.map} +0 -0
  286. /package/dist/{chunk-H5DIRCZP.js.map → chunk-64U42JL5.js.map} +0 -0
  287. /package/dist/{chunk-4XINO77L.js.map → chunk-66JOLMWS.js.map} +0 -0
  288. /package/dist/{chunk-WHROVQ5Q.js.map → chunk-66PEACZP.js.map} +0 -0
  289. /package/dist/{chunk-GKL6QYZO.js.map → chunk-6L63IN3M.js.map} +0 -0
  290. /package/dist/{chunk-Y3XPSGNF.js.map → chunk-6QS5NQJH.js.map} +0 -0
  291. /package/dist/{chunk-M4IEZGCK.js.map → chunk-BFBS7OFQ.js.map} +0 -0
  292. /package/dist/{chunk-4PG5R6BJ.js.map → chunk-BNU5TQLR.js.map} +0 -0
  293. /package/dist/{chunk-B3MWELZ3.js.map → chunk-BW33YTF3.js.map} +0 -0
  294. /package/dist/{chunk-H6VJSITJ.js.map → chunk-CIL4GHMD.js.map} +0 -0
  295. /package/dist/{chunk-5UM4H64E.js.map → chunk-D3AGBN6G.js.map} +0 -0
  296. /package/dist/{chunk-IG3EZ3TF.js.map → chunk-DM3UZUCN.js.map} +0 -0
  297. /package/dist/{chunk-PMDOGXA2.js.map → chunk-EF3HNVL4.js.map} +0 -0
  298. /package/dist/{chunk-TVE7W67N.js.map → chunk-EIZ4MK33.js.map} +0 -0
  299. /package/dist/{chunk-AUFPPBC3.js.map → chunk-ENPJX3BB.js.map} +0 -0
  300. /package/dist/{chunk-63A2BU2B.js.map → chunk-F4ZHKDBW.js.map} +0 -0
  301. /package/dist/{chunk-3LBDEGQA.js.map → chunk-FDUCIUQ5.js.map} +0 -0
  302. /package/dist/{chunk-CTH2CQCB.js.map → chunk-FM6UKZAE.js.map} +0 -0
  303. /package/dist/{chunk-XVZA6VXC.js.map → chunk-FZVROZKC.js.map} +0 -0
  304. /package/dist/{chunk-IAGVM7EY.js.map → chunk-GMSRLDVR.js.map} +0 -0
  305. /package/dist/{chunk-HPB5TF55.js.map → chunk-GT4G2S2Y.js.map} +0 -0
  306. /package/dist/{chunk-645WOU5I.js.map → chunk-H7BZBSP6.js.map} +0 -0
  307. /package/dist/{chunk-5MGKGEL7.js.map → chunk-HNF7IMYQ.js.map} +0 -0
  308. /package/dist/{chunk-W7K5BP3M.js.map → chunk-HQPFACXO.js.map} +0 -0
  309. /package/dist/{chunk-TECK5IVY.js.map → chunk-HZNURRDS.js.map} +0 -0
  310. /package/dist/{chunk-SXTVZ6JL.js.map → chunk-I6HRHJRA.js.map} +0 -0
  311. /package/dist/{chunk-NHWDTOBN.js.map → chunk-ILD7N3XC.js.map} +0 -0
  312. /package/dist/{chunk-4NV7O76Q.js.map → chunk-J5TVHCDG.js.map} +0 -0
  313. /package/dist/{chunk-AL3TCNSJ.js.map → chunk-J6GZ2254.js.map} +0 -0
  314. /package/dist/{chunk-OH55F76C.js.map → chunk-JEX2JL3U.js.map} +0 -0
  315. /package/dist/{chunk-AVGDYYMA.js.map → chunk-JGQEF33K.js.map} +0 -0
  316. /package/dist/{chunk-6P7U3HWG.js.map → chunk-K36RMINQ.js.map} +0 -0
  317. /package/dist/{chunk-UW6LBFYN.js.map → chunk-K47G2CBP.js.map} +0 -0
  318. /package/dist/{chunk-3RHU5MD7.js.map → chunk-K7LLGCPH.js.map} +0 -0
  319. /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
  320. /package/dist/{chunk-R2BIVU2Z.js.map → chunk-LTST4R7U.js.map} +0 -0
  321. /package/dist/{chunk-IZBIDPMY.js.map → chunk-LYTKNK5O.js.map} +0 -0
  322. /package/dist/{chunk-HH73M65H.js.map → chunk-MCFI6G7U.js.map} +0 -0
  323. /package/dist/{chunk-V6SBA5O7.js.map → chunk-MFKBYXBY.js.map} +0 -0
  324. /package/dist/{chunk-AYJQ644F.js.map → chunk-MK7HXGM6.js.map} +0 -0
  325. /package/dist/{chunk-MOIJWWNA.js.map → chunk-MKMIY3E7.js.map} +0 -0
  326. /package/dist/{chunk-EXQJMFRF.js.map → chunk-MRN2MKB7.js.map} +0 -0
  327. /package/dist/{chunk-W26D2USW.js.map → chunk-MUH7RR6T.js.map} +0 -0
  328. /package/dist/{chunk-64JLBFCQ.js.map → chunk-N7NAGECH.js.map} +0 -0
  329. /package/dist/{chunk-75DCOS6I.js.map → chunk-OAKOAOEG.js.map} +0 -0
  330. /package/dist/{chunk-OPFVXM2D.js.map → chunk-OX6GY22Q.js.map} +0 -0
  331. /package/dist/{chunk-XWE3EOWX.js.map → chunk-RZED7FAU.js.map} +0 -0
  332. /package/dist/{chunk-YSATDEBY.js.map → chunk-SBRUTD5O.js.map} +0 -0
  333. /package/dist/{chunk-ZORS5BG5.js.map → chunk-SQO5SEKA.js.map} +0 -0
  334. /package/dist/{chunk-PRKNAIGZ.js.map → chunk-T6T2RMWW.js.map} +0 -0
  335. /package/dist/{chunk-O5ZINWW3.js.map → chunk-THFSCUFC.js.map} +0 -0
  336. /package/dist/{chunk-CIEEYVUB.js.map → chunk-TPX7XXY4.js.map} +0 -0
  337. /package/dist/{chunk-OCK4D5PC.js.map → chunk-TSZP7GGJ.js.map} +0 -0
  338. /package/dist/{chunk-L334PSLW.js.map → chunk-U2A56LLF.js.map} +0 -0
  339. /package/dist/{chunk-TGQW3AMK.js.map → chunk-UIFWZMO7.js.map} +0 -0
  340. /package/dist/{chunk-LN4X2NDA.js.map → chunk-UJ2FP2RV.js.map} +0 -0
  341. /package/dist/{chunk-4MPAKYZU.js.map → chunk-VDYRXI3E.js.map} +0 -0
  342. /package/dist/{chunk-IHCXMYSE.js.map → chunk-VPEIWMUT.js.map} +0 -0
  343. /package/dist/{chunk-ELHJK7LM.js.map → chunk-WBN66HPI.js.map} +0 -0
  344. /package/dist/{chunk-BJLCV2P3.js.map → chunk-WGRAB3KB.js.map} +0 -0
  345. /package/dist/{chunk-IVUVQPTQ.js.map → chunk-WIW4LOZL.js.map} +0 -0
  346. /package/dist/{chunk-5KDDAQ6K.js.map → chunk-WN2DASTS.js.map} +0 -0
  347. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  348. /package/dist/{chunk-5Y6VA6X2.js.map → chunk-XDUP6SWG.js.map} +0 -0
  349. /package/dist/{chunk-GHHQLSSY.js.map → chunk-XR4QR3JT.js.map} +0 -0
  350. /package/dist/{chunk-64S6X4IU.js.map → chunk-XTN54NML.js.map} +0 -0
  351. /package/dist/{chunk-QGFR73SN.js.map → chunk-YIG4HPY4.js.map} +0 -0
  352. /package/dist/{chunk-UYUFDTIX.js.map → chunk-YIXWY4OZ.js.map} +0 -0
  353. /package/dist/{chunk-GJ4HU6LK.js.map → chunk-YLMTFRZR.js.map} +0 -0
  354. /package/dist/{chunk-GGD2YDS6.js.map → chunk-YRMSHA7H.js.map} +0 -0
  355. /package/dist/{chunk-KZZ5EZ7O.js.map → chunk-ZS5LREUZ.js.map} +0 -0
  356. /package/dist/{chunk-JLGBJ4AX.js.map → chunk-ZYEWTVNQ.js.map} +0 -0
@@ -188,20 +188,72 @@ export class LcmArchive {
188
188
  .all(sessionId, fromTurn, toTurn) as LcmMessage[];
189
189
  }
190
190
 
191
- /** Retrieve unsummarized messages (after last leaf summary). */
192
- getUnsummarizedMessages(sessionId: string): LcmMessage[] {
193
- const lastLeafEnd = this.db
191
+ /** Highest turn index covered by a leaf summary (-1 when none exist). */
192
+ private lastLeafSummaryEnd(sessionId: string): number {
193
+ // SAFETY: the query returns exactly one aggregate column, `last_end`.
194
+ const row = this.db
194
195
  .prepare(
195
196
  "SELECT MAX(msg_end) as last_end FROM lcm_summary_nodes WHERE session_id = ? AND depth = 0",
196
197
  )
197
198
  .get(sessionId) as { last_end: number | null } | undefined;
199
+ return row?.last_end ?? -1;
200
+ }
198
201
 
199
- const lastSummarized = lastLeafEnd?.last_end ?? -1;
200
- return this.db
202
+ /** Retrieve unsummarized messages (after last leaf summary), optionally bounded. */
203
+ getUnsummarizedMessages(sessionId: string, limit?: number): LcmMessage[] {
204
+ const lastSummarized = this.lastLeafSummaryEnd(sessionId);
205
+ if (limit !== undefined && limit <= 0) return [];
206
+
207
+ const stmt = this.db.prepare(
208
+ limit === undefined
209
+ ? "SELECT * FROM lcm_messages WHERE session_id = ? AND turn_index > ? ORDER BY turn_index"
210
+ : "SELECT * FROM lcm_messages WHERE session_id = ? AND turn_index > ? ORDER BY turn_index LIMIT ?",
211
+ );
212
+ // SAFETY: the statement selects the lcm_messages row shape.
213
+ const rows = (
214
+ limit === undefined ? stmt.all(sessionId, lastSummarized) : stmt.all(sessionId, lastSummarized, limit)
215
+ ) as LcmMessage[];
216
+ return rows;
217
+ }
218
+
219
+ /** Count archived turns that no leaf summary covers yet. */
220
+ countUnsummarizedMessages(sessionId: string): number {
221
+ const lastSummarized = this.lastLeafSummaryEnd(sessionId);
222
+ // SAFETY: the query returns exactly one aggregate column, `n`.
223
+ const row = this.db
224
+ .prepare(
225
+ "SELECT COUNT(*) as n FROM lcm_messages WHERE session_id = ? AND turn_index > ?",
226
+ )
227
+ .get(sessionId, lastSummarized) as { n: number };
228
+ return row.n;
229
+ }
230
+
231
+ /**
232
+ * Sessions whose durable archive still holds at least `minTurns`
233
+ * unsummarized turns. Metadata only — used to resume outstanding summary
234
+ * work after a restart.
235
+ */
236
+ listSessionsWithUnsummarizedTurns(minTurns: number): string[] {
237
+ if (minTurns <= 0) return [];
238
+ // SAFETY: the query returns one `session_id` column per session.
239
+ const rows = this.db
201
240
  .prepare(
202
- "SELECT * FROM lcm_messages WHERE session_id = ? AND turn_index > ? ORDER BY turn_index",
241
+ `
242
+ SELECT m.session_id AS session_id
243
+ FROM lcm_messages m
244
+ LEFT JOIN (
245
+ SELECT session_id, MAX(msg_end) AS last_end
246
+ FROM lcm_summary_nodes
247
+ WHERE depth = 0
248
+ GROUP BY session_id
249
+ ) leaves ON leaves.session_id = m.session_id
250
+ WHERE m.turn_index > COALESCE(leaves.last_end, -1)
251
+ GROUP BY m.session_id
252
+ HAVING COUNT(*) >= ?
253
+ `,
203
254
  )
204
- .all(sessionId, lastSummarized) as LcmMessage[];
255
+ .all(minTurns) as Array<{ session_id: string }>;
256
+ return rows.map((row) => row.session_id);
205
257
  }
206
258
 
207
259
  /** Full-text search across all messages. */
package/src/lcm/engine.ts CHANGED
@@ -96,6 +96,17 @@ const EMPTY_LCM_STATS = {
96
96
  maxDepth: -1,
97
97
  } as const;
98
98
 
99
+ /**
100
+ * Upper bound on leaf batches summarized by one background job. Keeps a single
101
+ * job's archive read and wall time bounded; leftover batches stay durable and
102
+ * the job re-queues itself.
103
+ */
104
+ const LCM_SUMMARIZE_MAX_LEAF_BATCHES_PER_JOB = 16;
105
+
106
+ /** Retry pacing for a failed summary pass: base delay and backoff ceiling. */
107
+ const LCM_SUMMARIZE_RETRY_BASE_MS = 500;
108
+ const LCM_SUMMARIZE_RETRY_MAX_MS = 30_000;
109
+
99
110
  export class LcmEngine {
100
111
  private db: Database.Database | null = null;
101
112
  private archive: LcmArchive | null = null;
@@ -106,6 +117,17 @@ export class LcmEngine {
106
117
  private readonly config: LcmEngineConfig;
107
118
  private readonly memoryDir: string;
108
119
  private initPromise: Promise<void> | null = null;
120
+ private readonly summarizeRetryTimers = new Map<string, NodeJS.Timeout>();
121
+ private readonly summarizeRetryAttempts = new Map<string, number>();
122
+ /** Resolved when the session's pending retry timer has fired or been cleared. */
123
+ private readonly summarizeRetryWaiters = new Map<
124
+ string,
125
+ { promise: Promise<void>; resolve: () => void }
126
+ >();
127
+ /** Explicit idle waiters per session; referenced only while nonzero. */
128
+ private readonly summarizeRetryIdleWaiters = new Map<string, number>();
129
+ /** Base backoff delay; tests shrink it to keep the retry loop fast. */
130
+ private summarizeRetryBaseMs = LCM_SUMMARIZE_RETRY_BASE_MS;
109
131
  private readonly pendingObserveInitCounts = new Map<string, number>();
110
132
  private readonly pendingObserveInitWaiters = new Map<string, Array<() => void>>();
111
133
  private readonly pendingObserveInitIdleWaiters: Array<() => void> = [];
@@ -158,8 +180,8 @@ export class LcmEngine {
158
180
  );
159
181
  const observeQueue = new LcmWorkQueue({
160
182
  concurrency: this.config.observeConcurrency,
161
- worker: async (sessionId, messages) => {
162
- await this.processObserveMessages(sessionId, messages);
183
+ worker: async (sessionId) => {
184
+ await this.processSummarizeJob(sessionId);
163
185
  },
164
186
  hooks: {
165
187
  onJobStart: ({ sessionId, depth, inFlight, waitMs }) => {
@@ -200,12 +222,31 @@ export class LcmEngine {
200
222
  this.dag = dag;
201
223
  this.summarizer = summarizer;
202
224
  this.observeQueue = observeQueue;
225
+ this.resumeOutstandingSummaries();
203
226
  log.info("LCM engine initialized");
204
227
  }
205
228
 
206
229
  /**
207
- * Enqueue messages from agent_end hook.
208
- * The queue worker performs the archive append and incremental summarization.
230
+ * Restart resume: sessions whose durable archive still holds complete
231
+ * unsummarized batches get a summary job without any new observe call.
232
+ * Only session references are queued — the content already lives in SQLite.
233
+ */
234
+ private resumeOutstandingSummaries(): void {
235
+ if (!this.archive || !this.observeQueue) return;
236
+ const sessions = this.archive.listSessionsWithUnsummarizedTurns(
237
+ this.config.leafBatchSize,
238
+ );
239
+ if (sessions.length === 0) return;
240
+ for (const sessionId of sessions) this.observeQueue.enqueue(sessionId);
241
+ log.info(`LCM resume scheduled summary jobs: sessions=${sessions.length}`);
242
+ }
243
+
244
+ /**
245
+ * Archive observed messages and schedule background summarization.
246
+ *
247
+ * The archive append completes BEFORE this resolves: an accepted observe is
248
+ * already durable in SQLite, so a crash cannot lose it. Only the
249
+ * summarization is background work, and it resumes from the durable archive.
209
250
  */
210
251
  async observeMessages(
211
252
  sessionId: string,
@@ -213,53 +254,60 @@ export class LcmEngine {
213
254
  ): Promise<void> {
214
255
  const normalizedSessionId = normalizeLcmSessionId(sessionId);
215
256
  if (!normalizedSessionId) return;
216
- this.enqueueObserveMessages(normalizedSessionId, messages);
257
+ await this.enqueueObserveMessages(normalizedSessionId, messages);
217
258
  }
218
259
 
219
- /** Enqueue an observe job without waiting for worker completion. */
220
- enqueueObserveMessages(
260
+ /**
261
+ * Durably archive messages, then queue a metadata-only summary job.
262
+ *
263
+ * Rejects when the archive append fails — callers must not treat a failed
264
+ * archive as an accepted observation.
265
+ */
266
+ async enqueueObserveMessages(
221
267
  sessionId: string,
222
268
  messages: LcmObserveMessage[],
223
- ): void {
224
- if (!this.config.enabled || this.closed) return;
269
+ ): Promise<void> {
270
+ // LCM off is a feature gate (the caller reports "not archived"); a closed
271
+ // engine is a failure — accepting the turn and archiving nothing would
272
+ // acknowledge a turn the archive never stored.
273
+ if (!this.config.enabled) return;
274
+ if (this.closed) throw new Error("LCM engine is closed");
225
275
  const normalizedSessionId = normalizeLcmSessionId(sessionId);
226
276
  if (!normalizedSessionId) return;
227
277
  if (messages.length === 0) return;
228
278
 
229
279
  this.reservePendingObserveInit(normalizedSessionId);
230
280
 
231
- void this.ensureInitialized()
232
- .then(() => {
233
- if (this.closed || !this.observeQueue) return;
234
- this.observeQueue.enqueue(normalizedSessionId, messages);
235
- log.debug(
236
- `LCM observe enqueued: session=${normalizedSessionId}, depth=${this.observeQueue.depth}, inFlight=${this.observeQueue.inFlightCount}`,
237
- );
238
- })
239
- .catch((err) => {
240
- if (this.closed) return;
241
- log.error(
242
- `LCM observe enqueue initialization error: ${err}${nativeBindingRecoveryHint(err)}`,
243
- );
244
- })
245
- .finally(() => {
246
- this.releasePendingObserveInit(normalizedSessionId);
247
- });
281
+ try {
282
+ await this.ensureInitialized();
283
+ if (this.closed) throw new Error("LCM engine is closed");
284
+ if (!this.archive || !this.observeQueue) {
285
+ throw new Error("LCM engine is not initialized");
286
+ }
287
+
288
+ const archived = this.archiveObservedMessages(normalizedSessionId, messages);
289
+ this.observeQueue.enqueue(normalizedSessionId);
290
+ log.debug(
291
+ `LCM observe archived: session=${normalizedSessionId}, messages=${archived}, depth=${this.observeQueue.depth}, inFlight=${this.observeQueue.inFlightCount}`,
292
+ );
293
+ } catch (err) {
294
+ log.error(
295
+ `LCM observe archive failed: session=${normalizedSessionId}, error=${err}${nativeBindingRecoveryHint(err)}`,
296
+ );
297
+ throw err;
298
+ } finally {
299
+ this.releasePendingObserveInit(normalizedSessionId);
300
+ }
248
301
  }
249
302
 
250
- private async processObserveMessages(
303
+ /** Append observed messages to the archive; returns the number of rows. */
304
+ private archiveObservedMessages(
251
305
  sessionId: string,
252
306
  messages: LcmObserveMessage[],
253
- ): Promise<void> {
254
- if (this.closed) return;
255
- await this.ensureInitialized();
256
- if (this.closed || !this.archive || !this.summarizer) return;
257
-
258
- if (messages.length === 0) return;
259
-
260
- const currentMax = this.archive.getMaxTurnIndex(sessionId);
307
+ ): number {
308
+ const currentMax = this.archive!.getMaxTurnIndex(sessionId);
261
309
  const { messagePartsEnabled } = this.config;
262
- const newMessages = messages.map((m, i) => ({
310
+ const rows = messages.map((m, i) => ({
263
311
  turnIndex: currentMax + 1 + i,
264
312
  role: m.role,
265
313
  content: m.content,
@@ -268,16 +316,109 @@ export class LcmEngine {
268
316
  sourceFormat: messagePartsEnabled ? m.sourceFormat : undefined,
269
317
  }));
270
318
 
271
- this.archive.appendMessages(sessionId, newMessages, {
272
- messagePartsEnabled,
273
- });
319
+ this.archive!.appendMessages(sessionId, rows, { messagePartsEnabled });
320
+ return rows.length;
321
+ }
322
+
323
+ /**
324
+ * Summarize one session's durable backlog.
325
+ *
326
+ * The pass is capped so a long backlog never holds a job open indefinitely;
327
+ * remaining batches stay durable and the job re-queues itself.
328
+ */
329
+ private async processSummarizeJob(sessionId: string): Promise<void> {
330
+ if (this.closed) return;
331
+ await this.ensureInitialized();
332
+ if (this.closed || !this.archive || !this.summarizer || !this.observeQueue)
333
+ return;
274
334
 
275
- // Trigger incremental summarization inside the worker, after append.
276
335
  try {
277
- await this.summarizer.summarizeIncremental(sessionId);
336
+ await this.summarizer.summarizeIncremental(sessionId, {
337
+ maxLeafBatches: LCM_SUMMARIZE_MAX_LEAF_BATCHES_PER_JOB,
338
+ });
278
339
  } catch (err) {
279
- log.debug(`LCM incremental summarization error: ${err}`);
340
+ // The archived turns are durable; a failed pass must not strand them
341
+ // until the next observe or a restart.
342
+ log.error(`LCM incremental summarization failed: session=${sessionId}, error=${err}`);
343
+ this.scheduleSummarizeRetry(sessionId);
344
+ return;
345
+ }
346
+
347
+ this.clearSummarizeRetry(sessionId);
348
+
349
+ if (this.hasCompleteBatchPending(sessionId)) {
350
+ this.observeQueue.enqueue(sessionId);
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Schedule a metadata-only retry for a failed summary pass.
356
+ *
357
+ * The retry keeps trying for as long as the engine is open and complete
358
+ * batches remain — a transient failure must never strand durable work.
359
+ * What is bounded is the RATE, not the ability to retry: one pending timer
360
+ * per session, exponential backoff capped at
361
+ * {@link LCM_SUMMARIZE_RETRY_MAX_MS}, and an unreferenced timer that is
362
+ * cleared when the engine closes.
363
+ */
364
+ private scheduleSummarizeRetry(sessionId: string): void {
365
+ if (this.closed || this.summarizeRetryTimers.has(sessionId)) return;
366
+ const attempt = (this.summarizeRetryAttempts.get(sessionId) ?? 0) + 1;
367
+ this.summarizeRetryAttempts.set(sessionId, attempt);
368
+ const delayMs = Math.min(
369
+ LCM_SUMMARIZE_RETRY_MAX_MS,
370
+ this.summarizeRetryBaseMs * 2 ** Math.min(attempt - 1, 16),
371
+ );
372
+ // The waiter makes the pending retry visible to idle waits, so a direct
373
+ // flush (pre-compaction) serializes with the retry instead of racing it.
374
+ const retry = Promise.withResolvers<void>();
375
+ this.summarizeRetryWaiters.set(sessionId, retry);
376
+ const timer = setTimeout(() => {
377
+ this.summarizeRetryTimers.delete(sessionId);
378
+ this.summarizeRetryWaiters.delete(sessionId);
379
+ if (this.closed) {
380
+ retry.resolve();
381
+ return;
382
+ }
383
+ if (!this.hasCompleteBatchPending(sessionId)) {
384
+ this.summarizeRetryAttempts.delete(sessionId);
385
+ retry.resolve();
386
+ return;
387
+ }
388
+ // Re-queue before releasing the waiter: an idle wait released by the
389
+ // retry must observe the queued job, never run beside it.
390
+ this.observeQueue?.enqueue(sessionId);
391
+ retry.resolve();
392
+ }, delayMs);
393
+ // Pure background: unreferenced, so a one-shot host is never kept alive
394
+ // by a retry loop. An explicit idle waiter (see waitForSessionObserveIdle)
395
+ // references it for the duration of the wait, and close() clears all.
396
+ timer.unref?.();
397
+ this.summarizeRetryTimers.set(sessionId, timer);
398
+ }
399
+
400
+ /** True when the session still holds a complete unsummarized batch. */
401
+ private hasCompleteBatchPending(sessionId: string): boolean {
402
+ if (!this.archive) return false;
403
+ return (
404
+ this.archive.countUnsummarizedMessages(sessionId) >=
405
+ this.config.leafBatchSize
406
+ );
407
+ }
408
+
409
+ /** Drop a pending retry, its attempt counter, and release idle waiters. */
410
+ private clearSummarizeRetry(sessionId: string): void {
411
+ const timer = this.summarizeRetryTimers.get(sessionId);
412
+ if (timer) {
413
+ clearTimeout(timer);
414
+ this.summarizeRetryTimers.delete(sessionId);
280
415
  }
416
+ const waiter = this.summarizeRetryWaiters.get(sessionId);
417
+ if (waiter) {
418
+ this.summarizeRetryWaiters.delete(sessionId);
419
+ waiter.resolve();
420
+ }
421
+ this.summarizeRetryAttempts.delete(sessionId);
281
422
  }
282
423
 
283
424
  get observeQueueDepth(): number {
@@ -288,12 +429,22 @@ export class LcmEngine {
288
429
  return this.observeQueue?.inFlightCount ?? 0;
289
430
  }
290
431
 
291
- async waitForObserveQueueIdle(): Promise<void> {
432
+ async waitForObserveQueueIdle(abortSignal?: AbortSignal): Promise<void> {
433
+ abortSignal?.throwIfAborted();
292
434
  if (!this.config.enabled || this.closed) return;
293
435
  await this.ensureInitialized();
436
+ abortSignal?.throwIfAborted();
294
437
  if (this.closed) return;
295
- await this.waitForPendingObserveInitIdle();
296
- await this.observeQueue?.whenIdle();
438
+ for (;;) {
439
+ abortSignal?.throwIfAborted();
440
+ await this.waitForPendingObserveInitIdle(undefined, abortSignal);
441
+ await this.observeQueue?.whenIdle(abortSignal);
442
+ abortSignal?.throwIfAborted();
443
+ const entry = [...this.summarizeRetryWaiters.entries()][0];
444
+ if (!entry) return;
445
+ await this.awaitSummarizeRetry(entry[0], entry[1], abortSignal);
446
+ // A fired retry may have re-queued a session: wait for the queue again.
447
+ }
297
448
  }
298
449
 
299
450
  async waitForSessionObserveIdle(sessionId: string, abortSignal?: AbortSignal): Promise<void> {
@@ -304,8 +455,42 @@ export class LcmEngine {
304
455
  await this.ensureInitialized();
305
456
  abortSignal?.throwIfAborted();
306
457
  if (this.closed) return;
307
- await this.waitForPendingObserveInitIdle(normalizedSessionId, abortSignal);
308
- await this.observeQueue?.whenSessionIdle(normalizedSessionId, abortSignal) ?? Promise.resolve();
458
+ // Serialize with pending summary retries: a direct flush (pre-compaction)
459
+ // must not run beside a retry pass for the same session.
460
+ for (;;) {
461
+ await this.waitForPendingObserveInitIdle(normalizedSessionId, abortSignal);
462
+ await this.observeQueue?.whenSessionIdle(normalizedSessionId, abortSignal) ?? Promise.resolve();
463
+ const waiter = this.summarizeRetryWaiters.get(normalizedSessionId);
464
+ if (!waiter) return;
465
+ await this.awaitSummarizeRetry(normalizedSessionId, waiter, abortSignal);
466
+ // The retry fired and may have re-queued the session: wait again.
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Park an explicit idle wait on a pending retry. While any wait is parked,
472
+ * the retry timer is referenced so a one-shot host cannot exit under the
473
+ * wait; with no waiters the timer stays unreferenced background work.
474
+ */
475
+ private async awaitSummarizeRetry(
476
+ sessionId: string,
477
+ waiter: { promise: Promise<void>; resolve: () => void },
478
+ abortSignal?: AbortSignal,
479
+ ): Promise<void> {
480
+ const waiters = (this.summarizeRetryIdleWaiters.get(sessionId) ?? 0) + 1;
481
+ this.summarizeRetryIdleWaiters.set(sessionId, waiters);
482
+ this.summarizeRetryTimers.get(sessionId)?.ref?.();
483
+ try {
484
+ await abortOnSignal(abortSignal, waiter.promise);
485
+ } finally {
486
+ const remaining = (this.summarizeRetryIdleWaiters.get(sessionId) ?? 1) - 1;
487
+ if (remaining <= 0) {
488
+ this.summarizeRetryIdleWaiters.delete(sessionId);
489
+ this.summarizeRetryTimers.get(sessionId)?.unref?.();
490
+ } else {
491
+ this.summarizeRetryIdleWaiters.set(sessionId, remaining);
492
+ }
493
+ }
309
494
  }
310
495
 
311
496
  private reservePendingObserveInit(sessionId: string): void {
@@ -359,8 +544,18 @@ export class LcmEngine {
359
544
  }
360
545
 
361
546
  if (this.pendingObserveInitCounts.size === 0) return;
362
- await new Promise<void>((resolve) => {
363
- this.pendingObserveInitIdleWaiters.push(resolve);
547
+ await new Promise<void>((resolve, reject) => {
548
+ const finish = (): void => {
549
+ abortSignal?.removeEventListener("abort", onAbort);
550
+ resolve();
551
+ };
552
+ const onAbort = (): void => {
553
+ const remaining = this.pendingObserveInitIdleWaiters.filter((waiter) => waiter !== finish);
554
+ this.pendingObserveInitIdleWaiters.splice(0, this.pendingObserveInitIdleWaiters.length, ...remaining);
555
+ reject(abortSignal?.reason instanceof Error ? abortSignal.reason : new DOMException("Aborted", "AbortError"));
556
+ };
557
+ this.pendingObserveInitIdleWaiters.push(finish);
558
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
364
559
  });
365
560
  }
366
561
 
@@ -697,23 +892,25 @@ export class LcmEngine {
697
892
  }
698
893
 
699
894
  /** Clear all LCM archive and summary state for one session. */
700
- async clearSession(sessionId: string): Promise<void> {
895
+ async clearSession(sessionId: string, abortSignal?: AbortSignal): Promise<void> {
701
896
  if (!this.config.enabled || this.closed) return;
702
897
  const normalizedSessionId = normalizeLcmSessionId(sessionId);
703
898
  if (!normalizedSessionId) return;
704
899
  await this.ensureInitialized();
705
900
  if (this.closed || !this.archive || !this.dag) return;
706
- await this.waitForSessionObserveIdle(normalizedSessionId);
901
+ await this.waitForSessionObserveIdle(normalizedSessionId, abortSignal);
902
+ abortSignal?.throwIfAborted();
707
903
  this.dag.deleteSession(normalizedSessionId);
708
904
  this.archive.deleteSession(normalizedSessionId);
709
905
  }
710
906
 
711
907
  /** Clear all LCM archive and summary state without closing the engine. */
712
- async clearAll(): Promise<void> {
908
+ async clearAll(abortSignal?: AbortSignal): Promise<void> {
713
909
  if (!this.config.enabled || this.closed) return;
714
910
  await this.ensureInitialized();
715
911
  if (this.closed || !this.archive || !this.dag) return;
716
- await this.waitForObserveQueueIdle();
912
+ await this.waitForObserveQueueIdle(abortSignal);
913
+ abortSignal?.throwIfAborted();
717
914
  this.dag.deleteAll();
718
915
  this.archive.deleteAll();
719
916
  }
@@ -735,6 +932,12 @@ export class LcmEngine {
735
932
  /** Close the database connection. */
736
933
  close(): void {
737
934
  this.closed = true;
935
+ for (const timer of this.summarizeRetryTimers.values()) clearTimeout(timer);
936
+ this.summarizeRetryTimers.clear();
937
+ this.summarizeRetryAttempts.clear();
938
+ for (const retry of this.summarizeRetryWaiters.values()) retry.resolve();
939
+ this.summarizeRetryWaiters.clear();
940
+ this.summarizeRetryIdleWaiters.clear();
738
941
  if (this.db) {
739
942
  this.db.close();
740
943
  }
@@ -26,9 +26,9 @@ test("LcmWorkQueue coalesces duplicate pending jobs per session", async () => {
26
26
  },
27
27
  });
28
28
 
29
- queue.enqueue("blocked", [{ role: "user", content: "hold" }]);
30
- queue.enqueue("session-a", [{ role: "user", content: "one" }]);
31
- queue.enqueue("session-a", [{ role: "assistant", content: "two" }]);
29
+ queue.enqueue("blocked");
30
+ queue.enqueue("session-a");
31
+ queue.enqueue("session-a");
32
32
 
33
33
  await Promise.resolve();
34
34
 
@@ -56,8 +56,8 @@ test("LcmWorkQueue continues after a worker failure", async () => {
56
56
  },
57
57
  });
58
58
 
59
- queue.enqueue("session-a", [{ role: "user", content: "one" }]);
60
- queue.enqueue("session-b", [{ role: "assistant", content: "two" }]);
59
+ queue.enqueue("session-a");
60
+ queue.enqueue("session-b");
61
61
 
62
62
  await queue.whenIdle();
63
63
 
@@ -87,14 +87,14 @@ test("LcmWorkQueue preserves first enqueue time when pending jobs coalesce", asy
87
87
  },
88
88
  });
89
89
 
90
- queue.enqueue("blocked", [{ role: "user", content: "hold" }]);
90
+ queue.enqueue("blocked");
91
91
  await Promise.resolve();
92
92
 
93
93
  now = 110;
94
- queue.enqueue("session-a", [{ role: "user", content: "one" }]);
94
+ queue.enqueue("session-a");
95
95
 
96
96
  now = 150;
97
- queue.enqueue("session-a", [{ role: "assistant", content: "two" }]);
97
+ queue.enqueue("session-a");
98
98
 
99
99
  now = 200;
100
100
  blocker.resolve();
@@ -127,10 +127,10 @@ test("LcmWorkQueue does not start a second job for a session already in flight",
127
127
  },
128
128
  });
129
129
 
130
- queue.enqueue("session-a", [{ role: "user", content: "first" }]);
130
+ queue.enqueue("session-a");
131
131
  await Promise.resolve();
132
- queue.enqueue("session-a", [{ role: "assistant", content: "second" }]);
133
- queue.enqueue("session-b", [{ role: "user", content: "other" }]);
132
+ queue.enqueue("session-a");
133
+ queue.enqueue("session-b");
134
134
 
135
135
  await Promise.resolve();
136
136
  await Promise.resolve();
@@ -159,8 +159,8 @@ test("LcmWorkQueue waits for a specific session without waiting for unrelated id
159
159
  },
160
160
  });
161
161
 
162
- queue.enqueue("blocked", [{ role: "user", content: "hold" }]);
163
- queue.enqueue("session-a", [{ role: "assistant", content: "queued" }]);
162
+ queue.enqueue("blocked");
163
+ queue.enqueue("session-a");
164
164
 
165
165
  await Promise.resolve();
166
166
  const sessionIdlePromise = queue.whenSessionIdle("session-a").then(() => {
@@ -185,7 +185,7 @@ test("#3077 whenSessionIdle abort unregisters the waiter", async () => {
185
185
  await blocker.promise;
186
186
  },
187
187
  });
188
- queue.enqueue("blocked", [{ role: "user", content: "hold" }]);
188
+ queue.enqueue("blocked");
189
189
  await Promise.resolve();
190
190
  const abort = new AbortController();
191
191
  const waiting = queue.whenSessionIdle("blocked", abort.signal);
@@ -195,3 +195,21 @@ test("#3077 whenSessionIdle abort unregisters the waiter", async () => {
195
195
  await queue.whenIdle();
196
196
  await queue.whenSessionIdle("blocked");
197
197
  });
198
+
199
+ test("whenIdle abort unregisters the waiter while a job is in flight", async () => {
200
+ const blocker = deferred();
201
+ const queue = new LcmWorkQueue({
202
+ concurrency: 1,
203
+ worker: async () => {
204
+ await blocker.promise;
205
+ },
206
+ });
207
+ queue.enqueue("blocked");
208
+ await Promise.resolve();
209
+ const abort = new AbortController();
210
+ const waiting = queue.whenIdle(abort.signal);
211
+ abort.abort();
212
+ await assert.rejects(waiting, { name: "AbortError" });
213
+ blocker.resolve();
214
+ await queue.whenIdle();
215
+ });