@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,2173 @@
1
+ // Round-4 regression suite for PR #3148 (issue #3146 embedding index).
2
+ // Covers the six review findings:
3
+ // 1. incoming legacy generation replaces local shards (cross-layout);
4
+ // 2. entry-shape validation at the read boundary (fail-closed, tagged);
5
+ // 3. unchanged-base shortcut requires the shared base to be supplied;
6
+ // 4. a deferral anywhere in a generation defers the WHOLE generation;
7
+ // 5. filtered-out generations are omitted (files + deletion metadata),
8
+ // never interpreted as deleted by the receiver;
9
+ // 6. atomic staged generation replacement with crash recovery, and
10
+ // secure-store staged ciphertext readable at the canonical path.
11
+ import assert from "node:assert/strict";
12
+ import { createHash, randomBytes } from "node:crypto";
13
+ import { mkdir, mkdtemp, readFile, readdir, rename, rm, symlink, writeFile } from "node:fs/promises";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ import { createServer } from "node:http";
17
+ import test from "node:test";
18
+
19
+ import { EmbeddingFallback } from "./embedding-fallback.js";
20
+ import {
21
+ EmbeddingIndexCapacityError,
22
+ type EmbeddingIndexEntry,
23
+ type EmbeddingIndexFile,
24
+ EmbeddingIndexFileStore,
25
+ EmbeddingIndexStorageError,
26
+ parseEmbeddingIndexDocument,
27
+ serializeEmbeddingShard,
28
+ shardEntriesForIndex,
29
+ shardIndexOf,
30
+ } from "./embedding-index-storage.js";
31
+ import {
32
+ applyEmbeddingGenerationRemoval,
33
+ applyEmbeddingGenerationTransaction,
34
+ computeOmittedEmbeddingGenerationPaths,
35
+ embeddingGenerationMembership,
36
+ } from "./offline-sync-embedding-generation.js";
37
+ import { withEmbeddingGenerationLock } from "./embedding-generation-lock.js";
38
+ import { respondOfflineSnapshotStream } from "./access-http-offline-stream.js";
39
+ import {
40
+ OFFLINE_SYNC_SNAPSHOT_FORMAT,
41
+ type OfflineSyncSnapshot,
42
+ applyOfflineSyncSnapshot,
43
+ buildOfflineSyncSnapshot,
44
+ buildOfflineSyncSnapshotFromBase,
45
+ iterateOfflineSyncSnapshotFileRecords,
46
+ normalizeOfflineSyncSnapshot,
47
+ } from "./offline-sync.js";
48
+ import { MAGIC_BYTES, readMaybeEncryptedFile } from "./secure-store/secure-fs.js";
49
+ import { gatherConsoleState } from "./console/state.js";
50
+ import { storageBackedIndexStoreIo } from "./offline-sync-embedding-generation.js";
51
+ import { StorageManager } from "./storage.js";
52
+ import { type SafeArchiveRoot, prepareSafeArchiveRoot } from "./transfer/fs-utils.js";
53
+
54
+ async function tempDir(name: string): Promise<string> {
55
+ return mkdtemp(path.join(os.tmpdir(), `${name}-`));
56
+ }
57
+
58
+ async function write(root: string, relPath: string, content: string | Buffer): Promise<void> {
59
+ const filePath = path.join(root, relPath);
60
+ await mkdir(path.dirname(filePath), { recursive: true });
61
+ await writeFile(filePath, content);
62
+ }
63
+
64
+ async function readUtf8(root: string, relPath: string): Promise<string> {
65
+ return readFile(path.join(root, relPath), "utf-8");
66
+ }
67
+
68
+ function trueShardRel(id: string): string {
69
+ return `state/embeddings/shard-${String(shardIndexOf(id, 64)).padStart(4, "0")}.json`;
70
+ }
71
+
72
+ function indexFile(provider: string, model: string, entries: Record<string, EmbeddingIndexEntry>): EmbeddingIndexFile {
73
+ return { version: 1, provider: provider as EmbeddingIndexFile["provider"], model, entries };
74
+ }
75
+
76
+ function serializeIndex(index: EmbeddingIndexFile): string {
77
+ return JSON.stringify(index);
78
+ }
79
+
80
+ /** Collect every entry across the published shard generation. */
81
+ async function readGenerationEntries(
82
+ root: string,
83
+ shardDirRel = "state/embeddings"
84
+ ): Promise<Record<string, EmbeddingIndexEntry>> {
85
+ const merged: Record<string, EmbeddingIndexEntry> = {};
86
+ const names = await readdir(path.join(root, ...shardDirRel.split("/")));
87
+ for (const name of names.filter((n) => /^shard-\d{4}\.json$/.test(n)).sort()) {
88
+ const read = parseEmbeddingIndexDocument(await readUtf8(root, `${shardDirRel}/${name}`));
89
+ assert.equal(read.outcome, "ok", `${name} must parse as a valid shard: ${JSON.stringify(read)}`);
90
+ if (read.outcome === "ok") Object.assign(merged, read.file.entries);
91
+ }
92
+ return merged;
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Finding 1 — incoming legacy generation replaces local shards
97
+ // ---------------------------------------------------------------------------
98
+
99
+ test("incoming legacy index replaces a local sharded generation as a whole", async () => {
100
+ const localRoot = await tempDir("remnic-3148-legacy-over-shards");
101
+ const remoteRoot = await tempDir("remnic-3148-legacy-remote");
102
+ try {
103
+ // Local sharded generation (older identity).
104
+ await write(
105
+ localRoot,
106
+ "state/embeddings/shard-0000.json",
107
+ serializeIndex(
108
+ indexFile("openai", "text-embedding-3-small", {
109
+ local1: { path: "memories/local1.md", vector: [1, 0] },
110
+ })
111
+ )
112
+ );
113
+ await write(
114
+ localRoot,
115
+ "state/embeddings/shard-0050.json",
116
+ serializeIndex(
117
+ indexFile("openai", "text-embedding-3-small", {
118
+ local2: { path: "memories/local2.md", vector: [0, 1] },
119
+ })
120
+ )
121
+ );
122
+ // Remote legacy generation: new identity, new entries only.
123
+ const incoming = indexFile("openai", "text-embedding-3-large", {
124
+ remote1: { path: "memories/remote1.md", vector: [0.5, 0.5] },
125
+ });
126
+ await write(remoteRoot, "facts/a.md", "alpha");
127
+ await write(remoteRoot, "state/embeddings.json", serializeIndex(incoming));
128
+ const snapshot = await buildOfflineSyncSnapshot({
129
+ root: remoteRoot,
130
+ sourceId: "remote",
131
+ includeContent: true,
132
+ });
133
+
134
+ const result = await applyOfflineSyncSnapshot({ root: localRoot, snapshot });
135
+
136
+ // The whole local generation is replaced by the converted incoming one.
137
+ const entries = await readGenerationEntries(localRoot);
138
+ assert.deepEqual(Object.keys(entries).sort(), ["remote1"]);
139
+ assert.deepEqual(entries.remote1?.vector, [0.5, 0.5]);
140
+ // The incoming marker is written as the inert remote-authoritative
141
+ // artifact; the published directory remains the layout marker.
142
+ assert.equal(await readUtf8(localRoot, "state/embeddings.json"), serializeIndex(incoming));
143
+ const store = new EmbeddingIndexFileStore(
144
+ path.join(localRoot, "state/embeddings.json"),
145
+ path.join(localRoot, "state/embeddings"),
146
+ path.join(localRoot, "state/embedding-fallback-status.json")
147
+ );
148
+ assert.equal(await store.detectLayout(), "sharded");
149
+ // Reload finds the incoming generation and a subsequent write works.
150
+ const merged: Record<string, EmbeddingIndexEntry> = {};
151
+ assert.deepEqual(await store.readShardGenerationInto(merged), {
152
+ provider: "openai",
153
+ model: "text-embedding-3-large",
154
+ });
155
+ await store.persist(
156
+ indexFile("openai", "text-embedding-3-large", {
157
+ ...merged,
158
+ remote2: { path: "memories/remote2.md", vector: [1, 1] },
159
+ }),
160
+ { touchedIds: ["remote2"] }
161
+ );
162
+ const after = await readGenerationEntries(localRoot);
163
+ assert.ok(after.remote1 && after.remote2);
164
+ assert.equal(result.upserted >= 1, true);
165
+ } finally {
166
+ await rm(localRoot, { recursive: true, force: true });
167
+ await rm(remoteRoot, { recursive: true, force: true });
168
+ }
169
+ });
170
+
171
+ test("sharded incoming generation over a local legacy marker removes the marker", async () => {
172
+ const localRoot = await tempDir("remnic-3148-shards-over-legacy");
173
+ const remoteRoot = await tempDir("remnic-3148-shards-remote");
174
+ try {
175
+ await write(
176
+ localRoot,
177
+ "state/embeddings.json",
178
+ serializeIndex(
179
+ indexFile("openai", "old-model", {
180
+ stale: { path: "memories/stale.md", vector: [9, 9] },
181
+ })
182
+ )
183
+ );
184
+ await write(
185
+ remoteRoot,
186
+ trueShardRel("fresh"),
187
+ serializeIndex(
188
+ indexFile("openai", "new-model", {
189
+ fresh: { path: "memories/fresh.md", vector: [1, 2] },
190
+ })
191
+ )
192
+ );
193
+ const snapshot = await buildOfflineSyncSnapshot({
194
+ root: remoteRoot,
195
+ sourceId: "remote",
196
+ includeContent: true,
197
+ });
198
+ await applyOfflineSyncSnapshot({ root: localRoot, snapshot });
199
+ // Legacy generation removed wholesale; incoming generation published.
200
+ assert.equal(await existsQuiet(localRoot, "state/embeddings.json"), false);
201
+ const entries = await readGenerationEntries(localRoot);
202
+ assert.deepEqual(Object.keys(entries), ["fresh"]);
203
+ } finally {
204
+ await rm(localRoot, { recursive: true, force: true });
205
+ await rm(remoteRoot, { recursive: true, force: true });
206
+ }
207
+ });
208
+
209
+ async function existsQuiet(root: string, relPath: string): Promise<boolean> {
210
+ try {
211
+ await readFile(path.join(root, relPath));
212
+ return true;
213
+ } catch {
214
+ return false;
215
+ }
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Finding 2 — entry-shape validation at the read boundary
220
+ // ---------------------------------------------------------------------------
221
+
222
+ test("parseEmbeddingIndexDocument rejects malformed entry shapes as unreadable", () => {
223
+ const base = { version: 1 as const, provider: "openai", model: "m" };
224
+ // entries is an array, not a plain record
225
+ const arrayEntries = parseEmbeddingIndexDocument(JSON.stringify({ ...base, entries: [] }));
226
+ assert.equal(arrayEntries.outcome, "unreadable");
227
+ assert.match(arrayEntries.outcome === "unreadable" ? arrayEntries.reason : "", /entries must be a plain record/);
228
+ // entries null
229
+ assert.equal(parseEmbeddingIndexDocument(JSON.stringify({ ...base, entries: null })).outcome, "unreadable");
230
+ // entry value is not an object
231
+ const scalarEntry = parseEmbeddingIndexDocument(JSON.stringify({ ...base, entries: { id1: "nope" } }));
232
+ assert.equal(scalarEntry.outcome, "unreadable");
233
+ // entry missing string path
234
+ const badPath = parseEmbeddingIndexDocument(JSON.stringify({ ...base, entries: { id1: { vector: [1] } } }));
235
+ assert.match(badPath.outcome === "unreadable" ? badPath.reason : "", /path must be a string/);
236
+ // entry vector not an array
237
+ const badVector = parseEmbeddingIndexDocument(
238
+ JSON.stringify({ ...base, entries: { id1: { path: "p", vector: "x" } } })
239
+ );
240
+ assert.match(badVector.outcome === "unreadable" ? badVector.reason : "", /vector must be an array/);
241
+ // non-finite vector component
242
+ const nonFinite = parseEmbeddingIndexDocument(
243
+ JSON.stringify({ ...base, entries: { id1: { path: "p", vector: [1, "x"] } } })
244
+ );
245
+ assert.match(nonFinite.outcome === "unreadable" ? nonFinite.reason : "", /finite/);
246
+ // a valid document still parses, and a foreign header stays foreign
247
+ assert.equal(
248
+ parseEmbeddingIndexDocument(JSON.stringify({ ...base, entries: { id1: { path: "p", vector: [1, 2] } } })).outcome,
249
+ "ok"
250
+ );
251
+ assert.equal(
252
+ parseEmbeddingIndexDocument(JSON.stringify({ version: 2, provider: "openai", model: "m", entries: {} })).outcome,
253
+ "foreign"
254
+ );
255
+ });
256
+
257
+ test("corrupt shard files stay in place and are tagged unreadable for the store", async () => {
258
+ const root = await tempDir("remnic-3148-corrupt-shard");
259
+ try {
260
+ const corrupt = JSON.stringify({
261
+ version: 1,
262
+ provider: "openai",
263
+ model: "m",
264
+ entries: [{ id: "oops" }],
265
+ });
266
+ await write(root, "state/embeddings.json", "{}");
267
+ await write(root, "state/embeddings/shard-0000.json", corrupt);
268
+ const store = new EmbeddingIndexFileStore(
269
+ path.join(root, "state/embeddings.json"),
270
+ path.join(root, "state/embeddings"),
271
+ path.join(root, "state/embedding-fallback-status.json")
272
+ );
273
+ const read = await store.readFileAt(path.join(root, "state/embeddings/shard-0000.json"));
274
+ assert.equal(read.outcome, "unreadable");
275
+ // Bytes preserved: the corrupt file is not rewritten or deleted.
276
+ assert.equal(await readUtf8(root, "state/embeddings/shard-0000.json"), corrupt);
277
+ // Strict mutation path fails closed with the tagged error.
278
+ await assert.rejects(
279
+ () => store.readShardGenerationInto({}),
280
+ (err: unknown) => err instanceof EmbeddingIndexStorageError
281
+ );
282
+ } finally {
283
+ await rm(root, { recursive: true, force: true });
284
+ }
285
+ });
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // Finding 4 — a deferral anywhere defers the WHOLE generation
289
+ // ---------------------------------------------------------------------------
290
+
291
+ test("a deferred incoming shard defers the whole incoming generation", async () => {
292
+ const localRoot = await tempDir("remnic-3148-defer-incoming");
293
+ const remoteRoot = await tempDir("remnic-3148-defer-remote");
294
+ try {
295
+ await write(
296
+ localRoot,
297
+ trueShardRel("neu"),
298
+ serializeIndex(indexFile("openai", "m1", { old: { path: "p", vector: [0] } }))
299
+ );
300
+ await write(localRoot, "state/embeddings/shard-0050.json", "local only 0050");
301
+ await write(
302
+ remoteRoot,
303
+ trueShardRel("neu"),
304
+ serializeIndex(indexFile("openai", "m2", { neu: { path: "p", vector: [1] } }))
305
+ );
306
+ await write(
307
+ remoteRoot,
308
+ trueShardRel("neu2"),
309
+ serializeIndex(indexFile("openai", "m2", { neu2: { path: "p2", vector: [2] } }))
310
+ );
311
+ const snapshot = await buildOfflineSyncSnapshot({
312
+ root: remoteRoot,
313
+ sourceId: "remote",
314
+ includeContent: true,
315
+ });
316
+ const result = await applyOfflineSyncSnapshot({
317
+ root: localRoot,
318
+ snapshot,
319
+ deferredPaths: [trueShardRel("neu2")],
320
+ });
321
+ assert.ok((await readUtf8(localRoot, trueShardRel("neu"))).includes('"m1"'));
322
+ assert.equal(await readUtf8(localRoot, "state/embeddings/shard-0050.json"), "local only 0050");
323
+ assert.equal(await existsQuiet(localRoot, trueShardRel("neu2")), false);
324
+ assert.equal(result.deleted, 0);
325
+ assert.equal(result.pendingLocal, 0);
326
+ assert.ok(result.skipped >= 3);
327
+ } finally {
328
+ await rm(localRoot, { recursive: true, force: true });
329
+ await rm(remoteRoot, { recursive: true, force: true });
330
+ }
331
+ });
332
+
333
+ test("a deferred legacy marker defers the whole cross-layout replacement", async () => {
334
+ const localRoot = await tempDir("remnic-3148-defer-marker");
335
+ const remoteRoot = await tempDir("remnic-3148-defer-marker-remote");
336
+ try {
337
+ await write(localRoot, "state/embeddings/shard-0000.json", "local shard");
338
+ const incoming = indexFile("openai", "m2", {
339
+ remote1: { path: "p", vector: [1] },
340
+ });
341
+ await write(remoteRoot, "state/embeddings.json", serializeIndex(incoming));
342
+ const snapshot = await buildOfflineSyncSnapshot({
343
+ root: remoteRoot,
344
+ sourceId: "remote",
345
+ includeContent: true,
346
+ });
347
+ const result = await applyOfflineSyncSnapshot({
348
+ root: localRoot,
349
+ snapshot,
350
+ deferredPaths: ["state/embeddings.json"],
351
+ });
352
+ assert.equal(await readUtf8(localRoot, "state/embeddings/shard-0000.json"), "local shard");
353
+ assert.equal(await existsQuiet(localRoot, "state/embeddings.json"), false);
354
+ assert.equal(await existsQuiet(localRoot, "state/embeddings/shard-0063.json"), false);
355
+ assert.equal(result.deleted, 0);
356
+ } finally {
357
+ await rm(localRoot, { recursive: true, force: true });
358
+ await rm(remoteRoot, { recursive: true, force: true });
359
+ }
360
+ });
361
+
362
+ // ---------------------------------------------------------------------------
363
+ // Finding 5 — filtered generations are omitted, never deleted
364
+ // ---------------------------------------------------------------------------
365
+
366
+ test("a user exclude on one shard omits the whole generation from files and deletions", async () => {
367
+ const root = await tempDir("remnic-3148-omission-build");
368
+ try {
369
+ await write(root, "facts/a.md", "alpha");
370
+ await write(root, "state/embeddings.json", serializeIndex(indexFile("openai", "m", {})));
371
+ await write(root, "state/embeddings/shard-0000.json", "shard 0000");
372
+ await write(root, "state/embeddings/shard-0003.json", "shard 0003");
373
+ const snapshot = await buildOfflineSyncSnapshot({
374
+ root,
375
+ sourceId: "source",
376
+ includeContent: true,
377
+ userExcludeRegexps: [/^state\/embeddings\/shard-0003\.json$/],
378
+ deletions: [{ path: "state/embeddings/shard-0005.json", mtimeMs: 1_000 }],
379
+ });
380
+ const paths = snapshot.files.map((file) => file.path);
381
+ assert.deepEqual(paths, ["facts/a.md"]);
382
+ assert.deepEqual(snapshot.omittedEmbeddingGenerationDirs, ["state/embeddings"]);
383
+ // The sibling shard's deletion revision is suppressed with the generation.
384
+ assert.deepEqual(snapshot.deletions, []);
385
+ } finally {
386
+ await rm(root, { recursive: true, force: true });
387
+ }
388
+ });
389
+
390
+ test("the excludeFile callback omission applies to the shared iterator and base builder", async () => {
391
+ const root = await tempDir("remnic-3148-omission-exclude-file");
392
+ try {
393
+ await write(root, "facts/a.md", "alpha");
394
+ await write(root, "state/embeddings/shard-0000.json", "shard 0000");
395
+ await write(root, "state/embeddings/shard-0003.json", "shard 0003");
396
+ const excludeFile = (target: { path: string }) => target.path.endsWith("shard-0003.json");
397
+ const yielded: string[] = [];
398
+ for await (const record of iterateOfflineSyncSnapshotFileRecords({
399
+ root,
400
+ excludeFile,
401
+ })) {
402
+ yielded.push(record.path);
403
+ }
404
+ assert.deepEqual(yielded, ["facts/a.md"]);
405
+ const fromBase = await buildOfflineSyncSnapshotFromBase({
406
+ root,
407
+ sourceId: "source",
408
+ excludeFile,
409
+ });
410
+ assert.deepEqual(
411
+ fromBase.files.map((file) => file.path),
412
+ ["facts/a.md"]
413
+ );
414
+ assert.deepEqual(fromBase.omittedEmbeddingGenerationDirs, ["state/embeddings"]);
415
+ } finally {
416
+ await rm(root, { recursive: true, force: true });
417
+ }
418
+ });
419
+
420
+ test("a receiver keeps an omitted generation instead of deleting it", async () => {
421
+ const localRoot = await tempDir("remnic-3148-omission-apply");
422
+ try {
423
+ await write(localRoot, "facts/incoming.md", "incoming");
424
+ await write(localRoot, "state/embeddings/shard-0000.json", "local 0000");
425
+ await write(localRoot, "state/embeddings/shard-0001.json", "local 0001");
426
+ const baseFiles = [
427
+ { path: "state/embeddings/shard-0000.json", sha256: sha256Of("local 0000"), bytes: Buffer.byteLength("local 0000"), mtimeMs: 1 },
428
+ { path: "state/embeddings/shard-0001.json", sha256: sha256Of("local 0001"), bytes: Buffer.byteLength("local 0001"), mtimeMs: 1 },
429
+ ];
430
+ const snapshot: OfflineSyncSnapshot = {
431
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
432
+ schemaVersion: 1,
433
+ createdAt: "2026-09-27T00:00:00.000Z",
434
+ sourceId: "remote",
435
+ includeTranscripts: true,
436
+ files: [
437
+ {
438
+ path: "facts/incoming.md",
439
+ sha256: sha256Of("incoming"),
440
+ bytes: "incoming".length,
441
+ mtimeMs: 1,
442
+ contentBase64: Buffer.from("incoming").toString("base64"),
443
+ },
444
+ ],
445
+ deletions: [],
446
+ omittedEmbeddingGenerationDirs: ["state/embeddings"],
447
+ };
448
+ const result = await applyOfflineSyncSnapshot({ root: localRoot, snapshot, baseFiles });
449
+ // Generation kept byte-for-byte; absence never became a delete.
450
+ assert.equal(await readUtf8(localRoot, "state/embeddings/shard-0000.json"), "local 0000");
451
+ assert.equal(await readUtf8(localRoot, "state/embeddings/shard-0001.json"), "local 0001");
452
+ assert.equal(result.deleted, 0);
453
+ assert.equal(result.pendingLocal >= 2, true);
454
+ assert.ok(result.nextBaseFiles.some((f) => f.path === "state/embeddings/shard-0000.json"));
455
+ } finally {
456
+ await rm(localRoot, { recursive: true, force: true });
457
+ }
458
+ });
459
+
460
+ test("an absent generation WITHOUT the omission marker still converges by deletion", async () => {
461
+ const localRoot = await tempDir("remnic-3148-absent-delete");
462
+ try {
463
+ await write(localRoot, "state/embeddings/shard-0000.json", "local 0000");
464
+ const baseFiles = [{ path: "state/embeddings/shard-0000.json", sha256: sha256Of("local 0000"), bytes: Buffer.byteLength("local 0000"), mtimeMs: 1 }];
465
+ const snapshot: OfflineSyncSnapshot = {
466
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
467
+ schemaVersion: 1,
468
+ createdAt: "2026-09-27T00:00:00.000Z",
469
+ sourceId: "remote",
470
+ includeTranscripts: true,
471
+ files: [],
472
+ };
473
+ const result = await applyOfflineSyncSnapshot({ root: localRoot, snapshot, baseFiles });
474
+ assert.equal(await existsQuiet(localRoot, "state/embeddings/shard-0000.json"), false);
475
+ assert.equal(result.deleted, 1);
476
+ } finally {
477
+ await rm(localRoot, { recursive: true, force: true });
478
+ }
479
+ });
480
+
481
+ function sha256Of(content: string): string {
482
+ return createHash("sha256").update(content).digest("hex");
483
+ }
484
+
485
+ // ---------------------------------------------------------------------------
486
+ // Finding 6 — atomic staged replacement, failure and crash recovery
487
+ // ---------------------------------------------------------------------------
488
+
489
+ test("a staging write failure preserves the local generation", async () => {
490
+ const localRoot = await tempDir("remnic-3148-stage-failure");
491
+ try {
492
+ await write(localRoot, trueShardRel("neu"), "local 0000");
493
+ await write(localRoot, "state/embeddings/shard-0050.json", "local 0050");
494
+ const incomingBuffers = new Map<string, Buffer>();
495
+ const safeRoot: SafeArchiveRoot = await prepareSafeArchiveRoot(localRoot, "test", "root");
496
+ incomingBuffers.set(
497
+ trueShardRel("neu"),
498
+ Buffer.from(
499
+ serializeIndex(
500
+ indexFile("openai", "m2", {
501
+ neu: { path: "p", vector: [1] },
502
+ })
503
+ )
504
+ )
505
+ );
506
+ await assert.rejects(
507
+ () =>
508
+ applyEmbeddingGenerationTransaction({
509
+ root: safeRoot,
510
+ shardDirRel: "state/embeddings",
511
+ incomingShardPaths: [trueShardRel("neu")],
512
+ incomingMarker: null,
513
+ incomingMarkerPresent: false,
514
+ incomingShardStates: new Map(),
515
+ incomingBuffers,
516
+ io: {
517
+ writeStagingFile: async () => {
518
+ throw new Error("disk full during stage");
519
+ },
520
+ },
521
+ now: 1,
522
+ }),
523
+ /disk full during stage/
524
+ );
525
+ // Local generation byte-identical, no staging leftovers.
526
+ assert.equal(await readUtf8(localRoot, trueShardRel("neu")), "local 0000");
527
+ assert.equal(await readUtf8(localRoot, "state/embeddings/shard-0050.json"), "local 0050");
528
+ const stateDir = await readdir(path.join(localRoot, "state"));
529
+ assert.equal(
530
+ stateDir.some((name) => name.startsWith("embeddings.staging.tmp-")),
531
+ false
532
+ );
533
+ } finally {
534
+ await rm(localRoot, { recursive: true, force: true });
535
+ }
536
+ });
537
+
538
+ test("a publish rename failure rolls the former generation back into place", async () => {
539
+ const root = await tempDir("remnic-3148-rename-failure");
540
+ try {
541
+ await write(root, "state/embeddings.json", "{}");
542
+ await write(root, "state/embeddings/shard-0000.json", "published 0000");
543
+ const store = new EmbeddingIndexFileStore(
544
+ path.join(root, "state/embeddings.json"),
545
+ path.join(root, "state/embeddings"),
546
+ path.join(root, "state/embedding-fallback-status.json")
547
+ );
548
+ await assert.rejects(
549
+ () => store.publishSwappedGeneration(path.join(root, "state/embeddings.staging.tmp-missing")),
550
+ (err: unknown) => (err as NodeJS.ErrnoException).code === "ENOENT"
551
+ );
552
+ // Rollback restored the former generation; no backup left behind.
553
+ assert.equal(await readUtf8(root, "state/embeddings/shard-0000.json"), "published 0000");
554
+ assert.equal(await existsQuiet(root, "state/embeddings.pre-replace.tmp"), false);
555
+ } finally {
556
+ await rm(root, { recursive: true, force: true });
557
+ }
558
+ });
559
+
560
+ test("a crash in the rename gap is recovered before the next sync replacement", async () => {
561
+ const localRoot = await tempDir("remnic-3148-rename-gap");
562
+ const remoteRoot = await tempDir("remnic-3148-gap-remote");
563
+ try {
564
+ // Simulate the gap: former generation sits in the fixed backup, the
565
+ // published directory is absent, no legacy marker.
566
+ await write(localRoot, "state/embeddings.pre-replace.tmp/shard-0000.json", "former 0000");
567
+ await write(
568
+ remoteRoot,
569
+ trueShardRel("neu"),
570
+ serializeIndex(indexFile("openai", "m2", { neu: { path: "p", vector: [1] } }))
571
+ );
572
+ await write(
573
+ remoteRoot,
574
+ trueShardRel("neu63"),
575
+ serializeIndex(indexFile("openai", "m2", { neu63: { path: "p63", vector: [3] } }))
576
+ );
577
+ const snapshot = await buildOfflineSyncSnapshot({
578
+ root: remoteRoot,
579
+ sourceId: "remote",
580
+ includeContent: true,
581
+ });
582
+ await applyOfflineSyncSnapshot({ root: localRoot, snapshot });
583
+ // The backup was restored first (so detectLayout stayed truthful), then
584
+ // the incoming generation replaced it wholesale.
585
+ assert.equal(await existsQuiet(localRoot, "state/embeddings.pre-replace.tmp"), false);
586
+ assert.equal(
587
+ await readUtf8(localRoot, trueShardRel("neu")),
588
+ serializeIndex(indexFile("openai", "m2", { neu: { path: "p", vector: [1] } }))
589
+ );
590
+ assert.equal(
591
+ await readUtf8(localRoot, trueShardRel("neu63")),
592
+ serializeIndex(indexFile("openai", "m2", { neu63: { path: "p63", vector: [3] } }))
593
+ );
594
+ } finally {
595
+ await rm(localRoot, { recursive: true, force: true });
596
+ await rm(remoteRoot, { recursive: true, force: true });
597
+ }
598
+ });
599
+
600
+ test("a symlinked generation directory is rejected, not followed", async () => {
601
+ const localRoot = await tempDir("remnic-3148-symlink");
602
+ const outside = await tempDir("remnic-3148-symlink-outside");
603
+ const remoteRoot = await tempDir("remnic-3148-symlink-remote");
604
+ try {
605
+ await mkdir(path.join(localRoot, "state"), { recursive: true });
606
+ await symlink(outside, path.join(localRoot, "state/embeddings"));
607
+ await write(outside, "shard-0000.json", "outside bytes");
608
+ await write(remoteRoot, "state/embeddings/shard-0000.json", "incoming 0000");
609
+ const snapshot = await buildOfflineSyncSnapshot({
610
+ root: remoteRoot,
611
+ sourceId: "remote",
612
+ includeContent: true,
613
+ });
614
+ await assert.rejects(() => applyOfflineSyncSnapshot({ root: localRoot, snapshot }));
615
+ // The symlink and its target are untouched.
616
+ assert.equal(await readUtf8(outside, "shard-0000.json"), "outside bytes");
617
+ } finally {
618
+ await rm(localRoot, { recursive: true, force: true });
619
+ await rm(outside, { recursive: true, force: true });
620
+ await rm(remoteRoot, { recursive: true, force: true });
621
+ }
622
+ });
623
+
624
+ test("secure-store staged ciphertext decrypts at the canonical path after publish", async () => {
625
+ const memoryDir = await tempDir("remnic-3148-secure-stage");
626
+ try {
627
+ const storage = new StorageManager(memoryDir);
628
+ const key = randomBytes(32);
629
+ await storage.setSecureStoreKeyAndWait(key, true);
630
+ const stagingRel = "state/embeddings.staging.tmp-sync-test/shard-0000.json";
631
+ const finalRel = "state/embeddings/shard-0000.json";
632
+ const content = Buffer.from(
633
+ serializeIndex(
634
+ indexFile("openai", "m", {
635
+ id1: { path: "p", vector: [1, 2, 3] },
636
+ })
637
+ )
638
+ );
639
+ await storage.writeOfflineSyncStagingFile(path.join(memoryDir, ...stagingRel.split("/")), content, {
640
+ aadFilePath: path.join(memoryDir, ...finalRel.split("/")),
641
+ });
642
+ // Encrypted at rest in the staging location.
643
+ const raw = await readFile(path.join(memoryDir, ...stagingRel.split("/")));
644
+ assert.equal(raw.subarray(0, MAGIC_BYTES.length).toString("ascii"), MAGIC_BYTES.toString("ascii"));
645
+ // Read back through the staging hook with the final-path AAD.
646
+ const staged = await storage.readOfflineSyncFile(path.join(memoryDir, ...stagingRel.split("/")), {
647
+ aadFilePath: path.join(memoryDir, ...finalRel.split("/")),
648
+ });
649
+ assert.deepEqual(staged, content);
650
+ // WITHOUT the final-path AAD the staging ciphertext cannot decrypt.
651
+ await assert.rejects(() => storage.readOfflineSyncFile(path.join(memoryDir, ...stagingRel.split("/"))));
652
+ // After the directory swap the canonical path decrypts normally.
653
+ await mkdir(path.dirname(path.join(memoryDir, ...finalRel.split("/"))), { recursive: true });
654
+ await rename(path.join(memoryDir, ...stagingRel.split("/")), path.join(memoryDir, ...finalRel.split("/")));
655
+ const published = await readMaybeEncryptedFile(path.join(memoryDir, ...finalRel.split("/")), key, memoryDir);
656
+ assert.equal(published, content.toString("utf-8"));
657
+ } finally {
658
+ await rm(memoryDir, { recursive: true, force: true });
659
+ }
660
+ });
661
+
662
+ // ---------------------------------------------------------------------------
663
+ // Store helpers shared with the transaction
664
+ // ---------------------------------------------------------------------------
665
+
666
+ test("shardEntriesForIndex and serializeEmbeddingShard round-trip through shards", () => {
667
+ const index = indexFile("openai", "m", {
668
+ a: { path: "a.md", vector: [1] },
669
+ b: { path: "b.md", vector: [2] },
670
+ c: { path: "c.md", vector: [3] },
671
+ });
672
+ const groups = shardEntriesForIndex(index);
673
+ let total = 0;
674
+ for (const [shardIndex, entries] of groups) {
675
+ const body = serializeEmbeddingShard(index, shardIndex, entries);
676
+ const read = parseEmbeddingIndexDocument(body);
677
+ assert.equal(read.outcome, "ok");
678
+ if (read.outcome === "ok") total += Object.keys(read.file.entries).length;
679
+ }
680
+ assert.equal(total, 3);
681
+ });
682
+
683
+ test("serializeEmbeddingShard surfaces the tagged capacity error", () => {
684
+ const index = indexFile("openai", "m", {});
685
+ assert.throws(
686
+ () => serializeEmbeddingShard(index, 0, { a: { path: "p", vector: [1] } }, 4),
687
+ (err: unknown) => err instanceof EmbeddingIndexCapacityError
688
+ );
689
+ });
690
+
691
+ test("embeddingGenerationMembership classifies generation members", () => {
692
+ assert.deepEqual(embeddingGenerationMembership("state/embeddings/shard-0000.json"), {
693
+ kind: "shard",
694
+ shardDir: "state/embeddings",
695
+ });
696
+ assert.deepEqual(embeddingGenerationMembership("namespaces/team/state/embeddings.json"), {
697
+ kind: "marker",
698
+ shardDir: "namespaces/team/state/embeddings",
699
+ });
700
+ assert.equal(embeddingGenerationMembership("state/other.json"), null);
701
+ assert.equal(embeddingGenerationMembership("state/embeddings/not-a-shard.json"), null);
702
+ });
703
+
704
+ test("computeOmittedEmbeddingGenerationPaths omits nothing when every member is kept", async () => {
705
+ const root = await tempDir("remnic-3148-omission-none");
706
+ try {
707
+ await write(root, "state/embeddings/shard-0000.json", "0000");
708
+ await write(root, "state/embeddings/shard-0003.json", "0003");
709
+ const omission = await computeOmittedEmbeddingGenerationPaths({
710
+ rootAbs: root,
711
+ userExcludeRegexps: [/^facts\//],
712
+ isExcludedRelPath: () => false,
713
+ });
714
+ assert.equal(omission.omittedDirs.length, 0);
715
+ assert.equal(omission.omittedPaths.size, 0);
716
+ } finally {
717
+ await rm(root, { recursive: true, force: true });
718
+ }
719
+ });
720
+
721
+
722
+ // ---------------------------------------------------------------------------
723
+ // Round-5 batch: hydration, fail-closed IO, backup links, encrypted store io
724
+ // ---------------------------------------------------------------------------
725
+
726
+ test("metadata-only pulls hydrate unchanged shards from local bytes", async () => {
727
+ const localRoot = await tempDir("remnic-3148-hydrate");
728
+ const remoteRoot = await tempDir("remnic-3148-hydrate-remote");
729
+ try {
730
+ const shardBody = serializeIndex(indexFile("openai", "m", {
731
+ kept: { path: "p", vector: [1, 2] },
732
+ }));
733
+ await write(localRoot, trueShardRel("kept"), shardBody);
734
+ await write(localRoot, "facts/readme.md", "readme");
735
+ // Remote: identical generation (metadata-only pull carries no content
736
+ // for the unchanged shard) plus one genuinely new shard.
737
+ await write(remoteRoot, trueShardRel("kept"), shardBody);
738
+ await write(remoteRoot, trueShardRel("neu"), serializeIndex(
739
+ indexFile("openai", "m", { neu: { path: "p1", vector: [3] } }),
740
+ ));
741
+ const full = await buildOfflineSyncSnapshot({
742
+ root: remoteRoot, sourceId: "remote", includeContent: true,
743
+ });
744
+ // Simulate the CLI metadata hydration: keep content only for the NEW
745
+ // shard, drop it for the locally-unchanged one.
746
+ const metadataOnly: OfflineSyncSnapshot = {
747
+ ...full,
748
+ files: full.files.map((file) =>
749
+ file.path === trueShardRel("kept")
750
+ ? { path: file.path, sha256: file.sha256, bytes: file.bytes, mtimeMs: file.mtimeMs }
751
+ : file,
752
+ ),
753
+ };
754
+ const result = await applyOfflineSyncSnapshot({ root: localRoot, snapshot: metadataOnly });
755
+ // The unchanged shard is intact (hydrated + republished) and the new
756
+ // shard landed: no "missing incoming content" failure.
757
+ assert.equal(await readUtf8(localRoot, trueShardRel("kept")), shardBody);
758
+ assert.ok((await readUtf8(localRoot, trueShardRel("neu"))).includes("neu"));
759
+ assert.equal(result.upserted >= 2, true);
760
+ } finally {
761
+ await rm(localRoot, { recursive: true, force: true });
762
+ await rm(remoteRoot, { recursive: true, force: true });
763
+ }
764
+ });
765
+
766
+ test("custom storage IO without the staging pair fails closed before touching local state", async () => {
767
+ const localRoot = await tempDir("remnic-3148-downgrade-guard");
768
+ try {
769
+ await write(localRoot, "state/embeddings/shard-0000.json", serializeIndex(
770
+ indexFile("openai", "m1", { local: { path: "p", vector: [0] } }),
771
+ ));
772
+ let writeFileCalls = 0;
773
+ const result = await applyOfflineSyncSnapshot({
774
+ root: localRoot,
775
+ snapshot: {
776
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
777
+ schemaVersion: 1,
778
+ createdAt: "2026-09-27T00:00:00.000Z",
779
+ sourceId: "remote",
780
+ includeTranscripts: true,
781
+ files: [{
782
+ path: "state/embeddings/shard-0000.json",
783
+ sha256: sha256Of("incoming"),
784
+ bytes: 8,
785
+ mtimeMs: 1,
786
+ contentBase64: Buffer.from("incoming").toString("base64"),
787
+ }],
788
+ },
789
+ writeFile: async () => { writeFileCalls += 1; },
790
+ }).then(() => null, (error: unknown) => error as Error);
791
+ assert.ok(result instanceof Error);
792
+ assert.match(result.message, /plaintext-downgrade|writeStagingFile and readStagingFile/);
793
+ // The local generation is byte-identical and no custom write happened.
794
+ assert.ok((await readUtf8(localRoot, "state/embeddings/shard-0000.json")).includes("m1"));
795
+ assert.equal(writeFileCalls, 0);
796
+ } finally {
797
+ await rm(localRoot, { recursive: true, force: true });
798
+ }
799
+ });
800
+
801
+ test("a planted symlink backup is rejected, never restored or followed", async () => {
802
+ const localRoot = await tempDir("remnic-3148-backup-link");
803
+ const outside = await tempDir("remnic-3148-backup-outside");
804
+ const remoteRoot = await tempDir("remnic-3148-backup-remote");
805
+ try {
806
+ await mkdir(path.join(localRoot, "state"), { recursive: true });
807
+ await symlink(outside, path.join(localRoot, "state/embeddings.pre-replace.tmp"));
808
+ await write(outside, "shard-0000.json", "outside bytes");
809
+ await write(remoteRoot, "state/embeddings/shard-0000.json", serializeIndex(
810
+ indexFile("openai", "m", { neu: { path: "p", vector: [1] } }),
811
+ ));
812
+ const snapshot = await buildOfflineSyncSnapshot({
813
+ root: remoteRoot, sourceId: "remote", includeContent: true,
814
+ });
815
+ await assert.rejects(() => applyOfflineSyncSnapshot({ root: localRoot, snapshot }));
816
+ // The planted target is untouched and no generation was published
817
+ // through the link.
818
+ assert.equal(await readUtf8(outside, "shard-0000.json"), "outside bytes");
819
+ assert.equal(await existsQuiet(localRoot, "state/embeddings/shard-0000.json"), false);
820
+ } finally {
821
+ await rm(localRoot, { recursive: true, force: true });
822
+ await rm(outside, { recursive: true, force: true });
823
+ await rm(remoteRoot, { recursive: true, force: true });
824
+ }
825
+ });
826
+
827
+ test("mixed incoming (shards + marker) keeps the marker without a delete-rewrite cycle", async () => {
828
+ const localRoot = await tempDir("remnic-3148-mixed-marker");
829
+ const remoteRoot = await tempDir("remnic-3148-mixed-remote");
830
+ try {
831
+ await write(localRoot, "state/embeddings/shard-0000.json", serializeIndex(
832
+ indexFile("openai", "m1", { stale: { path: "p", vector: [9] } }),
833
+ ));
834
+ await write(localRoot, "state/embeddings.json", serializeIndex(indexFile("openai", "m0", {})));
835
+ await write(remoteRoot, trueShardRel("fresh"), serializeIndex(
836
+ indexFile("openai", "m1", { fresh: { path: "p", vector: [1] } }),
837
+ ));
838
+ const marker = serializeIndex(indexFile("openai", "m1", {}));
839
+ await write(remoteRoot, "state/embeddings.json", marker);
840
+ const snapshot = await buildOfflineSyncSnapshot({
841
+ root: remoteRoot, sourceId: "remote", includeContent: true,
842
+ });
843
+ await applyOfflineSyncSnapshot({ root: localRoot, snapshot });
844
+ // The incoming marker survived (inert artifact), no flip-flop deletion.
845
+ assert.equal(await readUtf8(localRoot, "state/embeddings.json"), marker);
846
+ assert.ok((await readUtf8(localRoot, trueShardRel("fresh"))).includes("fresh"));
847
+ } finally {
848
+ await rm(localRoot, { recursive: true, force: true });
849
+ await rm(remoteRoot, { recursive: true, force: true });
850
+ }
851
+ });
852
+
853
+ test("wired store io keeps encrypted migration, replacement, reload and writes canonical", async () => {
854
+ const memoryDir = await tempDir("remnic-3148-secure-store-io");
855
+ const prevLimit = process.env.REMNIC_EMBEDDING_INDEX_FILE_CHAR_LIMIT;
856
+ try {
857
+ const storage = new StorageManager(memoryDir);
858
+ await storage.setSecureStoreKeyAndWait(randomBytes(32), true);
859
+ // Emulate the orchestrator wiring: daemon store IO via StorageManager.
860
+ const shardLimit = 1024; // resolver floor; forces one-way migration quickly
861
+ process.env.REMNIC_EMBEDDING_INDEX_FILE_CHAR_LIMIT = String(shardLimit);
862
+ const entry = (i: number): EmbeddingIndexEntry => ({
863
+ path: `memories/m${i}.md`,
864
+ vector: Array.from({ length: 16 }, (_, k) => (i * 16 + k) / 4096),
865
+ });
866
+ const entries: Record<string, EmbeddingIndexEntry> = {};
867
+ for (let i = 0; i < 40; i += 1) entries[`id-${i}`] = entry(i);
868
+ const store = new EmbeddingIndexFileStore(
869
+ path.join(memoryDir, "state/embeddings.json"),
870
+ path.join(memoryDir, "state/embeddings"),
871
+ path.join(memoryDir, "state/embedding-fallback-status.json"),
872
+ {
873
+ readUtf8: async (filePath) =>
874
+ (await storage.readOfflineSyncFile(filePath)).toString("utf-8"),
875
+ writeUtf8: async (filePath, contents, opts) =>
876
+ storage.writeOfflineSyncStagingFile(
877
+ filePath,
878
+ Buffer.from(contents, "utf-8"),
879
+ opts?.finalAadFilePath === undefined ? undefined : { aadFilePath: opts.finalAadFilePath },
880
+ ),
881
+ },
882
+ );
883
+ // One-way migration under the small budget: staged ciphertext binds the
884
+ // FINAL shard paths, so the store can read its own migration back.
885
+ await store.persist(indexFile("openai", "m", entries));
886
+ assert.equal(await store.detectLayout(), "sharded");
887
+ const merged: Record<string, EmbeddingIndexEntry> = {};
888
+ assert.deepEqual(await store.readShardGenerationInto(merged), {
889
+ provider: "openai",
890
+ model: "m",
891
+ });
892
+ assert.equal(Object.keys(merged).length, 40);
893
+ // Raw bytes are encrypted at rest, canonical path decrypts via the io.
894
+ const someShard = path.join(memoryDir, "state/embeddings/shard-0000.json");
895
+ const raw = await readFile(someShard);
896
+ assert.equal(
897
+ raw.subarray(0, MAGIC_BYTES.length).toString("ascii"),
898
+ MAGIC_BYTES.toString("ascii"),
899
+ );
900
+ // Identity replacement also publishes encrypted + readable.
901
+ await store.persist(indexFile("openai", "m2", {
902
+ swapped: { path: "p", vector: [1] },
903
+ }));
904
+ const replaced: Record<string, EmbeddingIndexEntry> = {};
905
+ assert.deepEqual(await store.readShardGenerationInto(replaced), {
906
+ provider: "openai",
907
+ model: "m2",
908
+ });
909
+ assert.deepEqual(Object.keys(replaced), ["swapped"]);
910
+ // Subsequent dirty-shard write stays canonical-decryptable.
911
+ await store.persist(indexFile("openai", "m2", {
912
+ ...replaced,
913
+ swapped2: { path: "p2", vector: [2] },
914
+ }), { touchedIds: ["swapped2"] });
915
+ const after: Record<string, EmbeddingIndexEntry> = {};
916
+ await store.readShardGenerationInto(after);
917
+ assert.ok(after.swapped && after.swapped2);
918
+ // Diagnostics status: ALWAYS plain, recorded even on a locked store, and
919
+ // counts real failures (round 6: no conditional vacuity — the outcomes
920
+ // are actually recorded and read back through the console_state reader).
921
+ await store.recordIndexWriteOutcome(new Error("boom-one"));
922
+ await store.recordIndexWriteOutcome(new Error("boom-two"));
923
+ const statusPath = path.join(memoryDir, "state/embedding-fallback-status.json");
924
+ const statusRaw = await readFile(statusPath, "utf-8");
925
+ assert.equal(statusRaw.includes("REMNIC-ENC"), false);
926
+ const status = JSON.parse(statusRaw) as { failureCount?: number; lastWriteFailure?: { message?: string } };
927
+ assert.equal(status.failureCount, 2);
928
+ assert.equal(status.lastWriteFailure?.message, "boom-two");
929
+ const snapshotState = await gatherConsoleState({ config: { memoryDir } } as never);
930
+ assert.equal(snapshotState.embeddingIndex?.status?.failureCount, 2);
931
+ assert.equal(snapshotState.embeddingIndex?.status?.lastWriteFailure?.message, "boom-two");
932
+ } finally {
933
+ if (prevLimit === undefined) delete process.env.REMNIC_EMBEDDING_INDEX_FILE_CHAR_LIMIT;
934
+ else process.env.REMNIC_EMBEDDING_INDEX_FILE_CHAR_LIMIT = prevLimit;
935
+ await rm(memoryDir, { recursive: true, force: true });
936
+ }
937
+ });
938
+
939
+ test("streamed snapshot header carries omission metadata and the receiver keeps the generation", async () => {
940
+ const localRoot = await tempDir("remnic-3148-stream-apply");
941
+ const remoteRoot = await tempDir("remnic-3148-stream-remote");
942
+ try {
943
+ await write(localRoot, "state/embeddings/shard-0000.json", "local 0000");
944
+ await write(localRoot, "state/embeddings/shard-0001.json", "local 0001");
945
+ await write(remoteRoot, "facts/only.md", "only");
946
+ await write(remoteRoot, "state/embeddings/shard-0000.json", "remote 0000");
947
+ await write(remoteRoot, "state/embeddings/shard-0003.json", "filtered 0003");
948
+ const build = await buildOfflineSyncSnapshot({
949
+ root: remoteRoot, sourceId: "remote", includeContent: true,
950
+ userExcludeRegexps: [/^state\/embeddings\/shard-0003\.json$/],
951
+ });
952
+ assert.deepEqual(build.omittedEmbeddingGenerationDirs, ["state/embeddings"]);
953
+ // Serve the snapshot through the REAL ndjson stream writer.
954
+ const { createServer } = await import("node:http");
955
+ const received: string[] = [];
956
+ const server = createServer((request, response) => {
957
+ void respondOfflineSnapshotStream(response, {
958
+ namespace: "test",
959
+ ...build,
960
+ files: (async function* () {
961
+ for (const file of build.files) yield file;
962
+ })(),
963
+ });
964
+ });
965
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
966
+ const address = server.address();
967
+ const url = `http://127.0.0.1:${typeof address === "object" && address ? address.port : 0}/`;
968
+ try {
969
+ const response = await fetch(url);
970
+ const text = await response.text();
971
+ for (const line of text.split("\n").filter((l) => l.trim().length > 0)) {
972
+ const parsed = JSON.parse(line) as Record<string, unknown>;
973
+ if (parsed.type === "snapshot") received.push(JSON.stringify(parsed));
974
+ }
975
+ } finally {
976
+ await new Promise<void>((resolve) => server.close(() => resolve()));
977
+ }
978
+ // Header round-trips the omission (server writer + client decoder field).
979
+ assert.equal(received.length, 1);
980
+ const header = JSON.parse(received[0] ?? "{}") as Record<string, unknown>;
981
+ assert.deepEqual(header.omittedEmbeddingGenerationDirs, ["state/embeddings"]);
982
+ // Receiver: normalize the streamed header+files exactly like the client
983
+ // and apply — the local generation must survive.
984
+ const streamed = normalizeOfflineSyncSnapshot({
985
+ format: header.format,
986
+ schemaVersion: header.schemaVersion,
987
+ createdAt: header.createdAt,
988
+ sourceId: header.sourceId,
989
+ includeTranscripts: header.includeTranscripts,
990
+ omittedEmbeddingGenerationDirs: header.omittedEmbeddingGenerationDirs as string[],
991
+ files: build.files,
992
+ });
993
+ const baseFiles = [
994
+ { path: "state/embeddings/shard-0000.json", sha256: "0".repeat(64), bytes: 9, mtimeMs: 1 },
995
+ { path: "state/embeddings/shard-0001.json", sha256: "1".repeat(64), bytes: 9, mtimeMs: 1 },
996
+ ];
997
+ const result = await applyOfflineSyncSnapshot({ root: localRoot, snapshot: streamed, baseFiles });
998
+ assert.equal(await readUtf8(localRoot, "state/embeddings/shard-0000.json"), "local 0000");
999
+ assert.equal(await readUtf8(localRoot, "state/embeddings/shard-0001.json"), "local 0001");
1000
+ assert.equal(result.deleted, 0);
1001
+ } finally {
1002
+ await rm(localRoot, { recursive: true, force: true });
1003
+ await rm(remoteRoot, { recursive: true, force: true });
1004
+ }
1005
+ });
1006
+
1007
+
1008
+ // ---------------------------------------------------------------------------
1009
+ // Round 6: legacy-marker hydration, shard membership, recovery diagnostics
1010
+ // ---------------------------------------------------------------------------
1011
+
1012
+ test("metadata-only pulls hydrate an unchanged legacy marker", async () => {
1013
+ const localRoot = await tempDir("remnic-3148-marker-hydrate");
1014
+ const remoteRoot = await tempDir("remnic-3148-marker-remote");
1015
+ try {
1016
+ const marker = serializeIndex(indexFile("openai", "m", {
1017
+ kept: { path: "p", vector: [1, 2, 3] },
1018
+ }));
1019
+ await write(localRoot, "facts/r.md", "r");
1020
+ await write(localRoot, "state/embeddings.json", marker);
1021
+ await write(remoteRoot, "state/embeddings.json", marker);
1022
+ const full = await buildOfflineSyncSnapshot({
1023
+ root: remoteRoot, sourceId: "remote", includeContent: true,
1024
+ });
1025
+ // Metadata-only pull: the marker's content is omitted because the local
1026
+ // hash already matches.
1027
+ const metadataOnly: OfflineSyncSnapshot = {
1028
+ ...full,
1029
+ files: full.files.map((file) =>
1030
+ file.path === "state/embeddings.json"
1031
+ ? { path: file.path, sha256: file.sha256, bytes: file.bytes, mtimeMs: file.mtimeMs }
1032
+ : file,
1033
+ ),
1034
+ };
1035
+ // Second (and every subsequent) sync must not throw
1036
+ // "missing decoded content for state/embeddings.json".
1037
+ const result = await applyOfflineSyncSnapshot({ root: localRoot, snapshot: metadataOnly });
1038
+ assert.equal(await readUtf8(localRoot, "state/embeddings.json"), marker);
1039
+ assert.equal(result.conflicts.length, 0);
1040
+ // The one-way conversion published the same entries as shards.
1041
+ const entries = await readGenerationEntries(localRoot);
1042
+ assert.deepEqual(Object.keys(entries), ["kept"]);
1043
+ } finally {
1044
+ await rm(localRoot, { recursive: true, force: true });
1045
+ await rm(remoteRoot, { recursive: true, force: true });
1046
+ }
1047
+ });
1048
+
1049
+ test("a shard file holding ids assigned to another shard is rejected", async () => {
1050
+ const root = await tempDir("remnic-3148-misplaced");
1051
+ try {
1052
+ await write(root, "state/embeddings.json", "{}");
1053
+ // Find an id that does NOT belong in shard 0000.
1054
+ let misplaced = "id-0";
1055
+ for (let i = 1; i < 4096; i += 1) {
1056
+ const candidate = `id-${i}`;
1057
+ if (shardEntriesForIndex(indexFile("openai", "m", {
1058
+ [candidate]: { path: "p", vector: [1] },
1059
+ })).has(0) === false) {
1060
+ misplaced = candidate;
1061
+ break;
1062
+ }
1063
+ }
1064
+ const misplacedBody = serializeIndex(indexFile("openai", "m", {
1065
+ [misplaced]: { path: "p", vector: [1] },
1066
+ }));
1067
+ await write(root, "state/embeddings/shard-0000.json", misplacedBody);
1068
+ const store = new EmbeddingIndexFileStore(
1069
+ path.join(root, "state/embeddings.json"),
1070
+ path.join(root, "state/embeddings"),
1071
+ path.join(root, "state/embedding-fallback-status.json"),
1072
+ );
1073
+ await assert.rejects(
1074
+ () => store.readShardGenerationInto({}),
1075
+ (err: unknown) => err instanceof EmbeddingIndexStorageError && err.message.includes(misplaced),
1076
+ );
1077
+ // Bytes preserved; incoming generation with the same misplaced shard is
1078
+ // rejected too (fail closed, no merge of misplaced vectors).
1079
+ assert.equal(await readUtf8(root, "state/embeddings/shard-0000.json"), misplacedBody);
1080
+ } finally {
1081
+ await rm(root, { recursive: true, force: true });
1082
+ }
1083
+ });
1084
+
1085
+ test("recovery failure is recorded durably before the mutation is rejected", async () => {
1086
+ const memoryDir = await tempDir("remnic-3148-recovery-recorded");
1087
+ const outside = await tempDir("remnic-3148-recovery-outside");
1088
+ try {
1089
+ const storage = new StorageManager(memoryDir);
1090
+ await storage.setSecureStoreKeyAndWait(randomBytes(32), true);
1091
+ const fallback = new EmbeddingFallback({ memoryDir } as never,
1092
+ storageBackedIndexStoreIo(storage));
1093
+ // Plant the rename gap with a symlinked backup: recovery must fail.
1094
+ await mkdir(path.join(memoryDir, "state"), { recursive: true });
1095
+ await symlink(outside, path.join(memoryDir, "state/embeddings.pre-replace.tmp"));
1096
+ await write(outside, "shard-0000.json", "outside bytes");
1097
+ const probe = fallback as unknown as {
1098
+ enqueueIndexMutation(m: () => Promise<void>): Promise<void>;
1099
+ };
1100
+ await assert.rejects(
1101
+ () => probe.enqueueIndexMutation(async () => undefined),
1102
+ (err: unknown) => err instanceof Error && /symlink/.test(err.message),
1103
+ );
1104
+ // The failure is DURABLY recorded (round 6: recovery happens before the
1105
+ // write-outcome try, so the wrapper must record it itself).
1106
+ const status = JSON.parse(
1107
+ await readFile(path.join(memoryDir, "state/embedding-fallback-status.json"), "utf-8"),
1108
+ ) as { failureCount?: number; lastWriteFailure?: { message?: string } };
1109
+ assert.equal(status.failureCount, 1);
1110
+ assert.match(status.lastWriteFailure?.message ?? "", /symlink/);
1111
+ assert.equal(await readUtf8(outside, "shard-0000.json"), "outside bytes");
1112
+ } finally {
1113
+ await rm(memoryDir, { recursive: true, force: true });
1114
+ await rm(outside, { recursive: true, force: true });
1115
+ }
1116
+ });
1117
+
1118
+
1119
+ // ---------------------------------------------------------------------------
1120
+ // Round 6.5: converted-shard base bookkeeping + atomic whole-generation removal
1121
+ // ---------------------------------------------------------------------------
1122
+
1123
+ test("converted shard paths are recorded in the persisted base immediately", async () => {
1124
+ const localRoot = await tempDir("remnic-3148-converted-base");
1125
+ const remoteRoot = await tempDir("remnic-3148-converted-base-remote");
1126
+ try {
1127
+ // Local has ONLY the legacy marker: the converted shards are brand-new
1128
+ // local paths that appear in none of the base/incoming/current maps.
1129
+ const incoming = serializeIndex(indexFile("openai", "m", {
1130
+ conv1: { path: "p1", vector: [1] },
1131
+ conv2: { path: "p2", vector: [2] },
1132
+ }));
1133
+ await write(remoteRoot, "state/embeddings.json", incoming);
1134
+ const snapshot = await buildOfflineSyncSnapshot({
1135
+ root: remoteRoot, sourceId: "remote", includeContent: true,
1136
+ });
1137
+ const result = await applyOfflineSyncSnapshot({ root: localRoot, snapshot });
1138
+ const shardPaths = result.nextBaseFiles
1139
+ .map((f) => f.path)
1140
+ .filter((path) => path.startsWith("state/embeddings/shard-"));
1141
+ assert.equal(shardPaths.length >= 2, true);
1142
+ // The state digests match the actually-published shard bytes.
1143
+ for (const state of result.nextBaseFiles.filter((f) => f.path.startsWith("state/embeddings/shard-"))) {
1144
+ const published = await readFile(path.join(localRoot, ...state.path.split("/")));
1145
+ assert.equal(createHash("sha256").update(published).digest("hex"), state.sha256);
1146
+ }
1147
+ } finally {
1148
+ await rm(localRoot, { recursive: true, force: true });
1149
+ await rm(remoteRoot, { recursive: true, force: true });
1150
+ }
1151
+ });
1152
+
1153
+ test("a remotely deleted generation is removed as one atomic transaction", async () => {
1154
+ const localRoot = await tempDir("remnic-3148-atomic-removal");
1155
+ try {
1156
+ await write(localRoot, "facts/keep.md", "keep");
1157
+ await write(localRoot, "state/embeddings/shard-0000.json", serializeIndex(
1158
+ indexFile("openai", "m", { a: { path: "p", vector: [1] } }),
1159
+ ));
1160
+ await write(localRoot, trueShardRel("hydrated"), serializeIndex(
1161
+ indexFile("openai", "m", { b: { path: "p2", vector: [2] } }),
1162
+ ));
1163
+ await write(localRoot, "state/embeddings.json", serializeIndex(indexFile("openai", "m", {})));
1164
+ const snapshot: OfflineSyncSnapshot = {
1165
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
1166
+ schemaVersion: 1,
1167
+ createdAt: "2026-09-27T00:00:00.000Z",
1168
+ sourceId: "remote",
1169
+ includeTranscripts: true,
1170
+ files: [{
1171
+ path: "facts/keep.md",
1172
+ sha256: sha256Of("keep"),
1173
+ bytes: 4,
1174
+ mtimeMs: 1,
1175
+ contentBase64: Buffer.from("keep").toString("base64"),
1176
+ }],
1177
+ deletions: [
1178
+ { path: "state/embeddings/shard-0000.json", mtimeMs: 100 },
1179
+ { path: trueShardRel("hydrated"), mtimeMs: 100 },
1180
+ { path: "state/embeddings.json", mtimeMs: 100 },
1181
+ ],
1182
+ };
1183
+ const recorded: string[] = [];
1184
+ const result = await applyOfflineSyncSnapshot({
1185
+ root: localRoot,
1186
+ snapshot,
1187
+ // Emulate the real storage hook contract: the hook DELETES the file
1188
+ // and records the replicated tombstone.
1189
+ deleteFile: async (target) => {
1190
+ recorded.push(target.path);
1191
+ await rm(path.join(localRoot, ...target.path.split("/")), { force: true });
1192
+ },
1193
+ });
1194
+ // The whole generation is gone in ONE swap: no shard, no marker.
1195
+ assert.equal(await existsQuiet(localRoot, "state/embeddings/shard-0000.json"), false);
1196
+ assert.equal(await existsQuiet(localRoot, trueShardRel("hydrated")), false);
1197
+ assert.equal(await existsQuiet(localRoot, "state/embeddings.json"), false);
1198
+ assert.equal(result.deleted >= 3, true);
1199
+ assert.equal(await existsQuiet(localRoot, "state/embeddings.pre-replace.tmp"), false);
1200
+ // Replicated tombstones are preserved through the configured hook.
1201
+ assert.ok(recorded.includes("state/embeddings/shard-0000.json"));
1202
+ assert.ok(recorded.includes(trueShardRel("hydrated")));
1203
+ assert.ok(recorded.includes("state/embeddings.json"));
1204
+ // The published directory remains as the EMPTY one-way layout marker:
1205
+ // even a stray legacy file planted afterwards can never win.
1206
+ const store = new EmbeddingIndexFileStore(
1207
+ path.join(localRoot, "state/embeddings.json"),
1208
+ path.join(localRoot, "state/embeddings"),
1209
+ path.join(localRoot, "state/embedding-fallback-status.json"),
1210
+ );
1211
+ assert.equal(await store.detectLayout(), "sharded");
1212
+ assert.equal((await readdir(path.join(localRoot, "state/embeddings"))).length, 0);
1213
+ await write(localRoot, "state/embeddings.json", serializeIndex(indexFile("openai", "stray", {
1214
+ stale: { path: "p", vector: [9] },
1215
+ })));
1216
+ const afterStray: Record<string, EmbeddingIndexEntry> = {};
1217
+ await store.readShardGenerationInto(afterStray);
1218
+ assert.deepEqual(Object.keys(afterStray), []);
1219
+ assert.equal(await store.detectLayout(), "sharded");
1220
+ } finally {
1221
+ await rm(localRoot, { recursive: true, force: true });
1222
+ }
1223
+ });
1224
+
1225
+
1226
+ test("a marker delete failure after a committed removal keeps the empty generation authoritative", async () => {
1227
+ const localRoot = await tempDir("remnic-3148-marker-delete-fail");
1228
+ try {
1229
+ await write(localRoot, "state/embeddings/shard-0000.json", serializeIndex(
1230
+ indexFile("openai", "m", { a: { path: "p", vector: [1] } }),
1231
+ ));
1232
+ await write(localRoot, "state/embeddings.json", serializeIndex(indexFile("openai", "m", {})));
1233
+ const snapshot: OfflineSyncSnapshot = {
1234
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
1235
+ schemaVersion: 1,
1236
+ createdAt: "2026-09-27T00:00:00.000Z",
1237
+ sourceId: "remote",
1238
+ includeTranscripts: true,
1239
+ files: [],
1240
+ deletions: [
1241
+ { path: "state/embeddings/shard-0000.json", mtimeMs: 100 },
1242
+ { path: "state/embeddings.json", mtimeMs: 100 },
1243
+ ],
1244
+ };
1245
+ await assert.rejects(() => applyOfflineSyncSnapshot({
1246
+ root: localRoot,
1247
+ snapshot,
1248
+ deleteFile: async (target) => {
1249
+ if (target.path === "state/embeddings.json") throw new Error("marker delete IO error");
1250
+ },
1251
+ }), /marker delete IO error/);
1252
+ // The removal COMMITTED before the marker delete failed: the empty
1253
+ // published generation stays authoritative across a restart, and the
1254
+ // stale marker can never resurrect entries.
1255
+ const store = new EmbeddingIndexFileStore(
1256
+ path.join(localRoot, "state/embeddings.json"),
1257
+ path.join(localRoot, "state/embeddings"),
1258
+ path.join(localRoot, "state/embedding-fallback-status.json"),
1259
+ );
1260
+ assert.equal(await store.detectLayout(), "sharded");
1261
+ assert.equal((await readdir(path.join(localRoot, "state/embeddings"))).length, 0);
1262
+ const merged: Record<string, EmbeddingIndexEntry> = {};
1263
+ await store.readShardGenerationInto(merged);
1264
+ assert.deepEqual(Object.keys(merged), []);
1265
+ assert.equal(await existsQuiet(localRoot, "state/embeddings.pre-replace.tmp"), false);
1266
+ } finally {
1267
+ await rm(localRoot, { recursive: true, force: true });
1268
+ }
1269
+ });
1270
+
1271
+
1272
+ // ---------------------------------------------------------------------------
1273
+ // Round 6.5b: base-evidenced remote absence (no tombstones needed)
1274
+ // ---------------------------------------------------------------------------
1275
+
1276
+ test("a base-shared generation absent from the incoming snapshot is removed without tombstones", async () => {
1277
+ const localRoot = await tempDir("remnic-3148-base-absent");
1278
+ try {
1279
+ await write(localRoot, "facts/base.md", "base");
1280
+ await write(localRoot, trueShardRel("a1"), serializeIndex(indexFile("openai", "m", {
1281
+ a1: { path: "p", vector: [1] },
1282
+ })));
1283
+ await write(localRoot, trueShardRel("a2"), serializeIndex(indexFile("openai", "m", {
1284
+ a2: { path: "p2", vector: [2] },
1285
+ })));
1286
+ const base = [
1287
+ { path: trueShardRel("a1"), sha256: "a".repeat(64), bytes: 2, mtimeMs: 1 },
1288
+ { path: trueShardRel("a2"), sha256: "b".repeat(64), bytes: 2, mtimeMs: 1 },
1289
+ ];
1290
+ void base;
1291
+ // Incoming: ONLY the fact file; the generation vanished remotely and the
1292
+ // snapshot carries NO deletions array at all.
1293
+ const snapshot: OfflineSyncSnapshot = {
1294
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
1295
+ schemaVersion: 1,
1296
+ createdAt: "2026-09-27T00:00:00.000Z",
1297
+ sourceId: "remote",
1298
+ includeTranscripts: true,
1299
+ files: [{
1300
+ path: "facts/base.md",
1301
+ sha256: sha256Of("base"),
1302
+ bytes: 4,
1303
+ mtimeMs: 1,
1304
+ contentBase64: Buffer.from("base").toString("base64"),
1305
+ }],
1306
+ };
1307
+ const baseEntries = await Promise.all([trueShardRel("a1"), trueShardRel("a2")].map(async (shardPath) => ({
1308
+ path: shardPath,
1309
+ sha256: sha256Of(await readFile(path.join(localRoot, ...shardPath.split("/")), "utf-8")),
1310
+ bytes: (await readFile(path.join(localRoot, ...shardPath.split("/")))).length,
1311
+ mtimeMs: 1,
1312
+ })));
1313
+ const result = await applyOfflineSyncSnapshot({
1314
+ root: localRoot,
1315
+ snapshot,
1316
+ baseFiles: baseEntries,
1317
+ });
1318
+ // Whole generation removed as one unit, no partial shard left behind.
1319
+ assert.equal(await existsQuiet(localRoot, trueShardRel("a1")), false);
1320
+ assert.equal(await existsQuiet(localRoot, trueShardRel("a2")), false);
1321
+ assert.equal(await existsQuiet(localRoot, "state/embeddings.json"), false);
1322
+ assert.equal(await existsQuiet(localRoot, "state/embeddings.pre-replace.tmp"), false);
1323
+ assert.equal(result.deleted >= 2, true);
1324
+ } finally {
1325
+ await rm(localRoot, { recursive: true, force: true });
1326
+ }
1327
+ });
1328
+
1329
+ async function readUtf8Async(absolutePath: string): Promise<string> {
1330
+ return readFile(absolutePath, "utf-8");
1331
+ }
1332
+
1333
+ test("a wholly-local generation with no base or tombstone evidence is preserved", async () => {
1334
+ const localRoot = await tempDir("remnic-3148-pure-local");
1335
+ try {
1336
+ await write(localRoot, "facts/other.md", "other");
1337
+ await write(localRoot, trueShardRel("local1"), serializeIndex(indexFile("openai", "m", {
1338
+ local1: { path: "p", vector: [1] },
1339
+ })));
1340
+ await write(localRoot, trueShardRel("local2"), serializeIndex(indexFile("openai", "m", {
1341
+ local2: { path: "p2", vector: [2] },
1342
+ })));
1343
+ const snapshot: OfflineSyncSnapshot = {
1344
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
1345
+ schemaVersion: 1,
1346
+ createdAt: "2026-09-27T00:00:00.000Z",
1347
+ sourceId: "remote",
1348
+ includeTranscripts: true,
1349
+ files: [{
1350
+ path: "facts/other.md",
1351
+ sha256: sha256Of("other"),
1352
+ bytes: 5,
1353
+ mtimeMs: 1,
1354
+ contentBase64: Buffer.from("other").toString("base64"),
1355
+ }],
1356
+ };
1357
+ const result = await applyOfflineSyncSnapshot({ root: localRoot, snapshot });
1358
+ // No base entries, no tombstones: the generation is untouched.
1359
+ assert.ok((await readUtf8(localRoot, trueShardRel("local1"))).includes("local1"));
1360
+ assert.ok((await readUtf8(localRoot, trueShardRel("local2"))).includes("local2"));
1361
+ assert.equal(result.deleted, 0);
1362
+ } finally {
1363
+ await rm(localRoot, { recursive: true, force: true });
1364
+ }
1365
+ });
1366
+
1367
+ test("remote deletion preserves a shared generation with local-only additions as one unit", async () => {
1368
+ const localRoot = await tempDir("remnic-3148-mixed-unit");
1369
+ try {
1370
+ await write(localRoot, trueShardRel("shared"), serializeIndex(indexFile("openai", "m", {
1371
+ shared: { path: "p", vector: [1] },
1372
+ })));
1373
+ await write(localRoot, trueShardRel("extra"), serializeIndex(indexFile("openai", "m", {
1374
+ extra: { path: "p2", vector: [2] },
1375
+ })));
1376
+ const baseFiles = [
1377
+ {
1378
+ path: trueShardRel("shared"),
1379
+ sha256: sha256Of(await readFile(path.join(localRoot, ...trueShardRel("shared").split("/")), "utf-8")),
1380
+ bytes: 0,
1381
+ mtimeMs: 1,
1382
+ },
1383
+ ];
1384
+ const snapshot: OfflineSyncSnapshot = {
1385
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
1386
+ schemaVersion: 1,
1387
+ createdAt: "2026-09-27T00:00:00.000Z",
1388
+ sourceId: "remote",
1389
+ includeTranscripts: true,
1390
+ files: [],
1391
+ };
1392
+ const result = await applyOfflineSyncSnapshot({ root: localRoot, snapshot, baseFiles });
1393
+ // The local addition conflicts with remote deletion: preserve the WHOLE
1394
+ // generation rather than deleting the shared member or the added shard.
1395
+ assert.equal(await existsQuiet(localRoot, trueShardRel("shared")), true);
1396
+ assert.equal(await existsQuiet(localRoot, trueShardRel("extra")), true);
1397
+ assert.equal(result.deleted, 0);
1398
+ } finally {
1399
+ await rm(localRoot, { recursive: true, force: true });
1400
+ }
1401
+ });
1402
+
1403
+
1404
+ // ---------------------------------------------------------------------------
1405
+ // Round 6.5c: remaining codex P1s + deterministic crash-state regressions
1406
+ // ---------------------------------------------------------------------------
1407
+
1408
+ test("remote tombstones over a partial known set sweep local-only extras and the marker", async () => {
1409
+ const localRoot = await tempDir("remnic-3148-sweep-extras");
1410
+ try {
1411
+ // Local: the two shards the remote knew + a local-only extra + an inert
1412
+ // legacy marker the remote never had.
1413
+ await write(localRoot, trueShardRel("known"), serializeIndex(indexFile("openai", "m", {
1414
+ known: { path: "p", vector: [1] },
1415
+ })));
1416
+ await write(localRoot, trueShardRel("extra"), serializeIndex(indexFile("openai", "m", {
1417
+ extra: { path: "p2", vector: [2] },
1418
+ })));
1419
+ await write(localRoot, "state/embeddings.json", serializeIndex(indexFile("openai", "m", {})));
1420
+ const snapshot: OfflineSyncSnapshot = {
1421
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
1422
+ schemaVersion: 1,
1423
+ createdAt: "2026-09-27T00:00:00.000Z",
1424
+ sourceId: "remote",
1425
+ includeTranscripts: true,
1426
+ files: [],
1427
+ deletions: [{ path: trueShardRel("known"), mtimeMs: 100 }],
1428
+ };
1429
+ const result = await applyOfflineSyncSnapshot({ root: localRoot, snapshot });
1430
+ // The remote deletion is established by the one known tombstone; the
1431
+ // sweep removes the local-only extra and the marker as the same unit.
1432
+ assert.equal(await existsQuiet(localRoot, trueShardRel("known")), false);
1433
+ assert.equal(await existsQuiet(localRoot, trueShardRel("extra")), false);
1434
+ assert.equal(await existsQuiet(localRoot, "state/embeddings.json"), false);
1435
+ assert.equal(result.deleted >= 3, true);
1436
+ } finally {
1437
+ await rm(localRoot, { recursive: true, force: true });
1438
+ }
1439
+ });
1440
+
1441
+ test("a snapshot that both includes and omits a generation fails closed", async () => {
1442
+ const localRoot = await tempDir("remnic-3148-include-omit");
1443
+ try {
1444
+ await write(localRoot, "facts/x.md", "x");
1445
+ await write(localRoot, trueShardRel("a1"), serializeIndex(indexFile("openai", "m", {
1446
+ a1: { path: "p", vector: [1] },
1447
+ })));
1448
+ const snapshot: OfflineSyncSnapshot = {
1449
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
1450
+ schemaVersion: 1,
1451
+ createdAt: "2026-09-27T00:00:00.000Z",
1452
+ sourceId: "remote",
1453
+ includeTranscripts: true,
1454
+ files: [{
1455
+ path: trueShardRel("a1"),
1456
+ sha256: sha256Of("incoming"),
1457
+ bytes: 8,
1458
+ mtimeMs: 1,
1459
+ contentBase64: Buffer.from("incoming").toString("base64"),
1460
+ }],
1461
+ omittedEmbeddingGenerationDirs: ["state/embeddings"],
1462
+ };
1463
+ await assert.rejects(() => applyOfflineSyncSnapshot({ root: localRoot, snapshot }),
1464
+ (err: unknown) => err instanceof EmbeddingIndexStorageError);
1465
+ // Nothing applied from the contradictory snapshot.
1466
+ assert.ok((await readUtf8(localRoot, trueShardRel("a1"))).includes("a1"));
1467
+ } finally {
1468
+ await rm(localRoot, { recursive: true, force: true });
1469
+ }
1470
+ });
1471
+
1472
+ test("an excluded tombstoned member omits its whole generation from the push", async () => {
1473
+ const root = await tempDir("remnic-3148-omitted-tombstone");
1474
+ try {
1475
+ await write(root, "facts/a.md", "alpha");
1476
+ await write(root, trueShardRel("kept"), serializeIndex(indexFile("openai", "m", {
1477
+ kept: { path: "p", vector: [1] },
1478
+ })));
1479
+ // shard-0007 was already deleted locally and is only represented by a
1480
+ // deletion revision; the user regexp excludes exactly that member.
1481
+ const snapshot = await buildOfflineSyncSnapshot({
1482
+ root,
1483
+ sourceId: "source",
1484
+ includeContent: true,
1485
+ userExcludeRegexps: [/^state\/embeddings\/shard-0007\.json$/],
1486
+ deletions: [{ path: "state/embeddings/shard-0007.json", mtimeMs: 500 }],
1487
+ });
1488
+ const paths = snapshot.files.map((file) => file.path);
1489
+ assert.deepEqual(paths, ["facts/a.md"]);
1490
+ assert.deepEqual(snapshot.omittedEmbeddingGenerationDirs, ["state/embeddings"]);
1491
+ // The tombstone for the excluded member is suppressed with the generation.
1492
+ assert.deepEqual(snapshot.deletions, []);
1493
+ } finally {
1494
+ await rm(root, { recursive: true, force: true });
1495
+ }
1496
+ });
1497
+
1498
+ test("a partially removed backup after a committed removal stays inert across restart", async () => {
1499
+ const localRoot = await tempDir("remnic-3148-partial-backup");
1500
+ try {
1501
+ await write(localRoot, "state/embeddings/shard-0000.json", serializeIndex(indexFile("openai", "m", {
1502
+ a: { path: "p", vector: [1] },
1503
+ })));
1504
+ await write(localRoot, "state/embeddings.json", serializeIndex(indexFile("openai", "m", {})));
1505
+ const snapshot: OfflineSyncSnapshot = {
1506
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
1507
+ schemaVersion: 1,
1508
+ createdAt: "2026-09-27T00:00:00.000Z",
1509
+ sourceId: "remote",
1510
+ includeTranscripts: true,
1511
+ files: [],
1512
+ deletions: [
1513
+ { path: "state/embeddings/shard-0000.json", mtimeMs: 100 },
1514
+ { path: "state/embeddings.json", mtimeMs: 100 },
1515
+ ],
1516
+ };
1517
+ await applyOfflineSyncSnapshot({ root: localRoot, snapshot });
1518
+ // Committed removal: the empty published generation is authoritative.
1519
+ const store = new EmbeddingIndexFileStore(
1520
+ path.join(localRoot, "state/embeddings.json"),
1521
+ path.join(localRoot, "state/embeddings"),
1522
+ path.join(localRoot, "state/embedding-fallback-status.json"),
1523
+ );
1524
+ assert.equal(await store.detectLayout(), "sharded");
1525
+ // Simulate a crash mid backup-cleanup: the demoted backup holds a
1526
+ // PARTIAL old generation on disk.
1527
+ await mkdir(path.join(localRoot, "state/embeddings.pre-replace.tmp"), { recursive: true });
1528
+ await write(localRoot, "state/embeddings.pre-replace.tmp/shard-0000.json", serializeIndex(
1529
+ indexFile("openai", "m", { resurrect: { path: "p", vector: [9] } }),
1530
+ ));
1531
+ // A FRESH instance (restart) still serves the empty authoritative
1532
+ // generation; the partial backup never resurrects entries.
1533
+ const fresh = new EmbeddingIndexFileStore(
1534
+ path.join(localRoot, "state/embeddings.json"),
1535
+ path.join(localRoot, "state/embeddings"),
1536
+ path.join(localRoot, "state/embedding-fallback-status.json"),
1537
+ );
1538
+ assert.equal(await fresh.detectLayout(), "sharded");
1539
+ const merged: Record<string, EmbeddingIndexEntry> = {};
1540
+ await fresh.readShardGenerationInto(merged);
1541
+ assert.deepEqual(Object.keys(merged), []);
1542
+ // A subsequent removal cleans the stale partial backup safely.
1543
+ await fresh.removePublishedGeneration();
1544
+ assert.equal(await existsQuiet(localRoot, "state/embeddings.pre-replace.tmp"), false);
1545
+ const mergedAfter: Record<string, EmbeddingIndexEntry> = {};
1546
+ await fresh.readShardGenerationInto(mergedAfter);
1547
+ assert.deepEqual(Object.keys(mergedAfter), []);
1548
+ } finally {
1549
+ await rm(localRoot, { recursive: true, force: true });
1550
+ }
1551
+ });
1552
+
1553
+
1554
+ test("remote tombstones for already-missing shards still sweep the local unit", async () => {
1555
+ const localRoot = await tempDir("remnic-3148-sweep-missing");
1556
+ try {
1557
+ // Local: ONE remaining extra shard. The remote tombstones two shards:
1558
+ // one already absent locally (removed in an earlier round) and... the
1559
+ // local extra itself is NOT tombstoned (the remote never had it).
1560
+ await write(localRoot, trueShardRel("extra"), serializeIndex(indexFile("openai", "m", {
1561
+ extra: { path: "p2", vector: [2] },
1562
+ })));
1563
+ const snapshot: OfflineSyncSnapshot = {
1564
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
1565
+ schemaVersion: 1,
1566
+ createdAt: "2026-09-27T00:00:00.000Z",
1567
+ sourceId: "remote",
1568
+ includeTranscripts: true,
1569
+ files: [],
1570
+ deletions: [{ path: trueShardRel("gone"), mtimeMs: 100 }],
1571
+ };
1572
+ const result = await applyOfflineSyncSnapshot({ root: localRoot, snapshot });
1573
+ // The unit sweep removes the remaining local shard even though the only
1574
+ // tombstone names an already-missing shard.
1575
+ assert.equal(await existsQuiet(localRoot, trueShardRel("extra")), false);
1576
+ assert.equal(await existsQuiet(localRoot, trueShardRel("gone")), false);
1577
+ assert.equal(result.deleted >= 1, true);
1578
+ } finally {
1579
+ await rm(localRoot, { recursive: true, force: true });
1580
+ }
1581
+ });
1582
+
1583
+ test("snapshot apply preserves locally diverged generations without CLI deferrals", async () => {
1584
+ const root = await tempDir("remnic-prepare-divergence");
1585
+ try {
1586
+ const relPath = trueShardRel("a1");
1587
+ const baseBody = serializeIndex(indexFile("openai", "m", { a1: { path: "p", vector: [1] } }));
1588
+ const localBody = serializeIndex(indexFile("openai", "m", { a1: { path: "p", vector: [2] } }));
1589
+ const record = { path: relPath, sha256: sha256Of(baseBody), bytes: Buffer.byteLength(baseBody), mtimeMs: 1 };
1590
+ await write(root, relPath, localBody);
1591
+ const result = await applyOfflineSyncSnapshot({
1592
+ root,
1593
+ baseFiles: [record],
1594
+ snapshot: {
1595
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT, schemaVersion: 1,
1596
+ createdAt: "2026-01-01T00:00:00.000Z", sourceId: "remote", includeTranscripts: true,
1597
+ files: [{ ...record, contentBase64: Buffer.from(baseBody).toString("base64") }],
1598
+ },
1599
+ });
1600
+ assert.equal(await readUtf8(root, relPath), localBody);
1601
+ assert.equal(result.upserted, 0);
1602
+ assert.equal(result.skipped, 1);
1603
+ assert.equal(result.nextBaseFiles.find((file) => file.path === relPath)?.sha256, record.sha256);
1604
+ } finally {
1605
+ await rm(root, { recursive: true, force: true });
1606
+ }
1607
+ });
1608
+
1609
+ test("generation deferral includes added and removed shards but not initial imports", async () => {
1610
+ const { divergedEmbeddingGenerationDeferrals } = await import("./offline-sync-embedding-generation.js");
1611
+ const existing = { path: trueShardRel("a1"), sha256: "base" };
1612
+ const added = { path: trueShardRel("extra"), sha256: "new-local" };
1613
+ assert.deepEqual(divergedEmbeddingGenerationDeferrals({
1614
+ baseFiles: [existing], incomingFiles: [existing], currentFiles: [existing, added],
1615
+ }), [existing.path, added.path].sort());
1616
+ assert.deepEqual(divergedEmbeddingGenerationDeferrals({
1617
+ baseFiles: [existing], incomingFiles: [], currentFiles: [{ ...existing, sha256: "modified" }],
1618
+ }), [existing.path]);
1619
+ assert.deepEqual(divergedEmbeddingGenerationDeferrals({
1620
+ baseFiles: [existing], incomingFiles: [existing], currentFiles: [],
1621
+ }), [existing.path]);
1622
+ assert.deepEqual(divergedEmbeddingGenerationDeferrals({
1623
+ baseFiles: [existing], incomingFiles: [existing, added], currentFiles: [existing, added],
1624
+ }), []);
1625
+ assert.deepEqual(divergedEmbeddingGenerationDeferrals({
1626
+ baseFiles: [], incomingFiles: [existing], currentFiles: [added],
1627
+ }), []);
1628
+ });
1629
+
1630
+ test("removing the last sharded entry propagates an explicit empty generation without a shared base", async () => {
1631
+ const source = await tempDir("remnic-last-shard-source");
1632
+ const receiver = await tempDir("remnic-last-shard-receiver");
1633
+ try {
1634
+ const old = indexFile("openai", "m", { last: { path: "facts/last.md", vector: [1] } });
1635
+ await write(source, trueShardRel("last"), serializeIndex(old));
1636
+ await write(receiver, trueShardRel("last"), serializeIndex(old));
1637
+ const store = new EmbeddingIndexFileStore(
1638
+ path.join(source, "state/embeddings.json"),
1639
+ path.join(source, "state/embeddings"),
1640
+ path.join(source, "state/embedding-fallback-status.json"),
1641
+ );
1642
+ await store.persist(indexFile("openai", "m", {}), { touchedIds: ["last"] });
1643
+ assert.deepEqual(await readGenerationEntries(source), {});
1644
+ const snapshot = await buildOfflineSyncSnapshot({ root: source, sourceId: "source", includeContent: true });
1645
+ await applyOfflineSyncSnapshot({ root: receiver, snapshot });
1646
+ assert.deepEqual(await readGenerationEntries(receiver), {}, "an empty source must clear stale receiver vectors without shared-base evidence");
1647
+ assert.ok(snapshot.files.some((file) => file.path.startsWith("state/embeddings/")), "empty generation must have transport-visible evidence");
1648
+ } finally {
1649
+ await rm(source, { recursive: true, force: true });
1650
+ await rm(receiver, { recursive: true, force: true });
1651
+ }
1652
+ });
1653
+
1654
+ // ---------------------------------------------------------------------------
1655
+ // Codex P1 — cross-process generation mutation lock (writer vs swap races)
1656
+ // ---------------------------------------------------------------------------
1657
+
1658
+ test("a concurrent locked writer during staging survives the sync swap", async () => {
1659
+ const localRoot = await tempDir("remnic-3148-staging-writer");
1660
+ try {
1661
+ const keptBody = serializeIndex(indexFile("openai", "m2", { kept: { path: "p", vector: [0] } }));
1662
+ await write(localRoot, "state/embeddings/shard-0000.json", keptBody);
1663
+ const safeRoot: SafeArchiveRoot = await prepareSafeArchiveRoot(localRoot, "test", "root");
1664
+ const stateDirAbs = path.join(localRoot, "state");
1665
+ const incomingBuffers = new Map<string, Buffer>();
1666
+ incomingBuffers.set(
1667
+ trueShardRel("neu"),
1668
+ Buffer.from(serializeIndex(indexFile("openai", "m2", { neu: { path: "p", vector: [1] } }))),
1669
+ );
1670
+ let writer: Promise<void> | undefined;
1671
+ await applyEmbeddingGenerationTransaction({
1672
+ root: safeRoot,
1673
+ shardDirRel: "state/embeddings",
1674
+ incomingShardPaths: [trueShardRel("neu")],
1675
+ incomingMarker: null,
1676
+ incomingMarkerPresent: false,
1677
+ incomingShardStates: new Map(),
1678
+ incomingBuffers,
1679
+ baseStates: new Map([["state/embeddings/shard-0000.json", { sha256: createHash("sha256").update(keptBody).digest("hex") }]]),
1680
+ io: {
1681
+ // A lock-taking daemon mutation racing the staging window must block
1682
+ // until the swap finished and then land IN the published generation —
1683
+ // never be discarded by the directory swap.
1684
+ writeStagingFile: async ({ filePath, content }) => {
1685
+ await writeFile(filePath, content);
1686
+ if (writer) return;
1687
+ writer = withEmbeddingGenerationLock(stateDirAbs, async () => {
1688
+ await writeFile(
1689
+ path.join(localRoot, "state/embeddings/shard-0099.json"),
1690
+ serializeIndex(indexFile("openai", "m2", { late: { path: "p", vector: [9] } })),
1691
+ );
1692
+ });
1693
+ },
1694
+ readStagingFile: async ({ filePath }) => readFile(filePath),
1695
+ },
1696
+ now: 1,
1697
+ });
1698
+ await Promise.race([
1699
+ writer,
1700
+ new Promise((_, reject) => setTimeout(() => reject(new Error("writer deadlocked on the generation lock")), 15_000)),
1701
+ ]);
1702
+ assert.ok(
1703
+ (await readdir(path.join(localRoot, "state/embeddings"))).includes("shard-0099.json"),
1704
+ "the concurrent locked writer's shard must survive the sync swap",
1705
+ );
1706
+ } finally {
1707
+ await rm(localRoot, { recursive: true, force: true });
1708
+ }
1709
+ });
1710
+
1711
+ test("a streamed generation capture never mixes a concurrent swap (codex P1)", async () => {
1712
+ const root = await tempDir("remnic-3148-stream-swap");
1713
+ try {
1714
+ const old0 = serializeIndex(indexFile("openai", "m1", { old0: { path: "p", vector: [0] } }));
1715
+ const old1 = serializeIndex(indexFile("openai", "m1", { old1: { path: "p", vector: [1] } }));
1716
+ await write(root, "state/embeddings/shard-0000.json", old0);
1717
+ await write(root, "state/embeddings/shard-0001.json", old1);
1718
+ const oldSha = (body: string) => createHash("sha256").update(body).digest("hex");
1719
+ const safeRoot: SafeArchiveRoot = await prepareSafeArchiveRoot(root, "test", "root");
1720
+ const stateDirAbs = path.join(root, "state");
1721
+
1722
+ // A whole-generation swap: kept shard name with NEW bytes plus a
1723
+ // new-only shard — exactly the overlap that used to look like a valid
1724
+ // partial generation.
1725
+ const idForShard = (target: number): string => {
1726
+ for (let k = 0; k < 10_000; k += 1) {
1727
+ if (shardIndexOf(`id${k}`, 64) === target) return `id${k}`;
1728
+ }
1729
+ throw new Error("no id found");
1730
+ };
1731
+ const shardName = (target: number): string => `state/embeddings/shard-${String(target).padStart(4, "0")}.json`;
1732
+ const [id0, id1, id2] = [idForShard(0), idForShard(1), idForShard(2)];
1733
+ const new0 = serializeIndex(indexFile("openai", "m2", { [id0]: { path: "p", vector: [10] } }));
1734
+ const new1 = serializeIndex(indexFile("openai", "m2", { [id1]: { path: "p", vector: [11] } }));
1735
+ const new2 = serializeIndex(indexFile("openai", "m2", { [id2]: { path: "p", vector: [12] } }));
1736
+ const incomingBuffers = new Map<string, Buffer>([
1737
+ [shardName(0), Buffer.from(new0)],
1738
+ [shardName(1), Buffer.from(new1)],
1739
+ [shardName(2), Buffer.from(new2)],
1740
+ ]);
1741
+ let swapPublished = false;
1742
+ let swapError: unknown = undefined;
1743
+ let swap: Promise<void> | undefined;
1744
+ let firstShardRead = false;
1745
+ const records = [];
1746
+ for await (const record of iterateOfflineSyncSnapshotFileRecords({
1747
+ root,
1748
+ includeContent: true,
1749
+ readFile: async ({ filePath }) => {
1750
+ if (!firstShardRead && filePath.endsWith("shard-0000.json")) {
1751
+ firstShardRead = true;
1752
+ // Launch the swap, then hold this read until the swap PUBLISHED
1753
+ // (pre-fix) or its bounded wait expires (post-fix, the capture
1754
+ // holds the lock so the swap cannot publish mid-iteration).
1755
+ swap = applyEmbeddingGenerationTransaction({
1756
+ root: safeRoot,
1757
+ shardDirRel: "state/embeddings",
1758
+ incomingShardPaths: [...incomingBuffers.keys()].sort(),
1759
+ incomingMarker: null,
1760
+ incomingMarkerPresent: false,
1761
+ incomingShardStates: new Map(),
1762
+ incomingBuffers,
1763
+ baseStates: new Map(),
1764
+ io: {},
1765
+ now: 1,
1766
+ }).then(
1767
+ () => {
1768
+ swapPublished = true;
1769
+ },
1770
+ (err) => {
1771
+ swapError = err;
1772
+ swapPublished = true;
1773
+ },
1774
+ );
1775
+ const deadline = Date.now() + 1500;
1776
+ while (!swapPublished && Date.now() < deadline) {
1777
+ await new Promise((resolve) => setTimeout(resolve, 10));
1778
+ }
1779
+ return Buffer.from(old0);
1780
+ }
1781
+ return readFile(filePath);
1782
+ },
1783
+ })) {
1784
+ records.push(record);
1785
+ }
1786
+ await Promise.race([
1787
+ swap,
1788
+ new Promise((_, reject) => setTimeout(() => reject(new Error("swap deadlocked on the generation lock")), 15_000)),
1789
+ ]);
1790
+ assert.equal(swapError, undefined);
1791
+
1792
+ // The captured census must be EXACTLY the old complete generation —
1793
+ // never a mix of the old and swapped generations.
1794
+ const captured = new Map(
1795
+ records
1796
+ .filter((record) => record.path.startsWith("state/embeddings/"))
1797
+ .map((record) => [record.path, record.sha256] as const),
1798
+ );
1799
+ assert.deepEqual([...captured.keys()].sort(), [
1800
+ "state/embeddings/shard-0000.json",
1801
+ "state/embeddings/shard-0001.json",
1802
+ ]);
1803
+ assert.equal(captured.get("state/embeddings/shard-0000.json"), oldSha(old0));
1804
+ assert.equal(captured.get("state/embeddings/shard-0001.json"), oldSha(old1));
1805
+ void swapError;
1806
+ } finally {
1807
+ await rm(root, { recursive: true, force: true });
1808
+ }
1809
+ });
1810
+
1811
+ test("a local divergence discovered under the lock defers the whole generation", async () => {
1812
+ const localRoot = await tempDir("remnic-3148-stale-census-divergence");
1813
+ try {
1814
+ const baseBody = serializeIndex(indexFile("openai", "m2", { base: { path: "p", vector: [0] } }));
1815
+ await write(localRoot, "state/embeddings/shard-0000.json", baseBody);
1816
+ const safeRoot: SafeArchiveRoot = await prepareSafeArchiveRoot(localRoot, "test", "root");
1817
+ const divergedBody = serializeIndex(indexFile("openai", "m2", { daemon: { path: "p", vector: [7] } }));
1818
+ // The daemon indexed after the early census: disk now diverges from the
1819
+ // shared base, while the incoming record still equals the base.
1820
+ await writeFile(path.join(localRoot, "state/embeddings/shard-0000.json"), divergedBody);
1821
+ const incomingBuffers = new Map<string, Buffer>();
1822
+ incomingBuffers.set(trueShardRel("neu"), Buffer.from(serializeIndex(indexFile("openai", "m2", { neu: { path: "p", vector: [1] } }))));
1823
+ const result = await applyEmbeddingGenerationTransaction({
1824
+ root: safeRoot,
1825
+ shardDirRel: "state/embeddings",
1826
+ incomingShardPaths: [trueShardRel("neu"), "state/embeddings/shard-0000.json"],
1827
+ incomingMarker: null,
1828
+ incomingMarkerPresent: false,
1829
+ incomingShardStates: new Map([
1830
+ [
1831
+ "state/embeddings/shard-0000.json",
1832
+ { path: "state/embeddings/shard-0000.json", sha256: createHash("sha256").update(baseBody).digest("hex"), bytes: Buffer.byteLength(baseBody), mtimeMs: 1 },
1833
+ ],
1834
+ ]),
1835
+ incomingBuffers,
1836
+ baseStates: new Map([["state/embeddings/shard-0000.json", { sha256: createHash("sha256").update(baseBody).digest("hex") }]]),
1837
+ io: {},
1838
+ now: 1,
1839
+ });
1840
+ assert.equal(result.deferredLocalDivergence, true, "the generation must defer, not publish over local work");
1841
+ assert.equal(result.upserted, 0);
1842
+ assert.ok(result.handledPaths.has(trueShardRel("neu")));
1843
+ assert.ok(result.handledPaths.has("state/embeddings/shard-0000.json"));
1844
+ assert.equal(
1845
+ await readUtf8(localRoot, "state/embeddings/shard-0000.json"),
1846
+ divergedBody,
1847
+ "the daemon's diverged bytes must be preserved",
1848
+ );
1849
+ const stateDir = await readdir(path.join(localRoot, "state"));
1850
+ assert.equal(
1851
+ stateDir.some((name) => name.startsWith("embeddings.staging.tmp-")),
1852
+ false,
1853
+ );
1854
+ } finally {
1855
+ await rm(localRoot, { recursive: true, force: true });
1856
+ }
1857
+ });
1858
+
1859
+ // ---------------------------------------------------------------------------
1860
+ // Codex P1 round 2 — fresh under-lock census (contentful incoming, added
1861
+ // shards, whole-generation deletion)
1862
+ // ---------------------------------------------------------------------------
1863
+
1864
+ test("contentful incoming over a stale census defers instead of discarding the daemon mutation", async () => {
1865
+ const localRoot = await tempDir("remnic-3148-contentful-stale");
1866
+ try {
1867
+ const baseBody = serializeIndex(indexFile("openai", "m2", { base: { path: "p", vector: [0] } }));
1868
+ await write(localRoot, "state/embeddings/shard-0000.json", baseBody);
1869
+ const safeRoot: SafeArchiveRoot = await prepareSafeArchiveRoot(localRoot, "test", "root");
1870
+ // The daemon indexed AFTER the census: disk diverges from the shared
1871
+ // base, while the incoming record STILL equals the base (contentful).
1872
+ const divergedBody = serializeIndex(indexFile("openai", "m2", { daemon: { path: "p", vector: [7] } }));
1873
+ await writeFile(path.join(localRoot, "state/embeddings/shard-0000.json"), divergedBody);
1874
+ const incomingBuffers = new Map<string, Buffer>();
1875
+ incomingBuffers.set(
1876
+ "state/embeddings/shard-0000.json",
1877
+ Buffer.from(baseBody),
1878
+ );
1879
+ incomingBuffers.set(
1880
+ "state/embeddings/shard-0001.json",
1881
+ Buffer.from(serializeIndex(indexFile("openai", "m2", { fresh: { path: "p", vector: [1] } }))),
1882
+ );
1883
+ const result = await applyEmbeddingGenerationTransaction({
1884
+ root: safeRoot,
1885
+ shardDirRel: "state/embeddings",
1886
+ incomingShardPaths: ["state/embeddings/shard-0000.json", "state/embeddings/shard-0001.json"],
1887
+ incomingMarker: null,
1888
+ incomingMarkerPresent: false,
1889
+ incomingShardStates: new Map([
1890
+ ["state/embeddings/shard-0000.json", { path: "state/embeddings/shard-0000.json", sha256: createHash("sha256").update(baseBody).digest("hex"), bytes: Buffer.byteLength(baseBody), mtimeMs: 1 }],
1891
+ ["state/embeddings/shard-0001.json", { path: "state/embeddings/shard-0001.json", sha256: createHash("sha256").update(incomingBuffers.get("state/embeddings/shard-0001.json")!).digest("hex"), bytes: incomingBuffers.get("state/embeddings/shard-0001.json")!.byteLength, mtimeMs: 1 }],
1892
+ ]),
1893
+ incomingBuffers,
1894
+ baseStates: new Map([["state/embeddings/shard-0000.json", { sha256: createHash("sha256").update(baseBody).digest("hex") }]]),
1895
+ io: {},
1896
+ now: 1,
1897
+ });
1898
+ assert.equal(result.deferredLocalDivergence, true, "contentful incoming must defer on under-lock divergence too");
1899
+ assert.equal(result.upserted, 0);
1900
+ assert.equal(
1901
+ await readUtf8(localRoot, "state/embeddings/shard-0000.json"),
1902
+ divergedBody,
1903
+ "the daemon's diverged bytes must be preserved",
1904
+ );
1905
+ assert.equal(await existsQuiet(localRoot, "state/embeddings/shard-0001.json"), false, "nothing may be published for a deferred generation");
1906
+ } finally {
1907
+ await rm(localRoot, { recursive: true, force: true });
1908
+ }
1909
+ });
1910
+
1911
+ test("a daemon-only added shard defers the whole generation under the lock", async () => {
1912
+ const localRoot = await tempDir("remnic-3148-added-shard");
1913
+ try {
1914
+ const baseBody = serializeIndex(indexFile("openai", "m2", { base: { path: "p", vector: [0] } }));
1915
+ await write(localRoot, "state/embeddings/shard-0000.json", baseBody);
1916
+ const addedBody = serializeIndex(indexFile("openai", "m2", { added: { path: "p", vector: [8] } }));
1917
+ await write(localRoot, "state/embeddings/shard-0050.json", addedBody);
1918
+ const safeRoot: SafeArchiveRoot = await prepareSafeArchiveRoot(localRoot, "test", "root");
1919
+ const incomingBuffers = new Map<string, Buffer>();
1920
+ incomingBuffers.set(
1921
+ "state/embeddings/shard-0000.json",
1922
+ Buffer.from(serializeIndex(indexFile("openai", "m2", { moved: { path: "p", vector: [3] } }))),
1923
+ );
1924
+ const result = await applyEmbeddingGenerationTransaction({
1925
+ root: safeRoot,
1926
+ shardDirRel: "state/embeddings",
1927
+ incomingShardPaths: ["state/embeddings/shard-0000.json"],
1928
+ incomingMarker: null,
1929
+ incomingMarkerPresent: false,
1930
+ incomingShardStates: new Map(),
1931
+ incomingBuffers,
1932
+ baseStates: new Map([["state/embeddings/shard-0000.json", { sha256: createHash("sha256").update(baseBody).digest("hex") }]]),
1933
+ io: {},
1934
+ now: 1,
1935
+ });
1936
+ assert.equal(result.deferredLocalDivergence, true, "a locally added shard must defer the generation");
1937
+ assert.equal(
1938
+ await readUtf8(localRoot, "state/embeddings/shard-0050.json"),
1939
+ addedBody,
1940
+ "the daemon-added shard must survive the deferred swap",
1941
+ );
1942
+ } finally {
1943
+ await rm(localRoot, { recursive: true, force: true });
1944
+ }
1945
+ });
1946
+
1947
+ test("a whole remote deletion defers when the live generation diverged from the base", async () => {
1948
+ const localRoot = await tempDir("remnic-3148-removal-divergence");
1949
+ try {
1950
+ const base0 = serializeIndex(indexFile("openai", "m2", { base0: { path: "p", vector: [0] } }));
1951
+ const base1 = serializeIndex(indexFile("openai", "m2", { base1: { path: "p", vector: [1] } }));
1952
+ await write(localRoot, "state/embeddings/shard-0000.json", base0);
1953
+ await write(localRoot, "state/embeddings/shard-0001.json", base1);
1954
+ const safeRoot: SafeArchiveRoot = await prepareSafeArchiveRoot(localRoot, "test", "root");
1955
+ // The daemon re-indexed shard-0000 after the census.
1956
+ const diverged0 = serializeIndex(indexFile("openai", "m2", { daemon0: { path: "p", vector: [7] } }));
1957
+ await writeFile(path.join(localRoot, "state/embeddings/shard-0000.json"), diverged0);
1958
+ const result = await applyEmbeddingGenerationRemoval({
1959
+ root: safeRoot,
1960
+ shardDirRel: "state/embeddings",
1961
+ io: { deleteFile: async () => {} },
1962
+ deletionMtimeByPath: new Map([
1963
+ ["state/embeddings/shard-0000.json", 2],
1964
+ ["state/embeddings/shard-0001.json", 2],
1965
+ ]),
1966
+ baseStates: new Map([
1967
+ ["state/embeddings/shard-0000.json", { sha256: createHash("sha256").update(base0).digest("hex") }],
1968
+ ["state/embeddings/shard-0001.json", { sha256: createHash("sha256").update(base1).digest("hex") }],
1969
+ ]),
1970
+ now: 1,
1971
+ });
1972
+ assert.equal(result.deferredLocalDivergence, true, "the removal must defer on under-lock divergence");
1973
+ assert.equal(result.deleted, 0);
1974
+ assert.equal(await readUtf8(localRoot, "state/embeddings/shard-0000.json"), diverged0);
1975
+ assert.equal(await readUtf8(localRoot, "state/embeddings/shard-0001.json"), base1);
1976
+ } finally {
1977
+ await rm(localRoot, { recursive: true, force: true });
1978
+ }
1979
+ });
1980
+
1981
+ test("encrypted generation removal compares plaintext census against the shared base", async () => {
1982
+ const root = await tempDir("remnic-3148-encrypted-removal");
1983
+ try {
1984
+ const storage = new StorageManager(root);
1985
+ await storage.setSecureStoreKeyAndWait(randomBytes(32), true);
1986
+ const relPath = "state/embeddings/shard-0000.json";
1987
+ const filePath = path.join(root, relPath);
1988
+ const content = Buffer.from(serializeIndex(indexFile("openai", "m", {})));
1989
+ await storage.writeOfflineSyncStagingFile(filePath, content, { aadFilePath: filePath });
1990
+ assert.notDeepEqual(await readFile(filePath), content, "fixture is encrypted at rest");
1991
+ const sha256 = createHash("sha256").update(content).digest("hex");
1992
+ const snapshot: OfflineSyncSnapshot = {
1993
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT, schemaVersion: 1,
1994
+ createdAt: "2026-09-27T00:00:00.000Z", sourceId: "remote",
1995
+ includeTranscripts: true,
1996
+ files: [{ path: "facts/other.md", sha256: createHash("sha256").update("fact").digest("hex"), bytes: 4, mtimeMs: 1, contentBase64: Buffer.from("fact").toString("base64") }],
1997
+ deletions: [{ path: relPath, mtimeMs: 1 }],
1998
+ };
1999
+ await write(root, "facts/other.md", "fact");
2000
+ const result = await applyOfflineSyncSnapshot({
2001
+ root, snapshot, baseFiles: [
2002
+ { path: relPath, sha256, bytes: content.length, mtimeMs: 1 },
2003
+ { path: "facts/other.md", sha256: createHash("sha256").update("fact").digest("hex"), bytes: 4, mtimeMs: 1 },
2004
+ ],
2005
+ readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
2006
+ deleteFile: async () => {},
2007
+ });
2008
+ assert.equal(result.deleted, 1, "unchanged encrypted generation should be removed");
2009
+ assert.deepEqual(await readdir(path.join(root, "state/embeddings")), []);
2010
+ } finally {
2011
+ await rm(root, { recursive: true, force: true });
2012
+ }
2013
+ });
2014
+
2015
+ test("late whole-generation deferral retains a locally deleted base-only shard", async () => {
2016
+ const root = await tempDir("remnic-3148-late-base-only");
2017
+ try {
2018
+ const member0 = "state/embeddings/shard-0000.json";
2019
+ const member1 = "state/embeddings/shard-0001.json";
2020
+ const body = serializeIndex(indexFile("openai", "m", {}));
2021
+ await write(root, member0, body);
2022
+ const baseFiles = [member0, member1].map((member) => ({
2023
+ path: member, sha256: createHash("sha256").update(body).digest("hex"),
2024
+ bytes: Buffer.byteLength(body), mtimeMs: 1,
2025
+ }));
2026
+ const snapshot: OfflineSyncSnapshot = {
2027
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT, schemaVersion: 1,
2028
+ createdAt: "2026-09-27T00:00:00.000Z", sourceId: "remote",
2029
+ includeTranscripts: true,
2030
+ files: [{ ...baseFiles[0], contentBase64: Buffer.from(body).toString("base64") }],
2031
+ };
2032
+ // The initial census still saw both shards, but a daemon deleted member1
2033
+ // before the transaction took the generation lock.
2034
+ const result = await applyOfflineSyncSnapshot({ root, snapshot, baseFiles, currentFiles: baseFiles });
2035
+ assert.ok(result.nextBaseFiles.some((file) => file.path === member1), "the deferred base-only member must not disappear from the shared base");
2036
+ assert.equal(result.deleted, 0);
2037
+ assert.equal(await readUtf8(root, member0), body);
2038
+ } finally {
2039
+ await rm(root, { recursive: true, force: true });
2040
+ }
2041
+ });
2042
+
2043
+ test("late whole-generation removal deferral keeps base when no handled paths survive", async () => {
2044
+ const root = await tempDir("remnic-3148-empty-generation-deferral");
2045
+ try {
2046
+ const relPath = "state/embeddings/shard-0000.json";
2047
+ const body = serializeIndex(indexFile("openai", "m", {}));
2048
+ const baseFiles = [{ path: relPath, sha256: createHash("sha256").update(body).digest("hex"), bytes: Buffer.byteLength(body), mtimeMs: 1 }];
2049
+ const snapshot: OfflineSyncSnapshot = {
2050
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT, schemaVersion: 1,
2051
+ createdAt: "2026-09-27T00:00:00.000Z", sourceId: "remote",
2052
+ includeTranscripts: true, files: [], deletions: [{ path: relPath, mtimeMs: 1 }],
2053
+ };
2054
+ // The caller's census is stale: the last member was deleted locally
2055
+ // before the transaction acquired the generation lock.
2056
+ const result = await applyOfflineSyncSnapshot({ root, snapshot, baseFiles, currentFiles: baseFiles });
2057
+ assert.ok(result.nextBaseFiles.some((file) => file.path === relPath), "local deletion must remain in the shared base for the next sync");
2058
+ assert.equal(result.deleted, 0);
2059
+ } finally {
2060
+ await rm(root, { recursive: true, force: true });
2061
+ }
2062
+ });
2063
+
2064
+ test("streamed omission header fails closed when an excluded shard appears after header construction", async () => {
2065
+ const root = await tempDir("remnic-3148-omission-race");
2066
+ try {
2067
+ await write(root, "state/embeddings/shard-0000.json", serializeIndex(indexFile("openai", "m", {})));
2068
+ const { buildEmbeddingAwareSnapshotStream } = await import("./offline-sync-snapshot-stream.js");
2069
+ const build = await buildEmbeddingAwareSnapshotStream({
2070
+ root, namespace: "test", includeContent: true, includeTranscripts: true,
2071
+ readFile: async ({ filePath }) => readFile(filePath),
2072
+ readFileDigest: async ({ filePath }) => {
2073
+ const bytes = await readFile(filePath);
2074
+ return { sha256: createHash("sha256").update(bytes).digest("hex"), bytes: bytes.length };
2075
+ },
2076
+ excludeFile: async () => false,
2077
+ userExcludeRegexps: [/shard-0003[.]json$/], deletions: [],
2078
+ });
2079
+ assert.equal(build.omittedEmbeddingGenerationDirs, undefined);
2080
+ await write(root, "state/embeddings/shard-0003.json", serializeIndex(indexFile("openai", "m", {})));
2081
+ await assert.rejects(async () => { for await (const _file of build.files) { /* consume */ } }, /became partially excluded/);
2082
+ } finally {
2083
+ await rm(root, { recursive: true, force: true });
2084
+ }
2085
+ });
2086
+
2087
+ test("post-publish marker cleanup failure preserves committed base and retries", async () => {
2088
+ const root = await tempDir("remnic-3148-marker-retry");
2089
+ const remote = await tempDir("remnic-3148-marker-retry-remote");
2090
+ try {
2091
+ const marker = "state/embeddings.json";
2092
+ const shard = "state/embeddings/shard-0000.json";
2093
+ await write(root, marker, serializeIndex(indexFile("openai", "m", {})));
2094
+ await write(remote, shard, serializeIndex(indexFile("openai", "m", {})));
2095
+ const base = (await buildOfflineSyncSnapshot({ root, sourceId: "local", includeContent: false })).files;
2096
+ const snapshot = await buildOfflineSyncSnapshot({ root: remote, sourceId: "remote", includeContent: true });
2097
+ let fail = true;
2098
+ const io = {
2099
+ writeStagingFile: async ({ filePath, content }: { filePath: string; content: Buffer }) => writeFile(filePath, content),
2100
+ readStagingFile: async ({ filePath }: { filePath: string }) => readFile(filePath),
2101
+ deleteFile: async ({ filePath }: { filePath: string }) => {
2102
+ if (fail) throw new Error("transient delete failure");
2103
+ await rm(filePath, { force: true });
2104
+ },
2105
+ };
2106
+ const first = await applyOfflineSyncSnapshot({ root, snapshot, baseFiles: base, ...io });
2107
+ assert.ok(first.nextBaseFiles.some((file) => file.path === shard), "committed shard must enter shared base");
2108
+ assert.ok(first.nextBaseFiles.some((file) => file.path === marker), "failed marker cleanup must remain retryable");
2109
+ fail = false;
2110
+ const retry = await applyOfflineSyncSnapshot({ root, snapshot, baseFiles: first.nextBaseFiles, ...io });
2111
+ assert.equal(await existsQuiet(root, marker), false);
2112
+ assert.equal(retry.nextBaseFiles.some((file) => file.path === marker), false);
2113
+ assert.ok(retry.nextBaseFiles.some((file) => file.path === shard));
2114
+ } finally {
2115
+ await rm(root, { recursive: true, force: true });
2116
+ await rm(remote, { recursive: true, force: true });
2117
+ }
2118
+ });
2119
+
2120
+ test("a slow stream consumer does not hold the writer lock across records", async () => {
2121
+ const root = await tempDir("remnic-3148-slow-stream");
2122
+ try {
2123
+ const shard0 = "state/embeddings/shard-0000.json";
2124
+ const shard1 = "state/embeddings/shard-0001.json";
2125
+ await write(root, shard0, serializeIndex(indexFile("openai", "m", {})));
2126
+ await write(root, shard1, serializeIndex(indexFile("openai", "m", {})));
2127
+ const records = iterateOfflineSyncSnapshotFileRecords({ root, includeContent: true })[Symbol.asyncIterator]();
2128
+ const first = await records.next();
2129
+ assert.equal(first.value?.path, shard0);
2130
+ let timer: ReturnType<typeof setTimeout> | undefined;
2131
+ try {
2132
+ await Promise.race([
2133
+ withEmbeddingGenerationLock(path.join(root, "state"), async () => {
2134
+ await writeFile(path.join(root, shard1), serializeIndex(indexFile("openai", "m2", {})));
2135
+ }),
2136
+ new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error("writer blocked by slow network consumer")), 1000); }),
2137
+ ]);
2138
+ } finally {
2139
+ if (timer) clearTimeout(timer);
2140
+ }
2141
+ await assert.rejects(() => records.next(), /generation changed while streaming/);
2142
+ await records.return?.(undefined);
2143
+ } finally {
2144
+ await rm(root, { recursive: true, force: true });
2145
+ }
2146
+ });
2147
+
2148
+ test("an omitted generation remains omitted when a new shard appears after the header", async () => {
2149
+ const root = await tempDir("remnic-3148-omitted-late-shard");
2150
+ try {
2151
+ const body = serializeIndex(indexFile("openai", "m", {}));
2152
+ await write(root, "state/embeddings/shard-0000.json", body);
2153
+ await write(root, "state/embeddings/shard-0003.json", body);
2154
+ const { buildEmbeddingAwareSnapshotStream } = await import("./offline-sync-snapshot-stream.js");
2155
+ const build = await buildEmbeddingAwareSnapshotStream({
2156
+ root, namespace: "test", includeContent: true, includeTranscripts: true,
2157
+ readFile: async ({ filePath }) => readFile(filePath),
2158
+ readFileDigest: async ({ filePath }) => {
2159
+ const content = await readFile(filePath);
2160
+ return { sha256: createHash("sha256").update(content).digest("hex"), bytes: content.length };
2161
+ },
2162
+ excludeFile: async () => false,
2163
+ userExcludeRegexps: [/shard-0003[.]json$/], deletions: [],
2164
+ });
2165
+ assert.deepEqual(build.omittedEmbeddingGenerationDirs, ["state/embeddings"]);
2166
+ await write(root, "state/embeddings/shard-0004.json", body);
2167
+ const files = [];
2168
+ for await (const file of build.files) files.push(file.path);
2169
+ assert.deepEqual(files, [], "no late shard may leak into an omitted generation");
2170
+ } finally {
2171
+ await rm(root, { recursive: true, force: true });
2172
+ }
2173
+ });