@remnic/core 10.0.69 → 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 (349) 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-DDBB55L2.js → chunk-OAQO5AZJ.js} +121 -121
  120. package/dist/{chunk-OPFVXM2D.js → chunk-OX6GY22Q.js} +2 -2
  121. package/dist/chunk-QCFZDYYG.js +34 -0
  122. package/dist/chunk-QCFZDYYG.js.map +1 -0
  123. package/dist/{chunk-MMCO4SRG.js → chunk-QNN4FYBD.js} +19 -11
  124. package/dist/chunk-QNN4FYBD.js.map +1 -0
  125. package/dist/{chunk-XWE3EOWX.js → chunk-RZED7FAU.js} +2 -2
  126. package/dist/{chunk-YSATDEBY.js → chunk-SBRUTD5O.js} +5 -5
  127. package/dist/{chunk-WVP2I6C5.js → chunk-SDL22RRW.js} +50 -11
  128. package/dist/chunk-SDL22RRW.js.map +1 -0
  129. package/dist/{chunk-ZORS5BG5.js → chunk-SQO5SEKA.js} +10 -10
  130. package/dist/{chunk-PRKNAIGZ.js → chunk-T6T2RMWW.js} +5 -5
  131. package/dist/{chunk-O5ZINWW3.js → chunk-THFSCUFC.js} +2 -2
  132. package/dist/{chunk-CIEEYVUB.js → chunk-TPX7XXY4.js} +2 -2
  133. package/dist/{chunk-OCK4D5PC.js → chunk-TSZP7GGJ.js} +2 -2
  134. package/dist/{chunk-L334PSLW.js → chunk-U2A56LLF.js} +8 -8
  135. package/dist/{chunk-TGQW3AMK.js → chunk-UIFWZMO7.js} +23 -23
  136. package/dist/{chunk-LN4X2NDA.js → chunk-UJ2FP2RV.js} +10 -10
  137. package/dist/{chunk-4MPAKYZU.js → chunk-VDYRXI3E.js} +5 -5
  138. package/dist/{chunk-IHCXMYSE.js → chunk-VPEIWMUT.js} +2 -2
  139. package/dist/{chunk-ELHJK7LM.js → chunk-WBN66HPI.js} +6 -6
  140. package/dist/{chunk-BJLCV2P3.js → chunk-WGRAB3KB.js} +2 -2
  141. package/dist/{chunk-IVUVQPTQ.js → chunk-WIW4LOZL.js} +2 -2
  142. package/dist/{chunk-5KDDAQ6K.js → chunk-WN2DASTS.js} +10 -10
  143. package/dist/chunk-WZU5AGO6.js +38 -0
  144. package/dist/chunk-WZU5AGO6.js.map +1 -0
  145. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  146. package/dist/{chunk-5Y6VA6X2.js → chunk-XDUP6SWG.js} +2 -2
  147. package/dist/{chunk-GHHQLSSY.js → chunk-XR4QR3JT.js} +3 -3
  148. package/dist/{chunk-64S6X4IU.js → chunk-XTN54NML.js} +4 -4
  149. package/dist/{chunk-QGFR73SN.js → chunk-YIG4HPY4.js} +2 -2
  150. package/dist/{chunk-UYUFDTIX.js → chunk-YIXWY4OZ.js} +2 -2
  151. package/dist/{chunk-GJ4HU6LK.js → chunk-YLMTFRZR.js} +2 -2
  152. package/dist/{chunk-GGD2YDS6.js → chunk-YRMSHA7H.js} +2 -2
  153. package/dist/{chunk-Y7427SL7.js → chunk-ZG4GNPRE.js} +216 -50
  154. package/dist/chunk-ZG4GNPRE.js.map +1 -0
  155. package/dist/{chunk-KZZ5EZ7O.js → chunk-ZS5LREUZ.js} +2 -2
  156. package/dist/{chunk-JLGBJ4AX.js → chunk-ZYEWTVNQ.js} +10 -10
  157. package/dist/cli.js +131 -128
  158. package/dist/coding/export-okf-codegraph.js +14 -14
  159. package/dist/coding/pre-action-gate.js +2 -2
  160. package/dist/compounding/engine.js +16 -16
  161. package/dist/config.js +24 -24
  162. package/dist/connectors/codex-materialize-runner.js +17 -17
  163. package/dist/connectors/codex-materialize.js +2 -2
  164. package/dist/connectors/index.js +18 -18
  165. package/dist/consolidation-provenance-check.js +2 -2
  166. package/dist/convergence-refresh.js +18 -18
  167. package/dist/corpus-watermark.js +18 -18
  168. package/dist/deep-recall-http-glue.js +13 -13
  169. package/dist/deep-recall.js +2 -2
  170. package/dist/embedding-fallback.js +2 -2
  171. package/dist/entity-retrieval.js +18 -18
  172. package/dist/event-order-recall.js +2 -2
  173. package/dist/external-wiki-access.js +81 -78
  174. package/dist/extraction-span-materialize.js +2 -2
  175. package/dist/extraction.js +30 -30
  176. package/dist/{graph-edge-decay-OEX5OGMI.js → graph-edge-decay-CXTNJ7IQ.js} +6 -6
  177. package/dist/graph-edge-decay-CXTNJ7IQ.js.map +1 -0
  178. package/dist/graph-jsonl.js +4 -4
  179. package/dist/graph-snapshot.js +4 -4
  180. package/dist/graph.js +3 -3
  181. package/dist/harmonic-retrieval.js +15 -15
  182. package/dist/index.js +279 -276
  183. package/dist/lcm/archive.d.ts +12 -2
  184. package/dist/lcm/archive.d.ts.map +1 -1
  185. package/dist/lcm/archive.js +2 -2
  186. package/dist/lcm/engine.d.ts +59 -8
  187. package/dist/lcm/engine.d.ts.map +1 -1
  188. package/dist/lcm/engine.js +6 -6
  189. package/dist/lcm/index.js +16 -16
  190. package/dist/lcm/queue.d.ts +7 -3
  191. package/dist/lcm/queue.d.ts.map +1 -1
  192. package/dist/lcm/queue.js +1 -1
  193. package/dist/lcm/summarizer.d.ts +8 -1
  194. package/dist/lcm/summarizer.d.ts.map +1 -1
  195. package/dist/lcm/summarizer.js +3 -3
  196. package/dist/local-llm.js +4 -4
  197. package/dist/location/index.js +17 -17
  198. package/dist/maintenance/memory-governance.js +16 -16
  199. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +15 -15
  200. package/dist/maintenance/rebuild-memory-projection-runner.js +18 -18
  201. package/dist/maintenance/rebuild-memory-projection.js +17 -17
  202. package/dist/memory-browse-operations.js +81 -78
  203. package/dist/memory-browse.js +6 -6
  204. package/dist/namespaces/migrate.js +16 -16
  205. package/dist/namespaces/search.js +7 -7
  206. package/dist/namespaces/storage.js +15 -15
  207. package/dist/offline-sync-changeset-generations.js +5 -5
  208. package/dist/offline-sync-embedding-generation.js +4 -4
  209. package/dist/offline-sync-snapshot-stream.js +7 -7
  210. package/dist/offline-sync.js +9 -9
  211. package/dist/operator-doctor-corpus.js +19 -19
  212. package/dist/operator-toolkit.js +39 -39
  213. package/dist/orchestration/buffer-drain-runner.js +194 -191
  214. package/dist/orchestration/buffer-drain-runner.js.map +1 -1
  215. package/dist/orchestration/maintenance.js +31 -31
  216. package/dist/orchestrator.js +195 -192
  217. package/dist/qmd.js +4 -4
  218. package/dist/recall-navigation-operations.js +81 -78
  219. package/dist/recall-navigation.js +80 -77
  220. package/dist/recall-state-view-admission.js +6 -6
  221. package/dist/recall-why-service.js +6 -6
  222. package/dist/resume-bundles.js +27 -27
  223. package/dist/search/factory.js +6 -6
  224. package/dist/search/index.js +12 -12
  225. package/dist/semantic-consolidation.js +23 -23
  226. package/dist/semantic-rule-promotion.js +15 -15
  227. package/dist/semantic-rule-verifier.js +15 -15
  228. package/dist/source-agent-qualifier.js +22 -22
  229. package/dist/standup/build.js +15 -15
  230. package/dist/storage.js +14 -14
  231. package/dist/summarizer.js +6 -6
  232. package/dist/support-passport/index.js +83 -80
  233. package/dist/transfer/backup.js +2 -2
  234. package/dist/transfer/capsule-export.js +2 -2
  235. package/dist/transfer/capsule-import.js +2 -2
  236. package/dist/transfer/export-okf.js +15 -15
  237. package/dist/verified-recall.js +15 -15
  238. package/dist/who-knows-cli.js +16 -16
  239. package/dist/who-knows.js +15 -15
  240. package/package.json +2 -2
  241. package/src/access-observe-lcm-archive.ts +30 -0
  242. package/src/access-observe-lcm-fail-closed.test.ts +118 -0
  243. package/src/access-observe-objective-state.ts +40 -0
  244. package/src/access-observe-stage-timing.test.ts +246 -0
  245. package/src/access-observe-stage-timing.ts +58 -0
  246. package/src/access-observe-write-surface.ts +45 -45
  247. package/src/access-service.ts +1 -0
  248. package/src/lcm/archive.ts +59 -7
  249. package/src/lcm/engine.ts +256 -53
  250. package/src/lcm/queue.test.ts +32 -14
  251. package/src/lcm/queue.ts +24 -16
  252. package/src/lcm/summarizer.ts +15 -2
  253. package/src/lcm-durability.test.ts +856 -0
  254. package/src/lcm-engine.test.ts +12 -3
  255. package/dist/chunk-6WKWL53V.js.map +0 -1
  256. package/dist/chunk-KOLJJN5G.js.map +0 -1
  257. package/dist/chunk-MMCO4SRG.js.map +0 -1
  258. package/dist/chunk-WVP2I6C5.js.map +0 -1
  259. package/dist/chunk-Y7427SL7.js.map +0 -1
  260. /package/dist/{capsule-crypto-GWVG7LGC.js.map → access-observe-lcm-archive.js.map} +0 -0
  261. /package/dist/{graph-edge-decay-OEX5OGMI.js.map → access-observe-objective-state.js.map} +0 -0
  262. /package/dist/{chunk-NCZGGGWI.js.map → chunk-2BBMCMJS.js.map} +0 -0
  263. /package/dist/{chunk-UVVFPCPX.js.map → chunk-2GV24KFD.js.map} +0 -0
  264. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  265. /package/dist/{chunk-QDZWQBLU.js.map → chunk-2R7OCBRR.js.map} +0 -0
  266. /package/dist/{chunk-U2CAGMUD.js.map → chunk-2YPIFJHL.js.map} +0 -0
  267. /package/dist/{chunk-4SOWMA55.js.map → chunk-3AASGFR4.js.map} +0 -0
  268. /package/dist/{chunk-CI3MAAIY.js.map → chunk-3F7N4I5I.js.map} +0 -0
  269. /package/dist/{chunk-PLCYKAWH.js.map → chunk-3ICAWZUU.js.map} +0 -0
  270. /package/dist/{chunk-B4DXREWC.js.map → chunk-3W37NITP.js.map} +0 -0
  271. /package/dist/{chunk-R4ZHF4K2.js.map → chunk-43FLJLT2.js.map} +0 -0
  272. /package/dist/{chunk-RLVI2M5Q.js.map → chunk-4BOBW6H4.js.map} +0 -0
  273. /package/dist/{chunk-HXFIILGF.js.map → chunk-4RASUAN3.js.map} +0 -0
  274. /package/dist/{chunk-JLMOH5JA.js.map → chunk-5FMGPV34.js.map} +0 -0
  275. /package/dist/{chunk-EI6M5NVK.js.map → chunk-5JBVPJCT.js.map} +0 -0
  276. /package/dist/{chunk-74AY3S7U.js.map → chunk-5RW247F5.js.map} +0 -0
  277. /package/dist/{chunk-P4TV3TQ2.js.map → chunk-5VQSKBNF.js.map} +0 -0
  278. /package/dist/{chunk-H5DIRCZP.js.map → chunk-64U42JL5.js.map} +0 -0
  279. /package/dist/{chunk-4XINO77L.js.map → chunk-66JOLMWS.js.map} +0 -0
  280. /package/dist/{chunk-WHROVQ5Q.js.map → chunk-66PEACZP.js.map} +0 -0
  281. /package/dist/{chunk-GKL6QYZO.js.map → chunk-6L63IN3M.js.map} +0 -0
  282. /package/dist/{chunk-Y3XPSGNF.js.map → chunk-6QS5NQJH.js.map} +0 -0
  283. /package/dist/{chunk-M4IEZGCK.js.map → chunk-BFBS7OFQ.js.map} +0 -0
  284. /package/dist/{chunk-4PG5R6BJ.js.map → chunk-BNU5TQLR.js.map} +0 -0
  285. /package/dist/{chunk-B3MWELZ3.js.map → chunk-BW33YTF3.js.map} +0 -0
  286. /package/dist/{chunk-H6VJSITJ.js.map → chunk-CIL4GHMD.js.map} +0 -0
  287. /package/dist/{chunk-5UM4H64E.js.map → chunk-D3AGBN6G.js.map} +0 -0
  288. /package/dist/{chunk-IG3EZ3TF.js.map → chunk-DM3UZUCN.js.map} +0 -0
  289. /package/dist/{chunk-PMDOGXA2.js.map → chunk-EF3HNVL4.js.map} +0 -0
  290. /package/dist/{chunk-TVE7W67N.js.map → chunk-EIZ4MK33.js.map} +0 -0
  291. /package/dist/{chunk-AUFPPBC3.js.map → chunk-ENPJX3BB.js.map} +0 -0
  292. /package/dist/{chunk-63A2BU2B.js.map → chunk-F4ZHKDBW.js.map} +0 -0
  293. /package/dist/{chunk-3LBDEGQA.js.map → chunk-FDUCIUQ5.js.map} +0 -0
  294. /package/dist/{chunk-CTH2CQCB.js.map → chunk-FM6UKZAE.js.map} +0 -0
  295. /package/dist/{chunk-XVZA6VXC.js.map → chunk-FZVROZKC.js.map} +0 -0
  296. /package/dist/{chunk-IAGVM7EY.js.map → chunk-GMSRLDVR.js.map} +0 -0
  297. /package/dist/{chunk-HPB5TF55.js.map → chunk-GT4G2S2Y.js.map} +0 -0
  298. /package/dist/{chunk-645WOU5I.js.map → chunk-H7BZBSP6.js.map} +0 -0
  299. /package/dist/{chunk-5MGKGEL7.js.map → chunk-HNF7IMYQ.js.map} +0 -0
  300. /package/dist/{chunk-W7K5BP3M.js.map → chunk-HQPFACXO.js.map} +0 -0
  301. /package/dist/{chunk-TECK5IVY.js.map → chunk-HZNURRDS.js.map} +0 -0
  302. /package/dist/{chunk-SXTVZ6JL.js.map → chunk-I6HRHJRA.js.map} +0 -0
  303. /package/dist/{chunk-NHWDTOBN.js.map → chunk-ILD7N3XC.js.map} +0 -0
  304. /package/dist/{chunk-4NV7O76Q.js.map → chunk-J5TVHCDG.js.map} +0 -0
  305. /package/dist/{chunk-AL3TCNSJ.js.map → chunk-J6GZ2254.js.map} +0 -0
  306. /package/dist/{chunk-OH55F76C.js.map → chunk-JEX2JL3U.js.map} +0 -0
  307. /package/dist/{chunk-AVGDYYMA.js.map → chunk-JGQEF33K.js.map} +0 -0
  308. /package/dist/{chunk-6P7U3HWG.js.map → chunk-K36RMINQ.js.map} +0 -0
  309. /package/dist/{chunk-UW6LBFYN.js.map → chunk-K47G2CBP.js.map} +0 -0
  310. /package/dist/{chunk-3RHU5MD7.js.map → chunk-K7LLGCPH.js.map} +0 -0
  311. /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
  312. /package/dist/{chunk-R2BIVU2Z.js.map → chunk-LTST4R7U.js.map} +0 -0
  313. /package/dist/{chunk-IZBIDPMY.js.map → chunk-LYTKNK5O.js.map} +0 -0
  314. /package/dist/{chunk-HH73M65H.js.map → chunk-MCFI6G7U.js.map} +0 -0
  315. /package/dist/{chunk-V6SBA5O7.js.map → chunk-MFKBYXBY.js.map} +0 -0
  316. /package/dist/{chunk-AYJQ644F.js.map → chunk-MK7HXGM6.js.map} +0 -0
  317. /package/dist/{chunk-MOIJWWNA.js.map → chunk-MKMIY3E7.js.map} +0 -0
  318. /package/dist/{chunk-EXQJMFRF.js.map → chunk-MRN2MKB7.js.map} +0 -0
  319. /package/dist/{chunk-W26D2USW.js.map → chunk-MUH7RR6T.js.map} +0 -0
  320. /package/dist/{chunk-64JLBFCQ.js.map → chunk-N7NAGECH.js.map} +0 -0
  321. /package/dist/{chunk-75DCOS6I.js.map → chunk-OAKOAOEG.js.map} +0 -0
  322. /package/dist/{chunk-DDBB55L2.js.map → chunk-OAQO5AZJ.js.map} +0 -0
  323. /package/dist/{chunk-OPFVXM2D.js.map → chunk-OX6GY22Q.js.map} +0 -0
  324. /package/dist/{chunk-XWE3EOWX.js.map → chunk-RZED7FAU.js.map} +0 -0
  325. /package/dist/{chunk-YSATDEBY.js.map → chunk-SBRUTD5O.js.map} +0 -0
  326. /package/dist/{chunk-ZORS5BG5.js.map → chunk-SQO5SEKA.js.map} +0 -0
  327. /package/dist/{chunk-PRKNAIGZ.js.map → chunk-T6T2RMWW.js.map} +0 -0
  328. /package/dist/{chunk-O5ZINWW3.js.map → chunk-THFSCUFC.js.map} +0 -0
  329. /package/dist/{chunk-CIEEYVUB.js.map → chunk-TPX7XXY4.js.map} +0 -0
  330. /package/dist/{chunk-OCK4D5PC.js.map → chunk-TSZP7GGJ.js.map} +0 -0
  331. /package/dist/{chunk-L334PSLW.js.map → chunk-U2A56LLF.js.map} +0 -0
  332. /package/dist/{chunk-TGQW3AMK.js.map → chunk-UIFWZMO7.js.map} +0 -0
  333. /package/dist/{chunk-LN4X2NDA.js.map → chunk-UJ2FP2RV.js.map} +0 -0
  334. /package/dist/{chunk-4MPAKYZU.js.map → chunk-VDYRXI3E.js.map} +0 -0
  335. /package/dist/{chunk-IHCXMYSE.js.map → chunk-VPEIWMUT.js.map} +0 -0
  336. /package/dist/{chunk-ELHJK7LM.js.map → chunk-WBN66HPI.js.map} +0 -0
  337. /package/dist/{chunk-BJLCV2P3.js.map → chunk-WGRAB3KB.js.map} +0 -0
  338. /package/dist/{chunk-IVUVQPTQ.js.map → chunk-WIW4LOZL.js.map} +0 -0
  339. /package/dist/{chunk-5KDDAQ6K.js.map → chunk-WN2DASTS.js.map} +0 -0
  340. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  341. /package/dist/{chunk-5Y6VA6X2.js.map → chunk-XDUP6SWG.js.map} +0 -0
  342. /package/dist/{chunk-GHHQLSSY.js.map → chunk-XR4QR3JT.js.map} +0 -0
  343. /package/dist/{chunk-64S6X4IU.js.map → chunk-XTN54NML.js.map} +0 -0
  344. /package/dist/{chunk-QGFR73SN.js.map → chunk-YIG4HPY4.js.map} +0 -0
  345. /package/dist/{chunk-UYUFDTIX.js.map → chunk-YIXWY4OZ.js.map} +0 -0
  346. /package/dist/{chunk-GJ4HU6LK.js.map → chunk-YLMTFRZR.js.map} +0 -0
  347. /package/dist/{chunk-GGD2YDS6.js.map → chunk-YRMSHA7H.js.map} +0 -0
  348. /package/dist/{chunk-KZZ5EZ7O.js.map → chunk-ZS5LREUZ.js.map} +0 -0
  349. /package/dist/{chunk-JLGBJ4AX.js.map → chunk-ZYEWTVNQ.js.map} +0 -0
@@ -0,0 +1,856 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import test from "node:test";
8
+
9
+ import { parseConfig } from "./config.js";
10
+ import { eventually } from "./testing/orchestrator-lite.js";
11
+ import { LcmArchive } from "./lcm/archive.js";
12
+ import { LcmDag } from "./lcm/dag.js";
13
+ import { LcmEngine } from "./lcm/engine.js";
14
+ import { ensureLcmStateDir, openLcmDatabase } from "./lcm/schema.js";
15
+ import { LcmSummarizer } from "./lcm/summarizer.js";
16
+
17
+ function createConfig(memoryDir: string) {
18
+ return parseConfig({
19
+ memoryDir,
20
+ lcmEnabled: true,
21
+ lcmLeafBatchSize: 2,
22
+ lcmRollupFanIn: 2,
23
+ lcmFreshTailTurns: 16,
24
+ lcmMaxDepth: 5,
25
+ lcmDeterministicMaxTokens: 512,
26
+ lcmTelemetryPrefilterEnabled: false,
27
+ lcmArchiveRetentionDays: 90,
28
+ lcmRecallBudgetShare: 0.15,
29
+ lcmObserveConcurrency: 1,
30
+ messagePartsEnabled: false,
31
+ });
32
+ }
33
+
34
+ /** Second SQLite handle on the same file: proves content is durable on disk,
35
+ * independent of the engine's in-memory queue state. */
36
+ function durableRowCount(memoryDir: string, sessionId: string): number {
37
+ const db = openLcmDatabase(memoryDir);
38
+ try {
39
+ // SAFETY: the query returns exactly one aggregate column, `n`.
40
+ const row = db
41
+ .prepare("SELECT COUNT(*) AS n FROM lcm_messages WHERE session_id = ?")
42
+ .get(sessionId) as { n: number };
43
+ return row.n;
44
+ } finally {
45
+ db.close();
46
+ }
47
+ }
48
+
49
+ function durableContents(memoryDir: string, sessionId: string): string[] {
50
+ const db = openLcmDatabase(memoryDir);
51
+ try {
52
+ // SAFETY: the query returns the lcm_messages `content` column.
53
+ const rows = db
54
+ .prepare(
55
+ "SELECT content FROM lcm_messages WHERE session_id = ? ORDER BY turn_index",
56
+ )
57
+ .all(sessionId) as Array<{ content: string }>;
58
+ return rows.map((r) => r.content);
59
+ } finally {
60
+ db.close();
61
+ }
62
+ }
63
+
64
+ function leafRanges(memoryDir: string, sessionId: string): Array<[number, number]> {
65
+ const db = openLcmDatabase(memoryDir);
66
+ try {
67
+ // SAFETY: the query returns the summary node range columns.
68
+ const rows = db
69
+ .prepare(
70
+ "SELECT msg_start, msg_end FROM lcm_summary_nodes WHERE session_id = ? AND depth = 0 ORDER BY msg_start",
71
+ )
72
+ .all(sessionId) as Array<{ msg_start: number; msg_end: number }>;
73
+ return rows.map((r) => [r.msg_start, r.msg_end]);
74
+ } finally {
75
+ db.close();
76
+ }
77
+ }
78
+
79
+ test("enqueueObserveMessages archives content durably before acknowledgement while the summarizer is blocked", async () => {
80
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-durable-1-"));
81
+ const { promise: summarizeStarted, resolve: resolveStarted } =
82
+ Promise.withResolvers<void>();
83
+ const { promise: releaseSummarize, resolve: resolveRelease } =
84
+ Promise.withResolvers<void>();
85
+ try {
86
+ const engine = new LcmEngine(createConfig(dir), async () => {
87
+ resolveStarted();
88
+ await releaseSummarize;
89
+ return "blocked summary";
90
+ });
91
+
92
+ await engine.enqueueObserveMessages("session-1", [
93
+ { role: "user", content: "burst payload alpha" },
94
+ ]);
95
+
96
+ // Acknowledgement returned with the summarizer still parked: the payload
97
+ // must already be in SQLite, not only in the in-memory queue.
98
+ assert.equal(durableRowCount(dir, "session-1"), 1);
99
+ assert.deepEqual(durableContents(dir, "session-1"), ["burst payload alpha"]);
100
+
101
+ resolveRelease();
102
+ await engine.waitForSessionObserveIdle("session-1");
103
+ engine.close();
104
+ } finally {
105
+ await rm(dir, { recursive: true, force: true });
106
+ }
107
+ });
108
+
109
+ test("burst enqueue keeps the pending queue metadata-only and every message lands in SQLite while summarization is blocked", async () => {
110
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-durable-2-"));
111
+ const { promise: releaseSummarize, resolve: resolveRelease } =
112
+ Promise.withResolvers<void>();
113
+ try {
114
+ const engine = new LcmEngine(createConfig(dir), async () => {
115
+ await releaseSummarize;
116
+ return "blocked summary";
117
+ });
118
+
119
+ for (let i = 0; i < 30; i += 1) {
120
+ await engine.enqueueObserveMessages("session-1", [
121
+ { role: "user", content: `burst message ${i}` },
122
+ ]);
123
+ }
124
+
125
+ // One running job plus one pending marker for the single session: the
126
+ // queue never grows with the number of archived messages.
127
+ assert.equal(engine.observeQueueDepth, 1);
128
+ assert.equal(engine.observeQueueInFlightCount, 1);
129
+
130
+ const contents = durableContents(dir, "session-1");
131
+ assert.equal(contents.length, 30);
132
+ assert.deepEqual(
133
+ contents,
134
+ Array.from({ length: 30 }, (_, i) => `burst message ${i}`),
135
+ );
136
+
137
+ resolveRelease();
138
+ await engine.waitForSessionObserveIdle("session-1");
139
+ engine.close();
140
+ } finally {
141
+ await rm(dir, { recursive: true, force: true });
142
+ }
143
+ });
144
+
145
+ test("close-and-recreate recovers the full archived session and resumes outstanding summaries automatically", async () => {
146
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-durable-3-"));
147
+ const { promise: summarizeStarted, resolve: resolveStarted } =
148
+ Promise.withResolvers<void>();
149
+ const { promise: releaseSummarize, resolve: resolveRelease } =
150
+ Promise.withResolvers<void>();
151
+ try {
152
+ const blockedEngine = new LcmEngine(createConfig(dir), async () => {
153
+ resolveStarted();
154
+ await releaseSummarize;
155
+ return "never finishes";
156
+ });
157
+ for (let i = 0; i < 4; i += 1) {
158
+ await blockedEngine.enqueueObserveMessages("session-1", [
159
+ { role: "user", content: `crash survivor ${i}` },
160
+ ]);
161
+ }
162
+ await summarizeStarted;
163
+ // Simulated crash: drop the engine (and its in-flight summarize job).
164
+ blockedEngine.close();
165
+ resolveRelease();
166
+
167
+ // Content survived the crash.
168
+ assert.equal(durableRowCount(dir, "session-1"), 4);
169
+
170
+ // Restart: outstanding complete batches must resume without any new observe.
171
+ let summarizeCalls = 0;
172
+ const resumedEngine = new LcmEngine(createConfig(dir), async () => {
173
+ summarizeCalls += 1;
174
+ return `resumed summary ${summarizeCalls}`;
175
+ });
176
+ await resumedEngine.waitForObserveQueueIdle();
177
+ assert.ok(summarizeCalls >= 1, "startup resume must summarize outstanding batches");
178
+ assert.deepEqual(leafRanges(dir, "session-1"), [
179
+ [0, 1],
180
+ [2, 3],
181
+ ]);
182
+ assert.equal(durableRowCount(dir, "session-1"), 4);
183
+
184
+ resumedEngine.close();
185
+ } finally {
186
+ await rm(dir, { recursive: true, force: true });
187
+ }
188
+ });
189
+
190
+ test("unsummarized archive reads are bounded by the requested limit without dropping source rows", async () => {
191
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-durable-4-"));
192
+ try {
193
+ await ensureLcmStateDir(dir);
194
+ const db = openLcmDatabase(dir);
195
+ const archive = new LcmArchive(db);
196
+ for (let i = 0; i < 10; i += 1) {
197
+ archive.appendMessage("session-1", i, "user", `bounded read ${i}`);
198
+ }
199
+
200
+ assert.equal(archive.getUnsummarizedMessages("session-1").length, 10);
201
+ assert.equal(archive.getUnsummarizedMessages("session-1", 4).length, 4);
202
+ assert.equal(archive.getUnsummarizedMessages("session-1", 0).length, 0);
203
+ // Rows themselves untouched.
204
+ assert.equal(archive.getMaxTurnIndex("session-1"), 9);
205
+ assert.equal(durableRowCount(dir, "session-1"), 10);
206
+ db.close();
207
+ } finally {
208
+ await rm(dir, { recursive: true, force: true });
209
+ }
210
+ });
211
+
212
+ test("maxLeafBatches caps one summarize pass and repeated passes keep contiguous lineage", async () => {
213
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-durable-5-"));
214
+ try {
215
+ await ensureLcmStateDir(dir);
216
+ const db = openLcmDatabase(dir);
217
+ const archive = new LcmArchive(db);
218
+ const dag = new LcmDag(db);
219
+ for (let i = 0; i < 6; i += 1) {
220
+ archive.appendMessage("session-1", i, "user", `capped pass ${i}`);
221
+ }
222
+ const summarizer = new LcmSummarizer(archive, dag, async () => "cap summary", {
223
+ leafBatchSize: 2,
224
+ rollupFanIn: 2,
225
+ maxDepth: 5,
226
+ deterministicMaxTokens: 512,
227
+ telemetryPrefilterEnabled: false,
228
+ });
229
+
230
+ assert.ok(
231
+ (await summarizer.summarizeIncremental("session-1", { maxLeafBatches: 1 })) >= 1,
232
+ );
233
+ assert.equal(dag.getNodesAtDepth("session-1", 0).length, 1);
234
+ assert.equal(archive.getUnsummarizedMessages("session-1").length, 4);
235
+
236
+ await summarizer.summarizeIncremental("session-1", { maxLeafBatches: 1 });
237
+ await summarizer.summarizeIncremental("session-1", { maxLeafBatches: 1 });
238
+ assert.equal(dag.getNodesAtDepth("session-1", 0).length, 3);
239
+ assert.equal(archive.getUnsummarizedMessages("session-1").length, 0);
240
+
241
+ assert.deepEqual(
242
+ dag.getNodesAtDepth("session-1", 0).map((l) => [l.msg_start, l.msg_end]),
243
+ [
244
+ [0, 1],
245
+ [2, 3],
246
+ [4, 5],
247
+ ],
248
+ );
249
+ db.close();
250
+ } finally {
251
+ await rm(dir, { recursive: true, force: true });
252
+ }
253
+ });
254
+
255
+ test("enqueueObserveMessages rejects instead of silently claiming acceptance when initialization fails", async () => {
256
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-durable-6-"));
257
+ // memoryDir points at a regular file: ensureLcmStateDir cannot mkdir beneath it.
258
+ const filePath = path.join(dir, "not-a-dir");
259
+ await writeFile(filePath, "occupied", "utf8");
260
+ try {
261
+ const engine = new LcmEngine(createConfig(filePath), async () => "unused");
262
+ let failure: unknown;
263
+ try {
264
+ await engine.enqueueObserveMessages("session-1", [
265
+ { role: "user", content: "must not be claimed durable" },
266
+ ]);
267
+ } catch (err) {
268
+ failure = err;
269
+ }
270
+ assert.ok(failure instanceof Error, "the archive failure must reject");
271
+ // Nothing was archived: the failed init left no database behind.
272
+ assert.equal(existsSync(path.join(filePath, "state")), false);
273
+ } finally {
274
+ await rm(dir, { recursive: true, force: true });
275
+ }
276
+ });
277
+
278
+ test("a closed engine rejects acceptance instead of acknowledging an empty archive", async () => {
279
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-closed-"));
280
+ try {
281
+ const engine = new LcmEngine(createConfig(dir), async () => "unused");
282
+ await engine.ensureInitialized();
283
+ engine.close();
284
+
285
+ let failure: unknown;
286
+ try {
287
+ await engine.enqueueObserveMessages("session-1", [
288
+ { role: "user", content: "must not be acknowledged" },
289
+ ]);
290
+ } catch (err) {
291
+ failure = err;
292
+ }
293
+ assert.ok(failure instanceof Error, "a closed engine must reject acceptance");
294
+ assert.match(failure.message, /closed/);
295
+ assert.equal(durableRowCount(dir, "session-1"), 0);
296
+ } finally {
297
+ await rm(dir, { recursive: true, force: true });
298
+ }
299
+ });
300
+
301
+ test("a failed summary pass is retried from the durable archive without a new observe", async () => {
302
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-retry-"));
303
+ try {
304
+ const engine = new LcmEngine(createConfig(dir), async () => "retry summary");
305
+ await engine.ensureInitialized();
306
+
307
+ // Fail the first summary pass, then let the retry succeed.
308
+ // SAFETY: the stub replaces the engine's own summarizer for this test.
309
+ const summarizer = (engine as any).summarizer as {
310
+ summarizeIncremental: (
311
+ sessionId: string,
312
+ options?: { maxLeafBatches?: number },
313
+ ) => Promise<number>;
314
+ };
315
+ const realSummarize = summarizer.summarizeIncremental.bind(summarizer);
316
+ let failNext = true;
317
+ summarizer.summarizeIncremental = async (
318
+ sessionId: string,
319
+ options?: { maxLeafBatches?: number },
320
+ ) => {
321
+ if (failNext) {
322
+ failNext = false;
323
+ throw new Error("transient summary failure");
324
+ }
325
+ return realSummarize(sessionId, options);
326
+ };
327
+
328
+ await engine.enqueueObserveMessages("session-1", [
329
+ { role: "user", content: "retry me" },
330
+ { role: "assistant", content: "retry me too" },
331
+ ]);
332
+
333
+ // The durable backlog must be summarized by the retry alone: no new
334
+ // observe, no restart.
335
+ await eventually(
336
+ async () => {
337
+ const summary = await engine.describeContext("session-1", 0, 1);
338
+ return summary?.depth === 0 ? true : null;
339
+ },
340
+ "the failed summary pass is retried",
341
+ );
342
+ assert.equal(durableRowCount(dir, "session-1"), 2);
343
+ engine.close();
344
+ } finally {
345
+ await rm(dir, { recursive: true, force: true });
346
+ }
347
+ });
348
+
349
+ test("a summary pass that keeps failing is retried until it succeeds", async () => {
350
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-retry-persist-"));
351
+ try {
352
+ const engine = new LcmEngine(createConfig(dir), async () => "persistent summary");
353
+ await engine.ensureInitialized();
354
+
355
+ // SAFETY: the test drives the engine's own retry pacing and summarizer.
356
+ const internals = engine as any;
357
+ internals.summarizeRetryBaseMs = 10;
358
+ // SAFETY: the stub replaces the engine's own summarizer for this test.
359
+ const summarizer = internals.summarizer as {
360
+ summarizeIncremental: (
361
+ sessionId: string,
362
+ options?: { maxLeafBatches?: number },
363
+ ) => Promise<number>;
364
+ };
365
+ const realSummarize = summarizer.summarizeIncremental.bind(summarizer);
366
+ let failures = 0;
367
+ summarizer.summarizeIncremental = async (
368
+ sessionId: string,
369
+ options?: { maxLeafBatches?: number },
370
+ ) => {
371
+ failures += 1;
372
+ // More failures than any attempt budget would allow: the retry must
373
+ // keep trying, not stop and strand the durable backlog.
374
+ if (failures <= 7) throw new Error("persistent summary failure");
375
+ return realSummarize(sessionId, options);
376
+ };
377
+
378
+ await engine.enqueueObserveMessages("session-1", [
379
+ { role: "user", content: "keep retrying" },
380
+ { role: "assistant", content: "keep retrying too" },
381
+ ]);
382
+
383
+ await eventually(
384
+ async () => {
385
+ const summary = await engine.describeContext("session-1", 0, 1);
386
+ return summary?.depth === 0 ? true : null;
387
+ },
388
+ "the failing summary pass is retried until it succeeds",
389
+ );
390
+ assert.ok(failures > 7, `expected more than 7 attempts, got ${failures}`);
391
+ assert.equal(durableRowCount(dir, "session-1"), 2);
392
+ engine.close();
393
+ } finally {
394
+ await rm(dir, { recursive: true, force: true });
395
+ }
396
+ });
397
+
398
+ test("closing the engine cancels a pending summary retry", async () => {
399
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-retry-close-"));
400
+ try {
401
+ const engine = new LcmEngine(createConfig(dir), async () => "unused");
402
+ await engine.ensureInitialized();
403
+
404
+ // SAFETY: the test drives the engine's own retry pacing and summarizer.
405
+ const internals = engine as any;
406
+ internals.summarizeRetryBaseMs = 10_000;
407
+ // SAFETY: the stub replaces the engine's own summarizer for this test.
408
+ const summarizer = internals.summarizer as {
409
+ summarizeIncremental: (
410
+ sessionId: string,
411
+ options?: { maxLeafBatches?: number },
412
+ ) => Promise<number>;
413
+ };
414
+ summarizer.summarizeIncremental = async () => {
415
+ throw new Error("permanent summary failure");
416
+ };
417
+
418
+ await engine.enqueueObserveMessages("session-1", [
419
+ { role: "user", content: "pending retry" },
420
+ { role: "assistant", content: "pending retry too" },
421
+ ]);
422
+ await eventually(
423
+ async () => (internals.summarizeRetryTimers.size > 0 ? true : null),
424
+ "a summary retry is pending",
425
+ );
426
+
427
+ engine.close();
428
+ assert.equal(internals.summarizeRetryTimers.size, 0, "close must clear pending retries");
429
+ assert.equal(internals.db, null, "close must not reinitialize the engine");
430
+ } finally {
431
+ await rm(dir, { recursive: true, force: true });
432
+ }
433
+ });
434
+
435
+ test("pre-compaction flush serializes with a pending summary retry", async () => {
436
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-retry-flush-"));
437
+ try {
438
+ const engine = new LcmEngine(createConfig(dir), async () => "compaction summary");
439
+ await engine.ensureInitialized();
440
+
441
+ // SAFETY: the test drives the engine's own retry pacing and summarizer.
442
+ const internals = engine as any;
443
+ internals.summarizeRetryBaseMs = 10;
444
+ // SAFETY: the stub replaces the engine's own summarizer for this test.
445
+ const summarizer = internals.summarizer as {
446
+ summarizeIncremental: (
447
+ sessionId: string,
448
+ options?: { maxLeafBatches?: number },
449
+ ) => Promise<number>;
450
+ };
451
+ const realSummarize = summarizer.summarizeIncremental.bind(summarizer);
452
+ let failures = 0;
453
+ summarizer.summarizeIncremental = async (
454
+ sessionId: string,
455
+ options?: { maxLeafBatches?: number },
456
+ ) => {
457
+ failures += 1;
458
+ if (failures === 1) throw new Error("transient summary failure");
459
+ // Keep later passes busy long enough for a retry timer to fire mid-pass,
460
+ // which is exactly the overlap a direct flush must not allow.
461
+ await new Promise((resolveSleep) => setTimeout(resolveSleep, 50));
462
+ return realSummarize(sessionId, options);
463
+ };
464
+
465
+ await engine.enqueueObserveMessages("session-1", [
466
+ { role: "user", content: "flush race" },
467
+ { role: "assistant", content: "flush race too" },
468
+ ]);
469
+
470
+ // Compaction starts while the retry delay is still pending: the flush
471
+ // must serialize behind the retry, not run a second pass beside it.
472
+ await engine.preCompactionFlush("session-1");
473
+ await engine.waitForObserveQueueIdle();
474
+
475
+ assert.ok(failures >= 2, "the retry must have run before the flush");
476
+ // Exactly one leaf for the batch: no duplicate summary ranges.
477
+ assert.deepEqual(leafRanges(dir, "session-1"), [[0, 1]]);
478
+ engine.close();
479
+ } finally {
480
+ await rm(dir, { recursive: true, force: true });
481
+ }
482
+ });
483
+
484
+ test("no summary pass starts while a retry is pending for the session", async () => {
485
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-retry-order-"));
486
+ try {
487
+ const engine = new LcmEngine(createConfig(dir), async () => "ordered summary");
488
+ await engine.ensureInitialized();
489
+ // Seed the archive directly: the backlog exists before any observe.
490
+ await ensureLcmStateDir(dir);
491
+ const seedDb = openLcmDatabase(dir);
492
+ const seedArchive = new LcmArchive(seedDb);
493
+ seedArchive.appendMessage("session-1", 0, "user", "order seed one");
494
+ seedArchive.appendMessage("session-1", 1, "assistant", "order seed two");
495
+ seedDb.close();
496
+
497
+ // SAFETY: the test drives the engine's own retry pacing and summarizer.
498
+ const internals = engine as any;
499
+ internals.summarizeRetryBaseMs = 150;
500
+ // SAFETY: the stub replaces the engine's own summarizer for this test.
501
+ const summarizer = internals.summarizer as {
502
+ summarizeIncremental: (
503
+ sessionId: string,
504
+ options?: { maxLeafBatches?: number },
505
+ ) => Promise<number>;
506
+ };
507
+ const realSummarize = summarizer.summarizeIncremental.bind(summarizer);
508
+ let failures = 0;
509
+ const passOrder: Array<"background" | "direct" | "retry"> = [];
510
+ summarizer.summarizeIncremental = async (
511
+ sessionId: string,
512
+ options?: { maxLeafBatches?: number },
513
+ ) => {
514
+ // A pass that starts while this session's retry timer is still pending
515
+ // is either the armed retry or a racy direct flush; the flush must not
516
+ // start until the retry has fired.
517
+ const retryPending = internals.summarizeRetryTimers.has(sessionId);
518
+ const label: (typeof passOrder)[number] =
519
+ failures === 0 ? "background" : retryPending ? "direct" : "retry";
520
+ passOrder.push(label);
521
+ failures += 1;
522
+ if (failures === 1) throw new Error("transient summary failure");
523
+ return realSummarize(sessionId, options);
524
+ };
525
+
526
+ // The background pass fails and arms a 150ms retry; compaction starts
527
+ // inside that window.
528
+ await engine.enqueueObserveMessages("session-1", [
529
+ { role: "user", content: "ordering one" },
530
+ { role: "assistant", content: "ordering two" },
531
+ ]);
532
+ await engine.preCompactionFlush("session-1");
533
+ await engine.waitForObserveQueueIdle();
534
+
535
+ // The flush must serialize behind the armed retry, never start while the
536
+ // retry timer is pending: no "direct" label may appear. The retry pass
537
+ // covers both seeded batches; the flush pass then finds nothing left.
538
+ assert.deepEqual(passOrder, ["background", "retry", "retry"]);
539
+ assert.deepEqual(leafRanges(dir, "session-1"), [
540
+ [0, 1],
541
+ [2, 3],
542
+ ]);
543
+ engine.close();
544
+ } finally {
545
+ await rm(dir, { recursive: true, force: true });
546
+ }
547
+ });
548
+
549
+ test("an aborted idle wait releases the retry timer reference and close clears everything", async () => {
550
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-retry-abort-"));
551
+ try {
552
+ const engine = new LcmEngine(createConfig(dir), async () => "unused");
553
+ await engine.ensureInitialized();
554
+
555
+ // SAFETY: the test drives the engine's own retry pacing and summarizer.
556
+ const internals = engine as any;
557
+ internals.summarizeRetryBaseMs = 60_000;
558
+ // SAFETY: the stub replaces the engine's own summarizer for this test.
559
+ const summarizer = internals.summarizer as {
560
+ summarizeIncremental: (
561
+ sessionId: string,
562
+ options?: { maxLeafBatches?: number },
563
+ ) => Promise<number>;
564
+ };
565
+ summarizer.summarizeIncremental = async () => {
566
+ throw new Error("permanent summary failure");
567
+ };
568
+
569
+ await engine.enqueueObserveMessages("session-1", [
570
+ { role: "user", content: "abort the wait" },
571
+ { role: "assistant", content: "abort the wait too" },
572
+ ]);
573
+ await eventually(
574
+ async () => (internals.summarizeRetryTimers.size > 0 ? true : null),
575
+ "a summary retry is pending",
576
+ );
577
+
578
+ const abort = new AbortController();
579
+ const waiting = engine.waitForSessionObserveIdle("session-1", abort.signal);
580
+ await eventually(
581
+ async () => (internals.summarizeRetryIdleWaiters.get("session-1") === 1 ? true : null),
582
+ "the idle wait is parked on the retry and holds a reference",
583
+ );
584
+
585
+ abort.abort(new Error("caller gave up"));
586
+ await assert.rejects(() => waiting);
587
+ assert.equal(internals.summarizeRetryIdleWaiters.size, 0, "abort releases the idle-waiter reference");
588
+
589
+ engine.close();
590
+ assert.equal(internals.summarizeRetryTimers.size, 0, "close clears pending retries");
591
+ assert.equal(internals.summarizeRetryWaiters.size, 0, "close resolves retry waiters");
592
+ assert.equal(internals.summarizeRetryIdleWaiters.size, 0, "close clears idle-waiter counts");
593
+ assert.equal(internals.db, null, "close does not reinitialize the engine");
594
+ } finally {
595
+ await rm(dir, { recursive: true, force: true });
596
+ }
597
+ });
598
+
599
+ test("an aborted global idle wait releases the retry timer reference", async () => {
600
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-retry-abort-global-"));
601
+ try {
602
+ const engine = new LcmEngine(createConfig(dir), async () => "unused");
603
+ await engine.ensureInitialized();
604
+
605
+ // SAFETY: the test drives the engine's own retry pacing and summarizer.
606
+ const internals = engine as any;
607
+ internals.summarizeRetryBaseMs = 60_000;
608
+ // SAFETY: the stub replaces the engine's own summarizer for this test.
609
+ const summarizer = internals.summarizer as {
610
+ summarizeIncremental: (
611
+ sessionId: string,
612
+ options?: { maxLeafBatches?: number },
613
+ ) => Promise<number>;
614
+ };
615
+ summarizer.summarizeIncremental = async () => {
616
+ throw new Error("permanent summary failure");
617
+ };
618
+
619
+ await engine.enqueueObserveMessages("session-1", [
620
+ { role: "user", content: "abort the global wait" },
621
+ { role: "assistant", content: "abort the global wait too" },
622
+ ]);
623
+ await eventually(
624
+ async () => (internals.summarizeRetryTimers.size > 0 ? true : null),
625
+ "a summary retry is pending",
626
+ );
627
+
628
+ const abort = new AbortController();
629
+ const waiting = engine.waitForObserveQueueIdle(abort.signal);
630
+ await eventually(
631
+ async () => (internals.summarizeRetryIdleWaiters.get("session-1") === 1 ? true : null),
632
+ "the global idle wait is parked on the retry and holds a reference",
633
+ );
634
+
635
+ abort.abort(new Error("caller gave up"));
636
+ await assert.rejects(() => waiting);
637
+ assert.equal(internals.summarizeRetryIdleWaiters.size, 0, "abort releases the global idle-waiter reference");
638
+
639
+ engine.close();
640
+ assert.equal(internals.summarizeRetryTimers.size, 0, "close clears pending retries");
641
+ assert.equal(internals.summarizeRetryWaiters.size, 0, "close resolves retry waiters");
642
+ assert.equal(internals.summarizeRetryIdleWaiters.size, 0, "close clears idle-waiter counts");
643
+ } finally {
644
+ await rm(dir, { recursive: true, force: true });
645
+ }
646
+ });
647
+
648
+ test("an aborted global idle wait rejects while a summary job is still in flight", async () => {
649
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-global-idle-abort-"));
650
+ try {
651
+ const engine = new LcmEngine(createConfig(dir), async () => "unused");
652
+ await engine.ensureInitialized();
653
+
654
+ // SAFETY: the stub replaces the engine's own summarizer: hold the first
655
+ // pass in flight so the wait is parked on the observe queue, not a retry.
656
+ let releaseFirstPass!: () => void;
657
+ const firstPassBlocked = new Promise<void>((resolve) => {
658
+ releaseFirstPass = resolve;
659
+ });
660
+ let firstPassEntered = false;
661
+ // SAFETY: the stub replaces the engine's own summarizer for this test.
662
+ const summarizer = (engine as any).summarizer as {
663
+ summarizeIncremental: (
664
+ sessionId: string,
665
+ options?: { maxLeafBatches?: number },
666
+ ) => Promise<number>;
667
+ };
668
+ summarizer.summarizeIncremental = async () => {
669
+ firstPassEntered = true;
670
+ await firstPassBlocked;
671
+ return 1;
672
+ };
673
+
674
+ await engine.enqueueObserveMessages("session-1", [
675
+ { role: "user", content: "abort mid job" },
676
+ { role: "assistant", content: "abort mid job too" },
677
+ ]);
678
+ await eventually(
679
+ async () => (firstPassEntered ? true : null),
680
+ "the summary pass is in flight",
681
+ );
682
+
683
+ const abort = new AbortController();
684
+ const waiting = engine.waitForObserveQueueIdle(abort.signal);
685
+ await new Promise((resolve) => setTimeout(resolve, 10));
686
+ abort.abort(new Error("caller gave up"));
687
+ await assert.rejects(() => waiting);
688
+
689
+ releaseFirstPass();
690
+ engine.close();
691
+ } finally {
692
+ await rm(dir, { recursive: true, force: true });
693
+ }
694
+ });
695
+
696
+ /** Spawn a real one-shot host child process with the shared engine setup.
697
+ * The retry timer is the only handle a quiet host has left: `mode` decides
698
+ * whether it parks an explicit global idle wait before ending its script. */
699
+ function spawnOneShotHost(
700
+ memoryDir: string,
701
+ mode: "global-idle" | "background",
702
+ moduleUrl: (name: string) => string,
703
+ ): Promise<{ code: number | null; stdout: string; stderr: string }> {
704
+ const childSource = [
705
+ // Child is a spawned `node -e` script receiving module URLs via argv:
706
+ // static imports are impossible for runtime-selected specifiers here.
707
+ `const [engineUrl, configUrl, memoryDir, mode] = process.argv.slice(1);`,
708
+ `const { LcmEngine } = await import(engineUrl);`,
709
+ `const { parseConfig } = await import(configUrl);`,
710
+ `const engine = new LcmEngine(parseConfig({`,
711
+ ` memoryDir, lcmEnabled: true, lcmLeafBatchSize: 2, lcmRollupFanIn: 2,`,
712
+ ` lcmFreshTailTurns: 16, lcmMaxDepth: 5, lcmDeterministicMaxTokens: 512,`,
713
+ ` lcmTelemetryPrefilterEnabled: false, lcmArchiveRetentionDays: 90,`,
714
+ ` lcmRecallBudgetShare: 0.15, lcmObserveConcurrency: 1, messagePartsEnabled: false,`,
715
+ `}), async () => "host summary");`,
716
+ `await engine.ensureInitialized();`,
717
+ `engine.summarizeRetryBaseMs = 150;`,
718
+ // SAFETY: stub replaces the engine's own summarizer. "global-idle" fails
719
+ // exactly once so a genuine retry pass runs against the real archive;
720
+ // "background" fails forever so a wrongly referenced timer would keep
721
+ // the host alive on an endless retry loop.
722
+ `const summarizer = engine.summarizer;`,
723
+ `if (mode === "global-idle") {`,
724
+ ` const realSummarize = summarizer.summarizeIncremental.bind(summarizer);`,
725
+ ` let failedOnce = false;`,
726
+ ` summarizer.summarizeIncremental = async (sessionId, options) => {`,
727
+ ` if (!failedOnce) { failedOnce = true; throw new Error("transient summary failure"); }`,
728
+ ` return realSummarize(sessionId, options);`,
729
+ ` };`,
730
+ `} else {`,
731
+ ` summarizer.summarizeIncremental = async () => { throw new Error("permanent summary failure"); };`,
732
+ `}`,
733
+ `await engine.enqueueObserveMessages("session-1", [`,
734
+ ` { role: "user", content: "one-shot host" },`,
735
+ ` { role: "assistant", content: "one-shot host too" },`,
736
+ `]);`,
737
+ // Wait (referenced) until the failed pass really left a retry pending.
738
+ `for (let i = 0; i < 200 && engine.summarizeRetryTimers.size === 0; i++) {`,
739
+ ` await new Promise((resolve) => setTimeout(resolve, 10));`,
740
+ `}`,
741
+ `if (engine.summarizeRetryTimers.size === 0) {`,
742
+ ` process.stdout.write("NO_RETRY_PENDING\\n");`,
743
+ ` process.exit(5);`,
744
+ `}`,
745
+ `if (mode === "background") {`,
746
+ // No explicit wait: the unreferenced retry is pure background work and
747
+ // must not keep the host alive. Natural exit proves it.
748
+ ` process.stdout.write("RETRY_PENDING_NO_WAITER\\n");`,
749
+ `} else {`,
750
+ ` process.on("beforeExit", () => {`,
751
+ ` process.stdout.write("PREMATURE_EXIT_UNDER_WAIT\\n");`,
752
+ ` process.exitCode = 3;`,
753
+ ` });`,
754
+ ` const startedAt = Date.now();`,
755
+ ` await engine.waitForObserveQueueIdle();`,
756
+ ` if (Date.now() - startedAt < 100) {`,
757
+ ` process.stdout.write("WAIT_RETURNED_EARLY " + (Date.now() - startedAt) + "\\n");`,
758
+ ` process.exit(4);`,
759
+ ` }`,
760
+ ` const summary = await engine.describeContext("session-1", 0, 1);`,
761
+ ` if (summary?.depth !== 0) {`,
762
+ ` process.stdout.write("RETRY_WORK_MISSING " + String(summary?.depth) + "\\n");`,
763
+ ` process.exit(6);`,
764
+ ` }`,
765
+ ` process.stdout.write("GLOBAL_IDLE_RESOLVED\\n");`,
766
+ ` process.exit(0);`,
767
+ `}`,
768
+ "",
769
+ ].join("\n");
770
+ const child = spawn(
771
+ process.execPath,
772
+ [
773
+ "--import", "tsx", "-e", childSource,
774
+ moduleUrl("./lcm/engine.js"),
775
+ moduleUrl("./config.js"),
776
+ memoryDir,
777
+ mode,
778
+ ],
779
+ { stdio: ["ignore", "pipe", "pipe"] },
780
+ );
781
+ let stdout = "";
782
+ let stderr = "";
783
+ child.stdout.setEncoding("utf8");
784
+ child.stderr.setEncoding("utf8");
785
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
786
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
787
+ const { promise, resolve, reject } = Promise.withResolvers<{
788
+ code: number | null;
789
+ stdout: string;
790
+ stderr: string;
791
+ }>();
792
+ // A one-shot host must settle in seconds; a wrongly referenced retry loop
793
+ // never exits, so the deadline kill is part of the assertion.
794
+ const deadline = setTimeout(() => {
795
+ child.kill("SIGKILL");
796
+ reject(new Error(`one-shot host (${mode}) did not exit within 10s; stderr: ${stderr || "(empty)"}`));
797
+ }, 10_000);
798
+ child.on("error", (err) => {
799
+ clearTimeout(deadline);
800
+ reject(err);
801
+ });
802
+ child.on("close", (code) => {
803
+ clearTimeout(deadline);
804
+ resolve({ code, stdout, stderr });
805
+ });
806
+ return promise;
807
+ }
808
+
809
+ // EXCEPTION (real timers): this test exercises genuine event-loop liveness —
810
+ // whether a real unref'd retry timer keeps a one-shot host alive. Fake timers
811
+ // cannot represent unref/ref or event-loop exhaustion, so real wall-clock
812
+ // backoff (150ms) is the subject under test, not an accidental delay.
813
+ test("a one-shot host survives a pending summary retry only while an idle wait is parked on it", async () => {
814
+ const dir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-retry-host-"));
815
+ try {
816
+ const moduleUrl = (name: string) => new URL(name, import.meta.url).href;
817
+
818
+ // Parked on the global idle wait: the host must stay alive while the
819
+ // retry timer pends, the retry must really fire and finish the work,
820
+ // and only then may the host exit.
821
+ const startedAt = Date.now();
822
+ const idleHost = await spawnOneShotHost(dir, "global-idle", moduleUrl);
823
+ assert.equal(
824
+ idleHost.code,
825
+ 0,
826
+ `host parked on global idle must exit cleanly; stderr: ${idleHost.stderr || "(empty)"}`,
827
+ );
828
+ assert.ok(
829
+ idleHost.stdout.includes("GLOBAL_IDLE_RESOLVED"),
830
+ `global idle wait must resolve via the retry; got: ${idleHost.stdout}`,
831
+ );
832
+ assert.ok(
833
+ !idleHost.stdout.includes("PREMATURE_EXIT_UNDER_WAIT"),
834
+ "host must not exit under the parked global idle wait",
835
+ );
836
+ assert.ok(
837
+ Date.now() - startedAt >= 100,
838
+ "the global idle wait must span the real retry backoff",
839
+ );
840
+
841
+ // No idle wait: the pending retry stays unreferenced background work and
842
+ // the host must still exit promptly (the previous referenced-timer bug).
843
+ const backgroundHost = await spawnOneShotHost(dir, "background", moduleUrl);
844
+ assert.equal(
845
+ backgroundHost.code,
846
+ 0,
847
+ `host with no idle waiter must not be kept alive by the retry; stderr: ${backgroundHost.stderr || "(empty)"}`,
848
+ );
849
+ assert.ok(
850
+ backgroundHost.stdout.includes("RETRY_PENDING_NO_WAITER"),
851
+ `expected a pending retry at exit; got: ${backgroundHost.stdout}`,
852
+ );
853
+ } finally {
854
+ await rm(dir, { recursive: true, force: true });
855
+ }
856
+ });