@remnic/core 9.69.87 → 9.69.88

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 (414) hide show
  1. package/dist/access-admin-ops-surface.js +61 -55
  2. package/dist/access-authorization-probe.js +62 -56
  3. package/dist/access-boundary.js +61 -55
  4. package/dist/access-citation-namespace.js +2 -2
  5. package/dist/access-cli.js +140 -134
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-extraction-force-flush.js +61 -55
  8. package/dist/access-http-briefing.js +62 -56
  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 +63 -57
  12. package/dist/access-http.js +85 -79
  13. package/dist/access-identity-continuity-surface.js +61 -55
  14. package/dist/access-lcm-surface.js +61 -55
  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 +74 -68
  18. package/dist/access-namespace-preflight.js +61 -55
  19. package/dist/access-observe-write-category.js +19 -13
  20. package/dist/access-observe-write-surface.js +61 -55
  21. package/dist/access-offline-manifest.js +15 -10
  22. package/dist/access-operations-batch.js +65 -59
  23. package/dist/access-operations.js +72 -66
  24. package/dist/access-recall-concurrency.js +61 -55
  25. package/dist/access-recall-response.js +61 -55
  26. package/dist/access-recall-surface.js +61 -55
  27. package/dist/access-schema.js +15 -7
  28. package/dist/access-service.d.ts.map +1 -1
  29. package/dist/access-service.js +61 -55
  30. package/dist/access-surface-catalog.js +63 -57
  31. package/dist/access-surface-catalog.js.map +1 -1
  32. package/dist/active-recall.js +27 -22
  33. package/dist/active-recall.js.map +1 -1
  34. package/dist/adapters/index.js +4 -4
  35. package/dist/adapters/registry.js +2 -2
  36. package/dist/{auto-sync-6IPX2BAC.js → auto-sync-U5N3CB5Y.js} +7 -7
  37. package/dist/briefing.js +15 -15
  38. package/dist/buffer.js +7 -7
  39. package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.js} +2 -2
  40. package/dist/{capsule-merge-3MNURAQ3.js → capsule-merge-ZCXUPLNJ.js} +2 -2
  41. package/dist/causal-behavior.js +2 -2
  42. package/dist/causal-chain.js +2 -2
  43. package/dist/causal-consolidation.js +18 -18
  44. package/dist/causal-retrieval.js +2 -2
  45. package/dist/causal-trajectory-graph.js +2 -2
  46. package/dist/causal-trajectory.js +1 -1
  47. package/dist/{chunk-JE334SIA.js → chunk-22DYJLFN.js} +2 -2
  48. package/dist/{chunk-JGSQ6P4D.js → chunk-2DLWLNO4.js} +3 -3
  49. package/dist/{chunk-LS3XH3UE.js → chunk-2NAD4YCI.js} +2 -2
  50. package/dist/{chunk-SY5KDWGU.js → chunk-2WLMWP2T.js} +2 -2
  51. package/dist/{chunk-P6WFK4ZJ.js → chunk-3O5BB45T.js} +4 -4
  52. package/dist/{chunk-SOZUJCME.js → chunk-3SGTV7RD.js} +6 -6
  53. package/dist/{chunk-YITN5WXI.js → chunk-4KN2JLQV.js} +2 -2
  54. package/dist/{chunk-JKMHZF3Q.js → chunk-4MP7XR5J.js} +2 -2
  55. package/dist/chunk-4NSLQ7EU.js +137 -0
  56. package/dist/chunk-4NSLQ7EU.js.map +1 -0
  57. package/dist/{chunk-7DE2NOON.js → chunk-4O4XCBOS.js} +3 -3
  58. package/dist/{chunk-SLEAJ67D.js → chunk-4OQET73E.js} +4 -4
  59. package/dist/{chunk-GDYU6VF4.js → chunk-5AYUHSAU.js} +2 -2
  60. package/dist/{chunk-QWW2JDDX.js → chunk-5E6RYRYW.js} +6 -6
  61. package/dist/{chunk-ICZQG4XE.js → chunk-5FAGCOZD.js} +4 -4
  62. package/dist/{chunk-4IEOGIPB.js → chunk-5MNUBT7T.js} +2 -2
  63. package/dist/{chunk-CK5KOVHW.js → chunk-5R627MYY.js} +3 -2
  64. package/dist/{chunk-CK5KOVHW.js.map → chunk-5R627MYY.js.map} +1 -1
  65. package/dist/chunk-5X4MXNEU.js +64 -0
  66. package/dist/chunk-5X4MXNEU.js.map +1 -0
  67. package/dist/{chunk-52II7PB6.js → chunk-645WOU5I.js} +4 -4
  68. package/dist/{chunk-DUJYRVUD.js → chunk-6O2VUWV6.js} +3 -3
  69. package/dist/{chunk-CS52MFDV.js → chunk-6UQFJULG.js} +2 -2
  70. package/dist/chunk-75PT4N7Q.js +75 -0
  71. package/dist/chunk-75PT4N7Q.js.map +1 -0
  72. package/dist/{chunk-HESDNYRB.js → chunk-76M5OK7V.js} +2 -2
  73. package/dist/{chunk-4CDYONWO.js → chunk-A4VSOUCB.js} +3 -3
  74. package/dist/{chunk-HBBHL4YF.js → chunk-AE5U5NMS.js} +2 -2
  75. package/dist/{chunk-CUCWX3MO.js → chunk-AEHVZY3X.js} +2 -2
  76. package/dist/{chunk-DZ525SNQ.js → chunk-AEU4EQGV.js} +1 -1
  77. package/dist/{chunk-VLLD4TOB.js → chunk-AWG3TIAY.js} +2 -2
  78. package/dist/{chunk-72JNNJJJ.js → chunk-AXC4WSF3.js} +3 -3
  79. package/dist/{chunk-N4S4NUX5.js → chunk-B3O3IRZN.js} +1 -1
  80. package/dist/{chunk-FAN73COB.js → chunk-BGI4NUAO.js} +2 -2
  81. package/dist/{chunk-XTYX6V52.js → chunk-BJVO4H5C.js} +2 -2
  82. package/dist/{chunk-EHS5MJEF.js → chunk-BLVR4WYU.js} +3 -3
  83. package/dist/{chunk-KFCMMDHH.js → chunk-BOZJD5PK.js} +2 -2
  84. package/dist/{chunk-TM3RXL4G.js → chunk-BUQKSQXL.js} +2 -2
  85. package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
  86. package/dist/{chunk-EVBI7S4J.js → chunk-C6U57KG4.js} +4 -4
  87. package/dist/{chunk-RB776UH3.js → chunk-C7V3KPKJ.js} +235 -93
  88. package/dist/chunk-C7V3KPKJ.js.map +1 -0
  89. package/dist/{chunk-VX6GCKBS.js → chunk-C7ZDKWPS.js} +4 -4
  90. package/dist/{chunk-ZI7QA4MK.js → chunk-CRXRZA3G.js} +2 -2
  91. package/dist/{chunk-VQNONC4E.js → chunk-CZJA2EQE.js} +2 -2
  92. package/dist/{chunk-M536WNTQ.js → chunk-D5BSQR63.js} +2 -2
  93. package/dist/{chunk-37S3IMYB.js → chunk-DGS34QSY.js} +2 -2
  94. package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
  95. package/dist/{chunk-3BTFBQLO.js → chunk-EFHNFTWB.js} +49 -3
  96. package/dist/chunk-EFHNFTWB.js.map +1 -0
  97. package/dist/{chunk-C3QXITIP.js → chunk-EQUD7FP4.js} +2 -2
  98. package/dist/chunk-ETUZFFPU.js +218 -0
  99. package/dist/chunk-ETUZFFPU.js.map +1 -0
  100. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  101. package/dist/{chunk-EQ6YOIKO.js → chunk-FJLQXMTU.js} +3 -3
  102. package/dist/{chunk-SRZW7FNU.js → chunk-FKD656D2.js} +2 -2
  103. package/dist/{chunk-CLXTDOKV.js → chunk-FUNBJQYR.js} +2 -2
  104. package/dist/{chunk-CLSYE5OZ.js → chunk-G2STTRYB.js} +2 -2
  105. package/dist/{chunk-KP7SANCF.js → chunk-GWM6T5EQ.js} +2 -2
  106. package/dist/{chunk-UA5DX5C3.js → chunk-GZDTVYK2.js} +2 -2
  107. package/dist/{chunk-3GQC3NMB.js → chunk-H56VY4BY.js} +5 -5
  108. package/dist/{chunk-L6UZEAID.js → chunk-H5BJWPUI.js} +4 -4
  109. package/dist/{chunk-YSJTCOHR.js → chunk-HLB7GKGG.js} +2 -2
  110. package/dist/{chunk-PUKTBULQ.js → chunk-HWLIMQE2.js} +2 -2
  111. package/dist/{chunk-CWHV574S.js → chunk-IOSZHHJC.js} +6 -6
  112. package/dist/{chunk-P2WZHLUF.js → chunk-JVPWRNHA.js} +2 -2
  113. package/dist/{chunk-6ET2VII7.js → chunk-JYG2334B.js} +2 -2
  114. package/dist/chunk-K6T33R4Q.js +535 -0
  115. package/dist/chunk-K6T33R4Q.js.map +1 -0
  116. package/dist/{chunk-WIKO4SCR.js → chunk-LHRQBMJN.js} +2 -2
  117. package/dist/{chunk-K4J4E7WV.js → chunk-LO4UKPKX.js} +2 -2
  118. package/dist/{chunk-LF5IGOOD.js → chunk-LU7E2MXE.js} +17 -17
  119. package/dist/{chunk-RJLV4BKR.js → chunk-LV5H6FTN.js} +2 -2
  120. package/dist/{chunk-3C3J243X.js → chunk-LW3HKRL7.js} +3 -3
  121. package/dist/{chunk-4BZP2B4U.js → chunk-M6C7SOR6.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-SPSRGEDQ.js → chunk-MXCAJKMA.js} +9 -9
  127. package/dist/{chunk-BMYZTZ5D.js → chunk-MY7B3HYC.js} +2 -2
  128. package/dist/{chunk-NB3LRY4T.js → chunk-N4PYB235.js} +2 -2
  129. package/dist/{chunk-S3YM4SZQ.js → chunk-N7QV3CMX.js} +2 -2
  130. package/dist/{chunk-AQK54Q7O.js → chunk-NYKIUQL6.js} +2 -2
  131. package/dist/{chunk-WQZZDN5M.js → chunk-OCFXVDBZ.js} +322 -376
  132. package/dist/chunk-OCFXVDBZ.js.map +1 -0
  133. package/dist/{chunk-NE6PHPLG.js → chunk-OK52IY3W.js} +10 -10
  134. package/dist/{chunk-6TT6HRE4.js → chunk-P2ODTYOF.js} +2 -2
  135. package/dist/{chunk-W4AOR52H.js → chunk-PEBHPCX2.js} +2 -2
  136. package/dist/{chunk-VYCOJKSX.js → chunk-PO7NXNGC.js} +2 -2
  137. package/dist/{chunk-G6SVZWPB.js → chunk-PZX3AQLF.js} +44 -1
  138. package/dist/chunk-PZX3AQLF.js.map +1 -0
  139. package/dist/{chunk-XAITGGX5.js → chunk-Q4AUIFUA.js} +4 -4
  140. package/dist/{chunk-MQJL6CDZ.js → chunk-QAW55ZHJ.js} +4 -4
  141. package/dist/{chunk-FOH7AMMI.js → chunk-QB57WIS6.js} +73 -82
  142. package/dist/chunk-QB57WIS6.js.map +1 -0
  143. package/dist/{chunk-QVS22WTO.js → chunk-QI7K7BZ5.js} +2 -2
  144. package/dist/{chunk-NE7I2RCT.js → chunk-R3X2T2LP.js} +2 -2
  145. package/dist/{chunk-S7GCT7NU.js → chunk-RBITPCMK.js} +8 -8
  146. package/dist/{chunk-YTQGGO3K.js → chunk-S2GE2K36.js} +81 -78
  147. package/dist/{chunk-YTQGGO3K.js.map → chunk-S2GE2K36.js.map} +1 -1
  148. package/dist/{chunk-BJRAELDH.js → chunk-SCXURCVT.js} +3 -3
  149. package/dist/{chunk-FRCESVV6.js → chunk-STEOT4DI.js} +19 -19
  150. package/dist/chunk-STEOT4DI.js.map +1 -0
  151. package/dist/{chunk-BFO3IR56.js → chunk-SWBC3UER.js} +2 -2
  152. package/dist/chunk-TYT4TEQV.js +45 -0
  153. package/dist/chunk-TYT4TEQV.js.map +1 -0
  154. package/dist/{chunk-YRLHEEQT.js → chunk-U6VC6RP3.js} +2 -2
  155. package/dist/chunk-UCPGOUDZ.js +726 -0
  156. package/dist/chunk-UCPGOUDZ.js.map +1 -0
  157. package/dist/{chunk-MOQPEFLN.js → chunk-UP75KL3L.js} +2 -2
  158. package/dist/{chunk-D5F7TKMH.js → chunk-V4JZCTMK.js} +2 -2
  159. package/dist/{chunk-QEV26L2F.js → chunk-VALKBYBN.js} +2 -2
  160. package/dist/{chunk-7LCGFUMB.js → chunk-VDX2J7OX.js} +4 -4
  161. package/dist/{chunk-TDYAIXHK.js → chunk-VVX5PZFS.js} +5 -5
  162. package/dist/{chunk-6NQ4LSMS.js → chunk-W3XXFPRI.js} +52 -52
  163. package/dist/{chunk-4IIMMZZK.js → chunk-WKY7N3L7.js} +10 -10
  164. package/dist/{chunk-VVDWZRZG.js → chunk-X5SHE2O3.js} +3 -3
  165. package/dist/{chunk-NR2ASHBL.js → chunk-XOV7NRY5.js} +2 -2
  166. package/dist/{chunk-6L3PBSAR.js → chunk-YJ7MW627.js} +2 -2
  167. package/dist/{chunk-LQG5O5EP.js → chunk-Z3O53WZR.js} +2 -2
  168. package/dist/{chunk-FB64XE3R.js → chunk-Z5LP6ILP.js} +2 -2
  169. package/dist/{chunk-VKQI47DJ.js → chunk-Z6EZCN6Z.js} +8 -8
  170. package/dist/{chunk-V2ES4WCZ.js → chunk-ZZZ4I455.js} +2 -2
  171. package/dist/cli.js +106 -100
  172. package/dist/coding/export-okf-codegraph.js +16 -16
  173. package/dist/coding/pre-action-gate.js +2 -2
  174. package/dist/compounding/engine.js +16 -16
  175. package/dist/config.js +27 -22
  176. package/dist/connectors/codex-materialize-runner.js +15 -15
  177. package/dist/connectors/index.js +15 -15
  178. package/dist/console/state.d.ts +30 -0
  179. package/dist/console/state.d.ts.map +1 -1
  180. package/dist/contradiction/index.js +5 -5
  181. package/dist/convergence-refresh.js +17 -17
  182. package/dist/corpus-watermark.js +17 -17
  183. package/dist/dashboard-runtime.js +2 -2
  184. package/dist/deep-recall-http-glue.js +15 -8
  185. package/dist/embedding-fallback.d.ts +41 -31
  186. package/dist/embedding-fallback.d.ts.map +1 -1
  187. package/dist/embedding-fallback.js +9 -1
  188. package/dist/embedding-generation-lock.d.ts +49 -0
  189. package/dist/embedding-generation-lock.d.ts.map +1 -0
  190. package/dist/embedding-generation-lock.js +19 -0
  191. package/dist/embedding-index-storage.d.ts +276 -0
  192. package/dist/embedding-index-storage.d.ts.map +1 -0
  193. package/dist/embedding-index-storage.js +28 -0
  194. package/dist/entity-retrieval.js +16 -16
  195. package/dist/event-order-recall.js +3 -3
  196. package/dist/explicit-capture.js +6 -6
  197. package/dist/external-wiki-access.js +62 -56
  198. package/dist/extraction.js +21 -21
  199. package/dist/{graph-edge-decay-KQGS3L3B.js → graph-edge-decay-MKIRZ762.js} +5 -5
  200. package/dist/graph-jsonl.js +3 -3
  201. package/dist/graph-snapshot.js +3 -3
  202. package/dist/graph.js +2 -2
  203. package/dist/harmonic-retrieval.js +1 -1
  204. package/dist/index.d.ts +2 -0
  205. package/dist/index.d.ts.map +1 -1
  206. package/dist/index.js +207 -177
  207. package/dist/lcm/engine.js +3 -3
  208. package/dist/lcm/index.js +9 -9
  209. package/dist/local-llm.js +2 -2
  210. package/dist/location/index.js +18 -18
  211. package/dist/maintenance/memory-governance.js +15 -15
  212. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +15 -15
  213. package/dist/maintenance/rebuild-memory-projection.js +16 -16
  214. package/dist/memory-browse-operations.js +62 -56
  215. package/dist/memory-subject.js +3 -3
  216. package/dist/namespaces/migrate.js +16 -16
  217. package/dist/namespaces/search.js +4 -4
  218. package/dist/namespaces/storage.js +15 -15
  219. package/dist/offline-sync-embedding-generation.d.ts +207 -0
  220. package/dist/offline-sync-embedding-generation.d.ts.map +1 -0
  221. package/dist/offline-sync-embedding-generation.js +42 -0
  222. package/dist/offline-sync-exclude-globs.d.ts +15 -0
  223. package/dist/offline-sync-exclude-globs.d.ts.map +1 -1
  224. package/dist/offline-sync-exclude-globs.js +11 -3
  225. package/dist/offline-sync-file-io.d.ts +7 -0
  226. package/dist/offline-sync-file-io.d.ts.map +1 -1
  227. package/dist/offline-sync-file-io.js +14 -2
  228. package/dist/offline-sync-impression-drain.js +3 -3
  229. package/dist/offline-sync-runtime-state.d.ts +5 -0
  230. package/dist/offline-sync-runtime-state.d.ts.map +1 -0
  231. package/dist/offline-sync-runtime-state.js +14 -0
  232. package/dist/offline-sync-runtime-state.js.map +1 -0
  233. package/dist/offline-sync-snapshot-stream.d.ts +26 -0
  234. package/dist/offline-sync-snapshot-stream.d.ts.map +1 -0
  235. package/dist/offline-sync-snapshot-stream.js +28 -0
  236. package/dist/offline-sync-snapshot-stream.js.map +1 -0
  237. package/dist/offline-sync-upload-staging.d.ts +34 -0
  238. package/dist/offline-sync-upload-staging.d.ts.map +1 -0
  239. package/dist/offline-sync-upload-staging.js +26 -0
  240. package/dist/offline-sync-upload-staging.js.map +1 -0
  241. package/dist/offline-sync.d.ts +46 -27
  242. package/dist/offline-sync.d.ts.map +1 -1
  243. package/dist/offline-sync.js +21 -8
  244. package/dist/operator-doctor-corpus.js +18 -18
  245. package/dist/operator-toolkit.js +38 -33
  246. package/dist/orchestration/maintenance.js +26 -26
  247. package/dist/orchestrator.d.ts.map +1 -1
  248. package/dist/orchestrator.js +142 -136
  249. package/dist/recall-navigation-operations.js +62 -56
  250. package/dist/recall-navigation.js +61 -55
  251. package/dist/reconcile/manifest.js +4 -4
  252. package/dist/{resolution-DNX7FO7A.js → resolution-K4A3VPKZ.js} +6 -6
  253. package/dist/resolution-K4A3VPKZ.js.map +1 -0
  254. package/dist/resume-bundles.js +28 -23
  255. package/dist/salvage-envelope.js +5 -5
  256. package/dist/search/factory.js +3 -3
  257. package/dist/search/index.js +7 -7
  258. package/dist/secure-store/index.js +2 -2
  259. package/dist/secure-store/secure-fs.d.ts +14 -1
  260. package/dist/secure-store/secure-fs.d.ts.map +1 -1
  261. package/dist/semantic-consolidation.js +16 -16
  262. package/dist/semantic-rule-promotion.js +15 -15
  263. package/dist/semantic-rule-verifier.js +15 -15
  264. package/dist/source-agent-qualifier.js +18 -18
  265. package/dist/standup/build.js +15 -15
  266. package/dist/{state-ITXJKXRE.js → state-JB47KHYV.js} +2 -2
  267. package/dist/state-JB47KHYV.js.map +1 -0
  268. package/dist/storage.d.ts +9 -3
  269. package/dist/storage.d.ts.map +1 -1
  270. package/dist/storage.js +14 -14
  271. package/dist/subject-config.js +4 -4
  272. package/dist/summarizer.js +3 -3
  273. package/dist/support-passport/index.js +64 -58
  274. package/dist/{trace-ZFVRH4GO.js → trace-LXXCMZJA.js} +3 -3
  275. package/dist/transfer/backup.js +2 -2
  276. package/dist/transfer/capsule-export.js +2 -2
  277. package/dist/transfer/capsule-import.js +3 -3
  278. package/dist/transfer/export-okf.js +16 -16
  279. package/dist/{tui-55UJMJX7.js → tui-2Z5WOV7L.js} +3 -3
  280. package/dist/tui-2Z5WOV7L.js.map +1 -0
  281. package/dist/verified-recall.js +15 -15
  282. package/dist/who-knows-cli.js +16 -16
  283. package/dist/who-knows.js +15 -15
  284. package/dist/write-envelope.js +4 -4
  285. package/package.json +2 -2
  286. package/src/access-http-offline-stream.ts +3 -0
  287. package/src/access-mcp-output-schemas.ts +1 -0
  288. package/src/access-service.ts +7 -18
  289. package/src/console/state.test.ts +56 -0
  290. package/src/console/state.ts +83 -0
  291. package/src/embedding-fallback.ts +325 -141
  292. package/src/embedding-generation-lock.ts +141 -0
  293. package/src/embedding-index-storage.test.ts +245 -0
  294. package/src/embedding-index-storage.ts +1068 -0
  295. package/src/index.ts +3 -3
  296. package/src/offline-sync-embedding-generation.test.ts +2173 -0
  297. package/src/offline-sync-embedding-generation.ts +885 -0
  298. package/src/offline-sync-exclude-globs.ts +112 -0
  299. package/src/offline-sync-file-io.ts +48 -0
  300. package/src/offline-sync-runtime-state.ts +48 -0
  301. package/src/offline-sync-snapshot-stream.ts +99 -0
  302. package/src/offline-sync-upload-staging.ts +291 -0
  303. package/src/offline-sync.test.ts +164 -1
  304. package/src/offline-sync.ts +458 -453
  305. package/src/orchestrator.ts +2 -1
  306. package/src/secure-store/secure-fs.ts +26 -6
  307. package/src/storage.ts +11 -6
  308. package/dist/chunk-3BTFBQLO.js.map +0 -1
  309. package/dist/chunk-AREGMX2J.js +0 -50
  310. package/dist/chunk-AREGMX2J.js.map +0 -1
  311. package/dist/chunk-FOH7AMMI.js.map +0 -1
  312. package/dist/chunk-FRCESVV6.js.map +0 -1
  313. package/dist/chunk-G6SVZWPB.js.map +0 -1
  314. package/dist/chunk-JN72EPOE.js.map +0 -1
  315. package/dist/chunk-RB776UH3.js.map +0 -1
  316. package/dist/chunk-WQZZDN5M.js.map +0 -1
  317. /package/dist/{auto-sync-6IPX2BAC.js.map → auto-sync-U5N3CB5Y.js.map} +0 -0
  318. /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
  319. /package/dist/{capsule-merge-3MNURAQ3.js.map → capsule-merge-ZCXUPLNJ.js.map} +0 -0
  320. /package/dist/{chunk-JE334SIA.js.map → chunk-22DYJLFN.js.map} +0 -0
  321. /package/dist/{chunk-JGSQ6P4D.js.map → chunk-2DLWLNO4.js.map} +0 -0
  322. /package/dist/{chunk-LS3XH3UE.js.map → chunk-2NAD4YCI.js.map} +0 -0
  323. /package/dist/{chunk-SY5KDWGU.js.map → chunk-2WLMWP2T.js.map} +0 -0
  324. /package/dist/{chunk-P6WFK4ZJ.js.map → chunk-3O5BB45T.js.map} +0 -0
  325. /package/dist/{chunk-SOZUJCME.js.map → chunk-3SGTV7RD.js.map} +0 -0
  326. /package/dist/{chunk-YITN5WXI.js.map → chunk-4KN2JLQV.js.map} +0 -0
  327. /package/dist/{chunk-JKMHZF3Q.js.map → chunk-4MP7XR5J.js.map} +0 -0
  328. /package/dist/{chunk-7DE2NOON.js.map → chunk-4O4XCBOS.js.map} +0 -0
  329. /package/dist/{chunk-SLEAJ67D.js.map → chunk-4OQET73E.js.map} +0 -0
  330. /package/dist/{chunk-GDYU6VF4.js.map → chunk-5AYUHSAU.js.map} +0 -0
  331. /package/dist/{chunk-QWW2JDDX.js.map → chunk-5E6RYRYW.js.map} +0 -0
  332. /package/dist/{chunk-ICZQG4XE.js.map → chunk-5FAGCOZD.js.map} +0 -0
  333. /package/dist/{chunk-4IEOGIPB.js.map → chunk-5MNUBT7T.js.map} +0 -0
  334. /package/dist/{chunk-52II7PB6.js.map → chunk-645WOU5I.js.map} +0 -0
  335. /package/dist/{chunk-DUJYRVUD.js.map → chunk-6O2VUWV6.js.map} +0 -0
  336. /package/dist/{chunk-CS52MFDV.js.map → chunk-6UQFJULG.js.map} +0 -0
  337. /package/dist/{chunk-HESDNYRB.js.map → chunk-76M5OK7V.js.map} +0 -0
  338. /package/dist/{chunk-4CDYONWO.js.map → chunk-A4VSOUCB.js.map} +0 -0
  339. /package/dist/{chunk-HBBHL4YF.js.map → chunk-AE5U5NMS.js.map} +0 -0
  340. /package/dist/{chunk-CUCWX3MO.js.map → chunk-AEHVZY3X.js.map} +0 -0
  341. /package/dist/{chunk-DZ525SNQ.js.map → chunk-AEU4EQGV.js.map} +0 -0
  342. /package/dist/{chunk-VLLD4TOB.js.map → chunk-AWG3TIAY.js.map} +0 -0
  343. /package/dist/{chunk-72JNNJJJ.js.map → chunk-AXC4WSF3.js.map} +0 -0
  344. /package/dist/{chunk-N4S4NUX5.js.map → chunk-B3O3IRZN.js.map} +0 -0
  345. /package/dist/{chunk-FAN73COB.js.map → chunk-BGI4NUAO.js.map} +0 -0
  346. /package/dist/{chunk-XTYX6V52.js.map → chunk-BJVO4H5C.js.map} +0 -0
  347. /package/dist/{chunk-EHS5MJEF.js.map → chunk-BLVR4WYU.js.map} +0 -0
  348. /package/dist/{chunk-KFCMMDHH.js.map → chunk-BOZJD5PK.js.map} +0 -0
  349. /package/dist/{chunk-TM3RXL4G.js.map → chunk-BUQKSQXL.js.map} +0 -0
  350. /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
  351. /package/dist/{chunk-EVBI7S4J.js.map → chunk-C6U57KG4.js.map} +0 -0
  352. /package/dist/{chunk-VX6GCKBS.js.map → chunk-C7ZDKWPS.js.map} +0 -0
  353. /package/dist/{chunk-ZI7QA4MK.js.map → chunk-CRXRZA3G.js.map} +0 -0
  354. /package/dist/{chunk-VQNONC4E.js.map → chunk-CZJA2EQE.js.map} +0 -0
  355. /package/dist/{chunk-M536WNTQ.js.map → chunk-D5BSQR63.js.map} +0 -0
  356. /package/dist/{chunk-37S3IMYB.js.map → chunk-DGS34QSY.js.map} +0 -0
  357. /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
  358. /package/dist/{chunk-C3QXITIP.js.map → chunk-EQUD7FP4.js.map} +0 -0
  359. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  360. /package/dist/{chunk-EQ6YOIKO.js.map → chunk-FJLQXMTU.js.map} +0 -0
  361. /package/dist/{chunk-SRZW7FNU.js.map → chunk-FKD656D2.js.map} +0 -0
  362. /package/dist/{chunk-CLXTDOKV.js.map → chunk-FUNBJQYR.js.map} +0 -0
  363. /package/dist/{chunk-CLSYE5OZ.js.map → chunk-G2STTRYB.js.map} +0 -0
  364. /package/dist/{chunk-KP7SANCF.js.map → chunk-GWM6T5EQ.js.map} +0 -0
  365. /package/dist/{chunk-UA5DX5C3.js.map → chunk-GZDTVYK2.js.map} +0 -0
  366. /package/dist/{chunk-3GQC3NMB.js.map → chunk-H56VY4BY.js.map} +0 -0
  367. /package/dist/{chunk-L6UZEAID.js.map → chunk-H5BJWPUI.js.map} +0 -0
  368. /package/dist/{chunk-YSJTCOHR.js.map → chunk-HLB7GKGG.js.map} +0 -0
  369. /package/dist/{chunk-PUKTBULQ.js.map → chunk-HWLIMQE2.js.map} +0 -0
  370. /package/dist/{chunk-CWHV574S.js.map → chunk-IOSZHHJC.js.map} +0 -0
  371. /package/dist/{chunk-P2WZHLUF.js.map → chunk-JVPWRNHA.js.map} +0 -0
  372. /package/dist/{chunk-6ET2VII7.js.map → chunk-JYG2334B.js.map} +0 -0
  373. /package/dist/{chunk-WIKO4SCR.js.map → chunk-LHRQBMJN.js.map} +0 -0
  374. /package/dist/{chunk-K4J4E7WV.js.map → chunk-LO4UKPKX.js.map} +0 -0
  375. /package/dist/{chunk-LF5IGOOD.js.map → chunk-LU7E2MXE.js.map} +0 -0
  376. /package/dist/{chunk-RJLV4BKR.js.map → chunk-LV5H6FTN.js.map} +0 -0
  377. /package/dist/{chunk-3C3J243X.js.map → chunk-LW3HKRL7.js.map} +0 -0
  378. /package/dist/{chunk-4BZP2B4U.js.map → chunk-M6C7SOR6.js.map} +0 -0
  379. /package/dist/{chunk-SPSRGEDQ.js.map → chunk-MXCAJKMA.js.map} +0 -0
  380. /package/dist/{chunk-BMYZTZ5D.js.map → chunk-MY7B3HYC.js.map} +0 -0
  381. /package/dist/{chunk-NB3LRY4T.js.map → chunk-N4PYB235.js.map} +0 -0
  382. /package/dist/{chunk-S3YM4SZQ.js.map → chunk-N7QV3CMX.js.map} +0 -0
  383. /package/dist/{chunk-AQK54Q7O.js.map → chunk-NYKIUQL6.js.map} +0 -0
  384. /package/dist/{chunk-NE6PHPLG.js.map → chunk-OK52IY3W.js.map} +0 -0
  385. /package/dist/{chunk-6TT6HRE4.js.map → chunk-P2ODTYOF.js.map} +0 -0
  386. /package/dist/{chunk-W4AOR52H.js.map → chunk-PEBHPCX2.js.map} +0 -0
  387. /package/dist/{chunk-VYCOJKSX.js.map → chunk-PO7NXNGC.js.map} +0 -0
  388. /package/dist/{chunk-XAITGGX5.js.map → chunk-Q4AUIFUA.js.map} +0 -0
  389. /package/dist/{chunk-MQJL6CDZ.js.map → chunk-QAW55ZHJ.js.map} +0 -0
  390. /package/dist/{chunk-QVS22WTO.js.map → chunk-QI7K7BZ5.js.map} +0 -0
  391. /package/dist/{chunk-NE7I2RCT.js.map → chunk-R3X2T2LP.js.map} +0 -0
  392. /package/dist/{chunk-S7GCT7NU.js.map → chunk-RBITPCMK.js.map} +0 -0
  393. /package/dist/{chunk-BJRAELDH.js.map → chunk-SCXURCVT.js.map} +0 -0
  394. /package/dist/{chunk-BFO3IR56.js.map → chunk-SWBC3UER.js.map} +0 -0
  395. /package/dist/{chunk-YRLHEEQT.js.map → chunk-U6VC6RP3.js.map} +0 -0
  396. /package/dist/{chunk-MOQPEFLN.js.map → chunk-UP75KL3L.js.map} +0 -0
  397. /package/dist/{chunk-D5F7TKMH.js.map → chunk-V4JZCTMK.js.map} +0 -0
  398. /package/dist/{chunk-QEV26L2F.js.map → chunk-VALKBYBN.js.map} +0 -0
  399. /package/dist/{chunk-7LCGFUMB.js.map → chunk-VDX2J7OX.js.map} +0 -0
  400. /package/dist/{chunk-TDYAIXHK.js.map → chunk-VVX5PZFS.js.map} +0 -0
  401. /package/dist/{chunk-6NQ4LSMS.js.map → chunk-W3XXFPRI.js.map} +0 -0
  402. /package/dist/{chunk-4IIMMZZK.js.map → chunk-WKY7N3L7.js.map} +0 -0
  403. /package/dist/{chunk-VVDWZRZG.js.map → chunk-X5SHE2O3.js.map} +0 -0
  404. /package/dist/{chunk-NR2ASHBL.js.map → chunk-XOV7NRY5.js.map} +0 -0
  405. /package/dist/{chunk-6L3PBSAR.js.map → chunk-YJ7MW627.js.map} +0 -0
  406. /package/dist/{chunk-LQG5O5EP.js.map → chunk-Z3O53WZR.js.map} +0 -0
  407. /package/dist/{chunk-FB64XE3R.js.map → chunk-Z5LP6ILP.js.map} +0 -0
  408. /package/dist/{chunk-VKQI47DJ.js.map → chunk-Z6EZCN6Z.js.map} +0 -0
  409. /package/dist/{chunk-V2ES4WCZ.js.map → chunk-ZZZ4I455.js.map} +0 -0
  410. /package/dist/{graph-edge-decay-KQGS3L3B.js.map → embedding-generation-lock.js.map} +0 -0
  411. /package/dist/{resolution-DNX7FO7A.js.map → embedding-index-storage.js.map} +0 -0
  412. /package/dist/{state-ITXJKXRE.js.map → graph-edge-decay-MKIRZ762.js.map} +0 -0
  413. /package/dist/{tui-55UJMJX7.js.map → offline-sync-embedding-generation.js.map} +0 -0
  414. /package/dist/{trace-ZFVRH4GO.js.map → trace-LXXCMZJA.js.map} +0 -0
@@ -0,0 +1,885 @@
1
+ // Embedding-index generation semantics for offline sync (issue #3146, PR
2
+ // #3148). Extracted from offline-sync.ts so the god-file line-count ratchet
3
+ // does not grow when the rules gain behavior. The snapshot walker treats
4
+ // every `state/embeddings/` shard set — or a legacy `state/embeddings.json`
5
+ // marker — as ONE generation, in both directions:
6
+ //
7
+ // 1. Atomic replacement (apply side): an incoming generation is staged
8
+ // complete, verified byte-for-byte, and published through the store's
9
+ // single backup-swap state machine. A crash or write failure can never
10
+ // leave a mix of the local and incoming generation on disk, and a
11
+ // local-only shard of a replaced generation is removed by the swap, not
12
+ // merged into the incoming set.
13
+ // 2. Filter-to-omit, never filter-to-delete (push side): when an exclusion
14
+ // filter removes ANY member of a local generation from a push, the
15
+ // WHOLE generation is omitted — files and deletion metadata — and the
16
+ // receiver keeps its copy. A filtered generation must never look
17
+ // deleted, and a partially filtered snapshot must never look like a
18
+ // complete generation.
19
+ import { createHash, randomBytes } from "node:crypto";
20
+ import { lstat, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
21
+ import path from "node:path";
22
+ import {
23
+ type EmbeddingIndexFile,
24
+ EmbeddingIndexFileStore,
25
+ type EmbeddingIndexIdentity,
26
+ type EmbeddingIndexStoreIo,
27
+ EmbeddingIndexStorageError,
28
+ parseEmbeddingIndexDocument,
29
+ serializeEmbeddingShard,
30
+ shardEntriesForIndex,
31
+ validateShardMembership,
32
+ } from "./embedding-index-storage.js";
33
+ import {
34
+ assertEmbeddingGenerationLockHeld,
35
+ type EmbeddingGenerationFence,
36
+ embeddingGenerationLockPath,
37
+ withEmbeddingGenerationLock,
38
+ } from "./embedding-generation-lock.js";
39
+ import type { OfflineSyncExcludeFile, OfflineSyncFileTarget } from "./offline-sync-file-io.js";
40
+ import { EMBEDDING_SHARD_FILE_PATTERN } from "./offline-sync-runtime-state.js";
41
+ import { log } from "./logger.js";
42
+ import type {
43
+ OfflineSyncFileDeleteTarget,
44
+ OfflineSyncFileStagingReadTarget,
45
+ OfflineSyncFileStagingWriteTarget,
46
+ OfflineSyncFileState,
47
+ } from "./offline-sync.js";
48
+ import { type SafeArchiveRoot, resolveSafeArchiveTarget } from "./transfer/fs-utils.js";
49
+
50
+ const EMBEDDING_MARKER_BASENAME = "embeddings.json";
51
+ export const EMBEDDING_SHARD_DIR_BASENAME = "embeddings";
52
+
53
+ /**
54
+ * Canonical `state` dir prefix ("state" or "namespaces/<ns>/state") for a
55
+ * path's segments, or null when the path is not canonical runtime state.
56
+ */
57
+ function canonicalStateDirPrefix(parts: string[]): string | null {
58
+ if (parts[0] === "state") return "state";
59
+ if (parts[0] === "namespaces" && parts.length >= 4 && parts[2] === "state") {
60
+ return `namespaces/${parts[1]}/state`;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ /** The sharded-generation directory when `relPath` is one of its shards. */
66
+ export function embeddingShardDirOf(relPath: string): string | null {
67
+ const parts = relPath.split("/");
68
+ if (parts.length < 3) return null;
69
+ if (!EMBEDDING_SHARD_FILE_PATTERN.test(parts[parts.length - 1] ?? "")) return null;
70
+ if (parts[parts.length - 2] !== EMBEDDING_SHARD_DIR_BASENAME) return null;
71
+ if (canonicalStateDirPrefix(parts) === null) return null;
72
+ return parts.slice(0, -1).join("/");
73
+ }
74
+
75
+ /**
76
+ * True when `dirPath` is a canonical embeddings generation dir
77
+ * (`state/embeddings` or `namespaces/<ns>/state/embeddings`). Encodes the
78
+ * snapshot-field shape contract without a ReDoS-shaped literal (issue #2439).
79
+ */
80
+ export function isEmbeddingGenerationDirPath(dirPath: string): boolean {
81
+ const membership = embeddingGenerationMembership(`${dirPath}/shard-0000.json`);
82
+ return membership !== null && membership.shardDir === dirPath;
83
+ }
84
+
85
+ /** The state dir when `relPath` is the legacy single-file marker. */
86
+ export function embeddingMarkerDirOf(relPath: string): string | null {
87
+ const parts = relPath.split("/");
88
+ if ((parts[parts.length - 1] ?? "") !== EMBEDDING_MARKER_BASENAME) return null;
89
+ if (canonicalStateDirPrefix(parts) === null) return null;
90
+ return parts.slice(0, -1).join("/");
91
+ }
92
+
93
+ /**
94
+ * Membership of `relPath` in an embedding generation: shards belong to their
95
+ * `.../embeddings` directory, the legacy marker belongs to the sibling
96
+ * `.../embeddings` directory of its state dir.
97
+ */
98
+ export function embeddingGenerationMembership(relPath: string): { kind: "shard" | "marker"; shardDir: string } | null {
99
+ const shardDir = embeddingShardDirOf(relPath);
100
+ if (shardDir) return { kind: "shard", shardDir };
101
+ const markerDir = embeddingMarkerDirOf(relPath);
102
+ if (markerDir) return { kind: "marker", shardDir: `${markerDir}/${EMBEDDING_SHARD_DIR_BASENAME}` };
103
+ return null;
104
+ }
105
+
106
+ export interface IncomingEmbeddingGenerations {
107
+ /** Generation dirs with at least one incoming shard. */
108
+ shardDirs: Set<string>;
109
+ /** State dirs whose incoming snapshot carries the legacy marker. */
110
+ legacyMarkerDirs: Set<string>;
111
+ }
112
+
113
+ /** Classify incoming snapshot paths into embedding generations. */
114
+ export function detectIncomingEmbeddingGenerations(incomingPaths: Iterable<string>): IncomingEmbeddingGenerations {
115
+ const shardDirs = new Set<string>();
116
+ const legacyMarkerDirs = new Set<string>();
117
+ for (const relPath of incomingPaths) {
118
+ const membership = embeddingGenerationMembership(relPath);
119
+ if (!membership) continue;
120
+ if (membership.kind === "shard") shardDirs.add(membership.shardDir);
121
+ else legacyMarkerDirs.add(membership.shardDir);
122
+ }
123
+ return { shardDirs, legacyMarkerDirs };
124
+ }
125
+
126
+ function shardRelPath(shardDirRel: string, shardIndex: number): string {
127
+ return `${shardDirRel}/shard-${String(shardIndex).padStart(4, "0")}.json`;
128
+ }
129
+
130
+ function describeIndexReadFailure(
131
+ read: { outcome: "absent" } | { outcome: "foreign" } | { outcome: "unreadable"; reason: string }
132
+ ): string {
133
+ if (read.outcome === "unreadable") return read.reason;
134
+ if (read.outcome === "foreign") return "unrecognized index format";
135
+ return "generation document is absent";
136
+ }
137
+
138
+ function sha256Hex(buffer: Buffer): string {
139
+ return createHash("sha256").update(buffer).digest("hex");
140
+ }
141
+
142
+ async function pathExists(absPath: string): Promise<boolean> {
143
+ try {
144
+ await lstat(absPath);
145
+ return true;
146
+ } catch (err) {
147
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return false;
148
+ throw err;
149
+ }
150
+ }
151
+
152
+ export interface EmbeddingGenerationTransactionIo {
153
+ /** Storage-backed local read: hydrates unchanged shards (metadata-only
154
+ * pulls skip their content) and decrypts secure-store files. */
155
+ readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
156
+ writeStagingFile?: (target: OfflineSyncFileStagingWriteTarget) => Promise<void>;
157
+ readStagingFile?: (target: OfflineSyncFileStagingReadTarget) => Promise<Buffer>;
158
+ deleteFile?: (target: OfflineSyncFileDeleteTarget) => Promise<void>;
159
+ }
160
+
161
+ export interface EmbeddingGenerationTransactionInput {
162
+ root: SafeArchiveRoot;
163
+ /** The generation dir, e.g. `state/embeddings`. */
164
+ shardDirRel: string;
165
+ /** Incoming shard rel paths — the complete incoming sharded generation. */
166
+ incomingShardPaths: string[];
167
+ /** Incoming legacy marker (rel path + verified content), when the
168
+ * incoming generation is legacy-layout. Mutually exclusive with
169
+ * `incomingShardPaths`. */
170
+ incomingMarker: { path: string; sha256: string; buffer: Buffer | null } | null;
171
+ /** True when the incoming snapshot ALSO carries the legacy marker while
172
+ * shards form the generation (mixed remote): the per-file loop then writes
173
+ * the marker as the inert remote artifact, so this transaction must NOT
174
+ * remove a local marker. */
175
+ incomingMarkerPresent: boolean;
176
+ /** Incoming shard records (sha256 for local hydration of unchanged
177
+ * metadata-only pulls). */
178
+ incomingShardStates: ReadonlyMap<string, OfflineSyncFileState>;
179
+ /** Verified incoming content by rel path (absent for shards the metadata
180
+ * hydration skipped because the local hash already matches). */
181
+ incomingBuffers: ReadonlyMap<string, Buffer>;
182
+ /**
183
+ * Shared-base states (pre-census): lets the transaction re-evaluate
184
+ * divergence against ACTUAL disk contents while holding the mutation
185
+ * lock, instead of trusting the census taken before the lock
186
+ * (codex P1: a daemon mutation between census and lock was discarded).
187
+ */
188
+ baseStates?: ReadonlyMap<string, { sha256: string }>;
189
+ io: EmbeddingGenerationTransactionIo;
190
+ now: number;
191
+ }
192
+
193
+ export interface EmbeddingGenerationTransactionResult {
194
+ shardDirRel: string;
195
+ upserted: number;
196
+ deleted: number;
197
+ /** nextBase states for files the transaction published. */
198
+ writtenStates: Map<string, OfflineSyncFileState>;
199
+ /** nextBase paths the transaction removed (stale shards, demoted marker). */
200
+ removedPaths: Set<string>;
201
+ /** Paths the per-file apply loop must skip — all handled here. */
202
+ handledPaths: Set<string>;
203
+ /** True when the generation was deferred (not published) because the disk
204
+ * diverged from the shared base while the lock was held. */
205
+ deferredLocalDivergence?: boolean;
206
+ }
207
+
208
+ /**
209
+ * Content for ONE incoming generation member: the verified incoming buffer,
210
+ * or the LOCAL file hash-verified against the incoming record (metadata-only
211
+ * pulls skip content whose local hash already matches — the local bytes ARE
212
+ * the incoming content). Shared by the shard and legacy-marker branches.
213
+ */
214
+ async function hydratedIncomingBuffer(
215
+ io: EmbeddingGenerationTransactionIo,
216
+ root: SafeArchiveRoot,
217
+ relPath: string,
218
+ expectedSha256: string,
219
+ incoming: Buffer | null | undefined,
220
+ ): Promise<Buffer> {
221
+ if (incoming) return incoming;
222
+ const filePath = await resolveSafeArchiveTarget(root, relPath);
223
+ const local = io.readFile
224
+ ? await io.readFile({ root: root.abs, path: relPath, filePath })
225
+ : await readFile(filePath);
226
+ if (sha256Hex(local) !== expectedSha256) {
227
+ throw new EmbeddingIndexStorageError(
228
+ `cannot hydrate unchanged embedding index file ${relPath}: local digest does not match the incoming record; local generation preserved`,
229
+ );
230
+ }
231
+ return local;
232
+ }
233
+
234
+ /**
235
+ * Apply ONE incoming embedding generation atomically (issue #3148, round 4):
236
+ * recover an interrupted replacement, validate the complete incoming
237
+ * generation (legacy markers are converted to the sharded layout so the
238
+ * one-way layout is preserved), stage it, verify the staged bytes, then
239
+ * publish through the shared backup-swap. Any failure before the swap leaves
240
+ * the local generation untouched; a crash in the swap gap is rolled back by
241
+ * the store's recovery on next use.
242
+ */
243
+ /**
244
+ * Enumerate and hash the ACTUAL local generation on disk (legacy marker +
245
+ * every shard) through the configured secure IO — the fresh in-lock census
246
+ * the stale pre-lock apply census cannot be trusted for (codex P1).
247
+ */
248
+ /** Scope the shared vault base to this physical embedding generation. */
249
+ function baseStatesForGeneration(
250
+ base: ReadonlyMap<string, { sha256: string }> | undefined,
251
+ shardDirRel: string,
252
+ ): Map<string, { sha256: string }> {
253
+ return new Map([...base ?? []].filter(([relPath]) => embeddingGenerationMembership(relPath)?.shardDir === shardDirRel));
254
+ }
255
+
256
+ async function censusGenerationState(
257
+ io: EmbeddingGenerationTransactionIo,
258
+ root: SafeArchiveRoot,
259
+ markerRel: string,
260
+ markerAbs: string,
261
+ shardDirAbs: string,
262
+ shardDirRel: string,
263
+ ): Promise<Map<string, string>> {
264
+ const census = new Map<string, string>();
265
+ const readSha = async (relPath: string, filePath: string) =>
266
+ sha256Hex(io.readFile ? await io.readFile({ root: root.abs, path: relPath, filePath }) : await readFile(filePath));
267
+ if (await pathExists(markerAbs)) census.set(markerRel, await readSha(markerRel, markerAbs));
268
+ const names = await readdir(shardDirAbs).catch((err: NodeJS.ErrnoException) => {
269
+ if (err.code === "ENOENT") return [] as string[];
270
+ throw err;
271
+ });
272
+ for (const name of names.filter((candidate) => EMBEDDING_SHARD_FILE_PATTERN.test(candidate)).sort()) {
273
+ const relPath = `${shardDirRel}/${name}`;
274
+ census.set(relPath, await readSha(relPath, path.join(shardDirAbs, name)));
275
+ }
276
+ return census;
277
+ }
278
+
279
+ /** Incoming member states for the divergence helper (digests as fallback). */
280
+ function incomingGenerationStates(input: EmbeddingGenerationTransactionInput): { path: string; sha256: string }[] {
281
+ const states = input.incomingShardPaths.map((relPath) => ({
282
+ path: relPath,
283
+ sha256: input.incomingShardStates.get(relPath)?.sha256 ?? sha256Hex(input.incomingBuffers.get(relPath) ?? Buffer.alloc(0)),
284
+ }));
285
+ if (input.incomingMarker) {
286
+ states.push({ path: input.incomingMarker.path, sha256: input.incomingMarker.sha256 });
287
+ }
288
+ return states;
289
+ }
290
+
291
+ export async function applyEmbeddingGenerationTransaction(
292
+ input: EmbeddingGenerationTransactionInput
293
+ ): Promise<EmbeddingGenerationTransactionResult> {
294
+ const { root, shardDirRel, io } = input;
295
+ const stateDirRel = shardDirRel.slice(0, -`/${EMBEDDING_SHARD_DIR_BASENAME}`.length);
296
+ const stateDirAbs = await resolveSafeArchiveTarget(root, stateDirRel);
297
+ return await withEmbeddingGenerationLock(stateDirAbs, async (section) => {
298
+ const lockPath = embeddingGenerationLockPath(stateDirAbs);
299
+ const fence: EmbeddingGenerationFence = () => assertEmbeddingGenerationLockHeld(lockPath, section);
300
+ const markerRel = `${stateDirRel}/${EMBEDDING_MARKER_BASENAME}`;
301
+ const shardDirAbs = await resolveSafeArchiveTarget(root, shardDirRel);
302
+ const markerAbs = await resolveSafeArchiveTarget(root, markerRel);
303
+ const store = new EmbeddingIndexFileStore(
304
+ markerAbs,
305
+ shardDirAbs,
306
+ await resolveSafeArchiveTarget(root, `${stateDirRel}/embedding-fallback-status.json`)
307
+ );
308
+
309
+ // 1. Close a pending rename gap (old generation restored) before staging:
310
+ // the swap below must never build on a half-replaced generation. The
311
+ // fixed backup path is validated first: a planted symlink (or any
312
+ // escape) must fail closed instead of being renamed into the published
313
+ // position or removed through. The restored target is re-resolved so a
314
+ // symlinked restoration cannot slip past the subsequent IO.
315
+ const backupRel = `${stateDirRel}/embeddings.pre-replace.tmp`;
316
+ await resolveSafeArchiveTarget(root, backupRel);
317
+ if (await store.recoverIfInterrupted(fence)) {
318
+ await resolveSafeArchiveTarget(root, shardDirRel);
319
+ }
320
+
321
+ // Codex P1: the apply census was taken BEFORE this lock, so it may be
322
+ // stale. Re-enumerate and hash the ACTUAL local generation through the
323
+ // secure IO and defer the WHOLE generation when the disk no longer
324
+ // matches the shared base — a concurrent daemon mutation must never be
325
+ // silently discarded by the swap.
326
+ // No shared base (fresh catch-up) = no divergence semantics to protect.
327
+ const baseStates = baseStatesForGeneration(input.baseStates, shardDirRel);
328
+ if (baseStates.size > 0) {
329
+ const localCensus = await censusGenerationState(io, root, markerRel, markerAbs, shardDirAbs, shardDirRel);
330
+ const baseArr = [...baseStates].map(([relPath, state]) => ({ path: relPath, sha256: state.sha256 }));
331
+ const censusArr = [...localCensus].map(([relPath, sha256]) => ({ path: relPath, sha256 }));
332
+ const deferrals = divergedEmbeddingGenerationDeferrals({
333
+ incomingFiles: incomingGenerationStates(input),
334
+ baseFiles: baseArr,
335
+ currentFiles: censusArr,
336
+ });
337
+ const addedOrRemovedSinceBase =
338
+ censusArr.some((member) => !baseStates.has(member.path)) ||
339
+ baseArr.some((member) => !localCensus.has(member.path));
340
+ if (deferrals.length > 0 || addedOrRemovedSinceBase) {
341
+ const handled = new Set<string>(input.incomingShardPaths);
342
+ if (input.incomingMarker) handled.add(input.incomingMarker.path);
343
+ if (input.incomingMarkerPresent) handled.add(markerRel);
344
+ for (const relPath of localCensus.keys()) handled.add(relPath);
345
+ log.warn(
346
+ `embedding generation ${shardDirRel} deferred: disk diverged from the shared base under the mutation lock (${deferrals.length > 0 ? deferrals.join(", ") : "generation membership changed since base"}); nothing published, nothing removed`,
347
+ );
348
+ return {
349
+ shardDirRel,
350
+ upserted: 0,
351
+ deleted: 0,
352
+ writtenStates: new Map<string, OfflineSyncFileState>(),
353
+ removedPaths: new Set<string>(),
354
+ handledPaths: handled,
355
+ deferredLocalDivergence: true,
356
+ };
357
+ }
358
+ }
359
+
360
+ const published = new Map<string, Buffer>();
361
+ let identity: EmbeddingIndexIdentity | null = null;
362
+ // Divergence re-evaluation happens HERE, under the lock, against real
363
+ // disk bytes: the apply-side census may already be stale.
364
+ // 2. Build the complete incoming generation (final rel path → plaintext
365
+ // bytes) and validate every document BEFORE touching local state.
366
+ if (input.incomingMarker) {
367
+ const markerBuffer = await hydratedIncomingBuffer(
368
+ io,
369
+ root,
370
+ input.incomingMarker.path,
371
+ input.incomingMarker.sha256,
372
+ input.incomingMarker.buffer,
373
+ );
374
+ const read = parseEmbeddingIndexDocument(markerBuffer.toString("utf-8"));
375
+ if (read.outcome !== "ok") {
376
+ throw new EmbeddingIndexStorageError(
377
+ `refusing incoming embedding index generation ${markerRel}: ${describeIndexReadFailure(read)}; local generation preserved`
378
+ );
379
+ }
380
+ identity = { provider: read.file.provider, model: read.file.model };
381
+ const groups = shardEntriesForIndex(read.file);
382
+ for (const [shardIndex, entries] of groups) {
383
+ if (Object.keys(entries).length === 0) continue;
384
+ published.set(
385
+ shardRelPath(shardDirRel, shardIndex),
386
+ Buffer.from(serializeEmbeddingShard(read.file, shardIndex, entries), "utf-8")
387
+ );
388
+ }
389
+ } else {
390
+ for (const relPath of input.incomingShardPaths) {
391
+ // Metadata-only pulls skip content whose local hash already matches
392
+ // the incoming record: the LOCAL bytes ARE the incoming content, so
393
+ // read and hash-verify them instead of failing (codex rounds 5-6).
394
+ const state = input.incomingShardStates.get(relPath);
395
+ const buffer = await hydratedIncomingBuffer(
396
+ io,
397
+ root,
398
+ relPath,
399
+ state?.sha256 ?? "",
400
+ input.incomingBuffers.get(relPath),
401
+ );
402
+ const read = parseEmbeddingIndexDocument(buffer.toString("utf-8"));
403
+ if (read.outcome !== "ok") {
404
+ throw new EmbeddingIndexStorageError(
405
+ `refusing incoming embedding index shard ${relPath}: ${describeIndexReadFailure(read)}; local generation preserved`
406
+ );
407
+ }
408
+ validateShardMembership(relPath.split("/").pop() ?? "", read.file);
409
+ const shardIdentity = { provider: read.file.provider, model: read.file.model };
410
+ if (!identity) identity = shardIdentity;
411
+ if (identity.provider !== shardIdentity.provider || identity.model !== shardIdentity.model) {
412
+ throw new EmbeddingIndexStorageError(
413
+ `refusing mixed-identity incoming embedding index generation in ${shardDirRel}: ${relPath} is ${shardIdentity.provider}/${shardIdentity.model} but the generation is ${identity.provider}/${identity.model}; local generation preserved`
414
+ );
415
+ }
416
+ published.set(relPath, buffer);
417
+ }
418
+ if (published.size === 0) {
419
+ throw new EmbeddingIndexStorageError(
420
+ `incoming embedding index generation ${shardDirRel} is empty; local generation preserved`
421
+ );
422
+ }
423
+ }
424
+
425
+ const stagingRel = `${stateDirRel}/embeddings.staging.tmp-sync-${process.pid}-${input.now}-${randomBytes(8).toString("hex")}`;
426
+ const stagingAbs = await resolveSafeArchiveTarget(root, stagingRel);
427
+ try {
428
+ await mkdir(stagingAbs, { recursive: true });
429
+
430
+ // 3. Stage the complete generation. With storage-backed IO the staged
431
+ // ciphertext binds its FINAL canonical path (`aadRelPath`), so after
432
+ // the directory swap it decrypts at the published path — a physical
433
+ // staging-path AAD would make the published generation unreadable.
434
+ for (const [relPath, buffer] of published) {
435
+ const stagedRel = `${stagingRel}/${path.basename(relPath)}`;
436
+ const stagedAbs = await resolveSafeArchiveTarget(root, stagedRel);
437
+ const finalAbs = await resolveSafeArchiveTarget(root, relPath);
438
+ if (io.writeStagingFile) {
439
+ await io.writeStagingFile({
440
+ root: root.abs,
441
+ path: stagedRel,
442
+ filePath: stagedAbs,
443
+ content: buffer,
444
+ finalAadFilePath: finalAbs,
445
+ });
446
+ } else {
447
+ await writeFile(stagedAbs, buffer, { mode: 0o600 });
448
+ }
449
+ }
450
+
451
+ // 4. Verify the staged bytes against the incoming content before the
452
+ // published generation is touched at all.
453
+ for (const [relPath, buffer] of published) {
454
+ const stagedRel = `${stagingRel}/${path.basename(relPath)}`;
455
+ const stagedAbs = await resolveSafeArchiveTarget(root, stagedRel);
456
+ const staged = io.readStagingFile
457
+ ? await io.readStagingFile({
458
+ root: root.abs,
459
+ path: stagedRel,
460
+ filePath: stagedAbs,
461
+ finalAadFilePath: await resolveSafeArchiveTarget(root, relPath),
462
+ })
463
+ : await readFile(stagedAbs);
464
+ if (!staged.equals(buffer)) {
465
+ throw new EmbeddingIndexStorageError(
466
+ `staged embedding index file ${stagedRel} does not match the incoming content; local generation preserved`
467
+ );
468
+ }
469
+ }
470
+
471
+ // 5. Publish with the ONE swap state machine shared with persist(). The
472
+ // old directory moves aside wholesale, so local shards absent from
473
+ // the incoming generation are removed by the swap — never merged.
474
+ const localShardRels = (
475
+ await readdir(shardDirAbs).catch((err) => {
476
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return [] as string[];
477
+ throw err;
478
+ })
479
+ )
480
+ .filter((name) => EMBEDDING_SHARD_FILE_PATTERN.test(name))
481
+ .map((name) => `${shardDirRel}/${name}`)
482
+ .sort();
483
+ await store.publishSwappedGeneration(stagingAbs, fence);
484
+
485
+ const writtenStates = new Map<string, OfflineSyncFileState>();
486
+ for (const [relPath, buffer] of published) {
487
+ writtenStates.set(relPath, {
488
+ path: relPath,
489
+ sha256: sha256Hex(buffer),
490
+ bytes: buffer.length,
491
+ mtimeMs: input.now,
492
+ });
493
+ }
494
+ const removedPaths = new Set<string>(localShardRels.filter((rel) => !published.has(rel)));
495
+ let deleted = removedPaths.size;
496
+ let markerCleanupFailed = false;
497
+ if ((await pathExists(markerAbs)) && !input.incomingMarker && !input.incomingMarkerPresent) {
498
+ // Publication is already committed. Keep its base advancement even if
499
+ // marker cleanup fails, retaining the marker in base for a retry.
500
+ try {
501
+ if (io.deleteFile) await io.deleteFile({ root: root.abs, path: markerRel, filePath: markerAbs });
502
+ else await rm(markerAbs, { force: true });
503
+ removedPaths.add(markerRel);
504
+ deleted += 1;
505
+ } catch (err) {
506
+ markerCleanupFailed = true;
507
+ log.warn(`embedding generation ${shardDirRel}: marker cleanup deferred after publish: ${err}`);
508
+ }
509
+ }
510
+ return {
511
+ shardDirRel,
512
+ upserted: published.size,
513
+ deleted,
514
+ writtenStates,
515
+ removedPaths,
516
+ handledPaths: new Set([...published.keys(), ...removedPaths, ...(markerCleanupFailed ? [markerRel] : [])]),
517
+ };
518
+ } catch (err) {
519
+ await rm(stagingAbs, { recursive: true, force: true }).catch(() => undefined);
520
+ throw err;
521
+ }
522
+ });
523
+ }
524
+
525
+ // ---------------------------------------------------------------------------
526
+ // Push-side whole-generation omission
527
+ // ---------------------------------------------------------------------------
528
+
529
+ export interface EmbeddingGenerationOmissionOptions {
530
+ rootAbs: string;
531
+ userExcludeRegexps?: readonly RegExp[];
532
+ excludeFile?: OfflineSyncExcludeFile;
533
+ /** Recorded deletion-revision paths: a tombstoned member counts as a
534
+ * generation member for omission detection even though it is absent on
535
+ * disk (its exclusion must omit the WHOLE generation). */
536
+ tombstonedPaths?: readonly string[];
537
+ /**
538
+ * Per-path push exclusion predicate (structural + node-local defaults +
539
+ * user regexps). Defaults to the built-in default excludes + the supplied
540
+ * user regexps, matching the walk that consumes the result.
541
+ */
542
+ isExcludedRelPath?: (relPath: string) => boolean;
543
+ }
544
+
545
+ export interface EmbeddingGenerationOmission {
546
+ /** Existing member paths omitted because their generation is partial. */
547
+ omittedPaths: Set<string>;
548
+ /** Generation dirs whose members are omitted (snapshot field value). */
549
+ omittedDirs: string[];
550
+ }
551
+
552
+ /**
553
+ * Compute the push-side whole-generation omission set: when ANY on-disk
554
+ * member of an embedding generation is excluded by a push filter, EVERY
555
+ * member of that generation is omitted (issue #3148, round 4).
556
+ */
557
+ export async function computeOmittedEmbeddingGenerationPaths(
558
+ options: EmbeddingGenerationOmissionOptions
559
+ ): Promise<EmbeddingGenerationOmission> {
560
+ const omittedPaths = new Set<string>();
561
+ const omittedDirs: string[] = [];
562
+ const stateDirs = ["state"];
563
+ let namespaceNames: string[] = [];
564
+ try {
565
+ namespaceNames = (await readdir(path.join(options.rootAbs, "namespaces"), { withFileTypes: true }))
566
+ .filter((entry) => entry.isDirectory())
567
+ .map((entry) => entry.name);
568
+ } catch {
569
+ // no namespaces
570
+ }
571
+ for (const ns of namespaceNames) stateDirs.push(`namespaces/${ns}/state`);
572
+ const isExcluded = options.isExcludedRelPath ??
573
+ ((relPath: string) =>
574
+ matchesOfflineSyncDefaultExclude(relPath) ||
575
+ Boolean(options.userExcludeRegexps?.some((re) => re.test(relPath))));
576
+
577
+ for (const stateDir of stateDirs) {
578
+ const markerRel = `${stateDir}/${EMBEDDING_MARKER_BASENAME}`;
579
+ const shardDirRel = `${stateDir}/${EMBEDDING_SHARD_DIR_BASENAME}`;
580
+ const shardDirAbs = path.join(options.rootAbs, ...shardDirRel.split("/"));
581
+ const memberRels: string[] = [];
582
+ if (await pathExists(path.join(options.rootAbs, ...markerRel.split("/")))) {
583
+ memberRels.push(markerRel);
584
+ }
585
+ let shardNames: string[] = [];
586
+ try {
587
+ shardNames = (await readdir(shardDirAbs, { withFileTypes: true }))
588
+ .filter((entry) => entry.isFile() && EMBEDDING_SHARD_FILE_PATTERN.test(entry.name))
589
+ .map((entry) => entry.name);
590
+ } catch (err) {
591
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
592
+ }
593
+ for (const name of shardNames) memberRels.push(`${shardDirRel}/${name}`);
594
+ // A tombstoned member is part of the generation even though it is
595
+ // already absent on disk: its exclusion must omit the WHOLE generation,
596
+ // not just drop its revision individually (codex round 6, XkFf).
597
+ for (const tombstoned of options.tombstonedPaths ?? []) {
598
+ const membership = embeddingGenerationMembership(tombstoned);
599
+ if (membership?.shardDir === shardDirRel && !memberRels.includes(tombstoned)) {
600
+ memberRels.push(tombstoned);
601
+ }
602
+ }
603
+ if (memberRels.length === 0) continue;
604
+
605
+ const excludedMembers: string[] = [];
606
+ for (const relPath of memberRels) {
607
+ let excluded = isExcluded(relPath);
608
+ if (!excluded && options.excludeFile) {
609
+ excluded = await options.excludeFile({
610
+ root: options.rootAbs,
611
+ path: relPath,
612
+ filePath: path.join(options.rootAbs, ...relPath.split("/")),
613
+ });
614
+ }
615
+ if (excluded) excludedMembers.push(relPath);
616
+ }
617
+ if (excludedMembers.length === 0) continue;
618
+ omittedDirs.push(shardDirRel);
619
+ omittedPaths.add(shardDirRel); // skip new shards after the omission header
620
+ for (const relPath of memberRels) omittedPaths.add(relPath);
621
+ }
622
+ return { omittedPaths, omittedDirs };
623
+ }
624
+
625
+ /** Reject an exclusion introduced after the snapshot header was computed. */
626
+ export async function assertEmbeddingGenerationStillIncluded(
627
+ rootAbs: string, shardDirRel: string, excludeFile: OfflineSyncExcludeFile | undefined,
628
+ isExcludedRelPath: (relPath: string) => boolean, tombstonedPaths: readonly string[],
629
+ ): Promise<void> {
630
+ const current = await computeOmittedEmbeddingGenerationPaths({ rootAbs, excludeFile, isExcludedRelPath, tombstonedPaths });
631
+ if (current.omittedDirs.includes(shardDirRel)) {
632
+ throw new EmbeddingIndexStorageError(
633
+ `embedding generation ${shardDirRel} became partially excluded during snapshot capture; retry with a fresh omission header`,
634
+ );
635
+ }
636
+ }
637
+
638
+ /** True when a path belongs to a generation the push omitted. */
639
+ export function isPathInOmittedEmbeddingGeneration(relPath: string, omittedDirs: ReadonlySet<string>): boolean {
640
+ const membership = embeddingGenerationMembership(relPath);
641
+ return membership !== null && omittedDirs.has(membership.shardDir);
642
+ }
643
+
644
+ export type { EmbeddingIndexFile };
645
+
646
+ import { matchesOfflineSyncDefaultExclude } from "./offline-sync-exclude-globs.js";
647
+ import type { OfflineSyncDeletionRevision } from "./offline-sync.js";
648
+
649
+ export interface PushEmbeddingGenerationState {
650
+ /** Generation dirs omitted by the push filters (snapshot header value). */
651
+ omittedDirs: string[];
652
+ /** All omitted member paths (walk skip set). */
653
+ omittedPaths: Set<string>;
654
+ /** Deletions with whole-generation suppression applied. */
655
+ deletions: OfflineSyncDeletionRevision[];
656
+ }
657
+
658
+ /**
659
+ * One-call push-side generation state for streaming snapshot builders: the
660
+ * whole-generation omission set (from user regexps, excludeFile, and default
661
+ * excludes) plus the deletion list with omitted-generation revisions
662
+ * suppressed — including revisions for members that are already absent, so a
663
+ * filtered generation can never be announced as deleted.
664
+ */
665
+ export async function resolvePushEmbeddingGenerationState(options: {
666
+ rootAbs: string;
667
+ includeTranscripts: boolean;
668
+ userExcludeRegexps?: readonly RegExp[];
669
+ excludeFile?: OfflineSyncExcludeFile;
670
+ deletions: readonly OfflineSyncDeletionRevision[];
671
+ }): Promise<PushEmbeddingGenerationState> {
672
+ const omission = await computeOmittedEmbeddingGenerationPaths({
673
+ rootAbs: options.rootAbs,
674
+ userExcludeRegexps: options.userExcludeRegexps,
675
+ excludeFile: options.excludeFile,
676
+ // RAW pre-filter tombstones: a deleted member's exclusion must omit the
677
+ // whole generation even though the member is already absent on disk.
678
+ tombstonedPaths: options.deletions.map((deletion) => deletion.path),
679
+ isExcludedRelPath: (relPath) =>
680
+ matchesOfflineSyncDefaultExclude(relPath) ||
681
+ Boolean(options.userExcludeRegexps?.some((re) => re.test(relPath))),
682
+ });
683
+ const omittedDirs = new Set(omission.omittedDirs);
684
+ const deletions = options.deletions.filter((deletion) => {
685
+ if (matchesOfflineSyncDefaultExclude(deletion.path)) return false;
686
+ if (options.userExcludeRegexps?.some((re) => re.test(deletion.path))) return false;
687
+ const membership = embeddingGenerationMembership(deletion.path);
688
+ if (membership && omittedDirs.has(membership.shardDir)) return false;
689
+ return true;
690
+ });
691
+ return { omittedDirs: omission.omittedDirs, omittedPaths: omission.omittedPaths, deletions };
692
+ }
693
+
694
+
695
+ interface StorageBackedIndexIoHost {
696
+ readOfflineSyncFile(filePath: string, opts?: { aadFilePath?: string }): Promise<Buffer>;
697
+ writeOfflineSyncStagingFile(
698
+ filePath: string,
699
+ content: Buffer,
700
+ opts?: { aadFilePath?: string },
701
+ ): Promise<void>;
702
+ }
703
+
704
+ /**
705
+ * Daemon wiring for the index store IO backed by a StorageManager:
706
+ * canonical-path secure reads/writes, so sync-published encrypted shards
707
+ * decrypt in the daemon and daemon-written shards decrypt on sync. Shard
708
+ * STAGING writes bind the FINAL published path.
709
+ */
710
+ export function storageBackedIndexStoreIo(storage: StorageBackedIndexIoHost): EmbeddingIndexStoreIo {
711
+ return {
712
+ readUtf8: async (filePath) => (await storage.readOfflineSyncFile(filePath)).toString("utf-8"),
713
+ writeUtf8: (filePath, contents, opts) =>
714
+ storage.writeOfflineSyncStagingFile(
715
+ filePath,
716
+ Buffer.from(contents, "utf-8"),
717
+ opts?.finalAadFilePath === undefined ? undefined : { aadFilePath: opts.finalAadFilePath },
718
+ ),
719
+ };
720
+ }
721
+
722
+ export interface EmbeddingGenerationRemovalInput {
723
+ root: SafeArchiveRoot;
724
+ /** The generation dir, e.g. `state/embeddings`. */
725
+ shardDirRel: string;
726
+ io: Pick<EmbeddingGenerationTransactionIo, "deleteFile" | "readFile">;
727
+ /** Incoming deletion revisions by path, threaded so each removed member's
728
+ * tombstone is recorded through the configured delete hook AFTER the
729
+ * atomic empty publication (round 6: raw removal must not lose them). */
730
+ deletionMtimeByPath?: ReadonlyMap<string, number>;
731
+ now: number;
732
+ /**
733
+ * Shared-base states: when the live generation diverged from the base
734
+ * (daemon indexed, added, or removed members after the census), the
735
+ * removal DEFERS instead of erasing the local work (codex P1).
736
+ */
737
+ baseStates?: ReadonlyMap<string, { sha256: string }>;
738
+ }
739
+
740
+ /**
741
+ * Remove a LOCAL embedding generation atomically because the remote deleted
742
+ * it wholesale (every member has a deletion revision): recover first, then
743
+ * demote the published directory through the store's single swap state
744
+ * machine and delete the backup — a crash or delete failure can never leave
745
+ * a partial same-identity generation serving silently missing memories.
746
+ * The local legacy marker, when present, is removed through the same delete
747
+ * hook the per-file path uses.
748
+ */
749
+ export async function applyEmbeddingGenerationRemoval(
750
+ input: EmbeddingGenerationRemovalInput,
751
+ ): Promise<EmbeddingGenerationTransactionResult> {
752
+ const { root, shardDirRel, io } = input;
753
+ const stateDirRel = shardDirRel.slice(0, -(`/${EMBEDDING_SHARD_DIR_BASENAME}`.length));
754
+ const stateDirAbs = await resolveSafeArchiveTarget(root, stateDirRel);
755
+ return await withEmbeddingGenerationLock(stateDirAbs, async (section) => {
756
+ const lockPath = embeddingGenerationLockPath(stateDirAbs);
757
+ const fence: EmbeddingGenerationFence = () => assertEmbeddingGenerationLockHeld(lockPath, section);
758
+ const markerRel = `${stateDirRel}/${EMBEDDING_MARKER_BASENAME}`;
759
+ const shardDirAbs = await resolveSafeArchiveTarget(root, shardDirRel);
760
+ const markerAbs = await resolveSafeArchiveTarget(root, markerRel);
761
+ const store = new EmbeddingIndexFileStore(
762
+ markerAbs,
763
+ shardDirAbs,
764
+ await resolveSafeArchiveTarget(root, `${stateDirRel}/embedding-fallback-status.json`),
765
+ );
766
+ await store.recoverIfInterrupted(fence);
767
+
768
+ // Codex P1: the deletion census is stale by the time the lock is held.
769
+ // When the live generation no longer matches the shared base (daemon
770
+ // indexed, added, or removed members), DEFER the removal — the local
771
+ // work survives and a later sync re-evaluates with a fresh base.
772
+ // No shared base (fresh catch-up) = no divergence semantics to protect.
773
+ const baseStates = baseStatesForGeneration(input.baseStates, shardDirRel);
774
+ if (baseStates.size > 0) {
775
+ const localCensus = await censusGenerationState(io, root, markerRel, markerAbs, shardDirAbs, shardDirRel);
776
+ const baseArr = [...baseStates].map(([relPath, state]) => ({ path: relPath, sha256: state.sha256 }));
777
+ const censusArr = [...localCensus].map(([relPath, sha256]) => ({ path: relPath, sha256 }));
778
+ const changedSinceBase =
779
+ censusArr.some((member) => {
780
+ // A MODIFIED member must also defer: removing the generation would
781
+ // erase the daemon's rewrite, not just the remote's stale copy.
782
+ const baseSha = baseStates.get(member.path)?.sha256;
783
+ return baseSha === undefined || baseSha !== member.sha256;
784
+ }) || baseArr.some((member) => !localCensus.has(member.path));
785
+ if (changedSinceBase) {
786
+ const removedPaths = new Set<string>([...localCensus.keys()]);
787
+ log.warn(
788
+ `embedding generation removal ${shardDirRel} deferred: disk diverged from the shared base under the mutation lock; local generation preserved`,
789
+ );
790
+ return {
791
+ shardDirRel,
792
+ upserted: 0,
793
+ deleted: 0,
794
+ writtenStates: new Map<string, OfflineSyncFileState>(),
795
+ removedPaths,
796
+ handledPaths: new Set(removedPaths),
797
+ deferredLocalDivergence: true,
798
+ };
799
+ }
800
+ }
801
+ const localShardRels = (await readdir(shardDirAbs).catch((err) => {
802
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return [] as string[];
803
+ throw err;
804
+ }))
805
+ .filter((name) => EMBEDDING_SHARD_FILE_PATTERN.test(name))
806
+ .map((name) => `${shardDirRel}/${name}`)
807
+ .sort();
808
+ await store.removePublishedGeneration(fence);
809
+ const removedPaths = new Set(localShardRels);
810
+ let deleted = removedPaths.size;
811
+ const markerPresent = await pathExists(markerAbs);
812
+ if (markerPresent) {
813
+ removedPaths.add(markerRel);
814
+ deleted += 1;
815
+ }
816
+ // Preserve the replicated tombstones through the configured hooks AFTER
817
+ // the atomic publication: raw removal must not lose the deletion
818
+ // revisions, or a downstream sync without base evidence retains stale
819
+ // entries. EVERY member (shards and marker) is recorded exactly ONCE with
820
+ // the remote revision when known, so a first hook call without a revision
821
+ // is never overwritten by an older replicated one. The storage hook
822
+ // tolerates already-absent paths.
823
+ let firstError: unknown = null;
824
+ for (const relPath of removedPaths) {
825
+ const memberAbs = relPath === markerRel ? markerAbs : await resolveSafeArchiveTarget(root, relPath);
826
+ const mtimeMs = input.deletionMtimeByPath?.get(relPath) ?? input.now;
827
+ if (io.deleteFile) {
828
+ try {
829
+ await io.deleteFile({
830
+ root: root.abs,
831
+ path: relPath,
832
+ filePath: await resolveSafeArchiveTarget(root, relPath),
833
+ mtimeMs,
834
+ });
835
+ } catch (err) {
836
+ firstError ??= err;
837
+ log.warn(`embedding generation removal: tombstone recording failed for ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
838
+ }
839
+ } else if (relPath === markerRel && markerPresent) {
840
+ await rm(markerAbs, { force: true });
841
+ }
842
+ }
843
+ if (firstError) throw firstError;
844
+ return {
845
+ shardDirRel,
846
+ upserted: 0,
847
+ deleted,
848
+ writtenStates: new Map(),
849
+ removedPaths,
850
+ handledPaths: new Set(removedPaths),
851
+ };
852
+ });
853
+ }
854
+
855
+ /** Defer a whole generation when the shared remote base would erase a local change. */
856
+ export function divergedEmbeddingGenerationDeferrals(options: {
857
+ incomingFiles: readonly { path: string; sha256: string }[];
858
+ baseFiles: readonly { path: string; sha256: string }[];
859
+ currentFiles: readonly { path: string; sha256: string }[];
860
+ }): string[] {
861
+ const base = new Map(options.baseFiles.map((f) => [f.path, f.sha256]));
862
+ const current = new Map(options.currentFiles.map((f) => [f.path, f.sha256]));
863
+ const incomingByPath = new Map(options.incomingFiles.map((f) => [f.path, f.sha256]));
864
+ const sharedDirs = new Set(options.baseFiles.map((f) => embeddingGenerationMembership(f.path)?.shardDir));
865
+ const conflictedDirs = new Set<string>();
866
+ for (const relPath of new Set([...base.keys(), ...current.keys(), ...incomingByPath.keys()])) {
867
+ const membership = embeddingGenerationMembership(relPath);
868
+ // Without a shared generation base this is an initial remote-authoritative
869
+ // import, not evidence of local indexing after a synchronized snapshot.
870
+ if (!membership || !sharedDirs.has(membership.shardDir)) continue;
871
+ const currentSha = current.get(relPath);
872
+ if (currentSha !== base.get(relPath) && currentSha !== incomingByPath.get(relPath)) {
873
+ conflictedDirs.add(membership.shardDir);
874
+ }
875
+ }
876
+ if (conflictedDirs.size === 0) return [];
877
+ const deferred: string[] = [];
878
+ // Include local/base members too: an all-absent remote generation still
879
+ // needs a concrete deferral so its removal cannot erase a local change.
880
+ for (const relPath of new Set([...base.keys(), ...current.keys(), ...incomingByPath.keys()])) {
881
+ const membership = embeddingGenerationMembership(relPath);
882
+ if (membership && conflictedDirs.has(membership.shardDir)) deferred.push(relPath);
883
+ }
884
+ return deferred.sort((left, right) => left.localeCompare(right));
885
+ }