@remnic/core 9.69.87 → 9.69.89

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 (416) hide show
  1. package/dist/access-admin-ops-surface.js +56 -49
  2. package/dist/access-authorization-probe.js +57 -50
  3. package/dist/access-boundary.js +56 -49
  4. package/dist/access-citation-namespace.js +2 -2
  5. package/dist/access-cli.js +129 -122
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-extraction-force-flush.js +56 -49
  8. package/dist/access-http-briefing.js +57 -50
  9. package/dist/access-http-offline-stream.d.ts.map +1 -1
  10. package/dist/access-http-offline-stream.js +1 -1
  11. package/dist/access-http-query.js +58 -51
  12. package/dist/access-http.js +78 -71
  13. package/dist/access-identity-continuity-surface.js +56 -49
  14. package/dist/access-lcm-surface.js +56 -49
  15. package/dist/access-mcp-output-schemas.d.ts.map +1 -1
  16. package/dist/access-mcp-output-schemas.js +1 -1
  17. package/dist/access-mcp.js +69 -62
  18. package/dist/access-namespace-preflight.js +56 -49
  19. package/dist/access-observe-write-category.js +20 -13
  20. package/dist/access-observe-write-surface.js +56 -49
  21. package/dist/access-offline-manifest.js +16 -10
  22. package/dist/access-operations-batch.js +60 -53
  23. package/dist/access-operations.js +67 -60
  24. package/dist/access-recall-concurrency.js +56 -49
  25. package/dist/access-recall-response.js +56 -49
  26. package/dist/access-recall-surface.js +56 -49
  27. package/dist/access-schema.js +16 -7
  28. package/dist/access-service.d.ts.map +1 -1
  29. package/dist/access-service.js +56 -49
  30. package/dist/access-surface-catalog.js +58 -51
  31. package/dist/access-surface-catalog.js.map +1 -1
  32. package/dist/active-recall.js +24 -18
  33. package/dist/active-recall.js.map +1 -1
  34. package/dist/{auto-sync-6IPX2BAC.js → auto-sync-U5N3CB5Y.js} +7 -7
  35. package/dist/briefing.js +11 -11
  36. package/dist/buffer.js +7 -7
  37. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-GWVG7LGC.js} +2 -2
  38. package/dist/{capsule-merge-3MNURAQ3.js → capsule-merge-ZCXUPLNJ.js} +2 -2
  39. package/dist/causal-behavior.js +2 -2
  40. package/dist/causal-chain.js +2 -2
  41. package/dist/causal-consolidation.js +14 -14
  42. package/dist/causal-retrieval.js +2 -2
  43. package/dist/causal-trajectory-graph.js +2 -2
  44. package/dist/causal-trajectory.js +1 -1
  45. package/dist/{chunk-JE334SIA.js → chunk-22DYJLFN.js} +2 -2
  46. package/dist/{chunk-YTQGGO3K.js → chunk-22LMCQ5B.js} +85 -82
  47. package/dist/{chunk-YTQGGO3K.js.map → chunk-22LMCQ5B.js.map} +1 -1
  48. package/dist/{chunk-JGSQ6P4D.js → chunk-2FX374UI.js} +2 -2
  49. package/dist/{chunk-SY5KDWGU.js → chunk-2WLMWP2T.js} +2 -2
  50. package/dist/{chunk-6TT6HRE4.js → chunk-3AGHSQPM.js} +2 -2
  51. package/dist/{chunk-P6WFK4ZJ.js → chunk-3O5BB45T.js} +4 -4
  52. package/dist/{chunk-3C3J243X.js → chunk-3QKO4QN7.js} +3 -3
  53. package/dist/{chunk-KFCMMDHH.js → chunk-3YD44CH2.js} +2 -2
  54. package/dist/{chunk-FRCESVV6.js → chunk-4HUCP4R4.js} +18 -18
  55. package/dist/chunk-4HUCP4R4.js.map +1 -0
  56. package/dist/chunk-4NSLQ7EU.js +137 -0
  57. package/dist/chunk-4NSLQ7EU.js.map +1 -0
  58. package/dist/{chunk-7DE2NOON.js → chunk-4O4XCBOS.js} +3 -3
  59. package/dist/{chunk-GDYU6VF4.js → chunk-5AYUHSAU.js} +2 -2
  60. package/dist/{chunk-ICZQG4XE.js → chunk-5FAGCOZD.js} +4 -4
  61. package/dist/{chunk-VKQI47DJ.js → chunk-5FNZ3G6X.js} +4 -4
  62. package/dist/{chunk-CK5KOVHW.js → chunk-5R627MYY.js} +3 -2
  63. package/dist/{chunk-CK5KOVHW.js.map → chunk-5R627MYY.js.map} +1 -1
  64. package/dist/{chunk-VQNONC4E.js → chunk-5STPRPE7.js} +2 -2
  65. package/dist/{chunk-52II7PB6.js → chunk-645WOU5I.js} +4 -4
  66. package/dist/{chunk-SRZW7FNU.js → chunk-64ZUALXK.js} +2 -2
  67. package/dist/{chunk-4BZP2B4U.js → chunk-6MNKDTDI.js} +2 -2
  68. package/dist/{chunk-DUJYRVUD.js → chunk-6O2VUWV6.js} +3 -3
  69. package/dist/chunk-75PT4N7Q.js +75 -0
  70. package/dist/chunk-75PT4N7Q.js.map +1 -0
  71. package/dist/{chunk-HESDNYRB.js → chunk-76M5OK7V.js} +2 -2
  72. package/dist/{chunk-FAN73COB.js → chunk-7D7OUO5W.js} +2 -2
  73. package/dist/{chunk-DZ525SNQ.js → chunk-7QE46QXI.js} +1 -1
  74. package/dist/{chunk-VX6GCKBS.js → chunk-7WBSMXGC.js} +4 -4
  75. package/dist/{chunk-VVDWZRZG.js → chunk-A6RVIHHK.js} +3 -3
  76. package/dist/{chunk-M536WNTQ.js → chunk-AEJ3TE5H.js} +2 -2
  77. package/dist/{chunk-72JNNJJJ.js → chunk-AXC4WSF3.js} +3 -3
  78. package/dist/chunk-AXGJMDBS.js +684 -0
  79. package/dist/chunk-AXGJMDBS.js.map +1 -0
  80. package/dist/{chunk-UA5DX5C3.js → chunk-AZIOEOK4.js} +2 -2
  81. package/dist/{chunk-N4S4NUX5.js → chunk-B3O3IRZN.js} +1 -1
  82. package/dist/{chunk-XTYX6V52.js → chunk-BJVO4H5C.js} +2 -2
  83. package/dist/{chunk-7LCGFUMB.js → chunk-BUKK5SWA.js} +4 -4
  84. package/dist/{chunk-TM3RXL4G.js → chunk-BUQKSQXL.js} +2 -2
  85. package/dist/chunk-C6HPNPNK.js +64 -0
  86. package/dist/chunk-C6HPNPNK.js.map +1 -0
  87. package/dist/{chunk-RB776UH3.js → chunk-C7V3KPKJ.js} +235 -93
  88. package/dist/chunk-C7V3KPKJ.js.map +1 -0
  89. package/dist/{chunk-FOH7AMMI.js → chunk-CALBQ2NG.js} +71 -72
  90. package/dist/chunk-CALBQ2NG.js.map +1 -0
  91. package/dist/{chunk-37S3IMYB.js → chunk-DGS34QSY.js} +2 -2
  92. package/dist/{chunk-4IEOGIPB.js → chunk-DO4C4AXD.js} +2 -2
  93. package/dist/{chunk-SPSRGEDQ.js → chunk-EAQQ2FD3.js} +9 -9
  94. package/dist/{chunk-3BTFBQLO.js → chunk-EFHNFTWB.js} +49 -3
  95. package/dist/chunk-EFHNFTWB.js.map +1 -0
  96. package/dist/{chunk-C3QXITIP.js → chunk-EQUD7FP4.js} +2 -2
  97. package/dist/chunk-ETUZFFPU.js +218 -0
  98. package/dist/chunk-ETUZFFPU.js.map +1 -0
  99. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  100. package/dist/{chunk-YITN5WXI.js → chunk-FC52YWAO.js} +2 -2
  101. package/dist/{chunk-ZI7QA4MK.js → chunk-FEIXG5FA.js} +2 -2
  102. package/dist/{chunk-CLXTDOKV.js → chunk-FUNBJQYR.js} +2 -2
  103. package/dist/{chunk-CLSYE5OZ.js → chunk-G2STTRYB.js} +2 -2
  104. package/dist/{chunk-TDYAIXHK.js → chunk-H36X4NRH.js} +5 -5
  105. package/dist/{chunk-3GQC3NMB.js → chunk-H56VY4BY.js} +5 -5
  106. package/dist/{chunk-PUKTBULQ.js → chunk-HWLIMQE2.js} +2 -2
  107. package/dist/{chunk-V2ES4WCZ.js → chunk-IIARQTZ2.js} +2 -2
  108. package/dist/{chunk-QEV26L2F.js → chunk-IKDRGVVJ.js} +2 -2
  109. package/dist/{chunk-WQZZDN5M.js → chunk-IVEHY2VT.js} +254 -377
  110. package/dist/chunk-IVEHY2VT.js.map +1 -0
  111. package/dist/{chunk-LS3XH3UE.js → chunk-IYIBGSHJ.js} +2 -2
  112. package/dist/{chunk-YSJTCOHR.js → chunk-J27JML5O.js} +2 -2
  113. package/dist/{chunk-AQK54Q7O.js → chunk-JB56XYHP.js} +2 -2
  114. package/dist/{chunk-NE6PHPLG.js → chunk-JBH26P62.js} +10 -10
  115. package/dist/{chunk-CWHV574S.js → chunk-JUT3OR2L.js} +6 -6
  116. package/dist/{chunk-6ET2VII7.js → chunk-JYG2334B.js} +2 -2
  117. package/dist/{chunk-LF5IGOOD.js → chunk-JYM4Y6SO.js} +16 -16
  118. package/dist/{chunk-FB64XE3R.js → chunk-JZ4TJHR2.js} +2 -2
  119. package/dist/{chunk-EHS5MJEF.js → chunk-KTKHO3UI.js} +3 -3
  120. package/dist/{chunk-WIKO4SCR.js → chunk-LHRQBMJN.js} +2 -2
  121. package/dist/{chunk-K4J4E7WV.js → chunk-LO4UKPKX.js} +2 -2
  122. package/dist/{chunk-JN72EPOE.js → chunk-MAAL6OZV.js} +7 -7
  123. package/dist/chunk-MAAL6OZV.js.map +1 -0
  124. package/dist/{chunk-MY2PLXSM.js → chunk-MCTM4YPL.js} +2 -1
  125. package/dist/{chunk-MY2PLXSM.js.map → chunk-MCTM4YPL.js.map} +1 -1
  126. package/dist/{chunk-6NQ4LSMS.js → chunk-MGZMC6WN.js} +47 -47
  127. package/dist/chunk-MSAI3Q7D.js +372 -0
  128. package/dist/chunk-MSAI3Q7D.js.map +1 -0
  129. package/dist/{chunk-BMYZTZ5D.js → chunk-MY7B3HYC.js} +2 -2
  130. package/dist/{chunk-NB3LRY4T.js → chunk-N4PYB235.js} +2 -2
  131. package/dist/{chunk-S3YM4SZQ.js → chunk-N7QV3CMX.js} +2 -2
  132. package/dist/{chunk-L6UZEAID.js → chunk-NJVR724O.js} +4 -4
  133. package/dist/{chunk-VLLD4TOB.js → chunk-NMZVDRVP.js} +2 -2
  134. package/dist/{chunk-P2WZHLUF.js → chunk-NOL7SEWN.js} +2 -2
  135. package/dist/{chunk-2NLLXCJG.js → chunk-OWHERGF2.js} +2 -2
  136. package/dist/{chunk-6L3PBSAR.js → chunk-PMNLL2TL.js} +2 -2
  137. package/dist/{chunk-VYCOJKSX.js → chunk-PO7NXNGC.js} +2 -2
  138. package/dist/{chunk-4IIMMZZK.js → chunk-PULDSMCJ.js} +10 -10
  139. package/dist/{chunk-G6SVZWPB.js → chunk-PZX3AQLF.js} +44 -1
  140. package/dist/chunk-PZX3AQLF.js.map +1 -0
  141. package/dist/{chunk-HBBHL4YF.js → chunk-QOXNTKXB.js} +2 -2
  142. package/dist/{chunk-CUCWX3MO.js → chunk-QZLCVO2V.js} +2 -2
  143. package/dist/{chunk-NE7I2RCT.js → chunk-R3X2T2LP.js} +2 -2
  144. package/dist/{chunk-S7GCT7NU.js → chunk-RBITPCMK.js} +8 -8
  145. package/dist/{chunk-EVBI7S4J.js → chunk-RMDGAAZF.js} +4 -4
  146. package/dist/{chunk-BJRAELDH.js → chunk-SCXURCVT.js} +3 -3
  147. package/dist/{chunk-EQ6YOIKO.js → chunk-ST2RQUXC.js} +3 -3
  148. package/dist/{chunk-CS52MFDV.js → chunk-SZG4WFAG.js} +2 -2
  149. package/dist/{chunk-BFO3IR56.js → chunk-TPEZ2SHV.js} +2 -2
  150. package/dist/chunk-TYT4TEQV.js +45 -0
  151. package/dist/chunk-TYT4TEQV.js.map +1 -0
  152. package/dist/{chunk-YRLHEEQT.js → chunk-U6VC6RP3.js} +2 -2
  153. package/dist/chunk-UCPGOUDZ.js +726 -0
  154. package/dist/chunk-UCPGOUDZ.js.map +1 -0
  155. package/dist/{chunk-MOQPEFLN.js → chunk-UP75KL3L.js} +2 -2
  156. package/dist/{chunk-W4AOR52H.js → chunk-UQDC3W77.js} +2 -2
  157. package/dist/{chunk-X7Y7WX73.js → chunk-UVYI6VIX.js} +1 -1
  158. package/dist/{chunk-D5F7TKMH.js → chunk-V4JZCTMK.js} +2 -2
  159. package/dist/{chunk-MQJL6CDZ.js → chunk-VA4O3U7U.js} +4 -4
  160. package/dist/{chunk-4CDYONWO.js → chunk-VDO32NLK.js} +3 -3
  161. package/dist/{chunk-SOZUJCME.js → chunk-VHDKPGMG.js} +6 -6
  162. package/dist/{chunk-QWW2JDDX.js → chunk-WNRTXEWW.js} +6 -6
  163. package/dist/{chunk-NR2ASHBL.js → chunk-XOV7NRY5.js} +2 -2
  164. package/dist/{chunk-KP7SANCF.js → chunk-YPI4RQWV.js} +2 -2
  165. package/dist/{chunk-JKMHZF3Q.js → chunk-YSWJXOOL.js} +2 -2
  166. package/dist/{chunk-LQG5O5EP.js → chunk-Z3O53WZR.js} +2 -2
  167. package/dist/{chunk-QVS22WTO.js → chunk-ZG5DWWKK.js} +2 -2
  168. package/dist/cli.js +99 -92
  169. package/dist/coding/export-okf-codegraph.js +12 -12
  170. package/dist/coding/pre-action-gate.js +2 -2
  171. package/dist/compounding/engine.js +12 -12
  172. package/dist/config.js +24 -18
  173. package/dist/connectors/codex-materialize-runner.js +11 -11
  174. package/dist/connectors/index.js +11 -11
  175. package/dist/console/state.d.ts +30 -0
  176. package/dist/console/state.d.ts.map +1 -1
  177. package/dist/contradiction/index.js +5 -5
  178. package/dist/convergence-refresh.js +13 -13
  179. package/dist/corpus-watermark.js +13 -13
  180. package/dist/dashboard-runtime.js +2 -2
  181. package/dist/deep-recall-http-glue.js +16 -8
  182. package/dist/embedding-fallback.d.ts +41 -31
  183. package/dist/embedding-fallback.d.ts.map +1 -1
  184. package/dist/embedding-fallback.js +9 -1
  185. package/dist/embedding-generation-lock.d.ts +49 -0
  186. package/dist/embedding-generation-lock.d.ts.map +1 -0
  187. package/dist/embedding-generation-lock.js +19 -0
  188. package/dist/embedding-index-storage.d.ts +276 -0
  189. package/dist/embedding-index-storage.d.ts.map +1 -0
  190. package/dist/embedding-index-storage.js +28 -0
  191. package/dist/entity-retrieval.js +12 -12
  192. package/dist/event-order-recall.js +3 -3
  193. package/dist/explicit-capture.js +6 -6
  194. package/dist/external-wiki-access.js +57 -50
  195. package/dist/extraction.js +17 -17
  196. package/dist/{first-start-migration-7OATHJKB.js → first-start-migration-GAT7G733.js} +4 -4
  197. package/dist/{graph-edge-decay-KQGS3L3B.js → graph-edge-decay-MKIRZ762.js} +5 -5
  198. package/dist/graph-jsonl.js +3 -3
  199. package/dist/graph-snapshot.js +3 -3
  200. package/dist/graph.js +2 -2
  201. package/dist/harmonic-retrieval.js +1 -1
  202. package/dist/index.d.ts +2 -0
  203. package/dist/index.d.ts.map +1 -1
  204. package/dist/index.js +195 -164
  205. package/dist/lcm/engine.js +2 -2
  206. package/dist/lcm/index.js +5 -5
  207. package/dist/local-llm.js +2 -2
  208. package/dist/location/index.js +14 -14
  209. package/dist/maintenance/memory-governance.js +11 -11
  210. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +11 -11
  211. package/dist/maintenance/rebuild-memory-projection.js +12 -12
  212. package/dist/memory-browse-operations.js +57 -50
  213. package/dist/memory-subject.js +3 -3
  214. package/dist/namespaces/migrate.js +12 -12
  215. package/dist/namespaces/storage.js +11 -11
  216. package/dist/offline-sync-changeset-generations.d.ts +82 -0
  217. package/dist/offline-sync-changeset-generations.d.ts.map +1 -0
  218. package/dist/offline-sync-changeset-generations.js +21 -0
  219. package/dist/offline-sync-embedding-generation.d.ts +257 -0
  220. package/dist/offline-sync-embedding-generation.d.ts.map +1 -0
  221. package/dist/offline-sync-embedding-generation.js +44 -0
  222. package/dist/offline-sync-embedding-generation.js.map +1 -0
  223. package/dist/offline-sync-exclude-globs.d.ts +15 -0
  224. package/dist/offline-sync-exclude-globs.d.ts.map +1 -1
  225. package/dist/offline-sync-exclude-globs.js +11 -3
  226. package/dist/offline-sync-file-io.d.ts +7 -0
  227. package/dist/offline-sync-file-io.d.ts.map +1 -1
  228. package/dist/offline-sync-file-io.js +14 -2
  229. package/dist/offline-sync-impression-drain.js +3 -3
  230. package/dist/offline-sync-runtime-state.d.ts +5 -0
  231. package/dist/offline-sync-runtime-state.d.ts.map +1 -0
  232. package/dist/offline-sync-runtime-state.js +14 -0
  233. package/dist/offline-sync-runtime-state.js.map +1 -0
  234. package/dist/offline-sync-snapshot-stream.d.ts +26 -0
  235. package/dist/offline-sync-snapshot-stream.d.ts.map +1 -0
  236. package/dist/offline-sync-snapshot-stream.js +29 -0
  237. package/dist/offline-sync-snapshot-stream.js.map +1 -0
  238. package/dist/offline-sync-upload-staging.d.ts +34 -0
  239. package/dist/offline-sync-upload-staging.d.ts.map +1 -0
  240. package/dist/offline-sync-upload-staging.js +26 -0
  241. package/dist/offline-sync-upload-staging.js.map +1 -0
  242. package/dist/offline-sync.d.ts +58 -28
  243. package/dist/offline-sync.d.ts.map +1 -1
  244. package/dist/offline-sync.js +22 -8
  245. package/dist/operator-doctor-corpus.js +14 -14
  246. package/dist/operator-toolkit.js +35 -29
  247. package/dist/orchestration/maintenance.js +22 -22
  248. package/dist/orchestrator.d.ts.map +1 -1
  249. package/dist/orchestrator.js +129 -122
  250. package/dist/recall-navigation-operations.js +57 -50
  251. package/dist/recall-navigation.js +56 -49
  252. package/dist/reconcile/manifest.js +4 -4
  253. package/dist/{resolution-DNX7FO7A.js → resolution-K4A3VPKZ.js} +6 -6
  254. package/dist/resolution-K4A3VPKZ.js.map +1 -0
  255. package/dist/resume-bundles.js +25 -19
  256. package/dist/salvage-envelope.js +5 -5
  257. package/dist/secure-store/index.js +2 -2
  258. package/dist/secure-store/secure-fs.d.ts +14 -1
  259. package/dist/secure-store/secure-fs.d.ts.map +1 -1
  260. package/dist/semantic-consolidation.js +12 -12
  261. package/dist/semantic-rule-promotion.js +11 -11
  262. package/dist/semantic-rule-verifier.js +11 -11
  263. package/dist/source-agent-qualifier.js +14 -14
  264. package/dist/standup/build.js +11 -11
  265. package/dist/{state-ITXJKXRE.js → state-JB47KHYV.js} +2 -2
  266. package/dist/state-JB47KHYV.js.map +1 -0
  267. package/dist/storage.d.ts +9 -3
  268. package/dist/storage.d.ts.map +1 -1
  269. package/dist/storage.js +10 -10
  270. package/dist/subject-config.js +4 -4
  271. package/dist/summarizer.js +3 -3
  272. package/dist/support-passport/index.js +59 -52
  273. package/dist/support-passport/offline-sync-guard.d.ts +6 -2
  274. package/dist/support-passport/offline-sync-guard.d.ts.map +1 -1
  275. package/dist/{trace-ZFVRH4GO.js → trace-LXXCMZJA.js} +3 -3
  276. package/dist/transfer/backup.js +2 -2
  277. package/dist/transfer/capsule-export.js +2 -2
  278. package/dist/transfer/capsule-import.js +3 -3
  279. package/dist/transfer/export-okf.js +12 -12
  280. package/dist/{tui-55UJMJX7.js → tui-2Z5WOV7L.js} +3 -3
  281. package/dist/tui-2Z5WOV7L.js.map +1 -0
  282. package/dist/verified-recall.js +11 -11
  283. package/dist/who-knows-cli.js +12 -12
  284. package/dist/who-knows.js +11 -11
  285. package/dist/write-envelope.js +4 -4
  286. package/package.json +2 -2
  287. package/src/access-http-offline-stream.ts +3 -0
  288. package/src/access-mcp-output-schemas.ts +1 -0
  289. package/src/access-service.ts +7 -18
  290. package/src/console/state.test.ts +56 -0
  291. package/src/console/state.ts +83 -0
  292. package/src/embedding-fallback.ts +325 -141
  293. package/src/embedding-generation-lock.ts +141 -0
  294. package/src/embedding-index-storage.test.ts +245 -0
  295. package/src/embedding-index-storage.ts +1068 -0
  296. package/src/index.ts +3 -3
  297. package/src/offline-sync-changeset-generations.test.ts +962 -0
  298. package/src/offline-sync-changeset-generations.ts +549 -0
  299. package/src/offline-sync-embedding-generation.test.ts +2173 -0
  300. package/src/offline-sync-embedding-generation.ts +1153 -0
  301. package/src/offline-sync-exclude-globs.ts +112 -0
  302. package/src/offline-sync-file-io.ts +48 -0
  303. package/src/offline-sync-runtime-state.ts +48 -0
  304. package/src/offline-sync-snapshot-stream.ts +99 -0
  305. package/src/offline-sync-upload-staging.ts +291 -0
  306. package/src/offline-sync.test.ts +164 -1
  307. package/src/offline-sync.ts +372 -454
  308. package/src/orchestrator.ts +2 -1
  309. package/src/secure-store/secure-fs.ts +26 -6
  310. package/src/storage.ts +11 -6
  311. package/src/support-passport/offline-sync-guard.ts +16 -2
  312. package/dist/chunk-3BTFBQLO.js.map +0 -1
  313. package/dist/chunk-AREGMX2J.js +0 -50
  314. package/dist/chunk-AREGMX2J.js.map +0 -1
  315. package/dist/chunk-FOH7AMMI.js.map +0 -1
  316. package/dist/chunk-FRCESVV6.js.map +0 -1
  317. package/dist/chunk-G6SVZWPB.js.map +0 -1
  318. package/dist/chunk-JN72EPOE.js.map +0 -1
  319. package/dist/chunk-RB776UH3.js.map +0 -1
  320. package/dist/chunk-WQZZDN5M.js.map +0 -1
  321. /package/dist/{auto-sync-6IPX2BAC.js.map → auto-sync-U5N3CB5Y.js.map} +0 -0
  322. /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
  323. /package/dist/{capsule-merge-3MNURAQ3.js.map → capsule-merge-ZCXUPLNJ.js.map} +0 -0
  324. /package/dist/{chunk-JE334SIA.js.map → chunk-22DYJLFN.js.map} +0 -0
  325. /package/dist/{chunk-JGSQ6P4D.js.map → chunk-2FX374UI.js.map} +0 -0
  326. /package/dist/{chunk-SY5KDWGU.js.map → chunk-2WLMWP2T.js.map} +0 -0
  327. /package/dist/{chunk-6TT6HRE4.js.map → chunk-3AGHSQPM.js.map} +0 -0
  328. /package/dist/{chunk-P6WFK4ZJ.js.map → chunk-3O5BB45T.js.map} +0 -0
  329. /package/dist/{chunk-3C3J243X.js.map → chunk-3QKO4QN7.js.map} +0 -0
  330. /package/dist/{chunk-KFCMMDHH.js.map → chunk-3YD44CH2.js.map} +0 -0
  331. /package/dist/{chunk-7DE2NOON.js.map → chunk-4O4XCBOS.js.map} +0 -0
  332. /package/dist/{chunk-GDYU6VF4.js.map → chunk-5AYUHSAU.js.map} +0 -0
  333. /package/dist/{chunk-ICZQG4XE.js.map → chunk-5FAGCOZD.js.map} +0 -0
  334. /package/dist/{chunk-VKQI47DJ.js.map → chunk-5FNZ3G6X.js.map} +0 -0
  335. /package/dist/{chunk-VQNONC4E.js.map → chunk-5STPRPE7.js.map} +0 -0
  336. /package/dist/{chunk-52II7PB6.js.map → chunk-645WOU5I.js.map} +0 -0
  337. /package/dist/{chunk-SRZW7FNU.js.map → chunk-64ZUALXK.js.map} +0 -0
  338. /package/dist/{chunk-4BZP2B4U.js.map → chunk-6MNKDTDI.js.map} +0 -0
  339. /package/dist/{chunk-DUJYRVUD.js.map → chunk-6O2VUWV6.js.map} +0 -0
  340. /package/dist/{chunk-HESDNYRB.js.map → chunk-76M5OK7V.js.map} +0 -0
  341. /package/dist/{chunk-FAN73COB.js.map → chunk-7D7OUO5W.js.map} +0 -0
  342. /package/dist/{chunk-DZ525SNQ.js.map → chunk-7QE46QXI.js.map} +0 -0
  343. /package/dist/{chunk-VX6GCKBS.js.map → chunk-7WBSMXGC.js.map} +0 -0
  344. /package/dist/{chunk-VVDWZRZG.js.map → chunk-A6RVIHHK.js.map} +0 -0
  345. /package/dist/{chunk-M536WNTQ.js.map → chunk-AEJ3TE5H.js.map} +0 -0
  346. /package/dist/{chunk-72JNNJJJ.js.map → chunk-AXC4WSF3.js.map} +0 -0
  347. /package/dist/{chunk-UA5DX5C3.js.map → chunk-AZIOEOK4.js.map} +0 -0
  348. /package/dist/{chunk-N4S4NUX5.js.map → chunk-B3O3IRZN.js.map} +0 -0
  349. /package/dist/{chunk-XTYX6V52.js.map → chunk-BJVO4H5C.js.map} +0 -0
  350. /package/dist/{chunk-7LCGFUMB.js.map → chunk-BUKK5SWA.js.map} +0 -0
  351. /package/dist/{chunk-TM3RXL4G.js.map → chunk-BUQKSQXL.js.map} +0 -0
  352. /package/dist/{chunk-37S3IMYB.js.map → chunk-DGS34QSY.js.map} +0 -0
  353. /package/dist/{chunk-4IEOGIPB.js.map → chunk-DO4C4AXD.js.map} +0 -0
  354. /package/dist/{chunk-SPSRGEDQ.js.map → chunk-EAQQ2FD3.js.map} +0 -0
  355. /package/dist/{chunk-C3QXITIP.js.map → chunk-EQUD7FP4.js.map} +0 -0
  356. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  357. /package/dist/{chunk-YITN5WXI.js.map → chunk-FC52YWAO.js.map} +0 -0
  358. /package/dist/{chunk-ZI7QA4MK.js.map → chunk-FEIXG5FA.js.map} +0 -0
  359. /package/dist/{chunk-CLXTDOKV.js.map → chunk-FUNBJQYR.js.map} +0 -0
  360. /package/dist/{chunk-CLSYE5OZ.js.map → chunk-G2STTRYB.js.map} +0 -0
  361. /package/dist/{chunk-TDYAIXHK.js.map → chunk-H36X4NRH.js.map} +0 -0
  362. /package/dist/{chunk-3GQC3NMB.js.map → chunk-H56VY4BY.js.map} +0 -0
  363. /package/dist/{chunk-PUKTBULQ.js.map → chunk-HWLIMQE2.js.map} +0 -0
  364. /package/dist/{chunk-V2ES4WCZ.js.map → chunk-IIARQTZ2.js.map} +0 -0
  365. /package/dist/{chunk-QEV26L2F.js.map → chunk-IKDRGVVJ.js.map} +0 -0
  366. /package/dist/{chunk-LS3XH3UE.js.map → chunk-IYIBGSHJ.js.map} +0 -0
  367. /package/dist/{chunk-YSJTCOHR.js.map → chunk-J27JML5O.js.map} +0 -0
  368. /package/dist/{chunk-AQK54Q7O.js.map → chunk-JB56XYHP.js.map} +0 -0
  369. /package/dist/{chunk-NE6PHPLG.js.map → chunk-JBH26P62.js.map} +0 -0
  370. /package/dist/{chunk-CWHV574S.js.map → chunk-JUT3OR2L.js.map} +0 -0
  371. /package/dist/{chunk-6ET2VII7.js.map → chunk-JYG2334B.js.map} +0 -0
  372. /package/dist/{chunk-LF5IGOOD.js.map → chunk-JYM4Y6SO.js.map} +0 -0
  373. /package/dist/{chunk-FB64XE3R.js.map → chunk-JZ4TJHR2.js.map} +0 -0
  374. /package/dist/{chunk-EHS5MJEF.js.map → chunk-KTKHO3UI.js.map} +0 -0
  375. /package/dist/{chunk-WIKO4SCR.js.map → chunk-LHRQBMJN.js.map} +0 -0
  376. /package/dist/{chunk-K4J4E7WV.js.map → chunk-LO4UKPKX.js.map} +0 -0
  377. /package/dist/{chunk-6NQ4LSMS.js.map → chunk-MGZMC6WN.js.map} +0 -0
  378. /package/dist/{chunk-BMYZTZ5D.js.map → chunk-MY7B3HYC.js.map} +0 -0
  379. /package/dist/{chunk-NB3LRY4T.js.map → chunk-N4PYB235.js.map} +0 -0
  380. /package/dist/{chunk-S3YM4SZQ.js.map → chunk-N7QV3CMX.js.map} +0 -0
  381. /package/dist/{chunk-L6UZEAID.js.map → chunk-NJVR724O.js.map} +0 -0
  382. /package/dist/{chunk-VLLD4TOB.js.map → chunk-NMZVDRVP.js.map} +0 -0
  383. /package/dist/{chunk-P2WZHLUF.js.map → chunk-NOL7SEWN.js.map} +0 -0
  384. /package/dist/{chunk-2NLLXCJG.js.map → chunk-OWHERGF2.js.map} +0 -0
  385. /package/dist/{chunk-6L3PBSAR.js.map → chunk-PMNLL2TL.js.map} +0 -0
  386. /package/dist/{chunk-VYCOJKSX.js.map → chunk-PO7NXNGC.js.map} +0 -0
  387. /package/dist/{chunk-4IIMMZZK.js.map → chunk-PULDSMCJ.js.map} +0 -0
  388. /package/dist/{chunk-HBBHL4YF.js.map → chunk-QOXNTKXB.js.map} +0 -0
  389. /package/dist/{chunk-CUCWX3MO.js.map → chunk-QZLCVO2V.js.map} +0 -0
  390. /package/dist/{chunk-NE7I2RCT.js.map → chunk-R3X2T2LP.js.map} +0 -0
  391. /package/dist/{chunk-S7GCT7NU.js.map → chunk-RBITPCMK.js.map} +0 -0
  392. /package/dist/{chunk-EVBI7S4J.js.map → chunk-RMDGAAZF.js.map} +0 -0
  393. /package/dist/{chunk-BJRAELDH.js.map → chunk-SCXURCVT.js.map} +0 -0
  394. /package/dist/{chunk-EQ6YOIKO.js.map → chunk-ST2RQUXC.js.map} +0 -0
  395. /package/dist/{chunk-CS52MFDV.js.map → chunk-SZG4WFAG.js.map} +0 -0
  396. /package/dist/{chunk-BFO3IR56.js.map → chunk-TPEZ2SHV.js.map} +0 -0
  397. /package/dist/{chunk-YRLHEEQT.js.map → chunk-U6VC6RP3.js.map} +0 -0
  398. /package/dist/{chunk-MOQPEFLN.js.map → chunk-UP75KL3L.js.map} +0 -0
  399. /package/dist/{chunk-W4AOR52H.js.map → chunk-UQDC3W77.js.map} +0 -0
  400. /package/dist/{chunk-X7Y7WX73.js.map → chunk-UVYI6VIX.js.map} +0 -0
  401. /package/dist/{chunk-D5F7TKMH.js.map → chunk-V4JZCTMK.js.map} +0 -0
  402. /package/dist/{chunk-MQJL6CDZ.js.map → chunk-VA4O3U7U.js.map} +0 -0
  403. /package/dist/{chunk-4CDYONWO.js.map → chunk-VDO32NLK.js.map} +0 -0
  404. /package/dist/{chunk-SOZUJCME.js.map → chunk-VHDKPGMG.js.map} +0 -0
  405. /package/dist/{chunk-QWW2JDDX.js.map → chunk-WNRTXEWW.js.map} +0 -0
  406. /package/dist/{chunk-NR2ASHBL.js.map → chunk-XOV7NRY5.js.map} +0 -0
  407. /package/dist/{chunk-KP7SANCF.js.map → chunk-YPI4RQWV.js.map} +0 -0
  408. /package/dist/{chunk-JKMHZF3Q.js.map → chunk-YSWJXOOL.js.map} +0 -0
  409. /package/dist/{chunk-LQG5O5EP.js.map → chunk-Z3O53WZR.js.map} +0 -0
  410. /package/dist/{chunk-QVS22WTO.js.map → chunk-ZG5DWWKK.js.map} +0 -0
  411. /package/dist/{graph-edge-decay-KQGS3L3B.js.map → embedding-generation-lock.js.map} +0 -0
  412. /package/dist/{resolution-DNX7FO7A.js.map → embedding-index-storage.js.map} +0 -0
  413. /package/dist/{first-start-migration-7OATHJKB.js.map → first-start-migration-GAT7G733.js.map} +0 -0
  414. /package/dist/{state-ITXJKXRE.js.map → graph-edge-decay-MKIRZ762.js.map} +0 -0
  415. /package/dist/{tui-55UJMJX7.js.map → offline-sync-changeset-generations.js.map} +0 -0
  416. /package/dist/{trace-ZFVRH4GO.js.map → trace-LXXCMZJA.js.map} +0 -0
@@ -0,0 +1,962 @@
1
+ // Regression suite for issue #3150: offline-sync CHANGESET pushes must be
2
+ // embedding-generation atomic, mirroring the snapshot-side guarantees from
3
+ // PR #3148 (issue #3146):
4
+ // 1. sender changesets carry a complete post-change membership manifest
5
+ // (`embeddingGenerations`) for every touched generation;
6
+ // 2. the receiver routes manifested generation changes through the shared
7
+ // locked atomic transaction — changed members publish, unchanged members
8
+ // carry over from local bytes (hash-verified against the manifest),
9
+ // removed members sweep — never a per-file mixed generation;
10
+ // 3. delete-only changes shrink through the manifest (survivors carried),
11
+ // while a manifest-less whole-generation tombstone removes atomically;
12
+ // 4. legacy changesets without a manifest DEFER shard upserts and surface
13
+ // CONFLICTS (never silent skips) so the push-side base cannot advance
14
+ // past an unapplied generation;
15
+ // 5. divergence under the lock (daemon rewrote a member, remote-only
16
+ // member) defers the WHOLE generation as conflicts, disk untouched;
17
+ // 6. secure staging hooks are required (fail closed) exactly like the
18
+ // snapshot path — no plaintext-downgrade of staged generations;
19
+ // 7. partially filtered generations are omitted whole (filter-to-omit).
20
+ import assert from "node:assert/strict";
21
+ import { createHash } from "node:crypto";
22
+ import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
23
+ import os from "node:os";
24
+ import path from "node:path";
25
+ import test from "node:test";
26
+
27
+ import {
28
+ type EmbeddingIndexEntry,
29
+ type EmbeddingIndexFile,
30
+ EmbeddingIndexStorageError,
31
+ parseEmbeddingIndexDocument,
32
+ } from "./embedding-index-storage.js";
33
+ import {
34
+ applyOfflineSyncChangeset,
35
+ buildOfflineSyncChangesetFromSnapshot,
36
+ normalizeOfflineSyncChangeset,
37
+ type OfflineSyncChangeset,
38
+ type OfflineSyncChangesetGeneration,
39
+ type OfflineSyncChange,
40
+ type OfflineSyncFileState,
41
+ } from "./offline-sync.js";
42
+ import { buildChangesetGenerationManifest } from "./offline-sync-changeset-generations.js";
43
+
44
+ const SHARD_COUNT = 64; // embedding-index-storage internal; ids below pre-hashed
45
+
46
+ /** Ids pre-hashed with shardIndexOf(id, 64): m8→shard 0, m10→1, m86→2, m45→3. */
47
+ const SHARD_IDS = ["m8", "m10", "m86", "m45"] as const;
48
+
49
+ function shardRel(index: number): string {
50
+ return `state/embeddings/shard-${String(index).padStart(4, "0")}.json`;
51
+ }
52
+
53
+ function sha256(content: string | Buffer): string {
54
+ return createHash("sha256").update(content).digest("hex");
55
+ }
56
+
57
+ function shardDoc(entries: Record<string, EmbeddingIndexEntry>): Buffer {
58
+ const file: EmbeddingIndexFile = {
59
+ version: 1,
60
+ provider: "openai",
61
+ model: "text-embedding-3-small",
62
+ entries,
63
+ };
64
+ return Buffer.from(JSON.stringify(file), "utf-8");
65
+ }
66
+
67
+ async function tempDir(name: string): Promise<string> {
68
+ return mkdtemp(path.join(os.tmpdir(), `${name}-`));
69
+ }
70
+
71
+ async function write(root: string, relPath: string, content: string | Buffer): Promise<void> {
72
+ await mkdir(path.join(root, path.dirname(relPath)), { recursive: true });
73
+ await writeFile(path.join(root, relPath), content);
74
+ }
75
+
76
+ async function readAll(root: string, relPath: string): Promise<Buffer> {
77
+ return readFile(path.join(root, relPath));
78
+ }
79
+
80
+ async function shardNames(root: string, shardDirRel = "state/embeddings"): Promise<string[]> {
81
+ try {
82
+ const names = await readdir(path.join(root, ...shardDirRel.split("/")));
83
+ return names.filter((n) => /^shard-\d{4}\.json$/.test(n)).sort();
84
+ } catch (err) {
85
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
86
+ throw err;
87
+ }
88
+ }
89
+
90
+ async function diskGeneration(root: string): Promise<Map<string, string>> {
91
+ const out = new Map<string, string>();
92
+ for (const name of await shardNames(root)) {
93
+ const rel = `state/embeddings/${name}`;
94
+ out.set(rel, sha256(await readAll(root, rel)));
95
+ }
96
+ return out;
97
+ }
98
+
99
+ function upsertChange(
100
+ relPath: string,
101
+ content: Buffer,
102
+ mtimeMs: number,
103
+ baseSha256?: string,
104
+ ): OfflineSyncChange {
105
+ return {
106
+ type: "upsert",
107
+ path: relPath,
108
+ ...(baseSha256 ? { baseSha256 } : {}),
109
+ file: {
110
+ path: relPath,
111
+ sha256: sha256(content),
112
+ bytes: content.length,
113
+ mtimeMs,
114
+ contentBase64: content.toString("base64"),
115
+ },
116
+ };
117
+ }
118
+
119
+ function deleteChange(relPath: string, baseSha256: string, mtimeMs?: number): OfflineSyncChange {
120
+ return { type: "delete", path: relPath, baseSha256, ...(mtimeMs === undefined ? {} : { mtimeMs }) };
121
+ }
122
+
123
+ function member(relPath: string, content: Buffer): { path: string; sha256: string; bytes: number } {
124
+ return { path: relPath, sha256: sha256(content), bytes: content.length };
125
+ }
126
+
127
+ function baseChangeset(changes: OfflineSyncChange[], embeddingGenerations?: OfflineSyncChangesetGeneration[]): OfflineSyncChangeset {
128
+ return {
129
+ format: "remnic.offline-sync.changeset.v1",
130
+ schemaVersion: 1,
131
+ createdAt: "2026-09-27T00:00:00.000Z",
132
+ sourceId: "test-sender",
133
+ includeTranscripts: true,
134
+ changes,
135
+ ...(embeddingGenerations === undefined ? {} : { embeddingGenerations }),
136
+ };
137
+ }
138
+
139
+ /** Secure-IO spies: real filesystem effects + call recording. */
140
+ function makeIo(root: string) {
141
+ const calls = { stagingWrites: [] as string[], stagingReads: [] as string[], plainWrites: [] as string[], deletes: [] as string[], deleteMtimes: [] as (number | undefined)[] };
142
+ return {
143
+ calls,
144
+ io: {
145
+ readFile: async (target: { path: string }) => readAll(root, target.path),
146
+ readFileDigest: async (target: { path: string }) => {
147
+ const content = await readAll(root, target.path);
148
+ return { sha256: sha256(content), bytes: content.length };
149
+ },
150
+ writeFile: async (target: { path: string; content: Buffer }) => {
151
+ calls.plainWrites.push(target.path);
152
+ await write(root, target.path, target.content);
153
+ },
154
+ writeStagingFile: async (target: { path: string; content: Buffer }) => {
155
+ calls.stagingWrites.push(target.path);
156
+ await write(root, target.path, target.content);
157
+ },
158
+ readStagingFile: async (target: { path: string }) => {
159
+ calls.stagingReads.push(target.path);
160
+ return readAll(root, target.path);
161
+ },
162
+ deleteFile: async (target: { path: string; mtimeMs?: number }) => {
163
+ calls.deletes.push(target.path);
164
+ calls.deleteMtimes.push(target.mtimeMs);
165
+ await rm(path.join(root, target.path), { force: true });
166
+ },
167
+ },
168
+ };
169
+ }
170
+
171
+ /** Local generation on disk: shards 0..count-1 with one entry each. */
172
+ async function seedLocalGeneration(root: string, count: number): Promise<Map<string, Buffer>> {
173
+ const local = new Map<string, Buffer>();
174
+ for (let i = 0; i < count; i++) {
175
+ const content = shardDoc({ [SHARD_IDS[i]]: { path: `memories/${SHARD_IDS[i]}.md`, vector: [i, 1] } });
176
+ local.set(shardRel(i), content);
177
+ await write(root, shardRel(i), content);
178
+ }
179
+ return local;
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Receiver — manifested changesets apply through the atomic transaction
184
+ // ---------------------------------------------------------------------------
185
+
186
+ test("manifested changeset replaces changed shards and carries unchanged members atomically", async () => {
187
+ const root = await tempDir("remnic-3150-apply");
188
+ try {
189
+ const local = await seedLocalGeneration(root, 3);
190
+ const new0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [9, 9] } });
191
+ const new1 = shardDoc({ [SHARD_IDS[1]]: { path: `memories/${SHARD_IDS[1]}.md`, vector: [8, 8] } });
192
+ const changeset = baseChangeset(
193
+ [
194
+ upsertChange(shardRel(0), new0, 100, sha256(local.get(shardRel(0))!)),
195
+ upsertChange(shardRel(1), new1, 100, sha256(local.get(shardRel(1))!)),
196
+ ],
197
+ [{ shardDir: "state/embeddings", members: [
198
+ member(shardRel(0), new0),
199
+ member(shardRel(1), new1),
200
+ member(shardRel(2), local.get(shardRel(2))!),
201
+ ] }],
202
+ );
203
+ const { io, calls } = makeIo(root);
204
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
205
+ assert.equal(result.conflicts.length, 0);
206
+ assert.ok(result.appliedUpserts >= 2, `changed shards must apply: ${JSON.stringify(result)}`);
207
+ // Changed members published; unchanged member carried over byte-for-byte.
208
+ assert.deepEqual(await readAll(root, shardRel(0)), new0);
209
+ assert.deepEqual(await readAll(root, shardRel(1)), new1);
210
+ assert.deepEqual(await readAll(root, shardRel(2)), local.get(shardRel(2))!);
211
+ // Atomic route: staged ciphertext for EVERY member, no per-file writes.
212
+ assert.ok(calls.stagingWrites.length >= 3, `all members must stage: ${JSON.stringify(calls)}`);
213
+ assert.deepEqual(calls.plainWrites, [], "generation members must never take the per-file write path");
214
+ } finally {
215
+ await rm(root, { recursive: true, force: true });
216
+ }
217
+ });
218
+
219
+ test("delete-only manifested changeset shrinks the generation instead of removing the dir", async () => {
220
+ const root = await tempDir("remnic-3150-shrink");
221
+ try {
222
+ const local = await seedLocalGeneration(root, 3);
223
+ const changeset = baseChangeset(
224
+ [deleteChange(shardRel(2), sha256(local.get(shardRel(2))!))],
225
+ [{ shardDir: "state/embeddings", members: [
226
+ member(shardRel(0), local.get(shardRel(0))!),
227
+ member(shardRel(1), local.get(shardRel(1))!),
228
+ ] }],
229
+ );
230
+ const { io, calls } = makeIo(root);
231
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
232
+ assert.equal(result.conflicts.length, 0);
233
+ assert.deepEqual(await shardNames(root), [shardRel(0).split("/")[2], shardRel(1).split("/")[2]]);
234
+ assert.deepEqual(await readAll(root, shardRel(0)), local.get(shardRel(0))!);
235
+ assert.deepEqual(await readAll(root, shardRel(1)), local.get(shardRel(1))!);
236
+ assert.ok(result.appliedDeletes >= 1, `removed shard must count: ${JSON.stringify(result)}`);
237
+ assert.ok(calls.stagingWrites.length >= 2, "survivors must stage through the atomic swap");
238
+ } finally {
239
+ await rm(root, { recursive: true, force: true });
240
+ }
241
+ });
242
+
243
+ test("manifest-less whole-generation deletions remove atomically with tombstones", async () => {
244
+ const root = await tempDir("remnic-3150-removal");
245
+ try {
246
+ await seedLocalGeneration(root, 2);
247
+ const before = await diskGeneration(root);
248
+ const changeset = baseChangeset([
249
+ deleteChange(shardRel(0), before.get(shardRel(0))!),
250
+ deleteChange(shardRel(1), before.get(shardRel(1))!),
251
+ ]);
252
+ const { io, calls } = makeIo(root);
253
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
254
+ assert.equal(result.conflicts.length, 0);
255
+ assert.deepEqual(await shardNames(root), [], "whole generation must be removed");
256
+ assert.deepEqual(calls.deletes.sort(), [shardRel(0), shardRel(1)].sort(), "tombstones recorded per member");
257
+ } finally {
258
+ await rm(root, { recursive: true, force: true });
259
+ }
260
+ });
261
+
262
+ test("manifest-less partial generation deletions defer as conflicts and preserve the generation", async () => {
263
+ const root = await tempDir("remnic-3150-partial-tombstone");
264
+ try {
265
+ const local = await seedLocalGeneration(root, 3);
266
+ const changeset = baseChangeset([
267
+ deleteChange(shardRel(1), sha256(local.get(shardRel(1))!)),
268
+ ]);
269
+ const { io } = makeIo(root);
270
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
271
+ // Incomplete deletion evidence cannot break the generation apart: the
272
+ // complete published generation survives and the deletes conflict, so
273
+ // the push side cannot advance its base past them.
274
+ assert.deepEqual(await shardNames(root), ["shard-0000.json", "shard-0001.json", "shard-0002.json"]);
275
+ const expected = new Map<string, string>();
276
+ for (const i of [0, 1, 2]) expected.set(shardRel(i), sha256(local.get(shardRel(i))!));
277
+ assert.deepEqual(await diskGeneration(root), expected);
278
+ assert.ok(
279
+ result.conflicts.some((c) => c.path === shardRel(1) && c.reason === "embedding_generation_manifest_required"),
280
+ `partial deletion must conflict: ${JSON.stringify(result.conflicts)}`,
281
+ );
282
+ } finally {
283
+ await rm(root, { recursive: true, force: true });
284
+ }
285
+ });
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // Receiver — deferrals surface as conflicts, never silent skips
289
+ // ---------------------------------------------------------------------------
290
+
291
+ test("legacy changeset without a manifest defers shard upserts as conflicts", async () => {
292
+ const root = await tempDir("remnic-3150-legacy");
293
+ try {
294
+ const local = await seedLocalGeneration(root, 2);
295
+ const new0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [7, 7] } });
296
+ const changeset = baseChangeset([
297
+ upsertChange(shardRel(0), new0, 100, sha256(local.get(shardRel(0))!)),
298
+ ]);
299
+ const { io } = makeIo(root);
300
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
301
+ assert.ok(
302
+ result.conflicts.some((c) => c.path === shardRel(0) && c.reason === "embedding_generation_manifest_required"),
303
+ `deferred member must conflict: ${JSON.stringify(result.conflicts)}`,
304
+ );
305
+ // Nothing written: the local generation is byte-identical to before.
306
+ assert.deepEqual(await readAll(root, shardRel(0)), local.get(shardRel(0))!);
307
+ assert.deepEqual(await readAll(root, shardRel(1)), local.get(shardRel(1))!);
308
+ } finally {
309
+ await rm(root, { recursive: true, force: true });
310
+ }
311
+ });
312
+
313
+ test("legacy marker-only upsert still replaces cross-layout through the transaction", async () => {
314
+ const root = await tempDir("remnic-3150-legacy-marker");
315
+ try {
316
+ await seedLocalGeneration(root, 2);
317
+ const marker = Buffer.from(JSON.stringify({
318
+ version: 1,
319
+ provider: "openai",
320
+ model: "text-embedding-3-small",
321
+ entries: { [SHARD_IDS[2]]: { path: `memories/${SHARD_IDS[2]}.md`, vector: [1, 2] } },
322
+ } satisfies EmbeddingIndexFile), "utf-8");
323
+ const changeset = baseChangeset([upsertChange("state/embeddings.json", marker, 100)]);
324
+ const { io, calls } = makeIo(root);
325
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
326
+ assert.equal(result.conflicts.length, 0, `marker migration must apply: ${JSON.stringify(result.conflicts)}`);
327
+ const names = await shardNames(root);
328
+ assert.ok(names.length > 0, "marker must convert to the sharded layout");
329
+ const converted = names.find((name) => name === "shard-0002.json");
330
+ assert.ok(converted, `marker entry must land in its shard: ${JSON.stringify(names)}`);
331
+ const read = parseEmbeddingIndexDocument((await readAll(root, `state/embeddings/${converted}`)).toString("utf-8"));
332
+ assert.equal(read.outcome, "ok");
333
+ if (read.outcome === "ok") assert.ok(read.file.entries[SHARD_IDS[2]]);
334
+ assert.ok(calls.stagingWrites.length > 0, "marker migration must stage through the swap");
335
+ } finally {
336
+ await rm(root, { recursive: true, force: true });
337
+ }
338
+ });
339
+
340
+ test("under-lock divergence defers the whole generation as conflicts, disk untouched", async () => {
341
+ const root = await tempDir("remnic-3150-diverged");
342
+ try {
343
+ const local = await seedLocalGeneration(root, 3);
344
+ const new0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [9, 9] } });
345
+ // Daemon rewrote shard 1 AFTER the shared base was captured.
346
+ const drifted = shardDoc({ [SHARD_IDS[1]]: { path: `memories/${SHARD_IDS[1]}.md`, vector: [5, 5] } });
347
+ await write(root, shardRel(1), drifted);
348
+ const changeset = baseChangeset(
349
+ [upsertChange(shardRel(0), new0, 100, sha256(local.get(shardRel(0))!))],
350
+ [{ shardDir: "state/embeddings", members: [
351
+ member(shardRel(0), new0),
352
+ member(shardRel(1), local.get(shardRel(1))!),
353
+ member(shardRel(2), local.get(shardRel(2))!),
354
+ ] }],
355
+ );
356
+ const { io } = makeIo(root);
357
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
358
+ assert.ok(
359
+ result.conflicts.some((c) => c.path === shardRel(0) && c.reason === "embedding_generation_diverged"),
360
+ `diverged generation must conflict: ${JSON.stringify(result.conflicts)}`,
361
+ );
362
+ assert.deepEqual(await readAll(root, shardRel(0)), local.get(shardRel(0))!);
363
+ assert.deepEqual(await readAll(root, shardRel(1)), drifted);
364
+ } finally {
365
+ await rm(root, { recursive: true, force: true });
366
+ }
367
+ });
368
+
369
+ test("a remote-only local member (not in base or manifest) conflicts the whole generation", async () => {
370
+ const root = await tempDir("remnic-3150-remote-only");
371
+ try {
372
+ const local = await seedLocalGeneration(root, 2);
373
+ const extra = shardDoc({ [SHARD_IDS[3]]: { path: `memories/${SHARD_IDS[3]}.md`, vector: [3, 3] } });
374
+ await write(root, shardRel(3), extra);
375
+ const new0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [9, 9] } });
376
+ const changeset = baseChangeset(
377
+ [upsertChange(shardRel(0), new0, 100, sha256(local.get(shardRel(0))!))],
378
+ [{ shardDir: "state/embeddings", members: [
379
+ member(shardRel(0), new0),
380
+ member(shardRel(1), local.get(shardRel(1))!),
381
+ ] }],
382
+ );
383
+ const { io } = makeIo(root);
384
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
385
+ assert.ok(
386
+ result.conflicts.some((c) => c.reason === "embedding_generation_diverged"),
387
+ `unexpected local member must conflict the generation: ${JSON.stringify(result.conflicts)}`,
388
+ );
389
+ assert.deepEqual(await readAll(root, shardRel(0)), local.get(shardRel(0))!);
390
+ assert.deepEqual(await readAll(root, shardRel(3)), extra);
391
+ } finally {
392
+ await rm(root, { recursive: true, force: true });
393
+ }
394
+ });
395
+
396
+ // ---------------------------------------------------------------------------
397
+ // Receiver — fail-closed staging and crash safety
398
+ // ---------------------------------------------------------------------------
399
+
400
+ test("custom IO without staging hooks refuses generation changesets before any write", async () => {
401
+ const root = await tempDir("remnic-3150-no-staging");
402
+ try {
403
+ const local = await seedLocalGeneration(root, 1);
404
+ const new0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [9, 9] } });
405
+ const changeset = baseChangeset(
406
+ [upsertChange(shardRel(0), new0, 100, sha256(local.get(shardRel(0))!))],
407
+ [{ shardDir: "state/embeddings", members: [member(shardRel(0), new0)] }],
408
+ );
409
+ let plainWrite = false;
410
+ await assert.rejects(
411
+ applyOfflineSyncChangeset({
412
+ root,
413
+ changeset,
414
+ returnCurrentFiles: false,
415
+ readFile: async (target) => readAll(root, target.path),
416
+ writeFile: async (target) => {
417
+ plainWrite = true;
418
+ await write(root, target.path, target.content);
419
+ },
420
+ }),
421
+ (error: unknown) => error instanceof EmbeddingIndexStorageError,
422
+ );
423
+ assert.equal(plainWrite, false, "refusal must happen before any publication");
424
+ assert.deepEqual(await readAll(root, shardRel(0)), local.get(shardRel(0))!);
425
+ } finally {
426
+ await rm(root, { recursive: true, force: true });
427
+ }
428
+ });
429
+
430
+ test("a failed mid-generation stage leaves the local generation byte-identical", async () => {
431
+ const root = await tempDir("remnic-3150-stage-crash");
432
+ try {
433
+ const local = await seedLocalGeneration(root, 2);
434
+ const new0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [9, 9] } });
435
+ const changeset = baseChangeset(
436
+ [upsertChange(shardRel(0), new0, 100, sha256(local.get(shardRel(0))!))],
437
+ [{ shardDir: "state/embeddings", members: [
438
+ member(shardRel(0), new0),
439
+ member(shardRel(1), local.get(shardRel(1))!),
440
+ ] }],
441
+ );
442
+ let staged = 0;
443
+ await assert.rejects(
444
+ applyOfflineSyncChangeset({
445
+ root,
446
+ changeset,
447
+ returnCurrentFiles: false,
448
+ readFile: async (target) => readAll(root, target.path),
449
+ // Fail on the second staged member: a crash mid-generation stage.
450
+ writeStagingFile: async (target) => {
451
+ staged += 1;
452
+ if (staged > 1) throw new Error("simulated stage failure");
453
+ await write(root, target.path, target.content);
454
+ },
455
+ readStagingFile: async (target) => readAll(root, target.path),
456
+ }),
457
+ /simulated stage failure/,
458
+ );
459
+ assert.ok(staged >= 1, "at least one member must have staged before the failure");
460
+ assert.deepEqual(await readAll(root, shardRel(0)), local.get(shardRel(0))!);
461
+ assert.deepEqual(await readAll(root, shardRel(1)), local.get(shardRel(1))!);
462
+ } finally {
463
+ await rm(root, { recursive: true, force: true });
464
+ }
465
+ });
466
+
467
+ test("concurrent changeset applies serialize into one complete generation", async () => {
468
+ const root = await tempDir("remnic-3150-concurrent");
469
+ try {
470
+ const local = await seedLocalGeneration(root, 2);
471
+ const build = (vector: number) => {
472
+ const new0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [vector, vector] } });
473
+ return baseChangeset(
474
+ [upsertChange(shardRel(0), new0, 100, sha256(local.get(shardRel(0))!))],
475
+ [{ shardDir: "state/embeddings", members: [
476
+ member(shardRel(0), new0),
477
+ member(shardRel(1), local.get(shardRel(1))!),
478
+ ] }],
479
+ );
480
+ };
481
+ const { io } = makeIo(root);
482
+ await Promise.all([
483
+ applyOfflineSyncChangeset({ root, changeset: build(7), returnCurrentFiles: false, ...io }),
484
+ applyOfflineSyncChangeset({ root, changeset: build(9), returnCurrentFiles: false, ...io }),
485
+ ]);
486
+ // Whichever transaction wins the lock, the disk must hold ONE complete
487
+ // generation — never a mix of both.
488
+ const census = await diskGeneration(root);
489
+ const winner7 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [7, 7] } });
490
+ const winner9 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [9, 9] } });
491
+ const matches7 = census.get(shardRel(0)) === sha256(winner7);
492
+ const matches9 = census.get(shardRel(0)) === sha256(winner9);
493
+ assert.ok(
494
+ (matches7 || matches9) && census.get(shardRel(1)) === sha256(local.get(shardRel(1))!),
495
+ "the published generation must be exactly one of the concurrent incoming sets",
496
+ );
497
+ } finally {
498
+ await rm(root, { recursive: true, force: true });
499
+ }
500
+ });
501
+
502
+ test("re-applying an already-applied manifested changeset stays idempotent", async () => {
503
+ const root = await tempDir("remnic-3150-idempotent");
504
+ try {
505
+ const local = await seedLocalGeneration(root, 1);
506
+ const new0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [9, 9] } });
507
+ const build = () => baseChangeset(
508
+ [upsertChange(shardRel(0), new0, 100, sha256(local.get(shardRel(0))!))],
509
+ [{ shardDir: "state/embeddings", members: [member(shardRel(0), new0)] }],
510
+ );
511
+ const { io } = makeIo(root);
512
+ await applyOfflineSyncChangeset({ root, changeset: build(), returnCurrentFiles: false, ...io });
513
+ const retry = await applyOfflineSyncChangeset({ root, changeset: build(), returnCurrentFiles: false, ...io });
514
+ assert.equal(retry.conflicts.length, 0, `retry must not conflict: ${JSON.stringify(retry.conflicts)}`);
515
+ assert.deepEqual(await readAll(root, shardRel(0)), new0);
516
+ } finally {
517
+ await rm(root, { recursive: true, force: true });
518
+ }
519
+ });
520
+
521
+ test("unbased manifested creation defers instead of overwriting an independent local generation", async () => {
522
+ const root = await tempDir("remnic-3150-unbased-create");
523
+ try {
524
+ const local = await seedLocalGeneration(root, 2);
525
+ // Fresh sender creation: base-less upserts, no shared base evidence.
526
+ const incoming0 = shardDoc({ [SHARD_IDS[2]]: { path: `memories/${SHARD_IDS[2]}.md`, vector: [7, 7] } });
527
+ const incoming1 = shardDoc({ [SHARD_IDS[3]]: { path: `memories/${SHARD_IDS[3]}.md`, vector: [6, 6] } });
528
+ const changeset = baseChangeset(
529
+ [upsertChange(shardRel(0), incoming0, 100), upsertChange(shardRel(1), incoming1, 100)],
530
+ [{ shardDir: "state/embeddings", members: [member(shardRel(0), incoming0), member(shardRel(1), incoming1)] }],
531
+ );
532
+ const { io } = makeIo(root);
533
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
534
+ assert.ok(
535
+ result.conflicts.some((c) => c.reason === "embedding_generation_diverged"),
536
+ `unbased creation over an existing generation must defer: ${JSON.stringify(result.conflicts)}`,
537
+ );
538
+ assert.deepEqual(await readAll(root, shardRel(0)), local.get(shardRel(0)), "local generation must be untouched");
539
+ assert.deepEqual(await readAll(root, shardRel(1)), local.get(shardRel(1)), "local generation must be untouched");
540
+ } finally {
541
+ await rm(root, { recursive: true, force: true });
542
+ }
543
+ });
544
+
545
+ test("unbased manifested creation applies to an empty receiver and to an exactly matching one", async () => {
546
+ const fresh = await tempDir("remnic-3150-unbased-fresh");
547
+ const matching = await tempDir("remnic-3150-unbased-match");
548
+ try {
549
+ const incoming0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [1, 1] } });
550
+ const incoming1 = shardDoc({ [SHARD_IDS[1]]: { path: `memories/${SHARD_IDS[1]}.md`, vector: [2, 2] } });
551
+ const build = () => baseChangeset(
552
+ [upsertChange(shardRel(0), incoming0, 100), upsertChange(shardRel(1), incoming1, 100)],
553
+ [{ shardDir: "state/embeddings", members: [member(shardRel(0), incoming0), member(shardRel(1), incoming1)] }],
554
+ );
555
+ const freshIo = makeIo(fresh);
556
+ const freshResult = await applyOfflineSyncChangeset({ root: fresh, changeset: build(), returnCurrentFiles: false, ...freshIo.io });
557
+ assert.equal(freshResult.conflicts.length, 0, `fresh create must apply: ${JSON.stringify(freshResult.conflicts)}`);
558
+ assert.deepEqual((await shardNames(fresh)).length, 2);
559
+
560
+ // Identical content already on disk: re-applying the same unbased create is idempotent, not divergent.
561
+ await write(matching, shardRel(0), incoming0);
562
+ await write(matching, shardRel(1), incoming1);
563
+ const matchingIo = makeIo(matching);
564
+ const matchResult = await applyOfflineSyncChangeset({ root: matching, changeset: build(), returnCurrentFiles: false, ...matchingIo.io });
565
+ assert.equal(matchResult.conflicts.length, 0, `identical unbased create must re-apply: ${JSON.stringify(matchResult.conflicts)}`);
566
+ assert.deepEqual(await readAll(matching, shardRel(0)), incoming0);
567
+ } finally {
568
+ await rm(fresh, { recursive: true, force: true });
569
+ await rm(matching, { recursive: true, force: true });
570
+ }
571
+ });
572
+
573
+ test("retrying a manifested shrink after response loss stays idempotent", async () => {
574
+ const root = await tempDir("remnic-3150-shrink-retry");
575
+ try {
576
+ const local = await seedLocalGeneration(root, 3);
577
+ const build = () => baseChangeset(
578
+ [deleteChange(shardRel(2), sha256(local.get(shardRel(2))!))],
579
+ [{ shardDir: "state/embeddings", members: [
580
+ member(shardRel(0), local.get(shardRel(0))!),
581
+ member(shardRel(1), local.get(shardRel(1))!),
582
+ ] }],
583
+ );
584
+ const { io } = makeIo(root);
585
+ await applyOfflineSyncChangeset({ root, changeset: build(), returnCurrentFiles: false, ...io });
586
+ const retry = await applyOfflineSyncChangeset({ root, changeset: build(), returnCurrentFiles: false, ...io });
587
+ assert.equal(retry.conflicts.length, 0, `shrink retry must not diverge: ${JSON.stringify(retry.conflicts)}`);
588
+ assert.deepEqual(await shardNames(root), ["shard-0000.json", "shard-0001.json"]);
589
+ } finally {
590
+ await rm(root, { recursive: true, force: true });
591
+ }
592
+ });
593
+
594
+ test("manifested shrink records deletion revisions for swept members", async () => {
595
+ const root = await tempDir("remnic-3150-shrink-tombstone");
596
+ try {
597
+ const local = await seedLocalGeneration(root, 3);
598
+ const DELETION_MTIME = 555555;
599
+ const changeset = baseChangeset(
600
+ [deleteChange(shardRel(2), sha256(local.get(shardRel(2))!), DELETION_MTIME)],
601
+ [{ shardDir: "state/embeddings", members: [
602
+ member(shardRel(0), local.get(shardRel(0))!),
603
+ member(shardRel(1), local.get(shardRel(1))!),
604
+ ] }],
605
+ );
606
+ const { io, calls } = makeIo(root);
607
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
608
+ assert.equal(result.conflicts.length, 0, `shrink must apply: ${JSON.stringify(result.conflicts)}`);
609
+ assert.deepEqual(await shardNames(root), ["shard-0000.json", "shard-0001.json"]);
610
+ assert.deepEqual(calls.deletes, [shardRel(2)], "swept shard must be tombstoned through the delete hook");
611
+ assert.deepEqual(calls.deleteMtimes, [DELETION_MTIME], "the supplied deletion timestamp must survive");
612
+ } finally {
613
+ await rm(root, { recursive: true, force: true });
614
+ }
615
+ });
616
+
617
+ test("a failing delete hook fails the manifested shrink after recording every tombstone", async () => {
618
+ const root = await tempDir("remnic-3150-shrink-tombstone-fail");
619
+ try {
620
+ const local = await seedLocalGeneration(root, 3);
621
+ const changeset = baseChangeset(
622
+ [deleteChange(shardRel(2), sha256(local.get(shardRel(2))!), 555555)],
623
+ [{ shardDir: "state/embeddings", members: [
624
+ member(shardRel(0), local.get(shardRel(0))!),
625
+ member(shardRel(1), local.get(shardRel(1))!),
626
+ ] }],
627
+ );
628
+ const io = makeIo(root);
629
+ let hookCalls = 0;
630
+ await assert.rejects(
631
+ applyOfflineSyncChangeset({
632
+ root,
633
+ changeset,
634
+ returnCurrentFiles: false,
635
+ ...io.io,
636
+ deleteFile: async () => {
637
+ hookCalls += 1;
638
+ throw new Error("hook down");
639
+ },
640
+ }),
641
+ /hook down/,
642
+ );
643
+ assert.equal(hookCalls, 1, "every swept member must get a tombstone attempt");
644
+ } finally {
645
+ await rm(root, { recursive: true, force: true });
646
+ }
647
+ });
648
+
649
+ test("a daemon rewrite of a to-be-deleted shard still defers the shrink", async () => {
650
+ const root = await tempDir("remnic-3150-shrink-drift");
651
+ try {
652
+ const local = await seedLocalGeneration(root, 3);
653
+ const drifted = shardDoc({ [SHARD_IDS[2]]: { path: `memories/${SHARD_IDS[2]}.md`, vector: [4, 4] } });
654
+ await write(root, shardRel(2), drifted);
655
+ const changeset = baseChangeset(
656
+ [deleteChange(shardRel(2), sha256(local.get(shardRel(2))!), 555555)],
657
+ [{ shardDir: "state/embeddings", members: [
658
+ member(shardRel(0), local.get(shardRel(0))!),
659
+ member(shardRel(1), local.get(shardRel(1))!),
660
+ ] }],
661
+ );
662
+ const { io } = makeIo(root);
663
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
664
+ assert.ok(
665
+ result.conflicts.some((c) => c.reason === "embedding_generation_diverged"),
666
+ `a drifted to-be-deleted shard must defer, not be swept: ${JSON.stringify(result.conflicts)}`,
667
+ );
668
+ assert.deepEqual(await readAll(root, shardRel(2)), drifted, "the daemon rewrite must survive");
669
+ } finally {
670
+ await rm(root, { recursive: true, force: true });
671
+ }
672
+ });
673
+
674
+ test("retrying a manifested addition after response loss stays idempotent", async () => {
675
+ const root = await tempDir("remnic-3150-add-retry");
676
+ try {
677
+ const local = await seedLocalGeneration(root, 2);
678
+ const added = shardDoc({ [SHARD_IDS[2]]: { path: `memories/${SHARD_IDS[2]}.md`, vector: [7, 7] } });
679
+ const build = () => baseChangeset(
680
+ [upsertChange(shardRel(2), added, 100)],
681
+ [{ shardDir: "state/embeddings", members: [
682
+ member(shardRel(0), local.get(shardRel(0))!),
683
+ member(shardRel(1), local.get(shardRel(1))!),
684
+ member(shardRel(2), added),
685
+ ] }],
686
+ );
687
+ const { io } = makeIo(root);
688
+ await applyOfflineSyncChangeset({ root, changeset: build(), returnCurrentFiles: false, ...io });
689
+ const retry = await applyOfflineSyncChangeset({ root, changeset: build(), returnCurrentFiles: false, ...io });
690
+ assert.equal(retry.conflicts.length, 0, `addition retry must not diverge: ${JSON.stringify(retry.conflicts)}`);
691
+ assert.deepEqual(await readAll(root, shardRel(2)), added);
692
+ } finally {
693
+ await rm(root, { recursive: true, force: true });
694
+ }
695
+ });
696
+
697
+ test("manifested shrink records tombstones even when deletes omit mtimeMs", async () => {
698
+ const root = await tempDir("remnic-3150-shrink-nomtime");
699
+ try {
700
+ const local = await seedLocalGeneration(root, 3);
701
+ const changeset = baseChangeset(
702
+ [deleteChange(shardRel(2), sha256(local.get(shardRel(2))!))],
703
+ [{ shardDir: "state/embeddings", members: [
704
+ member(shardRel(0), local.get(shardRel(0))!),
705
+ member(shardRel(1), local.get(shardRel(1))!),
706
+ ] }],
707
+ );
708
+ const { io, calls } = makeIo(root);
709
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
710
+ assert.equal(result.conflicts.length, 0, `shrink must apply: ${JSON.stringify(result.conflicts)}`);
711
+ assert.deepEqual(calls.deletes, [shardRel(2)], "swept shard must be tombstoned even without a supplied mtime");
712
+ } finally {
713
+ await rm(root, { recursive: true, force: true });
714
+ }
715
+ });
716
+
717
+ test("marker-only conversion consumes the marker change so the per-file loop never rewrites it", async () => {
718
+ const root = await tempDir("remnic-3150-marker-handled");
719
+ try {
720
+ await seedLocalGeneration(root, 2);
721
+ const marker = Buffer.from(JSON.stringify({
722
+ version: 1,
723
+ provider: "openai",
724
+ model: "text-embedding-3-small",
725
+ entries: { [SHARD_IDS[2]]: { path: `memories/${SHARD_IDS[2]}.md`, vector: [1, 2] } },
726
+ } satisfies EmbeddingIndexFile), "utf-8");
727
+ const changeset = baseChangeset([upsertChange("state/embeddings.json", marker, 100)]);
728
+ const { io, calls } = makeIo(root);
729
+ const result = await applyOfflineSyncChangeset({ root, changeset, returnCurrentFiles: false, ...io });
730
+ assert.equal(result.conflicts.length, 0, `marker conversion must apply: ${JSON.stringify(result.conflicts)}`);
731
+ const names = await shardNames(root);
732
+ assert.ok(names.includes("shard-0002.json"), `marker entry must convert into its shard: ${JSON.stringify(names)}`);
733
+ await assert.rejects(
734
+ readAll(root, "state/embeddings.json"),
735
+ /ENOENT/,
736
+ "the marker change is consumed by the conversion — the per-file loop must not rewrite the legacy marker",
737
+ );
738
+ assert.deepEqual(calls.plainWrites, [], "the marker must never take the per-file write path");
739
+ } finally {
740
+ await rm(root, { recursive: true, force: true });
741
+ }
742
+ });
743
+
744
+ // ---------------------------------------------------------------------------
745
+ // Sender — complete manifests, whole-generation omission, validation
746
+ // ---------------------------------------------------------------------------
747
+
748
+ test("manifest builder emits complete post-change membership and omits filtered generations", async () => {
749
+ const root = await tempDir("remnic-3150-manifest");
750
+ try {
751
+ const base = await seedLocalGeneration(root, 3);
752
+ const new0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [9, 9] } });
753
+ const currentFiles: OfflineSyncFileState[] = [
754
+ { path: shardRel(0), sha256: sha256(new0), bytes: new0.length, mtimeMs: 100 },
755
+ { path: shardRel(1), sha256: sha256(base.get(shardRel(1))!), bytes: base.get(shardRel(1))!.length, mtimeMs: 1 },
756
+ { path: shardRel(2), sha256: sha256(base.get(shardRel(2))!), bytes: base.get(shardRel(2))!.length, mtimeMs: 1 },
757
+ ];
758
+ const baseFiles: OfflineSyncFileState[] = (await Promise.all(
759
+ [0, 1, 2].map(async (i) => ({
760
+ path: shardRel(i),
761
+ sha256: sha256(base.get(shardRel(i))!),
762
+ bytes: base.get(shardRel(i))!.length,
763
+ mtimeMs: 1,
764
+ })),
765
+ ));
766
+ const changes = [
767
+ upsertChange(shardRel(0), new0, 100, sha256(base.get(shardRel(0))!)),
768
+ upsertChange("notes/a.md", Buffer.from("note"), 1),
769
+ ];
770
+ const generations = buildChangesetGenerationManifest({
771
+ changes,
772
+ currentFiles,
773
+ baseFiles,
774
+ isExcluded: () => false,
775
+ });
776
+ assert.deepEqual(generations, [{
777
+ shardDir: "state/embeddings",
778
+ members: [
779
+ member(shardRel(0), new0),
780
+ member(shardRel(1), base.get(shardRel(1))!),
781
+ member(shardRel(2), base.get(shardRel(2))!),
782
+ ],
783
+ }]);
784
+ // Filter-to-omit: one excluded member omits the whole generation.
785
+ const omitted = buildChangesetGenerationManifest({
786
+ changes,
787
+ currentFiles,
788
+ baseFiles,
789
+ isExcluded: (relPath) => relPath === shardRel(2),
790
+ });
791
+ assert.deepEqual(omitted, [], "a partially filtered generation must not travel");
792
+ } finally {
793
+ await rm(root, { recursive: true, force: true });
794
+ }
795
+ });
796
+
797
+ test("the CLI changeset builder still push-filters runtime state (#1786)", async () => {
798
+ const root = await tempDir("remnic-3150-builder-filter");
799
+ try {
800
+ const base = await seedLocalGeneration(root, 2);
801
+ const new0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [9, 9] } });
802
+ await write(root, shardRel(0), new0);
803
+ const changeset = await buildOfflineSyncChangesetFromSnapshot({
804
+ root,
805
+ sourceId: "sender",
806
+ currentFiles: [
807
+ { path: shardRel(0), sha256: sha256(new0), bytes: new0.length, mtimeMs: 100 },
808
+ { path: shardRel(1), sha256: sha256(base.get(shardRel(1))!), bytes: base.get(shardRel(1))!.length, mtimeMs: 1 },
809
+ ],
810
+ baseFiles: [
811
+ { path: shardRel(0), sha256: sha256(base.get(shardRel(0))!), bytes: base.get(shardRel(0))!.length, mtimeMs: 1 },
812
+ { path: shardRel(1), sha256: sha256(base.get(shardRel(1))!), bytes: base.get(shardRel(1))!.length, mtimeMs: 1 },
813
+ ],
814
+ readFile: async (target) => readAll(root, target.path),
815
+ });
816
+ // Node pushes are pull-restored for runtime state: shards never ride the
817
+ // inline changeset from this builder, so it carries no generation
818
+ // protocol fields at all (no unnecessary sender logic).
819
+ assert.deepEqual(changeset.changes, []);
820
+ assert.equal(changeset.embeddingGenerations, undefined);
821
+ } finally {
822
+ await rm(root, { recursive: true, force: true });
823
+ }
824
+ });
825
+
826
+ test("the CLI changeset builder never announces a partially filtered generation", async () => {
827
+ const root = await tempDir("remnic-3150-filtered-builder");
828
+ try {
829
+ const base = await seedLocalGeneration(root, 3);
830
+ const new0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [9, 9] } });
831
+ await write(root, shardRel(0), new0);
832
+ // Guard-rail for the builder itself: even if the #1786 runtime filter
833
+ // ever lifts, the builder must not emit changes for a generation whose
834
+ // membership the push filters would split (filter-to-omit, #3148).
835
+ const generations = buildChangesetGenerationManifest({
836
+ changes: [upsertChange(shardRel(0), new0, 100, sha256(base.get(shardRel(0))!))],
837
+ currentFiles: [
838
+ { path: shardRel(0), sha256: sha256(new0), bytes: new0.length, mtimeMs: 100 },
839
+ { path: shardRel(1), sha256: sha256(base.get(shardRel(1))!), bytes: base.get(shardRel(1))!.length, mtimeMs: 1 },
840
+ { path: shardRel(2), sha256: sha256(base.get(shardRel(2))!), bytes: base.get(shardRel(2))!.length, mtimeMs: 1 },
841
+ ],
842
+ baseFiles: [
843
+ { path: shardRel(1), sha256: sha256(base.get(shardRel(1))!), bytes: base.get(shardRel(1))!.length, mtimeMs: 1 },
844
+ ],
845
+ isExcluded: (relPath) => relPath === shardRel(2),
846
+ });
847
+ assert.deepEqual(generations, []);
848
+ } finally {
849
+ await rm(root, { recursive: true, force: true });
850
+ }
851
+ });
852
+
853
+ test("a partially filtered generation is omitted whole from the changeset", async () => {
854
+ const root = await tempDir("remnic-3150-filtered");
855
+ try {
856
+ const base = await seedLocalGeneration(root, 3);
857
+ const new0 = shardDoc({ [SHARD_IDS[0]]: { path: `memories/${SHARD_IDS[0]}.md`, vector: [9, 9] } });
858
+ await write(root, shardRel(0), new0);
859
+ const changeset = await buildOfflineSyncChangesetFromSnapshot({
860
+ root,
861
+ sourceId: "sender",
862
+ currentFiles: [
863
+ { path: shardRel(0), sha256: sha256(new0), bytes: new0.length, mtimeMs: 100 },
864
+ { path: shardRel(1), sha256: sha256(base.get(shardRel(1))!), bytes: base.get(shardRel(1))!.length, mtimeMs: 1 },
865
+ { path: shardRel(2), sha256: sha256(base.get(shardRel(2))!), bytes: base.get(shardRel(2))!.length, mtimeMs: 1 },
866
+ ],
867
+ baseFiles: [
868
+ { path: shardRel(0), sha256: sha256(base.get(shardRel(0))!), bytes: base.get(shardRel(0))!.length, mtimeMs: 1 },
869
+ { path: shardRel(1), sha256: sha256(base.get(shardRel(1))!), bytes: base.get(shardRel(1))!.length, mtimeMs: 1 },
870
+ { path: shardRel(2), sha256: sha256(base.get(shardRel(2))!), bytes: base.get(shardRel(2))!.length, mtimeMs: 1 },
871
+ ],
872
+ userExcludeRegexps: [/shard-0002\.json$/],
873
+ readFile: async (target) => readAll(root, target.path),
874
+ });
875
+ assert.deepEqual(
876
+ changeset.changes.filter((change) => change.path.startsWith("state/embeddings")),
877
+ [],
878
+ "no member of a partially filtered generation may travel",
879
+ );
880
+ assert.equal(changeset.embeddingGenerations, undefined);
881
+ } finally {
882
+ await rm(root, { recursive: true, force: true });
883
+ }
884
+ });
885
+
886
+ test("normalize rejects incoherent manifests before any apply", async () => {
887
+ const local = shardDoc({ [SHARD_IDS[0]]: { path: "memories/m8.md", vector: [1, 1] } });
888
+ const changed = shardDoc({ [SHARD_IDS[0]]: { path: "memories/m8.md", vector: [2, 2] } });
889
+ const upsert = upsertChange(shardRel(0), changed, 1, sha256(local));
890
+ const generation = (members: ReturnType<typeof member>[]) =>
891
+ baseChangeset([upsert], [{ shardDir: "state/embeddings", members }]);
892
+
893
+ // Duplicate member paths.
894
+ assert.throws(
895
+ () => normalizeOfflineSyncChangeset(generation([member(shardRel(0), changed), member(shardRel(0), changed)])),
896
+ /duplicate/,
897
+ );
898
+ // Noncanonical dir.
899
+ assert.throws(
900
+ () => normalizeOfflineSyncChangeset(baseChangeset([upsert], [{ shardDir: "state/other", members: [member(shardRel(0), changed)] }])),
901
+ /canonical|embedding/,
902
+ );
903
+ // Member outside its dir.
904
+ assert.throws(
905
+ () => normalizeOfflineSyncChangeset(generation([member("state/other/shard-0000.json", changed)])),
906
+ /member/,
907
+ );
908
+ // Upserted shard missing from the manifest (protocol violation for manifest-bearing senders).
909
+ assert.throws(
910
+ () => normalizeOfflineSyncChangeset(generation([member(shardRel(1), changed)])),
911
+ /manifest/,
912
+ );
913
+ // Legacy changesets (field absent) keep normalizing.
914
+ const legacy = baseChangeset([upsert]) as OfflineSyncChangeset & { embeddingGenerations?: unknown };
915
+ assert.equal(legacy.embeddingGenerations, undefined);
916
+ assert.equal(normalizeOfflineSyncChangeset(baseChangeset([upsert])).changes.length, 1);
917
+ });
918
+
919
+ // ---------------------------------------------------------------------------
920
+ // Integration — sender changeset reproduces the receiver's generation exactly
921
+ // ---------------------------------------------------------------------------
922
+
923
+ test("end-to-end: receiver census equals sender census after a manifested changeset", async () => {
924
+ const senderRoot = await tempDir("remnic-3150-e2e-sender");
925
+ const receiverRoot = await tempDir("remnic-3150-e2e-receiver");
926
+ try {
927
+ const base = await seedLocalGeneration(senderRoot, 4);
928
+ await seedLocalGeneration(receiverRoot, 4);
929
+ const new1 = shardDoc({ [SHARD_IDS[1]]: { path: `memories/${SHARD_IDS[1]}.md`, vector: [8, 8] } });
930
+ await write(senderRoot, shardRel(1), new1);
931
+ await rm(path.join(senderRoot, shardRel(3)), { force: true }); // shrink 4 → 3
932
+
933
+ const currentFiles: OfflineSyncFileState[] = [
934
+ { path: shardRel(0), sha256: sha256(base.get(shardRel(0))!), bytes: base.get(shardRel(0))!.length, mtimeMs: 1 },
935
+ { path: shardRel(1), sha256: sha256(new1), bytes: new1.length, mtimeMs: 100 },
936
+ { path: shardRel(2), sha256: sha256(base.get(shardRel(2))!), bytes: base.get(shardRel(2))!.length, mtimeMs: 1 },
937
+ ];
938
+ const baseFiles: OfflineSyncFileState[] = [0, 1, 2, 3].map((i) => ({
939
+ path: shardRel(i),
940
+ sha256: sha256(base.get(shardRel(i))!),
941
+ bytes: base.get(shardRel(i))!.length,
942
+ mtimeMs: 1,
943
+ }));
944
+ const changes = [
945
+ upsertChange(shardRel(1), new1, 100, sha256(base.get(shardRel(1))!)),
946
+ deleteChange(shardRel(3), sha256(base.get(shardRel(3))!)),
947
+ ];
948
+ const changeset = baseChangeset(changes, buildChangesetGenerationManifest({
949
+ changes,
950
+ currentFiles,
951
+ baseFiles,
952
+ isExcluded: () => false,
953
+ }));
954
+ const { io } = makeIo(receiverRoot);
955
+ const result = await applyOfflineSyncChangeset({ root: receiverRoot, changeset, returnCurrentFiles: false, ...io });
956
+ assert.equal(result.conflicts.length, 0, `no conflicts expected: ${JSON.stringify(result.conflicts)}`);
957
+ assert.deepEqual(await diskGeneration(receiverRoot), await diskGeneration(senderRoot));
958
+ } finally {
959
+ await rm(senderRoot, { recursive: true, force: true });
960
+ await rm(receiverRoot, { recursive: true, force: true });
961
+ }
962
+ });