@remnic/core 9.3.718 → 9.3.720

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 (323) hide show
  1. package/dist/access-boundary.d.ts +10 -7
  2. package/dist/access-boundary.js +18 -17
  3. package/dist/access-cli.js +39 -37
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +11 -8
  6. package/dist/access-http.js +25 -24
  7. package/dist/access-mcp.d.ts +11 -8
  8. package/dist/access-mcp.js +21 -20
  9. package/dist/access-operations-batch.js +19 -18
  10. package/dist/access-operations.d.ts +11 -8
  11. package/dist/access-operations.js +20 -19
  12. package/dist/access-schema.d.ts +19 -19
  13. package/dist/{access-service-CZbnt0t2.d.ts → access-service-Dw-tK1pa.d.ts} +3 -3
  14. package/dist/access-service.d.ts +11 -8
  15. package/dist/access-service.js +17 -16
  16. package/dist/access-surface-catalog.d.ts +10 -7
  17. package/dist/action-confidence.d.ts +1 -1
  18. package/dist/active-memory-bridge.d.ts +1 -1
  19. package/dist/active-recall.d.ts +1 -1
  20. package/dist/active-recall.js +1 -1
  21. package/dist/adapters/index.js +7 -7
  22. package/dist/adapters/registry.js +3 -3
  23. package/dist/behavior-learner.d.ts +1 -1
  24. package/dist/behavior-signals.d.ts +1 -1
  25. package/dist/bootstrap.d.ts +11 -8
  26. package/dist/briefing.d.ts +1 -1
  27. package/dist/briefing.js +4 -4
  28. package/dist/buffer-surprise-report.d.ts +1 -1
  29. package/dist/buffer.d.ts +1 -1
  30. package/dist/calibration.d.ts +1 -1
  31. package/dist/capabilities.d.ts +30 -2
  32. package/dist/capabilities.js +3 -1
  33. package/dist/{catalog-CEYlMs9X.d.ts → catalog-B9SPfcsE.d.ts} +1 -1
  34. package/dist/causal-behavior.d.ts +1 -1
  35. package/dist/causal-behavior.js +2 -2
  36. package/dist/causal-chain.js +2 -2
  37. package/dist/causal-consolidation.d.ts +1 -1
  38. package/dist/causal-consolidation.js +7 -7
  39. package/dist/causal-retrieval.js +2 -2
  40. package/dist/causal-trajectory-graph.d.ts +4 -0
  41. package/dist/causal-trajectory-graph.js +1 -1
  42. package/dist/causal-trajectory.js +1 -1
  43. package/dist/{chunk-7EEVXW7X.js → chunk-2RBMGTK4.js} +2 -2
  44. package/dist/{chunk-L5ASZA2F.js → chunk-3XEGHEQ2.js} +17 -2
  45. package/dist/chunk-3XEGHEQ2.js.map +1 -0
  46. package/dist/{chunk-YYOK4GVF.js → chunk-422TIKIH.js} +3 -3
  47. package/dist/{chunk-KRGC4EDX.js → chunk-5YQN2XYC.js} +2 -2
  48. package/dist/{chunk-ANI5DUAE.js → chunk-6S5ZI7HB.js} +7 -7
  49. package/dist/{chunk-JIZGYQYH.js → chunk-ATGRQ2NE.js} +4 -4
  50. package/dist/chunk-BCDKBDSY.js +173 -0
  51. package/dist/chunk-BCDKBDSY.js.map +1 -0
  52. package/dist/{chunk-P5IPFZVH.js → chunk-BGQ7WKMT.js} +5 -5
  53. package/dist/{chunk-JAKYZGV4.js → chunk-CYJV5AYC.js} +3 -3
  54. package/dist/{chunk-APUJIMDM.js → chunk-DG3ORVCU.js} +2 -2
  55. package/dist/{chunk-PBH4JUAB.js → chunk-DVRHLN2O.js} +18 -3
  56. package/dist/chunk-DVRHLN2O.js.map +1 -0
  57. package/dist/{chunk-S35FX6MU.js → chunk-H2AGJ5SV.js} +2 -2
  58. package/dist/{chunk-A66YRSOS.js → chunk-HAPVZ4CO.js} +2 -2
  59. package/dist/{chunk-4KZGLDJJ.js → chunk-HHSO6VCQ.js} +2 -2
  60. package/dist/{chunk-2VHMVWPU.js → chunk-HTIL3EJK.js} +2 -2
  61. package/dist/{chunk-UCJ5CDTK.js → chunk-HU4EBIXO.js} +22 -14
  62. package/dist/chunk-HU4EBIXO.js.map +1 -0
  63. package/dist/{chunk-CTOQEZSN.js → chunk-IPJD7WJH.js} +2 -2
  64. package/dist/{chunk-DIFU33GU.js → chunk-JV7Q226R.js} +2 -2
  65. package/dist/{chunk-U5H7WHTS.js → chunk-KNN6DAZQ.js} +4 -4
  66. package/dist/{chunk-3JJWNZTT.js → chunk-L2B3EE7B.js} +102 -42
  67. package/dist/chunk-L2B3EE7B.js.map +1 -0
  68. package/dist/{chunk-5RHM43BJ.js → chunk-LLLG5PTE.js} +3 -3
  69. package/dist/{chunk-F3PVXKHP.js → chunk-M3GBOKUX.js} +22 -16
  70. package/dist/chunk-M3GBOKUX.js.map +1 -0
  71. package/dist/{chunk-2PTBLS2S.js → chunk-NBYXOEO7.js} +2 -2
  72. package/dist/{chunk-URM3HW2R.js → chunk-NE5WFIY6.js} +2 -2
  73. package/dist/{chunk-REWQAFSA.js → chunk-NERBFQNY.js} +2 -2
  74. package/dist/{chunk-773DPI6T.js → chunk-NO4DWSQE.js} +10 -10
  75. package/dist/{chunk-OZ2KQ7HF.js → chunk-O6SEGJNV.js} +2 -2
  76. package/dist/{chunk-IF362TF6.js → chunk-PMBWVQ7D.js} +2 -2
  77. package/dist/chunk-PMBWVQ7D.js.map +1 -0
  78. package/dist/{chunk-OJU73BUX.js → chunk-PPVCZPYA.js} +2 -2
  79. package/dist/{chunk-S3OPGGZB.js → chunk-RIW3UDDY.js} +2 -2
  80. package/dist/{chunk-AU7Q3LSC.js → chunk-RKNJBZ55.js} +7 -7
  81. package/dist/{chunk-EH27LHML.js → chunk-RX7EBFFJ.js} +2 -2
  82. package/dist/{chunk-2COL35PK.js → chunk-T3XSXAWK.js} +2 -2
  83. package/dist/{chunk-2KAYTPPT.js → chunk-UUMYB3FR.js} +4 -4
  84. package/dist/{chunk-72VDBALG.js → chunk-V5EGLYXE.js} +52 -43
  85. package/dist/chunk-V5EGLYXE.js.map +1 -0
  86. package/dist/{chunk-LXSG4L55.js → chunk-V7UUIIR7.js} +2 -2
  87. package/dist/{chunk-HLIR3RJ7.js → chunk-WBHQ5LP5.js} +2 -2
  88. package/dist/{chunk-YGKUAX2B.js → chunk-XROP4ZHB.js} +8 -8
  89. package/dist/chunk-XROP4ZHB.js.map +1 -0
  90. package/dist/{chunk-NFRBADNR.js → chunk-Y4I7SOJN.js} +559 -318
  91. package/dist/chunk-Y4I7SOJN.js.map +1 -0
  92. package/dist/{chunk-YTZLBBEB.js → chunk-YMPEGBLL.js} +3 -3
  93. package/dist/chunk-YN2UN7N3.js +217 -0
  94. package/dist/chunk-YN2UN7N3.js.map +1 -0
  95. package/dist/{chunk-YI7RZO7Y.js → chunk-ZE7GJ76H.js} +1 -1
  96. package/dist/{chunk-74MYJSKB.js → chunk-ZG2WP425.js} +2 -2
  97. package/dist/{chunk-A5TUPZ3N.js → chunk-ZKUW2MJG.js} +43 -1
  98. package/dist/chunk-ZKUW2MJG.js.map +1 -0
  99. package/dist/{chunk-N3DUH6OO.js → chunk-ZW5I3UNP.js} +1 -1
  100. package/dist/chunk-ZW5I3UNP.js.map +1 -0
  101. package/dist/{cli-1orcdXOY.d.ts → cli-qcLCJlOD.d.ts} +3 -3
  102. package/dist/cli.d.ts +9 -6
  103. package/dist/cli.js +43 -42
  104. package/dist/compounding/engine.d.ts +1 -1
  105. package/dist/compounding/engine.js +4 -4
  106. package/dist/compounding/preference-consolidator.d.ts +1 -1
  107. package/dist/compression-optimizer.d.ts +1 -1
  108. package/dist/config.d.ts +1 -1
  109. package/dist/config.js +1 -1
  110. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  111. package/dist/connectors/codex-materialize-runner.js +4 -4
  112. package/dist/connectors/codex-materialize.d.ts +1 -1
  113. package/dist/connectors/index.d.ts +1 -1
  114. package/dist/connectors/index.js +4 -4
  115. package/dist/consolidation-provenance-check.d.ts +1 -1
  116. package/dist/consolidation-undo.d.ts +1 -1
  117. package/dist/contradiction/index.d.ts +1 -1
  118. package/dist/conversation-index/backend.d.ts +1 -1
  119. package/dist/conversation-index/chunker.d.ts +1 -1
  120. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  121. package/dist/conversation-index/indexer.d.ts +1 -1
  122. package/dist/conversation-index/search.d.ts +1 -1
  123. package/dist/day-summary.d.ts +1 -1
  124. package/dist/delinearize.d.ts +1 -1
  125. package/dist/direct-answer-wiring.d.ts +1 -1
  126. package/dist/direct-answer.d.ts +1 -1
  127. package/dist/embedding-fallback.d.ts +1 -1
  128. package/dist/enrichment/index.d.ts +1 -1
  129. package/dist/entity-retrieval.d.ts +1 -1
  130. package/dist/entity-retrieval.js +4 -4
  131. package/dist/entity-schema.d.ts +1 -1
  132. package/dist/explicit-capture.d.ts +11 -8
  133. package/dist/extraction-faithfulness.d.ts +1 -1
  134. package/dist/extraction-judge-telemetry.d.ts +1 -1
  135. package/dist/extraction-judge-training.d.ts +1 -1
  136. package/dist/extraction-judge.d.ts +1 -1
  137. package/dist/extraction.d.ts +1 -1
  138. package/dist/fallback-llm.d.ts +1 -1
  139. package/dist/{forget-U66KPDPY.js → forget-OEE5OELK.js} +2 -2
  140. package/dist/graph-dashboard-diff.d.ts +4 -0
  141. package/dist/graph-dashboard-key.d.ts +4 -0
  142. package/dist/graph-dashboard-parser.d.ts +4 -0
  143. package/dist/{graph-edge-decay-JLOF3YW4.js → graph-edge-decay-QDDC6AVV.js} +2 -2
  144. package/dist/graph-edge-reinforcement.d.ts +4 -0
  145. package/dist/graph-snapshot.d.ts +7 -3
  146. package/dist/graph-snapshot.js +2 -2
  147. package/dist/graph.d.ts +7 -1
  148. package/dist/graph.js +1 -1
  149. package/dist/identity-continuity.d.ts +1 -1
  150. package/dist/importance.d.ts +1 -1
  151. package/dist/index.d.ts +571 -570
  152. package/dist/index.js +57 -55
  153. package/dist/index.js.map +1 -1
  154. package/dist/intent.d.ts +1 -1
  155. package/dist/lcm/engine.d.ts +1 -1
  156. package/dist/lcm/engine.js +4 -4
  157. package/dist/lcm/index.d.ts +1 -1
  158. package/dist/lcm/index.js +8 -8
  159. package/dist/lcm/tools.d.ts +1 -1
  160. package/dist/lifecycle.d.ts +1 -1
  161. package/dist/live-connectors-runner.d.ts +1 -1
  162. package/dist/local-llm.d.ts +1 -1
  163. package/dist/maintenance/memory-governance.d.ts +1 -1
  164. package/dist/maintenance/memory-governance.js +4 -4
  165. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
  166. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  167. package/dist/mcp-memory-inspector-app.d.ts +11 -8
  168. package/dist/memory-action-policy.d.ts +1 -1
  169. package/dist/memory-cache.d.ts +1 -1
  170. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  171. package/dist/memory-projection-store.d.ts +1 -1
  172. package/dist/memory-provenance.d.ts +1 -1
  173. package/dist/memory-worth-outcomes.d.ts +1 -1
  174. package/dist/models-json.d.ts +1 -1
  175. package/dist/namespaces/migrate.d.ts +2 -2
  176. package/dist/namespaces/migrate.js +9 -9
  177. package/dist/namespaces/principal.d.ts +1 -1
  178. package/dist/namespaces/search.d.ts +1 -1
  179. package/dist/namespaces/search.js +4 -4
  180. package/dist/namespaces/storage.d.ts +2 -2
  181. package/dist/namespaces/storage.js +4 -4
  182. package/dist/native-knowledge.d.ts +1 -1
  183. package/dist/operator-toolkit.d.ts +3 -2
  184. package/dist/operator-toolkit.js +16 -15
  185. package/dist/orchestration/maintenance.d.ts +2 -2
  186. package/dist/orchestration/maintenance.js +6 -6
  187. package/dist/{orchestrator-kOxredSr.d.ts → orchestrator-Bg5Fnza6.d.ts} +178 -81
  188. package/dist/orchestrator.d.ts +8 -5
  189. package/dist/orchestrator.js +31 -29
  190. package/dist/patterns-cli.d.ts +1 -1
  191. package/dist/policy-runtime.d.ts +1 -1
  192. package/dist/provenance.d.ts +1 -1
  193. package/dist/qmd-recall-cache.d.ts +1 -1
  194. package/dist/qmd.d.ts +1 -1
  195. package/dist/recall-disclosure-escalation.d.ts +1 -1
  196. package/dist/recall-explain-renderer.d.ts +1 -1
  197. package/dist/recall-explain-renderer.js +3 -3
  198. package/dist/recall-planner-llm.d.ts +1 -1
  199. package/dist/recall-state.d.ts +1 -1
  200. package/dist/recall-tag-filter.d.ts +1 -1
  201. package/dist/recall-xray-cli.d.ts +1 -1
  202. package/dist/recall-xray-cli.js +4 -4
  203. package/dist/recall-xray-renderer.d.ts +1 -1
  204. package/dist/recall-xray-renderer.js +3 -3
  205. package/dist/recall-xray.d.ts +18 -1
  206. package/dist/recall-xray.js +2 -2
  207. package/dist/resolve-auth-token.d.ts +1 -1
  208. package/dist/resume-bundles.js +2 -2
  209. package/dist/retrieval-agents.d.ts +1 -1
  210. package/dist/retrieval-tiers.d.ts +1 -1
  211. package/dist/routing/engine.d.ts +1 -1
  212. package/dist/routing/store.d.ts +1 -1
  213. package/dist/search/embed-helper.d.ts +1 -1
  214. package/dist/search/factory.d.ts +1 -1
  215. package/dist/search/factory.js +3 -3
  216. package/dist/search/index.d.ts +1 -1
  217. package/dist/search/index.js +7 -7
  218. package/dist/search/lancedb-backend.d.ts +1 -1
  219. package/dist/search/meilisearch-backend.d.ts +1 -1
  220. package/dist/search/noop-backend.d.ts +1 -1
  221. package/dist/search/orama-backend.d.ts +1 -1
  222. package/dist/search/port.d.ts +1 -1
  223. package/dist/search/remote-backend.d.ts +1 -1
  224. package/dist/{semantic-consolidation-B58v3S-a.d.ts → semantic-consolidation-ClOiS6KP.d.ts} +1 -1
  225. package/dist/semantic-consolidation.d.ts +2 -2
  226. package/dist/semantic-consolidation.js +5 -5
  227. package/dist/semantic-rule-promotion.js +4 -4
  228. package/dist/semantic-rule-verifier.d.ts +1 -1
  229. package/dist/semantic-rule-verifier.js +4 -4
  230. package/dist/session-observer-bands.d.ts +1 -1
  231. package/dist/session-observer-state.d.ts +1 -1
  232. package/dist/shared-context/manager.d.ts +1 -1
  233. package/dist/signal.d.ts +1 -1
  234. package/dist/storage.d.ts +45 -1
  235. package/dist/storage.js +3 -3
  236. package/dist/summarizer.d.ts +1 -1
  237. package/dist/summary-snapshot.d.ts +1 -1
  238. package/dist/temporal-supersession.d.ts +1 -1
  239. package/dist/temporal-validity.d.ts +1 -1
  240. package/dist/threading.d.ts +1 -1
  241. package/dist/tier-migration.d.ts +1 -1
  242. package/dist/tier-routing.d.ts +1 -1
  243. package/dist/topics.d.ts +1 -1
  244. package/dist/transcript.d.ts +1 -1
  245. package/dist/trust-score-stage.d.ts +178 -0
  246. package/dist/trust-score-stage.js +18 -0
  247. package/dist/trust-score-stage.js.map +1 -0
  248. package/dist/trust-score.d.ts +164 -0
  249. package/dist/trust-score.js +22 -0
  250. package/dist/trust-score.js.map +1 -0
  251. package/dist/{types-ACmDOScJ.d.ts → types-BSdUWePh.d.ts} +37 -1
  252. package/dist/types.d.ts +1 -1
  253. package/dist/types.js +1 -1
  254. package/dist/utility-runtime.d.ts +1 -1
  255. package/dist/verified-recall.js +4 -4
  256. package/package.json +2 -2
  257. package/src/access-service.ts +8 -5
  258. package/src/capabilities-cluster-b.test.ts +78 -0
  259. package/src/capabilities.test.ts +11 -1
  260. package/src/capabilities.ts +58 -0
  261. package/src/cli/research-status-commands.ts +7 -3
  262. package/src/cli.ts +11 -6
  263. package/src/config.ts +51 -0
  264. package/src/graph-snapshot.ts +3 -3
  265. package/src/graph.ts +8 -7
  266. package/src/lifecycle/tombstones.test.ts +242 -1
  267. package/src/lifecycle/tombstones.ts +157 -70
  268. package/src/operator-toolkit.ts +24 -7
  269. package/src/orchestration/tier-migration-coordinator.ts +286 -0
  270. package/src/orchestrator.ts +331 -222
  271. package/src/recall-xray.test.ts +62 -0
  272. package/src/recall-xray.ts +34 -0
  273. package/src/trust-score-recall-paths.test.ts +157 -0
  274. package/src/trust-score-stage.test.ts +350 -0
  275. package/src/trust-score-stage.ts +398 -0
  276. package/src/trust-score.test.ts +282 -0
  277. package/src/trust-score.ts +449 -0
  278. package/src/types.ts +42 -0
  279. package/dist/chunk-3JJWNZTT.js.map +0 -1
  280. package/dist/chunk-72VDBALG.js.map +0 -1
  281. package/dist/chunk-A5TUPZ3N.js.map +0 -1
  282. package/dist/chunk-F3PVXKHP.js.map +0 -1
  283. package/dist/chunk-IF362TF6.js.map +0 -1
  284. package/dist/chunk-L5ASZA2F.js.map +0 -1
  285. package/dist/chunk-N3DUH6OO.js.map +0 -1
  286. package/dist/chunk-NFRBADNR.js.map +0 -1
  287. package/dist/chunk-PBH4JUAB.js.map +0 -1
  288. package/dist/chunk-UCJ5CDTK.js.map +0 -1
  289. package/dist/chunk-YGKUAX2B.js.map +0 -1
  290. /package/dist/{chunk-7EEVXW7X.js.map → chunk-2RBMGTK4.js.map} +0 -0
  291. /package/dist/{chunk-YYOK4GVF.js.map → chunk-422TIKIH.js.map} +0 -0
  292. /package/dist/{chunk-KRGC4EDX.js.map → chunk-5YQN2XYC.js.map} +0 -0
  293. /package/dist/{chunk-ANI5DUAE.js.map → chunk-6S5ZI7HB.js.map} +0 -0
  294. /package/dist/{chunk-JIZGYQYH.js.map → chunk-ATGRQ2NE.js.map} +0 -0
  295. /package/dist/{chunk-P5IPFZVH.js.map → chunk-BGQ7WKMT.js.map} +0 -0
  296. /package/dist/{chunk-JAKYZGV4.js.map → chunk-CYJV5AYC.js.map} +0 -0
  297. /package/dist/{chunk-APUJIMDM.js.map → chunk-DG3ORVCU.js.map} +0 -0
  298. /package/dist/{chunk-S35FX6MU.js.map → chunk-H2AGJ5SV.js.map} +0 -0
  299. /package/dist/{chunk-A66YRSOS.js.map → chunk-HAPVZ4CO.js.map} +0 -0
  300. /package/dist/{chunk-4KZGLDJJ.js.map → chunk-HHSO6VCQ.js.map} +0 -0
  301. /package/dist/{chunk-2VHMVWPU.js.map → chunk-HTIL3EJK.js.map} +0 -0
  302. /package/dist/{chunk-CTOQEZSN.js.map → chunk-IPJD7WJH.js.map} +0 -0
  303. /package/dist/{chunk-DIFU33GU.js.map → chunk-JV7Q226R.js.map} +0 -0
  304. /package/dist/{chunk-U5H7WHTS.js.map → chunk-KNN6DAZQ.js.map} +0 -0
  305. /package/dist/{chunk-5RHM43BJ.js.map → chunk-LLLG5PTE.js.map} +0 -0
  306. /package/dist/{chunk-2PTBLS2S.js.map → chunk-NBYXOEO7.js.map} +0 -0
  307. /package/dist/{chunk-URM3HW2R.js.map → chunk-NE5WFIY6.js.map} +0 -0
  308. /package/dist/{chunk-REWQAFSA.js.map → chunk-NERBFQNY.js.map} +0 -0
  309. /package/dist/{chunk-773DPI6T.js.map → chunk-NO4DWSQE.js.map} +0 -0
  310. /package/dist/{chunk-OZ2KQ7HF.js.map → chunk-O6SEGJNV.js.map} +0 -0
  311. /package/dist/{chunk-OJU73BUX.js.map → chunk-PPVCZPYA.js.map} +0 -0
  312. /package/dist/{chunk-S3OPGGZB.js.map → chunk-RIW3UDDY.js.map} +0 -0
  313. /package/dist/{chunk-AU7Q3LSC.js.map → chunk-RKNJBZ55.js.map} +0 -0
  314. /package/dist/{chunk-EH27LHML.js.map → chunk-RX7EBFFJ.js.map} +0 -0
  315. /package/dist/{chunk-2COL35PK.js.map → chunk-T3XSXAWK.js.map} +0 -0
  316. /package/dist/{chunk-2KAYTPPT.js.map → chunk-UUMYB3FR.js.map} +0 -0
  317. /package/dist/{chunk-LXSG4L55.js.map → chunk-V7UUIIR7.js.map} +0 -0
  318. /package/dist/{chunk-HLIR3RJ7.js.map → chunk-WBHQ5LP5.js.map} +0 -0
  319. /package/dist/{chunk-YTZLBBEB.js.map → chunk-YMPEGBLL.js.map} +0 -0
  320. /package/dist/{chunk-YI7RZO7Y.js.map → chunk-ZE7GJ76H.js.map} +0 -0
  321. /package/dist/{chunk-74MYJSKB.js.map → chunk-ZG2WP425.js.map} +0 -0
  322. /package/dist/{forget-U66KPDPY.js.map → forget-OEE5OELK.js.map} +0 -0
  323. /package/dist/{graph-edge-decay-JLOF3YW4.js.map → graph-edge-decay-QDDC6AVV.js.map} +0 -0
@@ -40,7 +40,7 @@
40
40
  // - Semantic tier (4) ships dark, off by default (rule 48).
41
41
  // ---------------------------------------------------------------------------
42
42
 
43
- import { serializeMutations } from "../utils/serialize-mutations.js";
43
+ import { serializeMutations, withHeldFileLock } from "../utils/serialize-mutations.js";
44
44
 
45
45
  /** Why a tombstone was emitted. */
46
46
  export type TombstoneReason =
@@ -129,6 +129,20 @@ export interface TombstoneStoreOptions {
129
129
  * skipped entirely.
130
130
  */
131
131
  semanticSimilarity?: (a: string, b: string) => number;
132
+ /**
133
+ * Cross-process write-lock timings for the tombstone JSONL mutation lock
134
+ * (issue #1639). The secure-store append is a read-merge-write (read
135
+ * encrypted → decrypt → concat → re-encrypt → atomic rename), so two
136
+ * Remnic processes appending concurrently can each read the same contents
137
+ * and the last writer drops the other's entry — breaking the
138
+ * non-resurrection invariant. The lock serializes the mutation across
139
+ * processes. When omitted, defaults that fit the short tombstone critical
140
+ * section (mirroring the summary-snapshot lock): staleMs 30s, maxWaitMs 5s,
141
+ * heartbeatMs floor(staleMs/3).
142
+ */
143
+ readonly lockStaleMs?: number;
144
+ readonly lockMaxWaitMs?: number;
145
+ readonly lockHeartbeatMs?: number;
132
146
  }
133
147
 
134
148
  /** Injected file I/O — the StorageManager wires its secure-store-aware
@@ -261,7 +275,33 @@ export class TombstoneStore {
261
275
  private readonly namespace: string,
262
276
  private readonly options: TombstoneStoreOptions,
263
277
  private readonly io: TombstoneFileIo,
264
- ) {}
278
+ ) {
279
+ this.lockStaleMs = this.options.lockStaleMs ?? 30_000;
280
+ this.lockMaxWaitMs = this.options.lockMaxWaitMs ?? 5_000;
281
+ this.lockHeartbeatMs =
282
+ this.options.lockHeartbeatMs ?? Math.max(100, Math.floor(this.lockStaleMs / 3));
283
+ }
284
+
285
+ /**
286
+ * Resolved cross-process write-lock timings (issue #1639). Defaults mirror
287
+ * the summary-snapshot lock: a tombstone append critical section is
288
+ * sub-second, so a stale lock older than 30s is a crashed holder; bounded
289
+ * acquisition gives up after 5s and strict-fails rather than clobbering a
290
+ * concurrent writer (which would reintroduce the lost-write race this lock
291
+ * closes).
292
+ */
293
+ private readonly lockStaleMs: number;
294
+ private readonly lockMaxWaitMs: number;
295
+ private readonly lockHeartbeatMs: number;
296
+
297
+ /**
298
+ * Sibling advisory lockfile: `<tombstones>.jsonl` → `<tombstones>.lock`.
299
+ * Shared by append, revoke, and rebuild so every JSONL mutation serializes
300
+ * against the others across processes (issue #1639: "shared with rebuilds").
301
+ */
302
+ private get lockPath(): string {
303
+ return this.filePath.replace(/\.jsonl$/i, "") + ".lock";
304
+ }
265
305
 
266
306
  /** Lazy load + build the in-memory index. Idempotent. */
267
307
  async load(): Promise<void> {
@@ -473,10 +513,48 @@ export class TombstoneStore {
473
513
 
474
514
  private serializeAppend(entry: TombstoneEntry): Promise<void> {
475
515
  const line = JSON.stringify(entry) + "\n";
476
- // serializeMutations recovers after rejection (rule 40): a failed append
477
- // surfaces to THIS caller but the next append is not poisoned.
516
+ // serializeMutations (in-process, rule 40 rejection recovery) wraps the
517
+ // cross-process lock: within one process appends are queued, and across
518
+ // processes the advisory lockfile serializes the read-merge-write so two
519
+ // Remnic processes appending concurrently cannot drop each other's entry
520
+ // (issue #1639). Under the lock we re-sync the in-memory index against
521
+ // disk first, so indexEntry builds on the peer's just-appended entries.
478
522
  return serializeMutations(`tombstone:${this.filePath}`, () =>
479
- this.io.append(this.filePath, line),
523
+ this.withWriteLock(async () => {
524
+ await this.ensureFreshAgainstDisk();
525
+ await this.io.append(this.filePath, line);
526
+ }),
527
+ );
528
+ }
529
+
530
+ /**
531
+ * Acquire the cross-process tombstone write lock and run `task` under it
532
+ * (issue #1639). Strict-fail on acquisition timeout: the secure-store append
533
+ * is a read-merge-write, so a best-effort unlocked run would clobber a
534
+ * concurrent writer — reintroducing the lost-write race this lock closes.
535
+ * The thrown error surfaces to the caller (rule 34: never silently drop);
536
+ * rebuild recovers the tombstone on the next maintenance cycle.
537
+ *
538
+ * In-process callers are already serialized by serializeMutations (one key
539
+ * for append/revoke/rebuild), so this lock only contends across processes —
540
+ * the single-process daemon (default deployment) acquires it immediately.
541
+ */
542
+ private withWriteLock<T>(task: () => Promise<T>): Promise<T> {
543
+ return withHeldFileLock(
544
+ this.lockPath,
545
+ {
546
+ staleMs: this.lockStaleMs,
547
+ maxWaitMs: this.lockMaxWaitMs,
548
+ heartbeatMs: this.lockHeartbeatMs,
549
+ },
550
+ async (acquired) => {
551
+ if (!acquired) {
552
+ throw new Error(
553
+ "could not acquire tombstone write lock (contention timeout or filesystem error)",
554
+ );
555
+ }
556
+ return task();
557
+ },
480
558
  );
481
559
  }
482
560
 
@@ -595,72 +673,81 @@ export class TombstoneStore {
595
673
  * Returns the count of tombstone entries written.
596
674
  */
597
675
  async rebuild(retiredMemories: ReadonlyArray<RetiredMemoryRecord>): Promise<number> {
598
- // Preserve existing revocations (all namespaces ids are globally
599
- // unique) so a rebuild does not silently un-revoke.
600
- const existingRevocations = this.entries.filter((e) => e.kind === "revocation");
601
- // Preserve tombstone entries from OTHER namespaces when the backing file
602
- // is shared (issue #1579 thread Oc2MJ). rebuild rewrites the entire file;
603
- // without preserving foreign entries, rebuilding namespace A would
604
- // silently delete namespace B's tombstones, allowing resurrection in B.
605
- const foreignTombstones = this.entries.filter(
606
- (e) => e.kind === "tombstone" && e.namespace !== this.namespace,
607
- );
608
- // Reuse existing tombstone ids for source-equivalent entries so a prior
609
- // revocation (which references the tombstone id) survives rebuild — minting
610
- // fresh ids would orphan the revocation and silently un-revoke the content.
611
- // Issue #1579 thread Oci-T: key the reuse map by (sourceMemoryId,
612
- // supersessionKey), not just sourceMemoryId. A retired fact with multiple
613
- // structured-attribute keys emits one record per key (see
614
- // collectRetiredMemoriesForRebuild); keying only by sourceMemoryId made
615
- // every rebuilt record share one id, so a revocation of key A silently
616
- // revoked key B (over-revoke) or, if a prior single-key tombstone was
617
- // revoked, the new multi-key records all inherited a revoked id and
618
- // silently un-blocked (orphan). Including the supersession-key
619
- // discriminator keeps each keyed tombstone's id (and thus its revocation)
620
- // independent. Records without a supersession key fall back to a stable
621
- // empty-string discriminator so they still reuse by sourceMemoryId alone.
622
- const existingBySource = new Map<string, string>();
623
- for (const e of this.entries) {
624
- if (e.kind === "tombstone") {
625
- existingBySource.set(`${e.sourceMemoryId}\u{0000}${e.supersessionKey ?? ""}`, e.id);
626
- }
627
- }
628
- const rebuilt: TombstoneEntry[] = retiredMemories.map((m) => ({
629
- id:
630
- existingBySource.get(`${m.memoryId}\u{0000}${m.supersessionKey ?? ""}`) ??
631
- newTombstoneId(),
632
- kind: "tombstone" as const,
633
- reason: m.reason,
634
- sourceMemoryId: m.memoryId,
635
- contentHash: m.contentHash ?? this.options.hashContent(m.rawContent),
636
- normalizedText: this.options.normalizeText(m.rawContent),
637
- ...(m.entityRef ? { entityRef: m.entityRef } : {}),
638
- ...(m.supersessionKey ? { supersessionKey: m.supersessionKey } : {}),
639
- namespace: this.namespace,
640
- createdAt: m.createdAt,
641
- createdBy: m.createdBy,
642
- }));
643
- // Sort deterministically (rule 38): createdAt, then id for stability.
644
- rebuilt.sort((a, b) =>
645
- a.createdAt === b.createdAt
646
- ? a.id < b.id ? -1 : a.id > b.id ? 1 : 0
647
- : a.createdAt < b.createdAt ? -1 : 1,
648
- );
649
- const all = [...rebuilt, ...existingRevocations, ...foreignTombstones].sort((a, b) =>
650
- a.createdAt === b.createdAt
651
- ? a.id < b.id ? -1 : a.id > b.id ? 1 : 0
652
- : a.createdAt < b.createdAt ? -1 : 1,
653
- );
654
- const serialized = all.map((e) => JSON.stringify(e)).join("\n") + (all.length > 0 ? "\n" : "");
655
- await serializeMutations(`tombstone-rebuild:${this.filePath}`, () =>
656
- this.io.write(this.filePath, serialized),
676
+ // The full read-merge-write runs UNDER the cross-process lock (issue #1639,
677
+ // cursor/codex review): we ensureFreshAgainstDisk() first so the in-memory
678
+ // index reflects any peer append/revoke that landed while we waited for the
679
+ // lock, THEN compute the payload. Rebuild rewrites the entire JSONL, so a
680
+ // payload built from a stale this.entries would overwrite a peer's just-
681
+ // written entry and resurrect a retired fact — the exact invariant this
682
+ // store exists to enforce. serializeMutations (in-process) wraps the
683
+ // cross-process lock; the key is unified with append/revoke so they cannot
684
+ // interleave in-process either.
685
+ const rebuiltCount = await serializeMutations(`tombstone:${this.filePath}`, () =>
686
+ this.withWriteLock(async () => {
687
+ await this.ensureFreshAgainstDisk();
688
+ // Preserve existing revocations (all namespaces ids are globally
689
+ // unique) so a rebuild does not silently un-revoke.
690
+ const existingRevocations = this.entries.filter((e) => e.kind === "revocation");
691
+ // Preserve tombstone entries from OTHER namespaces when the backing
692
+ // file is shared (issue #1579 thread Oc2MJ). rebuild rewrites the
693
+ // entire file; without preserving foreign entries, rebuilding
694
+ // namespace A would silently delete namespace B's tombstones, allowing
695
+ // resurrection in B.
696
+ const foreignTombstones = this.entries.filter(
697
+ (e) => e.kind === "tombstone" && e.namespace !== this.namespace,
698
+ );
699
+ // Reuse existing tombstone ids for source-equivalent entries so a prior
700
+ // revocation (which references the tombstone id) survives rebuild —
701
+ // minting fresh ids would orphan the revocation and silently un-revoke.
702
+ // Issue #1579 thread Oci-T: key the reuse map by (sourceMemoryId,
703
+ // supersessionKey). See the append path above for the full rationale.
704
+ const existingBySource = new Map<string, string>();
705
+ for (const e of this.entries) {
706
+ if (e.kind === "tombstone") {
707
+ existingBySource.set(`${e.sourceMemoryId}\u{0000}${e.supersessionKey ?? ""}`, e.id);
708
+ }
709
+ }
710
+ const rebuilt: TombstoneEntry[] = retiredMemories.map((m) => ({
711
+ id:
712
+ existingBySource.get(`${m.memoryId}\u{0000}${m.supersessionKey ?? ""}`) ??
713
+ newTombstoneId(),
714
+ kind: "tombstone" as const,
715
+ reason: m.reason,
716
+ sourceMemoryId: m.memoryId,
717
+ contentHash: m.contentHash ?? this.options.hashContent(m.rawContent),
718
+ normalizedText: this.options.normalizeText(m.rawContent),
719
+ ...(m.entityRef ? { entityRef: m.entityRef } : {}),
720
+ ...(m.supersessionKey ? { supersessionKey: m.supersessionKey } : {}),
721
+ namespace: this.namespace,
722
+ createdAt: m.createdAt,
723
+ createdBy: m.createdBy,
724
+ }));
725
+ // Sort deterministically (rule 38): createdAt, then id for stability.
726
+ rebuilt.sort((a, b) =>
727
+ a.createdAt === b.createdAt
728
+ ? a.id < b.id ? -1 : a.id > b.id ? 1 : 0
729
+ : a.createdAt < b.createdAt ? -1 : 1,
730
+ );
731
+ const all = [...rebuilt, ...existingRevocations, ...foreignTombstones].sort((a, b) =>
732
+ a.createdAt === b.createdAt
733
+ ? a.id < b.id ? -1 : a.id > b.id ? 1 : 0
734
+ : a.createdAt < b.createdAt ? -1 : 1,
735
+ );
736
+ const serialized =
737
+ all.map((e) => JSON.stringify(e)).join("\n") + (all.length > 0 ? "\n" : "");
738
+ await this.io.write(this.filePath, serialized);
739
+ // Update the in-memory index under the same lock so it stays consistent
740
+ // with the file we just wrote (no window for a lookup to see a stale
741
+ // index that misses a just-rebuilt entry).
742
+ this.resetIndex();
743
+ for (const entry of all) this.indexEntry(entry);
744
+ this.corruptedLines = 0;
745
+ this.loaded = true;
746
+ this.markWritten();
747
+ return rebuilt.length;
748
+ }),
657
749
  );
658
- this.resetIndex();
659
- for (const entry of all) this.indexEntry(entry);
660
- this.corruptedLines = 0;
661
- this.loaded = true;
662
- this.markWritten();
663
- return rebuilt.length;
750
+ return rebuiltCount;
664
751
  }
665
752
  }
666
753
 
@@ -24,6 +24,11 @@ import {
24
24
  type EvalHarnessStatus,
25
25
  } from "./evals.js";
26
26
  import { analyzeGraphHealth, type GraphHealthReport } from "./graph.js";
27
+ import {
28
+ resolveAccessSetupCapabilities,
29
+ resolveCapabilities,
30
+ resolveGraphConstructionCapabilities,
31
+ } from "./capabilities.js";
27
32
  import {
28
33
  analyzeSessionIntegrity,
29
34
  applySessionRepair,
@@ -412,7 +417,12 @@ export interface BenchmarkRecallOptions {
412
417
  export interface OperatorRepairOptions {
413
418
  config: Pick<
414
419
  PluginConfig,
415
- "memoryDir" | "entityGraphEnabled" | "timeGraphEnabled" | "causalGraphEnabled"
420
+ | "memoryDir"
421
+ | "entityGraphEnabled"
422
+ | "timeGraphEnabled"
423
+ | "causalGraphEnabled"
424
+ | "multiGraphMemoryEnabled"
425
+ | "graphWriteSessionAdjacencyEnabled"
416
426
  >;
417
427
  apply?: boolean;
418
428
  dryRun?: boolean;
@@ -904,6 +914,9 @@ async function buildOperatorConfigReviewReport(input: {
904
914
  config: PluginConfig;
905
915
  }): Promise<OperatorConfigReviewReport> {
906
916
  const { now, configStatus, config } = input;
917
+ // Resolve recall-operation gates once so this diagnostic reads capabilities,
918
+ // not scattered config flags (#1566 Cluster C — rerank is a mixed-op flag).
919
+ const caps = resolveCapabilities(config);
907
920
  const findings: OperatorConfigReviewFinding[] = [];
908
921
  const searchBackend = config.searchBackend ?? "qmd";
909
922
  const workspaceBootstrapFiles = [
@@ -920,7 +933,7 @@ async function buildOperatorConfigReviewReport(input: {
920
933
  config.memoryOsPreset !== "local-llm-heavy" &&
921
934
  config.queryAwareIndexingEnabled === false &&
922
935
  config.verbatimArtifactsEnabled === false &&
923
- config.rerankEnabled === false
936
+ !caps.rerank
924
937
  ) {
925
938
  findings.push(buildConfigReviewFinding({
926
939
  key: "balanced_preset",
@@ -1855,8 +1868,10 @@ function summarizeSecurityMitigations(
1855
1868
  | "recallAuditAnomalyRapidFireLimit"
1856
1869
  >,
1857
1870
  ): OperatorDoctorCheck {
1858
- const budgetEnabled = config.recallCrossNamespaceBudgetEnabled === true;
1859
- const anomalyEnabled = config.recallAuditAnomalyDetectionEnabled === true;
1871
+ // Resolve access-setup gates once (#1566 Cluster B).
1872
+ const accessCaps = resolveAccessSetupCapabilities(config);
1873
+ const budgetEnabled = accessCaps.recallCrossNamespaceBudget;
1874
+ const anomalyEnabled = accessCaps.recallAuditAnomalyDetection;
1860
1875
 
1861
1876
  if (!budgetEnabled && !anomalyEnabled) {
1862
1877
  return {
@@ -2338,10 +2353,12 @@ export async function runOperatorRepair(options: OperatorRepairOptions): Promise
2338
2353
  const sessionRepairApply = await applySessionRepair({
2339
2354
  plan: sessionRepairPlan,
2340
2355
  });
2356
+ // Resolve graph-construction gates once for this diagnostic (#1566 Cluster A).
2357
+ const graphCaps = resolveGraphConstructionCapabilities(options.config);
2341
2358
  const graphHealth = await analyzeGraphHealth(options.config.memoryDir, {
2342
- entityGraphEnabled: options.config.entityGraphEnabled,
2343
- timeGraphEnabled: options.config.timeGraphEnabled,
2344
- causalGraphEnabled: options.config.causalGraphEnabled,
2359
+ entityGraphEnabled: graphCaps.entityGraph,
2360
+ timeGraphEnabled: graphCaps.timeGraph,
2361
+ causalGraphEnabled: graphCaps.causalGraph,
2345
2362
  includeRepairGuidance: true,
2346
2363
  });
2347
2364
  return {
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Tier migration coordinator — extracted from the orchestrator (issue #1526).
3
+ *
4
+ * Owns the hot/cold tier-migration lifecycle for a single cycle:
5
+ * - gating (config flags, in-flight guard, min-interval throttle)
6
+ * - budget selection (compounding override or config default)
7
+ * - candidate scanning across hot + cold storage
8
+ * - promotion/demotion execution via TierMigrationExecutor
9
+ * - status recording through TierMigrationStatusStore
10
+ *
11
+ * The orchestrator constructs one instance and delegates the three public
12
+ * entrypoints (`runTierMigrationCycle`, `runTierMigrationNow`,
13
+ * `getTierMigrationStatus`) to it. Storage is passed per-call so extraction
14
+ * (which writes to a namespace-scoped storage) and maintenance/manual (which
15
+ * use the default storage) share one coordinator.
16
+ *
17
+ * Behavior-preserving move from orchestrator.ts. No logic changes — the
18
+ * orchestrator keeps thin delegating methods so existing call sites and
19
+ * tests that exercise the public API continue to work.
20
+ */
21
+
22
+ import path from "node:path";
23
+
24
+ // StorageManager type comes from the package barrel (type-only) so this
25
+ // module does not add a direct storage.ts import (#1533 ratchet).
26
+ import type { StorageManager } from "../index.js";
27
+ import type { SearchBackend } from "../search/port.js";
28
+ import { TierMigrationExecutor } from "../tier-migration.js";
29
+ import { decideTierTransition, type MemoryTier } from "../tier-routing.js";
30
+ import {
31
+ TierMigrationStatusStore,
32
+ type TierMigrationCycleSummary,
33
+ type TierMigrationStatusSnapshot,
34
+ } from "../recall-state.js";
35
+ import {
36
+ type CompoundingEngine,
37
+ defaultTierMigrationCycleBudget,
38
+ } from "../compounding/engine.js";
39
+ import {
40
+ applyUtilityPromotionRuntimePolicy,
41
+ type UtilityRuntimeValues,
42
+ } from "../utility-runtime.js";
43
+ import type { PluginConfig, MemoryFile } from "../types.js";
44
+ import { log } from "../logger.js";
45
+
46
+ /** Dependencies injected by the orchestrator. All stable references or live
47
+ * accessors (the orchestrator reassigns `qmd` to NoopSearchBackend and
48
+ * `utilityRuntimeValues` after async init, so those arrive as getters). */
49
+ export interface TierMigrationCoordinatorDeps {
50
+ config: PluginConfig;
51
+ /** Live accessor — the orchestrator reassigns this.qmd to NoopSearchBackend
52
+ * after construction when the collection is missing. */
53
+ getQmd: () => SearchBackend;
54
+ tierMigrationStatus: TierMigrationStatusStore;
55
+ /** Live accessor — loaded asynchronously during init, null before that. */
56
+ getUtilityRuntimeValues: () => UtilityRuntimeValues | null;
57
+ /** Live accessor — undefined when compounding is disabled. */
58
+ getCompounding: () => CompoundingEngine | undefined;
59
+ /** Constructs the cold-tier StorageManager for a given parent memory dir.
60
+ * Injected so this module owns no direct storage.ts import (#1533). */
61
+ createColdStorage: (parentDir: string) => StorageManager;
62
+ }
63
+
64
+ /**
65
+ * Coordinates a single tier-migration cycle. Owns the in-flight guard and
66
+ * last-run timestamp that previously lived as private orchestrator fields.
67
+ */
68
+ export class TierMigrationCoordinator {
69
+ private inFlight = false;
70
+ private lastRunAtMs = 0;
71
+
72
+ constructor(private readonly deps: TierMigrationCoordinatorDeps) {}
73
+
74
+ /** Whether a migration cycle is currently executing (diagnostics). */
75
+ isInFlight(): boolean {
76
+ return this.inFlight;
77
+ }
78
+
79
+ /** Latest status snapshot (delegates to the status store). */
80
+ getStatus(): TierMigrationStatusSnapshot {
81
+ return this.deps.tierMigrationStatus.get();
82
+ }
83
+
84
+ /**
85
+ * Run one tier-migration cycle. Behavior-preserving move of the
86
+ * orchestrator's former `runTierMigrationCycle`.
87
+ */
88
+ async runCycle(
89
+ storage: StorageManager,
90
+ trigger: "extraction" | "maintenance" | "manual",
91
+ options?: {
92
+ dryRun?: boolean;
93
+ limitOverride?: number;
94
+ force?: boolean;
95
+ },
96
+ ): Promise<TierMigrationCycleSummary> {
97
+ const { config, tierMigrationStatus } = this.deps;
98
+ const dryRun = options?.dryRun === true;
99
+ const persistSkipped = options?.force === true || trigger === "manual";
100
+ if (!config.qmdTierMigrationEnabled && options?.force !== true) {
101
+ const skipped: TierMigrationCycleSummary = {
102
+ trigger,
103
+ scanned: 0,
104
+ migrated: 0,
105
+ promoted: 0,
106
+ demoted: 0,
107
+ limit: 0,
108
+ dryRun,
109
+ skipped: "tier_migration_disabled",
110
+ };
111
+ if (persistSkipped) await tierMigrationStatus.recordCycle(skipped);
112
+ return skipped;
113
+ }
114
+ if (
115
+ trigger === "maintenance" &&
116
+ !config.qmdTierAutoBackfillEnabled &&
117
+ options?.force !== true
118
+ ) {
119
+ const skipped: TierMigrationCycleSummary = {
120
+ trigger,
121
+ scanned: 0,
122
+ migrated: 0,
123
+ promoted: 0,
124
+ demoted: 0,
125
+ limit: 0,
126
+ dryRun,
127
+ skipped: "maintenance_backfill_disabled",
128
+ };
129
+ if (persistSkipped) await tierMigrationStatus.recordCycle(skipped);
130
+ return skipped;
131
+ }
132
+ if (this.inFlight) {
133
+ const skipped: TierMigrationCycleSummary = {
134
+ trigger,
135
+ scanned: 0,
136
+ migrated: 0,
137
+ promoted: 0,
138
+ demoted: 0,
139
+ limit: 0,
140
+ dryRun,
141
+ skipped: "migration_in_flight",
142
+ };
143
+ if (persistSkipped) await tierMigrationStatus.recordCycle(skipped);
144
+ return skipped;
145
+ }
146
+
147
+ const budgetTrigger = trigger === "manual" ? "maintenance" : trigger;
148
+ const budget =
149
+ this.deps.getCompounding()?.tierMigrationCycleBudget(budgetTrigger) ??
150
+ defaultTierMigrationCycleBudget(config, budgetTrigger);
151
+ const limit =
152
+ options?.limitOverride !== undefined
153
+ ? Math.max(0, Math.floor(options.limitOverride))
154
+ : budget.limit;
155
+ const nowMs = Date.now();
156
+ if (
157
+ options?.force !== true &&
158
+ nowMs - this.lastRunAtMs < budget.minIntervalMs
159
+ ) {
160
+ const skipped: TierMigrationCycleSummary = {
161
+ trigger,
162
+ scanned: 0,
163
+ migrated: 0,
164
+ promoted: 0,
165
+ demoted: 0,
166
+ limit,
167
+ dryRun,
168
+ skipped: "min_interval",
169
+ };
170
+ if (persistSkipped) await tierMigrationStatus.recordCycle(skipped);
171
+ return skipped;
172
+ }
173
+
174
+ const policy = applyUtilityPromotionRuntimePolicy(
175
+ {
176
+ enabled: config.qmdTierMigrationEnabled,
177
+ demotionMinAgeDays: config.qmdTierDemotionMinAgeDays,
178
+ demotionValueThreshold: config.qmdTierDemotionValueThreshold,
179
+ promotionValueThreshold: config.qmdTierPromotionValueThreshold,
180
+ },
181
+ this.deps.getUtilityRuntimeValues(),
182
+ );
183
+
184
+ this.inFlight = true;
185
+ try {
186
+ const coldStorage = this.deps.createColdStorage(
187
+ path.join(storage.dir, "cold"),
188
+ );
189
+ const [hotMemories, coldMemories] = await Promise.all([
190
+ storage.readAllMemories(),
191
+ coldStorage.readAllMemories(),
192
+ ]);
193
+ const now = new Date();
194
+ const scanLimit = Math.max(0, Math.floor(budget.scanLimit));
195
+ const hotScanLimit = Math.min(
196
+ hotMemories.length,
197
+ Math.ceil(scanLimit * 0.75),
198
+ );
199
+ const coldScanLimit = Math.min(
200
+ coldMemories.length,
201
+ Math.max(0, scanLimit - hotScanLimit),
202
+ );
203
+ const toTimestamp = (memory: MemoryFile): number =>
204
+ Date.parse(memory.frontmatter.updated ?? memory.frontmatter.created);
205
+ const hotCandidates = hotMemories
206
+ .map((memory) => ({ memory, tier: "hot" as MemoryTier }))
207
+ .sort((a, b) => toTimestamp(a.memory) - toTimestamp(b.memory))
208
+ .slice(0, hotScanLimit);
209
+ const coldCandidates = coldMemories
210
+ .map((memory) => ({ memory, tier: "cold" as MemoryTier }))
211
+ .sort((a, b) => toTimestamp(b.memory) - toTimestamp(a.memory))
212
+ .slice(0, coldScanLimit);
213
+ const candidates = [...hotCandidates, ...coldCandidates];
214
+
215
+ const migration = new TierMigrationExecutor({
216
+ storage,
217
+ qmd: this.deps.getQmd(),
218
+ hotCollection: config.qmdCollection,
219
+ coldCollection:
220
+ config.qmdColdCollection ?? `${config.qmdCollection}-cold`,
221
+ autoEmbed: config.qmdAutoEmbedEnabled,
222
+ });
223
+
224
+ let migrated = 0;
225
+ let promoted = 0;
226
+ let demoted = 0;
227
+ for (const candidate of candidates) {
228
+ if (migrated >= limit) break;
229
+ const decision = decideTierTransition(
230
+ candidate.memory,
231
+ candidate.tier,
232
+ policy,
233
+ now,
234
+ );
235
+ if (!decision.changed) continue;
236
+
237
+ if (!dryRun) {
238
+ const res = await migration.migrateMemory({
239
+ memory: candidate.memory,
240
+ fromTier: candidate.tier,
241
+ toTier: decision.nextTier,
242
+ reason: `${trigger}:${decision.reason}`,
243
+ });
244
+ if (!res.changed) continue;
245
+ }
246
+ migrated += 1;
247
+ if (decision.nextTier === "cold") demoted += 1;
248
+ if (decision.nextTier === "hot") promoted += 1;
249
+ }
250
+
251
+ if (!dryRun) this.lastRunAtMs = Date.now();
252
+ log.debug(
253
+ `tier migration cycle completed: trigger=${trigger} scanned=${candidates.length} migrated=${migrated} limit=${limit}${dryRun ? " dryRun=true" : ""}`,
254
+ );
255
+ const summary: TierMigrationCycleSummary = {
256
+ trigger,
257
+ scanned: candidates.length,
258
+ migrated,
259
+ promoted,
260
+ demoted,
261
+ limit,
262
+ dryRun,
263
+ };
264
+ const shouldPersistCycle = trigger === "manual" || migrated > 0;
265
+ if (shouldPersistCycle) await tierMigrationStatus.recordCycle(summary);
266
+ return summary;
267
+ } catch (err) {
268
+ this.lastRunAtMs = Date.now();
269
+ log.warn(`tier migration cycle failed (${trigger}, fail-open): ${err}`);
270
+ const failed: TierMigrationCycleSummary = {
271
+ trigger,
272
+ scanned: 0,
273
+ migrated: 0,
274
+ promoted: 0,
275
+ demoted: 0,
276
+ limit,
277
+ dryRun,
278
+ errorCount: 1,
279
+ };
280
+ await tierMigrationStatus.recordCycle(failed);
281
+ return failed;
282
+ } finally {
283
+ this.inFlight = false;
284
+ }
285
+ }
286
+ }