@remnic/core 9.69.22 → 9.69.24

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 (772) hide show
  1. package/dist/abstraction-nodes.js +3 -2
  2. package/dist/access-admin-ops-surface.d.ts +14 -12
  3. package/dist/access-admin-ops-surface.js +94 -91
  4. package/dist/access-authorization-probe.d.ts +14 -12
  5. package/dist/access-authorization-probe.js +95 -92
  6. package/dist/access-boundary.d.ts +14 -12
  7. package/dist/access-boundary.js +94 -91
  8. package/dist/access-citation-namespace.js +3 -3
  9. package/dist/access-cli.js +200 -195
  10. package/dist/access-cli.js.map +1 -1
  11. package/dist/access-coding-context-resolution.d.ts +2 -1
  12. package/dist/access-extraction-force-flush.d.ts +14 -12
  13. package/dist/access-extraction-force-flush.js +94 -91
  14. package/dist/access-health-types.d.ts +2 -1
  15. package/dist/access-http-briefing.d.ts +156 -0
  16. package/dist/access-http-briefing.js +233 -0
  17. package/dist/access-http-lcm-compaction.d.ts +14 -12
  18. package/dist/access-http-lifecycle-flush.d.ts +14 -12
  19. package/dist/access-http-offline-stream.d.ts +14 -12
  20. package/dist/access-http-query.d.ts +3 -2
  21. package/dist/access-http-query.js +96 -93
  22. package/dist/access-http.d.ts +15 -13
  23. package/dist/access-http.js +119 -114
  24. package/dist/access-identity-continuity-surface.d.ts +12 -10
  25. package/dist/access-identity-continuity-surface.js +94 -91
  26. package/dist/access-lcm-surface.d.ts +14 -12
  27. package/dist/access-lcm-surface.js +94 -91
  28. package/dist/access-mcp.d.ts +14 -12
  29. package/dist/access-mcp.js +105 -101
  30. package/dist/access-memory-search-fanout.d.ts +3 -2
  31. package/dist/{access-namespace-preflight-BMHhxoL9.d.ts → access-namespace-preflight-BsvmyENS.d.ts} +1 -1
  32. package/dist/access-namespace-preflight.d.ts +4 -3
  33. package/dist/access-namespace-preflight.js +94 -91
  34. package/dist/access-observe-transcript.d.ts +12 -10
  35. package/dist/access-observe-write-surface.d.ts +14 -12
  36. package/dist/access-observe-write-surface.js +94 -91
  37. package/dist/access-offline-manifest.d.ts +12 -10
  38. package/dist/access-offline-manifest.js +15 -15
  39. package/dist/access-operations-batch.js +99 -95
  40. package/dist/access-operations.d.ts +14 -12
  41. package/dist/access-operations.js +104 -100
  42. package/dist/access-recall-concurrency.d.ts +14 -12
  43. package/dist/access-recall-concurrency.js +94 -91
  44. package/dist/access-recall-response.d.ts +14 -12
  45. package/dist/access-recall-response.js +94 -91
  46. package/dist/access-recall-surface.d.ts +14 -12
  47. package/dist/access-recall-surface.js +94 -91
  48. package/dist/access-schema.js +8 -7
  49. package/dist/{access-service-BlsNXFwx.d.ts → access-service-DWdY9Mlk.d.ts} +7 -7
  50. package/dist/access-service-helpers.d.ts +14 -12
  51. package/dist/access-service.d.ts +14 -12
  52. package/dist/access-service.js +94 -91
  53. package/dist/access-surface-catalog.d.ts +14 -12
  54. package/dist/access-surface-catalog.js +97 -93
  55. package/dist/access-surface-catalog.js.map +1 -1
  56. package/dist/access-wearables-meetings-surface.d.ts +4 -3
  57. package/dist/action-confidence.d.ts +2 -1
  58. package/dist/active-memory-bridge.d.ts +2 -1
  59. package/dist/active-recall.d.ts +2 -1
  60. package/dist/active-recall.js +16 -13
  61. package/dist/active-recall.js.map +1 -1
  62. package/dist/adapters/index.js +7 -7
  63. package/dist/adapters/registry.js +3 -3
  64. package/dist/ambient-provenance.d.ts +2 -1
  65. package/dist/artifact-search.d.ts +2 -1
  66. package/dist/{auto-sync-GRUJFD2F.js → auto-sync-WNGG2GC2.js} +9 -9
  67. package/dist/behavior-learner.d.ts +2 -1
  68. package/dist/behavior-signals.d.ts +2 -1
  69. package/dist/bootstrap.d.ts +12 -10
  70. package/dist/briefing-window.d.ts +3 -2
  71. package/dist/briefing.d.ts +3 -2
  72. package/dist/briefing.js +36 -36
  73. package/dist/buffer-surprise-report.d.ts +2 -1
  74. package/dist/buffer-turn-helpers.d.ts +2 -1
  75. package/dist/buffer.d.ts +3 -2
  76. package/dist/buffer.js +13 -13
  77. package/dist/bulk-import/index.d.ts +4 -3
  78. package/dist/calibration.d.ts +2 -1
  79. package/dist/calibration.js +2 -1
  80. package/dist/calibration.js.map +1 -1
  81. package/dist/capabilities.d.ts +2 -1
  82. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.js} +2 -2
  83. package/dist/{capsule-merge-SEHERWWP.js → capsule-merge-IPG4IOFP.js} +5 -5
  84. package/dist/{catalog-C1qHsGew.d.ts → catalog-CDZLyiXL.d.ts} +1 -1
  85. package/dist/causal-behavior.d.ts +2 -1
  86. package/dist/causal-behavior.js +4 -3
  87. package/dist/causal-behavior.js.map +1 -1
  88. package/dist/causal-chain.js +4 -3
  89. package/dist/causal-consolidation.d.ts +2 -1
  90. package/dist/causal-consolidation.js +42 -42
  91. package/dist/causal-retrieval.js +4 -3
  92. package/dist/causal-retrieval.js.map +1 -1
  93. package/dist/causal-trajectory-graph.d.ts +5 -2
  94. package/dist/causal-trajectory-graph.js +6 -2
  95. package/dist/causal-trajectory-graph.js.map +1 -1
  96. package/dist/causal-trajectory.js +3 -2
  97. package/dist/{chunk-KTVSPFCQ.js → chunk-22FVR2KE.js} +2 -2
  98. package/dist/{chunk-7Y5V3LAP.js → chunk-27QVNSQ3.js} +2 -2
  99. package/dist/{chunk-R7LY4YKF.js → chunk-2EKJDQZB.js} +2 -2
  100. package/dist/{chunk-FCATYKQA.js → chunk-2IOHTUP3.js} +263 -13
  101. package/dist/chunk-2IOHTUP3.js.map +1 -0
  102. package/dist/{chunk-HOUST5UX.js → chunk-2ITE4C2Z.js} +2 -2
  103. package/dist/{chunk-JOYC4RYC.js → chunk-2JVJ7UHC.js} +2 -2
  104. package/dist/{chunk-X2NFFBTO.js → chunk-2L5FSNPC.js} +7 -7
  105. package/dist/{chunk-DUAJKRR2.js → chunk-2SGLMP2E.js} +2 -2
  106. package/dist/{chunk-6FUMA6AA.js → chunk-2Z4GS5E6.js} +2 -2
  107. package/dist/{chunk-G6MXN2G5.js → chunk-37KUQPX4.js} +6 -6
  108. package/dist/{chunk-DDQQY2E3.js → chunk-3BTFBQLO.js} +7 -7
  109. package/dist/chunk-3BTFBQLO.js.map +1 -0
  110. package/dist/{chunk-UR6PBX2J.js → chunk-3K7V3HE6.js} +35 -60
  111. package/dist/chunk-3K7V3HE6.js.map +1 -0
  112. package/dist/{chunk-WKAXBXVA.js → chunk-3QQTXYZ6.js} +2 -2
  113. package/dist/{chunk-DJUKBGAD.js → chunk-46EST7JO.js} +5 -5
  114. package/dist/chunk-46EST7JO.js.map +1 -0
  115. package/dist/{chunk-4ZWCQJNM.js → chunk-46WM6JX2.js} +5 -5
  116. package/dist/{chunk-CASWJ2HM.js → chunk-46ZPIGWI.js} +3 -3
  117. package/dist/{chunk-HTIIXNJQ.js → chunk-4FIH6MPS.js} +6 -3
  118. package/dist/chunk-4FIH6MPS.js.map +1 -0
  119. package/dist/{chunk-MCA2X4WH.js → chunk-4IHBN6WU.js} +2 -2
  120. package/dist/{chunk-W7IGLX4G.js → chunk-4O6WRBGE.js} +7 -7
  121. package/dist/chunk-4O6WRBGE.js.map +1 -0
  122. package/dist/{chunk-FB7LAJQM.js → chunk-4TKJGOCJ.js} +2 -2
  123. package/dist/{chunk-H7BZBSP6.js → chunk-52II7PB6.js} +4 -4
  124. package/dist/{chunk-N4Y5T6VD.js → chunk-54WHLA65.js} +4 -4
  125. package/dist/chunk-56Y6EKZN.js +24 -0
  126. package/dist/chunk-56Y6EKZN.js.map +1 -0
  127. package/dist/{chunk-YVLENSNS.js → chunk-5ATKSPEQ.js} +2 -2
  128. package/dist/{chunk-OLT3YTDH.js → chunk-5FAGCOZD.js} +37 -24
  129. package/dist/chunk-5FAGCOZD.js.map +1 -0
  130. package/dist/{chunk-ZVZHG7AI.js → chunk-5UBH37SL.js} +3 -3
  131. package/dist/{chunk-N3WZU76A.js → chunk-66OJSCI6.js} +2 -2
  132. package/dist/{chunk-D35XLYQ7.js → chunk-6CVNSDCZ.js} +11 -11
  133. package/dist/{chunk-KA2HZCMG.js → chunk-6NXSYYGP.js} +2 -2
  134. package/dist/{chunk-2HIJHXB3.js → chunk-6WHJ2EAB.js} +2 -2
  135. package/dist/{chunk-ZZM476NV.js → chunk-6XL73HQ4.js} +4 -4
  136. package/dist/{chunk-EJ5RKUXW.js → chunk-AQ7SYDVO.js} +2 -2
  137. package/dist/{chunk-2Z3DG5CQ.js → chunk-B3O3IRZN.js} +2 -2
  138. package/dist/{chunk-P77QX3ID.js → chunk-B6O2YLUR.js} +21 -20
  139. package/dist/chunk-B6O2YLUR.js.map +1 -0
  140. package/dist/{chunk-RACZB5HT.js → chunk-BD7JTBYP.js} +4 -4
  141. package/dist/{chunk-I762GW7V.js → chunk-BLJWYCNO.js} +5 -5
  142. package/dist/{chunk-ACPCA4NO.js → chunk-BMWN7B3Y.js} +6 -6
  143. package/dist/chunk-BMWN7B3Y.js.map +1 -0
  144. package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
  145. package/dist/{chunk-KK66P5GQ.js → chunk-C553ZTLF.js} +46 -48
  146. package/dist/chunk-C553ZTLF.js.map +1 -0
  147. package/dist/{chunk-GCASGYIO.js → chunk-C7HJ3NEI.js} +2 -2
  148. package/dist/{chunk-C24UZSFW.js → chunk-CEH2AFXG.js} +6 -6
  149. package/dist/{chunk-WVGXCBOC.js → chunk-D5F7TKMH.js} +2 -2
  150. package/dist/chunk-DGS34QSY.js +141 -0
  151. package/dist/chunk-DGS34QSY.js.map +1 -0
  152. package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
  153. package/dist/{chunk-2OXIPF3A.js → chunk-DZD3WGCD.js} +4 -4
  154. package/dist/{chunk-UMEOXLIZ.js → chunk-E2P7TZTF.js} +5 -5
  155. package/dist/{chunk-6FC3IZ26.js → chunk-EBA5DA77.js} +2 -2
  156. package/dist/{chunk-N7JK2RKG.js → chunk-EOQ6FSQP.js} +2 -2
  157. package/dist/chunk-EUT2WCWS.js +54 -0
  158. package/dist/chunk-EUT2WCWS.js.map +1 -0
  159. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  160. package/dist/{chunk-XDMI5VKE.js → chunk-FVOP3NT7.js} +2 -2
  161. package/dist/{chunk-NRLELHAY.js → chunk-G7NYVKD6.js} +2 -2
  162. package/dist/{chunk-B3W7GWQB.js → chunk-GHTSNGBZ.js} +2 -2
  163. package/dist/chunk-GKCXTSFF.js +17 -0
  164. package/dist/chunk-GKCXTSFF.js.map +1 -0
  165. package/dist/{chunk-LUDBXTBJ.js → chunk-H272ZPQ3.js} +5 -5
  166. package/dist/{chunk-SN4JXNLL.js → chunk-H3TKENU4.js} +2 -2
  167. package/dist/{chunk-4HYNMLT6.js → chunk-H4SEGIZY.js} +2 -2
  168. package/dist/{chunk-ACZN5ZXD.js → chunk-H54VWRPS.js} +13 -13
  169. package/dist/{chunk-HTP6XRUI.js → chunk-HM7RHKS4.js} +2 -2
  170. package/dist/{chunk-Z3SQEEB6.js → chunk-HWLIMQE2.js} +2 -2
  171. package/dist/{chunk-NQFCLAYT.js → chunk-ICKJ52PE.js} +99 -322
  172. package/dist/chunk-ICKJ52PE.js.map +1 -0
  173. package/dist/{chunk-2QSZNTDO.js → chunk-JBPKEARU.js} +7 -7
  174. package/dist/{chunk-DK6GMO6D.js → chunk-JEC4DVM3.js} +16 -16
  175. package/dist/chunk-JEC4DVM3.js.map +1 -0
  176. package/dist/{chunk-ET2CNMLD.js → chunk-JN72EPOE.js} +2 -2
  177. package/dist/{chunk-U62OK7BB.js → chunk-JPKK466T.js} +188 -21
  178. package/dist/chunk-JPKK466T.js.map +1 -0
  179. package/dist/{chunk-JCCWJHOP.js → chunk-JYZSRNR7.js} +2 -2
  180. package/dist/{chunk-7D6HXODY.js → chunk-K7724CH2.js} +2 -2
  181. package/dist/{chunk-X33MJFD2.js → chunk-KAXCYM66.js} +3 -3
  182. package/dist/{chunk-7RRUHJPG.js → chunk-KB2G2I6Q.js} +3 -3
  183. package/dist/{chunk-ZXBTTMGZ.js → chunk-KEF46KL2.js} +5 -5
  184. package/dist/{chunk-5UZXUTVO.js → chunk-KNA3BXXL.js} +6 -2
  185. package/dist/chunk-KNA3BXXL.js.map +1 -0
  186. package/dist/{chunk-UWTDARPF.js → chunk-KTWG7CNY.js} +6 -6
  187. package/dist/{chunk-XQYTSZW3.js → chunk-L7Z57SID.js} +6 -6
  188. package/dist/{chunk-JMAG7WZ7.js → chunk-LO4UKPKX.js} +3 -3
  189. package/dist/{chunk-LDWIPYJ6.js → chunk-LS2FHUML.js} +3 -3
  190. package/dist/{chunk-GDB4J2H3.js → chunk-MPC7TYTZ.js} +2 -2
  191. package/dist/{chunk-2GF27PGH.js → chunk-MRPOZBJC.js} +3 -3
  192. package/dist/{chunk-BR3OOQZQ.js → chunk-MWN6QEI2.js} +2 -2
  193. package/dist/{chunk-NZGO2GKL.js → chunk-MY5EDR46.js} +4 -4
  194. package/dist/{chunk-LCMUZDBZ.js → chunk-N3ZOP7KC.js} +3 -3
  195. package/dist/{chunk-TVW4C7PV.js → chunk-NB3LRY4T.js} +6 -6
  196. package/dist/{chunk-QJCWK2A7.js → chunk-ND6IQGSZ.js} +2 -2
  197. package/dist/{chunk-NOB2T2FC.js → chunk-NHLLKEKK.js} +2 -2
  198. package/dist/{chunk-7GSSHAXC.js → chunk-NLYVH66G.js} +6 -6
  199. package/dist/chunk-NLYVH66G.js.map +1 -0
  200. package/dist/{chunk-EJIWBTVM.js → chunk-O6ZNNVSP.js} +2 -2
  201. package/dist/{chunk-AG46P24C.js → chunk-OB3XMWX5.js} +4 -4
  202. package/dist/{chunk-NYRB7BKY.js → chunk-OEYZXDQI.js} +6 -6
  203. package/dist/chunk-OEYZXDQI.js.map +1 -0
  204. package/dist/{chunk-46QMN7I7.js → chunk-PAZY2ZKP.js} +6 -6
  205. package/dist/{chunk-EF4JUM55.js → chunk-PX4UMA5Z.js} +2 -2
  206. package/dist/chunk-QLK3SL7Q.js +242 -0
  207. package/dist/chunk-QLK3SL7Q.js.map +1 -0
  208. package/dist/{chunk-GGYDJJBO.js → chunk-QOOBICJF.js} +5 -5
  209. package/dist/chunk-QOOBICJF.js.map +1 -0
  210. package/dist/{chunk-NNZFPYYX.js → chunk-R7QY5ZVB.js} +2 -2
  211. package/dist/{chunk-RO2X2EYM.js → chunk-R7RTXEYG.js} +2 -2
  212. package/dist/{chunk-3LJPSRAN.js → chunk-RBCQVOVJ.js} +3 -3
  213. package/dist/{chunk-N4F7ZSSG.js → chunk-RC5DXXWC.js} +2 -2
  214. package/dist/{chunk-VLAQV4HT.js → chunk-RJYJ7KR7.js} +1636 -611
  215. package/dist/chunk-RJYJ7KR7.js.map +1 -0
  216. package/dist/{chunk-5CF2R4NS.js → chunk-RKBLRTNT.js} +2 -2
  217. package/dist/{chunk-5QZR2RA3.js → chunk-ROBHL3GL.js} +2 -2
  218. package/dist/{chunk-D5ZNOKR7.js → chunk-S7T2TY65.js} +3 -3
  219. package/dist/{chunk-C2JCVZWX.js → chunk-SCXURCVT.js} +25 -13
  220. package/dist/chunk-SCXURCVT.js.map +1 -0
  221. package/dist/{chunk-3LADWWMA.js → chunk-SJDQ5RSN.js} +3 -3
  222. package/dist/{chunk-45XVII3J.js → chunk-SMWTZLHE.js} +99 -89
  223. package/dist/{chunk-45XVII3J.js.map → chunk-SMWTZLHE.js.map} +1 -1
  224. package/dist/{chunk-XOQGUGJL.js → chunk-SO7SV523.js} +1 -1
  225. package/dist/{chunk-XOQGUGJL.js.map → chunk-SO7SV523.js.map} +1 -1
  226. package/dist/{chunk-CHWUICVY.js → chunk-SV4Z2S67.js} +4 -4
  227. package/dist/{chunk-JHP45LO5.js → chunk-SVXJLDDC.js} +9 -7
  228. package/dist/{chunk-JHP45LO5.js.map → chunk-SVXJLDDC.js.map} +1 -1
  229. package/dist/{chunk-SEF4IOFE.js → chunk-SWIKABKI.js} +2 -2
  230. package/dist/{chunk-JRRQ747T.js → chunk-TBXAY2SS.js} +13 -13
  231. package/dist/{chunk-J6A3CX5N.js → chunk-TG3FAM2S.js} +33 -8
  232. package/dist/{chunk-J6A3CX5N.js.map → chunk-TG3FAM2S.js.map} +1 -1
  233. package/dist/{chunk-XZBXINXZ.js → chunk-TNSCIPBQ.js} +4 -4
  234. package/dist/{chunk-S37NN23M.js → chunk-TVEONGBU.js} +2 -2
  235. package/dist/{chunk-5RPMTFB6.js → chunk-TWF35IC6.js} +3 -3
  236. package/dist/{chunk-BZTJEILZ.js → chunk-U6N5IBVY.js} +2 -2
  237. package/dist/{chunk-7TUB6JWA.js → chunk-UCKYTA2T.js} +3 -3
  238. package/dist/{chunk-PPOT6SPR.js → chunk-UNNPNBR2.js} +11 -11
  239. package/dist/{chunk-KQAFEZQX.js → chunk-VDX2J7OX.js} +2 -2
  240. package/dist/{chunk-J2R275FN.js → chunk-VVX5PZFS.js} +4 -4
  241. package/dist/{chunk-ME35HVDJ.js → chunk-VYCOJKSX.js} +2 -2
  242. package/dist/{chunk-F5UWQ4LX.js → chunk-W24T5YMW.js} +2 -2
  243. package/dist/{chunk-HDQUQBPW.js → chunk-W3K3HITK.js} +2 -2
  244. package/dist/{chunk-GDASG7NC.js → chunk-W4F3C7N7.js} +2 -2
  245. package/dist/{chunk-LFK623SF.js → chunk-WJHGDE7X.js} +5 -5
  246. package/dist/{chunk-DP4EBV7C.js → chunk-WMLEPWMO.js} +16 -16
  247. package/dist/chunk-WMLEPWMO.js.map +1 -0
  248. package/dist/{chunk-3Q6RD76J.js → chunk-WQZZDN5M.js} +5 -5
  249. package/dist/{chunk-ZJWDWPX6.js → chunk-X5K5TAQZ.js} +4 -4
  250. package/dist/{chunk-ZL3XX5LW.js → chunk-XM24CDZX.js} +3 -3
  251. package/dist/{chunk-JTWZKNUH.js → chunk-XOVKXJEP.js} +3 -3
  252. package/dist/{chunk-OGTKQ5MJ.js → chunk-XSGUMQQQ.js} +62 -50
  253. package/dist/chunk-XSGUMQQQ.js.map +1 -0
  254. package/dist/{chunk-X5YYSAZX.js → chunk-XUE46UYW.js} +3 -3
  255. package/dist/{chunk-DJORULH3.js → chunk-YRLHEEQT.js} +2 -2
  256. package/dist/{chunk-TA7UZZMW.js → chunk-Z3O53WZR.js} +2 -2
  257. package/dist/{chunk-4LG4CPOC.js → chunk-Z6COGHHN.js} +2 -2
  258. package/dist/{chunk-RE3H6Y2X.js → chunk-ZIW5JVRM.js} +2 -2
  259. package/dist/{cli-BNtvR-nU.d.ts → cli-DD0P-lu8.d.ts} +6 -6
  260. package/dist/cli.d.ts +16 -14
  261. package/dist/cli.js +154 -149
  262. package/dist/coding/export-okf-codegraph.d.ts +2 -1
  263. package/dist/coding/export-okf-codegraph.js +37 -37
  264. package/dist/coding/pre-action-gate.d.ts +2 -1
  265. package/dist/coding/pre-action-gate.js +4 -3
  266. package/dist/commitment-ledger.js +3 -2
  267. package/dist/compounding/engine.d.ts +3 -2
  268. package/dist/compounding/engine.js +37 -37
  269. package/dist/compounding/preference-consolidator.d.ts +2 -1
  270. package/dist/compression-optimizer.d.ts +2 -1
  271. package/dist/config.d.ts +2 -1
  272. package/dist/config.js +16 -13
  273. package/dist/connectors/codex-materialize-runner.d.ts +2 -1
  274. package/dist/connectors/codex-materialize-runner.js +38 -38
  275. package/dist/connectors/codex-materialize.d.ts +2 -1
  276. package/dist/connectors/codex-materialize.js +4 -4
  277. package/dist/connectors/index.d.ts +2 -1
  278. package/dist/connectors/index.js +38 -38
  279. package/dist/consolidation-provenance-check.d.ts +3 -2
  280. package/dist/consolidation-provenance-check.js +4 -4
  281. package/dist/consolidation-undo.d.ts +3 -2
  282. package/dist/consolidation-undo.js +3 -3
  283. package/dist/contradiction/index.d.ts +4 -3
  284. package/dist/contradiction/index.js +9 -9
  285. package/dist/converge-config.d.ts +2 -1
  286. package/dist/convergence-refresh.d.ts +3 -2
  287. package/dist/convergence-refresh.js +38 -38
  288. package/dist/conversation-index/backend.d.ts +2 -1
  289. package/dist/conversation-index/chunker.d.ts +2 -1
  290. package/dist/conversation-index/faiss-adapter.d.ts +2 -1
  291. package/dist/conversation-index/indexer.d.ts +2 -1
  292. package/dist/conversation-index/search.d.ts +2 -1
  293. package/dist/corpus-watermark.d.ts +2 -1
  294. package/dist/corpus-watermark.js +38 -38
  295. package/dist/cross-lingual-recall.d.ts +2 -1
  296. package/dist/cue-anchors.js +4 -3
  297. package/dist/dashboard-runtime.js +2 -2
  298. package/dist/day-summary.d.ts +2 -1
  299. package/dist/deep-recall-http-glue.d.ts +14 -12
  300. package/dist/deep-recall-http-glue.js +9 -8
  301. package/dist/deep-recall-mcp-tools.d.ts +14 -12
  302. package/dist/deep-recall-policy-llm.d.ts +2 -1
  303. package/dist/deep-recall-seeds.d.ts +2 -1
  304. package/dist/deep-recall.js +2 -2
  305. package/dist/delinearize.d.ts +2 -1
  306. package/dist/dependency-propagation-config.d.ts +2 -1
  307. package/dist/{dependency-propagation-delivery-DjpUlhLi.d.ts → dependency-propagation-delivery-CQPCu0o5.d.ts} +1 -1
  308. package/dist/direct-answer-wiring.d.ts +2 -1
  309. package/dist/direct-answer.d.ts +2 -1
  310. package/dist/embedding-fallback.d.ts +2 -1
  311. package/dist/enrichment/index.d.ts +2 -1
  312. package/dist/entity-origin-fields.d.ts +2 -1
  313. package/dist/entity-retrieval.d.ts +3 -2
  314. package/dist/entity-retrieval.js +36 -36
  315. package/dist/entity-schema.d.ts +2 -1
  316. package/dist/episodic-context.d.ts +2 -1
  317. package/dist/eval-trajectory-contracts.d.ts +2 -1
  318. package/dist/eval-trajectory-store.d.ts +2 -1
  319. package/dist/eval-trajectory-store.js +4 -3
  320. package/dist/eval-trajectory.d.ts +2 -1
  321. package/dist/eval-trajectory.js +4 -3
  322. package/dist/evals.js +3 -2
  323. package/dist/event-order-recall.js +2 -2
  324. package/dist/explicit-capture.d.ts +12 -10
  325. package/dist/explicit-capture.js +10 -10
  326. package/dist/external-wiki-access.d.ts +14 -12
  327. package/dist/external-wiki-access.js +95 -92
  328. package/dist/external-wiki-collection-registration.d.ts +2 -1
  329. package/dist/external-wiki-collection.d.ts +2 -1
  330. package/dist/external-wiki-mcp-tools.d.ts +14 -12
  331. package/dist/extraction-error-classification.d.ts +2 -1
  332. package/dist/extraction-faithfulness.d.ts +2 -1
  333. package/dist/extraction-judge-telemetry.d.ts +2 -1
  334. package/dist/extraction-judge-training.d.ts +2 -1
  335. package/dist/extraction-judge.d.ts +2 -1
  336. package/dist/extraction-liveness.d.ts +2 -1
  337. package/dist/extraction-normalization.d.ts +2 -1
  338. package/dist/extraction-prompt.d.ts +2 -1
  339. package/dist/extraction-source-grounding-rules.d.ts +2 -1
  340. package/dist/extraction-source-grounding.d.ts +2 -1
  341. package/dist/extraction.d.ts +2 -1
  342. package/dist/extraction.js +43 -43
  343. package/dist/fallback-llm.d.ts +2 -1
  344. package/dist/{first-start-migration-KHIMCBIC.js → first-start-migration-RC345GPL.js} +6 -5
  345. package/dist/{first-start-migration-KHIMCBIC.js.map → first-start-migration-RC345GPL.js.map} +1 -1
  346. package/dist/{forget-2AE4MDK3.js → forget-SZ2BXDGF.js} +3 -3
  347. package/dist/{graph-444E-DrF.d.ts → graph-DRkl22KE.d.ts} +10 -12
  348. package/dist/graph-append-error.d.ts +40 -0
  349. package/dist/graph-append-error.js +8 -0
  350. package/dist/graph-dashboard-diff.d.ts +5 -2
  351. package/dist/graph-dashboard-key.d.ts +5 -2
  352. package/dist/graph-dashboard-parser.d.ts +5 -2
  353. package/dist/{graph-edge-decay-DGTBFALQ.js → graph-edge-decay-MKIRZ762.js} +10 -4
  354. package/dist/graph-edge-decay-MKIRZ762.js.map +1 -0
  355. package/dist/graph-edge-reinforcement.d.ts +5 -2
  356. package/dist/graph-jsonl.d.ts +82 -0
  357. package/dist/graph-jsonl.js +23 -0
  358. package/dist/graph-jsonl.js.map +1 -0
  359. package/dist/graph-path-reconstruction.d.ts +5 -2
  360. package/dist/graph-path-scoring.d.ts +5 -2
  361. package/dist/graph-snapshot.d.ts +5 -2
  362. package/dist/graph-snapshot.js +7 -3
  363. package/dist/graph-write-lock.d.ts +84 -0
  364. package/dist/graph-write-lock.js +15 -0
  365. package/dist/graph-write-lock.js.map +1 -0
  366. package/dist/graph.d.ts +5 -2
  367. package/dist/graph.js +10 -3
  368. package/dist/harmonic-construction.js +5 -4
  369. package/dist/harmonic-retrieval.js +5 -4
  370. package/dist/importance.d.ts +2 -1
  371. package/dist/importers/index.d.ts +2 -1
  372. package/dist/in-flight-reads.d.ts +2 -1
  373. package/dist/{index-CsUrdMyL.d.ts → index-C9VXenR8.d.ts} +2 -2
  374. package/dist/index.d.ts +39 -25
  375. package/dist/index.js +291 -284
  376. package/dist/intent.d.ts +2 -1
  377. package/dist/json-store.js +2 -1
  378. package/dist/lcm/engine.d.ts +2 -1
  379. package/dist/lcm/engine.js +3 -3
  380. package/dist/lcm/index.d.ts +2 -1
  381. package/dist/lcm/index.js +6 -6
  382. package/dist/lcm/tools.d.ts +2 -1
  383. package/dist/lifecycle.d.ts +2 -1
  384. package/dist/live-connectors-runner.d.ts +2 -1
  385. package/dist/live-connectors-runner.js +3 -2
  386. package/dist/local-llm.d.ts +2 -1
  387. package/dist/local-model-endpoint.d.ts +2 -1
  388. package/dist/location/index.d.ts +4 -3
  389. package/dist/location/index.js +39 -39
  390. package/dist/maintenance/memory-governance.d.ts +2 -1
  391. package/dist/maintenance/memory-governance.js +36 -36
  392. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +3 -2
  393. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +36 -36
  394. package/dist/maintenance/rebuild-memory-projection.d.ts +3 -2
  395. package/dist/maintenance/rebuild-memory-projection.js +37 -37
  396. package/dist/{maintenance-D1GRGLTE.d.ts → maintenance-C14sorKa.d.ts} +3 -3
  397. package/dist/mcp-memory-inspector-app.d.ts +14 -12
  398. package/dist/memory-action-policy.d.ts +2 -1
  399. package/dist/memory-cache.d.ts +2 -1
  400. package/dist/memory-controller.d.ts +2 -1
  401. package/dist/memory-lifecycle-ledger-utils.d.ts +2 -1
  402. package/dist/memory-projection-mutations.js +3 -3
  403. package/dist/memory-projection-store.d.ts +2 -1
  404. package/dist/memory-provenance.d.ts +2 -1
  405. package/dist/memory-snapshot.d.ts +2 -1
  406. package/dist/memory-subject-cli.d.ts +14 -12
  407. package/dist/memory-subject.d.ts +14 -12
  408. package/dist/memory-subject.js +5 -5
  409. package/dist/memory-worth-outcomes.d.ts +3 -2
  410. package/dist/models-json.d.ts +2 -1
  411. package/dist/namespaces/migrate.d.ts +4 -3
  412. package/dist/namespaces/migrate.js +37 -37
  413. package/dist/namespaces/principal.d.ts +2 -1
  414. package/dist/namespaces/search.d.ts +2 -1
  415. package/dist/namespaces/search.js +11 -11
  416. package/dist/namespaces/storage.d.ts +4 -3
  417. package/dist/namespaces/storage.js +36 -36
  418. package/dist/native-knowledge.d.ts +2 -1
  419. package/dist/objective-state-writers.js +4 -3
  420. package/dist/objective-state.js +3 -2
  421. package/dist/offline-sync-file-io.js +4 -3
  422. package/dist/offline-sync-impression-drain.d.ts +2 -1
  423. package/dist/offline-sync-impression-drain.js +7 -7
  424. package/dist/offline-sync.js +5 -4
  425. package/dist/operator-doctor-corpus.d.ts +2 -1
  426. package/dist/operator-doctor-corpus.js +39 -39
  427. package/dist/operator-doctor-replica.d.ts +2 -1
  428. package/dist/operator-toolkit.d.ts +6 -4
  429. package/dist/operator-toolkit.js +61 -58
  430. package/dist/orchestration/compression-guideline-coordinator.d.ts +3 -2
  431. package/dist/orchestration/maintenance.d.ts +5 -4
  432. package/dist/orchestration/maintenance.js +46 -46
  433. package/dist/{orchestrator-DeqaPGT5.d.ts → orchestrator-DgyvFfPR.d.ts} +9 -9
  434. package/dist/orchestrator.d.ts +12 -10
  435. package/dist/orchestrator.js +201 -196
  436. package/dist/page-versioning.d.ts +73 -4
  437. package/dist/page-versioning.js +11 -3
  438. package/dist/patterns-cli.d.ts +2 -1
  439. package/dist/{pipeline-umUzU0D1.d.ts → pipeline-DRUwHb1X.d.ts} +1 -1
  440. package/dist/policy-runtime.d.ts +2 -1
  441. package/dist/proactive-contention.d.ts +2 -1
  442. package/dist/provenance.d.ts +2 -1
  443. package/dist/{public-http-Bevrjll9.d.ts → public-http-C4i8Opxc.d.ts} +1 -1
  444. package/dist/{purge-OP6BEDFY.js → purge-TZKRE6ZQ.js} +5 -5
  445. package/dist/purge-TZKRE6ZQ.js.map +1 -0
  446. package/dist/{qmd-BasT78PP.d.ts → qmd-BVWlg1YS.d.ts} +1 -1
  447. package/dist/qmd-preflight.d.ts +3 -2
  448. package/dist/qmd-recall-cache.d.ts +2 -1
  449. package/dist/qmd.d.ts +3 -2
  450. package/dist/qmd.js +4 -4
  451. package/dist/recall-concurrency-config.d.ts +2 -1
  452. package/dist/recall-disclosure-escalation.d.ts +2 -1
  453. package/dist/recall-explain-renderer.d.ts +2 -1
  454. package/dist/recall-explain-renderer.js +3 -3
  455. package/dist/recall-memory-map.d.ts +2 -1
  456. package/dist/recall-navigate-access.d.ts +2 -1
  457. package/dist/recall-navigate-access.js +2 -2
  458. package/dist/recall-navigate-disclosure.d.ts +2 -1
  459. package/dist/recall-navigate-disclosure.js +1 -1
  460. package/dist/recall-navigate.d.ts +2 -1
  461. package/dist/recall-navigate.js +2 -2
  462. package/dist/recall-planner-llm.d.ts +2 -1
  463. package/dist/recall-state.d.ts +2 -1
  464. package/dist/recall-state.js +3 -3
  465. package/dist/recall-tag-filter.d.ts +2 -1
  466. package/dist/recall-timings.d.ts +2 -1
  467. package/dist/recall-xray-cli.d.ts +2 -1
  468. package/dist/recall-xray-cli.js +4 -4
  469. package/dist/recall-xray-renderer.d.ts +2 -1
  470. package/dist/recall-xray-renderer.js +3 -3
  471. package/dist/recall-xray.d.ts +2 -1
  472. package/dist/recall-xray.js +2 -2
  473. package/dist/reconcile/cursor.d.ts +2 -1
  474. package/dist/reconcile/manifest.d.ts +2 -1
  475. package/dist/reconcile/manifest.js +7 -7
  476. package/dist/reconcile/plan.d.ts +2 -1
  477. package/dist/replay/normalizers/chatgpt.d.ts +2 -1
  478. package/dist/replay/normalizers/claude.d.ts +2 -1
  479. package/dist/replay/normalizers/openclaw.d.ts +2 -1
  480. package/dist/replay/normalizers/shared.d.ts +2 -1
  481. package/dist/replay/runner.d.ts +2 -1
  482. package/dist/replay/types.d.ts +2 -1
  483. package/dist/replica-divergence.d.ts +2 -1
  484. package/dist/replica-peers-config.d.ts +2 -1
  485. package/dist/{resolution-4DLI3IOR.js → resolution-VKATP2HT.js} +10 -10
  486. package/dist/resolution-VKATP2HT.js.map +1 -0
  487. package/dist/resolve-auth-token.d.ts +2 -1
  488. package/dist/resume-bundles.js +21 -18
  489. package/dist/retrieval-agents.d.ts +3 -2
  490. package/dist/retrieval-tiers.d.ts +2 -1
  491. package/dist/routing/engine.d.ts +2 -1
  492. package/dist/routing/store.d.ts +2 -1
  493. package/dist/salvage-envelope.d.ts +2 -1
  494. package/dist/salvage-envelope.js +7 -7
  495. package/dist/{scope-profiles-DXYzyOXW.d.ts → scope-profiles-Vl00B1KU.d.ts} +1 -1
  496. package/dist/search/document-scanner.js +3 -3
  497. package/dist/search/embed-helper.d.ts +2 -1
  498. package/dist/search/factory.d.ts +2 -1
  499. package/dist/search/factory.js +10 -10
  500. package/dist/search/index.d.ts +2 -1
  501. package/dist/search/index.js +16 -16
  502. package/dist/search/lancedb-backend.d.ts +2 -1
  503. package/dist/search/lancedb-backend.js +4 -4
  504. package/dist/search/meilisearch-backend.d.ts +2 -1
  505. package/dist/search/meilisearch-backend.js +4 -4
  506. package/dist/search/noop-backend.d.ts +2 -1
  507. package/dist/search/orama-backend.d.ts +2 -1
  508. package/dist/search/orama-backend.js +4 -4
  509. package/dist/search/port.d.ts +2 -1
  510. package/dist/search/remote-backend.d.ts +2 -1
  511. package/dist/secure-store/index.js +3 -3
  512. package/dist/{semantic-consolidation-CqDeaY0U.d.ts → semantic-consolidation-BC9-GFuT.d.ts} +1 -1
  513. package/dist/semantic-consolidation.d.ts +3 -2
  514. package/dist/semantic-consolidation.js +39 -39
  515. package/dist/semantic-merge-config.d.ts +17 -0
  516. package/dist/semantic-merge-config.js +1 -0
  517. package/dist/semantic-merge-config.js.map +1 -0
  518. package/dist/semantic-rule-promotion.js +36 -36
  519. package/dist/semantic-rule-verifier.d.ts +2 -1
  520. package/dist/semantic-rule-verifier.js +36 -36
  521. package/dist/{service-BKD-DVYZ.d.ts → service-BOy6dHfc.d.ts} +2 -2
  522. package/dist/session-observer-bands.d.ts +2 -1
  523. package/dist/session-observer-state.d.ts +2 -1
  524. package/dist/shared-context/manager.d.ts +2 -1
  525. package/dist/signal.d.ts +2 -1
  526. package/dist/source-agent-qualifier.d.ts +2 -1
  527. package/dist/source-agent-qualifier.js +42 -42
  528. package/dist/source-attribution.d.ts +18 -6
  529. package/dist/source-attribution.js +3 -1
  530. package/dist/standup/build.js +36 -36
  531. package/dist/{storage-BcFd1lN_.d.ts → storage-BLJRnFi6.d.ts} +15 -8
  532. package/dist/storage.d.ts +3 -2
  533. package/dist/storage.js +35 -35
  534. package/dist/subject-config.d.ts +2 -1
  535. package/dist/subject-config.js +6 -6
  536. package/dist/summarizer.d.ts +2 -1
  537. package/dist/summary-snapshot.d.ts +2 -1
  538. package/dist/support-passport/index.d.ts +17 -15
  539. package/dist/support-passport/index.js +97 -94
  540. package/dist/temporal-supersession.d.ts +3 -2
  541. package/dist/temporal-timeline-recall.d.ts +2 -1
  542. package/dist/temporal-validity.d.ts +2 -1
  543. package/dist/threading.d.ts +2 -1
  544. package/dist/tier-migration.d.ts +3 -2
  545. package/dist/tier-routing.d.ts +2 -1
  546. package/dist/{tier-stats-2TOW3QNL.js → tier-stats-KA6OZX2C.js} +6 -5
  547. package/dist/{tier-stats-2TOW3QNL.js.map → tier-stats-KA6OZX2C.js.map} +1 -1
  548. package/dist/topics.d.ts +2 -1
  549. package/dist/transcript.d.ts +2 -1
  550. package/dist/transfer/backup.js +2 -2
  551. package/dist/transfer/capsule-export.js +2 -2
  552. package/dist/transfer/capsule-import.js +6 -6
  553. package/dist/transfer/export-okf.d.ts +2 -1
  554. package/dist/transfer/export-okf.js +37 -37
  555. package/dist/trust-score-stage.d.ts +2 -1
  556. package/dist/trust-score.d.ts +2 -1
  557. package/dist/trust-zones.js +3 -2
  558. package/dist/{types-BRg3tMyp.d.ts → types-YY-eKKU7.d.ts} +3 -0
  559. package/dist/types.d.ts +2 -1
  560. package/dist/types.js +1 -1
  561. package/dist/utility-learner.js +4 -3
  562. package/dist/utility-runtime.d.ts +2 -1
  563. package/dist/utility-runtime.js +5 -4
  564. package/dist/utility-telemetry.js +3 -2
  565. package/dist/verified-recall.js +36 -36
  566. package/dist/who-knows-cli.d.ts +14 -12
  567. package/dist/who-knows-cli.js +37 -37
  568. package/dist/who-knows.d.ts +14 -12
  569. package/dist/who-knows.js +36 -36
  570. package/dist/work/board.js +3 -3
  571. package/dist/work/storage.js +2 -2
  572. package/dist/work-product-ledger.js +3 -2
  573. package/dist/write-envelope.d.ts +2 -1
  574. package/dist/write-envelope.js +6 -6
  575. package/package.json +2 -2
  576. package/src/abstraction-nodes.ts +1 -4
  577. package/src/access-http-briefing.ts +35 -0
  578. package/src/access-http.test.ts +76 -0
  579. package/src/access-http.ts +20 -23
  580. package/src/access-surface-catalog.test.ts +9 -0
  581. package/src/access-surface-catalog.ts +1 -0
  582. package/src/binary-lifecycle/backend.ts +2 -1
  583. package/src/config.ts +2 -0
  584. package/src/connectors/live/github.ts +10 -17
  585. package/src/connectors/live/gmail.ts +8 -19
  586. package/src/connectors/live/google-drive.ts +6 -13
  587. package/src/connectors/live/notion.ts +11 -21
  588. package/src/cue-anchors.ts +1 -6
  589. package/src/dedup/connector-scope.ts +39 -0
  590. package/src/dedup/merge-content.test.ts +21 -0
  591. package/src/dedup/merge-content.ts +21 -7
  592. package/src/dedup/merge-judge.ts +146 -0
  593. package/src/dedup/merge.test.ts +3542 -0
  594. package/src/dedup/merge.ts +384 -0
  595. package/src/dedup/novelty-gate.ts +12 -10
  596. package/src/dedup/semantic.ts +7 -9
  597. package/src/graph-append-error.ts +28 -0
  598. package/src/graph-jsonl.ts +267 -0
  599. package/src/graph-write-lock.test.ts +228 -0
  600. package/src/graph-write-lock.ts +132 -0
  601. package/src/graph.ts +49 -45
  602. package/src/harmonic-construction.test.ts +329 -0
  603. package/src/json-store.ts +17 -20
  604. package/src/live-connectors-runner.ts +2 -1
  605. package/src/location/store.ts +1 -4
  606. package/src/maintenance/graph-edge-decay.ts +24 -13
  607. package/src/maintenance/purge.ts +2 -5
  608. package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +3 -6
  609. package/src/maintenance/rebuild-memory-projection.ts +2 -5
  610. package/src/namespaces/catalog.ts +1 -9
  611. package/src/offline-sync-file-io.test.ts +39 -0
  612. package/src/offline-sync-file-io.ts +3 -7
  613. package/src/orchestration/contradiction-localization.integration.test.ts +35 -0
  614. package/src/orchestration/extraction-anchor-snapshot.ts +6 -1
  615. package/src/orchestration/extraction-persist-deps.ts +2 -1
  616. package/src/orchestration/extraction-persist-profile-promotion.test.ts +456 -0
  617. package/src/orchestration/extraction-persist-promotion.test.ts +534 -0
  618. package/src/orchestration/extraction-persist-promotion.ts +387 -1
  619. package/src/orchestration/extraction-persist.ts +95 -94
  620. package/src/orchestration/harmonic-construction-persist.test.ts +69 -0
  621. package/src/orchestration/harmonic-construction-persist.ts +55 -0
  622. package/src/orchestration/persistence-index.ts +35 -7
  623. package/src/orchestration/semantic-merge-commit-effects.ts +354 -0
  624. package/src/orchestration/semantic-merge-persist.ts +1095 -0
  625. package/src/orchestration/semantic-merge-promotion-payload.ts +161 -0
  626. package/src/orchestrator.ts +2 -2
  627. package/src/page-versioning.ts +396 -22
  628. package/src/procedural/procedure-miner.ts +1 -9
  629. package/src/search/document-scanner.ts +3 -6
  630. package/src/semantic-merge-config.ts +16 -0
  631. package/src/semantic-rule-promotion.ts +1 -4
  632. package/src/source-attribution-roundtrip.test.ts +21 -0
  633. package/src/source-attribution.ts +57 -17
  634. package/src/storage-contract/versioning.test.ts +500 -1
  635. package/src/storage.ts +30 -30
  636. package/src/testing/subjects/graph-edge-decay.test.ts +163 -0
  637. package/src/testing/subjects/semantic-merge-lifecycle.test.ts +1175 -0
  638. package/src/types.ts +2 -0
  639. package/src/utils/errno.test.ts +31 -0
  640. package/src/utils/errno.ts +5 -0
  641. package/src/wearables/index.ts +1 -0
  642. package/src/wearables/registry.ts +23 -0
  643. package/dist/chunk-5UZXUTVO.js.map +0 -1
  644. package/dist/chunk-7GSSHAXC.js.map +0 -1
  645. package/dist/chunk-ACPCA4NO.js.map +0 -1
  646. package/dist/chunk-C2JCVZWX.js.map +0 -1
  647. package/dist/chunk-DDQQY2E3.js.map +0 -1
  648. package/dist/chunk-DJUKBGAD.js.map +0 -1
  649. package/dist/chunk-DK6GMO6D.js.map +0 -1
  650. package/dist/chunk-DP4EBV7C.js.map +0 -1
  651. package/dist/chunk-FCATYKQA.js.map +0 -1
  652. package/dist/chunk-GGYDJJBO.js.map +0 -1
  653. package/dist/chunk-HTIIXNJQ.js.map +0 -1
  654. package/dist/chunk-KK66P5GQ.js.map +0 -1
  655. package/dist/chunk-NQFCLAYT.js.map +0 -1
  656. package/dist/chunk-NYRB7BKY.js.map +0 -1
  657. package/dist/chunk-OGTKQ5MJ.js.map +0 -1
  658. package/dist/chunk-OLT3YTDH.js.map +0 -1
  659. package/dist/chunk-P77QX3ID.js.map +0 -1
  660. package/dist/chunk-U62OK7BB.js.map +0 -1
  661. package/dist/chunk-UR6PBX2J.js.map +0 -1
  662. package/dist/chunk-VLAQV4HT.js.map +0 -1
  663. package/dist/chunk-W7IGLX4G.js.map +0 -1
  664. package/dist/purge-OP6BEDFY.js.map +0 -1
  665. /package/dist/{capsule-crypto-7FJQINUR.js.map → access-http-briefing.js.map} +0 -0
  666. /package/dist/{auto-sync-GRUJFD2F.js.map → auto-sync-WNGG2GC2.js.map} +0 -0
  667. /package/dist/{graph-edge-decay-DGTBFALQ.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
  668. /package/dist/{capsule-merge-SEHERWWP.js.map → capsule-merge-IPG4IOFP.js.map} +0 -0
  669. /package/dist/{chunk-KTVSPFCQ.js.map → chunk-22FVR2KE.js.map} +0 -0
  670. /package/dist/{chunk-7Y5V3LAP.js.map → chunk-27QVNSQ3.js.map} +0 -0
  671. /package/dist/{chunk-R7LY4YKF.js.map → chunk-2EKJDQZB.js.map} +0 -0
  672. /package/dist/{chunk-HOUST5UX.js.map → chunk-2ITE4C2Z.js.map} +0 -0
  673. /package/dist/{chunk-JOYC4RYC.js.map → chunk-2JVJ7UHC.js.map} +0 -0
  674. /package/dist/{chunk-X2NFFBTO.js.map → chunk-2L5FSNPC.js.map} +0 -0
  675. /package/dist/{chunk-DUAJKRR2.js.map → chunk-2SGLMP2E.js.map} +0 -0
  676. /package/dist/{chunk-6FUMA6AA.js.map → chunk-2Z4GS5E6.js.map} +0 -0
  677. /package/dist/{chunk-G6MXN2G5.js.map → chunk-37KUQPX4.js.map} +0 -0
  678. /package/dist/{chunk-WKAXBXVA.js.map → chunk-3QQTXYZ6.js.map} +0 -0
  679. /package/dist/{chunk-4ZWCQJNM.js.map → chunk-46WM6JX2.js.map} +0 -0
  680. /package/dist/{chunk-CASWJ2HM.js.map → chunk-46ZPIGWI.js.map} +0 -0
  681. /package/dist/{chunk-MCA2X4WH.js.map → chunk-4IHBN6WU.js.map} +0 -0
  682. /package/dist/{chunk-FB7LAJQM.js.map → chunk-4TKJGOCJ.js.map} +0 -0
  683. /package/dist/{chunk-H7BZBSP6.js.map → chunk-52II7PB6.js.map} +0 -0
  684. /package/dist/{chunk-N4Y5T6VD.js.map → chunk-54WHLA65.js.map} +0 -0
  685. /package/dist/{chunk-YVLENSNS.js.map → chunk-5ATKSPEQ.js.map} +0 -0
  686. /package/dist/{chunk-ZVZHG7AI.js.map → chunk-5UBH37SL.js.map} +0 -0
  687. /package/dist/{chunk-N3WZU76A.js.map → chunk-66OJSCI6.js.map} +0 -0
  688. /package/dist/{chunk-D35XLYQ7.js.map → chunk-6CVNSDCZ.js.map} +0 -0
  689. /package/dist/{chunk-KA2HZCMG.js.map → chunk-6NXSYYGP.js.map} +0 -0
  690. /package/dist/{chunk-2HIJHXB3.js.map → chunk-6WHJ2EAB.js.map} +0 -0
  691. /package/dist/{chunk-ZZM476NV.js.map → chunk-6XL73HQ4.js.map} +0 -0
  692. /package/dist/{chunk-EJ5RKUXW.js.map → chunk-AQ7SYDVO.js.map} +0 -0
  693. /package/dist/{chunk-2Z3DG5CQ.js.map → chunk-B3O3IRZN.js.map} +0 -0
  694. /package/dist/{chunk-RACZB5HT.js.map → chunk-BD7JTBYP.js.map} +0 -0
  695. /package/dist/{chunk-I762GW7V.js.map → chunk-BLJWYCNO.js.map} +0 -0
  696. /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
  697. /package/dist/{chunk-GCASGYIO.js.map → chunk-C7HJ3NEI.js.map} +0 -0
  698. /package/dist/{chunk-C24UZSFW.js.map → chunk-CEH2AFXG.js.map} +0 -0
  699. /package/dist/{chunk-WVGXCBOC.js.map → chunk-D5F7TKMH.js.map} +0 -0
  700. /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
  701. /package/dist/{chunk-2OXIPF3A.js.map → chunk-DZD3WGCD.js.map} +0 -0
  702. /package/dist/{chunk-UMEOXLIZ.js.map → chunk-E2P7TZTF.js.map} +0 -0
  703. /package/dist/{chunk-6FC3IZ26.js.map → chunk-EBA5DA77.js.map} +0 -0
  704. /package/dist/{chunk-N7JK2RKG.js.map → chunk-EOQ6FSQP.js.map} +0 -0
  705. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  706. /package/dist/{chunk-XDMI5VKE.js.map → chunk-FVOP3NT7.js.map} +0 -0
  707. /package/dist/{chunk-NRLELHAY.js.map → chunk-G7NYVKD6.js.map} +0 -0
  708. /package/dist/{chunk-B3W7GWQB.js.map → chunk-GHTSNGBZ.js.map} +0 -0
  709. /package/dist/{chunk-LUDBXTBJ.js.map → chunk-H272ZPQ3.js.map} +0 -0
  710. /package/dist/{chunk-SN4JXNLL.js.map → chunk-H3TKENU4.js.map} +0 -0
  711. /package/dist/{chunk-4HYNMLT6.js.map → chunk-H4SEGIZY.js.map} +0 -0
  712. /package/dist/{chunk-ACZN5ZXD.js.map → chunk-H54VWRPS.js.map} +0 -0
  713. /package/dist/{chunk-HTP6XRUI.js.map → chunk-HM7RHKS4.js.map} +0 -0
  714. /package/dist/{chunk-Z3SQEEB6.js.map → chunk-HWLIMQE2.js.map} +0 -0
  715. /package/dist/{chunk-2QSZNTDO.js.map → chunk-JBPKEARU.js.map} +0 -0
  716. /package/dist/{chunk-ET2CNMLD.js.map → chunk-JN72EPOE.js.map} +0 -0
  717. /package/dist/{chunk-JCCWJHOP.js.map → chunk-JYZSRNR7.js.map} +0 -0
  718. /package/dist/{chunk-7D6HXODY.js.map → chunk-K7724CH2.js.map} +0 -0
  719. /package/dist/{chunk-X33MJFD2.js.map → chunk-KAXCYM66.js.map} +0 -0
  720. /package/dist/{chunk-7RRUHJPG.js.map → chunk-KB2G2I6Q.js.map} +0 -0
  721. /package/dist/{chunk-ZXBTTMGZ.js.map → chunk-KEF46KL2.js.map} +0 -0
  722. /package/dist/{chunk-UWTDARPF.js.map → chunk-KTWG7CNY.js.map} +0 -0
  723. /package/dist/{chunk-XQYTSZW3.js.map → chunk-L7Z57SID.js.map} +0 -0
  724. /package/dist/{chunk-JMAG7WZ7.js.map → chunk-LO4UKPKX.js.map} +0 -0
  725. /package/dist/{chunk-LDWIPYJ6.js.map → chunk-LS2FHUML.js.map} +0 -0
  726. /package/dist/{chunk-GDB4J2H3.js.map → chunk-MPC7TYTZ.js.map} +0 -0
  727. /package/dist/{chunk-2GF27PGH.js.map → chunk-MRPOZBJC.js.map} +0 -0
  728. /package/dist/{chunk-BR3OOQZQ.js.map → chunk-MWN6QEI2.js.map} +0 -0
  729. /package/dist/{chunk-NZGO2GKL.js.map → chunk-MY5EDR46.js.map} +0 -0
  730. /package/dist/{chunk-LCMUZDBZ.js.map → chunk-N3ZOP7KC.js.map} +0 -0
  731. /package/dist/{chunk-TVW4C7PV.js.map → chunk-NB3LRY4T.js.map} +0 -0
  732. /package/dist/{chunk-QJCWK2A7.js.map → chunk-ND6IQGSZ.js.map} +0 -0
  733. /package/dist/{chunk-NOB2T2FC.js.map → chunk-NHLLKEKK.js.map} +0 -0
  734. /package/dist/{chunk-EJIWBTVM.js.map → chunk-O6ZNNVSP.js.map} +0 -0
  735. /package/dist/{chunk-AG46P24C.js.map → chunk-OB3XMWX5.js.map} +0 -0
  736. /package/dist/{chunk-46QMN7I7.js.map → chunk-PAZY2ZKP.js.map} +0 -0
  737. /package/dist/{chunk-EF4JUM55.js.map → chunk-PX4UMA5Z.js.map} +0 -0
  738. /package/dist/{chunk-NNZFPYYX.js.map → chunk-R7QY5ZVB.js.map} +0 -0
  739. /package/dist/{chunk-RO2X2EYM.js.map → chunk-R7RTXEYG.js.map} +0 -0
  740. /package/dist/{chunk-3LJPSRAN.js.map → chunk-RBCQVOVJ.js.map} +0 -0
  741. /package/dist/{chunk-N4F7ZSSG.js.map → chunk-RC5DXXWC.js.map} +0 -0
  742. /package/dist/{chunk-5CF2R4NS.js.map → chunk-RKBLRTNT.js.map} +0 -0
  743. /package/dist/{chunk-5QZR2RA3.js.map → chunk-ROBHL3GL.js.map} +0 -0
  744. /package/dist/{chunk-D5ZNOKR7.js.map → chunk-S7T2TY65.js.map} +0 -0
  745. /package/dist/{chunk-3LADWWMA.js.map → chunk-SJDQ5RSN.js.map} +0 -0
  746. /package/dist/{chunk-CHWUICVY.js.map → chunk-SV4Z2S67.js.map} +0 -0
  747. /package/dist/{chunk-SEF4IOFE.js.map → chunk-SWIKABKI.js.map} +0 -0
  748. /package/dist/{chunk-JRRQ747T.js.map → chunk-TBXAY2SS.js.map} +0 -0
  749. /package/dist/{chunk-XZBXINXZ.js.map → chunk-TNSCIPBQ.js.map} +0 -0
  750. /package/dist/{chunk-S37NN23M.js.map → chunk-TVEONGBU.js.map} +0 -0
  751. /package/dist/{chunk-5RPMTFB6.js.map → chunk-TWF35IC6.js.map} +0 -0
  752. /package/dist/{chunk-BZTJEILZ.js.map → chunk-U6N5IBVY.js.map} +0 -0
  753. /package/dist/{chunk-7TUB6JWA.js.map → chunk-UCKYTA2T.js.map} +0 -0
  754. /package/dist/{chunk-PPOT6SPR.js.map → chunk-UNNPNBR2.js.map} +0 -0
  755. /package/dist/{chunk-KQAFEZQX.js.map → chunk-VDX2J7OX.js.map} +0 -0
  756. /package/dist/{chunk-J2R275FN.js.map → chunk-VVX5PZFS.js.map} +0 -0
  757. /package/dist/{chunk-ME35HVDJ.js.map → chunk-VYCOJKSX.js.map} +0 -0
  758. /package/dist/{chunk-F5UWQ4LX.js.map → chunk-W24T5YMW.js.map} +0 -0
  759. /package/dist/{chunk-HDQUQBPW.js.map → chunk-W3K3HITK.js.map} +0 -0
  760. /package/dist/{chunk-GDASG7NC.js.map → chunk-W4F3C7N7.js.map} +0 -0
  761. /package/dist/{chunk-LFK623SF.js.map → chunk-WJHGDE7X.js.map} +0 -0
  762. /package/dist/{chunk-3Q6RD76J.js.map → chunk-WQZZDN5M.js.map} +0 -0
  763. /package/dist/{chunk-ZJWDWPX6.js.map → chunk-X5K5TAQZ.js.map} +0 -0
  764. /package/dist/{chunk-ZL3XX5LW.js.map → chunk-XM24CDZX.js.map} +0 -0
  765. /package/dist/{chunk-JTWZKNUH.js.map → chunk-XOVKXJEP.js.map} +0 -0
  766. /package/dist/{chunk-X5YYSAZX.js.map → chunk-XUE46UYW.js.map} +0 -0
  767. /package/dist/{chunk-DJORULH3.js.map → chunk-YRLHEEQT.js.map} +0 -0
  768. /package/dist/{chunk-TA7UZZMW.js.map → chunk-Z3O53WZR.js.map} +0 -0
  769. /package/dist/{chunk-4LG4CPOC.js.map → chunk-Z6COGHHN.js.map} +0 -0
  770. /package/dist/{chunk-RE3H6Y2X.js.map → chunk-ZIW5JVRM.js.map} +0 -0
  771. /package/dist/{forget-2AE4MDK3.js.map → forget-SZ2BXDGF.js.map} +0 -0
  772. /package/dist/{resolution-4DLI3IOR.js.map → graph-append-error.js.map} +0 -0
@@ -0,0 +1,3542 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, mkdir, readFile, rm, writeFile, chmod } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { test, mock } from "node:test";
6
+ import { parseConfig } from "../config.js";
7
+ import { createVersion, listVersions, type VersionTrigger } from "../page-versioning.js";
8
+ import { inferIntentFromText } from "../intent.js";
9
+ import {
10
+ applySemanticMergeAtPersist,
11
+ runMergedTargetPostEffects,
12
+ type ApplySemanticMergeOptions,
13
+ } from "../orchestration/semantic-merge-persist.js";
14
+ import { buildMergedTargetPromotionPayload } from "../orchestration/semantic-merge-promotion-payload.js";
15
+ import { appendEdge, GraphIndex, type GraphEdge } from "../graph.js";
16
+ import { PersistenceIndexCoordinator } from "../orchestration/persistence-index.js";
17
+ import { createBatchPromotedCopyProbe, promotionWithholdsToolScope } from "../orchestration/extraction-persist-promotion.js";
18
+ import { persistMergedTargetThreadEpisode, persistRepairedContentHash } from "../orchestration/semantic-merge-commit-effects.js";
19
+ import { ThreadingManager } from "../threading.js";
20
+ import { withholdToolScopedFromSharedNamespace } from "../tool-scoped-memory.js";
21
+ import type { ExtractionPersistDeps } from "../orchestration/extraction-persist-deps.js";
22
+ import { StorageManager } from "../index.js";
23
+ import { sanitizeMemoryContent } from "../sanitize.js";
24
+ import { ContentHashIndex } from "../storage/content-hash-index.js";
25
+ import { initLogger, resetLogger, type LoggerBackend } from "../logger.js";
26
+ import {
27
+ DEFAULT_UNTRUSTED_ORIGINS,
28
+ renderAuthorityFence,
29
+ } from "../security/origin-authority.js";
30
+ import { renderAuthorityBoundContent } from "../recall-context-composition.js";
31
+ import type {
32
+ MemoryFile,
33
+ MemoryFrontmatter,
34
+ MemoryStatus,
35
+ MemorySubject,
36
+ ProvenanceSource,
37
+ PluginConfig,
38
+ } from "../types.js";
39
+ import { confidenceTier } from "../types.js";
40
+ import {
41
+ DEFAULT_SEMANTIC_MERGE_CANDIDATES,
42
+ DEFAULT_SEMANTIC_MERGE_MIN,
43
+ decideSemanticMerge,
44
+ MERGEABLE_MEMORY_CATEGORIES,
45
+ parseSemanticMergeConfig,
46
+ type MergeCandidate,
47
+ type MergeJudgeRawVerdict,
48
+ } from "./merge.js";
49
+ import type { SemanticDedupHit } from "./semantic.js";
50
+ import { invalidationCommitFingerprint } from "../storage/deletion-revision-store.js";
51
+
52
+ // The merge snapshot trigger must stay a literal union: a widened
53
+ // `VersionTrigger` (e.g. annotated `readonly string[]`) would let an unknown
54
+ // trigger compile and then fail `readManifest` at runtime.
55
+ // @ts-expect-error "not-a-trigger" is not a VersionTrigger
56
+ const rejectedTrigger: VersionTrigger = "not-a-trigger";
57
+ void rejectedTrigger;
58
+
59
+ // ── Fixtures (synthetic; no real paths, hosts, or memory content) ─────────────
60
+
61
+ const INCOMING = "Deploys of the billing service run on Tuesdays at 09:00 UTC.";
62
+ const EXISTING = "Billing service deploys happen on Tuesdays.";
63
+ const MERGED = "Billing service deploys run on Tuesdays at 09:00 UTC.";
64
+
65
+ const MERGE_CONFIG = {
66
+ enabled: true,
67
+ minSimilarity: 0.8,
68
+ maxCandidates: 3,
69
+ categories: ["fact", "preference"],
70
+ shadowMode: false,
71
+ } as const;
72
+
73
+ function hits(...scores: Array<[string, number]>): SemanticDedupHit[] {
74
+ return scores.map(([id, score]) => ({ id, score }));
75
+ }
76
+
77
+ function candidateResolver(
78
+ overrides: { category?: string; status?: string; content?: string } = {},
79
+ ) {
80
+ return async (memoryId: string) => ({
81
+ content: overrides.content ?? `${EXISTING} (${memoryId})`,
82
+ category: overrides.category ?? "fact",
83
+ status: overrides.status ?? "active",
84
+ });
85
+ }
86
+
87
+ const mergeJudge = async (input: {
88
+ candidates: readonly MergeCandidate[];
89
+ }): Promise<MergeJudgeRawVerdict> => ({
90
+ decision: "merge",
91
+ targetId: input.candidates[0]?.memoryId ?? null,
92
+ mergedContent: MERGED,
93
+ reason: "same deploy cadence",
94
+ });
95
+
96
+ // ── Decision: band, gates, and backend-failure taxonomy ──────────────────────
97
+
98
+ test("decideSemanticMerge: only the [minSimilarity, dedupThreshold) band merges", async () => {
99
+ for (const [score, expected] of [
100
+ [0.79, "create"],
101
+ [0.8, "merge"],
102
+ [0.91, "merge"],
103
+ [0.92, "create"],
104
+ [0.99, "create"],
105
+ ] as Array<[number, string]>) {
106
+ const decision = await decideSemanticMerge({
107
+ content: INCOMING,
108
+ category: "fact",
109
+ config: MERGE_CONFIG,
110
+ dedupThreshold: 0.92,
111
+ lookup: async () => hits(["mem-1", score]),
112
+ resolveCandidate: candidateResolver(),
113
+ judge: mergeJudge,
114
+ });
115
+ assert.equal(decision.action, expected, `score ${score}`);
116
+ }
117
+ });
118
+
119
+ test("decideSemanticMerge: maxCandidates=0 short-circuits before any lookup", async () => {
120
+ let lookups = 0;
121
+ const decision = await decideSemanticMerge({
122
+ content: INCOMING,
123
+ category: "fact",
124
+ config: { ...MERGE_CONFIG, maxCandidates: 0 },
125
+ dedupThreshold: 0.92,
126
+ lookup: async () => {
127
+ lookups++;
128
+ return hits(["mem-1", 0.85]);
129
+ },
130
+ resolveCandidate: candidateResolver(),
131
+ judge: mergeJudge,
132
+ });
133
+ assert.equal(lookups, 0, "0 must disable merging without touching the backend");
134
+ assert.deepEqual(decision, { action: "create", reason: "disabled" });
135
+ });
136
+
137
+ test("decideSemanticMerge: a lookup failure is backend_unavailable, not no_candidates", async () => {
138
+ const decision = await decideSemanticMerge({
139
+ content: INCOMING,
140
+ category: "fact",
141
+ config: MERGE_CONFIG,
142
+ dedupThreshold: 0.92,
143
+ lookup: async () => {
144
+ throw new Error("embedding backend down");
145
+ },
146
+ resolveCandidate: candidateResolver(),
147
+ judge: mergeJudge,
148
+ });
149
+ assert.deepEqual(decision, { action: "create", reason: "backend_unavailable" });
150
+ });
151
+
152
+ test("decideSemanticMerge: every non-active status is ineligible", async () => {
153
+ const inactive: MemoryStatus[] = [
154
+ "pending_review",
155
+ "rejected",
156
+ "quarantined",
157
+ "superseded",
158
+ "archived",
159
+ "forgotten",
160
+ ];
161
+ for (const status of inactive) {
162
+ const decision = await decideSemanticMerge({
163
+ content: INCOMING,
164
+ category: "fact",
165
+ config: MERGE_CONFIG,
166
+ dedupThreshold: 0.92,
167
+ lookup: async () => hits(["mem-1", 0.85]),
168
+ resolveCandidate: candidateResolver({ status }),
169
+ judge: mergeJudge,
170
+ });
171
+ assert.deepEqual(decision, { action: "create", reason: "no_candidates" }, status);
172
+ }
173
+ });
174
+
175
+ test("decideSemanticMerge: category gate and cross-category candidates create", async () => {
176
+ const offCategory = await decideSemanticMerge({
177
+ content: INCOMING,
178
+ category: "moment",
179
+ config: MERGE_CONFIG,
180
+ dedupThreshold: 0.92,
181
+ lookup: async () => hits(["mem-1", 0.85]),
182
+ resolveCandidate: candidateResolver(),
183
+ judge: mergeJudge,
184
+ });
185
+ assert.deepEqual(offCategory, { action: "create", reason: "disabled" });
186
+
187
+ const mismatched = await decideSemanticMerge({
188
+ content: INCOMING,
189
+ category: "fact",
190
+ config: MERGE_CONFIG,
191
+ dedupThreshold: 0.92,
192
+ lookup: async () => hits(["mem-1", 0.85]),
193
+ resolveCandidate: candidateResolver({ category: "preference" }),
194
+ judge: mergeJudge,
195
+ });
196
+ assert.deepEqual(mismatched, { action: "create", reason: "no_candidates" });
197
+ });
198
+
199
+ test("decideSemanticMerge: fabricated ids, bad content, and refusals all create", async () => {
200
+ const base = {
201
+ content: INCOMING,
202
+ category: "fact",
203
+ config: MERGE_CONFIG,
204
+ dedupThreshold: 0.92,
205
+ lookup: async () => hits(["mem-1", 0.85]),
206
+ resolveCandidate: candidateResolver(),
207
+ };
208
+ const cases: Array<[MergeJudgeRawVerdict, string]> = [
209
+ [{ decision: "merge", targetId: "mem-not-a-candidate", mergedContent: MERGED, reason: "x" }, "judge_invalid"],
210
+ [{ decision: "merge", targetId: "mem-1", mergedContent: " ", reason: "x" }, "judge_invalid"],
211
+ [{ decision: "merge", targetId: "mem-1", mergedContent: "z".repeat(100_000), reason: "x" }, "judge_invalid"],
212
+ [{ decision: "contradicts", reason: "state changed" }, "judge_contradicts"],
213
+ [{ decision: "create", reason: "different concept" }, "judge_create"],
214
+ ];
215
+ for (const [verdict, reason] of cases) {
216
+ const decision = await decideSemanticMerge({ ...base, judge: async () => verdict });
217
+ assert.deepEqual(decision, { action: "create", reason }, reason);
218
+ }
219
+ const thrown = await decideSemanticMerge({
220
+ ...base,
221
+ judge: async () => {
222
+ throw new Error("timeout");
223
+ },
224
+ });
225
+ assert.deepEqual(thrown, { action: "create", reason: "judge_error" });
226
+ });
227
+
228
+ test("decideSemanticMerge: highest-similarity candidate wins with a stable tiebreak", async () => {
229
+ const seen: string[] = [];
230
+ const decision = await decideSemanticMerge({
231
+ content: INCOMING,
232
+ category: "fact",
233
+ config: MERGE_CONFIG,
234
+ dedupThreshold: 0.92,
235
+ lookup: async () => hits(["mem-b", 0.84], ["mem-c", 0.9], ["mem-a", 0.9]),
236
+ resolveCandidate: candidateResolver(),
237
+ judge: async (input) => {
238
+ seen.push(...input.candidates.map((c) => c.memoryId));
239
+ return { decision: "merge", targetId: input.candidates[0].memoryId, mergedContent: MERGED, reason: "x" };
240
+ },
241
+ });
242
+ assert.deepEqual(seen, ["mem-a", "mem-c", "mem-b"]);
243
+ assert.equal(decision.action === "merge" && decision.targetId, "mem-a");
244
+ });
245
+
246
+ // Round N+17 (A): eligible-first candidate truncation.
247
+ test("decideSemanticMerge: ineligible hits ranked above a valid target never crowd it out", async () => {
248
+ // The index truncates at the requested limit, exactly like the real
249
+ // embedding backend. maxCandidates=1 with the raw top-of-index hits being
250
+ // ineligible (inactive, foreign connector, off-category): pre-fix, the
251
+ // fetch asked for maxCandidates hits, the filters removed them all, and
252
+ // the valid target ranked immediately below never reached the judge.
253
+ const ALL_HITS: SemanticDedupHit[] = [
254
+ { id: "stale-target", score: 0.9 },
255
+ { id: "foreign-connector-target", score: 0.89, sourceConnector: "connector-a" },
256
+ { id: "off-category-target", score: 0.88 },
257
+ { id: "valid-target", score: 0.85 },
258
+ ];
259
+ const offered: string[] = [];
260
+ let lookupLimit = 0;
261
+ const decision = await decideSemanticMerge({
262
+ content: INCOMING,
263
+ category: "fact",
264
+ config: { ...MERGE_CONFIG, maxCandidates: 1 },
265
+ dedupThreshold: 0.92,
266
+ lookup: async (_content, limit) => {
267
+ lookupLimit = limit;
268
+ return ALL_HITS.slice(0, limit);
269
+ },
270
+ resolveCandidate: async (memoryId) => ({
271
+ content: `${EXISTING} (${memoryId})`,
272
+ category: memoryId === "off-category-target" ? "preference" : "fact",
273
+ status: memoryId === "stale-target" ? "superseded" : "active",
274
+ }),
275
+ judge: async (input) => {
276
+ offered.push(...input.candidates.map((c) => c.memoryId));
277
+ return {
278
+ decision: "merge",
279
+ targetId: input.candidates[0]?.memoryId ?? null,
280
+ mergedContent: MERGED,
281
+ reason: "same deploy cadence",
282
+ };
283
+ },
284
+ });
285
+ assert.ok(lookupLimit > 1, "the fetch must overshoot maxCandidates so filtering precedes truncation");
286
+ assert.deepEqual(
287
+ offered,
288
+ ["valid-target"],
289
+ "the judge must receive the first ELIGIBLE neighbor, not the raw top-of-index hit",
290
+ );
291
+ assert.equal(decision.action, "merge");
292
+ assert.equal(decision.action === "merge" && decision.targetId, "valid-target");
293
+ });
294
+
295
+ // ── Config parsing ───────────────────────────────────────────────────────────
296
+
297
+ test("parseSemanticMergeConfig: defaults are off with a valid band", () => {
298
+ const { semanticMerge } = parseConfig({});
299
+ assert.equal(semanticMerge.enabled, false);
300
+ assert.equal(semanticMerge.minSimilarity, DEFAULT_SEMANTIC_MERGE_MIN);
301
+ assert.equal(semanticMerge.maxCandidates, DEFAULT_SEMANTIC_MERGE_CANDIDATES);
302
+ assert.equal(semanticMerge.shadowMode, false);
303
+ assert.deepEqual(semanticMerge.categories, [
304
+ "fact",
305
+ "preference",
306
+ "decision",
307
+ "relationship",
308
+ "skill",
309
+ ]);
310
+ });
311
+
312
+ test("parseSemanticMergeConfig: CLI strings coerce, and 0 stays 0", () => {
313
+ const { semanticMerge } = parseSemanticMergeConfig({
314
+ semanticMerge: { enabled: "false", shadowMode: "true", maxCandidates: "0" },
315
+ });
316
+ assert.equal(semanticMerge.enabled, false);
317
+ assert.equal(semanticMerge.shadowMode, true);
318
+ assert.equal(semanticMerge.maxCandidates, 0);
319
+ const enabled = parseSemanticMergeConfig({ semanticMerge: { enabled: "1" } });
320
+ assert.equal(enabled.semanticMerge.enabled, true);
321
+ });
322
+
323
+ test("parseSemanticMergeConfig: an empty or inverted band is rejected", () => {
324
+ assert.throws(
325
+ () => parseSemanticMergeConfig({ semanticMerge: { minSimilarity: 0.92 } }),
326
+ /strictly below semanticDedupThreshold/,
327
+ );
328
+ assert.throws(
329
+ () => parseSemanticMergeConfig({ semanticMerge: { minSimilarity: 0.95 }, semanticDedupThreshold: 0.9 }),
330
+ /strictly below semanticDedupThreshold/,
331
+ );
332
+ assert.throws(
333
+ () => parseSemanticMergeConfig({ semanticMerge: { enabled: "sometimes" } }),
334
+ /semanticMerge.enabled must be a boolean/,
335
+ );
336
+ assert.throws(
337
+ () => parseSemanticMergeConfig({ semanticMerge: { maxCandidates: -1 } }),
338
+ /maxCandidates must be an integer/,
339
+ );
340
+ });
341
+
342
+ test("parseSemanticMergeConfig: non-integer maxCandidates is rejected, not floored", () => {
343
+ for (const bad of [0.5, 3.7, "2.5"]) {
344
+ assert.throws(
345
+ () => parseSemanticMergeConfig({ semanticMerge: { maxCandidates: bad } }),
346
+ /maxCandidates must be an integer/,
347
+ String(bad),
348
+ );
349
+ }
350
+ // 0 stays the documented disable value, CLI string form included.
351
+ assert.equal(
352
+ parseSemanticMergeConfig({ semanticMerge: { maxCandidates: 0 } }).semanticMerge.maxCandidates,
353
+ 0,
354
+ );
355
+ assert.equal(
356
+ parseSemanticMergeConfig({ semanticMerge: { maxCandidates: "0" } }).semanticMerge.maxCandidates,
357
+ 0,
358
+ );
359
+ });
360
+ // #2330 round N+8 (P2-D, checklist #46): presence is OWN-property presence.
361
+ // A composed programmatic config can carry `semanticMerge: { enabled: undefined }`
362
+ // — present-but-invalid — and an object built with Object.create can satisfy a
363
+ // bracket read through its prototype. Both must be told apart from an absent
364
+ // key: absent defaults, present-but-invalid throws, inherited never applies.
365
+ test("parseSemanticMergeConfig: present-but-undefined keys throw instead of silently defaulting (P2-D)", () => {
366
+ assert.throws(
367
+ () => parseSemanticMergeConfig({ semanticMerge: { enabled: undefined } }),
368
+ /semanticMerge\.enabled/,
369
+ );
370
+ assert.throws(
371
+ () => parseSemanticMergeConfig({ semanticMerge: { shadowMode: undefined } }),
372
+ /semanticMerge\.shadowMode/,
373
+ );
374
+ assert.throws(
375
+ () => parseSemanticMergeConfig({ semanticMerge: { maxCandidates: undefined } }),
376
+ /maxCandidates/,
377
+ );
378
+ assert.throws(
379
+ () => parseSemanticMergeConfig({ semanticMerge: { minSimilarity: undefined } }),
380
+ /minScore/,
381
+ );
382
+ assert.throws(
383
+ () => parseSemanticMergeConfig({ semanticMerge: { categories: undefined } }),
384
+ /categories must be an array/,
385
+ );
386
+ assert.throws(
387
+ () => parseSemanticMergeConfig({ semanticMerge: undefined }),
388
+ /semanticMerge must be an object/,
389
+ );
390
+ });
391
+
392
+ test("parseSemanticMergeConfig: inherited keys are ignored — own-property presence decides (P2-D)", () => {
393
+ const inheritedFields = parseSemanticMergeConfig({
394
+ semanticMerge: Object.create({
395
+ enabled: true,
396
+ shadowMode: true,
397
+ maxCandidates: 9,
398
+ minSimilarity: 0.9,
399
+ categories: ["fact"],
400
+ }),
401
+ });
402
+ assert.equal(inheritedFields.semanticMerge.enabled, false);
403
+ assert.equal(inheritedFields.semanticMerge.shadowMode, false);
404
+ assert.equal(
405
+ inheritedFields.semanticMerge.maxCandidates,
406
+ DEFAULT_SEMANTIC_MERGE_CANDIDATES,
407
+ );
408
+ assert.equal(inheritedFields.semanticMerge.minSimilarity, DEFAULT_SEMANTIC_MERGE_MIN);
409
+ assert.deepEqual(inheritedFields.semanticMerge.categories, [
410
+ "fact",
411
+ "preference",
412
+ "decision",
413
+ "relationship",
414
+ "skill",
415
+ ]);
416
+ // An inherited semanticMerge BLOCK on the config object is equally absent.
417
+ const inheritedBlock = parseSemanticMergeConfig(
418
+ Object.create({ semanticMerge: { enabled: true } }) as Record<string, unknown>,
419
+ );
420
+ assert.equal(inheritedBlock.semanticMerge.enabled, false);
421
+ });
422
+
423
+ test("parseSemanticMergeConfig: a present non-object block is rejected; only absent means defaults", () => {
424
+ for (const bad of [true, "enabled", null, 1, ["fact"]]) {
425
+ assert.throws(
426
+ () => parseSemanticMergeConfig({ semanticMerge: bad }),
427
+ /semanticMerge must be an object/,
428
+ JSON.stringify(bad),
429
+ );
430
+ }
431
+ const defaults = parseSemanticMergeConfig({});
432
+ assert.equal(defaults.semanticMerge.enabled, false);
433
+ assert.equal(defaults.semanticMerge.maxCandidates, DEFAULT_SEMANTIC_MERGE_CANDIDATES);
434
+ });
435
+
436
+ test("parseSemanticMergeConfig: malformed categories are rejected, not silently defaulted", () => {
437
+ for (const bad of ["fact", ["fact", 7], ["fact", ""], {}]) {
438
+ assert.throws(
439
+ () => parseSemanticMergeConfig({ semanticMerge: { categories: bad } }),
440
+ /categories must be an array/,
441
+ JSON.stringify(bad),
442
+ );
443
+ }
444
+ assert.deepEqual(
445
+ parseSemanticMergeConfig({ semanticMerge: { categories: ["preference"] } }).semanticMerge
446
+ .categories,
447
+ ["preference"],
448
+ );
449
+ });
450
+
451
+ // ── Persistence ──────────────────────────────────────────────────────────────
452
+
453
+ interface MergeHarness {
454
+ deps: ExtractionPersistDeps;
455
+ storage: StorageManager;
456
+ target: MemoryFile;
457
+ /** Simulate another writer committing a new body to the target. */
458
+ setTargetContent: (content: string) => Promise<void>;
459
+ calls: {
460
+ contentUpdates: Array<{ id: string; content: string; actor?: string }>;
461
+ frontmatterPatches: Array<{ id: string; patch: Partial<MemoryFrontmatter>; actor?: string }>;
462
+ hashRemovals: string[];
463
+ hashAdds: string[];
464
+ hashRegistrations: Array<{ id: string; hash: string }>;
465
+ reindexed: string[];
466
+ lookupStorages: string[];
467
+ };
468
+ }
469
+
470
+ const TARGET_SOURCE: ProvenanceSource = {
471
+ sessionKey: "project/example/2026-08-20T00:00:00.000Z",
472
+ observedAt: "2026-08-20T00:00:00.000Z",
473
+ quote: "deploys happen on Tuesdays",
474
+ };
475
+
476
+ const INCOMING_SOURCE: ProvenanceSource = {
477
+ sessionKey: "project/example/2026-08-21T00:00:00.000Z",
478
+ observedAt: "2026-08-21T00:00:00.000Z",
479
+ quote: "deploys run at 09:00 UTC",
480
+ };
481
+
482
+ async function harness(
483
+ overrides: {
484
+ config?: Partial<Record<string, unknown>>;
485
+ targetCategory?: string;
486
+ targetStatus?: MemoryStatus;
487
+ /** Stamp `toolScoped: true` on the target's frontmatter (finding A). */
488
+ targetToolScoped?: boolean;
489
+ /** Stamp `subject` on the target's frontmatter (finding A). */
490
+ targetSubject?: MemorySubject;
491
+ /** Stamp `origin` on the target's frontmatter (finding A). */
492
+ targetOrigin?: string;
493
+ /** Stamp `confidence`/`confidenceTier` on the target's frontmatter (final round A). */
494
+ targetConfidence?: number;
495
+ /** Stamp `provenance` on the target's frontmatter (finding B). */
496
+ targetProvenance?: "verified" | "unverified" | "none";
497
+ /** Stamp `faithfulness` on the target's frontmatter (finding C). */
498
+ targetFaithfulness?: { verdict: "entailed" | "contradicted" | "unsupported" | "unchecked" | "skipped_no_span" };
499
+ /** Stamp `sourceConnector` on the target's frontmatter (finding D). */
500
+ targetSourceConnector?: string;
501
+ /** Stamp `valid_at` on the target's frontmatter (round N+2 A). */
502
+ targetValidAt?: string;
503
+ /** Stamp `invalid_at` on the target's frontmatter (round N+2 A). */
504
+ targetInvalidAt?: string;
505
+ /** Stamp `memoryKind` on the target's frontmatter (round N+2 B). */
506
+ targetMemoryKind?: MemoryFrontmatter["memoryKind"];
507
+ /** Stamp stale intent routing fields on the target's frontmatter (round N+3 A). */
508
+ targetIntent?: { goal: string; actionType: string; entityTypes: string[] };
509
+ /** Top-level parseConfig overrides (citation enablement, round N+2 C). */
510
+ topLevelConfig?: Record<string, unknown>;
511
+ lookupHits?: SemanticDedupHit[];
512
+ mutateOnWrite?: string;
513
+ /**
514
+ * Simulate a concurrent writer replacing the target between the content
515
+ * commit and the provenance patch — the window the id-keyed patch left
516
+ * open.
517
+ */
518
+ mutateAtPatch?: string;
519
+ /** Force the frontmatter patch to fail after the content update commits. */
520
+ frontmatterFails?: boolean;
521
+ /** Force the automatic rollback of that committed content to fail. */
522
+ rollbackFails?: boolean;
523
+ } = {},
524
+ ): Promise<MergeHarness> {
525
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-"));
526
+ const factsDir = path.join(dir, "facts", "2026-08-20");
527
+ await mkdir(factsDir, { recursive: true });
528
+ const targetPath = path.join(factsDir, "fact-target.md");
529
+ const targetCategory = overrides.targetCategory ?? "fact";
530
+ const frontmatter = {
531
+ id: "fact-target",
532
+ category: targetCategory,
533
+ ...(overrides.targetStatus ? { status: overrides.targetStatus } : {}),
534
+ ...(overrides.targetToolScoped ? { toolScoped: true as const } : {}),
535
+ ...(overrides.targetSubject ? { subject: overrides.targetSubject } : {}),
536
+ ...(overrides.targetOrigin ? { origin: overrides.targetOrigin } : {}),
537
+ ...(overrides.targetProvenance ? { provenance: overrides.targetProvenance } : {}),
538
+ ...(overrides.targetConfidence !== undefined
539
+ ? {
540
+ confidence: overrides.targetConfidence,
541
+ confidenceTier: confidenceTier(overrides.targetConfidence),
542
+ }
543
+ : {}),
544
+ ...(overrides.targetFaithfulness ? { faithfulness: overrides.targetFaithfulness } : {}),
545
+ ...(overrides.targetSourceConnector ? { sourceConnector: overrides.targetSourceConnector } : {}),
546
+ ...(overrides.targetValidAt ? { valid_at: overrides.targetValidAt } : {}),
547
+ ...(overrides.targetInvalidAt ? { invalid_at: overrides.targetInvalidAt } : {}),
548
+ ...(overrides.targetMemoryKind ? { memoryKind: overrides.targetMemoryKind } : {}),
549
+ ...(overrides.targetIntent
550
+ ? {
551
+ intentGoal: overrides.targetIntent.goal,
552
+ intentActionType: overrides.targetIntent.actionType,
553
+ intentEntityTypes: overrides.targetIntent.entityTypes,
554
+ }
555
+ : {}),
556
+ sources: [TARGET_SOURCE],
557
+ } as unknown as MemoryFrontmatter;
558
+ await writeFile(targetPath, `---\nid: fact-target\ncategory: fact\n---\n\n${EXISTING}\n`, "utf8");
559
+ const target: MemoryFile = { path: targetPath, frontmatter, content: EXISTING };
560
+ // Live state: the CAS compares against what is on disk NOW, so a stub that
561
+ // returned the original snapshot forever could never fail a compare.
562
+ let state: MemoryFile = { ...target };
563
+
564
+ const calls: MergeHarness["calls"] = {
565
+ contentUpdates: [],
566
+ frontmatterPatches: [],
567
+ hashRemovals: [],
568
+ hashAdds: [],
569
+ hashRegistrations: [],
570
+ reindexed: [],
571
+ lookupStorages: [],
572
+ };
573
+ const commit = async (content: string): Promise<void> => {
574
+ state = { ...state, content };
575
+ await writeFile(targetPath, `---\nid: fact-target\ncategory: fact\n---\n\n${content}\n`, "utf8");
576
+ };
577
+ // Live state: reads come off the file, so a stub can neither invent a
578
+ // restore that never touched storage nor compare a snapshot against itself.
579
+ const read = async (): Promise<MemoryFile> => {
580
+ const raw = await readFile(targetPath, "utf8");
581
+ const body = raw.slice(raw.indexOf("---", 3) + 4).trim();
582
+ return { path: targetPath, frontmatter: state.frontmatter, content: body };
583
+ };
584
+ const unchanged = async (expected: MemoryFile): Promise<boolean> => {
585
+ const current = await read();
586
+ // The production compare, not a content-only approximation (checklist #21).
587
+ return invalidationCommitFingerprint(current) === invalidationCommitFingerprint(expected);
588
+ };
589
+ const storage = {
590
+ dir,
591
+ getMemoryByIdIncludingArchived: async (id: string) =>
592
+ id === state.frontmatter.id ? await read() : null,
593
+ readMemoryByPath: async (p: string) => (p === targetPath ? await read() : null),
594
+ // The id-keyed APIs: they re-read and overwrite/stamp whatever is there
595
+ // now. Present so that a revert to either unsafe call fails on the
596
+ // assertions below rather than on a missing stub method.
597
+ updateMemory: async (id: string, content: string, options?: { actor?: string }) => {
598
+ if (id !== state.frontmatter.id) return false;
599
+ if (overrides.mutateOnWrite !== undefined && options?.actor === "semantic-merge") {
600
+ await commit(overrides.mutateOnWrite);
601
+ }
602
+ calls.contentUpdates.push({ id, content, actor: options?.actor });
603
+ await commit(content);
604
+ return true;
605
+ },
606
+ updateMemoryFrontmatter: async (id: string, patch: Partial<MemoryFrontmatter>) => {
607
+ calls.frontmatterPatches.push({ id, patch });
608
+ return overrides.frontmatterFails !== true;
609
+ },
610
+ // Signature-faithful to StorageManager.updateMemoryIfUnchanged: the
611
+ // compare is on the caller's snapshot, not on the id.
612
+ updateMemoryIfUnchanged: async (
613
+ expected: MemoryFile,
614
+ content: string,
615
+ options?: { actor?: string },
616
+ ) => {
617
+ if (overrides.rollbackFails && options?.actor === "semantic-merge-rollback") return false;
618
+ if (overrides.mutateOnWrite !== undefined && options?.actor === "semantic-merge") {
619
+ await commit(overrides.mutateOnWrite);
620
+ }
621
+ if (!(await unchanged(expected))) return false;
622
+ calls.contentUpdates.push({ id: expected.frontmatter.id, content, actor: options?.actor });
623
+ await commit(content);
624
+ return true;
625
+ },
626
+ // Signature-faithful to StorageManager.writeMemoryFrontmatterIfUnchanged.
627
+ writeMemoryFrontmatterIfUnchanged: async (
628
+ expected: MemoryFile,
629
+ patch: Partial<MemoryFrontmatter>,
630
+ options?: { actor?: string },
631
+ ) => {
632
+ // A concurrent writer inside the patch window: after the caller's
633
+ // verifying read, before storage takes its own lock.
634
+ if (overrides.mutateAtPatch !== undefined) await commit(overrides.mutateAtPatch);
635
+ if (!(await unchanged(expected))) return false;
636
+ calls.frontmatterPatches.push({ id: expected.frontmatter.id, patch, actor: options?.actor });
637
+ return overrides.frontmatterFails !== true;
638
+ },
639
+ removeFactContentHashesForMemories: async (memories: MemoryFile[]) => {
640
+ calls.hashRemovals.push(...memories.map((m) => m.content));
641
+ },
642
+ restoreFactHashAfterApproval: async (id: string) => {
643
+ calls.hashAdds.push(id);
644
+ },
645
+ registerFactContentHash: async (id: string, hash: string, _expectedContent: string) => {
646
+ calls.hashRegistrations.push({ id, hash });
647
+ },
648
+ } as unknown as StorageManager;
649
+
650
+ const config = parseConfig({
651
+ memoryDir: dir,
652
+ versioningEnabled: true,
653
+ ...(overrides.topLevelConfig ?? {}),
654
+ semanticMerge: { enabled: true, ...(overrides.config ?? {}) },
655
+ }) as PluginConfig;
656
+
657
+ const deps = {
658
+ config,
659
+ getLocalLlm: () => null,
660
+ semanticDedupLookup: async (_content: string, _limit: number, targetStorage: StorageManager) => {
661
+ calls.lookupStorages.push(targetStorage.dir);
662
+ return overrides.lookupHits ?? hits(["fact-target", 0.85]);
663
+ },
664
+ indexPersistedMemory: async (_storage: StorageManager, memoryId: string) => {
665
+ calls.reindexed.push(memoryId);
666
+ },
667
+ } as unknown as ExtractionPersistDeps;
668
+
669
+ return { deps, storage, target, setTargetContent: commit, calls };
670
+ }
671
+
672
+ const acceptingJudge = async (input: {
673
+ candidates: readonly MergeCandidate[];
674
+ }): Promise<MergeJudgeRawVerdict> => ({
675
+ decision: "merge",
676
+ targetId: input.candidates[0]?.memoryId ?? null,
677
+ mergedContent: MERGED,
678
+ reason: "same underlying deploy cadence",
679
+ });
680
+
681
+ test("applySemanticMergeAtPersist: merges in place with snapshot, provenance, hash resync, and reindex", async () => {
682
+ const h = await harness();
683
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
684
+ storage: h.storage,
685
+ content: INCOMING,
686
+ category: "fact",
687
+ sources: [INCOMING_SOURCE],
688
+ judgeCall: (options) => acceptingJudge(options),
689
+ });
690
+
691
+ assert.deepEqual(outcome, { action: "merged", targetId: "fact-target", mergedContent: MERGED, provenancePatched: true });
692
+ // Same id, same file path — an update, never a new fragment.
693
+ assert.deepEqual(h.calls.contentUpdates, [
694
+ { id: "fact-target", content: MERGED, actor: "semantic-merge" },
695
+ ]);
696
+ assert.equal(await readFile(h.target.path, "utf8").then((t) => t.includes(MERGED)), true);
697
+
698
+ // Rollback data exists under the dedicated trigger.
699
+ const history = await listVersions(h.target.path, { enabled: true, maxVersionsPerPage: 20, sidecarDir: ".versions" }, h.storage.dir);
700
+ assert.equal(history.versions.length, 1);
701
+ assert.equal(history.versions[0].trigger, "semantic-merge");
702
+
703
+ // Provenance is appended, never replaced, and the merge is stamped.
704
+ assert.equal(h.calls.frontmatterPatches.length, 1);
705
+ const patch = h.calls.frontmatterPatches[0].patch;
706
+ assert.equal(patch.derived_via, "merge");
707
+ assert.equal(patch.reinforcement_count, 1);
708
+ assert.equal(patch.sources?.length, 2);
709
+ assert.equal(patch.sources?.[1].quote, INCOMING_SOURCE.quote);
710
+
711
+ // Hash index: the pre-merge form is removed, the target re-registered.
712
+ assert.deepEqual(h.calls.hashRemovals, [EXISTING]);
713
+ assert.deepEqual(h.calls.hashAdds, ["fact-target"]);
714
+ assert.deepEqual(h.calls.reindexed, ["fact-target"]);
715
+ // Candidates come from the write's own namespace-scoped storage.
716
+ assert.deepEqual(h.calls.lookupStorages, [h.storage.dir]);
717
+ });
718
+
719
+ // #2330 round N+10 (B): the merge commits into a target living under
720
+ // `cold/` and then hands the target to indexPersistedMemory for the
721
+ // embedding-fallback refresh. PersistenceIndexCoordinator used to resolve
722
+ // the id with the HOT-only getMemoryById, found nothing, and returned — the
723
+ // fallback index kept serving the pre-merge text. This drives the REAL
724
+ // coordinator against a REAL cold-tier file.
725
+ test("applySemanticMergeAtPersist: a cold-tier target merge refreshes the embedding fallback index (round N+10 B)", async () => {
726
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-cold-idx-"));
727
+ try {
728
+ const storage = new StorageManager(dir);
729
+ await storage.ensureDirectories();
730
+ // Seed straight into cold/ at the tier layout buildTierMemoryPath uses
731
+ // (cold/<category>/<created-date>/<id>.md) — a long-lived merge target
732
+ // that was demoted to the cold tier.
733
+ const targetPath = path.join(dir, "cold", "facts", "2026-08-20", "fact-target.md");
734
+ await mkdir(path.dirname(targetPath), { recursive: true });
735
+ await writeFile(
736
+ targetPath,
737
+ [
738
+ "---",
739
+ "id: fact-target",
740
+ "category: fact",
741
+ "created: 2026-08-20T00:00:00.000Z",
742
+ "updated: 2026-08-20T00:00:00.000Z",
743
+ "status: active",
744
+ "---",
745
+ "",
746
+ EXISTING,
747
+ "",
748
+ ].join("\n"),
749
+ "utf8",
750
+ );
751
+ assert.ok(
752
+ await storage.getMemoryByIdIncludingArchived("fact-target"),
753
+ "the cold-tier target must resolve through the cold-aware lookup",
754
+ );
755
+ const indexed: Array<{ id: string; content: string; path: string }> = [];
756
+ const config = parseConfig({
757
+ memoryDir: dir,
758
+ versioningEnabled: true,
759
+ embeddingFallbackEnabled: true,
760
+ semanticMerge: { enabled: true },
761
+ });
762
+ const coordinator = new PersistenceIndexCoordinator({
763
+ config,
764
+ embeddingFallback: {
765
+ isAvailable: async () => true,
766
+ indexFile: async (id: string, content: string, p: string) => {
767
+ indexed.push({ id, content, path: p });
768
+ },
769
+ removeFromIndex: async () => {},
770
+ },
771
+ } as unknown as ConstructorParameters<typeof PersistenceIndexCoordinator>[0]);
772
+ const deps = {
773
+ config,
774
+ getLocalLlm: () => null,
775
+ semanticDedupLookup: async () => [{ id: "fact-target", score: 0.85 }],
776
+ indexPersistedMemory: (s: StorageManager, id: string) =>
777
+ coordinator.indexPersistedMemory(s, id),
778
+ } as unknown as ExtractionPersistDeps;
779
+
780
+ const outcome = await applySemanticMergeAtPersist(deps, {
781
+ storage,
782
+ content: INCOMING,
783
+ category: "fact",
784
+ sources: [INCOMING_SOURCE],
785
+ judgeCall: (options) => acceptingJudge(options),
786
+ });
787
+ assert.equal(outcome.action, "merged", `merge outcome: ${JSON.stringify(outcome)}`);
788
+
789
+ assert.deepEqual(
790
+ indexed.map((entry) => ({ id: entry.id, content: entry.content })),
791
+ [{ id: "fact-target", content: MERGED }],
792
+ `the fallback index must serve the merged text for the cold-tier target (got ${JSON.stringify(indexed)})`,
793
+ );
794
+ assert.ok(
795
+ indexed[0]?.path.includes(path.join("cold")),
796
+ `the indexed path must be the target's cold-tier location (got ${indexed[0]?.path})`,
797
+ );
798
+ } finally {
799
+ await StorageManager.clearAllStaticCaches();
800
+ }
801
+ });
802
+
803
+ test("applySemanticMergeAtPersist: intent routing recomputes intent fields from the committed merged body", async () => {
804
+ const h = await harness({
805
+ targetIntent: { goal: "close_deal", actionType: "summarize", entityTypes: ["client"] },
806
+ topLevelConfig: { intentRoutingEnabled: true },
807
+ });
808
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
809
+ storage: h.storage,
810
+ content: INCOMING,
811
+ category: "fact",
812
+ sources: [INCOMING_SOURCE],
813
+ judgeCall: (options) => acceptingJudge(options),
814
+ });
815
+ assert.equal(outcome.action, "merged");
816
+ const patch = h.calls.frontmatterPatches.at(-1)?.patch;
817
+ assert.ok(patch, "the merge must land a conditional frontmatter patch");
818
+ // The recomputation the ordinary write path runs over the same body:
819
+ // category + tags + RAW pre-citation content of the committed record.
820
+ const expected = inferIntentFromText(`fact ${""} ${MERGED}`);
821
+ assert.equal(patch.intentGoal, expected.goal);
822
+ assert.equal(patch.intentActionType, expected.actionType);
823
+ assert.deepEqual(patch.intentEntityTypes, expected.entityTypes);
824
+ // Concretely: the merged body's "deploys" cue routes to release, and the
825
+ // target's stale client-deal routing is replaced, not retained.
826
+ assert.equal(patch.intentGoal, "release");
827
+ assert.equal(patch.intentActionType, "unknown");
828
+ assert.deepEqual(patch.intentEntityTypes, []);
829
+ });
830
+
831
+ test("applySemanticMergeAtPersist: intent routing off leaves intent fields untouched", async () => {
832
+ const h = await harness({
833
+ targetIntent: { goal: "close_deal", actionType: "summarize", entityTypes: ["client"] },
834
+ });
835
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
836
+ storage: h.storage,
837
+ content: INCOMING,
838
+ category: "fact",
839
+ sources: [INCOMING_SOURCE],
840
+ judgeCall: (options) => acceptingJudge(options),
841
+ });
842
+ assert.equal(outcome.action, "merged");
843
+ const patch = h.calls.frontmatterPatches.at(-1)?.patch;
844
+ assert.ok(patch);
845
+ assert.equal("intentGoal" in patch, false);
846
+ assert.equal("intentActionType" in patch, false);
847
+ assert.equal("intentEntityTypes" in patch, false);
848
+ });
849
+
850
+ test("applySemanticMergeAtPersist: shadow mode decides but never mutates", async () => {
851
+ const h = await harness({ config: { shadowMode: true } });
852
+ const before = await readFile(h.target.path, "utf8");
853
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
854
+ storage: h.storage,
855
+ content: INCOMING,
856
+ category: "fact",
857
+ sources: [INCOMING_SOURCE],
858
+ judgeCall: (options) => acceptingJudge(options),
859
+ });
860
+ assert.deepEqual(outcome, { action: "created", reason: "shadow_would_merge" });
861
+ assert.equal(await readFile(h.target.path, "utf8"), before);
862
+ assert.deepEqual(h.calls.contentUpdates, []);
863
+ assert.deepEqual(h.calls.frontmatterPatches, []);
864
+ const history = await listVersions(h.target.path, { enabled: true, maxVersionsPerPage: 20, sidecarDir: ".versions" }, h.storage.dir);
865
+ assert.deepEqual(history.versions, []);
866
+ });
867
+
868
+ test("applySemanticMergeAtPersist: disabled config runs no lookup and no judge", async () => {
869
+ const h = await harness({ config: { enabled: false } });
870
+ let judged = 0;
871
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
872
+ storage: h.storage,
873
+ content: INCOMING,
874
+ category: "fact",
875
+ judgeCall: async (options) => {
876
+ judged++;
877
+ return acceptingJudge(options);
878
+ },
879
+ });
880
+ assert.deepEqual(outcome, { action: "created", reason: "disabled" });
881
+ assert.deepEqual(h.calls.lookupStorages, []);
882
+ assert.equal(judged, 0);
883
+ });
884
+
885
+ test("applySemanticMergeAtPersist: an inactive target and a caller skip both create", async () => {
886
+ const inactive = await harness({ targetStatus: "superseded" });
887
+ const inactiveOutcome = await applySemanticMergeAtPersist(inactive.deps, {
888
+ storage: inactive.storage,
889
+ content: INCOMING,
890
+ category: "fact",
891
+ judgeCall: (options) => acceptingJudge(options),
892
+ });
893
+ assert.equal(inactiveOutcome.action, "created");
894
+ assert.deepEqual(inactive.calls.contentUpdates, []);
895
+
896
+ const skipped = await harness();
897
+ const skippedOutcome = await applySemanticMergeAtPersist(skipped.deps, {
898
+ storage: skipped.storage,
899
+ content: INCOMING,
900
+ category: "fact",
901
+ skip: true,
902
+ judgeCall: (options) => acceptingJudge(options),
903
+ });
904
+ assert.deepEqual(skippedOutcome, { action: "created", reason: "disabled" });
905
+ assert.deepEqual(skipped.calls.lookupStorages, []);
906
+ });
907
+
908
+ test("applySemanticMergeAtPersist: versioning off refuses to merge (no rollback data)", async () => {
909
+ const h = await harness();
910
+ const noVersioning = {
911
+ ...h.deps,
912
+ config: { ...h.deps.config, versioningEnabled: false },
913
+ } as unknown as ExtractionPersistDeps;
914
+ const outcome = await applySemanticMergeAtPersist(noVersioning, {
915
+ storage: h.storage,
916
+ content: INCOMING,
917
+ category: "fact",
918
+ judgeCall: (options) => acceptingJudge(options),
919
+ });
920
+ assert.deepEqual(outcome, { action: "created", reason: "snapshot_unavailable" });
921
+ assert.deepEqual(h.calls.contentUpdates, []);
922
+ });
923
+
924
+ // ── Finding 1: connector boundaries survive a merge ──────────────────────────
925
+
926
+ test("decideSemanticMerge: a foreign-connector neighbor is never a merge target", async () => {
927
+ const base = {
928
+ content: INCOMING,
929
+ category: "fact",
930
+ config: MERGE_CONFIG,
931
+ dedupThreshold: 0.92,
932
+ resolveCandidate: candidateResolver(),
933
+ judge: mergeJudge,
934
+ };
935
+ // Connector B's fact must not rewrite connector A's memory: A's
936
+ // `sourceConnector` frontmatter would still name A after the merge.
937
+ const foreign = await decideSemanticMerge({
938
+ ...base,
939
+ sourceConnector: "connector-b",
940
+ lookup: async () => [{ id: "mem-a", score: 0.85, sourceConnector: "connector-a" }],
941
+ });
942
+ assert.deepEqual(foreign, { action: "create", reason: "no_candidates" });
943
+
944
+ // An unattributed neighbor is equally ineligible for a scoped candidate.
945
+ const unattributed = await decideSemanticMerge({
946
+ ...base,
947
+ sourceConnector: "connector-b",
948
+ lookup: async () => hits(["mem-operator", 0.85]),
949
+ });
950
+ assert.deepEqual(unattributed, { action: "create", reason: "no_candidates" });
951
+
952
+ // Same connector merges, and a whitespace-only scope is "unattributed".
953
+ const same = await decideSemanticMerge({
954
+ ...base,
955
+ sourceConnector: "connector-b",
956
+ lookup: async () => [{ id: "mem-b", score: 0.85, sourceConnector: "connector-b" }],
957
+ });
958
+ assert.equal(same.action, "merge");
959
+ // Finding B: an UNSCOPED incoming fact must not merge into a
960
+ // connector-owned target either — the merge would rewrite A's body while
961
+ // A's `sourceConnector` frontmatter still names A, so recall would label
962
+ // the unscoped claims as A's. Merge selection requires BOTH sides
963
+ // unscoped or the identical connector (stricter than the dedup gates).
964
+ const unscoped = await decideSemanticMerge({
965
+ ...base,
966
+ sourceConnector: " ",
967
+ lookup: async () => [{ id: "mem-a", score: 0.85, sourceConnector: "connector-a" }],
968
+ });
969
+ assert.deepEqual(unscoped, { action: "create", reason: "no_candidates" });
970
+ const bothUnscoped = await decideSemanticMerge({
971
+ ...base,
972
+ sourceConnector: " ",
973
+ lookup: async () => hits(["mem-operator", 0.85]),
974
+ });
975
+ assert.equal(bothUnscoped.action, "merge");
976
+ });
977
+
978
+ test("applySemanticMergeAtPersist: the incoming fact's connector scopes the lookup", async () => {
979
+ const h = await harness({
980
+ lookupHits: [{ id: "fact-target", score: 0.85, sourceConnector: "connector-a" }],
981
+ });
982
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
983
+ storage: h.storage,
984
+ content: INCOMING,
985
+ category: "fact",
986
+ sourceConnector: "connector-b",
987
+ judgeCall: (options) => acceptingJudge(options),
988
+ });
989
+ assert.deepEqual(outcome, { action: "created", reason: "no_candidates" });
990
+ assert.deepEqual(h.calls.contentUpdates, []);
991
+ assert.equal(await readFile(h.target.path, "utf8").then((t) => t.includes(EXISTING)), true);
992
+ });
993
+
994
+ // ── Finding 2: the write is conditional on the judged snapshot ───────────────
995
+
996
+ test("applySemanticMergeAtPersist: a target changed after judging creates, never clobbers", async () => {
997
+ const CONCURRENT = "Billing service deploys happen on Tuesdays, paused during freeze weeks.";
998
+ const h = await harness();
999
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1000
+ storage: h.storage,
1001
+ content: INCOMING,
1002
+ category: "fact",
1003
+ sources: [INCOMING_SOURCE],
1004
+ // Another extraction lands between the judge resolving the target body
1005
+ // and the write. `MERGED` was composed from the older body.
1006
+ judgeCall: async (options) => {
1007
+ const verdict = await acceptingJudge(options);
1008
+ await h.setTargetContent(CONCURRENT);
1009
+ return verdict;
1010
+ },
1011
+ });
1012
+ assert.deepEqual(outcome, { action: "created", reason: "target_changed" });
1013
+ assert.deepEqual(h.calls.contentUpdates, []);
1014
+ // The concurrent writer's details survive verbatim.
1015
+ const onDisk = await readFile(h.target.path, "utf8");
1016
+ assert.equal(onDisk.includes(CONCURRENT), true);
1017
+ assert.equal(onDisk.includes(MERGED), false);
1018
+ });
1019
+
1020
+ test("applySemanticMergeAtPersist: a failed compare-and-swap creates instead of overwriting", async () => {
1021
+ const RACED = "Billing service deploys happen on Tuesdays, except during a freeze.";
1022
+ const h = await harness({ mutateOnWrite: RACED });
1023
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1024
+ storage: h.storage,
1025
+ content: INCOMING,
1026
+ category: "fact",
1027
+ judgeCall: (options) => acceptingJudge(options),
1028
+ });
1029
+ assert.deepEqual(outcome, { action: "created", reason: "target_changed" });
1030
+ assert.deepEqual(h.calls.contentUpdates, []);
1031
+ const onDisk = await readFile(h.target.path, "utf8");
1032
+ assert.equal(onDisk.includes(RACED), true);
1033
+ assert.equal(onDisk.includes(MERGED), false);
1034
+ });
1035
+
1036
+ // ── Finding 3: a failed frontmatter patch really restores the body ───────────
1037
+
1038
+ test("applySemanticMergeAtPersist: a failed frontmatter patch restores the pre-merge body", async () => {
1039
+ const h = await harness({ frontmatterFails: true });
1040
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1041
+ storage: h.storage,
1042
+ content: INCOMING,
1043
+ category: "fact",
1044
+ sources: [INCOMING_SOURCE],
1045
+ judgeCall: (options) => acceptingJudge(options),
1046
+ });
1047
+ // Reporting `created` is only honest once the target no longer holds the
1048
+ // merged text — otherwise the caller writes a duplicate. Read the body back
1049
+ // out of storage: a rollback that only claimed success would pass an
1050
+ // assertion on the recorded calls alone.
1051
+ assert.deepEqual(outcome, { action: "created", reason: "update_failed" });
1052
+ const restored = await h.storage.getMemoryByIdIncludingArchived("fact-target");
1053
+ assert.equal(restored?.content, EXISTING);
1054
+ const onDisk = await readFile(h.target.path, "utf8");
1055
+ assert.equal(onDisk.includes(EXISTING), true);
1056
+ assert.equal(onDisk.includes(MERGED), false);
1057
+ assert.deepEqual(h.calls.contentUpdates, [
1058
+ { id: "fact-target", content: MERGED, actor: "semantic-merge" },
1059
+ { id: "fact-target", content: EXISTING, actor: "semantic-merge-rollback" },
1060
+ ]);
1061
+ // Neither hash resync nor reindex may run for a merge that did not stand.
1062
+ assert.deepEqual(h.calls.hashRemovals, []);
1063
+ assert.deepEqual(h.calls.reindexed, []);
1064
+ });
1065
+
1066
+ test("applySemanticMergeAtPersist: a writer inside the patch window keeps its body and blocks a false merge", async () => {
1067
+ const RACED = "Billing service deploys are paused during freeze weeks.";
1068
+ // Another writer replaces the target after the content commit and after the
1069
+ // verifying read — the window an id-keyed frontmatter patch left open. That
1070
+ // patch would have stamped this merge's provenance onto the other writer's
1071
+ // body and still returned `merged`, so the caller would drop the fact.
1072
+ const h = await harness({ mutateAtPatch: RACED });
1073
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1074
+ storage: h.storage,
1075
+ content: INCOMING,
1076
+ category: "fact",
1077
+ sources: [INCOMING_SOURCE],
1078
+ judgeCall: (options) => acceptingJudge(options),
1079
+ });
1080
+ // Never `merged`: the merged body was overwritten, so the fact is unwritten.
1081
+ assert.deepEqual(outcome, { action: "created", reason: "update_failed" });
1082
+ // No provenance was attached to a body this merge never composed.
1083
+ assert.deepEqual(h.calls.frontmatterPatches, []);
1084
+ // The other writer's body survives verbatim — the rollback must not treat
1085
+ // someone else's content as its own to revert.
1086
+ const current = await h.storage.getMemoryByIdIncludingArchived("fact-target");
1087
+ assert.equal(current?.content, RACED);
1088
+ assert.deepEqual(
1089
+ h.calls.contentUpdates.map((c) => c.actor),
1090
+ ["semantic-merge"],
1091
+ );
1092
+ const onDisk = await readFile(h.target.path, "utf8");
1093
+ assert.equal(onDisk.includes(MERGED), false);
1094
+ assert.equal(onDisk.includes(EXISTING), false);
1095
+ assert.deepEqual(h.calls.hashRemovals, []);
1096
+ assert.deepEqual(h.calls.reindexed, []);
1097
+ });
1098
+
1099
+ test("applySemanticMergeAtPersist: an unrollbackable patch failure reports merged, not created", async () => {
1100
+ const h = await harness({ frontmatterFails: true, rollbackFails: true });
1101
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1102
+ storage: h.storage,
1103
+ content: INCOMING,
1104
+ category: "fact",
1105
+ judgeCall: (options) => acceptingJudge(options),
1106
+ });
1107
+ // The merged text IS in the target; claiming `created` would duplicate it.
1108
+ assert.deepEqual(outcome, {
1109
+ action: "merged",
1110
+ targetId: "fact-target",
1111
+ mergedContent: MERGED,
1112
+ provenancePatched: false,
1113
+ });
1114
+ assert.equal(await readFile(h.target.path, "utf8").then((t) => t.includes(MERGED)), true);
1115
+ // Item C — the degraded success still repairs the indexes before returning:
1116
+ // hash resync and reindex run, so QMD and the fact-hash index do not hold
1117
+ // the pre-merge identity until unrelated maintenance.
1118
+ assert.deepEqual(h.calls.hashRemovals, [EXISTING]);
1119
+ // Round N+16 (C): the degraded repair registers the COMMITTED body's hash
1120
+ // (asserted in the N+16 C test below) instead of restoring the persisted
1121
+ // pre-merge identity the old hash-adds route re-registered.
1122
+ assert.deepEqual(h.calls.hashAdds, []);
1123
+ assert.deepEqual(
1124
+ h.calls.hashRegistrations,
1125
+ [{ id: "fact-target", hash: ContentHashIndex.computeHash(sanitizeMemoryContent(MERGED).text) }],
1126
+ );
1127
+ assert.deepEqual(h.calls.reindexed, ["fact-target"]);
1128
+ });
1129
+
1130
+ // ── Round N+16 (C): the degraded repair must index what storage HOLDS. With
1131
+ // the body committed and both the frontmatter patch and its rollback failed,
1132
+ // the record holds the MERGED body under the PRE-merge `contentHash`.
1133
+ // `restoreFactHashAfterApproval` prefers that persisted value, so the repair
1134
+ // used to re-register the stale identity and exact dedup never saw the merged
1135
+ // body — a second copy of the same content could be written. ────────────────
1136
+
1137
+ test("applySemanticMergeAtPersist: the degraded repair registers the COMMITTED body's hash, not the stale persisted one (round N+16 C)", async () => {
1138
+ const h = await harness({ frontmatterFails: true, rollbackFails: true });
1139
+ // The double failure leaves the frontmatter holding the PRE-merge identity.
1140
+ const STALE = ContentHashIndex.computeHash(sanitizeMemoryContent(EXISTING).text);
1141
+ h.target.frontmatter.contentHash = STALE;
1142
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1143
+ storage: h.storage,
1144
+ content: INCOMING,
1145
+ category: "fact",
1146
+ judgeCall: (options) => acceptingJudge(options),
1147
+ });
1148
+ assert.equal(outcome.action, "merged");
1149
+ assert.equal(outcome.provenancePatched, false);
1150
+ const EXPECTED = ContentHashIndex.computeHash(sanitizeMemoryContent(MERGED).text);
1151
+ assert.notEqual(EXPECTED, STALE, "fixture sanity: merged and pre-merge bodies must differ");
1152
+ assert.deepEqual(
1153
+ h.calls.hashRegistrations,
1154
+ [{ id: "fact-target", hash: EXPECTED }],
1155
+ "the degraded repair must register the committed merged body's hash — never the stale persisted frontmatter value",
1156
+ );
1157
+ assert.equal(
1158
+ h.calls.hashAdds.length,
1159
+ 0,
1160
+ "the degraded path must not route hash restoration through the persisted-frontmatter reader",
1161
+ );
1162
+ });
1163
+
1164
+ test("registerFactContentHash: exact dedup finds the degraded record's merged body (round N+16 C)", async () => {
1165
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-hash-"));
1166
+ try {
1167
+ const storage = new StorageManager(dir);
1168
+ await storage.ensureDirectories();
1169
+ const { id } = await storage.writeMemory("fact", EXISTING, { source: "extraction" });
1170
+ // Reproduce the degraded state the way the merge creates it: the body is
1171
+ // compare-and-swapped to the merged text while the frontmatter keeps the
1172
+ // old identity (updateMemoryIfUnchanged preserves contentHash).
1173
+ const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id)!;
1174
+ assert.equal(
1175
+ await storage.updateMemoryIfUnchanged(before, MERGED, { actor: "semantic-merge" }),
1176
+ true,
1177
+ );
1178
+ const degraded = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id)!;
1179
+ const staleHash = degraded.frontmatter.contentHash;
1180
+ const mergedHash = ContentHashIndex.computeHash(sanitizeMemoryContent(MERGED).text);
1181
+ assert.ok(staleHash, "fixture sanity: the pre-merge identity is persisted");
1182
+ assert.notEqual(staleHash, mergedHash, "fixture sanity: the persisted identity is stale");
1183
+ assert.equal(
1184
+ await storage.hasFactContentHash(MERGED),
1185
+ false,
1186
+ "the degraded record's merged body must not already be indexed",
1187
+ );
1188
+ // The PRE-FIX repair: remove + restore-from-record re-registers the STALE
1189
+ // persisted identity, so the merged body stays invisible to exact dedup.
1190
+ await storage.removeFactContentHashesForMemories([degraded]);
1191
+ await storage.restoreFactHashAfterApproval(id);
1192
+ assert.equal(
1193
+ await storage.hasFactContentHash(MERGED),
1194
+ false,
1195
+ "the persisted-frontmatter reader cannot find the merged body — the N+16 C bug",
1196
+ );
1197
+ assert.equal(await storage.hasFactContentHash(EXISTING), true, "it re-registered the stale pre-merge identity instead");
1198
+ // The FIXED repair: register the hash of what is actually STORED.
1199
+ await storage.removeFactContentHashesForMemories([degraded]);
1200
+ await storage.registerFactContentHash(id, mergedHash, MERGED);
1201
+ assert.equal(
1202
+ await storage.hasFactContentHash(MERGED),
1203
+ true,
1204
+ "exact dedup must find the committed merged body's hash",
1205
+ );
1206
+ } finally {
1207
+ await rm(dir, { recursive: true, force: true });
1208
+ }
1209
+ });
1210
+
1211
+ // ── Round N+19 (B): the degraded repair must also be DURABLE. The N+16 C
1212
+ // registration lives in the process-local fact-hash index while the record's
1213
+ // PERSISTED frontmatter still carries the stale pre-merge `contentHash` — and
1214
+ // a restart's first-use index rebuild derives hashes from the corpus, where
1215
+ // the persisted value wins whenever it disagrees with the current body. The
1216
+ // repair must therefore also restamp the persisted identity. ────────────────
1217
+
1218
+ test("applySemanticMergeAtPersist: the degraded repair persists the committed body's hash in the frontmatter (round N+19 B)", async () => {
1219
+ const h = await harness({ frontmatterFails: true, rollbackFails: true });
1220
+ const STALE = ContentHashIndex.computeHash(sanitizeMemoryContent(EXISTING).text);
1221
+ h.target.frontmatter.contentHash = STALE;
1222
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1223
+ storage: h.storage,
1224
+ content: INCOMING,
1225
+ category: "fact",
1226
+ judgeCall: (options) => acceptingJudge(options),
1227
+ });
1228
+ assert.equal(outcome.action, "merged");
1229
+ // The provenance patch itself failed (frontmatterFails); the repair must
1230
+ // still attempt a CAS-guarded restamp of ONLY the content identity. The
1231
+ // provenance patch also carries a contentHash, so identify the repair as
1232
+ // the LAST patch — the degraded branch runs after the failed patch.
1233
+ const EXPECTED = ContentHashIndex.computeHash(sanitizeMemoryContent(MERGED).text);
1234
+ const patches = h.calls.frontmatterPatches;
1235
+ assert.equal(patches.length, 2, "the failed provenance patch, then the durable repair");
1236
+ const repair = patches[1];
1237
+ assert.deepEqual(
1238
+ Object.keys(repair.patch),
1239
+ ["contentHash"],
1240
+ "the repair patch must touch only the content identity — provenance fields stay unpatched on the degraded record",
1241
+ );
1242
+ assert.equal(repair.patch.contentHash, EXPECTED);
1243
+ assert.equal(repair.actor, "semantic-merge");
1244
+ });
1245
+
1246
+ test("persistRepairedContentHash: the repaired hash survives a restart's corpus rebuild (round N+19 B)", async () => {
1247
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-hash-durable-"));
1248
+ try {
1249
+ const storage = new StorageManager(dir);
1250
+ const { id } = await storage.writeMemory("fact", EXISTING, { source: "extraction" });
1251
+ // Reproduce the degraded state the way the merge creates it: the body is
1252
+ // compare-and-swapped to the merged text while the frontmatter keeps the
1253
+ // old identity (updateMemoryIfUnchanged preserves contentHash).
1254
+ const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id)!;
1255
+ assert.equal(await storage.updateMemoryIfUnchanged(before, MERGED, { actor: "semantic-merge" }), true);
1256
+ const mergedHash = ContentHashIndex.computeHash(sanitizeMemoryContent(MERGED).text);
1257
+ // The N+16 C repair alone (process-local registration).
1258
+ await storage.registerFactContentHash(id, mergedHash, MERGED);
1259
+ // Simulated restart WITHOUT the durable repair: the fresh instance ALWAYS
1260
+ // rebuilds the index from the durable corpus on first use, and the corpus
1261
+ // reader prefers the stale persisted identity — the pre-fix loss this
1262
+ // regression pins.
1263
+ const lost = new StorageManager(dir);
1264
+ assert.equal(
1265
+ await lost.hasFactContentHash(MERGED),
1266
+ false,
1267
+ "pre-fix behavior: the process-local registration does not survive the corpus rebuild",
1268
+ );
1269
+ // The durable repair restamps the persisted identity (CAS-guarded).
1270
+ await persistRepairedContentHash(storage, id, MERGED, mergedHash);
1271
+ const repaired = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id)!;
1272
+ assert.equal(repaired.frontmatter.contentHash, mergedHash, "the persisted identity is the merged body's hash");
1273
+ const restarted = new StorageManager(dir);
1274
+ assert.equal(
1275
+ await restarted.hasFactContentHash(MERGED),
1276
+ true,
1277
+ "the corpus rebuild must serve the merged-body hash after the durable repair",
1278
+ );
1279
+ assert.equal(
1280
+ await restarted.hasFactContentHash(EXISTING),
1281
+ false,
1282
+ "the stale pre-merge identity must no longer be the record's registered hash",
1283
+ );
1284
+ } finally {
1285
+ await rm(dir, { recursive: true, force: true });
1286
+ }
1287
+ });
1288
+
1289
+ // ── Round N+20 (B): hash registration is body-coupled. A writer that
1290
+ // replaces the target between this writer's content commit and the degraded
1291
+ // repair keeps its own record; registering OUR (now-obsolete) merged-body
1292
+ // hash on it created a phantom exact-dedup hit that suppressed a later real
1293
+ // extraction of that body. ─────────────────────────────────────────────────
1294
+
1295
+ test("registerFactContentHash: a record replaced after the commit gets no phantom hash (round N+20 B)", async () => {
1296
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-hash-phantom-"));
1297
+ try {
1298
+ const storage = new StorageManager(dir);
1299
+ await storage.ensureDirectories();
1300
+ const REPLACED = "Deploys were moved to Thursdays by the platform team.";
1301
+ const { id } = await storage.writeMemory("fact", EXISTING, { source: "extraction" });
1302
+ const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id)!;
1303
+ await storage.updateMemoryIfUnchanged(before, MERGED, { actor: "semantic-merge" });
1304
+ const mergedHash = ContentHashIndex.computeHash(sanitizeMemoryContent(MERGED).text);
1305
+ // Another writer replaces the target after this writer's content commit,
1306
+ // before the degraded repair's registration runs.
1307
+ const degraded = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id)!;
1308
+ await storage.updateMemoryIfUnchanged(degraded, REPLACED, { actor: "other-writer" });
1309
+ await storage.registerFactContentHash(id, mergedHash, MERGED);
1310
+ assert.equal(
1311
+ (await storage.readAllMemories()).find((m) => m.frontmatter.id === id)?.content,
1312
+ REPLACED,
1313
+ "fixture sanity: the record was replaced after the degraded writer's commit",
1314
+ );
1315
+ assert.equal(
1316
+ await storage.hasFactContentHash(MERGED),
1317
+ false,
1318
+ "no phantom exact-dedup hit for a body the record no longer holds",
1319
+ );
1320
+ } finally {
1321
+ await rm(dir, { recursive: true, force: true });
1322
+ }
1323
+ });
1324
+
1325
+ // ── Round N+20 (C): a degraded SUCCESS (body committed and kept) commits its
1326
+ // recovery snapshot. Pre-fix, the staged version kept `pending` forever, and
1327
+ // pruneExcessVersions excludes pending entries — repeated degraded merges
1328
+ // grew the manifest and snapshot directory past maxVersionsPerPage with no
1329
+ // later prune able to recover the bound. ────────────────────────────────────
1330
+
1331
+ test("applySemanticMergeAtPersist: consecutive degraded merges keep the manifest within the page cap (round N+20 C)", async () => {
1332
+ const h = await harness({ frontmatterFails: true, rollbackFails: true, topLevelConfig: { versioningMaxPerPage: 2 } });
1333
+ const versioning = { enabled: true, maxVersionsPerPage: 2, sidecarDir: ".versions" };
1334
+ for (let i = 1; i <= 2; i++) {
1335
+ await createVersion(h.target.path, `${EXISTING} (history fill ${i})`, "write", versioning, undefined, undefined, h.storage.dir);
1336
+ }
1337
+ for (let i = 1; i <= 2; i++) {
1338
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1339
+ storage: h.storage,
1340
+ content: INCOMING,
1341
+ category: "fact",
1342
+ judgeCall: (options) => acceptingJudge(options),
1343
+ });
1344
+ assert.equal(outcome.action, "merged", `degraded merge ${i} must report merged`);
1345
+ assert.equal(outcome.provenancePatched, false, `degraded merge ${i} is the double-failure path`);
1346
+ }
1347
+ const history = await listVersions(h.target.path, versioning, h.storage.dir);
1348
+ assert.ok(
1349
+ history.versions.length <= versioning.maxVersionsPerPage,
1350
+ `the manifest must stay within maxVersionsPerPage (got ${history.versions.length}: ${history.versions.map((v) => `${v.versionId}${v.pending ? "p" : ""}`).join(",")})`,
1351
+ );
1352
+ assert.equal(
1353
+ history.versions.some((version) => version.pending === true),
1354
+ false,
1355
+ "a degraded success's recovery snapshot must be committed, not pending forever",
1356
+ );
1357
+ assert.equal(history.versions.at(-1)?.trigger, "semantic-merge", "the newest entry is the kept recovery point");
1358
+ });
1359
+
1360
+ // ── Item A: extraction metadata a merge cannot carry ─────────────────────────
1361
+
1362
+ test("applySemanticMergeAtPersist: new metadata the merge cannot carry bypasses merging", async () => {
1363
+ const cases: Array<[string, ApplySemanticMergeOptions["incomingMetadata"]]> = [
1364
+ ["structuredAttributes", { structuredAttributes: { region: "us-east" } }],
1365
+ ["bi-temporal bounds", { biTemporal: true }],
1366
+ ["an agent subject the unlabeled target lacks", { subject: "agent" }],
1367
+ ["a new validAt", { validAt: "2026-08-21T00:00:00.000Z" }],
1368
+ ["a tag the target lacks", { tags: ["deploy"] }],
1369
+ ["a higher importance", { importanceScore: 0.9 }],
1370
+ ["a stronger provenance", { provenanceStrength: "verified" }],
1371
+ ];
1372
+ for (const [label, incomingMetadata] of cases) {
1373
+ const h = await harness();
1374
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1375
+ storage: h.storage,
1376
+ content: INCOMING,
1377
+ category: "fact",
1378
+ judgeCall: (options) => acceptingJudge(options),
1379
+ incomingMetadata,
1380
+ });
1381
+ assert.deepEqual(
1382
+ outcome,
1383
+ { action: "created", reason: "metadata_unpreservable" },
1384
+ label,
1385
+ );
1386
+ assert.deepEqual(h.calls.contentUpdates, [], label);
1387
+ assert.equal(
1388
+ await readFile(h.target.path, "utf8").then((t) => t.includes(MERGED)),
1389
+ false,
1390
+ label,
1391
+ );
1392
+ }
1393
+ });
1394
+
1395
+ test("applySemanticMergeAtPersist: metadata the target already carries still merges", async () => {
1396
+ const h = await harness();
1397
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1398
+ storage: h.storage,
1399
+ content: INCOMING,
1400
+ category: "fact",
1401
+ sources: [INCOMING_SOURCE],
1402
+ judgeCall: (options) => acceptingJudge(options),
1403
+ // No new tag, no higher importance, no stronger provenance: nothing the
1404
+ // write path would persist is lost, so the merge runs.
1405
+ incomingMetadata: { tags: [], importanceScore: 0, provenanceStrength: "none" },
1406
+ });
1407
+ assert.deepEqual(outcome, { action: "merged", targetId: "fact-target", mergedContent: MERGED, provenancePatched: true });
1408
+ assert.deepEqual(h.calls.contentUpdates, [
1409
+ { id: "fact-target", content: MERGED, actor: "semantic-merge" },
1410
+ ]);
1411
+ });
1412
+
1413
+ // ── Item B: the merged body registers under the write path's identity ────────
1414
+
1415
+ test("applySemanticMergeAtPersist: the merged fact restamps contentHash off the canonical raw form", async () => {
1416
+ const h = await harness();
1417
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1418
+ storage: h.storage,
1419
+ content: INCOMING,
1420
+ category: "fact",
1421
+ sources: [INCOMING_SOURCE],
1422
+ judgeCall: (options) => acceptingJudge(options),
1423
+ });
1424
+ assert.equal(outcome.action, "merged");
1425
+ const patch = h.calls.frontmatterPatches[0].patch;
1426
+ // The SAME canonical form normal persistence hashes: sanitized raw
1427
+ // pre-citation content — never a cited variant of the merged body.
1428
+ assert.equal(
1429
+ patch.contentHash,
1430
+ ContentHashIndex.computeHash(sanitizeMemoryContent(MERGED).text),
1431
+ );
1432
+ });
1433
+
1434
+ test("applySemanticMergeAtPersist: a non-fact merge stamps no contentHash", async () => {
1435
+ const h = await harness({ targetCategory: "preference" });
1436
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1437
+ storage: h.storage,
1438
+ content: INCOMING,
1439
+ category: "preference",
1440
+ sources: [INCOMING_SOURCE],
1441
+ judgeCall: (options) => acceptingJudge(options),
1442
+ });
1443
+ assert.equal(outcome.action, "merged");
1444
+ assert.equal(h.calls.frontmatterPatches[0].patch.contentHash, undefined);
1445
+ });
1446
+
1447
+ test("applySemanticMergeAtPersist: real storage registers the merged body in the fact-hash index", async () => {
1448
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-hash-"));
1449
+ const storage = new StorageManager(dir);
1450
+ await storage.ensureDirectories();
1451
+ const created = await storage.writeMemory("fact", EXISTING, { source: "test" });
1452
+ const deps = {
1453
+ config: parseConfig({
1454
+ memoryDir: dir,
1455
+ versioningEnabled: true,
1456
+ semanticMerge: { enabled: true },
1457
+ }),
1458
+ getLocalLlm: () => null,
1459
+ semanticDedupLookup: async () => [{ id: created.id, score: 0.85 }],
1460
+ indexPersistedMemory: async () => {},
1461
+ } as unknown as ExtractionPersistDeps;
1462
+ const outcome = await applySemanticMergeAtPersist(deps, {
1463
+ storage,
1464
+ content: INCOMING,
1465
+ category: "fact",
1466
+ sources: [INCOMING_SOURCE],
1467
+ judgeCall: (options) => acceptingJudge(options),
1468
+ });
1469
+ assert.deepEqual(outcome, { action: "merged", targetId: created.id, mergedContent: MERGED, provenancePatched: true });
1470
+ // The merged body is registered in the exact-dedup index under the same
1471
+ // canonical form the write path hashes; the pre-merge identity is gone.
1472
+ assert.equal(await storage.hasFactContentHash(MERGED), true);
1473
+ assert.equal(await storage.hasFactContentHash(EXISTING), false);
1474
+ });
1475
+
1476
+ test("applySemanticMergeAtPersist: repair registers the merged hash for a cold-tier target whose patch hash sync failed (round N+13 C)", async () => {
1477
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-cold-hash-"));
1478
+ try {
1479
+ const storage = new StorageManager(dir);
1480
+ await storage.ensureDirectories();
1481
+ // Cold-tier target (same tier layout as the embedding-index test above):
1482
+ // a long-lived merge target that was demoted to cold/.
1483
+ const targetPath = path.join(dir, "cold", "facts", "2026-08-20", "fact-cold-target.md");
1484
+ await mkdir(path.dirname(targetPath), { recursive: true });
1485
+ const existingHash = ContentHashIndex.computeHash(sanitizeMemoryContent(EXISTING).text);
1486
+ await writeFile(
1487
+ targetPath,
1488
+ [
1489
+ "---",
1490
+ "id: fact-cold-target",
1491
+ "category: fact",
1492
+ "created: 2026-08-20T00:00:00.000Z",
1493
+ "updated: 2026-08-20T00:00:00.000Z",
1494
+ "status: active",
1495
+ `contentHash: ${existingHash}`,
1496
+ "---",
1497
+ "",
1498
+ EXISTING,
1499
+ "",
1500
+ ].join("\n"),
1501
+ "utf8",
1502
+ );
1503
+ // Make the fact-hash index authoritative BEFORE the merge. The corpus
1504
+ // rebuild is cold-aware, so a post-merge rebuild would register the
1505
+ // merged hash no matter what the repair did — pre-loading pins the
1506
+ // assertion to the in-process repair path the finding names.
1507
+ assert.equal(await storage.isFactContentHashAuthoritative(), true);
1508
+
1509
+ // One-shot swallow: the provenance patch's internal hash sync fails
1510
+ // (logged, non-fatal — the write stands), so ONLY repairIndexes can
1511
+ // register the merged hash.
1512
+ let swallowed = false;
1513
+ const originalAdd = ContentHashIndex.prototype.addFactByHash;
1514
+ const addSpy = mock.method(
1515
+ ContentHashIndex.prototype,
1516
+ "addFactByHash",
1517
+ function (this: ContentHashIndex, hash: string) {
1518
+ if (!swallowed) {
1519
+ swallowed = true;
1520
+ throw new Error("simulated swallowed frontmatter-hash sync failure");
1521
+ }
1522
+ return originalAdd.call(this, hash);
1523
+ },
1524
+ );
1525
+ try {
1526
+ const deps = {
1527
+ config: parseConfig({
1528
+ memoryDir: dir,
1529
+ versioningEnabled: true,
1530
+ semanticMerge: { enabled: true },
1531
+ }),
1532
+ getLocalLlm: () => null,
1533
+ semanticDedupLookup: async () => [{ id: "fact-cold-target", score: 0.85 }],
1534
+ indexPersistedMemory: async () => {},
1535
+ } as unknown as ExtractionPersistDeps;
1536
+ const outcome = await applySemanticMergeAtPersist(deps, {
1537
+ storage,
1538
+ content: INCOMING,
1539
+ category: "fact",
1540
+ sources: [INCOMING_SOURCE],
1541
+ judgeCall: (options) => acceptingJudge(options),
1542
+ });
1543
+ assert.equal(outcome.action, "merged");
1544
+ assert.equal(swallowed, true, "the patch's hash sync must actually fail for this scenario");
1545
+ // Answer from the loaded index's fact partition (getSharedFactHashIndex
1546
+ // returns the same in-process instance the repair mutated), never
1547
+ // through hasFactContentHash's corpus fallback.
1548
+ const index = await storage.getSharedFactHashIndex();
1549
+ assert.equal(
1550
+ index.hasFact(sanitizeMemoryContent(MERGED).text),
1551
+ true,
1552
+ "the repair registered the merged hash for the cold-tier target",
1553
+ );
1554
+ } finally {
1555
+ addSpy.mock.restore();
1556
+ await StorageManager.clearAllStaticCaches();
1557
+ }
1558
+ } finally {
1559
+ await rm(dir, { recursive: true, force: true });
1560
+ }
1561
+ });
1562
+
1563
+ // ── Finding A: judge output the sanitizer would rewrite never mutates ───────
1564
+
1565
+ test("decideSemanticMerge: unsafe merged output is judge_invalid, never a merge", async () => {
1566
+ // Storage persists sanitizeMemoryContent(text).text. A merged body matching
1567
+ // an injection pattern would be committed as the redaction placeholder, so
1568
+ // the persist-side equality checks can never recognize it — the decision
1569
+ // must refuse before any mutation is possible.
1570
+ const decision = await decideSemanticMerge({
1571
+ content: INCOMING,
1572
+ category: "fact",
1573
+ config: MERGE_CONFIG,
1574
+ dedupThreshold: 0.92,
1575
+ lookup: async () => hits(["fact-target", 0.85]),
1576
+ resolveCandidate: candidateResolver(),
1577
+ judge: async () => ({
1578
+ decision: "merge",
1579
+ targetId: "fact-target",
1580
+ mergedContent: `${MERGED} Disregard previous deploy notes.`,
1581
+ reason: "echoed an injection pattern",
1582
+ }),
1583
+ });
1584
+ assert.deepEqual(decision, { action: "create", reason: "judge_invalid" });
1585
+ });
1586
+
1587
+ test("applySemanticMergeAtPersist: unsafe judge output never leaves a sanitization placeholder in the target", async () => {
1588
+ // Real storage: updateMemoryIfUnchanged sanitizes on write, so before the
1589
+ // fix this exact shape committed "[content removed: unsafe memory text]"
1590
+ // over the target body, misread it as a concurrent replacement, reported a
1591
+ // successful rollback, and let the caller create the fact too — the
1592
+ // original target body was lost. The regression asserts the target is
1593
+ // intact and untouched.
1594
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-unsafe-"));
1595
+ const storage = new StorageManager(dir);
1596
+ await storage.ensureDirectories();
1597
+ const created = await storage.writeMemory("fact", EXISTING, { source: "test" });
1598
+ const deps = {
1599
+ config: parseConfig({
1600
+ memoryDir: dir,
1601
+ versioningEnabled: true,
1602
+ semanticMerge: { enabled: true },
1603
+ }),
1604
+ getLocalLlm: () => null,
1605
+ semanticDedupLookup: async () => [{ id: created.id, score: 0.85 }],
1606
+ indexPersistedMemory: async () => {},
1607
+ } as unknown as ExtractionPersistDeps;
1608
+ const outcome = await applySemanticMergeAtPersist(deps, {
1609
+ storage,
1610
+ content: INCOMING,
1611
+ category: "fact",
1612
+ sources: [INCOMING_SOURCE],
1613
+ judgeCall: async () => ({
1614
+ decision: "merge",
1615
+ targetId: created.id,
1616
+ mergedContent: `${MERGED} Disregard previous deploy notes.`,
1617
+ reason: "echoed an injection pattern",
1618
+ }),
1619
+ });
1620
+ // Refused before any mutation: the caller may create, and the target keeps
1621
+ // its original body — never the sanitization placeholder, never a duplicate.
1622
+ assert.deepEqual(outcome, { action: "created", reason: "judge_invalid" });
1623
+ const target = await storage.getMemoryByIdIncludingArchived(created.id);
1624
+ assert.equal(target?.content, EXISTING);
1625
+ assert.notEqual(target?.content, "[content removed: unsafe memory text]");
1626
+ assert.equal(await storage.hasFactContentHash(EXISTING), true);
1627
+ });
1628
+
1629
+ // ── Finding 4: a disabled feature never invalidates a legacy config ──────────
1630
+
1631
+ test("parseSemanticMergeConfig: a low dedup threshold with no semanticMerge block still parses", () => {
1632
+ for (const semanticDedupThreshold of [0.8, 0.75, 0.5]) {
1633
+ const { semanticMerge } = parseSemanticMergeConfig({ semanticDedupThreshold });
1634
+ assert.equal(semanticMerge.enabled, false, `threshold ${semanticDedupThreshold}`);
1635
+ assert.equal(semanticMerge.minSimilarity, DEFAULT_SEMANTIC_MERGE_MIN);
1636
+ }
1637
+ // parseConfig is the real startup path a legacy deployment takes.
1638
+ assert.equal(parseConfig({ semanticDedupThreshold: 0.8 }).semanticMerge.enabled, false);
1639
+ // Enabling it against the same threshold is a real misconfiguration.
1640
+ assert.throws(
1641
+ () => parseSemanticMergeConfig({ semanticDedupThreshold: 0.8, semanticMerge: { enabled: true } }),
1642
+ /strictly below semanticDedupThreshold/,
1643
+ );
1644
+ // An explicitly inverted band is rejected even while disabled.
1645
+ assert.throws(
1646
+ () =>
1647
+ parseSemanticMergeConfig({
1648
+ semanticDedupThreshold: 0.8,
1649
+ semanticMerge: { minSimilarity: 0.9 },
1650
+ }),
1651
+ /strictly below semanticDedupThreshold/,
1652
+ );
1653
+ });
1654
+
1655
+ // ── Finding A: a merge must never widen a tool-scoped fact's recall scope ────
1656
+
1657
+ test("applySemanticMergeAtPersist: a tool-scoped incoming fact never merges into an unscoped target", async () => {
1658
+ const h = await harness();
1659
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1660
+ storage: h.storage,
1661
+ content: INCOMING,
1662
+ category: "fact",
1663
+ sources: [INCOMING_SOURCE],
1664
+ incomingMetadata: { toolScoped: true },
1665
+ judgeCall: (options) => acceptingJudge(options),
1666
+ });
1667
+
1668
+ // The unscoped target would serve the tool-specific claims to every
1669
+ // connector; the write path (which stamps `toolScoped: true`) must run.
1670
+ assert.deepEqual(outcome, { action: "created", reason: "metadata_unpreservable" });
1671
+ assert.deepEqual(h.calls.contentUpdates, []);
1672
+ assert.deepEqual(h.calls.frontmatterPatches, []);
1673
+ const history = await listVersions(h.target.path, { enabled: true, maxVersionsPerPage: 20, sidecarDir: ".versions" }, h.storage.dir);
1674
+ assert.equal(history.versions.length, 0, "a bypassed merge must leave no snapshot");
1675
+ });
1676
+
1677
+ test("applySemanticMergeAtPersist: a tool-scoped target keeps its stricter scope through a merge", async () => {
1678
+ const h = await harness({ targetToolScoped: true });
1679
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1680
+ storage: h.storage,
1681
+ content: INCOMING,
1682
+ category: "fact",
1683
+ sources: [INCOMING_SOURCE],
1684
+ incomingMetadata: { toolScoped: true },
1685
+ judgeCall: (options) => acceptingJudge(options),
1686
+ });
1687
+ assert.deepEqual(outcome, { action: "merged", targetId: "fact-target", mergedContent: MERGED, provenancePatched: true });
1688
+ // The merge patch carries no `toolScoped` key at all — the stricter flag
1689
+ // survives because the patch never touches it, never because it is rewritten.
1690
+ assert.equal(h.calls.frontmatterPatches.length, 1);
1691
+ assert.equal("toolScoped" in h.calls.frontmatterPatches[0].patch, false);
1692
+ });
1693
+
1694
+ // ── Finding A: a merge must not relabel content across the subject guard ────
1695
+
1696
+
1697
+ test("applySemanticMergeAtPersist: a user-subject fact never merges into an agent-labeled target", async () => {
1698
+ const h = await harness({ targetSubject: "agent" });
1699
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1700
+ storage: h.storage,
1701
+ content: INCOMING,
1702
+ category: "fact",
1703
+ sources: [INCOMING_SOURCE],
1704
+ incomingMetadata: { subject: "user" },
1705
+ judgeCall: (options) => acceptingJudge(options),
1706
+ });
1707
+
1708
+ // Merging would increment reinforcement_count on an agent-labeled memory
1709
+ // while its body now carries user-specific claims — the promotion path
1710
+ // treats reinforced agent memories as shared-promotion candidates. The
1711
+ // create path must run so the fact persists under its own subject.
1712
+ assert.deepEqual(outcome, { action: "created", reason: "metadata_unpreservable" });
1713
+ assert.deepEqual(h.calls.contentUpdates, []);
1714
+ assert.deepEqual(h.calls.frontmatterPatches, []);
1715
+ const history = await listVersions(h.target.path, { enabled: true, maxVersionsPerPage: 20, sidecarDir: ".versions" }, h.storage.dir);
1716
+ assert.equal(history.versions.length, 0, "a bypassed merge must leave no snapshot");
1717
+ });
1718
+
1719
+ test("applySemanticMergeAtPersist: an identical subject still merges", async () => {
1720
+ const h = await harness({ targetSubject: "agent" });
1721
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1722
+ storage: h.storage,
1723
+ content: INCOMING,
1724
+ category: "fact",
1725
+ sources: [INCOMING_SOURCE],
1726
+ incomingMetadata: { subject: "agent" },
1727
+ judgeCall: (options) => acceptingJudge(options),
1728
+ });
1729
+ assert.deepEqual(outcome, { action: "merged", targetId: "fact-target", mergedContent: MERGED, provenancePatched: true });
1730
+ });
1731
+
1732
+ // ── Finding C: promoted copies are reconciled only by the create path ────────
1733
+
1734
+ test("applySemanticMergeAtPersist: a target with promoted copies bypasses the merge before any mutation", async () => {
1735
+ const h = await harness();
1736
+ const probedIds: string[] = [];
1737
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1738
+ storage: h.storage,
1739
+ content: INCOMING,
1740
+ category: "fact",
1741
+ sources: [INCOMING_SOURCE],
1742
+ targetHasPromotedCopies: async (targetId) => {
1743
+ probedIds.push(targetId);
1744
+ return true;
1745
+ },
1746
+ judgeCall: (options) => acceptingJudge(options),
1747
+ });
1748
+
1749
+ assert.deepEqual(outcome, { action: "created", reason: "promoted_copy_present" });
1750
+ assert.deepEqual(probedIds, ["fact-target"], "the probe runs for the judged target only");
1751
+ assert.deepEqual(h.calls.contentUpdates, []);
1752
+ assert.deepEqual(h.calls.frontmatterPatches, []);
1753
+ const history = await listVersions(h.target.path, { enabled: true, maxVersionsPerPage: 20, sidecarDir: ".versions" }, h.storage.dir);
1754
+ assert.equal(history.versions.length, 0, "the bypass must precede the rollback snapshot");
1755
+ });
1756
+
1757
+ // ── Round N+11 (A): only ACTIVE promoted copies block a merge ────────────────
1758
+
1759
+ test("mergeTargetHasPromotedCopies: a namespace holding only a superseded copy does not block the merge (round N+11 A)", async () => {
1760
+ const sourceDir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-scan-src-"));
1761
+ const sharedDir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-scan-shr-"));
1762
+ try {
1763
+ // A superseded copy in the shared namespace, linked back to the target.
1764
+ const copyDir = path.join(sharedDir, "facts", "2026-08-20");
1765
+ await mkdir(copyDir, { recursive: true });
1766
+ await writeFile(
1767
+ path.join(copyDir, "copy-superseded.md"),
1768
+ [
1769
+ "---",
1770
+ "id: fact-copy-superseded",
1771
+ "category: fact",
1772
+ "created: 2026-08-20T00:00:00.000Z",
1773
+ "updated: 2026-08-20T00:00:00.000Z",
1774
+ "status: superseded",
1775
+ "sourceMemoryId: fact-target",
1776
+ "---",
1777
+ "",
1778
+ EXISTING,
1779
+ "",
1780
+ ].join("\n"),
1781
+ "utf8",
1782
+ );
1783
+ const sourceStorage = new StorageManager(sourceDir);
1784
+ const sharedStorage = new StorageManager(sharedDir);
1785
+ await sourceStorage.ensureDirectories();
1786
+ await sharedStorage.ensureDirectories();
1787
+ const config = parseConfig({ memoryDir: sourceDir }) as PluginConfig;
1788
+ const probe = createBatchPromotedCopyProbe(
1789
+ config,
1790
+ () => ({
1791
+ storageFor: async (namespace: string) =>
1792
+ namespace === config.sharedNamespace ? sharedStorage : sourceStorage,
1793
+ }),
1794
+ null,
1795
+ );
1796
+ // A superseded copy serves no body, so it must not read as promoted.
1797
+ assert.equal(
1798
+ await probe.check(sourceStorage, "fact-target"),
1799
+ false,
1800
+ "a namespace holding only a superseded copy must not report promoted copies",
1801
+ );
1802
+
1803
+ // End to end: the REAL probe wired into the merge gate lets judge-approved
1804
+ // updates merge instead of accumulating as new fragments.
1805
+ const h = await harness();
1806
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1807
+ storage: h.storage,
1808
+ content: INCOMING,
1809
+ category: "fact",
1810
+ sources: [INCOMING_SOURCE],
1811
+ targetHasPromotedCopies: (targetId) => probe.check(h.storage, targetId),
1812
+ judgeCall: (options) => acceptingJudge(options),
1813
+ });
1814
+ assert.deepEqual(outcome, { action: "merged", targetId: "fact-target", mergedContent: MERGED, provenancePatched: true });
1815
+ } finally {
1816
+ await StorageManager.clearAllStaticCaches();
1817
+ await rm(sourceDir, { recursive: true, force: true });
1818
+ await rm(sharedDir, { recursive: true, force: true });
1819
+ }
1820
+ });
1821
+
1822
+ // ── Round N+11 (C): a failed CAS must not discard the oldest rollback point ──
1823
+
1824
+ test("applySemanticMergeAtPersist: a failed CAS at a full version history keeps the oldest rollback point (round N+11 C)", async () => {
1825
+ const RACED = "Billing service deploys happen on Tuesdays, except during a freeze.";
1826
+ const h = await harness({ mutateOnWrite: RACED, topLevelConfig: { versioningMaxPerPage: 3 } });
1827
+ const versioning = { enabled: true, maxVersionsPerPage: 3, sidecarDir: ".versions" };
1828
+ for (let i = 1; i <= 3; i++) {
1829
+ await createVersion(h.target.path, `${EXISTING} (history fill ${i})`, "write", versioning, undefined, undefined, h.storage.dir);
1830
+ }
1831
+ const before = await listVersions(h.target.path, versioning, h.storage.dir);
1832
+ assert.equal(before.versions.length, 3, "history starts full at the cap");
1833
+
1834
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1835
+ storage: h.storage,
1836
+ content: INCOMING,
1837
+ category: "fact",
1838
+ sources: [INCOMING_SOURCE],
1839
+ judgeCall: (options) => acceptingJudge(options),
1840
+ });
1841
+ assert.deepEqual(outcome, { action: "created", reason: "target_changed" });
1842
+
1843
+ const after = await listVersions(h.target.path, versioning, h.storage.dir);
1844
+ assert.ok(
1845
+ after.versions.some((v) => v.versionId === "1"),
1846
+ `the oldest rollback point must survive the failed attempt (got ${JSON.stringify(after.versions.map((v) => v.versionId))})`,
1847
+ );
1848
+ });
1849
+
1850
+ test("applySemanticMergeAtPersist: a committed merge still finalizes the prune back to the cap (round N+11 C)", async () => {
1851
+ const h = await harness({ topLevelConfig: { versioningMaxPerPage: 3 } });
1852
+ const versioning = { enabled: true, maxVersionsPerPage: 3, sidecarDir: ".versions" };
1853
+ for (let i = 1; i <= 3; i++) {
1854
+ await createVersion(h.target.path, `${EXISTING} (history fill ${i})`, "write", versioning, undefined, undefined, h.storage.dir);
1855
+ }
1856
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1857
+ storage: h.storage,
1858
+ content: INCOMING,
1859
+ category: "fact",
1860
+ sources: [INCOMING_SOURCE],
1861
+ judgeCall: (options) => acceptingJudge(options),
1862
+ });
1863
+ assert.equal(outcome.action, "merged");
1864
+ const after = await listVersions(h.target.path, versioning, h.storage.dir);
1865
+ assert.equal(after.versions.length, 3, "a committed merge prunes the history back to the configured cap");
1866
+ assert.equal(after.versions.at(-1)?.trigger, "semantic-merge");
1867
+ });
1868
+
1869
+ // ── Round N+12 (A): a rejected frontmatter patch must leave version history
1870
+ // untouched — pruning finalizes only after BOTH compare-and-swaps commit. ────
1871
+
1872
+ test("applySemanticMergeAtPersist: a rejected frontmatter patch at a full version history keeps the oldest rollback point (round N+12 A)", async () => {
1873
+ const h = await harness({ frontmatterFails: true, topLevelConfig: { versioningMaxPerPage: 3 } });
1874
+ const versioning = { enabled: true, maxVersionsPerPage: 3, sidecarDir: ".versions" };
1875
+ for (let i = 1; i <= 3; i++) {
1876
+ await createVersion(h.target.path, `${EXISTING} (history fill ${i})`, "write", versioning, undefined, undefined, h.storage.dir);
1877
+ }
1878
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1879
+ storage: h.storage,
1880
+ content: INCOMING,
1881
+ category: "fact",
1882
+ sources: [INCOMING_SOURCE],
1883
+ judgeCall: (options) => acceptingJudge(options),
1884
+ });
1885
+ assert.deepEqual(outcome, { action: "created", reason: "update_failed" });
1886
+
1887
+ const after = await listVersions(h.target.path, versioning, h.storage.dir);
1888
+ assert.ok(
1889
+ after.versions.some((v) => v.versionId === "1"),
1890
+ `the oldest rollback point must survive the unsuccessful attempt (got ${JSON.stringify(after.versions.map((v) => v.versionId))})`,
1891
+ );
1892
+ assert.equal(
1893
+ await readFile(h.target.path, "utf8").then((raw) => raw.includes(EXISTING)),
1894
+ true,
1895
+ "the rolled-back target body is the pre-merge text",
1896
+ );
1897
+ });
1898
+
1899
+ // ── Round N+13 (B): staging itself mutates history — an aborted attempt must
1900
+ // roll the staged snapshot back out, or repeated failures grow history past
1901
+ // the cap and a later successful merge's prune trades real rollback states
1902
+ // for duplicate failed-attempt snapshots. ────────────────────────────────────
1903
+
1904
+ test("applySemanticMergeAtPersist: a lost CAS race rolls the staged snapshot back out of history (round N+13 B)", async () => {
1905
+ const RACED = "Billing service deploys happen on Tuesdays, except during a freeze.";
1906
+ const h = await harness({ mutateOnWrite: RACED, topLevelConfig: { versioningMaxPerPage: 3 } });
1907
+ const versioning = { enabled: true, maxVersionsPerPage: 3, sidecarDir: ".versions" };
1908
+ for (let i = 1; i <= 3; i++) {
1909
+ await createVersion(h.target.path, `${EXISTING} (history fill ${i})`, "write", versioning, undefined, undefined, h.storage.dir);
1910
+ }
1911
+ const before = await listVersions(h.target.path, versioning, h.storage.dir);
1912
+ assert.equal(before.versions.length, 3, "history starts full at the cap");
1913
+
1914
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1915
+ storage: h.storage,
1916
+ content: INCOMING,
1917
+ category: "fact",
1918
+ sources: [INCOMING_SOURCE],
1919
+ judgeCall: (options) => acceptingJudge(options),
1920
+ });
1921
+ assert.deepEqual(outcome, { action: "created", reason: "target_changed" });
1922
+
1923
+ const after = await listVersions(h.target.path, versioning, h.storage.dir);
1924
+ assert.deepEqual(
1925
+ after.versions.map((v) => v.versionId),
1926
+ before.versions.map((v) => v.versionId),
1927
+ "a lost CAS race must leave exactly the pre-attempt history",
1928
+ );
1929
+ assert.equal(after.currentVersion, before.currentVersion, "currentVersion must return to the pre-attempt newest");
1930
+ });
1931
+
1932
+ test("applySemanticMergeAtPersist: a reverted metadata failure rolls the staged snapshot back out of history (round N+13 B)", async () => {
1933
+ const h = await harness({ frontmatterFails: true, topLevelConfig: { versioningMaxPerPage: 3 } });
1934
+ const versioning = { enabled: true, maxVersionsPerPage: 3, sidecarDir: ".versions" };
1935
+ for (let i = 1; i <= 3; i++) {
1936
+ await createVersion(h.target.path, `${EXISTING} (history fill ${i})`, "write", versioning, undefined, undefined, h.storage.dir);
1937
+ }
1938
+ const before = await listVersions(h.target.path, versioning, h.storage.dir);
1939
+ assert.equal(before.versions.length, 3, "history starts full at the cap");
1940
+
1941
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1942
+ storage: h.storage,
1943
+ content: INCOMING,
1944
+ category: "fact",
1945
+ sources: [INCOMING_SOURCE],
1946
+ judgeCall: (options) => acceptingJudge(options),
1947
+ });
1948
+ assert.deepEqual(outcome, { action: "created", reason: "update_failed" });
1949
+
1950
+ const after = await listVersions(h.target.path, versioning, h.storage.dir);
1951
+ assert.deepEqual(
1952
+ after.versions.map((v) => v.versionId),
1953
+ before.versions.map((v) => v.versionId),
1954
+ "a successfully reverted metadata failure must leave exactly the pre-attempt history",
1955
+ );
1956
+ assert.equal(after.currentVersion, before.currentVersion, "currentVersion must return to the pre-attempt newest");
1957
+ });
1958
+
1959
+ // ── Final round: origin authority, log privacy, least-privilege subject ──────
1960
+
1961
+ test("applySemanticMergeAtPersist: a cross-origin merge into a user-authority target is refused (finding A)", async () => {
1962
+ const h = await harness({ targetOrigin: "user" });
1963
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1964
+ storage: h.storage,
1965
+ content: INCOMING,
1966
+ category: "fact",
1967
+ incomingMetadata: { origin: "tool_output" },
1968
+ judgeCall: (options) => acceptingJudge(options),
1969
+ });
1970
+
1971
+ assert.deepEqual(outcome, { action: "created", reason: "metadata_unpreservable" });
1972
+ assert.deepEqual(h.calls.contentUpdates, [], "the user-authority target must keep its body");
1973
+ assert.equal(
1974
+ (await readFile(h.target.path, "utf8")).includes(INCOMING),
1975
+ false,
1976
+ "the target file must not carry the untrusted text",
1977
+ );
1978
+
1979
+ // The rendered-authority contract, not just frontmatter. Under the target's
1980
+ // retained `origin: user` the merged body would reach model context with NO
1981
+ // authority fence — the escalation the refusal prevents:
1982
+ assert.equal(
1983
+ renderAuthorityBoundContent(MERGED, "user", { enabled: true, untrustedOrigins: DEFAULT_UNTRUSTED_ORIGINS }),
1984
+ MERGED,
1985
+ "the would-be merged body renders UNFENCED under the retained user origin",
1986
+ );
1987
+ // Persisted instead through the create path, the incoming body renders
1988
+ // fenced under its own untrusted origin:
1989
+ assert.equal(
1990
+ renderAuthorityBoundContent(INCOMING, "tool_output", { enabled: true, untrustedOrigins: DEFAULT_UNTRUSTED_ORIGINS }),
1991
+ renderAuthorityFence(INCOMING, "tool_output"),
1992
+ "the created fact's body renders inside the authority fence",
1993
+ );
1994
+ });
1995
+
1996
+ test("applySemanticMergeAtPersist: an identical origin still merges", async () => {
1997
+ const h = await harness({ targetOrigin: "user" });
1998
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
1999
+ storage: h.storage,
2000
+ content: INCOMING,
2001
+ category: "fact",
2002
+ incomingMetadata: { origin: "user" },
2003
+ judgeCall: (options) => acceptingJudge(options),
2004
+ });
2005
+ assert.equal(outcome.action, "merged");
2006
+ // Final round: the promotion re-reads the committed record, so origin is
2007
+ // asserted off that record — the exact source the promotion now stamps from.
2008
+ const committedRecord = await h.storage.getMemoryByIdIncludingArchived("fact-target");
2009
+ assert.equal(committedRecord?.frontmatter.origin, "user");
2010
+ });
2011
+
2012
+ test("applySemanticMergeAtPersist: a user-origin fact still merges into a legacy unstamped target", async () => {
2013
+ const h = await harness();
2014
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2015
+ storage: h.storage,
2016
+ content: INCOMING,
2017
+ category: "fact",
2018
+ incomingMetadata: { origin: "user" },
2019
+ judgeCall: (options) => acceptingJudge(options),
2020
+ });
2021
+ // The unstamped target renders as `unknown` (fenced) at recall, so the
2022
+ // merged body is fenced at least as strictly as the incoming fact — no
2023
+ // escalation, and legacy targets keep receiving user-origin facts.
2024
+ assert.deepEqual(outcome, { action: "merged", targetId: "fact-target", mergedContent: MERGED, provenancePatched: true });
2025
+ });
2026
+
2027
+ test("applySemanticMergeAtPersist: shadow-mode create telemetry carries no fact content (finding B)", async () => {
2028
+ const entries: Array<{ level: string; message: string }> = [];
2029
+ const backend: LoggerBackend = {
2030
+ info: (msg: string) => entries.push({ level: "info", message: msg }),
2031
+ warn: () => {},
2032
+ error: () => {},
2033
+ debug: () => {},
2034
+ };
2035
+ initLogger(backend, true);
2036
+ try {
2037
+ const h = await harness({ config: { shadowMode: true } });
2038
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2039
+ storage: h.storage,
2040
+ content: INCOMING,
2041
+ category: "fact",
2042
+ judgeCall: async () => ({
2043
+ decision: "create",
2044
+ targetId: null,
2045
+ mergedContent: null,
2046
+ reason: "distinct underlying concept",
2047
+ }),
2048
+ });
2049
+ assert.deepEqual(outcome, { action: "created", reason: "judge_create" });
2050
+
2051
+ const line = entries.find((e) => e.message.includes("semantic-merge[shadow]: would create"))
2052
+ ?.message;
2053
+ assert.ok(line, "the shadow create line must be logged at info level");
2054
+ assert.ok(line.includes("category=fact"), "category is logged");
2055
+ assert.ok(line.includes(`length=${INCOMING.length}`), "content length is logged");
2056
+ assert.ok(line.includes('reason="judge_create"'), "reason is logged");
2057
+ assert.ok(!line.includes(INCOMING.slice(0, 60)), "no content prefix may appear");
2058
+ for (const token of INCOMING.split(" ")) {
2059
+ if (token.length >= 4) {
2060
+ assert.ok(!line.includes(token), `log must not carry fact content (found "${token}")`);
2061
+ }
2062
+ }
2063
+ } finally {
2064
+ resetLogger();
2065
+ }
2066
+ });
2067
+
2068
+ test("applySemanticMergeAtPersist: an absent incoming subject never reinforces an agent target (finding C)", async () => {
2069
+ const h = await harness({ targetSubject: "agent" });
2070
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2071
+ storage: h.storage,
2072
+ content: INCOMING,
2073
+ category: "fact",
2074
+ // Subject classification disabled: the write path stamps no subject, and
2075
+ // the guard must treat that absent subject as the least-privileged "user".
2076
+ incomingMetadata: {},
2077
+ judgeCall: (options) => acceptingJudge(options),
2078
+ });
2079
+ assert.deepEqual(outcome, { action: "created", reason: "metadata_unpreservable" });
2080
+ assert.deepEqual(h.calls.contentUpdates, [], "the agent target must keep its body");
2081
+ assert.deepEqual(
2082
+ h.calls.frontmatterPatches,
2083
+ [],
2084
+ "an unclassified fact must not bump reinforcement_count on an agent target",
2085
+ );
2086
+ });
2087
+
2088
+ // ── Final round: the create-path parity gate ─────────────────────────────────
2089
+
2090
+ test("applySemanticMergeAtPersist: a merged outcome carries the committed merged body (finding A)", async () => {
2091
+ const h = await harness();
2092
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2093
+ storage: h.storage,
2094
+ content: INCOMING,
2095
+ category: "fact",
2096
+ sources: [INCOMING_SOURCE],
2097
+ judgeCall: (options) => acceptingJudge(options),
2098
+ });
2099
+ if (outcome.action !== "merged") {
2100
+ assert.fail(`expected a merge, got ${JSON.stringify(outcome)}`);
2101
+ }
2102
+ // The caller's merged-target promotion copies THIS body. It is the judge's
2103
+ // merged text — the committed union of old and new claims — never the
2104
+ // incoming fact alone.
2105
+ assert.equal(outcome.mergedContent, MERGED);
2106
+ assert.notEqual(outcome.mergedContent, INCOMING);
2107
+ const committed = await h.storage.getMemoryByIdIncludingArchived("fact-target");
2108
+ assert.equal(committed?.content, outcome.mergedContent);
2109
+ });
2110
+
2111
+ test("applySemanticMergeAtPersist: a weaker incoming provenance retags the merged body, never upgrades it (finding B)", async () => {
2112
+ const h = await harness({ targetProvenance: "verified" });
2113
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2114
+ storage: h.storage,
2115
+ content: INCOMING,
2116
+ category: "fact",
2117
+ sources: [INCOMING_SOURCE],
2118
+ incomingMetadata: { provenanceStrength: "unverified" },
2119
+ judgeCall: (options) => acceptingJudge(options),
2120
+ });
2121
+ assert.deepEqual(outcome, {
2122
+ action: "merged",
2123
+ targetId: "fact-target",
2124
+ mergedContent: MERGED,
2125
+ provenancePatched: true,
2126
+ });
2127
+ // trust-score.ts maps `verified` to the maximum provenance contribution;
2128
+ // the combined body now holds unverified claims, so the tag must drop.
2129
+ assert.equal(
2130
+ h.calls.frontmatterPatches[0]?.patch.provenance,
2131
+ "unverified",
2132
+ "the merged body must not stay tagged verified",
2133
+ );
2134
+ });
2135
+
2136
+ test("applySemanticMergeAtPersist: an unprovenanced incoming fact downgrades a verified target to none (finding B)", async () => {
2137
+ // Extraction omits `provenance` when the strength is "none", so an absent
2138
+ // incoming value is the weakest case, not a neutral one.
2139
+ const h = await harness({ targetProvenance: "verified" });
2140
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2141
+ storage: h.storage,
2142
+ content: INCOMING,
2143
+ category: "fact",
2144
+ sources: [INCOMING_SOURCE],
2145
+ incomingMetadata: {},
2146
+ judgeCall: (options) => acceptingJudge(options),
2147
+ });
2148
+ assert.equal(outcome.action, "merged");
2149
+ assert.equal(h.calls.frontmatterPatches[0]?.patch.provenance, "none");
2150
+ });
2151
+
2152
+ test("applySemanticMergeAtPersist: equal provenance merges without touching the tag (finding B)", async () => {
2153
+ const h = await harness({ targetProvenance: "unverified" });
2154
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2155
+ storage: h.storage,
2156
+ content: INCOMING,
2157
+ category: "fact",
2158
+ sources: [INCOMING_SOURCE],
2159
+ incomingMetadata: { provenanceStrength: "unverified" },
2160
+ judgeCall: (options) => acceptingJudge(options),
2161
+ });
2162
+ assert.equal(outcome.action, "merged");
2163
+ assert.equal(
2164
+ "provenance" in (h.calls.frontmatterPatches[0]?.patch ?? {}),
2165
+ false,
2166
+ "an equal tag needs no rewrite",
2167
+ );
2168
+ });
2169
+
2170
+ test("applySemanticMergeAtPersist: an incoming faithfulness verdict the target cannot preserve bypasses the merge (finding C)", async () => {
2171
+ // Shadow mode: the contradicted fact carries a verdict but no
2172
+ // pending_review status, so only the parity gate can catch it. The target
2173
+ // keeps `entailed`; the trust stage would keep injecting the combined
2174
+ // body instead of applying the incoming negative evidence.
2175
+ const h = await harness({ targetFaithfulness: { verdict: "entailed" } });
2176
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2177
+ storage: h.storage,
2178
+ content: INCOMING,
2179
+ category: "fact",
2180
+ sources: [INCOMING_SOURCE],
2181
+ incomingMetadata: { faithfulness: { verdict: "contradicted" } },
2182
+ judgeCall: (options) => acceptingJudge(options),
2183
+ });
2184
+ assert.deepEqual(outcome, { action: "created", reason: "metadata_unpreservable" });
2185
+ assert.deepEqual(h.calls.contentUpdates, []);
2186
+ assert.deepEqual(h.calls.frontmatterPatches, []);
2187
+ });
2188
+
2189
+ test("applySemanticMergeAtPersist: an effectively identical faithfulness verdict still merges (finding C)", async () => {
2190
+ // `skipped_no_span` and `unchecked` both read as "unchecked" to the trust
2191
+ // stage, so the verdict IS preserved and the merge proceeds.
2192
+ const h = await harness({ targetFaithfulness: { verdict: "skipped_no_span" } });
2193
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2194
+ storage: h.storage,
2195
+ content: INCOMING,
2196
+ category: "fact",
2197
+ sources: [INCOMING_SOURCE],
2198
+ incomingMetadata: { faithfulness: { verdict: "unchecked" } },
2199
+ judgeCall: (options) => acceptingJudge(options),
2200
+ });
2201
+ assert.equal(outcome.action, "merged");
2202
+ });
2203
+
2204
+ test("applySemanticMergeAtPersist: a one-sided faithfulness verdict bypasses the merge — gate-disabled incoming into a judged target (finding C)", async () => {
2205
+ // Faithfulness gate off for the incoming fact: its claims were never
2206
+ // checked, but the target's `entailed` maps to the trust stage's maximum
2207
+ // contribution. Merging would let unchecked claims inherit an entailment
2208
+ // rendered over the target's claims alone, so the merge bypasses and the
2209
+ // create path stores the fact without a verdict, exactly as it would.
2210
+ const h = await harness({ targetFaithfulness: { verdict: "entailed" } });
2211
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2212
+ storage: h.storage,
2213
+ content: INCOMING,
2214
+ category: "fact",
2215
+ sources: [INCOMING_SOURCE],
2216
+ judgeCall: (options) => acceptingJudge(options),
2217
+ });
2218
+ assert.deepEqual(outcome, { action: "created", reason: "metadata_unpreservable" });
2219
+ assert.deepEqual(h.calls.contentUpdates, []);
2220
+ assert.deepEqual(h.calls.frontmatterPatches, []);
2221
+ });
2222
+
2223
+ test("applySemanticMergeAtPersist: a one-sided faithfulness verdict bypasses the merge — judged incoming into a legacy target (finding C)", async () => {
2224
+ // Mirror of the gate-off case: the incoming `entailed` covers only the
2225
+ // incoming claims, and no verdict was ever rendered over the target's.
2226
+ // The combined body must not carry a one-sided verdict, so the merge
2227
+ // bypasses and the create path persists the verdict it computed.
2228
+ const h = await harness();
2229
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2230
+ storage: h.storage,
2231
+ content: INCOMING,
2232
+ category: "fact",
2233
+ sources: [INCOMING_SOURCE],
2234
+ incomingMetadata: { faithfulness: { verdict: "entailed" } },
2235
+ judgeCall: (options) => acceptingJudge(options),
2236
+ });
2237
+ assert.deepEqual(outcome, { action: "created", reason: "metadata_unpreservable" });
2238
+ assert.deepEqual(h.calls.contentUpdates, []);
2239
+ assert.deepEqual(h.calls.frontmatterPatches, []);
2240
+ });
2241
+
2242
+ test("applySemanticMergeAtPersist: even an EQUAL entailed verdict bypasses the merge (round N+9 A)", async () => {
2243
+ // Both sides carry `entailed` — the equality the finding-C gate called
2244
+ // preservable. But the target's verdict was rendered over its PRE-merge
2245
+ // body; the committed record would hold the judge's mergedContent, a body
2246
+ // the faithfulness gate never saw. The trust stage maps `entailed` to its
2247
+ // maximum contribution, so the committed record must not carry it: the
2248
+ // merge bypasses and the create path persists the verdict it computed.
2249
+ // (Re-running the gate inside the compare-and-swap window would need a
2250
+ // per-merge LLM call; bypass is the safe mechanism.)
2251
+ const h = await harness({ targetFaithfulness: { verdict: "entailed" } });
2252
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2253
+ storage: h.storage,
2254
+ content: INCOMING,
2255
+ category: "fact",
2256
+ sources: [INCOMING_SOURCE],
2257
+ incomingMetadata: { faithfulness: { verdict: "entailed" } },
2258
+ judgeCall: (options) => acceptingJudge(options),
2259
+ });
2260
+ assert.deepEqual(outcome, { action: "created", reason: "metadata_unpreservable" });
2261
+ assert.deepEqual(h.calls.contentUpdates, []);
2262
+ assert.deepEqual(h.calls.frontmatterPatches, []);
2263
+ });
2264
+
2265
+ test("applySemanticMergeAtPersist: an unscoped fact never merges into a connector-owned cold target (finding D)", async () => {
2266
+ // The lookup hit carries NO sourceConnector — exactly what the hot-only
2267
+ // enrichment in persistence-index.ts produces for a cold-tier target or a
2268
+ // failed enrichment read. The lookup-side scope check therefore sees two
2269
+ // unscoped sides and passes; only the cold-aware re-read of the target can
2270
+ // supply the authoritative connector and fail the merge closed.
2271
+ const h = await harness({
2272
+ targetSourceConnector: "connector-a",
2273
+ lookupHits: [{ id: "fact-target", score: 0.85 }],
2274
+ });
2275
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2276
+ storage: h.storage,
2277
+ content: INCOMING,
2278
+ category: "fact",
2279
+ sources: [INCOMING_SOURCE],
2280
+ judgeCall: (options) => acceptingJudge(options),
2281
+ });
2282
+ assert.deepEqual(outcome, { action: "created", reason: "metadata_unpreservable" });
2283
+ assert.deepEqual(h.calls.contentUpdates, [], "the connector-owned body must keep its claims");
2284
+ assert.equal(
2285
+ (await readFile(h.target.path, "utf8")).includes(MERGED),
2286
+ false,
2287
+ "the target file must not carry the merged body",
2288
+ );
2289
+ });
2290
+
2291
+ test("applySemanticMergeAtPersist: an identical connector scope still merges (finding D)", async () => {
2292
+ const h = await harness({
2293
+ targetSourceConnector: "connector-a",
2294
+ lookupHits: [{ id: "fact-target", score: 0.85, sourceConnector: "connector-a" }],
2295
+ });
2296
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2297
+ storage: h.storage,
2298
+ content: INCOMING,
2299
+ category: "fact",
2300
+ sources: [INCOMING_SOURCE],
2301
+ sourceConnector: "connector-a",
2302
+ judgeCall: (options) => acceptingJudge(options),
2303
+ });
2304
+ assert.equal(outcome.action, "merged");
2305
+ });
2306
+
2307
+ // ── Final round: min confidence across a merge; no promotion from an inactive target ──
2308
+
2309
+ test("applySemanticMergeAtPersist: a lower incoming confidence downgrades the merged record (final round A)", async () => {
2310
+ const h = await harness({ targetConfidence: 0.9 });
2311
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2312
+ storage: h.storage,
2313
+ content: INCOMING,
2314
+ category: "fact",
2315
+ sources: [INCOMING_SOURCE],
2316
+ incomingMetadata: { confidence: 0.5 },
2317
+ judgeCall: (options) => acceptingJudge(options),
2318
+ });
2319
+ assert.equal(outcome.action, "merged");
2320
+ // The create path would have stored the incoming fact at 0.5 ("inferred").
2321
+ // The merged record must not keep 0.9/"explicit": lifecycle scoring,
2322
+ // preference consolidation, and the merged-target promotion all read these
2323
+ // fields off the committed record.
2324
+ const patch = h.calls.frontmatterPatches[0]?.patch;
2325
+ assert.equal(patch?.confidence, 0.5);
2326
+ assert.equal(patch?.confidenceTier, "inferred");
2327
+ });
2328
+
2329
+ test("applySemanticMergeAtPersist: the committed merged record carries the lower confidence (final round A)", async () => {
2330
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-conf-"));
2331
+ const storage = new StorageManager(dir);
2332
+ await storage.ensureDirectories();
2333
+ const created = await storage.writeMemory("fact", EXISTING, {
2334
+ source: "test",
2335
+ confidence: 0.9,
2336
+ });
2337
+ const deps = {
2338
+ config: parseConfig({
2339
+ memoryDir: dir,
2340
+ versioningEnabled: true,
2341
+ semanticMerge: { enabled: true },
2342
+ }),
2343
+ getLocalLlm: () => null,
2344
+ semanticDedupLookup: async () => [{ id: created.id, score: 0.85 }],
2345
+ indexPersistedMemory: async () => {},
2346
+ } as unknown as ExtractionPersistDeps;
2347
+ const outcome = await applySemanticMergeAtPersist(deps, {
2348
+ storage,
2349
+ content: INCOMING,
2350
+ category: "fact",
2351
+ sources: [INCOMING_SOURCE],
2352
+ incomingMetadata: { confidence: 0.5 },
2353
+ judgeCall: (options) => acceptingJudge(options),
2354
+ });
2355
+ assert.equal(outcome.action, "merged");
2356
+ const committed = await storage.getMemoryByIdIncludingArchived(created.id);
2357
+ assert.equal(committed?.frontmatter.confidence, 0.5);
2358
+ assert.equal(committed?.frontmatter.confidenceTier, "inferred");
2359
+ // The committed record is the promotion payload's sole source, so the
2360
+ // shared/profile copy stamps the downgraded value too.
2361
+ const payload = await buildMergedTargetPromotionPayload(storage, {
2362
+ targetId: created.id,
2363
+ mergedContent: MERGED,
2364
+ provenancePatched: true,
2365
+ });
2366
+ assert.ok(payload);
2367
+ assert.equal(payload.confidence, 0.5);
2368
+ });
2369
+
2370
+ test("applySemanticMergeAtPersist: an incoming confidence at or above the target's never rewrites confidence (final round A)", async () => {
2371
+ const h = await harness({ targetConfidence: 0.9 });
2372
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2373
+ storage: h.storage,
2374
+ content: INCOMING,
2375
+ category: "fact",
2376
+ sources: [INCOMING_SOURCE],
2377
+ incomingMetadata: { confidence: 0.95 },
2378
+ judgeCall: (options) => acceptingJudge(options),
2379
+ });
2380
+ assert.equal(outcome.action, "merged");
2381
+ // min(0.95, 0.9) is the target's own 0.9 — already on the record, so the
2382
+ // patch carries no confidence keys at all.
2383
+ const patch = h.calls.frontmatterPatches[0]?.patch ?? {};
2384
+ assert.equal("confidence" in patch, false);
2385
+ assert.equal("confidenceTier" in patch, false);
2386
+ });
2387
+
2388
+ test("applySemanticMergeAtPersist: an unreadable incoming confidence bypasses the merge (final round A)", async () => {
2389
+ for (const bad of [Number.NaN, 1.5, -0.1]) {
2390
+ const h = await harness({ targetConfidence: 0.9 });
2391
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2392
+ storage: h.storage,
2393
+ content: INCOMING,
2394
+ category: "fact",
2395
+ incomingMetadata: { confidence: bad },
2396
+ judgeCall: (options) => acceptingJudge(options),
2397
+ });
2398
+ assert.deepEqual(
2399
+ outcome,
2400
+ { action: "created", reason: "metadata_unpreservable" },
2401
+ String(bad),
2402
+ );
2403
+ assert.deepEqual(h.calls.contentUpdates, [], String(bad));
2404
+ }
2405
+ });
2406
+
2407
+ test("buildMergedTargetPromotionPayload: never promotes from an inactive committed target (final round B)", async () => {
2408
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-inactive-"));
2409
+ const storage = new StorageManager(dir);
2410
+ await storage.ensureDirectories();
2411
+ const created = await storage.writeMemory("fact", EXISTING, { source: "test" });
2412
+ // The merge has committed — the target body IS the merged body. Then a
2413
+ // concurrent lifecycle operation retires the target BEFORE the promotion
2414
+ // reread: the multi-writer interleaving, simulated directly.
2415
+ const snapshot = await storage.getMemoryByIdIncludingArchived(created.id);
2416
+ assert.ok(snapshot);
2417
+ assert.equal(await storage.updateMemoryIfUnchanged(snapshot, MERGED), true);
2418
+ for (const status of ["superseded", "archived"] as const) {
2419
+ assert.equal(
2420
+ await storage.updateMemoryFrontmatter(created.id, { status }),
2421
+ true,
2422
+ status,
2423
+ );
2424
+ // Null, never a payload: the caller skips promoteMemoryToShared, so no
2425
+ // new active copy resurrects what the lifecycle operation retired.
2426
+ assert.equal(
2427
+ await buildMergedTargetPromotionPayload(storage, {
2428
+ targetId: created.id,
2429
+ mergedContent: MERGED,
2430
+ provenancePatched: true,
2431
+ }),
2432
+ null,
2433
+ status,
2434
+ );
2435
+ }
2436
+ // A still-active committed target still grounds the promotion.
2437
+ assert.equal(
2438
+ await storage.updateMemoryFrontmatter(created.id, { status: "active" }),
2439
+ true,
2440
+ );
2441
+ const payload = await buildMergedTargetPromotionPayload(storage, {
2442
+ targetId: created.id,
2443
+ mergedContent: MERGED,
2444
+ provenancePatched: true,
2445
+ });
2446
+ assert.ok(payload);
2447
+ assert.equal(payload.content, MERGED);
2448
+ });
2449
+
2450
+ function realStorageDeps(
2451
+ dir: string,
2452
+ createdId: string,
2453
+ hitConnector?: string,
2454
+ ): ExtractionPersistDeps {
2455
+ return {
2456
+ config: parseConfig({
2457
+ memoryDir: dir,
2458
+ versioningEnabled: true,
2459
+ semanticMerge: { enabled: true },
2460
+ }),
2461
+ getLocalLlm: () => null,
2462
+ semanticDedupLookup: async () => [
2463
+ { id: createdId, score: 0.85, ...(hitConnector ? { sourceConnector: hitConnector } : {}) },
2464
+ ],
2465
+ indexPersistedMemory: async () => {},
2466
+ } as unknown as ExtractionPersistDeps;
2467
+ }
2468
+
2469
+ test("buildMergedTargetPromotionPayload: carries the committed tool-scope marker and connector (finding A)", async () => {
2470
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-scope-"));
2471
+ const storage = new StorageManager(dir);
2472
+ await storage.ensureDirectories();
2473
+ const created = await storage.writeMemory("fact", EXISTING, {
2474
+ source: "test",
2475
+ sourceConnector: "connector-a",
2476
+ toolScoped: true,
2477
+ });
2478
+ // Same-connector incoming fact that is NOT independently tool-scoped: the
2479
+ // parity gate keeps the target's narrower scope and lets the merge run.
2480
+ const outcome = await applySemanticMergeAtPersist(
2481
+ realStorageDeps(dir, created.id, "connector-a"),
2482
+ {
2483
+ storage,
2484
+ content: INCOMING,
2485
+ category: "fact",
2486
+ sources: [INCOMING_SOURCE],
2487
+ sourceConnector: "connector-a",
2488
+ judgeCall: (options) => acceptingJudge(options),
2489
+ },
2490
+ );
2491
+ assert.equal(outcome.action, "merged");
2492
+ const payload = await buildMergedTargetPromotionPayload(storage, {
2493
+ targetId: created.id,
2494
+ mergedContent: MERGED,
2495
+ provenancePatched: true,
2496
+ });
2497
+ assert.ok(payload);
2498
+ // The content heuristics alone would NOT withhold the merged body — only
2499
+ // the committed marker keeps the promoted copy out of the shared namespace.
2500
+ assert.equal(
2501
+ withholdToolScopedFromSharedNamespace({
2502
+ content: payload.content,
2503
+ sourceConnector: payload.sourceConnector,
2504
+ }),
2505
+ false,
2506
+ );
2507
+ assert.equal(payload.toolScoped, true);
2508
+ assert.equal(payload.sourceConnector, "connector-a");
2509
+ // The exact gate promoteMemoryToShared runs over the spread payload.
2510
+ assert.equal(promotionWithholdsToolScope(payload), true);
2511
+ });
2512
+
2513
+ test("buildMergedTargetPromotionPayload: an unscoped target promotes a payload without scope fields (finding A)", async () => {
2514
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-plain-"));
2515
+ const storage = new StorageManager(dir);
2516
+ await storage.ensureDirectories();
2517
+ const created = await storage.writeMemory("fact", EXISTING, { source: "test" });
2518
+ const outcome = await applySemanticMergeAtPersist(realStorageDeps(dir, created.id), {
2519
+ storage,
2520
+ content: INCOMING,
2521
+ category: "fact",
2522
+ sources: [INCOMING_SOURCE],
2523
+ judgeCall: (options) => acceptingJudge(options),
2524
+ });
2525
+ assert.equal(outcome.action, "merged");
2526
+ const payload = await buildMergedTargetPromotionPayload(storage, {
2527
+ targetId: created.id,
2528
+ mergedContent: MERGED,
2529
+ provenancePatched: true,
2530
+ });
2531
+ assert.ok(payload);
2532
+ assert.equal("toolScoped" in payload, false);
2533
+ assert.equal("sourceConnector" in payload, false);
2534
+ assert.equal(promotionWithholdsToolScope(payload), false);
2535
+ });
2536
+
2537
+ test("applySemanticMergeAtPersist: suggested links attach to the merged target and stay traversable (finding B)", async () => {
2538
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-links-"));
2539
+ const storage = new StorageManager(dir);
2540
+ await storage.ensureDirectories();
2541
+ const created = await storage.writeMemory("fact", EXISTING, { source: "test" });
2542
+ const neighbor = await storage.writeMemory(
2543
+ "fact",
2544
+ "Releases are cut after the deploy window closes.",
2545
+ { source: "test" },
2546
+ );
2547
+ const linked = await storage.writeMemory(
2548
+ "fact",
2549
+ "The on-call rotation follows the release train.",
2550
+ { source: "test" },
2551
+ );
2552
+ await storage.addLinksToMemory(created.id, [
2553
+ { targetId: linked.id, linkType: "supports", strength: 0.9 },
2554
+ ]);
2555
+ const outcome = await applySemanticMergeAtPersist(realStorageDeps(dir, created.id), {
2556
+ storage,
2557
+ content: INCOMING,
2558
+ category: "fact",
2559
+ sources: [INCOMING_SOURCE],
2560
+ incomingLinks: [
2561
+ { targetId: neighbor.id, linkType: "related", strength: 0.8, reason: "same cadence" },
2562
+ { targetId: linked.id, linkType: "supports", strength: 0.5 },
2563
+ ],
2564
+ judgeCall: (options) => acceptingJudge(options),
2565
+ });
2566
+ assert.equal(outcome.action, "merged");
2567
+ // Traversal surfaces (dependency propagation, recall-navigate) read
2568
+ // frontmatter.links off the committed record — so the round-tripped
2569
+ // record IS the navigation contract.
2570
+ const committed = await storage.getMemoryByIdIncludingArchived(created.id);
2571
+ assert.ok(committed);
2572
+ const links = committed.frontmatter.links ?? [];
2573
+ assert.equal(links.length, 2, JSON.stringify(links));
2574
+ assert.ok(
2575
+ links.some((l) => l.targetId === neighbor.id && l.linkType === "related"),
2576
+ "the suggested link must be attached to the target",
2577
+ );
2578
+ assert.ok(
2579
+ links.some(
2580
+ (l) => l.targetId === linked.id && l.linkType === "supports" && l.strength === 0.9,
2581
+ ),
2582
+ "the committed link is deduped against, not replaced by, the incoming duplicate",
2583
+ );
2584
+ });
2585
+
2586
+ test("applySemanticMergeAtPersist: no suggested links leaves the target's committed links untouched (finding B)", async () => {
2587
+ const h = await harness();
2588
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2589
+ storage: h.storage,
2590
+ content: INCOMING,
2591
+ category: "fact",
2592
+ sources: [INCOMING_SOURCE],
2593
+ judgeCall: (options) => acceptingJudge(options),
2594
+ });
2595
+ assert.equal(outcome.action, "merged");
2596
+ // An empty links patch would ERASE committed links — the key must be absent.
2597
+ assert.equal("links" in (h.calls.frontmatterPatches[0]?.patch ?? {}), false);
2598
+ });
2599
+
2600
+ test("applySemanticMergeAtPersist: a suggested link naming the merge target itself is discarded, not attached (finding B)", async () => {
2601
+ // Memory linking and the merge judge both search on the incoming content,
2602
+ // so the suggested neighbor is often the merge target itself. Attaching it
2603
+ // would make the surviving record its own neighbor — recall-navigate would
2604
+ // return the current memory and burn traversal budget for nothing.
2605
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-selflink-"));
2606
+ const storage = new StorageManager(dir);
2607
+ await storage.ensureDirectories();
2608
+ const created = await storage.writeMemory("fact", EXISTING, { source: "test" });
2609
+ const neighbor = await storage.writeMemory(
2610
+ "fact",
2611
+ "Releases are cut after the deploy window closes.",
2612
+ { source: "test" },
2613
+ );
2614
+ const outcome = await applySemanticMergeAtPersist(realStorageDeps(dir, created.id), {
2615
+ storage,
2616
+ content: INCOMING,
2617
+ category: "fact",
2618
+ sources: [INCOMING_SOURCE],
2619
+ incomingLinks: [
2620
+ { targetId: created.id, linkType: "related", strength: 0.8, reason: "same cadence" },
2621
+ { targetId: neighbor.id, linkType: "related", strength: 0.7 },
2622
+ ],
2623
+ judgeCall: (options) => acceptingJudge(options),
2624
+ });
2625
+ assert.equal(outcome.action, "merged");
2626
+ const committed = await storage.getMemoryByIdIncludingArchived(created.id);
2627
+ assert.ok(committed);
2628
+ const links = committed.frontmatter.links ?? [];
2629
+ assert.equal(
2630
+ links.some((l) => l.targetId === created.id),
2631
+ false,
2632
+ "no self-edge on the committed record",
2633
+ );
2634
+ assert.ok(
2635
+ links.some((l) => l.targetId === neighbor.id && l.linkType === "related"),
2636
+ "other suggested links still attach",
2637
+ );
2638
+ });
2639
+
2640
+ test("parseSemanticMergeConfig: unknown or never-mergeable categories are rejected with the valid list (finding C)", () => {
2641
+ for (const bad of [["facts"], ["fact", "procedue"], ["procedure"], ["reasoning_trace"], ["moment"], ["correction"]]) {
2642
+ assert.throws(
2643
+ () => parseSemanticMergeConfig({ semanticMerge: { categories: bad } }),
2644
+ /Valid categories: fact, preference, entity, decision, relationship, principle, commitment, skill, rule/,
2645
+ JSON.stringify(bad),
2646
+ );
2647
+ }
2648
+ assert.throws(
2649
+ () => parseSemanticMergeConfig({ semanticMerge: { categories: ["facts"] } }),
2650
+ /The episodic and immutable categories \(procedure, reasoning_trace, moment, correction\) never merge/,
2651
+ );
2652
+ assert.deepEqual(
2653
+ parseSemanticMergeConfig({
2654
+ semanticMerge: { categories: [...MERGEABLE_MEMORY_CATEGORIES, "rule"] },
2655
+ }).semanticMerge.categories,
2656
+ [...MERGEABLE_MEMORY_CATEGORIES, "rule"],
2657
+ );
2658
+ });
2659
+
2660
+ // ── Round N+2: validity bounds, memoryKind parity, raw hash, strict config ───
2661
+
2662
+ test("applySemanticMergeAtPersist: a target with invalid_at never takes unbounded claims (round N+2 A)", async () => {
2663
+ // inferMemoryStatus ignores temporal validity, so this target is still
2664
+ // lifecycle-active — only the parity gate stands between the fresh claim
2665
+ // and isValidityExpiredNow removing the merged body from normal recall.
2666
+ const h = await harness({ targetInvalidAt: "2026-08-01T00:00:00.000Z" });
2667
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2668
+ storage: h.storage,
2669
+ content: INCOMING,
2670
+ category: "fact",
2671
+ sources: [INCOMING_SOURCE],
2672
+ incomingMetadata: {},
2673
+ judgeCall: (options) => acceptingJudge(options),
2674
+ });
2675
+ assert.deepEqual(outcome, { action: "created", reason: "metadata_unpreservable" });
2676
+ assert.deepEqual(h.calls.contentUpdates, []);
2677
+ assert.deepEqual(h.calls.frontmatterPatches, []);
2678
+
2679
+ // A valid_at the incoming fact does not carry is equally unmergable: the
2680
+ // merged body would silently inherit the target's bound.
2681
+ const bounded = await harness({ targetValidAt: "2026-08-01T00:00:00.000Z" });
2682
+ const boundedOutcome = await applySemanticMergeAtPersist(bounded.deps, {
2683
+ storage: bounded.storage,
2684
+ content: INCOMING,
2685
+ category: "fact",
2686
+ sources: [INCOMING_SOURCE],
2687
+ incomingMetadata: {},
2688
+ judgeCall: (options) => acceptingJudge(options),
2689
+ });
2690
+ assert.deepEqual(boundedOutcome, { action: "created", reason: "metadata_unpreservable" });
2691
+ assert.deepEqual(bounded.calls.contentUpdates, []);
2692
+ });
2693
+
2694
+ test("applySemanticMergeAtPersist: a bounded-compatible pair still merges (round N+2 A)", async () => {
2695
+ const VALID = "2026-08-01T00:00:00.000Z";
2696
+ const h = await harness({ targetValidAt: VALID });
2697
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2698
+ storage: h.storage,
2699
+ content: INCOMING,
2700
+ category: "fact",
2701
+ sources: [INCOMING_SOURCE],
2702
+ incomingMetadata: { validAt: VALID },
2703
+ judgeCall: (options) => acceptingJudge(options),
2704
+ });
2705
+ assert.equal(outcome.action, "merged");
2706
+ });
2707
+
2708
+ test("applySemanticMergeAtPersist: mismatched memoryKind bypasses to the create path (round N+2 B)", async () => {
2709
+ // A time-specific (episode) fact into a note target: the merged record
2710
+ // would keep `note`, so the incoming claims never reach the episode cache
2711
+ // or the episode-only verification/promotion paths. The refusal is the
2712
+ // documented rule — episode-cache membership follows the record's kind,
2713
+ // and the create path stamps the kind it computed.
2714
+ const episode = await harness({ targetMemoryKind: "note" });
2715
+ const episodeOutcome = await applySemanticMergeAtPersist(episode.deps, {
2716
+ storage: episode.storage,
2717
+ content: INCOMING,
2718
+ category: "fact",
2719
+ sources: [INCOMING_SOURCE],
2720
+ incomingMetadata: { memoryKind: "episode" },
2721
+ judgeCall: (options) => acceptingJudge(options),
2722
+ });
2723
+ assert.deepEqual(episodeOutcome, { action: "created", reason: "metadata_unpreservable" });
2724
+ assert.deepEqual(episode.calls.contentUpdates, []);
2725
+ assert.deepEqual(episode.calls.frontmatterPatches, []);
2726
+
2727
+ // The reverse: a stable note into an episode target would wrongly expose
2728
+ // the merged claims through the episode-only paths.
2729
+ const note = await harness({ targetMemoryKind: "episode" });
2730
+ const noteOutcome = await applySemanticMergeAtPersist(note.deps, {
2731
+ storage: note.storage,
2732
+ content: INCOMING,
2733
+ category: "fact",
2734
+ sources: [INCOMING_SOURCE],
2735
+ incomingMetadata: { memoryKind: "note" },
2736
+ judgeCall: (options) => acceptingJudge(options),
2737
+ });
2738
+ assert.deepEqual(noteOutcome, { action: "created", reason: "metadata_unpreservable" });
2739
+ assert.deepEqual(note.calls.contentUpdates, []);
2740
+
2741
+ // A kinded fact into an unkinded legacy target also bypasses: the merged
2742
+ // record would carry no kind for claims the classifier did see.
2743
+ const legacy = await harness();
2744
+ const legacyOutcome = await applySemanticMergeAtPersist(legacy.deps, {
2745
+ storage: legacy.storage,
2746
+ content: INCOMING,
2747
+ category: "fact",
2748
+ sources: [INCOMING_SOURCE],
2749
+ incomingMetadata: { memoryKind: "note" },
2750
+ judgeCall: (options) => acceptingJudge(options),
2751
+ });
2752
+ assert.deepEqual(legacyOutcome, { action: "created", reason: "metadata_unpreservable" });
2753
+ });
2754
+
2755
+ test("applySemanticMergeAtPersist: equal kinds and classification-off still merge (round N+2 B)", async () => {
2756
+ const equal = await harness({ targetMemoryKind: "note" });
2757
+ const equalOutcome = await applySemanticMergeAtPersist(equal.deps, {
2758
+ storage: equal.storage,
2759
+ content: INCOMING,
2760
+ category: "fact",
2761
+ sources: [INCOMING_SOURCE],
2762
+ incomingMetadata: { memoryKind: "note" },
2763
+ judgeCall: (options) => acceptingJudge(options),
2764
+ });
2765
+ assert.equal(equalOutcome.action, "merged");
2766
+
2767
+ // episodeNoteMode off: the incoming fact carries no kind, no episode path
2768
+ // consults the field, and a kinded target keeps merging.
2769
+ const off = await harness({ targetMemoryKind: "note" });
2770
+ const offOutcome = await applySemanticMergeAtPersist(off.deps, {
2771
+ storage: off.storage,
2772
+ content: INCOMING,
2773
+ category: "fact",
2774
+ sources: [INCOMING_SOURCE],
2775
+ incomingMetadata: {},
2776
+ judgeCall: (options) => acceptingJudge(options),
2777
+ });
2778
+ assert.equal(offOutcome.action, "merged");
2779
+ });
2780
+
2781
+ test("applySemanticMergeAtPersist: a cited merged body hashes as its raw pre-citation form (round N+2 C)", async () => {
2782
+ const CITED_MERGED = `${MERGED} [Source: agent=test-agent, session=project/example/1, ts=2026-08-20T00:00:00Z]`;
2783
+ const h = await harness({ topLevelConfig: { inlineSourceAttributionEnabled: true } });
2784
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
2785
+ storage: h.storage,
2786
+ content: INCOMING,
2787
+ category: "fact",
2788
+ sources: [INCOMING_SOURCE],
2789
+ judgeCall: async () => ({
2790
+ decision: "merge",
2791
+ targetId: "fact-target",
2792
+ mergedContent: CITED_MERGED,
2793
+ reason: "same cadence, citation retained from the stored target",
2794
+ }),
2795
+ });
2796
+ assert.equal(outcome.action, "merged");
2797
+ const stamped = h.calls.frontmatterPatches.at(-1)?.patch.contentHash;
2798
+ // The ordinary write path hashes sanitizeMemoryContent(contentHashSource)
2799
+ // — the raw fact body BEFORE any citation is attached (storage.ts).
2800
+ const ordinaryWriteHash = ContentHashIndex.computeHash(sanitizeMemoryContent(MERGED).text);
2801
+ const citedFormHash = ContentHashIndex.computeHash(sanitizeMemoryContent(CITED_MERGED).text);
2802
+ assert.equal(stamped, ordinaryWriteHash);
2803
+ assert.notEqual(stamped, citedFormHash);
2804
+ });
2805
+
2806
+ test("applySemanticMergeAtPersist: real storage dedups the raw equivalent of a cited merge (round N+2 C)", async () => {
2807
+ const CITED_MERGED = `${MERGED} [Source: agent=test-agent, session=project/example/1, ts=2026-08-20T00:00:00Z]`;
2808
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-cited-"));
2809
+ const storage = new StorageManager(dir);
2810
+ await storage.ensureDirectories();
2811
+ const created = await storage.writeMemory("fact", EXISTING, { source: "test" });
2812
+ const deps = {
2813
+ config: parseConfig({
2814
+ memoryDir: dir,
2815
+ versioningEnabled: true,
2816
+ inlineSourceAttributionEnabled: true,
2817
+ semanticMerge: { enabled: true },
2818
+ }),
2819
+ getLocalLlm: () => null,
2820
+ semanticDedupLookup: async () => [{ id: created.id, score: 0.85 }],
2821
+ indexPersistedMemory: async () => {},
2822
+ } as unknown as ExtractionPersistDeps;
2823
+ const outcome = await applySemanticMergeAtPersist(deps, {
2824
+ storage,
2825
+ content: INCOMING,
2826
+ category: "fact",
2827
+ sources: [INCOMING_SOURCE],
2828
+ judgeCall: async () => ({
2829
+ decision: "merge",
2830
+ targetId: created.id,
2831
+ mergedContent: CITED_MERGED,
2832
+ reason: "same cadence, citation retained",
2833
+ }),
2834
+ });
2835
+ assert.equal(outcome.action, "merged");
2836
+ // The raw-body identity is registered, so the equivalent raw fact dedups
2837
+ // instead of fragmenting; the cited form is NOT a separate identity.
2838
+ assert.equal(await storage.hasFactContentHash(MERGED), true);
2839
+ assert.equal(await storage.hasFactContentHash(CITED_MERGED), false);
2840
+ });
2841
+
2842
+ test("parseSemanticMergeConfig: present-but-unparseable maxCandidates throws; absent still defaults (round N+2 D)", () => {
2843
+ for (const bad of [
2844
+ "abc",
2845
+ Number.NaN,
2846
+ Number.POSITIVE_INFINITY,
2847
+ Number.NEGATIVE_INFINITY,
2848
+ { count: 3 },
2849
+ null,
2850
+ ]) {
2851
+ assert.throws(
2852
+ () => parseSemanticMergeConfig({ semanticMerge: { enabled: true, maxCandidates: bad } }),
2853
+ /semanticMerge\.maxCandidates must be an integer >= 0/,
2854
+ String(bad),
2855
+ );
2856
+ }
2857
+ // Only an absent key takes the default; an unparseable value under
2858
+ // enabled: true must not silently arm judge lookups at 3.
2859
+ assert.equal(
2860
+ parseSemanticMergeConfig({ semanticMerge: {} }).semanticMerge.maxCandidates,
2861
+ DEFAULT_SEMANTIC_MERGE_CANDIDATES,
2862
+ );
2863
+ assert.equal(
2864
+ parseSemanticMergeConfig({ semanticMerge: { maxCandidates: "12" } }).semanticMerge.maxCandidates,
2865
+ 12,
2866
+ );
2867
+ });
2868
+ // ── Round N+7: trust gate, promotion reconciliation, citation, graph parity ──
2869
+
2870
+ test("buildMergedTargetPromotionPayload: a degraded merge never yields a promotion payload (round N+7 A)", async () => {
2871
+ // Patch fails AND rollback fails: the target holds the merged body with its
2872
+ // OLD confidence/provenance/sources/hash. A payload built off that record
2873
+ // would publish the merged claims under the target's stronger pre-merge
2874
+ // metadata, so the payload builder must refuse the degraded outcome.
2875
+ const h = await harness({ frontmatterFails: true, rollbackFails: true });
2876
+ const degraded = await applySemanticMergeAtPersist(h.deps, {
2877
+ storage: h.storage,
2878
+ content: INCOMING,
2879
+ category: "fact",
2880
+ judgeCall: (options) => acceptingJudge(options),
2881
+ });
2882
+ assert.equal(degraded.action, "merged");
2883
+ if (degraded.action !== "merged") return;
2884
+ assert.equal(degraded.provenancePatched, false);
2885
+ assert.equal(
2886
+ await buildMergedTargetPromotionPayload(h.storage, degraded),
2887
+ null,
2888
+ "no shared/profile copy may be built from an unpatched provenance record",
2889
+ );
2890
+
2891
+ // Positive control: a fully patched merge of the same shape still promotes.
2892
+ const ok = await harness();
2893
+ const patched = await applySemanticMergeAtPersist(ok.deps, {
2894
+ storage: ok.storage,
2895
+ content: INCOMING,
2896
+ category: "fact",
2897
+ judgeCall: (options) => acceptingJudge(options),
2898
+ });
2899
+ assert.equal(patched.action, "merged");
2900
+ if (patched.action !== "merged") return;
2901
+ assert.equal(patched.provenancePatched, true);
2902
+ const payload = await buildMergedTargetPromotionPayload(ok.storage, patched);
2903
+ assert.ok(payload);
2904
+ assert.equal(payload?.sourceMemoryId, "fact-target");
2905
+ });
2906
+
2907
+ test("buildMergedTargetPromotionPayload: a failed post-commit reread fails open to a logged null (round N+18 B)", async () => {
2908
+ // The merge has already committed; the hash-index repair makes any retry
2909
+ // dedupe against the committed body. A reread that throws here (secure
2910
+ // store locked, corpus read I/O error) must degrade to "no promotion" —
2911
+ // never abort the caller's remaining durable effects (thread episode,
2912
+ // temporal/tag tracking, harmonic construction, graph rebuild, behavior
2913
+ // signals, artifact write).
2914
+ const entries: Array<{ level: string; message: string }> = [];
2915
+ const backend: LoggerBackend = {
2916
+ info: (msg: string) => entries.push({ level: "info", message: msg }),
2917
+ warn: (msg: string) => entries.push({ level: "warn", message: msg }),
2918
+ error: () => {},
2919
+ debug: () => {},
2920
+ };
2921
+ initLogger(backend, true);
2922
+ try {
2923
+ const lockedStore = {
2924
+ getMemoryByIdIncludingArchived: async () => {
2925
+ throw new Error("secure store locked");
2926
+ },
2927
+ } as unknown as StorageManager;
2928
+ const payload = await buildMergedTargetPromotionPayload(lockedStore, {
2929
+ targetId: "fact-target",
2930
+ mergedContent: "merged body",
2931
+ provenancePatched: true,
2932
+ });
2933
+ assert.equal(payload, null, "a reread failure must resolve to null, not a rejection");
2934
+ const line = entries.find(
2935
+ (e) => e.level === "warn" && e.message.includes("semantic-merge") && e.message.includes("fact-target"),
2936
+ )?.message;
2937
+ assert.ok(line, "the skipped promotion must be surfaced as a warn");
2938
+ } finally {
2939
+ resetLogger();
2940
+ }
2941
+ });
2942
+
2943
+ test("applySemanticMergeAtPersist: the committed merged body preserves the incoming citation (round N+7 C)", async () => {
2944
+ const OLD_CIT = "[Source: agent=agent-a, session=proj/s-old, ts=2026-08-19T00:00:00Z]";
2945
+ const NEW_CIT = "[Source: agent=agent-a, session=proj/s-new, ts=2026-08-21T00:00:00Z]";
2946
+ const h = await harness({ topLevelConfig: { inlineSourceAttributionEnabled: true } });
2947
+ // The target carries an older citation from its original write; the judge's
2948
+ // merged body embeds it. Without the incoming marker, the combined body
2949
+ // reads as wholly attributed to the earlier source.
2950
+ await h.setTargetContent(`${EXISTING} ${OLD_CIT}`);
2951
+ const MERGED_WITH_OLD = `${MERGED} ${OLD_CIT}`;
2952
+ const mergeOptions = {
2953
+ storage: h.storage,
2954
+ content: INCOMING,
2955
+ category: "fact",
2956
+ // The caller's single cited body for this write — the same string the
2957
+ // verbatim artifact stores, so memory and artifact share one timestamp.
2958
+ incomingCitedContent: `${INCOMING} ${NEW_CIT}`,
2959
+ judgeCall: async () => ({
2960
+ decision: "merge",
2961
+ targetId: "fact-target",
2962
+ mergedContent: MERGED_WITH_OLD,
2963
+ reason: "same cadence, target citation embedded",
2964
+ }),
2965
+ } as ApplySemanticMergeOptions;
2966
+ const outcome = await applySemanticMergeAtPersist(h.deps, mergeOptions);
2967
+ assert.equal(outcome.action, "merged");
2968
+ const committed = await h.storage.getMemoryByIdIncludingArchived("fact-target");
2969
+ // BOTH attributions survive on the committed body.
2970
+ assert.equal(committed?.content, `${MERGED_WITH_OLD} ${NEW_CIT}`);
2971
+ if (outcome.action !== "merged") return;
2972
+ assert.equal(outcome.mergedContent, `${MERGED_WITH_OLD} ${NEW_CIT}`);
2973
+ // The hash rule survives too: identity stays on the RAW pre-citation body
2974
+ // with BOTH markers stripped — never on the cited combined form.
2975
+ const stamped = h.calls.frontmatterPatches.at(-1)?.patch.contentHash;
2976
+ assert.equal(stamped, ContentHashIndex.computeHash(sanitizeMemoryContent(MERGED).text));
2977
+ });
2978
+
2979
+ // #2330 round N+8 (P2-C): a target written under the DEFAULT citation format
2980
+ // that later merges under a CUSTOM template carries BOTH markers on the
2981
+ // committed body. Canonicalization must strip every recognized form before
2982
+ // hashing — the default-only fast path left the custom timestamped marker in
2983
+ // the hash input, giving the merged record a different identity than the
2984
+ // equivalent raw write.
2985
+ test("applySemanticMergeAtPersist: mixed default+custom citation markers hash as the raw body (P2-C)", async () => {
2986
+ const DEFAULT_CIT = "[Source: agent=agent-a, session=proj/s-old, ts=2026-08-19T00:00:00Z]";
2987
+ const CUSTOM_TEMPLATE = "[src:{agent}/{sessionId}@{date}]";
2988
+ const CUSTOM_CIT = "[src:agent-a/proj-s-new@2026-08-21]";
2989
+ const h = await harness({
2990
+ topLevelConfig: {
2991
+ inlineSourceAttributionEnabled: true,
2992
+ inlineSourceAttributionFormat: CUSTOM_TEMPLATE,
2993
+ },
2994
+ });
2995
+ // The target carries the default-format marker from its original write; the
2996
+ // incoming fact was cited with the now-configured custom template.
2997
+ await h.setTargetContent(`${EXISTING} ${DEFAULT_CIT}`);
2998
+ const MERGED_WITH_OLD = `${MERGED} ${DEFAULT_CIT}`;
2999
+ const outcome = await applySemanticMergeAtPersist(h.deps, {
3000
+ storage: h.storage,
3001
+ content: INCOMING,
3002
+ category: "fact",
3003
+ incomingCitedContent: `${INCOMING} ${CUSTOM_CIT}`,
3004
+ judgeCall: async () => ({
3005
+ decision: "merge",
3006
+ targetId: "fact-target",
3007
+ mergedContent: MERGED_WITH_OLD,
3008
+ reason: "same cadence, mixed citation templates",
3009
+ }),
3010
+ } as ApplySemanticMergeOptions);
3011
+ assert.equal(outcome.action, "merged");
3012
+ const committed = await h.storage.getMemoryByIdIncludingArchived("fact-target");
3013
+ // BOTH attributions survive on the committed body.
3014
+ assert.equal(committed?.content, `${MERGED_WITH_OLD} ${CUSTOM_CIT}`);
3015
+ // Identity stays on the RAW pre-citation body with BOTH forms stripped.
3016
+ const stamped = h.calls.frontmatterPatches.at(-1)?.patch.contentHash;
3017
+ assert.equal(stamped, ContentHashIndex.computeHash(sanitizeMemoryContent(MERGED).text));
3018
+ });
3019
+
3020
+ /** Minimal real-dir harness for the merged-target post-effects executor. */
3021
+ async function postEffectsHarness(options: {
3022
+ graphEdgeThrows?: boolean;
3023
+ } = {}): Promise<{
3024
+ deps: ExtractionPersistDeps;
3025
+ storage: StorageManager;
3026
+ dir: string;
3027
+ targetRelPath: string;
3028
+ }> {
3029
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-pe-"));
3030
+ const factsDay = path.join(dir, "facts", "2026-08-20");
3031
+ await mkdir(factsDay, { recursive: true });
3032
+ const targetPath = path.join(factsDay, "fact-target.md");
3033
+ await writeFile(
3034
+ targetPath,
3035
+ `---\nid: fact-target\ncategory: fact\nentityRef: entity-billing-service\n---\n\n${MERGED}\n`,
3036
+ "utf8",
3037
+ );
3038
+ const storage = {
3039
+ dir,
3040
+ getMemoryByIdIncludingArchived: async (id: string) =>
3041
+ id === "fact-target"
3042
+ ? {
3043
+ path: targetPath,
3044
+ frontmatter: { id, category: "fact", entityRef: "entity-billing-service" },
3045
+ content: MERGED,
3046
+ }
3047
+ : null,
3048
+ } as unknown as StorageManager;
3049
+ const deps = {
3050
+ config: parseConfig({ memoryDir: dir }),
3051
+ ...(options.graphEdgeThrows
3052
+ ? {
3053
+ buildGraphEdge: async () => {
3054
+ throw new Error("graph append I/O failure");
3055
+ },
3056
+ }
3057
+ : {}),
3058
+ } as unknown as ExtractionPersistDeps;
3059
+ return { deps, storage, dir, targetRelPath: path.relative(dir, targetPath) };
3060
+ }
3061
+
3062
+ const ALL_GRAPH_CAPS = {
3063
+ entityGraph: true,
3064
+ timeGraph: true,
3065
+ causalGraph: true,
3066
+ multiGraphMemory: true,
3067
+ graphWriteSessionAdjacency: false,
3068
+ } as const;
3069
+
3070
+ async function seedGraphFile(dir: string, type: "entity" | "time" | "causal", edges: unknown[]) {
3071
+ const graphsDir = path.join(dir, "state", "graphs");
3072
+ await mkdir(graphsDir, { recursive: true });
3073
+ await writeFile(
3074
+ path.join(graphsDir, `${type}.jsonl`),
3075
+ edges.map((e) => JSON.stringify(e)).join("\n") + "\n",
3076
+ "utf8",
3077
+ );
3078
+ }
3079
+
3080
+ async function readGraphFile(dir: string, type: "entity" | "time" | "causal") {
3081
+ try {
3082
+ const raw = await readFile(path.join(dir, "state", "graphs", `${type}.jsonl`), "utf8");
3083
+ return raw
3084
+ .split("\n")
3085
+ .filter((line) => line.trim().length > 0)
3086
+ .map((line) => JSON.parse(line));
3087
+ } catch (err) {
3088
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
3089
+ throw err;
3090
+ }
3091
+ }
3092
+
3093
+ test("runMergedTargetPostEffects: a failing graph rebuild leaves the prior edges intact (round N+7 E)", async () => {
3094
+ const h = await postEffectsHarness({ graphEdgeThrows: true });
3095
+ const OTHER = "facts/2026-08-19/other.md";
3096
+ const priorEntity = { from: h.targetRelPath, to: OTHER, type: "entity", weight: 1, label: "entity-billing-service", ts: "2026-08-19T00:00:00.000Z" };
3097
+ const priorTime = { from: OTHER, to: h.targetRelPath, type: "time", weight: 1, label: "thread-0", ts: "2026-08-19T00:00:00.000Z" };
3098
+ const priorCausal = { from: OTHER, to: h.targetRelPath, type: "causal", weight: 1, label: "because", ts: "2026-08-19T00:00:00.000Z" };
3099
+ await seedGraphFile(h.dir, "entity", [priorEntity]);
3100
+ await seedGraphFile(h.dir, "time", [priorTime]);
3101
+ await seedGraphFile(h.dir, "causal", [priorCausal]);
3102
+
3103
+ await runMergedTargetPostEffects(h.deps, h.storage, { targetId: "fact-target", mergedContent: MERGED }, {
3104
+ category: "fact",
3105
+ incomingContent: INCOMING,
3106
+ incomingConfidence: 0.9,
3107
+ namespace: "default",
3108
+ graphCaps: ALL_GRAPH_CAPS,
3109
+ graphContext: { allMemsForGraph: [], memoryPathById: new Map() },
3110
+ threadIdForEdge: undefined,
3111
+ threadEpisodeIdsForGraph: undefined,
3112
+ });
3113
+
3114
+ // Failure leaves the OLD complete set, never none: the target's generated
3115
+ // edges in every enabled graph type survive the failed rebuild verbatim.
3116
+ assert.deepEqual(await readGraphFile(h.dir, "entity"), [priorEntity]);
3117
+ assert.deepEqual(await readGraphFile(h.dir, "time"), [priorTime]);
3118
+ assert.deepEqual(await readGraphFile(h.dir, "causal"), [priorCausal]);
3119
+ });
3120
+
3121
+ // #2330 round N+8 (P2-B): the production append failure must reach the
3122
+ // restore path. GraphIndex.onMemoryWritten used to swallow every append
3123
+ // error, so the caller's restore catch never ran and the edges
3124
+ // removeNodeEdgesForRewrite had dropped were permanently lost. The test
3125
+ // above fakes the failure at deps.buildGraphEdge; this one injects it at the
3126
+ // REAL seam — appendEdge's appendFile — by making the entity JSONL
3127
+ // read-only, and runs the real PersistenceIndexCoordinator → GraphIndex chain.
3128
+ test("runMergedTargetPostEffects: a REAL append failure restores prior edges at the GraphIndex seam (P2-B)", async () => {
3129
+ if (process.getuid?.() === 0) {
3130
+ // A root process may open a 0444 file for append, so the EACCES injection
3131
+ // would not fail. CI runners and dev boxes run unprivileged; skip for root.
3132
+ return;
3133
+ }
3134
+ const h = await postEffectsHarness();
3135
+ const OTHER = "facts/2026-08-19/other.md";
3136
+ const priorTime = { from: OTHER, to: h.targetRelPath, type: "time", weight: 1, label: "thread-0", ts: "2026-08-19T00:00:00.000Z" };
3137
+ // An entity edge this node did NOT generate: removal skips writing
3138
+ // entity.jsonl entirely, so the read-only mode survives until the append.
3139
+ const unrelatedEntity = { from: OTHER, to: "facts/2026-08-18/unrelated.md", type: "entity", weight: 1, label: "entity-billing-service", ts: "2026-08-19T00:00:00.000Z" };
3140
+ await seedGraphFile(h.dir, "entity", [unrelatedEntity]);
3141
+ await seedGraphFile(h.dir, "time", [priorTime]);
3142
+ const entityPath = path.join(h.dir, "state", "graphs", "entity.jsonl");
3143
+ await chmod(entityPath, 0o444);
3144
+ try {
3145
+ const graphConfig = parseConfig({ memoryDir: h.dir });
3146
+ const graphIndex = new GraphIndex(h.dir, graphConfig);
3147
+ const coordinator = new PersistenceIndexCoordinator({
3148
+ config: graphConfig,
3149
+ graphIndexFor: () => graphIndex,
3150
+ } as unknown as ConstructorParameters<typeof PersistenceIndexCoordinator>[0]);
3151
+ const deps = {
3152
+ ...h.deps,
3153
+ // The REAL chain: runMergedTargetPostEffects → coordinator.buildGraphEdge
3154
+ // → GraphIndex.onMemoryWritten → appendEdge. No stubbed seam anywhere.
3155
+ buildGraphEdge: coordinator.buildGraphEdge.bind(coordinator),
3156
+ } as unknown as ExtractionPersistDeps;
3157
+ // A sibling sharing the target's entityRef makes the ENTITY append fire —
3158
+ // the coordinator derives siblings from this corpus.
3159
+ const sibling = {
3160
+ path: path.join(h.dir, OTHER),
3161
+ frontmatter: { id: "mem-sibling", category: "fact", entityRef: "entity-billing-service" },
3162
+ content: "Sibling fact sharing the billing entity.",
3163
+ } as unknown as MemoryFile;
3164
+ await runMergedTargetPostEffects(
3165
+ deps,
3166
+ h.storage,
3167
+ { targetId: "fact-target", mergedContent: MERGED },
3168
+ {
3169
+ category: "fact",
3170
+ incomingContent: INCOMING,
3171
+ incomingConfidence: 0.9,
3172
+ namespace: "default",
3173
+ graphCaps: ALL_GRAPH_CAPS,
3174
+ graphContext: { allMemsForGraph: [sibling], memoryPathById: new Map() },
3175
+ threadIdForEdge: undefined,
3176
+ threadEpisodeIdsForGraph: undefined,
3177
+ },
3178
+ );
3179
+ const timeEdges = await readGraphFile(h.dir, "time");
3180
+ assert.ok(
3181
+ timeEdges.some((e) => e.from === OTHER && e.to === h.targetRelPath),
3182
+ `the prior time edges must be restored after the real append failure (time.jsonl: ${JSON.stringify(timeEdges)})`,
3183
+ );
3184
+ assert.deepEqual(
3185
+ await readGraphFile(h.dir, "entity"),
3186
+ [unrelatedEntity],
3187
+ "no entity edge may land when the append fails",
3188
+ );
3189
+ } finally {
3190
+ await chmod(entityPath, 0o644);
3191
+ }
3192
+ });
3193
+
3194
+ // #2330 round N+10 (C): the failure P2-B models strikes at the FIRST
3195
+ // append. Here the ENTITY append SUCCEEDS and the TIME append fails, so the
3196
+ // failed build has already written rows for this node. The restore loop used
3197
+ // to only add the removed old rows back, leaving old + partial-new edges
3198
+ // that spreadingActivation double-counts. Failure must leave EXACTLY the
3199
+ // old edge set — the partial new rows are removed before the restore.
3200
+ test("runMergedTargetPostEffects: a failure after a successful entity append leaves exactly the old edge set (round N+10 C)", async () => {
3201
+ if (process.getuid?.() === 0) {
3202
+ // Root may append to a 0444 file, so the EACCES injection cannot fail.
3203
+ return;
3204
+ }
3205
+ const h = await postEffectsHarness();
3206
+ const OTHER = "facts/2026-08-19/other.md";
3207
+ // The target's OWN prior entity edge: removal drops it (restoring later).
3208
+ const priorEntity = { from: h.targetRelPath, to: OTHER, type: "entity", weight: 1, label: "entity-billing-service", ts: "2026-08-19T00:00:00.000Z" };
3209
+ // A time edge this node did NOT generate: removal skips rewriting
3210
+ // time.jsonl entirely, so the read-only mode survives until the append.
3211
+ const unrelatedTime = { from: "facts/2026-08-18/earlier.md", to: "facts/2026-08-18/earliest.md", type: "time", weight: 1, label: "thread-0", ts: "2026-08-19T00:00:00.000Z" };
3212
+ await seedGraphFile(h.dir, "entity", [priorEntity]);
3213
+ await seedGraphFile(h.dir, "time", [unrelatedTime]);
3214
+ const timePath = path.join(h.dir, "state", "graphs", "time.jsonl");
3215
+ await chmod(timePath, 0o444);
3216
+ try {
3217
+ const graphConfig = parseConfig({ memoryDir: h.dir });
3218
+ const graphIndex = new GraphIndex(h.dir, graphConfig);
3219
+ const coordinator = new PersistenceIndexCoordinator({
3220
+ config: graphConfig,
3221
+ graphIndexFor: () => graphIndex,
3222
+ } as unknown as ConstructorParameters<typeof PersistenceIndexCoordinator>[0]);
3223
+ const deps = {
3224
+ ...h.deps,
3225
+ // The REAL chain: runMergedTargetPostEffects → coordinator.buildGraphEdge
3226
+ // → GraphIndex.onMemoryWritten → appendEdge. No stubbed seam anywhere.
3227
+ buildGraphEdge: coordinator.buildGraphEdge.bind(coordinator),
3228
+ } as unknown as ExtractionPersistDeps;
3229
+ // A sibling sharing the target's entityRef makes the ENTITY append fire
3230
+ // and succeed; the thread episode list makes the TIME append fire next
3231
+ // and hit the read-only file.
3232
+ const sibling = {
3233
+ path: path.join(h.dir, OTHER),
3234
+ frontmatter: { id: "mem-sibling", category: "fact", entityRef: "entity-billing-service" },
3235
+ content: "Sibling fact sharing the billing entity.",
3236
+ } as unknown as MemoryFile;
3237
+ await runMergedTargetPostEffects(
3238
+ deps,
3239
+ h.storage,
3240
+ { targetId: "fact-target", mergedContent: MERGED },
3241
+ {
3242
+ category: "fact",
3243
+ incomingContent: INCOMING,
3244
+ incomingConfidence: 0.9,
3245
+ namespace: "default",
3246
+ graphCaps: ALL_GRAPH_CAPS,
3247
+ graphContext: {
3248
+ allMemsForGraph: [sibling],
3249
+ memoryPathById: new Map([["mem-earlier", OTHER]]),
3250
+ },
3251
+ threadIdForEdge: "thread-1",
3252
+ threadEpisodeIdsForGraph: ["mem-earlier", "fact-target"],
3253
+ },
3254
+ );
3255
+ assert.deepEqual(
3256
+ await readGraphFile(h.dir, "entity"),
3257
+ [priorEntity],
3258
+ "exactly the old entity edge set may remain — the partially appended new edge must be removed, not left beside the restored old rows",
3259
+ );
3260
+ assert.deepEqual(
3261
+ await readGraphFile(h.dir, "time"),
3262
+ [unrelatedTime],
3263
+ "the unrelated time edge survives untouched",
3264
+ );
3265
+ } finally {
3266
+ await chmod(timePath, 0o644);
3267
+ }
3268
+ });
3269
+
3270
+ test("runMergedTargetPostEffects: a re-merge records the target as the LATEST thread event (round N+7 F)", async () => {
3271
+ const h = await postEffectsHarness();
3272
+ // The target is already in the thread's episode list (it was created there
3273
+ // before this merge): the merge must move it to the end, not leave it at
3274
+ // its old position.
3275
+ const episodes = ["fact-target", "mem-earlier"];
3276
+ await runMergedTargetPostEffects(h.deps, h.storage, { targetId: "fact-target", mergedContent: MERGED }, {
3277
+ category: "fact",
3278
+ incomingContent: INCOMING,
3279
+ incomingConfidence: 0.9,
3280
+ namespace: "default",
3281
+ graphCaps: ALL_GRAPH_CAPS,
3282
+ graphContext: { allMemsForGraph: [], memoryPathById: new Map() },
3283
+ threadIdForEdge: "thread-1",
3284
+ threadEpisodeIdsForGraph: episodes,
3285
+ });
3286
+ assert.deepEqual(episodes, ["mem-earlier", "fact-target"]);
3287
+ });
3288
+
3289
+ // ── Round N+12 (C): the remove-and-rebuild stays guarded against a later
3290
+ // writer committing a NEWER merge between the committed-body check and the
3291
+ // final edge install. Writer A must not clobber writer B's edges. ────────────
3292
+
3293
+ test("runMergedTargetPostEffects: a writer committing mid-rebuild keeps its edges — the stale writer restores, not installs (round N+12 C)", async () => {
3294
+ const h = await postEffectsHarness();
3295
+ const OTHER = "facts/2026-08-19/other.md";
3296
+ // B's committed state: B already rebuilt the target's entity edge with the
3297
+ // NEWER body's label. A's rebuild derives from the OLDER body.
3298
+ const newerEntity = { from: h.targetRelPath, to: OTHER, type: "entity", weight: 1, label: "entity-billing-service-v2", ts: "2026-08-22T00:00:00.000Z" };
3299
+ await seedGraphFile(h.dir, "entity", [newerEntity]);
3300
+ const NEWER_BODY = "Billing service deploys run on Tuesdays at 09:00 UTC with payments.";
3301
+ // A passes its committed-body check against A's body, then B commits AFTER
3302
+ // the check: every read from the build onward observes B's newer body.
3303
+ let buildGraphEdgeCalls = 0;
3304
+ const storage = {
3305
+ dir: h.dir,
3306
+ getMemoryByIdIncludingArchived: async (id: string) =>
3307
+ id === "fact-target"
3308
+ ? {
3309
+ path: path.join(h.dir, h.targetRelPath),
3310
+ frontmatter: { id, category: "fact", entityRef: "entity-billing-service" },
3311
+ // The flip: the initial check sees A's body; anything after the
3312
+ // rebuild began sees B's committed body.
3313
+ content: buildGraphEdgeCalls > 0 ? NEWER_BODY : MERGED,
3314
+ }
3315
+ : null,
3316
+ } as unknown as StorageManager;
3317
+ const deps = {
3318
+ ...h.deps,
3319
+ buildGraphEdge: async () => {
3320
+ buildGraphEdgeCalls++;
3321
+ },
3322
+ } as unknown as ExtractionPersistDeps;
3323
+ const previousPersisted = { current: "facts/2026-08-18/prev.md" };
3324
+ await runMergedTargetPostEffects(
3325
+ deps,
3326
+ storage,
3327
+ { targetId: "fact-target", mergedContent: MERGED },
3328
+ {
3329
+ category: "fact",
3330
+ incomingContent: INCOMING,
3331
+ incomingConfidence: 0.9,
3332
+ namespace: "default",
3333
+ graphCaps: ALL_GRAPH_CAPS,
3334
+ graphContext: { allMemsForGraph: [], memoryPathById: new Map(), previousPersistedRelPath: previousPersisted.current },
3335
+ threadIdForEdge: undefined,
3336
+ threadEpisodeIdsForGraph: undefined,
3337
+ },
3338
+ );
3339
+ assert.deepEqual(
3340
+ await readGraphFile(h.dir, "entity"),
3341
+ [newerEntity],
3342
+ "writer A must restore B's newer edges, never install edges derived from A's older body",
3343
+ );
3344
+ assert.equal(
3345
+ previousPersisted.current,
3346
+ "facts/2026-08-18/prev.md",
3347
+ "a superseded rebuild must not advance the batch's adjacency chain",
3348
+ );
3349
+ });
3350
+
3351
+ // ── Round N+14: a NEWER writer completing its rebuild INSIDE the stale
3352
+ // writer's remove→check window. B's rows land after A's removal, so they are
3353
+ // NOT in A's restore snapshot; the rollback must remove only the rows A's
3354
+ // build appended and must not resurrect the pre-A snapshot over B's live
3355
+ // rows. ────────────────────────────────────────────────────────────────────
3356
+
3357
+ test("runMergedTargetPostEffects: a stale writer's rollback preserves the newer writer's rebuilt edges (round N+14)", async () => {
3358
+ const h = await postEffectsHarness();
3359
+ const OTHER = "facts/2026-08-19/other.md";
3360
+ // The pre-A edge set: exactly what A's removal sweeps into its snapshot.
3361
+ const priorEntity: GraphEdge = { from: h.targetRelPath, to: OTHER, type: "entity", weight: 1, label: "entity-billing-service", ts: "2026-08-19T00:00:00.000Z" };
3362
+ await seedGraphFile(h.dir, "entity", [priorEntity]);
3363
+ // B's completed rebuild, from B's newer body: a different label and B's own
3364
+ // timestamp, appended DURING A's window — never captured by A's snapshot.
3365
+ const bEntity: GraphEdge = { from: h.targetRelPath, to: OTHER, type: "entity", weight: 1, label: "entity-billing-service-v2", ts: "2026-08-22T01:00:00.000Z" };
3366
+ const NEWER_BODY = "Billing service deploys run on Tuesdays at 09:00 UTC with payments.";
3367
+ let buildStarted = false;
3368
+ const storage = {
3369
+ dir: h.dir,
3370
+ getMemoryByIdIncludingArchived: async (id: string) =>
3371
+ id === "fact-target"
3372
+ ? {
3373
+ path: path.join(h.dir, h.targetRelPath),
3374
+ frontmatter: {
3375
+ id,
3376
+ category: "fact",
3377
+ entityRef: "entity-billing-service",
3378
+ // B's commit flips both the body and the revision once A's
3379
+ // rebuild has begun: the initial committed-body check sees A's
3380
+ // state, everything from the build onward sees B's.
3381
+ ...(buildStarted ? { updated: "2026-08-22T01:00:00.000Z" } : {}),
3382
+ },
3383
+ content: buildStarted ? NEWER_BODY : MERGED,
3384
+ }
3385
+ : null,
3386
+ } as unknown as StorageManager;
3387
+ const graphConfig = parseConfig({ memoryDir: h.dir });
3388
+ const graphIndex = new GraphIndex(h.dir, graphConfig);
3389
+ const coordinator = new PersistenceIndexCoordinator({
3390
+ config: graphConfig,
3391
+ graphIndexFor: () => graphIndex,
3392
+ } as unknown as ConstructorParameters<typeof PersistenceIndexCoordinator>[0]);
3393
+ const realBuild = coordinator.buildGraphEdge.bind(coordinator);
3394
+ const sibling = {
3395
+ path: path.join(h.dir, OTHER),
3396
+ frontmatter: { id: "mem-sibling", category: "fact", entityRef: "entity-billing-service" },
3397
+ content: "Sibling fact sharing the billing entity.",
3398
+ } as unknown as MemoryFile;
3399
+ const deps = {
3400
+ ...h.deps,
3401
+ // The REAL chain for A's appends (so A's rows are exactly tracked), with
3402
+ // B's completed rebuild injected at the start of A's build — after A's
3403
+ // removal, before A's revision check.
3404
+ buildGraphEdge: async (...args: Parameters<typeof realBuild>) => {
3405
+ buildStarted = true;
3406
+ await appendEdge(h.dir, { ...bEntity });
3407
+ return realBuild(...args);
3408
+ },
3409
+ } as unknown as ExtractionPersistDeps;
3410
+ await runMergedTargetPostEffects(
3411
+ deps,
3412
+ storage,
3413
+ { targetId: "fact-target", mergedContent: MERGED },
3414
+ {
3415
+ category: "fact",
3416
+ incomingContent: INCOMING,
3417
+ incomingConfidence: 0.9,
3418
+ namespace: "default",
3419
+ graphCaps: ALL_GRAPH_CAPS,
3420
+ graphContext: { allMemsForGraph: [sibling], memoryPathById: new Map() },
3421
+ threadIdForEdge: undefined,
3422
+ threadEpisodeIdsForGraph: undefined,
3423
+ },
3424
+ );
3425
+ const entityEdges = await readGraphFile(h.dir, "entity");
3426
+ assert.deepEqual(
3427
+ entityEdges,
3428
+ [bEntity],
3429
+ `the graph must hold exactly B's rebuilt edges after the stale writer's rollback — no pre-A rows, no A rows (entity.jsonl: ${JSON.stringify(entityEdges)})`,
3430
+ );
3431
+ });
3432
+
3433
+ // ── Round N+16 (A): the N+14 surgical rollback leaned on build()'s RETURN
3434
+ // value. A build that THROWS mid-append (the entity rows landed, a later
3435
+ // append failed) loses that return, and the rollback fell back to the
3436
+ // node-wide sweep — which also deleted a NEWER writer's rebuild that
3437
+ // completed inside this writer's remove→rollback window. The partial row
3438
+ // set must be tracked incrementally, BEFORE build returns, and a throw must
3439
+ // roll back exactly that partial set. ─────────────────────────────────────
3440
+
3441
+ test("runMergedTargetPostEffects: a THROWING build's rollback preserves the newer writer's rebuilt edges (round N+16 A)", async () => {
3442
+ if (process.getuid?.() === 0) {
3443
+ // Root may append to a 0444 file, so the EACCES injection cannot fail.
3444
+ return;
3445
+ }
3446
+ const h = await postEffectsHarness();
3447
+ const OTHER = "facts/2026-08-19/other.md";
3448
+ // A's pre-state: exactly what A's removal sweeps into its snapshot.
3449
+ const priorEntity: GraphEdge = { from: h.targetRelPath, to: OTHER, type: "entity", weight: 1, label: "entity-billing-service", ts: "2026-08-19T00:00:00.000Z" };
3450
+ await seedGraphFile(h.dir, "entity", [priorEntity]);
3451
+ // An unrelated time edge keeps time.jsonl untouched by the removal, so its
3452
+ // read-only mode survives until the append (same injection as round N+10 C).
3453
+ const unrelatedTime = { from: "facts/2026-08-18/earlier.md", to: "facts/2026-08-18/earliest.md", type: "time", weight: 1, label: "thread-0", ts: "2026-08-19T00:00:00.000Z" };
3454
+ await seedGraphFile(h.dir, "time", [unrelatedTime]);
3455
+ // B's completed rebuild, from B's newer body — appended AFTER A's removal
3456
+ // (never in A's restore snapshot), BEFORE A's failing append.
3457
+ const bEntity: GraphEdge = { from: h.targetRelPath, to: OTHER, type: "entity", weight: 1, label: "entity-billing-service-v2", ts: "2026-08-22T01:00:00.000Z" };
3458
+ const timePath = path.join(h.dir, "state", "graphs", "time.jsonl");
3459
+ await chmod(timePath, 0o444);
3460
+ try {
3461
+ const graphConfig = parseConfig({ memoryDir: h.dir });
3462
+ const graphIndex = new GraphIndex(h.dir, graphConfig);
3463
+ const coordinator = new PersistenceIndexCoordinator({
3464
+ config: graphConfig,
3465
+ graphIndexFor: () => graphIndex,
3466
+ } as unknown as ConstructorParameters<typeof PersistenceIndexCoordinator>[0]);
3467
+ const realBuild = coordinator.buildGraphEdge.bind(coordinator);
3468
+ const sibling = {
3469
+ path: path.join(h.dir, OTHER),
3470
+ frontmatter: { id: "mem-sibling", category: "fact", entityRef: "entity-billing-service" },
3471
+ content: "Sibling fact sharing the billing entity.",
3472
+ } as unknown as MemoryFile;
3473
+ const deps = {
3474
+ ...h.deps,
3475
+ // The REAL chain for A's appends: B's completed rebuild lands first,
3476
+ // then A's entity append SUCCEEDS and the time append THROWS (EACCES) —
3477
+ // a build that wrote rows and still rejects.
3478
+ buildGraphEdge: async (...args: Parameters<typeof realBuild>) => {
3479
+ await appendEdge(h.dir, { ...bEntity });
3480
+ return realBuild(...args);
3481
+ },
3482
+ } as unknown as ExtractionPersistDeps;
3483
+ await runMergedTargetPostEffects(
3484
+ deps,
3485
+ h.storage,
3486
+ { targetId: "fact-target", mergedContent: MERGED },
3487
+ {
3488
+ category: "fact",
3489
+ incomingContent: INCOMING,
3490
+ incomingConfidence: 0.9,
3491
+ namespace: "default",
3492
+ graphCaps: ALL_GRAPH_CAPS,
3493
+ graphContext: {
3494
+ allMemsForGraph: [sibling],
3495
+ memoryPathById: new Map([["mem-earlier", OTHER]]),
3496
+ },
3497
+ threadIdForEdge: "thread-1",
3498
+ threadEpisodeIdsForGraph: ["mem-earlier", "fact-target"],
3499
+ },
3500
+ );
3501
+ const entityEdges = await readGraphFile(h.dir, "entity");
3502
+ assert.deepEqual(
3503
+ entityEdges,
3504
+ [bEntity],
3505
+ `the throwing build's rollback must remove only its own partial rows and never sweep the newer writer's rebuild (entity.jsonl: ${JSON.stringify(entityEdges)})`,
3506
+ );
3507
+ assert.deepEqual(
3508
+ await readGraphFile(h.dir, "time"),
3509
+ [unrelatedTime],
3510
+ "the unrelated time edge survives the throwing build untouched",
3511
+ );
3512
+ } finally {
3513
+ await chmod(timePath, 0o644);
3514
+ }
3515
+ });
3516
+
3517
+ test("persistMergedTargetThreadEpisode: a re-merge moves an existing earlier target to the durable thread tail (round N+12 D)", async () => {
3518
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-merge-thread-"));
3519
+ try {
3520
+ const threading = new ThreadingManager(path.join(dir, "threads"), 90);
3521
+ const threadId = "thread-1";
3522
+ await threading.saveThread({
3523
+ id: threadId,
3524
+ title: "Billing deploys",
3525
+ createdAt: "2026-08-20T00:00:00.000Z",
3526
+ updatedAt: "2026-08-20T00:00:00.000Z",
3527
+ episodeIds: ["fact-target", "mem-earlier"],
3528
+ linkedThreadIds: [],
3529
+ });
3530
+ await persistMergedTargetThreadEpisode(threading, threadId, "fact-target");
3531
+ // The next extraction reloads the thread from disk — the reloaded order
3532
+ // is what resolveRecentThreadMemoryPaths(...).slice(-3) will see.
3533
+ const reloaded = await threading.loadThread(threadId);
3534
+ assert.deepEqual(
3535
+ reloaded?.episodeIds,
3536
+ ["mem-earlier", "fact-target"],
3537
+ "a re-merged target must sit at the durable thread tail after reload",
3538
+ );
3539
+ } finally {
3540
+ await rm(dir, { recursive: true, force: true });
3541
+ }
3542
+ });