@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
@@ -1,5 +1,4 @@
1
1
  import path from "node:path";
2
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
2
  import { log } from "./logger.js";
4
3
  import {
5
4
  resolveMemoryLifecycleCapabilities,
@@ -13,9 +12,23 @@ import {
13
12
  type HostEmbeddingProvider,
14
13
  normalizeHostEmbeddingVector,
15
14
  } from "./host-embedding-provider.js";
16
-
17
- type EmbeddingProviderType = "openai" | "local" | "host";
18
-
15
+ import {
16
+ EmbeddingIndexFileStore,
17
+ EmbeddingIndexStorageError,
18
+ type EmbeddingIndexEntry,
19
+ type EmbeddingIndexFile,
20
+ type EmbeddingIndexIdentity,
21
+ type EmbeddingProviderType,
22
+ EmbeddingIndexStoreIo,
23
+ } from "./embedding-index-storage.js";
24
+ export {
25
+ EmbeddingIndexCapacityError,
26
+ EmbeddingIndexStorageError,
27
+ } from "./embedding-index-storage.js";
28
+ export type {
29
+ EmbeddingIndexStatusFile,
30
+ EmbeddingIndexWriteFailure,
31
+ } from "./embedding-index-storage.js";
19
32
  type ProviderConfig = {
20
33
  type: EmbeddingProviderType;
21
34
  model: string;
@@ -24,24 +37,11 @@ type ProviderConfig = {
24
37
  hostProvider?: HostEmbeddingProvider;
25
38
  };
26
39
 
27
- type EmbeddingIndexEntry = {
28
- vector: number[];
29
- path: string;
30
- };
31
-
32
40
  type EmbeddingResult = {
33
41
  provider: ProviderConfig;
34
42
  vector: number[];
35
43
  };
36
44
 
37
- type EmbeddingIndexFile = {
38
- version: 1;
39
- provider: EmbeddingProviderType;
40
- model: string;
41
- entries: Record<string, EmbeddingIndexEntry>;
42
- };
43
-
44
- type EmbeddingIndexIdentity = Pick<EmbeddingIndexFile, "provider" | "model">;
45
45
  type EmbeddingIndexComparable =
46
46
  | EmbeddingIndexIdentity
47
47
  | Pick<ProviderConfig, "type" | "model">;
@@ -77,6 +77,14 @@ const DEFAULT_OPENAI_MODEL = "text-embedding-3-small";
77
77
  * The class name is kept for backward compatibility — `EmbeddingTimeoutError`
78
78
  * now signals "lookup backend unavailable" rather than strictly "timed out".
79
79
  */
80
+ import {
81
+ assertEmbeddingGenerationLockHeld,
82
+ embeddingGenerationLockPath,
83
+ EmbeddingGenerationLockUnavailableError,
84
+ withEmbeddingGenerationLock,
85
+ type EmbeddingGenerationLockSection,
86
+ } from "./embedding-generation-lock.js";
87
+
80
88
  export class EmbeddingTimeoutError extends Error {
81
89
  override readonly name = "EmbeddingTimeoutError" as const;
82
90
  constructor(message: string) {
@@ -165,12 +173,27 @@ function resolveEmbeddingIndexTimeoutMs(): number {
165
173
  export type EmbedMode = "lookup" | "index";
166
174
 
167
175
  export class EmbeddingFallback {
168
- private readonly indexPath: string;
176
+ private readonly store: EmbeddingIndexFileStore;
169
177
  private loaded: EmbeddingIndexFile | null = null;
178
+ /** True when `loaded` was read from disk rather than started fresh, so identity probes can trust it. */
179
+ private loadedFromDisk = false;
170
180
  private mutationQueue: Promise<void> = Promise.resolve();
171
181
 
172
- constructor(private readonly config: PluginConfig) {
173
- this.indexPath = path.join(config.memoryDir, "state", "embeddings.json");
182
+ /** The canonical state dir scoping this fallback's generation mutation lock. */
183
+ private readonly generationStateDir: string;
184
+ private readonly generationLockPath: string;
185
+ private lockSection: EmbeddingGenerationLockSection | null = null;
186
+
187
+ constructor(private readonly config: PluginConfig, storeIo?: EmbeddingIndexStoreIo) {
188
+ const stateDir = path.join(config.memoryDir, "state");
189
+ this.generationStateDir = stateDir;
190
+ this.generationLockPath = embeddingGenerationLockPath(stateDir);
191
+ this.store = new EmbeddingIndexFileStore(
192
+ path.join(stateDir, "embeddings.json"),
193
+ path.join(stateDir, "embeddings"),
194
+ path.join(stateDir, "embedding-fallback-status.json"),
195
+ storeIo,
196
+ );
174
197
  }
175
198
 
176
199
  async isAvailable(): Promise<boolean> {
@@ -265,61 +288,97 @@ export class EmbeddingFallback {
265
288
 
266
289
  let queryResult = await this.embedForSearch(query, provider, options);
267
290
  if (!queryResult) return [];
268
-
269
- const diskIdentity = await this.readIndexIdentityFromDisk();
270
- if (diskIdentity && !sameIndexIdentity(diskIdentity, queryResult.provider)) {
271
- const diskProvider = await this.resolveFallbackProviderForIndexIdentity(diskIdentity);
272
- if (diskProvider) {
273
- const diskQueryResult = await this.embedForSearch(query, diskProvider, options);
274
- if (diskQueryResult && sameIndexIdentity(diskIdentity, diskQueryResult.provider)) {
275
- queryResult = diskQueryResult;
291
+ // Effective query embedding: swapped to the disk index's provider
292
+ // below when the on-disk generation has a different identity. `let` so
293
+ // the swap reassigns; TS narrowing bound for control flow.
294
+ let active = queryResult;
295
+
296
+ // Recall fails OPEN on index read failures (corrupt/oversized/mixed
297
+ // generations, unreadable shard dirs): return [] without caching so a
298
+ // later mutation still revalidates and fails closed (issue #3146).
299
+ try {
300
+ const diskIdentity = await this.readIndexIdentityFromDisk();
301
+ if (diskIdentity && !sameIndexIdentity(diskIdentity, queryResult.provider)) {
302
+ // The provider swap needs a NETWORK embed: run it OUTSIDE the
303
+ // generation lock, then re-verify the identity under the lock below.
304
+ const diskProvider = await this.resolveFallbackProviderForIndexIdentity(diskIdentity);
305
+ if (diskProvider) {
306
+ const diskQueryResult = await this.embedForSearch(query, diskProvider, options);
307
+ if (diskQueryResult && sameIndexIdentity(diskIdentity, diskQueryResult.provider)) {
308
+ active = diskQueryResult;
309
+ } else {
310
+ log.debug(
311
+ `embedding fallback search skipped: preserved ${diskIdentity.provider}/${diskIdentity.model} index is unavailable for lookup`,
312
+ );
313
+ return [];
314
+ }
276
315
  } else {
277
316
  log.debug(
278
- `embedding fallback search skipped: preserved ${diskIdentity.provider}/${diskIdentity.model} index is unavailable for lookup`,
317
+ `embedding fallback search skipped: query provider ${queryResult.provider.type}/${queryResult.provider.model} does not match existing ${diskIdentity.provider}/${diskIdentity.model} index`,
279
318
  );
280
319
  return [];
281
320
  }
282
- } else {
321
+ }
322
+
323
+ // Cold load only (cache miss): the complete shard enumeration + read
324
+ // happens under the generation mutation lock so a concurrent swap can
325
+ // never leave a cached SUBSET of the generation (codex
326
+ // PRRT_kwDORJXyws6mZ72p). Warm cached searches stay lock-free.
327
+ let index: EmbeddingIndexFile;
328
+ try {
329
+ index = await this.loadIndexUnderGenerationLock(active.provider);
330
+ } catch (err) {
331
+ if (err instanceof EmbeddingGenerationLockUnavailableError) {
332
+ // Existing read diagnostic, recorded once per failed load; recall
333
+ // itself fails open.
334
+ await this.store.recordIndexStatusForLoad(
335
+ `generation mutation lock unavailable: ${err instanceof Error ? err.message : String(err)}`,
336
+ );
337
+ return [];
338
+ }
339
+ throw err;
340
+ }
341
+ const ids = Object.keys(index.entries);
342
+ if (ids.length === 0) return [];
343
+
344
+ const includePrefix = normalizePathPrefix(options.pathPrefix);
345
+ const excludePrefixes = (options.pathExcludePrefixes ?? [])
346
+ .map((p) => normalizePathPrefix(p))
347
+ .filter((p): p is string => typeof p === "string");
348
+
349
+ const scored = ids
350
+ .map((id) => {
351
+ const entry = index.entries[id];
352
+ return {
353
+ id,
354
+ path: entry.path,
355
+ score: cosineSimilarity(active.vector, entry.vector),
356
+ };
357
+ })
358
+ .filter((r) => {
359
+ if (!Number.isFinite(r.score)) return false;
360
+ const normalized = normalizeEntryPath(r.path);
361
+ if (includePrefix !== undefined && !normalized.startsWith(includePrefix)) {
362
+ return false;
363
+ }
364
+ for (const excl of excludePrefixes) {
365
+ if (normalized.startsWith(excl)) return false;
366
+ }
367
+ return true;
368
+ })
369
+ .sort((a, b) => b.score - a.score)
370
+ .slice(0, Math.max(1, limit));
371
+
372
+ return scored;
373
+ } catch (err) {
374
+ if (err instanceof EmbeddingIndexStorageError) {
283
375
  log.debug(
284
- `embedding fallback search skipped: query provider ${queryResult.provider.type}/${queryResult.provider.model} does not match existing ${diskIdentity.provider}/${diskIdentity.model} index`,
376
+ `embedding fallback index unreadable on recall path, failing open: ${err instanceof Error ? err.message : String(err)}`,
285
377
  );
286
378
  return [];
287
379
  }
380
+ throw err;
288
381
  }
289
-
290
- const index = await this.loadIndex(queryResult.provider);
291
- const ids = Object.keys(index.entries);
292
- if (ids.length === 0) return [];
293
-
294
- const includePrefix = normalizePathPrefix(options.pathPrefix);
295
- const excludePrefixes = (options.pathExcludePrefixes ?? [])
296
- .map((p) => normalizePathPrefix(p))
297
- .filter((p): p is string => typeof p === "string");
298
-
299
- const scored = ids
300
- .map((id) => {
301
- const entry = index.entries[id];
302
- return {
303
- id,
304
- path: entry.path,
305
- score: cosineSimilarity(queryResult.vector, entry.vector),
306
- };
307
- })
308
- .filter((r) => {
309
- if (!Number.isFinite(r.score)) return false;
310
- const normalized = normalizeEntryPath(r.path);
311
- if (includePrefix !== undefined && !normalized.startsWith(includePrefix)) {
312
- return false;
313
- }
314
- for (const excl of excludePrefixes) {
315
- if (normalized.startsWith(excl)) return false;
316
- }
317
- return true;
318
- })
319
- .sort((a, b) => b.score - a.score)
320
- .slice(0, Math.max(1, limit));
321
-
322
- return scored;
323
382
  }
324
383
 
325
384
  async indexFile(memoryId: string, content: string, filePath: string): Promise<void> {
@@ -336,25 +395,31 @@ export class EmbeddingFallback {
336
395
  });
337
396
  if (!result) return;
338
397
 
339
- await this.enqueueIndexMutation(async () => {
340
- const existing = await this.readIndexIdentityFromDisk();
341
- if (
342
- existing &&
343
- !sameIndexIdentity(existing, result.provider) &&
344
- !canReplaceIndexIdentity(existing, result.provider)
345
- ) {
346
- log.debug(
347
- `embedding fallback index update skipped: ${result.provider.type}/${result.provider.model} would replace existing ${existing.provider}/${existing.model} index`,
348
- );
349
- return;
398
+ await this.enqueueIndexMutation(memoryId, async () => {
399
+ try {
400
+ const existing = await this.readIndexIdentityFromDisk();
401
+ if (
402
+ existing &&
403
+ !sameIndexIdentity(existing, result.provider) &&
404
+ !canReplaceIndexIdentity(existing, result.provider)
405
+ ) {
406
+ log.debug(
407
+ `embedding fallback index update skipped: ${result.provider.type}/${result.provider.model} would replace existing ${existing.provider}/${existing.model} index`,
408
+ );
409
+ return;
410
+ }
411
+ const index = await this.loadIndex(result.provider);
412
+ const relPath = toMemoryRelativePath(this.config.memoryDir, filePath);
413
+ index.entries[memoryId] = {
414
+ vector: result.vector,
415
+ path: relPath,
416
+ };
417
+ await this.saveIndex(index, { touchedIds: [memoryId], memoryId });
418
+ } catch (err) {
419
+ await this.store.recordIndexWriteOutcome(err, memoryId);
420
+ throw err;
350
421
  }
351
- const index = await this.loadIndex(result.provider);
352
- const relPath = toMemoryRelativePath(this.config.memoryDir, filePath);
353
- index.entries[memoryId] = {
354
- vector: result.vector,
355
- path: relPath,
356
- };
357
- await this.saveIndex(index);
422
+ await this.store.recordIndexWriteOutcome(null, memoryId);
358
423
  });
359
424
  }
360
425
 
@@ -362,7 +427,7 @@ export class EmbeddingFallback {
362
427
  const provider = await this.resolveProvider();
363
428
  if (!provider) return;
364
429
 
365
- await this.enqueueIndexMutation(async () => {
430
+ await this.enqueueIndexMutation(memoryId, async () => {
366
431
  const providers = [provider];
367
432
  const diskIdentity = await this.readIndexIdentityFromDisk();
368
433
  if (
@@ -381,17 +446,71 @@ export class EmbeddingFallback {
381
446
  }
382
447
  }
383
448
 
449
+ let saved = false;
384
450
  for (const indexProvider of providers) {
385
- const index = await this.loadIndex(indexProvider);
386
- if (!index.entries[memoryId]) continue;
387
- delete index.entries[memoryId];
388
- await this.saveIndex(index);
451
+ try {
452
+ const index = await this.loadIndex(indexProvider);
453
+ if (!index.entries[memoryId]) continue;
454
+ delete index.entries[memoryId];
455
+ await this.saveIndex(index, { touchedIds: [memoryId], memoryId });
456
+ saved = true;
457
+ } catch (err) {
458
+ await this.store.recordIndexWriteOutcome(err, memoryId);
459
+ throw err;
460
+ }
389
461
  }
462
+ // A no-op removal (id absent from every candidate index) performs no
463
+ // save: it must NOT clear a recorded persistence failure, or the
464
+ // operator console would report an unresolved failure as fixed
465
+ // without any demonstration that storage is writable (round 6).
466
+ if (saved) await this.store.recordIndexWriteOutcome(null, memoryId);
390
467
  });
391
468
  }
392
469
 
393
- private enqueueIndexMutation<T>(mutation: () => Promise<T>): Promise<T> {
394
- const run = this.mutationQueue.catch(() => undefined).then(mutation);
470
+ private enqueueIndexMutation<T>(memoryId: string | undefined, mutation: () => Promise<T>): Promise<T> {
471
+ const run = this.mutationQueue
472
+ .catch(() => undefined)
473
+ .then(() =>
474
+ withEmbeddingGenerationLock(this.generationStateDir, async (section) => {
475
+ this.lockSection = section;
476
+ try {
477
+ // A peer process may have persisted while this process did not
478
+ // hold the generation lock: drop any cached index so the
479
+ // mutation loads the real disk state (codex P1).
480
+ this.loaded = null;
481
+ this.loadedFromDisk = false;
482
+ // Recover an interrupted replacement BEFORE the mutation's identity
483
+ // probes and existence checks: a deletion no-op must not skip
484
+ // persistence while the published generation sits in the rename-gap
485
+ // backup, and insertions must load the restored vectors rather than
486
+ // a fresh empty index (issue #3148, codex round 3).
487
+ let recovered = false;
488
+ try {
489
+ recovered = await this.store.recoverIfInterrupted(this.generationFence());
490
+ } catch (err) {
491
+ // Record the recovery failure durably BEFORE rejecting the
492
+ // mutation: the write-outcome try below is never reached.
493
+ await this.store.recordIndexWriteOutcome(err).catch(() => undefined);
494
+ throw err;
495
+ }
496
+ if (recovered) {
497
+ this.loaded = null;
498
+ this.loadedFromDisk = false;
499
+ }
500
+ return await mutation();
501
+ } finally {
502
+ this.lockSection = null;
503
+ }
504
+ }).catch(async (err) => {
505
+ // A lock ACQUISITION failure never reaches the mutation closure's
506
+ // own write-outcome recording. Route it through the existing
507
+ // durable diagnostic exactly once (codex PRRT_kwDORJXyws6mZ8qS);
508
+ // in-lock failures were already recorded by the closure.
509
+ if (err instanceof EmbeddingGenerationLockUnavailableError) {
510
+ await this.store.recordIndexWriteOutcome(err, memoryId).catch(() => undefined);
511
+ }
512
+ throw err;
513
+ }));
395
514
  this.mutationQueue = run.then(
396
515
  () => undefined,
397
516
  () => undefined,
@@ -666,32 +785,108 @@ export class EmbeddingFallback {
666
785
  }
667
786
  }
668
787
 
788
+ /**
789
+ * Load the authoritative generation for `provider`. STRICT: an unreadable,
790
+ * malformed, foreign-shaped, or mixed-identity index throws
791
+ * EmbeddingIndexStorageError instead of ever returning a partial view —
792
+ * recall catches it at its boundary and fails open uncached; mutations
793
+ * propagate it and fail closed. This.loaded is only ever assigned a fully
794
+ * validated generation.
795
+ */
796
+ /**
797
+ * A fence the store calls immediately before destructive writes: reasserts
798
+ * THIS section's lock ownership after potentially long serialization.
799
+ * Fail closed when no section is held — persisting unsynchronized would
800
+ * reintroduce the lost-update race.
801
+ */
802
+ private generationFence(): () => Promise<void> {
803
+ const section = this.lockSection;
804
+ if (!section) {
805
+ return async () => {
806
+ throw new EmbeddingIndexStorageError(
807
+ "embedding index mutation attempted outside the generation lock; refusing to publish",
808
+ );
809
+ };
810
+ }
811
+ return () => assertEmbeddingGenerationLockHeld(this.generationLockPath, section);
812
+ }
813
+
814
+ /**
815
+ * Cache-warm path: a normal search serves the in-memory index WITHOUT the
816
+ * lock (pre-existing staleness semantics: a peer swap between searches is
817
+ * refreshed by the next cold load). On a cache miss the COMPLETE load runs
818
+ * under the generation mutation lock so the enumeration + shard reads see
819
+ * exactly one generation. The identity is re-checked under the lock on the
820
+ * cold path; the caller has already performed any provider-swap embed
821
+ * outside the lock.
822
+ */
823
+ private async loadIndexUnderGenerationLock(provider: ProviderConfig): Promise<EmbeddingIndexFile> {
824
+ if (this.loaded && this.loadedFromDisk) return this.loaded;
825
+ return await withEmbeddingGenerationLock(this.generationStateDir, async () => {
826
+ // Another in-process consumer may have warmed the cache while this
827
+ // call waited on the lock.
828
+ if (this.loaded && this.loadedFromDisk) return this.loaded;
829
+ // Re-check the on-disk identity UNDER the lock: a peer swap during the
830
+ // caller's embed means the cold load must fail open, never serve a
831
+ // mixed generation (codex PRRT_kwDORJXyws6mZ72p).
832
+ const diskIdentity = await this.store.identityFromDisk();
833
+ if (diskIdentity && !sameIndexIdentity(diskIdentity, provider)) {
834
+ throw new EmbeddingIndexStorageError(
835
+ `embedding index identity swapped to ${diskIdentity.provider}/${diskIdentity.model} during the cold load; failing open`,
836
+ );
837
+ }
838
+ return await this.loadIndex(provider);
839
+ });
840
+ }
841
+
669
842
  private async loadIndex(provider: ProviderConfig): Promise<EmbeddingIndexFile> {
670
843
  if (this.loaded && this.loaded.provider === provider.type && this.loaded.model === provider.model) {
671
844
  return this.loaded;
672
845
  }
673
846
 
674
- try {
675
- const raw = await readFile(this.indexPath, "utf-8");
676
- const parsed = JSON.parse(raw) as EmbeddingIndexFile;
677
- if (
678
- parsed &&
679
- parsed.version === 1 &&
680
- parsed.provider === provider.type &&
681
- parsed.model === provider.model &&
682
- parsed.entries &&
683
- typeof parsed.entries === "object"
684
- ) {
685
- this.loaded = {
686
- version: 1,
687
- provider: provider.type,
688
- model: provider.model,
689
- entries: parsed.entries,
690
- };
691
- return this.loaded;
847
+ const merged: Record<string, EmbeddingIndexEntry> = {};
848
+ let diskIdentity: EmbeddingIndexIdentity | null = null;
849
+
850
+ const layout = await this.store.detectLayout();
851
+ if (layout === "sharded") {
852
+ // Published generation: shards are the ONLY authoritative state. A
853
+ // legacy embeddings.json left over by a crash between the atomic
854
+ // publish and its recovery rename is never merged (single-generation
855
+ // rule, issue #3146).
856
+ diskIdentity = await this.store.readShardGenerationInto(merged);
857
+ } else if (layout === "legacy") {
858
+ const read = await this.store.readLegacy();
859
+ if (read.outcome === "ok") {
860
+ diskIdentity = { provider: read.file.provider, model: read.file.model };
861
+ Object.assign(merged, read.file.entries);
862
+ } else if (read.outcome === "unreadable") {
863
+ await this.store.recordIndexStatusForLoad(read.reason);
864
+ throw new EmbeddingIndexStorageError(
865
+ `refusing to continue from an unreadable embedding index at ${this.store.legacyPath}: ${read.reason}`,
866
+ );
867
+ } else if (read.outcome === "foreign") {
868
+ // Valid JSON but not an index we recognize (wrong version/shape).
869
+ // Overwriting it would destroy unknown data, so mutation fails
870
+ // closed; the file stays in place for recovery.
871
+ await this.store.recordIndexStatusForLoad("unrecognized index format");
872
+ throw new EmbeddingIndexStorageError(
873
+ `refusing to continue from a malformed embedding index at ${this.store.legacyPath} (unrecognized format); file preserved in place`,
874
+ );
692
875
  }
693
- } catch {
694
- // ignore and create a new index
876
+ // absent falls through to a fresh index; a readable legacy index with
877
+ // a different provider identity keeps the existing replace semantics
878
+ // (gated by canReplaceIndexIdentity at the mutation sites).
879
+ }
880
+
881
+ if (diskIdentity && diskIdentity.provider === provider.type && diskIdentity.model === provider.model) {
882
+ this.loaded = {
883
+ version: 1,
884
+ provider: provider.type,
885
+ model: provider.model,
886
+ entries: merged,
887
+ };
888
+ this.loadedFromDisk = true;
889
+ return this.loaded;
695
890
  }
696
891
 
697
892
  this.loaded = {
@@ -700,49 +895,38 @@ export class EmbeddingFallback {
700
895
  model: provider.model,
701
896
  entries: {},
702
897
  };
898
+ this.loadedFromDisk = false;
703
899
  return this.loaded;
704
900
  }
705
901
 
706
902
  private async readIndexIdentityFromDisk(): Promise<EmbeddingIndexIdentity | null> {
707
- try {
708
- const raw = await readFile(this.indexPath, "utf-8");
709
- const parsed = JSON.parse(raw) as Partial<EmbeddingIndexFile> | null;
710
- if (
711
- parsed &&
712
- parsed.version === 1 &&
713
- (parsed.provider === "openai" ||
714
- parsed.provider === "local" ||
715
- parsed.provider === "host") &&
716
- typeof parsed.model === "string" &&
717
- parsed.model.length > 0
718
- ) {
719
- return {
720
- provider: parsed.provider,
721
- model: parsed.model,
722
- };
723
- }
724
- } catch {
725
- // Missing or invalid indexes are treated as absent; loadIndex() owns
726
- // creating a fresh file when it is safe to write.
903
+ // After a load, `loaded` mirrors the disk state exactly (every mutation
904
+ // saves through saveIndex and a failed save invalidates the cache), so
905
+ // skip re-reading on every mutation.
906
+ if (this.loaded && this.loadedFromDisk) {
907
+ return { provider: this.loaded.provider, model: this.loaded.model };
727
908
  }
728
- return null;
909
+ return this.store.identityFromDisk();
729
910
  }
730
911
 
731
- private async saveIndex(index: EmbeddingIndexFile): Promise<void> {
732
- const dir = path.dirname(this.indexPath);
733
- await mkdir(dir, { recursive: true });
734
- const tempPath = path.join(
735
- dir,
736
- `.embeddings.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`,
737
- );
912
+ /**
913
+ * Persist the index through the file store. On any failure the in-memory
914
+ * cache is invalidated: entries mutated in the cached map must never be
915
+ * mistaken for durable state by a retry or a later search.
916
+ */
917
+ private async saveIndex(
918
+ index: EmbeddingIndexFile,
919
+ opts: { touchedIds?: readonly string[]; memoryId?: string } = {},
920
+ ): Promise<void> {
738
921
  try {
739
- await writeFile(tempPath, JSON.stringify(index), "utf-8");
740
- await rename(tempPath, this.indexPath);
922
+ await this.store.persist(index, { ...opts, fence: this.generationFence() });
741
923
  } catch (err) {
742
- await rm(tempPath, { force: true }).catch(() => undefined);
924
+ this.loaded = null;
925
+ this.loadedFromDisk = false;
743
926
  throw err;
744
927
  }
745
928
  this.loaded = index;
929
+ this.loadedFromDisk = true;
746
930
  }
747
931
  }
748
932