@remnic/core 9.3.719 → 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
@@ -0,0 +1,78 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseConfig } from "./config.js";
5
+ import {
6
+ resolveAccessSetupCapabilities,
7
+ type AccessSetupCapabilitySet,
8
+ } from "./capabilities.js";
9
+
10
+ /**
11
+ * Parity tests for the access-setup capability set (issue #1566 Cluster B).
12
+ *
13
+ * Every access-setup capability field must project from its `<field>Enabled`
14
+ * config flag, and the default-off semantics for both flags must be preserved.
15
+ */
16
+
17
+ const ACCESS_FIELD_TO_FLAG: Record<keyof AccessSetupCapabilitySet, string> = {
18
+ recallCrossNamespaceBudget: "recallCrossNamespaceBudgetEnabled",
19
+ recallAuditAnomalyDetection: "recallAuditAnomalyDetectionEnabled",
20
+ };
21
+
22
+ const ACCESS_FIELDS = Object.keys(ACCESS_FIELD_TO_FLAG) as Array<
23
+ keyof AccessSetupCapabilitySet
24
+ >;
25
+
26
+ test("resolveAccessSetupCapabilities projects every field from its flag (true variant)", () => {
27
+ const overrides: Record<string, boolean> = {};
28
+ for (const flag of Object.values(ACCESS_FIELD_TO_FLAG)) overrides[flag] = true;
29
+ const config = parseConfig(overrides);
30
+ const accessCaps = resolveAccessSetupCapabilities(config);
31
+
32
+ for (const field of ACCESS_FIELDS) {
33
+ const flag = ACCESS_FIELD_TO_FLAG[field];
34
+ assert.equal(
35
+ accessCaps[field],
36
+ (config as unknown as Record<string, boolean>)[flag],
37
+ `accessCaps.${field} must equal config.${flag} (true variant)`,
38
+ );
39
+ }
40
+ });
41
+
42
+ test("resolveAccessSetupCapabilities projects every field from its flag (false variant)", () => {
43
+ const overrides: Record<string, boolean> = {};
44
+ for (const flag of Object.values(ACCESS_FIELD_TO_FLAG)) overrides[flag] = false;
45
+ const config = parseConfig(overrides);
46
+ const accessCaps = resolveAccessSetupCapabilities(config);
47
+
48
+ for (const field of ACCESS_FIELDS) {
49
+ assert.equal(
50
+ accessCaps[field],
51
+ false,
52
+ `accessCaps.${field} must be false when its flag is false`,
53
+ );
54
+ }
55
+ });
56
+
57
+ test("resolveAccessSetupCapabilities preserves default-off semantics when flags are undefined", () => {
58
+ // parseConfig with no overrides exercises the documented defaults. Both
59
+ // access-setup flags default to false.
60
+ const config = parseConfig({});
61
+ const accessCaps = resolveAccessSetupCapabilities(config);
62
+
63
+ assert.equal(
64
+ accessCaps.recallCrossNamespaceBudget,
65
+ false,
66
+ "recallCrossNamespaceBudget must default to false",
67
+ );
68
+ assert.equal(
69
+ accessCaps.recallAuditAnomalyDetection,
70
+ false,
71
+ "recallAuditAnomalyDetection must default to false",
72
+ );
73
+ });
74
+
75
+ test("resolveAccessSetupCapabilities returns a frozen object", () => {
76
+ const accessCaps = resolveAccessSetupCapabilities(parseConfig({}));
77
+ assert.equal(Object.isFrozen(accessCaps), true, "AccessSetupCapabilitySet must be frozen");
78
+ });
@@ -2,7 +2,12 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
4
  import { parseConfig } from "./config.js";
5
- import { resolveCapabilities, type CapabilitySet } from "./capabilities.js";
5
+ import {
6
+ resolveCapabilities,
7
+ resolveAccessSetupCapabilities,
8
+ type CapabilitySet,
9
+ type AccessSetupCapabilitySet,
10
+ } from "./capabilities.js";
6
11
 
7
12
  /**
8
13
  * Characterization tests for the recall-operation CapabilitySet (issue #1523).
@@ -22,6 +27,7 @@ const FIELD_TO_FLAG: Record<keyof CapabilitySet, string> = {
22
27
  rerankCache: "rerankCacheEnabled",
23
28
  recallDirectAnswer: "recallDirectAnswerEnabled",
24
29
  recallMemoryWorthFilter: "recallMemoryWorthFilterEnabled",
30
+ recallTrustScore: "trustScoreEnabled",
25
31
  recallMmr: "recallMmrEnabled",
26
32
  recallReasoningTraceBoost: "recallReasoningTraceBoostEnabled",
27
33
  recallPlannerLlm: "recallPlannerLlmEnabled",
@@ -30,6 +36,10 @@ const FIELD_TO_FLAG: Record<keyof CapabilitySet, string> = {
30
36
  graphRecall: "graphRecallEnabled",
31
37
  graphAssistInFullMode: "graphAssistInFullModeEnabled",
32
38
  graphExpandedIntent: "graphExpandedIntentEnabled",
39
+ // Issue #1566 Cluster C: mixed-operation flags.
40
+ rerank: "rerankEnabled",
41
+ harmonicRetrieval: "harmonicRetrievalEnabled",
42
+ parallelRetrieval: "parallelRetrievalEnabled",
33
43
  };
34
44
 
35
45
  const FIELDS = Object.keys(FIELD_TO_FLAG) as Array<keyof CapabilitySet>;
@@ -38,6 +38,8 @@ export interface CapabilitySet {
38
38
  readonly recallDirectAnswer: boolean;
39
39
  /** `recallMemoryWorthFilterEnabled` — Memory-Worth score reweighting. */
40
40
  readonly recallMemoryWorthFilter: boolean;
41
+ /** `trustScoreEnabled` — unified TrustScore recall stage (issue #1577). */
42
+ readonly recallTrustScore: boolean;
41
43
  /** `recallMmrEnabled` — maximal-marginal-relevance diversification. */
42
44
  readonly recallMmr: boolean;
43
45
  /** `recallReasoningTraceBoostEnabled` — boost reasoning-trace memories. */
@@ -54,6 +56,13 @@ export interface CapabilitySet {
54
56
  readonly graphAssistInFullMode: boolean;
55
57
  /** `graphExpandedIntentEnabled` — promote broad-intent asks to graph mode. */
56
58
  readonly graphExpandedIntent: boolean;
59
+ // --- Issue #1566 Cluster C: mixed-operation flags (recall + summarizer/CLI/writes) ---
60
+ /** `rerankEnabled` — optional LLM reranking of recall candidates. */
61
+ readonly rerank: boolean;
62
+ /** `harmonicRetrievalEnabled` — abstraction-node harmonic retrieval tier. */
63
+ readonly harmonicRetrieval: boolean;
64
+ /** `parallelRetrievalEnabled` — three-agent parallel retrieval (DirectFact + Contextual + Temporal). */
65
+ readonly parallelRetrieval: boolean;
57
66
  }
58
67
 
59
68
  /**
@@ -72,6 +81,9 @@ export function resolveCapabilities(config: PluginConfig): CapabilitySet {
72
81
  rerankCache: config.rerankCacheEnabled,
73
82
  recallDirectAnswer: config.recallDirectAnswerEnabled,
74
83
  recallMemoryWorthFilter: config.recallMemoryWorthFilterEnabled,
84
+ // Issue #1577: TrustScore subsumes the Memory Worth multiplier when on;
85
+ // the orchestrator runs exactly one of the two (mutual exclusion, rule 39).
86
+ recallTrustScore: config.trustScoreEnabled === true,
75
87
  recallMmr: config.recallMmrEnabled,
76
88
  recallReasoningTraceBoost: config.recallReasoningTraceBoostEnabled,
77
89
  recallPlannerLlm: config.recallPlannerLlmEnabled,
@@ -82,6 +94,10 @@ export function resolveCapabilities(config: PluginConfig): CapabilitySet {
82
94
  // migrated call sites used (`!== false` = default-on, `=== true` = default-off).
83
95
  graphAssistInFullMode: config.graphAssistInFullModeEnabled !== false,
84
96
  graphExpandedIntent: config.graphExpandedIntentEnabled === true,
97
+ // Issue #1566 Cluster C: mixed-operation flags resolved once per recall op.
98
+ rerank: config.rerankEnabled,
99
+ harmonicRetrieval: config.harmonicRetrievalEnabled,
100
+ parallelRetrieval: config.parallelRetrievalEnabled,
85
101
  });
86
102
  }
87
103
 
@@ -145,3 +161,45 @@ export function resolveGraphConstructionCapabilities(
145
161
  graphWriteSessionAdjacency: config.graphWriteSessionAdjacencyEnabled !== false,
146
162
  });
147
163
  }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Access-setup capability set (issue #1566 Cluster B).
167
+ //
168
+ // These two flags gate cross-namespace recall budgeting and anomaly-detection
169
+ // auditing. Resolved once at access-service handler entry (and the operator
170
+ // doctor check) rather than re-derived from raw config at each site.
171
+ // ---------------------------------------------------------------------------
172
+
173
+ /**
174
+ * Frozen projection of access-setup feature gates (issue #1566 Cluster B).
175
+ *
176
+ * Every field is `readonly boolean`. Composition (including the default-off
177
+ * semantics for the optional `recallAuditAnomalyDetectionEnabled`) lives ONLY
178
+ * in {@link resolveAccessSetupCapabilities}.
179
+ */
180
+ export interface AccessSetupCapabilitySet {
181
+ /** `recallCrossNamespaceBudgetEnabled` — rolling cross-namespace recall budget. */
182
+ readonly recallCrossNamespaceBudget: boolean;
183
+ /** `recallAuditAnomalyDetectionEnabled` — access-audit anomaly detection (default-off). */
184
+ readonly recallAuditAnomalyDetection: boolean;
185
+ }
186
+
187
+ /**
188
+ * Resolve the {@link AccessSetupCapabilitySet} from parsed config.
189
+ *
190
+ * Call this ONCE at access-service handler entry and thread the result down.
191
+ */
192
+ export type AccessSetupConfigProjection = Pick<
193
+ PluginConfig,
194
+ "recallCrossNamespaceBudgetEnabled" | "recallAuditAnomalyDetectionEnabled"
195
+ >;
196
+
197
+ export function resolveAccessSetupCapabilities(
198
+ config: AccessSetupConfigProjection,
199
+ ): AccessSetupCapabilitySet {
200
+ return Object.freeze({
201
+ recallCrossNamespaceBudget: config.recallCrossNamespaceBudgetEnabled,
202
+ // Optional/default-off flag: preserve `=== true` semantics.
203
+ recallAuditAnomalyDetection: config.recallAuditAnomalyDetectionEnabled === true,
204
+ });
205
+ }
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import type { Orchestrator } from "../orchestrator.js";
21
+ import { resolveCapabilities } from "../capabilities.js";
21
22
  import type { CliCommand } from "../cli.js";
22
23
  import type { UtilityTelemetryEvent } from "../utility-telemetry.js";
23
24
  import {
@@ -42,6 +43,9 @@ export function registerResearchStatusCommands(
42
43
  cmd: CliCommand,
43
44
  orchestrator: Orchestrator,
44
45
  ): void {
46
+ // Resolve recall-operation gates once (#1566 Cluster C — harmonicRetrieval is a
47
+ // mixed-op flag read here on the CLI-search path as well as in recall).
48
+ const caps = resolveCapabilities(orchestrator.config);
45
49
  cmd
46
50
  .command("objective-state-status")
47
51
  .description("Show objective-state store status, snapshot counts, and latest stored snapshot")
@@ -111,7 +115,7 @@ cmd
111
115
  const status = await runAbstractionNodeStatusCliCommand({
112
116
  memoryDir: orchestrator.config.memoryDir,
113
117
  abstractionNodeStoreDir: orchestrator.config.abstractionNodeStoreDir,
114
- harmonicRetrievalEnabled: orchestrator.config.harmonicRetrievalEnabled,
118
+ harmonicRetrievalEnabled: caps.harmonicRetrieval,
115
119
  abstractionAnchorsEnabled: orchestrator.config.abstractionAnchorsEnabled,
116
120
  });
117
121
  console.log(JSON.stringify(status, null, 2));
@@ -125,7 +129,7 @@ cmd
125
129
  const status = await runCueAnchorStatusCliCommand({
126
130
  memoryDir: orchestrator.config.memoryDir,
127
131
  abstractionNodeStoreDir: orchestrator.config.abstractionNodeStoreDir,
128
- harmonicRetrievalEnabled: orchestrator.config.harmonicRetrievalEnabled,
132
+ harmonicRetrievalEnabled: caps.harmonicRetrieval,
129
133
  abstractionAnchorsEnabled: orchestrator.config.abstractionAnchorsEnabled,
130
134
  });
131
135
  console.log(JSON.stringify(status, null, 2));
@@ -147,7 +151,7 @@ cmd
147
151
  const results = await runHarmonicSearchCliCommand({
148
152
  memoryDir: orchestrator.config.memoryDir,
149
153
  abstractionNodeStoreDir: orchestrator.config.abstractionNodeStoreDir,
150
- harmonicRetrievalEnabled: orchestrator.config.harmonicRetrievalEnabled,
154
+ harmonicRetrievalEnabled: caps.harmonicRetrieval,
151
155
  abstractionAnchorsEnabled: orchestrator.config.abstractionAnchorsEnabled,
152
156
  query,
153
157
  maxResults: Number.isFinite(maxResults) ? maxResults : 3,
package/src/cli.ts CHANGED
@@ -125,6 +125,7 @@ import {
125
125
  validateEvalBenchmarkPack,
126
126
  } from "./evals.js";
127
127
  import { analyzeGraphHealth, type GraphHealthReport } from "./graph.js";
128
+ import { resolveGraphConstructionCapabilities } from "./capabilities.js";
128
129
  import {
129
130
  getCausalTrajectoryStoreStatus,
130
131
  type CausalTrajectoryStoreStatus,
@@ -6360,11 +6361,12 @@ export function registerCli(
6360
6361
  .option("--repair-guidance", "Include non-destructive repair guidance")
6361
6362
  .action(async (...args: unknown[]) => {
6362
6363
  const options = (args[0] ?? {}) as Record<string, unknown>;
6364
+ const graphCaps = resolveGraphConstructionCapabilities(orchestrator.config);
6363
6365
  const report = await runGraphHealthCliCommand({
6364
6366
  memoryDir: orchestrator.config.memoryDir,
6365
- entityGraphEnabled: orchestrator.config.entityGraphEnabled,
6366
- timeGraphEnabled: orchestrator.config.timeGraphEnabled,
6367
- causalGraphEnabled: orchestrator.config.causalGraphEnabled,
6367
+ entityGraphEnabled: graphCaps.entityGraph,
6368
+ timeGraphEnabled: graphCaps.timeGraph,
6369
+ causalGraphEnabled: graphCaps.causalGraph,
6368
6370
  includeRepairGuidance: options.repairGuidance === true,
6369
6371
  });
6370
6372
  console.log(JSON.stringify(report, null, 2));
@@ -6415,12 +6417,15 @@ export function registerCli(
6415
6417
  .option("--json", "Emit machine-readable JSON only")
6416
6418
  .action(async (...args: unknown[]) => {
6417
6419
  const options = (args[0] ?? {}) as Record<string, unknown>;
6420
+ const repairGraphCaps = resolveGraphConstructionCapabilities(orchestrator.config);
6418
6421
  const report = await runOperatorRepair({
6419
6422
  config: {
6420
6423
  memoryDir: orchestrator.config.memoryDir,
6421
- entityGraphEnabled: orchestrator.config.entityGraphEnabled,
6422
- timeGraphEnabled: orchestrator.config.timeGraphEnabled,
6423
- causalGraphEnabled: orchestrator.config.causalGraphEnabled,
6424
+ entityGraphEnabled: repairGraphCaps.entityGraph,
6425
+ timeGraphEnabled: repairGraphCaps.timeGraph,
6426
+ causalGraphEnabled: repairGraphCaps.causalGraph,
6427
+ multiGraphMemoryEnabled: repairGraphCaps.multiGraphMemory,
6428
+ graphWriteSessionAdjacencyEnabled: repairGraphCaps.graphWriteSessionAdjacency,
6424
6429
  },
6425
6430
  apply: options.apply === true,
6426
6431
  dryRun: options.dryRun === true,
package/src/config.ts CHANGED
@@ -28,6 +28,7 @@ import type {
28
28
  SlotBehaviorConfig,
29
29
  SlotMismatchMode,
30
30
  TriggerMode,
31
+ TrustWeights,
31
32
  } from "./types.js";
32
33
  import { log } from "./logger.js";
33
34
  import { cloneDefaultSessionObserverBands } from "./session-observer-bands.js";
@@ -2336,6 +2337,27 @@ export function parseConfig(
2336
2337
  const n = coerceNumber(cfg.recallMemoryWorthHalfLifeMs);
2337
2338
  return n !== undefined && n >= 0 ? n : 0;
2338
2339
  })(),
2340
+ // Unified TrustScore recall stage (issue #1577). Default off: the stage
2341
+ // is absent and ranking is byte-identical to the pre-feature pipeline
2342
+ // (rule 39). Flip the master gate via #1574 ablation evidence only.
2343
+ trustScoreEnabled: coerceBool(cfg.trustScoreEnabled) ?? false,
2344
+ // Epistemic rendering ships behind its own gate so scoring can land first.
2345
+ trustScoreEpistemicRendering: coerceBool(cfg.trustScoreEpistemicRendering) ?? false,
2346
+ // Quarantine defaults on (only effective under the master gate); keeps
2347
+ // hard negatives out of injection but visible in X-ray (rule 34).
2348
+ trustScoreQuarantine: coerceBool(cfg.trustScoreQuarantine) ?? true,
2349
+ trustScoreMinMultiplier: (() => {
2350
+ const n = coerceNumber(cfg.trustScoreMinMultiplier);
2351
+ return n !== undefined && n >= 0 && n <= 1 ? n : 0.5;
2352
+ })(),
2353
+ trustScoreMaxMultiplier: (() => {
2354
+ const n = coerceNumber(cfg.trustScoreMaxMultiplier);
2355
+ return n !== undefined && n >= 1 && n <= 4 ? n : 1.25;
2356
+ })(),
2357
+ // Per-component weights. Accept a nested object from config; invalid
2358
+ // entries (non-finite, outside [0,1]) are dropped by resolveTrustWeights,
2359
+ // which falls back to the documented defaults (rule 51).
2360
+ trustScoreWeights: parseTrustScoreWeights(cfg.trustScoreWeights),
2339
2361
  // Memory Linking (Phase 3A)
2340
2362
  memoryLinkingEnabled: cfg.memoryLinkingEnabled === true, // Off by default initially
2341
2363
  // Conversation Threading (Phase 3B)
@@ -4639,3 +4661,32 @@ function buildRecallPipelineConfig(cfg: Record<string, unknown>): RecallPipeline
4639
4661
 
4640
4662
  return { recallBudgetChars, pipeline };
4641
4663
  }
4664
+
4665
+ /**
4666
+ * Parse the `trustScoreWeights` config value into a {@link TrustWeights}
4667
+ * object. Accepts a plain object of per-component numbers; non-finite or
4668
+ * out-of-`[0,1]` entries are dropped (the scorer falls back to defaults via
4669
+ * `resolveTrustWeights`). A non-object value yields `{}` → all defaults.
4670
+ * Rule 51: invalid weight rejected, never silently clamped to an extreme.
4671
+ */
4672
+ function parseTrustScoreWeights(value: unknown): TrustWeights {
4673
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
4674
+ const src = value as Record<string, unknown>;
4675
+ const out: TrustWeights = {};
4676
+ const keys: Array<keyof TrustWeights> = [
4677
+ "memoryWorth",
4678
+ "provenance",
4679
+ "faithfulness",
4680
+ "corroboration",
4681
+ "contradiction",
4682
+ "domainCalibration",
4683
+ "feedback",
4684
+ "recency",
4685
+ ];
4686
+ for (const key of keys) {
4687
+ const raw = src[key as string];
4688
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0 || raw > 1) continue;
4689
+ (out as Record<string, number>)[key as string] = raw;
4690
+ }
4691
+ return out;
4692
+ }
@@ -132,9 +132,9 @@ export function parseGraphSnapshotSince(raw: string | undefined): number | undef
132
132
  export async function buildGraphSnapshot(opts: {
133
133
  memoryDir: string;
134
134
  graphConfig: {
135
- entityGraphEnabled: boolean;
136
- timeGraphEnabled: boolean;
137
- causalGraphEnabled: boolean;
135
+ entityGraph: boolean;
136
+ timeGraph: boolean;
137
+ causalGraph: boolean;
138
138
  };
139
139
  request: GraphSnapshotRequest;
140
140
  loadNode: GraphSnapshotNodeLoader;
package/src/graph.ts CHANGED
@@ -15,6 +15,7 @@ import * as path from "node:path";
15
15
 
16
16
  import { readEdgeConfidence } from "./graph-edge-reinforcement.js";
17
17
  import { emitGraphEvent } from "./graph-events.js";
18
+ import type { GraphConstructionCapabilitySet } from "./capabilities.js";
18
19
 
19
20
  export type GraphType = "entity" | "time" | "causal";
20
21
 
@@ -198,12 +199,12 @@ export async function readEdgesStrict(memoryDir: string, type: GraphType): Promi
198
199
  */
199
200
  export async function readAllEdges(
200
201
  memoryDir: string,
201
- config: Pick<GraphConfig, "entityGraphEnabled" | "timeGraphEnabled" | "causalGraphEnabled">
202
+ graphCaps: Pick<GraphConstructionCapabilitySet, "entityGraph" | "timeGraph" | "causalGraph">,
202
203
  ): Promise<GraphEdge[]> {
203
204
  const parts: GraphEdge[][] = await Promise.all([
204
- config.entityGraphEnabled ? readEdges(memoryDir, "entity") : Promise.resolve([]),
205
- config.timeGraphEnabled ? readEdges(memoryDir, "time") : Promise.resolve([]),
206
- config.causalGraphEnabled ? readEdges(memoryDir, "causal") : Promise.resolve([]),
205
+ graphCaps.entityGraph ? readEdges(memoryDir, "entity") : Promise.resolve([]),
206
+ graphCaps.timeGraph ? readEdges(memoryDir, "time") : Promise.resolve([]),
207
+ graphCaps.causalGraph ? readEdges(memoryDir, "causal") : Promise.resolve([]),
207
208
  ]);
208
209
  return parts.flat();
209
210
  }
@@ -396,9 +397,9 @@ export class GraphIndex {
396
397
  return this.edgeCache.allEdges;
397
398
  }
398
399
  const allEdges = await readAllEdges(this.memoryDir, {
399
- entityGraphEnabled: this.cfg.entityGraphEnabled,
400
- timeGraphEnabled: this.cfg.timeGraphEnabled,
401
- causalGraphEnabled: this.cfg.causalGraphEnabled,
400
+ entityGraph: this.cfg.entityGraphEnabled,
401
+ timeGraph: this.cfg.timeGraphEnabled,
402
+ causalGraph: this.cfg.causalGraphEnabled,
402
403
  });
403
404
  this.edgeCache = { allEdges, loadedAt: Date.now() };
404
405
  return allEdges;
@@ -1,7 +1,9 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdtemp, readFile, writeFile, rm, mkdir, appendFile } from "node:fs/promises";
3
+ import { mkdtemp, readFile, writeFile, rm, mkdir, appendFile, utimes } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
+ import { spawn } from "node:child_process";
6
+ import { statSync } from "node:fs";
5
7
  import path from "node:path";
6
8
  import { createHash } from "node:crypto";
7
9
  import {
@@ -345,6 +347,245 @@ describe("TombstoneStore — enabled gate (rule 30)", () => {
345
347
  });
346
348
  });
347
349
 
350
+ // ─────────────────────────────────────────────────────────────────────────────
351
+ // Cross-process tombstone write lock (issue #1639)
352
+ // ─────────────────────────────────────────────────────────────────────────────
353
+
354
+ /**
355
+ * A read-merge-write file io that simulates the secure-store append path
356
+ * (read encrypted → decrypt → concat → re-encrypt → atomic rename). Unlike a
357
+ * raw O_APPEND, this is NOT atomic across processes: two writers that each
358
+ * read the same contents and write back drop the other line — the exact
359
+ * lost-write race the #1639 cross-process lock closes. The setImmediate yield
360
+ * widens the race window so a missing lock would demonstrably lose entries.
361
+ */
362
+ function makeReadMergeWriteIo() {
363
+ return {
364
+ read: (p: string) => readFile(p, "utf8"),
365
+ append: async (p: string, c: string) => {
366
+ await mkdir(path.dirname(p), { recursive: true });
367
+ let existing = "";
368
+ try {
369
+ existing = await readFile(p, "utf8");
370
+ } catch (err) {
371
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
372
+ }
373
+ // Yield so two concurrent read-merge-write appends observably overlap
374
+ // when the cross-process lock is absent.
375
+ await new Promise<void>((resolve) => setImmediate(resolve));
376
+ await writeFile(p, existing + c, "utf8");
377
+ },
378
+ write: async (p: string, c: string) => {
379
+ await mkdir(path.dirname(p), { recursive: true });
380
+ await writeFile(p, c, "utf8");
381
+ },
382
+ stat: (p: string) => statSync(p),
383
+ };
384
+ }
385
+
386
+ describe("TombstoneStore — cross-process write lock (issue #1639)", () => {
387
+ it("two processes appending concurrently produce no lost entries", async () => {
388
+ const dir = await mkdtemp(path.join(tmpdir(), "tomb-xproc-"));
389
+ const moduleUrl = new URL("./tombstones.ts", import.meta.url).href;
390
+ const entriesPerWorker = 15;
391
+
392
+ // Worker source: plain JS, no nested template literals. Each child imports
393
+ // the store, points it at the shared tombstones.jsonl with a read-merge-
394
+ // write io, and appends N entries. The cross-process lock serializes the
395
+ // appends across the two processes so no entry is dropped.
396
+ const workerSource = [
397
+ "(async () => {",
398
+ "const { TombstoneStore } = await import(process.argv[1]);",
399
+ "const { mkdir, readFile, writeFile } = await import(\"node:fs/promises\");",
400
+ "const { statSync } = await import(\"node:fs\");",
401
+ "const path = await import(\"node:path\");",
402
+ "const { createHash } = await import(\"node:crypto\");",
403
+ "const dir = process.argv[2];",
404
+ "const workerId = Number(process.argv[3]);",
405
+ "const count = Number(process.argv[4]);",
406
+ "const filePath = path.join(dir, \"tombstones.jsonl\");",
407
+ "function hash(c){return createHash(\"sha256\").update(c).digest(\"hex\");}",
408
+ "function normalize(c){return c;}",
409
+ "const io = {",
410
+ " read: (p) => readFile(p, \"utf8\"),",
411
+ " append: async (p, c) => {",
412
+ " await mkdir(path.dirname(p), { recursive: true });",
413
+ " let existing = \"\";",
414
+ " try { existing = await readFile(p, \"utf8\"); } catch (e) { if (e.code !== \"ENOENT\") throw e; }",
415
+ " await new Promise((r) => setImmediate(r));",
416
+ " await writeFile(p, existing + c, \"utf8\");",
417
+ " },",
418
+ " write: async (p, c) => { await mkdir(path.dirname(p), { recursive: true }); await writeFile(p, c, \"utf8\"); },",
419
+ " stat: (p) => statSync(p),",
420
+ "};",
421
+ "const store = new TombstoneStore(filePath, \"default\", { enabled: true, semanticMatch: false, semanticThreshold: 0.9, hashContent: hash, normalizeText: normalize }, io);",
422
+ "await store.load();",
423
+ "for (let i = 0; i < count; i += 1) {",
424
+ " await store.appendTombstone({ reason: \"correction\", createdBy: \"user_correction\", sourceMemoryId: \"w\" + workerId + \"-f\" + i, rawContent: \"worker \" + workerId + \" fact \" + i });",
425
+ "}",
426
+ "})();",
427
+ ].join("\n");
428
+
429
+ function runWorker(workerId: number): Promise<void> {
430
+ return new Promise((resolve, reject) => {
431
+ const child = spawn(
432
+ process.execPath,
433
+ ["--import", "tsx", "-e", workerSource, moduleUrl, dir, String(workerId), String(entriesPerWorker)],
434
+ { stdio: ["ignore", "pipe", "pipe"] },
435
+ );
436
+ let stderr = "";
437
+ child.stderr.setEncoding("utf8");
438
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
439
+ child.on("error", reject);
440
+ child.on("close", (code) => {
441
+ if (code === 0) resolve();
442
+ else reject(new Error(`worker ${workerId} exited ${code}: ${stderr}`));
443
+ });
444
+ });
445
+ }
446
+
447
+ // Two genuinely concurrent processes contending on the same lockfile.
448
+ await Promise.all([runWorker(0), runWorker(1)]);
449
+
450
+ const raw = await readFile(path.join(dir, "tombstones.jsonl"), "utf8");
451
+ const fileLines = raw.split("\n").filter((l) => l.trim().length > 0);
452
+ // No lost entries: every line parses and the total is exactly 2 × N.
453
+ assert.equal(fileLines.length, entriesPerWorker * 2);
454
+ for (const line of fileLines) {
455
+ assert.doesNotThrow(() => JSON.parse(line));
456
+ }
457
+ // Every worker entry is present (none dropped by a lost write).
458
+ const sourceIds = new Set(fileLines.map((l) => JSON.parse(l).sourceMemoryId));
459
+ for (let w = 0; w < 2; w += 1) {
460
+ for (let i = 0; i < entriesPerWorker; i += 1) {
461
+ assert.ok(sourceIds.has(`w${w}-f${i}`), `missing w${w}-f${i} — lost write`);
462
+ }
463
+ }
464
+ // The advisory lockfile is released after the run (no lingering lock).
465
+ await rm(path.join(dir, "tombstones.lock"), { force: true });
466
+ });
467
+
468
+ it("recovers from a stale lock left by a crashed holder", async () => {
469
+ const dir = await mkdtemp(path.join(tmpdir(), "tomb-stale-lock-"));
470
+ const filePath = path.join(dir, "tombstones.jsonl");
471
+ const lockPath = path.join(dir, "tombstones.lock");
472
+ // Seed a lockfile written by a (now-crashed) holder: <pid> <uuid> <iso>.
473
+ await mkdir(dir, { recursive: true });
474
+ await writeFile(
475
+ lockPath,
476
+ `${process.pid} 00000000-0000-0000-0000-000000000000 2000-01-01T00:00:00.000Z\n`,
477
+ "utf8",
478
+ );
479
+ // Make it older than staleMs so the next acquire breaks it.
480
+ const old = new Date(Date.now() - 120_000);
481
+ await utimes(lockPath, old, old);
482
+
483
+ const store = new TombstoneStore(
484
+ filePath,
485
+ "default",
486
+ { enabled: true, semanticMatch: false, semanticThreshold: 0.9, hashContent: computeHash, normalizeText: normalizeContent, lockStaleMs: 1_000 },
487
+ makeReadMergeWriteIo(),
488
+ );
489
+ // The append must break the stale lock, acquire a fresh one, and succeed.
490
+ const id = await store.appendTombstone({
491
+ reason: "correction",
492
+ createdBy: "user_correction",
493
+ sourceMemoryId: "fact-stale",
494
+ rawContent: "Recovered from a stale lock.",
495
+ });
496
+ assert.match(id, /^tomb-/);
497
+ // The tombstone landed durably.
498
+ const raw = await readFile(filePath, "utf8");
499
+ const lines2 = raw.split("\n").filter((l) => l.trim().length > 0);
500
+ assert.equal(lines2.length, 1);
501
+ // Our fresh lock was released on completion (the stale one was broken).
502
+ await rm(lockPath, { force: true });
503
+ });
504
+
505
+ it("rebuild preserves a peer's revocation appended before rebuild acquired the lock (cursor/codex #1639)", async () => {
506
+ // Regression for the rebuild race: rebuild must re-read the log under the
507
+ // lock (ensureFreshAgainstDisk) before computing its payload, otherwise a
508
+ // revocation a peer wrote while rebuild waited for the lock is dropped and
509
+ // the retired fact silently un-revokes (resurrection).
510
+ const dir = await mkdtemp(path.join(tmpdir(), "tomb-rebuild-race-"));
511
+ const filePath = path.join(dir, "tombstones.jsonl");
512
+ const opts = { enabled: true, semanticMatch: false, semanticThreshold: 0.9, hashContent: computeHash, normalizeText: normalizeContent };
513
+ const io = makeReadMergeWriteIo();
514
+ // Store A writes tombstone-1, then goes stale (never sees the peer write).
515
+ const storeA = new TombstoneStore(filePath, "default", opts, io);
516
+ const t1 = await storeA.appendTombstone({
517
+ reason: "correction",
518
+ createdBy: "user_correction",
519
+ sourceMemoryId: "fact-rebuild-1",
520
+ rawContent: "The DB is MySQL.",
521
+ });
522
+ // Peer (store B, same file) revokes t1 durably. Store A's in-memory index
523
+ // is now stale (it still thinks t1 is active).
524
+ const storeB = new TombstoneStore(filePath, "default", opts, io);
525
+ await storeB.revoke(t1, "user_correction");
526
+ // Store A rebuilds from a retired-memory corpus that reconstructs t1.
527
+ // Without the under-lock re-read, A computes existingRevocations from its
528
+ // stale index (empty), overwrites the file with [rebuilt-t1], and the
529
+ // revocation is LOST — t1 is active again (resurrection). With the fix,
530
+ // ensureFreshAgainstDisk reloads the revocation under the lock and rebuild
531
+ // preserves it.
532
+ await storeA.rebuild([
533
+ {
534
+ memoryId: "fact-rebuild-1",
535
+ rawContent: "The DB is MySQL.",
536
+ reason: "correction" as const,
537
+ createdBy: "user_correction" as const,
538
+ createdAt: "2026-01-01T00:00:00.000Z",
539
+ },
540
+ ]);
541
+ // The rebuilt file must still contain the revocation (the retired fact
542
+ // stays revoked, not silently un-revoked by the rebuild).
543
+ const raw = await readFile(filePath, "utf8");
544
+ const rebuiltLines = raw.split("\n").filter((l) => l.trim().length > 0);
545
+ const hasRevocation = rebuiltLines.some((l) => {
546
+ try { return JSON.parse(l).kind === "revocation"; } catch { return false; }
547
+ });
548
+ assert.ok(hasRevocation, "rebuild dropped the peer's revocation — resurrection risk");
549
+ // And the rebuilt store A must report the fact as revoked (lookup returns null).
550
+ await storeA.load();
551
+ assert.equal(
552
+ storeA.lookup({ namespace: "default", contentHash: computeHash("The DB is MySQL.") }),
553
+ null,
554
+ "revoked fact resurrected after rebuild",
555
+ );
556
+ await rm(path.join(dir, "tombstones.lock"), { force: true });
557
+ });
558
+
559
+ it("in-process concurrent appends still serialize under the lock", async () => {
560
+ const dir = await mkdtemp(path.join(tmpdir(), "tomb-inproc-lock-"));
561
+ const filePath = path.join(dir, "tombstones.jsonl");
562
+ const store = new TombstoneStore(
563
+ filePath,
564
+ "default",
565
+ { enabled: true, semanticMatch: false, semanticThreshold: 0.9, hashContent: computeHash, normalizeText: normalizeContent },
566
+ makeReadMergeWriteIo(),
567
+ );
568
+ const contents = Array.from({ length: 25 }, (_, i) => `locked content ${i}`);
569
+ await Promise.all(
570
+ contents.map((c) =>
571
+ store.appendTombstone({
572
+ reason: "correction",
573
+ createdBy: "user_correction",
574
+ sourceMemoryId: `fact-${c}`,
575
+ rawContent: c,
576
+ }),
577
+ ),
578
+ );
579
+ await store.load();
580
+ const raw = await readFile(filePath, "utf8");
581
+ const lines3 = raw.split("\n").filter((l) => l.trim().length > 0);
582
+ // No lost writes despite the read-merge-write io + concurrent appends.
583
+ assert.equal(lines3.length, contents.length);
584
+ assert.equal(store.stats().count, contents.length);
585
+ await rm(path.join(dir, "tombstones.lock"), { force: true });
586
+ });
587
+ });
588
+
348
589
  describe("parseTombstoneLine", () => {
349
590
  it("rejects malformed input", () => {
350
591
  assert.equal(parseTombstoneLine(""), null);