@remnic/core 9.69.86 → 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 (513) hide show
  1. package/dist/access-admin-ops-surface.js +74 -67
  2. package/dist/access-authorization-probe.js +75 -68
  3. package/dist/access-boundary.js +74 -67
  4. package/dist/access-citation-namespace.js +2 -2
  5. package/dist/access-cli.js +158 -151
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-extraction-force-flush.d.ts.map +1 -1
  8. package/dist/access-extraction-force-flush.js +74 -67
  9. package/dist/access-http-briefing.js +75 -68
  10. package/dist/access-http-offline-stream.d.ts.map +1 -1
  11. package/dist/access-http-offline-stream.js +1 -1
  12. package/dist/access-http-query.js +76 -69
  13. package/dist/access-http.js +100 -93
  14. package/dist/access-identity-continuity-surface.js +74 -67
  15. package/dist/access-lcm-surface.js +74 -67
  16. package/dist/access-mcp-output-schemas.d.ts.map +1 -1
  17. package/dist/access-mcp-output-schemas.js +1 -1
  18. package/dist/access-mcp.d.ts.map +1 -1
  19. package/dist/access-mcp.js +89 -82
  20. package/dist/access-namespace-preflight.js +74 -67
  21. package/dist/access-observe-write-category.js +19 -13
  22. package/dist/access-observe-write-surface.js +74 -67
  23. package/dist/access-offline-manifest.js +15 -10
  24. package/dist/access-operations-batch.js +78 -71
  25. package/dist/access-operations.js +85 -78
  26. package/dist/access-recall-concurrency.js +74 -67
  27. package/dist/access-recall-diagnostics.js +4 -4
  28. package/dist/access-recall-response.js +74 -67
  29. package/dist/access-recall-surface.js +74 -67
  30. package/dist/access-schema.js +15 -7
  31. package/dist/access-service-helpers.d.ts +1 -0
  32. package/dist/access-service-helpers.d.ts.map +1 -1
  33. package/dist/access-service-helpers.js +1 -1
  34. package/dist/access-service.d.ts.map +1 -1
  35. package/dist/access-service.js +74 -67
  36. package/dist/access-surface-catalog.js +76 -69
  37. package/dist/access-surface-catalog.js.map +1 -1
  38. package/dist/active-recall.js +34 -28
  39. package/dist/active-recall.js.map +1 -1
  40. package/dist/adapters/index.js +4 -4
  41. package/dist/adapters/registry.js +2 -2
  42. package/dist/{auto-sync-WEUDMXUW.js → auto-sync-U5N3CB5Y.js} +10 -10
  43. package/dist/background-generation.js +2 -2
  44. package/dist/briefing.js +18 -18
  45. package/dist/buffer.js +9 -8
  46. package/dist/calibration.js +2 -2
  47. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-YO5QJ6L3.js} +2 -2
  48. package/dist/{capsule-merge-3MNURAQ3.js → capsule-merge-ZCXUPLNJ.js} +2 -2
  49. package/dist/causal-behavior.js +2 -2
  50. package/dist/causal-chain.js +2 -2
  51. package/dist/causal-consolidation.js +21 -21
  52. package/dist/causal-retrieval.js +2 -2
  53. package/dist/causal-trajectory-graph.js +3 -3
  54. package/dist/causal-trajectory.js +1 -1
  55. package/dist/{chunk-JE334SIA.js → chunk-22DYJLFN.js} +2 -2
  56. package/dist/{chunk-DGAWEJVI.js → chunk-2DLWLNO4.js} +3 -3
  57. package/dist/{chunk-J3IWHCMF.js → chunk-2DT7PIDZ.js} +3 -3
  58. package/dist/{chunk-NKFLHW7J.js → chunk-2NAD4YCI.js} +2 -2
  59. package/dist/{chunk-SV72F4DS.js → chunk-2WLMWP2T.js} +2 -2
  60. package/dist/{chunk-IVH7INFM.js → chunk-3O5BB45T.js} +7 -7
  61. package/dist/{chunk-RDEUG2M2.js → chunk-3SGTV7RD.js} +6 -6
  62. package/dist/{chunk-QOPB6KE3.js → chunk-3TB2XQ3C.js} +2 -2
  63. package/dist/{chunk-C7JAUNFC.js → chunk-4KN2JLQV.js} +2 -2
  64. package/dist/{chunk-TIQZNDZ5.js → chunk-4MP7XR5J.js} +2 -2
  65. package/dist/chunk-4NSLQ7EU.js +137 -0
  66. package/dist/chunk-4NSLQ7EU.js.map +1 -0
  67. package/dist/{chunk-7DE2NOON.js → chunk-4O4XCBOS.js} +3 -3
  68. package/dist/{chunk-SLEAJ67D.js → chunk-4OQET73E.js} +4 -4
  69. package/dist/{chunk-Q3THVX75.js → chunk-4RL3JOB3.js} +2 -2
  70. package/dist/{chunk-GDYU6VF4.js → chunk-5AYUHSAU.js} +2 -2
  71. package/dist/{chunk-B47OHVFT.js → chunk-5E6RYRYW.js} +7 -7
  72. package/dist/{chunk-2U3R3DDH.js → chunk-5FAGCOZD.js} +9 -9
  73. package/dist/{chunk-CRKWSSS4.js → chunk-5MNUBT7T.js} +2 -2
  74. package/dist/{chunk-CK5KOVHW.js → chunk-5R627MYY.js} +3 -2
  75. package/dist/{chunk-CK5KOVHW.js.map → chunk-5R627MYY.js.map} +1 -1
  76. package/dist/chunk-5X4MXNEU.js +64 -0
  77. package/dist/chunk-5X4MXNEU.js.map +1 -0
  78. package/dist/{chunk-52II7PB6.js → chunk-645WOU5I.js} +4 -4
  79. package/dist/{chunk-7G6WUOLB.js → chunk-66DKTOBY.js} +2 -2
  80. package/dist/{chunk-AZ2X5LNA.js → chunk-6LYAEM54.js} +2 -2
  81. package/dist/{chunk-DUJYRVUD.js → chunk-6O2VUWV6.js} +3 -3
  82. package/dist/{chunk-IMAP236M.js → chunk-6UQFJULG.js} +2 -2
  83. package/dist/chunk-75PT4N7Q.js +75 -0
  84. package/dist/chunk-75PT4N7Q.js.map +1 -0
  85. package/dist/{chunk-2I6VHX6Q.js → chunk-76M5OK7V.js} +2 -2
  86. package/dist/{chunk-OEBLZZKK.js → chunk-7XG7KMIW.js} +2 -2
  87. package/dist/{chunk-FYAU4S6E.js → chunk-A4VSOUCB.js} +3 -3
  88. package/dist/{chunk-GEX5NI36.js → chunk-AE5U5NMS.js} +2 -2
  89. package/dist/{chunk-UNJOB4DY.js → chunk-AEHVZY3X.js} +2 -2
  90. package/dist/{chunk-ML2MVAEZ.js → chunk-AEU4EQGV.js} +1 -1
  91. package/dist/{chunk-3SG4FGDV.js → chunk-AWD36R4D.js} +5 -2
  92. package/dist/chunk-AWD36R4D.js.map +1 -0
  93. package/dist/{chunk-JVY556FW.js → chunk-AWG3TIAY.js} +2 -2
  94. package/dist/{chunk-JX6USXZR.js → chunk-AXC4WSF3.js} +4 -4
  95. package/dist/{chunk-BOUFVFV2.js → chunk-AZSGSRIM.js} +2 -2
  96. package/dist/{chunk-VZBW6CDG.js → chunk-B3O3IRZN.js} +1 -1
  97. package/dist/{chunk-4V44AA7O.js → chunk-BGI4NUAO.js} +2 -2
  98. package/dist/{chunk-BNPLDVM6.js → chunk-BJVO4H5C.js} +2 -2
  99. package/dist/{chunk-S42HZILI.js → chunk-BLVR4WYU.js} +3 -3
  100. package/dist/{chunk-JXMECXMF.js → chunk-BOZJD5PK.js} +2 -2
  101. package/dist/{chunk-6DTJBTHL.js → chunk-BUQKSQXL.js} +15 -3
  102. package/dist/chunk-BUQKSQXL.js.map +1 -0
  103. package/dist/{chunk-OWHERGF2.js → chunk-BXLOS5AJ.js} +2 -2
  104. package/dist/{chunk-EVBI7S4J.js → chunk-C6U57KG4.js} +4 -4
  105. package/dist/{chunk-RB776UH3.js → chunk-C7V3KPKJ.js} +235 -93
  106. package/dist/chunk-C7V3KPKJ.js.map +1 -0
  107. package/dist/{chunk-T4BE5KJM.js → chunk-C7ZDKWPS.js} +4 -4
  108. package/dist/{chunk-HKFJ3PF3.js → chunk-CRXRZA3G.js} +2 -2
  109. package/dist/{chunk-NMBX4D7P.js → chunk-CZJA2EQE.js} +2 -2
  110. package/dist/{chunk-BJX3VQWZ.js → chunk-D5BSQR63.js} +2 -2
  111. package/dist/{chunk-MFZACYZ6.js → chunk-DGS34QSY.js} +2 -2
  112. package/dist/{chunk-UVYI6VIX.js → chunk-DQEMWVMT.js} +1 -1
  113. package/dist/{chunk-DQISIT63.js → chunk-DZ42MDZE.js} +3 -3
  114. package/dist/{chunk-5ELENZTA.js → chunk-EDIH6X2P.js} +2 -2
  115. package/dist/{chunk-3BTFBQLO.js → chunk-EFHNFTWB.js} +49 -3
  116. package/dist/chunk-EFHNFTWB.js.map +1 -0
  117. package/dist/{chunk-C3QXITIP.js → chunk-EQUD7FP4.js} +2 -2
  118. package/dist/chunk-ETUZFFPU.js +218 -0
  119. package/dist/chunk-ETUZFFPU.js.map +1 -0
  120. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  121. package/dist/{chunk-IA3X2Y4K.js → chunk-FJLQXMTU.js} +3 -3
  122. package/dist/{chunk-TY446V34.js → chunk-FKD656D2.js} +2 -2
  123. package/dist/{chunk-CLXTDOKV.js → chunk-FUNBJQYR.js} +2 -2
  124. package/dist/{chunk-CLSYE5OZ.js → chunk-G2STTRYB.js} +2 -2
  125. package/dist/{chunk-SQDKX4RW.js → chunk-GWM6T5EQ.js} +2 -2
  126. package/dist/{chunk-AXDOF4VI.js → chunk-GZDTVYK2.js} +2 -2
  127. package/dist/{chunk-55FCXCXJ.js → chunk-H56VY4BY.js} +7 -7
  128. package/dist/{chunk-OM5BIPBR.js → chunk-H5BJWPUI.js} +4 -4
  129. package/dist/{chunk-7QK5TUTW.js → chunk-HLB7GKGG.js} +2 -2
  130. package/dist/{chunk-CHDGJJV6.js → chunk-HWLIMQE2.js} +2 -2
  131. package/dist/{chunk-QB75LKQQ.js → chunk-IOSZHHJC.js} +34 -15
  132. package/dist/chunk-IOSZHHJC.js.map +1 -0
  133. package/dist/{chunk-NAFPHFDM.js → chunk-JNLEJE4A.js} +2 -2
  134. package/dist/{chunk-7UUKRS3J.js → chunk-JVPWRNHA.js} +3 -3
  135. package/dist/{chunk-6ET2VII7.js → chunk-JYG2334B.js} +2 -2
  136. package/dist/chunk-K6T33R4Q.js +535 -0
  137. package/dist/chunk-K6T33R4Q.js.map +1 -0
  138. package/dist/{chunk-2QG4SLZI.js → chunk-KF7PQ7G2.js} +2 -2
  139. package/dist/{chunk-WIKO4SCR.js → chunk-LHRQBMJN.js} +2 -2
  140. package/dist/{chunk-IFCKHPY4.js → chunk-LO4UKPKX.js} +2 -2
  141. package/dist/{chunk-TEV2JGU5.js → chunk-LU7E2MXE.js} +19 -19
  142. package/dist/{chunk-RJLV4BKR.js → chunk-LV5H6FTN.js} +2 -2
  143. package/dist/{chunk-IBGEHKQ6.js → chunk-LW3HKRL7.js} +3 -3
  144. package/dist/{chunk-3XQYM4RE.js → chunk-M6C7SOR6.js} +2 -2
  145. package/dist/{chunk-JN72EPOE.js → chunk-MAAL6OZV.js} +7 -7
  146. package/dist/chunk-MAAL6OZV.js.map +1 -0
  147. package/dist/{chunk-MY2PLXSM.js → chunk-MCTM4YPL.js} +2 -1
  148. package/dist/{chunk-MY2PLXSM.js.map → chunk-MCTM4YPL.js.map} +1 -1
  149. package/dist/{chunk-T4QGUXCV.js → chunk-MXCAJKMA.js} +9 -9
  150. package/dist/{chunk-BMYZTZ5D.js → chunk-MY7B3HYC.js} +2 -2
  151. package/dist/{chunk-NB3LRY4T.js → chunk-N4PYB235.js} +2 -2
  152. package/dist/{chunk-S3YM4SZQ.js → chunk-N7QV3CMX.js} +2 -2
  153. package/dist/{chunk-3GN2PEOT.js → chunk-NYKIUQL6.js} +2 -2
  154. package/dist/{chunk-WQZZDN5M.js → chunk-OCFXVDBZ.js} +322 -376
  155. package/dist/chunk-OCFXVDBZ.js.map +1 -0
  156. package/dist/{chunk-L2LBMKSI.js → chunk-OK52IY3W.js} +10 -10
  157. package/dist/{chunk-WB2I2EQK.js → chunk-P2ODTYOF.js} +2 -2
  158. package/dist/{chunk-RIEX7NOZ.js → chunk-PEBHPCX2.js} +2 -2
  159. package/dist/{chunk-VYCOJKSX.js → chunk-PO7NXNGC.js} +2 -2
  160. package/dist/{chunk-G6SVZWPB.js → chunk-PZX3AQLF.js} +44 -1
  161. package/dist/chunk-PZX3AQLF.js.map +1 -0
  162. package/dist/{chunk-XAITGGX5.js → chunk-Q4AUIFUA.js} +4 -4
  163. package/dist/{chunk-EU3N5MK3.js → chunk-QAW55ZHJ.js} +4 -4
  164. package/dist/{chunk-SVZWVZ7E.js → chunk-QB57WIS6.js} +101 -93
  165. package/dist/chunk-QB57WIS6.js.map +1 -0
  166. package/dist/{chunk-I4G4UWQA.js → chunk-QI2E6ISQ.js} +1 -1
  167. package/dist/{chunk-CK5U7JGH.js → chunk-QI7K7BZ5.js} +2 -2
  168. package/dist/{chunk-VTPASHTO.js → chunk-R3X2T2LP.js} +3 -3
  169. package/dist/{chunk-I53NAHZ4.js → chunk-RBITPCMK.js} +10 -9
  170. package/dist/chunk-RBITPCMK.js.map +1 -0
  171. package/dist/{chunk-EJPF6XDQ.js → chunk-RDJ6C7NZ.js} +3 -3
  172. package/dist/{chunk-C2WWQ4OR.js → chunk-RMMA44HE.js} +1 -1
  173. package/dist/chunk-RMMA44HE.js.map +1 -0
  174. package/dist/{chunk-QC56VZXC.js → chunk-S2GE2K36.js} +85 -82
  175. package/dist/{chunk-QC56VZXC.js.map → chunk-S2GE2K36.js.map} +1 -1
  176. package/dist/{chunk-2UPTRDGM.js → chunk-SCXURCVT.js} +3 -3
  177. package/dist/{chunk-S55WTPFJ.js → chunk-STEOT4DI.js} +20 -20
  178. package/dist/chunk-STEOT4DI.js.map +1 -0
  179. package/dist/{chunk-N3MNDVJO.js → chunk-SWBC3UER.js} +2 -2
  180. package/dist/{chunk-ZTHGFTMW.js → chunk-TKC5LZ43.js} +2 -2
  181. package/dist/chunk-TYT4TEQV.js +45 -0
  182. package/dist/chunk-TYT4TEQV.js.map +1 -0
  183. package/dist/{chunk-YRLHEEQT.js → chunk-U6VC6RP3.js} +2 -2
  184. package/dist/chunk-UCPGOUDZ.js +726 -0
  185. package/dist/chunk-UCPGOUDZ.js.map +1 -0
  186. package/dist/{chunk-MOQPEFLN.js → chunk-UP75KL3L.js} +2 -2
  187. package/dist/{chunk-D5F7TKMH.js → chunk-V4JZCTMK.js} +2 -2
  188. package/dist/{chunk-NAYBHNGN.js → chunk-VALKBYBN.js} +2 -2
  189. package/dist/{chunk-BUKK5SWA.js → chunk-VDX2J7OX.js} +2 -2
  190. package/dist/{chunk-H36X4NRH.js → chunk-VVX5PZFS.js} +2 -2
  191. package/dist/{chunk-RCQKAOOB.js → chunk-W3XXFPRI.js} +52 -52
  192. package/dist/{chunk-W75BBBSY.js → chunk-WKY7N3L7.js} +11 -11
  193. package/dist/{chunk-GZAGLCQH.js → chunk-X5SHE2O3.js} +3 -3
  194. package/dist/{chunk-OAOBL6OY.js → chunk-XOV7NRY5.js} +5 -5
  195. package/dist/{chunk-FZVROZKC.js → chunk-XVZA6VXC.js} +4 -4
  196. package/dist/{chunk-RGMVMVMF.js → chunk-YBA52OKG.js} +2 -1
  197. package/dist/chunk-YBA52OKG.js.map +1 -0
  198. package/dist/{chunk-IP5VLSLO.js → chunk-YJ7MW627.js} +2 -2
  199. package/dist/{chunk-HS75MCQF.js → chunk-YRULQW5J.js} +2 -2
  200. package/dist/{chunk-DINRT75P.js → chunk-YSBL7VPO.js} +2 -2
  201. package/dist/{chunk-QWUJWBZ5.js → chunk-Z3O53WZR.js} +2 -2
  202. package/dist/{chunk-KERNN6BC.js → chunk-Z5LP6ILP.js} +2 -2
  203. package/dist/{chunk-5FNZ3G6X.js → chunk-Z6EZCN6Z.js} +5 -5
  204. package/dist/chunk-ZJP66Q4I.js +48 -0
  205. package/dist/chunk-ZJP66Q4I.js.map +1 -0
  206. package/dist/{chunk-JS3S2HVG.js → chunk-ZZZ4I455.js} +2 -2
  207. package/dist/cli.js +122 -115
  208. package/dist/coding/export-okf-codegraph.js +19 -19
  209. package/dist/coding/pre-action-gate.js +2 -2
  210. package/dist/compounding/engine.js +19 -19
  211. package/dist/config.js +34 -28
  212. package/dist/connectors/codex-materialize-runner.js +18 -18
  213. package/dist/connectors/index.js +18 -18
  214. package/dist/console/state.d.ts +30 -0
  215. package/dist/console/state.d.ts.map +1 -1
  216. package/dist/contradiction/index.js +7 -7
  217. package/dist/{contradiction-scan-5EJYRKD3.js → contradiction-scan-TAGMOUQ2.js} +3 -3
  218. package/dist/convergence-refresh.js +20 -20
  219. package/dist/corpus-watermark.js +20 -20
  220. package/dist/correction/correction-access-wiring.d.ts.map +1 -1
  221. package/dist/dashboard-runtime.js +2 -2
  222. package/dist/deep-recall-http-glue.js +15 -8
  223. package/dist/deep-recall-policy-llm.js +3 -3
  224. package/dist/deep-recall.js +2 -2
  225. package/dist/embedding-fallback.d.ts +41 -31
  226. package/dist/embedding-fallback.d.ts.map +1 -1
  227. package/dist/embedding-fallback.js +9 -1
  228. package/dist/embedding-generation-lock.d.ts +49 -0
  229. package/dist/embedding-generation-lock.d.ts.map +1 -0
  230. package/dist/embedding-generation-lock.js +19 -0
  231. package/dist/embedding-index-storage.d.ts +276 -0
  232. package/dist/embedding-index-storage.d.ts.map +1 -0
  233. package/dist/embedding-index-storage.js +28 -0
  234. package/dist/entity-retrieval.js +19 -19
  235. package/dist/event-order-recall.js +3 -3
  236. package/dist/explicit-capture.js +6 -6
  237. package/dist/external-wiki-access.js +75 -68
  238. package/dist/extraction-faithfulness.js +3 -3
  239. package/dist/extraction-judge.js +3 -3
  240. package/dist/extraction-liveness.d.ts +9 -0
  241. package/dist/extraction-liveness.d.ts.map +1 -1
  242. package/dist/extraction-liveness.js +2 -1
  243. package/dist/extraction-llm-health.d.ts +32 -0
  244. package/dist/extraction-llm-health.d.ts.map +1 -0
  245. package/dist/extraction-llm-health.js +17 -0
  246. package/dist/extraction-span-config.js +3 -2
  247. package/dist/extraction-span-materialize.js +2 -2
  248. package/dist/extraction.js +29 -28
  249. package/dist/fallback-llm.js +2 -2
  250. package/dist/{first-start-migration-GAT7G733.js → first-start-migration-7OATHJKB.js} +4 -4
  251. package/dist/{graph-edge-decay-PVKDKLQK.js → graph-edge-decay-MKIRZ762.js} +6 -6
  252. package/dist/graph-jsonl.js +4 -4
  253. package/dist/graph-snapshot.js +4 -4
  254. package/dist/graph.js +5 -5
  255. package/dist/harmonic-retrieval.js +1 -1
  256. package/dist/index.d.ts +2 -0
  257. package/dist/index.d.ts.map +1 -1
  258. package/dist/index.js +237 -206
  259. package/dist/json-extract.d.ts.map +1 -1
  260. package/dist/json-extract.js +1 -1
  261. package/dist/lcm/engine.js +2 -2
  262. package/dist/lcm/index.js +5 -5
  263. package/dist/local-llm.d.ts.map +1 -1
  264. package/dist/local-llm.js +2 -2
  265. package/dist/location/index.js +21 -21
  266. package/dist/maintenance/memory-governance.js +18 -18
  267. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +18 -18
  268. package/dist/maintenance/rebuild-memory-projection.js +19 -19
  269. package/dist/memory-browse-operations.js +75 -68
  270. package/dist/memory-browse.js +3 -3
  271. package/dist/memory-subject.js +3 -3
  272. package/dist/namespaces/migrate.js +19 -19
  273. package/dist/namespaces/search.js +4 -4
  274. package/dist/namespaces/storage.js +18 -18
  275. package/dist/offline-sync-embedding-generation.d.ts +207 -0
  276. package/dist/offline-sync-embedding-generation.d.ts.map +1 -0
  277. package/dist/offline-sync-embedding-generation.js +42 -0
  278. package/dist/offline-sync-embedding-generation.js.map +1 -0
  279. package/dist/offline-sync-exclude-globs.d.ts +15 -0
  280. package/dist/offline-sync-exclude-globs.d.ts.map +1 -1
  281. package/dist/offline-sync-exclude-globs.js +11 -3
  282. package/dist/offline-sync-file-io.d.ts +7 -0
  283. package/dist/offline-sync-file-io.d.ts.map +1 -1
  284. package/dist/offline-sync-file-io.js +14 -2
  285. package/dist/offline-sync-impression-drain.js +3 -3
  286. package/dist/offline-sync-runtime-state.d.ts +5 -0
  287. package/dist/offline-sync-runtime-state.d.ts.map +1 -0
  288. package/dist/offline-sync-runtime-state.js +14 -0
  289. package/dist/offline-sync-runtime-state.js.map +1 -0
  290. package/dist/offline-sync-snapshot-stream.d.ts +26 -0
  291. package/dist/offline-sync-snapshot-stream.d.ts.map +1 -0
  292. package/dist/offline-sync-snapshot-stream.js +28 -0
  293. package/dist/offline-sync-snapshot-stream.js.map +1 -0
  294. package/dist/offline-sync-upload-staging.d.ts +34 -0
  295. package/dist/offline-sync-upload-staging.d.ts.map +1 -0
  296. package/dist/offline-sync-upload-staging.js +26 -0
  297. package/dist/offline-sync-upload-staging.js.map +1 -0
  298. package/dist/offline-sync.d.ts +46 -27
  299. package/dist/offline-sync.d.ts.map +1 -1
  300. package/dist/offline-sync.js +21 -8
  301. package/dist/operator-doctor-corpus.js +21 -21
  302. package/dist/operator-toolkit.js +46 -40
  303. package/dist/orchestration/extraction-run.d.ts.map +1 -1
  304. package/dist/orchestration/maintenance.js +30 -30
  305. package/dist/orchestrator.d.ts.map +1 -1
  306. package/dist/orchestrator.js +159 -152
  307. package/dist/recall-navigation-operations.js +75 -68
  308. package/dist/recall-navigation.js +74 -67
  309. package/dist/recall-planner-llm.js +2 -2
  310. package/dist/recall-state-view-admission.js +3 -3
  311. package/dist/recall-why-service.js +6 -6
  312. package/dist/recall-why.js +3 -3
  313. package/dist/reconcile/manifest.js +4 -4
  314. package/dist/{resolution-UYJU5JUA.js → resolution-K4A3VPKZ.js} +8 -8
  315. package/dist/resolution-K4A3VPKZ.js.map +1 -0
  316. package/dist/resume-bundles.js +35 -29
  317. package/dist/salvage-envelope.js +5 -5
  318. package/dist/search/factory.js +3 -3
  319. package/dist/search/index.js +7 -7
  320. package/dist/secure-store/index.js +2 -2
  321. package/dist/secure-store/secure-fs.d.ts +14 -1
  322. package/dist/secure-store/secure-fs.d.ts.map +1 -1
  323. package/dist/semantic-consolidation.js +19 -19
  324. package/dist/semantic-rule-promotion.js +18 -18
  325. package/dist/semantic-rule-verifier.js +18 -18
  326. package/dist/source-agent-qualifier.js +23 -22
  327. package/dist/standup/build.js +18 -18
  328. package/dist/{state-ITXJKXRE.js → state-JB47KHYV.js} +2 -2
  329. package/dist/state-JB47KHYV.js.map +1 -0
  330. package/dist/storage.d.ts +9 -3
  331. package/dist/storage.d.ts.map +1 -1
  332. package/dist/storage.js +17 -17
  333. package/dist/subject-config.js +4 -4
  334. package/dist/summarizer.js +6 -6
  335. package/dist/support-passport/index.js +77 -70
  336. package/dist/{trace-ZFVRH4GO.js → trace-LXXCMZJA.js} +3 -3
  337. package/dist/transfer/backup.js +2 -2
  338. package/dist/transfer/capsule-export.js +2 -2
  339. package/dist/transfer/capsule-import.js +3 -3
  340. package/dist/transfer/export-okf.js +19 -19
  341. package/dist/{tui-55UJMJX7.js → tui-2Z5WOV7L.js} +3 -3
  342. package/dist/tui-2Z5WOV7L.js.map +1 -0
  343. package/dist/verified-recall.js +18 -18
  344. package/dist/who-knows-cli.js +19 -19
  345. package/dist/who-knows.js +18 -18
  346. package/dist/write-envelope.js +4 -4
  347. package/package.json +2 -2
  348. package/src/access-extraction-force-flush.ts +15 -7
  349. package/src/access-http-offline-stream.ts +3 -0
  350. package/src/access-mcp-output-schemas.ts +1 -0
  351. package/src/access-mcp.ts +90 -28
  352. package/src/access-service-health.test.ts +41 -5
  353. package/src/access-service-helpers.ts +1 -0
  354. package/src/access-service-observe-lcm-parity.test.ts +56 -24
  355. package/src/access-service.ts +7 -18
  356. package/src/console/state.test.ts +56 -0
  357. package/src/console/state.ts +83 -0
  358. package/src/correction/correction-access-wiring.ts +36 -8
  359. package/src/correction/correction-classify-parse.test.ts +178 -0
  360. package/src/embedding-fallback.ts +325 -141
  361. package/src/embedding-generation-lock.ts +141 -0
  362. package/src/embedding-index-storage.test.ts +245 -0
  363. package/src/embedding-index-storage.ts +1068 -0
  364. package/src/extraction-liveness.ts +13 -1
  365. package/src/extraction-llm-health.test.ts +103 -0
  366. package/src/extraction-llm-health.ts +71 -0
  367. package/src/index.ts +3 -3
  368. package/src/json-extract.ts +6 -0
  369. package/src/local-llm-thinking.test.ts +12 -0
  370. package/src/local-llm.ts +1 -0
  371. package/src/offline-sync-embedding-generation.test.ts +2173 -0
  372. package/src/offline-sync-embedding-generation.ts +885 -0
  373. package/src/offline-sync-exclude-globs.ts +112 -0
  374. package/src/offline-sync-file-io.ts +48 -0
  375. package/src/offline-sync-runtime-state.ts +48 -0
  376. package/src/offline-sync-snapshot-stream.ts +99 -0
  377. package/src/offline-sync-upload-staging.ts +291 -0
  378. package/src/offline-sync.test.ts +164 -1
  379. package/src/offline-sync.ts +458 -453
  380. package/src/orchestration/extraction-run.test.ts +55 -1
  381. package/src/orchestration/extraction-run.ts +13 -0
  382. package/src/orchestrator.ts +2 -1
  383. package/src/secure-store/secure-fs.ts +26 -6
  384. package/src/storage.ts +11 -6
  385. package/dist/chunk-3BTFBQLO.js.map +0 -1
  386. package/dist/chunk-3SG4FGDV.js.map +0 -1
  387. package/dist/chunk-6DTJBTHL.js.map +0 -1
  388. package/dist/chunk-AREGMX2J.js +0 -50
  389. package/dist/chunk-AREGMX2J.js.map +0 -1
  390. package/dist/chunk-C2WWQ4OR.js.map +0 -1
  391. package/dist/chunk-G6SVZWPB.js.map +0 -1
  392. package/dist/chunk-I53NAHZ4.js.map +0 -1
  393. package/dist/chunk-JN72EPOE.js.map +0 -1
  394. package/dist/chunk-QB75LKQQ.js.map +0 -1
  395. package/dist/chunk-RB776UH3.js.map +0 -1
  396. package/dist/chunk-RGMVMVMF.js.map +0 -1
  397. package/dist/chunk-S55WTPFJ.js.map +0 -1
  398. package/dist/chunk-SVZWVZ7E.js.map +0 -1
  399. package/dist/chunk-WQZZDN5M.js.map +0 -1
  400. /package/dist/{auto-sync-WEUDMXUW.js.map → auto-sync-U5N3CB5Y.js.map} +0 -0
  401. /package/dist/{capsule-crypto-GWVG7LGC.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
  402. /package/dist/{capsule-merge-3MNURAQ3.js.map → capsule-merge-ZCXUPLNJ.js.map} +0 -0
  403. /package/dist/{chunk-JE334SIA.js.map → chunk-22DYJLFN.js.map} +0 -0
  404. /package/dist/{chunk-DGAWEJVI.js.map → chunk-2DLWLNO4.js.map} +0 -0
  405. /package/dist/{chunk-J3IWHCMF.js.map → chunk-2DT7PIDZ.js.map} +0 -0
  406. /package/dist/{chunk-NKFLHW7J.js.map → chunk-2NAD4YCI.js.map} +0 -0
  407. /package/dist/{chunk-SV72F4DS.js.map → chunk-2WLMWP2T.js.map} +0 -0
  408. /package/dist/{chunk-IVH7INFM.js.map → chunk-3O5BB45T.js.map} +0 -0
  409. /package/dist/{chunk-RDEUG2M2.js.map → chunk-3SGTV7RD.js.map} +0 -0
  410. /package/dist/{chunk-QOPB6KE3.js.map → chunk-3TB2XQ3C.js.map} +0 -0
  411. /package/dist/{chunk-C7JAUNFC.js.map → chunk-4KN2JLQV.js.map} +0 -0
  412. /package/dist/{chunk-TIQZNDZ5.js.map → chunk-4MP7XR5J.js.map} +0 -0
  413. /package/dist/{chunk-7DE2NOON.js.map → chunk-4O4XCBOS.js.map} +0 -0
  414. /package/dist/{chunk-SLEAJ67D.js.map → chunk-4OQET73E.js.map} +0 -0
  415. /package/dist/{chunk-Q3THVX75.js.map → chunk-4RL3JOB3.js.map} +0 -0
  416. /package/dist/{chunk-GDYU6VF4.js.map → chunk-5AYUHSAU.js.map} +0 -0
  417. /package/dist/{chunk-B47OHVFT.js.map → chunk-5E6RYRYW.js.map} +0 -0
  418. /package/dist/{chunk-2U3R3DDH.js.map → chunk-5FAGCOZD.js.map} +0 -0
  419. /package/dist/{chunk-CRKWSSS4.js.map → chunk-5MNUBT7T.js.map} +0 -0
  420. /package/dist/{chunk-52II7PB6.js.map → chunk-645WOU5I.js.map} +0 -0
  421. /package/dist/{chunk-7G6WUOLB.js.map → chunk-66DKTOBY.js.map} +0 -0
  422. /package/dist/{chunk-AZ2X5LNA.js.map → chunk-6LYAEM54.js.map} +0 -0
  423. /package/dist/{chunk-DUJYRVUD.js.map → chunk-6O2VUWV6.js.map} +0 -0
  424. /package/dist/{chunk-IMAP236M.js.map → chunk-6UQFJULG.js.map} +0 -0
  425. /package/dist/{chunk-2I6VHX6Q.js.map → chunk-76M5OK7V.js.map} +0 -0
  426. /package/dist/{chunk-OEBLZZKK.js.map → chunk-7XG7KMIW.js.map} +0 -0
  427. /package/dist/{chunk-FYAU4S6E.js.map → chunk-A4VSOUCB.js.map} +0 -0
  428. /package/dist/{chunk-GEX5NI36.js.map → chunk-AE5U5NMS.js.map} +0 -0
  429. /package/dist/{chunk-UNJOB4DY.js.map → chunk-AEHVZY3X.js.map} +0 -0
  430. /package/dist/{chunk-ML2MVAEZ.js.map → chunk-AEU4EQGV.js.map} +0 -0
  431. /package/dist/{chunk-JVY556FW.js.map → chunk-AWG3TIAY.js.map} +0 -0
  432. /package/dist/{chunk-JX6USXZR.js.map → chunk-AXC4WSF3.js.map} +0 -0
  433. /package/dist/{chunk-BOUFVFV2.js.map → chunk-AZSGSRIM.js.map} +0 -0
  434. /package/dist/{chunk-VZBW6CDG.js.map → chunk-B3O3IRZN.js.map} +0 -0
  435. /package/dist/{chunk-4V44AA7O.js.map → chunk-BGI4NUAO.js.map} +0 -0
  436. /package/dist/{chunk-BNPLDVM6.js.map → chunk-BJVO4H5C.js.map} +0 -0
  437. /package/dist/{chunk-S42HZILI.js.map → chunk-BLVR4WYU.js.map} +0 -0
  438. /package/dist/{chunk-JXMECXMF.js.map → chunk-BOZJD5PK.js.map} +0 -0
  439. /package/dist/{chunk-OWHERGF2.js.map → chunk-BXLOS5AJ.js.map} +0 -0
  440. /package/dist/{chunk-EVBI7S4J.js.map → chunk-C6U57KG4.js.map} +0 -0
  441. /package/dist/{chunk-T4BE5KJM.js.map → chunk-C7ZDKWPS.js.map} +0 -0
  442. /package/dist/{chunk-HKFJ3PF3.js.map → chunk-CRXRZA3G.js.map} +0 -0
  443. /package/dist/{chunk-NMBX4D7P.js.map → chunk-CZJA2EQE.js.map} +0 -0
  444. /package/dist/{chunk-BJX3VQWZ.js.map → chunk-D5BSQR63.js.map} +0 -0
  445. /package/dist/{chunk-MFZACYZ6.js.map → chunk-DGS34QSY.js.map} +0 -0
  446. /package/dist/{chunk-UVYI6VIX.js.map → chunk-DQEMWVMT.js.map} +0 -0
  447. /package/dist/{chunk-DQISIT63.js.map → chunk-DZ42MDZE.js.map} +0 -0
  448. /package/dist/{chunk-5ELENZTA.js.map → chunk-EDIH6X2P.js.map} +0 -0
  449. /package/dist/{chunk-C3QXITIP.js.map → chunk-EQUD7FP4.js.map} +0 -0
  450. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  451. /package/dist/{chunk-IA3X2Y4K.js.map → chunk-FJLQXMTU.js.map} +0 -0
  452. /package/dist/{chunk-TY446V34.js.map → chunk-FKD656D2.js.map} +0 -0
  453. /package/dist/{chunk-CLXTDOKV.js.map → chunk-FUNBJQYR.js.map} +0 -0
  454. /package/dist/{chunk-CLSYE5OZ.js.map → chunk-G2STTRYB.js.map} +0 -0
  455. /package/dist/{chunk-SQDKX4RW.js.map → chunk-GWM6T5EQ.js.map} +0 -0
  456. /package/dist/{chunk-AXDOF4VI.js.map → chunk-GZDTVYK2.js.map} +0 -0
  457. /package/dist/{chunk-55FCXCXJ.js.map → chunk-H56VY4BY.js.map} +0 -0
  458. /package/dist/{chunk-OM5BIPBR.js.map → chunk-H5BJWPUI.js.map} +0 -0
  459. /package/dist/{chunk-7QK5TUTW.js.map → chunk-HLB7GKGG.js.map} +0 -0
  460. /package/dist/{chunk-CHDGJJV6.js.map → chunk-HWLIMQE2.js.map} +0 -0
  461. /package/dist/{chunk-NAFPHFDM.js.map → chunk-JNLEJE4A.js.map} +0 -0
  462. /package/dist/{chunk-7UUKRS3J.js.map → chunk-JVPWRNHA.js.map} +0 -0
  463. /package/dist/{chunk-6ET2VII7.js.map → chunk-JYG2334B.js.map} +0 -0
  464. /package/dist/{chunk-2QG4SLZI.js.map → chunk-KF7PQ7G2.js.map} +0 -0
  465. /package/dist/{chunk-WIKO4SCR.js.map → chunk-LHRQBMJN.js.map} +0 -0
  466. /package/dist/{chunk-IFCKHPY4.js.map → chunk-LO4UKPKX.js.map} +0 -0
  467. /package/dist/{chunk-TEV2JGU5.js.map → chunk-LU7E2MXE.js.map} +0 -0
  468. /package/dist/{chunk-RJLV4BKR.js.map → chunk-LV5H6FTN.js.map} +0 -0
  469. /package/dist/{chunk-IBGEHKQ6.js.map → chunk-LW3HKRL7.js.map} +0 -0
  470. /package/dist/{chunk-3XQYM4RE.js.map → chunk-M6C7SOR6.js.map} +0 -0
  471. /package/dist/{chunk-T4QGUXCV.js.map → chunk-MXCAJKMA.js.map} +0 -0
  472. /package/dist/{chunk-BMYZTZ5D.js.map → chunk-MY7B3HYC.js.map} +0 -0
  473. /package/dist/{chunk-NB3LRY4T.js.map → chunk-N4PYB235.js.map} +0 -0
  474. /package/dist/{chunk-S3YM4SZQ.js.map → chunk-N7QV3CMX.js.map} +0 -0
  475. /package/dist/{chunk-3GN2PEOT.js.map → chunk-NYKIUQL6.js.map} +0 -0
  476. /package/dist/{chunk-L2LBMKSI.js.map → chunk-OK52IY3W.js.map} +0 -0
  477. /package/dist/{chunk-WB2I2EQK.js.map → chunk-P2ODTYOF.js.map} +0 -0
  478. /package/dist/{chunk-RIEX7NOZ.js.map → chunk-PEBHPCX2.js.map} +0 -0
  479. /package/dist/{chunk-VYCOJKSX.js.map → chunk-PO7NXNGC.js.map} +0 -0
  480. /package/dist/{chunk-XAITGGX5.js.map → chunk-Q4AUIFUA.js.map} +0 -0
  481. /package/dist/{chunk-EU3N5MK3.js.map → chunk-QAW55ZHJ.js.map} +0 -0
  482. /package/dist/{chunk-I4G4UWQA.js.map → chunk-QI2E6ISQ.js.map} +0 -0
  483. /package/dist/{chunk-CK5U7JGH.js.map → chunk-QI7K7BZ5.js.map} +0 -0
  484. /package/dist/{chunk-VTPASHTO.js.map → chunk-R3X2T2LP.js.map} +0 -0
  485. /package/dist/{chunk-EJPF6XDQ.js.map → chunk-RDJ6C7NZ.js.map} +0 -0
  486. /package/dist/{chunk-2UPTRDGM.js.map → chunk-SCXURCVT.js.map} +0 -0
  487. /package/dist/{chunk-N3MNDVJO.js.map → chunk-SWBC3UER.js.map} +0 -0
  488. /package/dist/{chunk-ZTHGFTMW.js.map → chunk-TKC5LZ43.js.map} +0 -0
  489. /package/dist/{chunk-YRLHEEQT.js.map → chunk-U6VC6RP3.js.map} +0 -0
  490. /package/dist/{chunk-MOQPEFLN.js.map → chunk-UP75KL3L.js.map} +0 -0
  491. /package/dist/{chunk-D5F7TKMH.js.map → chunk-V4JZCTMK.js.map} +0 -0
  492. /package/dist/{chunk-NAYBHNGN.js.map → chunk-VALKBYBN.js.map} +0 -0
  493. /package/dist/{chunk-BUKK5SWA.js.map → chunk-VDX2J7OX.js.map} +0 -0
  494. /package/dist/{chunk-H36X4NRH.js.map → chunk-VVX5PZFS.js.map} +0 -0
  495. /package/dist/{chunk-RCQKAOOB.js.map → chunk-W3XXFPRI.js.map} +0 -0
  496. /package/dist/{chunk-W75BBBSY.js.map → chunk-WKY7N3L7.js.map} +0 -0
  497. /package/dist/{chunk-GZAGLCQH.js.map → chunk-X5SHE2O3.js.map} +0 -0
  498. /package/dist/{chunk-OAOBL6OY.js.map → chunk-XOV7NRY5.js.map} +0 -0
  499. /package/dist/{chunk-FZVROZKC.js.map → chunk-XVZA6VXC.js.map} +0 -0
  500. /package/dist/{chunk-IP5VLSLO.js.map → chunk-YJ7MW627.js.map} +0 -0
  501. /package/dist/{chunk-HS75MCQF.js.map → chunk-YRULQW5J.js.map} +0 -0
  502. /package/dist/{chunk-DINRT75P.js.map → chunk-YSBL7VPO.js.map} +0 -0
  503. /package/dist/{chunk-QWUJWBZ5.js.map → chunk-Z3O53WZR.js.map} +0 -0
  504. /package/dist/{chunk-KERNN6BC.js.map → chunk-Z5LP6ILP.js.map} +0 -0
  505. /package/dist/{chunk-5FNZ3G6X.js.map → chunk-Z6EZCN6Z.js.map} +0 -0
  506. /package/dist/{chunk-JS3S2HVG.js.map → chunk-ZZZ4I455.js.map} +0 -0
  507. /package/dist/{contradiction-scan-5EJYRKD3.js.map → contradiction-scan-TAGMOUQ2.js.map} +0 -0
  508. /package/dist/{graph-edge-decay-PVKDKLQK.js.map → embedding-generation-lock.js.map} +0 -0
  509. /package/dist/{resolution-UYJU5JUA.js.map → embedding-index-storage.js.map} +0 -0
  510. /package/dist/{state-ITXJKXRE.js.map → extraction-llm-health.js.map} +0 -0
  511. /package/dist/{first-start-migration-GAT7G733.js.map → first-start-migration-7OATHJKB.js.map} +0 -0
  512. /package/dist/{tui-55UJMJX7.js.map → graph-edge-decay-MKIRZ762.js.map} +0 -0
  513. /package/dist/{trace-ZFVRH4GO.js.map → trace-LXXCMZJA.js.map} +0 -0
@@ -0,0 +1,1068 @@
1
+ /**
2
+ * File-level storage for the embedding fallback index (issue #3146).
3
+ *
4
+ * Two single-generation layouts, exactly one authoritative at a time:
5
+ * - `legacy`: the single `state/embeddings.json` file, used while the
6
+ * whole index serializes within `resolveIndexFileCharLimit()` chars;
7
+ * - `sharded`: the published `state/embeddings/` directory of hash-assigned
8
+ * shard files, migrated to ONE-WAY and ATOMICALLY (stage a complete shard
9
+ * set, publish with a single directory rename, demote the legacy file to
10
+ * a non-authoritative recovery artifact). Generations never merge, so a
11
+ * crash cannot resurrect deleted entries or drop published ones.
12
+ *
13
+ * This module owns files, budgets, and durable diagnostics only — no index
14
+ * caching or provider logic (that lives in `embedding-fallback.ts`).
15
+ */
16
+ import path from "node:path";
17
+ import { constants as bufferConstants } from "node:buffer";
18
+ import { lstat, mkdir, mkdtemp, realpath, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
19
+ import { log } from "./logger.js";
20
+ import { readEnvVar } from "./runtime/env.js";
21
+ import type { EmbeddingGenerationFence } from "./embedding-generation-lock.js";
22
+
23
+ export type EmbeddingProviderType = "openai" | "local" | "host";
24
+
25
+ export type EmbeddingIndexEntry = {
26
+ vector: number[];
27
+ path: string;
28
+ };
29
+
30
+ export type EmbeddingIndexFile = {
31
+ version: 1;
32
+ provider: EmbeddingProviderType;
33
+ model: string;
34
+ entries: Record<string, EmbeddingIndexEntry>;
35
+ };
36
+
37
+ export type EmbeddingIndexIdentity = Pick<EmbeddingIndexFile, "provider" | "model">;
38
+
39
+ /** Outcome of reading one managed index file (legacy or shard). */
40
+ export type ManagedIndexRead =
41
+ | { outcome: "ok"; file: EmbeddingIndexFile }
42
+ | { outcome: "absent" }
43
+ | { outcome: "foreign" }
44
+ | { outcome: "unreadable"; reason: string };
45
+
46
+ /** Which generation owns the index right now. */
47
+ export type IndexLayout = "empty" | "legacy" | "sharded";
48
+
49
+ /**
50
+ * Thrown when an embedding index (or one of its shards) cannot be persisted
51
+ * within the per-file serialization budget. Issue #3146: index files are
52
+ * materialized as single V8 strings, so a JSON document at or past
53
+ * `bufferConstants.MAX_STRING_LENGTH` chars makes every read and write throw
54
+ * `RangeError: Invalid string length`. The index is sharded before that
55
+ * ceiling; this tagged error is reserved for the residual case where even
56
+ * one shard cannot hold the payload (e.g. a single entry with an enormous
57
+ * vector), so callers can classify it as recoverable-with-record instead of
58
+ * an opaque background failure.
59
+ */
60
+ export class EmbeddingIndexCapacityError extends Error {
61
+ override readonly name = "EmbeddingIndexCapacityError" as const;
62
+ constructor(
63
+ message: string,
64
+ readonly memoryId?: string,
65
+ ) {
66
+ super(message);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Thrown when an index file that should be authoritative cannot be read
72
+ * (I/O error, over-budget size, unparseable JSON). Mutation paths fail
73
+ * CLOSED on this: they must not continue with a fresh empty index and
74
+ * pretend the previous vectors were loaded — the file is left untouched in
75
+ * place for recovery. Read-only paths (recall) fail open to empty results
76
+ * and record the reason durably.
77
+ */
78
+ export class EmbeddingIndexStorageError extends Error {
79
+ override readonly name = "EmbeddingIndexStorageError" as const;
80
+ }
81
+
82
+ /** Durable diagnostic row describing the last failed embedding-index write. */
83
+ export interface EmbeddingIndexWriteFailure {
84
+ ts: string;
85
+ kind: "capacity" | "io";
86
+ message: string;
87
+ memoryId?: string;
88
+ }
89
+
90
+ /**
91
+ * Durable embedding-index diagnostics (issue #3146), persisted at
92
+ * `<memoryDir>/state/embedding-fallback-status.json` and surfaced through
93
+ * console_state, so background ingestion failures are visible to operators
94
+ * instead of only ever appearing as log lines.
95
+ */
96
+ export interface EmbeddingIndexStatusFile {
97
+ version: 1;
98
+ /** Cumulative count of failed index writes since the file was created. */
99
+ failureCount: number;
100
+ /** Active write failure; cleared by the next successful save. */
101
+ lastWriteFailure?: EmbeddingIndexWriteFailure;
102
+ /** Set when an unreadable index file was detected instead of overwritten. */
103
+ lastReadRecovery?: { ts: string; message: string };
104
+ lastSuccessAt?: string;
105
+ }
106
+
107
+ /**
108
+ * Per-file serialization budget (JSON-string chars) for WRITES of the legacy
109
+ * `state/embeddings.json` file and of every shard under `state/embeddings/`.
110
+ * The 16MiB margin keeps the write round-trip clear of the V8 single-string
111
+ * ceiling even with UTF-8 byte/count skew and small header growth. Override
112
+ * per environment (tests, headroom tuning) via
113
+ * `REMNIC_EMBEDDING_INDEX_FILE_CHAR_LIMIT`.
114
+ *
115
+ * READS are gated by the HARD ceiling (`resolveIndexHardReadCharLimit`),
116
+ * never by this budget: a legacy index written by an older version may
117
+ * legitimately sit between the soft budget and the hard ceiling (the
118
+ * reported production file does), and it must still load and migrate.
119
+ */
120
+ const DEFAULT_INDEX_FILE_CHAR_LIMIT =
121
+ bufferConstants.MAX_STRING_LENGTH - 16 * 1024 * 1024;
122
+
123
+ /**
124
+ * Hard single-string ceiling for READS: files at or above this size are not
125
+ * attempted (a readFile would throw `RangeError: Invalid string length`);
126
+ * their bytes are preserved in place and mutations fail closed with an
127
+ * actionable error. Defaults to the V8 maximum; tests inject a small value
128
+ * via `REMNIC_EMBEDDING_INDEX_HARD_READ_LIMIT_CHARS`.
129
+ */
130
+ function resolveIndexHardReadCharLimit(): number {
131
+ const soft = resolveIndexFileCharLimit();
132
+ const raw = readEnvVar("REMNIC_EMBEDDING_INDEX_HARD_READ_LIMIT_CHARS");
133
+ if (raw) {
134
+ const parsed = Number(raw);
135
+ if (Number.isInteger(parsed) && parsed >= soft) return parsed;
136
+ }
137
+ return bufferConstants.MAX_STRING_LENGTH;
138
+ }
139
+
140
+ /** Hash shards under `state/embeddings/` used once the budget is exceeded. */
141
+ const SHARD_COUNT = 64;
142
+
143
+ const SHARD_FILE_PATTERN = /^shard-\d{4}\.json$/;
144
+
145
+ export function resolveIndexFileCharLimit(): number {
146
+ const raw = readEnvVar("REMNIC_EMBEDDING_INDEX_FILE_CHAR_LIMIT");
147
+ if (raw) {
148
+ const parsed = Number(raw);
149
+ if (Number.isInteger(parsed) && parsed >= 1024) return parsed;
150
+ }
151
+ return DEFAULT_INDEX_FILE_CHAR_LIMIT;
152
+ }
153
+
154
+ function shardFileName(shardIndex: number): string {
155
+ return `shard-${String(shardIndex).padStart(4, "0")}.json`;
156
+ }
157
+
158
+ /**
159
+ * Fixed transaction backup path for identity replacements. A fixed (not
160
+ * per-process) name is what makes the rename gap recoverable: whatever
161
+ * process restarts after a crash finds the demoted former generation here
162
+ * and rolls it back into place.
163
+ */
164
+ function replacementBackupPath(shardDir: string): string {
165
+ return path.join(path.dirname(shardDir), "embeddings.pre-replace.tmp");
166
+ }
167
+
168
+ /**
169
+ * Stable FNV-1a shard assignment: an entry always hashes to the same shard,
170
+ * so an update or removal never needs a manifest to find its file.
171
+ */
172
+ export function shardIndexOf(memoryId: string, shardCount: number): number {
173
+ let hash = 0x811c9dc5;
174
+ for (let i = 0; i < memoryId.length; i++) {
175
+ hash ^= memoryId.charCodeAt(i);
176
+ hash = Math.imul(hash, 0x01000193) >>> 0;
177
+ }
178
+ return hash % shardCount;
179
+ }
180
+
181
+ /**
182
+ * A shard file named `shard-NNNN.json` must only contain ids that hash to
183
+ * that shard: a misplaced copy would be loaded today and silently resurrect
184
+ * stale vectors after the computed shard is rewritten (issue #3148, round 6).
185
+ * Throws the tagged storage error so mutations fail closed and recall fails
186
+ * open at its boundary.
187
+ */
188
+ export function validateShardMembership(shardName: string, file: EmbeddingIndexFile): void {
189
+ const match = /^shard-(\d{4})\.json$/.exec(shardName);
190
+ if (!match) return; // not a shard file (legacy documents have no constraint)
191
+ const shardIndex = Number(match[1]);
192
+ for (const id of Object.keys(file.entries)) {
193
+ if (shardIndexOf(id, SHARD_COUNT) !== shardIndex) {
194
+ throw new EmbeddingIndexStorageError(
195
+ `refusing embedding index shard ${shardName}: id ${id} hashes to shard ${shardIndexOf(id, SHARD_COUNT)}, not ${shardIndex}; file preserved in place`,
196
+ );
197
+ }
198
+ }
199
+ }
200
+
201
+ export function isInvalidStringLengthError(err: unknown): boolean {
202
+ return err instanceof RangeError && /invalid string length/i.test(err.message);
203
+ }
204
+
205
+ /**
206
+ * Parse and FULLY validate one serialized index document (legacy or shard).
207
+ * The single read boundary shared by disk reads and offline-sync pulls: a
208
+ * document with a valid header but a malformed body (`entries` not a plain
209
+ * record, or an entry whose `path` is not a string / whose `vector` is not a
210
+ * finite-number array) is `unreadable`, not `ok` — recall fails open on the
211
+ * tagged reason, mutations fail closed, and the bytes are never overwritten
212
+ * (issue #3148 review, round 4).
213
+ */
214
+ export function parseEmbeddingIndexDocument(raw: string): ManagedIndexRead {
215
+ let parsed: unknown;
216
+ try {
217
+ parsed = JSON.parse(raw);
218
+ } catch (err) {
219
+ return {
220
+ outcome: "unreadable",
221
+ reason: `JSON parse failed: ${err instanceof Error ? err.message : String(err)}`,
222
+ };
223
+ }
224
+ if (
225
+ !parsed ||
226
+ typeof parsed !== "object" ||
227
+ Array.isArray(parsed) ||
228
+ (parsed as Partial<EmbeddingIndexFile>).version !== 1 ||
229
+ typeof (parsed as Partial<EmbeddingIndexFile>).provider !== "string" ||
230
+ typeof (parsed as Partial<EmbeddingIndexFile>).model !== "string" ||
231
+ ((parsed as Partial<EmbeddingIndexFile>).model as string).length === 0
232
+ ) {
233
+ return { outcome: "foreign" };
234
+ }
235
+ // An unsupported identity must fail closed like any other malformed
236
+ // document: recall cannot serve it and mutations must not adopt it as
237
+ // authoritative (issue #3148, round 6).
238
+ if (
239
+ (parsed as Partial<EmbeddingIndexFile>).provider !== "openai" &&
240
+ (parsed as Partial<EmbeddingIndexFile>).provider !== "local" &&
241
+ (parsed as Partial<EmbeddingIndexFile>).provider !== "host"
242
+ ) {
243
+ return { outcome: "foreign" };
244
+ }
245
+ const entries = (parsed as Partial<EmbeddingIndexFile>).entries;
246
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
247
+ return {
248
+ outcome: "unreadable",
249
+ reason: "entries must be a plain record of memory id -> entry",
250
+ };
251
+ }
252
+ for (const [id, entry] of Object.entries(entries as Record<string, unknown>)) {
253
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
254
+ return { outcome: "unreadable", reason: `entry ${id} must be an object` };
255
+ }
256
+ const candidate = entry as Partial<EmbeddingIndexEntry>;
257
+ if (typeof candidate.path !== "string") {
258
+ return { outcome: "unreadable", reason: `entry ${id}.path must be a string` };
259
+ }
260
+ if (!Array.isArray(candidate.vector)) {
261
+ return { outcome: "unreadable", reason: `entry ${id}.vector must be an array` };
262
+ }
263
+ for (const component of candidate.vector) {
264
+ if (!Number.isFinite(component)) {
265
+ return {
266
+ outcome: "unreadable",
267
+ reason: `entry ${id}.vector must contain only finite numbers`,
268
+ };
269
+ }
270
+ }
271
+ }
272
+ return { outcome: "ok", file: parsed as EmbeddingIndexFile };
273
+ }
274
+
275
+ /**
276
+ * Group a complete index into per-shard entry records using the stable FNV
277
+ * assignment. Shared by `persist()` and the offline-sync generation
278
+ * transaction so a converted generation shards identically everywhere.
279
+ */
280
+ export function shardEntriesForIndex(
281
+ index: EmbeddingIndexFile,
282
+ ): Map<number, Record<string, EmbeddingIndexEntry>> {
283
+ const groups = new Map<number, Record<string, EmbeddingIndexEntry>>();
284
+ for (const id of Object.keys(index.entries)) {
285
+ const shardIndex = shardIndexOf(id, SHARD_COUNT);
286
+ const group = groups.get(shardIndex);
287
+ if (group) group[id] = index.entries[id];
288
+ else groups.set(shardIndex, { [id]: index.entries[id] });
289
+ }
290
+ return groups;
291
+ }
292
+
293
+ /**
294
+ * Serialize one shard, translating a ceiling overflow into a tagged,
295
+ * recoverable capacity error. Exported for the offline-sync generation
296
+ * transaction, which stages incoming generations through the same budget.
297
+ */
298
+ export function serializeEmbeddingShard(
299
+ index: EmbeddingIndexFile,
300
+ shardIndex: number,
301
+ entries: Record<string, EmbeddingIndexEntry>,
302
+ limit: number = resolveIndexFileCharLimit(),
303
+ ): string {
304
+ let body: string;
305
+ try {
306
+ body = JSON.stringify({
307
+ version: 1 as const,
308
+ provider: index.provider,
309
+ model: index.model,
310
+ entries,
311
+ });
312
+ } catch (err) {
313
+ throw new EmbeddingIndexCapacityError(
314
+ `embedding index shard ${shardFileName(shardIndex)} exceeded the single-string ceiling while serializing: ${err instanceof Error ? err.message : String(err)}`,
315
+ largestEntryId(entries) || undefined,
316
+ );
317
+ }
318
+ if (body.length > limit) {
319
+ const worstId = largestEntryId(entries);
320
+ const worstLength = worstId && entries[worstId] ? JSON.stringify(entries[worstId]).length : 0;
321
+ throw new EmbeddingIndexCapacityError(
322
+ `embedding index shard ${shardFileName(shardIndex)} needs ${body.length} chars, over the ${limit}-char single-file limit; largest entry ${worstId || "(unknown)"} serializes to ${worstLength} chars`,
323
+ worstId || undefined,
324
+ );
325
+ }
326
+ return body;
327
+ }
328
+
329
+ function largestEntryId(entries: Record<string, EmbeddingIndexEntry>): string {
330
+ let worstId = "";
331
+ let worstLength = 0;
332
+ for (const [id, entry] of Object.entries(entries)) {
333
+ let length: number;
334
+ try {
335
+ length = JSON.stringify(entry).length;
336
+ } catch {
337
+ return id;
338
+ }
339
+ if (length > worstLength) {
340
+ worstLength = length;
341
+ worstId = id;
342
+ }
343
+ }
344
+ return worstId;
345
+ }
346
+
347
+ /**
348
+ * Optional canonical secure-store IO for the daemon. When provided (both
349
+ * halves), file content is read/written through the SAME
350
+ * StorageManager-backed contract offline sync uses: canonical-path AAD,
351
+ * secure-key-aware, atomic. Shard STAGING writes carry the FINAL published
352
+ * path so the post-publish rename keeps the ciphertext decryptable; dirty
353
+ * shard, status, and legacy writes are their own canonical final path.
354
+ * When omitted, the store behaves exactly as before (raw plaintext fs).
355
+ * The diagnostics status file is intentionally ALWAYS plain: it holds no
356
+ * memory content (failure strings/counts only) and console_state reads it
357
+ * without unlocking the store.
358
+ */
359
+ export interface EmbeddingIndexStoreIo {
360
+ readUtf8(filePath: string): Promise<string>;
361
+ writeUtf8(filePath: string, contents: string, opts?: { finalAadFilePath?: string }): Promise<void>;
362
+ }
363
+
364
+ export class EmbeddingIndexFileStore {
365
+ constructor(
366
+ private readonly indexPath: string,
367
+ private readonly shardDir: string,
368
+ private readonly statusPath: string,
369
+ private readonly io?: EmbeddingIndexStoreIo,
370
+ ) {
371
+ if (io && (Boolean(io.readUtf8) !== Boolean(io.writeUtf8))) {
372
+ throw new EmbeddingIndexStorageError(
373
+ "embedding index store io requires both readUtf8 and writeUtf8; a half-wired io would mix encrypted and plaintext files",
374
+ );
375
+ }
376
+ }
377
+
378
+ /** Legacy single-file layout path (public for error messages). */
379
+ get legacyPath(): string {
380
+ return this.indexPath;
381
+ }
382
+
383
+ /** Record an unreadable-authoritative-file reason (both read and mutation paths). */
384
+ async recordIndexStatusForLoad(reason: string): Promise<void> {
385
+ await this.recordIndexStatus({
386
+ lastReadRecovery: { ts: new Date().toISOString(), message: reason },
387
+ });
388
+ }
389
+
390
+ /**
391
+ * Which generation owns the index right now. The published
392
+ * `state/embeddings/` directory IS the layout marker: until it exists the
393
+ * legacy file is authoritative; from the atomic rename onward it never
394
+ * merges again.
395
+ */
396
+ async detectLayout(): Promise<IndexLayout> {
397
+ // An interrupted identity replacement leaves the published directory
398
+ // absent with the fixed transaction backup still on disk (the publish
399
+ // rename never completed). Report SHARDED for that gap: the backup holds
400
+ // the authoritative generation, so neither the legacy file nor a fresh
401
+ // index may win, and reads fail open to empty until the mutation queue
402
+ // performs the rollback. The rollback itself only ever runs inside
403
+ // persist() — a write — so read paths never race an in-flight
404
+ // replacement with a recovery rename (issue #3148 review).
405
+ try {
406
+ // lstat, NOT stat: a symlinked generation directory must be rejected
407
+ // outright — following it would read or write OUTSIDE the state
408
+ // directory through the link (codex PRRT_kwDORJXyws6mZ72r).
409
+ const info = await lstat(this.shardDir);
410
+ if (info.isSymbolicLink()) {
411
+ throw new EmbeddingIndexStorageError(
412
+ `embedding index shard directory is a symlink; refusing to read or write through it: ${this.shardDir}`,
413
+ );
414
+ }
415
+ return "sharded";
416
+ } catch (err) {
417
+ if (err instanceof EmbeddingIndexStorageError) throw err;
418
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
419
+ // The pointer exists but cannot be stat'ed — fail towards the
420
+ // published generation so its readers surface the I/O error.
421
+ log.warn(`embedding index: shard dir stat failed (${err instanceof Error ? err.message : String(err)}); assuming sharded layout`);
422
+ return "sharded";
423
+ }
424
+ if (await this.replacementBackupExists()) return "sharded";
425
+ return this.legacyOrEmptyLayout();
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Roll the fixed transaction backup back into the published position.
431
+ * Returns true when a rollback happened.
432
+ */
433
+ private async replacementBackupExists(): Promise<boolean> {
434
+ const backupPath = replacementBackupPath(this.shardDir);
435
+ try {
436
+ await stat(backupPath);
437
+ return true;
438
+ } catch (err) {
439
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return false;
440
+ // An unreadable backup must not be silently treated as absent —
441
+ // that would permit the legacy fallback over a generation we cannot
442
+ // inspect (issue #3148 review, round 1).
443
+ throw new EmbeddingIndexStorageError(
444
+ `cannot stat embedding index replacement backup ${backupPath}: ${err instanceof Error ? err.message : String(err)}`,
445
+ );
446
+ }
447
+ }
448
+
449
+ /**
450
+ * The fixed transaction backup is a path with well-known semantics; a
451
+ * planted symlink must never be restored into the published position (it
452
+ * would point the generation outside the memory dir) nor removed through
453
+ * as if it were the former generation. Symlinks are rejected outright.
454
+ */
455
+ private async assertBackupNotSymlink(backupPath: string): Promise<void> {
456
+ const info = await lstat(backupPath).catch((err) => {
457
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
458
+ throw err;
459
+ });
460
+ if (info?.isSymbolicLink()) {
461
+ throw new EmbeddingIndexStorageError(
462
+ `embedding index replacement backup is a symlink; refusing to touch it: ${backupPath}`,
463
+ );
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Mutation-queue entry point: roll the fixed transaction backup back into
469
+ * the published position after an interrupted replacement. Returns true
470
+ * when a rollback happened. Only fires in the actual rename gap
471
+ * (published directory absent); callers must invalidate any cached index
472
+ * view when this returns true.
473
+ */
474
+ async recoverIfInterrupted(fence?: EmbeddingGenerationFence): Promise<boolean> {
475
+ // Only the rename gap (published directory absent) is recoverable. When
476
+ // the directory is present - including after a crash that followed a
477
+ // successful publish - the generation on disk is already authoritative
478
+ // and renaming the backup onto it would fail with EEXIST (issue #3148
479
+ // review, round 2).
480
+ try {
481
+ await stat(this.shardDir);
482
+ return false;
483
+ } catch (err) {
484
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
485
+ log.warn(`embedding index: recovery skipped because shard dir stat failed: ${err instanceof Error ? err.message : String(err)}`);
486
+ return false;
487
+ }
488
+ }
489
+ const backupPath = replacementBackupPath(this.shardDir);
490
+ if (!(await this.replacementBackupExists())) return false;
491
+ await this.assertBackupNotSymlink(backupPath);
492
+ // The rollback rename is destructive: reassert lock ownership first.
493
+ await fence?.();
494
+ await rename(backupPath, this.shardDir);
495
+ log.warn(
496
+ `embedding index: recovered interrupted replacement; former generation restored from ${backupPath}`,
497
+ );
498
+ await this.recordIndexStatus({
499
+ lastReadRecovery: {
500
+ ts: new Date().toISOString(),
501
+ message: "interrupted identity replacement rolled back to the former generation",
502
+ },
503
+ });
504
+ return true;
505
+ }
506
+
507
+ private async legacyOrEmptyLayout(): Promise<IndexLayout> {
508
+ try {
509
+ await stat(this.indexPath);
510
+ return "legacy";
511
+ } catch (err) {
512
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return "empty";
513
+ throw err;
514
+ }
515
+ }
516
+
517
+ /** Header identity of the authoritative generation, read leniently. */
518
+ async identityFromDisk(): Promise<EmbeddingIndexIdentity | null> {
519
+ const layout = await this.detectLayout();
520
+ if (layout === "sharded") {
521
+ // Shard files are bounded by the per-file budget, so the identity
522
+ // probe never reads a ceiling-sized string.
523
+ for (const name of await this.readShardNames()) {
524
+ const read = await this.readFileAt(path.join(this.shardDir, name));
525
+ if (read.outcome === "ok") {
526
+ return { provider: read.file.provider, model: read.file.model };
527
+ }
528
+ }
529
+ return null;
530
+ }
531
+ if (layout === "legacy") {
532
+ const read = await this.readFileAt(this.indexPath);
533
+ if (read.outcome === "ok") {
534
+ return { provider: read.file.provider, model: read.file.model };
535
+ }
536
+ }
537
+ return null;
538
+ }
539
+
540
+ /**
541
+ * Load every readable shard into `merged`. Returns the generation identity.
542
+ * Unreadable shards record durable diagnostics; `strict` additionally
543
+ * fails the mutation closed.
544
+ */
545
+ async readShardGenerationInto(
546
+ merged: Record<string, EmbeddingIndexEntry>,
547
+ ): Promise<EmbeddingIndexIdentity | null> {
548
+ let identity: EmbeddingIndexIdentity | null = null;
549
+ for (const name of await this.readShardNames()) {
550
+ const read = await this.readFileAt(path.join(this.shardDir, name));
551
+ if (read.outcome !== "ok") {
552
+ if (read.outcome === "unreadable") {
553
+ await this.recordIndexStatus({
554
+ lastReadRecovery: { ts: new Date().toISOString(), message: `${name}: ${read.reason}` },
555
+ });
556
+ throw new EmbeddingIndexStorageError(
557
+ `refusing to continue from unreadable embedding index shard ${name}: ${read.reason}`,
558
+ );
559
+ }
560
+ await this.recordIndexStatus({
561
+ lastReadRecovery: { ts: new Date().toISOString(), message: `${name}: unrecognized index format` },
562
+ });
563
+ throw new EmbeddingIndexStorageError(
564
+ `refusing to continue from malformed embedding index shard ${name} (unrecognized format); file preserved in place`,
565
+ );
566
+ }
567
+ try {
568
+ validateShardMembership(name, read.file);
569
+ } catch (error) {
570
+ await this.recordIndexStatus({
571
+ lastReadRecovery: { ts: new Date().toISOString(), message: String(error) },
572
+ });
573
+ throw error;
574
+ }
575
+ const shardIdentity = { provider: read.file.provider, model: read.file.model };
576
+ if (!identity) identity = shardIdentity;
577
+ if (!sameIndexIdentity(shardIdentity, identity)) {
578
+ // A partial rewrite from this map would silently drop the foreign
579
+ // shard's entries, so even recall must not serve this generation.
580
+ throw new EmbeddingIndexStorageError(
581
+ `refusing to continue from mixed-identity embedding index generation: ${name} is ${shardIdentity.provider}/${shardIdentity.model} but the generation identity is ${identity.provider}/${identity.model}; files preserved in place`,
582
+ );
583
+ }
584
+ Object.assign(merged, read.file.entries);
585
+ }
586
+ return identity;
587
+ }
588
+
589
+ async readLegacy(): Promise<ManagedIndexRead> {
590
+ return this.readFileAt(this.indexPath);
591
+ }
592
+
593
+ /**
594
+ * Read one managed index file (legacy or shard). The file is never
595
+ * modified or moved here: an unreadable file stays in place for recovery
596
+ * and is reported through the returned outcome (mutation paths fail
597
+ * closed on it; read-only paths fail open and record the reason).
598
+ */
599
+ /**
600
+ * Read/mutation boundary for any generation member or the legacy marker
601
+ * (codex PRRT_kwDORJXyws6mZ72r): a symlinked member is rejected outright
602
+ * and its real target must stay inside the canonical state directory —
603
+ * following a planted link would read or write outside the memory root.
604
+ * Runs BEFORE the secure IO so storage-backed reads are covered too.
605
+ */
606
+ private async assertGenerationFileNotSymlinked(filePath: string): Promise<void> {
607
+ const info = await lstat(filePath).catch((err) => {
608
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
609
+ throw new EmbeddingIndexStorageError(
610
+ `cannot lstat embedding index file ${filePath}: ${err instanceof Error ? err.message : String(err)}`,
611
+ );
612
+ });
613
+ const stateDir = path.dirname(this.shardDir);
614
+ // Containment must be checked even when the member itself is a regular
615
+ // file: a SYMLINKED GENERATION DIRECTORY lets reads through to whatever
616
+ // path the link resolves to — a regular file in a linked dir is still
617
+ // outside the memory root and must be rejected.
618
+ const real = await realpath(filePath).catch(() => null);
619
+ if (real) {
620
+ const scope = await realpath(stateDir).catch(() => stateDir);
621
+ const rel = path.relative(scope, real);
622
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
623
+ throw new EmbeddingIndexStorageError(
624
+ `embedding index file escapes the state directory via symlink; refusing: ${filePath} -> ${real}`,
625
+ );
626
+ }
627
+ }
628
+ if (!info || !info.isSymbolicLink()) return;
629
+ throw new EmbeddingIndexStorageError(
630
+ `embedding index file is a symlink; refusing to read or write through it: ${filePath}`,
631
+ );
632
+ }
633
+
634
+ async readFileAt(filePath: string): Promise<ManagedIndexRead> {
635
+ await this.assertGenerationFileNotSymlinked(filePath);
636
+ const hardLimit = resolveIndexHardReadCharLimit();
637
+ let size = 0;
638
+ try {
639
+ size = (await stat(filePath)).size;
640
+ } catch (err) {
641
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return { outcome: "absent" };
642
+ return { outcome: "unreadable", reason: `stat failed: ${err instanceof Error ? err.message : String(err)}` };
643
+ }
644
+ // Reads are gated by the HARD single-string ceiling only — NOT by the
645
+ // soft write budget — so a legacy index from an older version sitting
646
+ // between the two still loads and migrates (issue #3146). Byte size is
647
+ // only a conservative bound for UTF-8 (bytes ≥ chars): at or below it a
648
+ // readFile is guaranteed materializable; past it the file MAY still be
649
+ // readable as a string, but refusing is the safe side. An over-ceiling
650
+ // file is preserved in place; the caller decides fail-open (recall) or
651
+ // fail-closed (mutation).
652
+ if (size > hardLimit) {
653
+ return {
654
+ outcome: "unreadable",
655
+ reason: `file is ${size} bytes, over the ${hardLimit}-char hard single-string read ceiling (V8 RangeError territory); bytes preserved in place — move or prune the file to recover`,
656
+ };
657
+ }
658
+ let raw: string;
659
+ try {
660
+ raw = this.io
661
+ ? await this.io.readUtf8(filePath)
662
+ : await readFile(filePath, "utf-8");
663
+ } catch (err) {
664
+ return { outcome: "unreadable", reason: `read failed: ${err instanceof Error ? err.message : String(err)}` };
665
+ }
666
+ return parseEmbeddingIndexDocument(raw);
667
+ }
668
+
669
+ private async readShardNames(): Promise<string[]> {
670
+ try {
671
+ return (await readdir(this.shardDir))
672
+ .filter((name) => SHARD_FILE_PATTERN.test(name))
673
+ .sort();
674
+ } catch (err) {
675
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
676
+ // The published generation exists but cannot be enumerated. Treating
677
+ // it as empty would let a mutation overwrite shards it never saw, so
678
+ // this must propagate — strict mutation fails closed, recall fails
679
+ // open at its boundary (issue #3146 review).
680
+ throw new EmbeddingIndexStorageError(
681
+ `cannot enumerate embedding index shard directory ${this.shardDir}: ${err instanceof Error ? `${err.message} (${(err as NodeJS.ErrnoException).code ?? "unknown"})` : String(err)}`,
682
+ );
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Persist the index. Layout decisions, in order:
688
+ * - published generation: rewrite only the shards this mutation touched;
689
+ * - legacy/empty generation and the whole index fits one bounded JSON
690
+ * string: stay the single `state/embeddings.json` file;
691
+ * - over budget: ONE-WAY migration — stage a complete shard set, publish
692
+ * it with a single directory rename, then rename the legacy file aside
693
+ * as a non-authoritative recovery artifact (issue #3146).
694
+ */
695
+ async persist(
696
+ index: EmbeddingIndexFile,
697
+ opts: {
698
+ touchedIds?: readonly string[];
699
+ memoryId?: string;
700
+ /** Reasserts mutation-lock ownership immediately before each destructive write (after serialization). */
701
+ fence?: EmbeddingGenerationFence;
702
+ } = {},
703
+ ): Promise<void> {
704
+ const stateDir = path.dirname(this.indexPath);
705
+ await mkdir(stateDir, { recursive: true });
706
+ await this.cleanForeignStagingDirs();
707
+ // NOTE: interrupted-replacement recovery runs in the mutation wrapper
708
+ // (EmbeddingFallback.enqueueIndexMutation) BEFORE this method, so the
709
+ // caller's index already reflects the restored generation.
710
+
711
+ const groups = shardEntriesForIndex(index);
712
+
713
+ const layout = await this.detectLayout();
714
+
715
+ if (layout === "sharded") {
716
+ const limit = resolveIndexFileCharLimit();
717
+ // An identity change on a published generation (a sharded host index
718
+ // replaced by a fallback provider, or a host-model change) must
719
+ // publish a COMPLETE replacement: the dirty-shard fast path would
720
+ // rewrite only the touched shard under the new identity and leave the
721
+ // former identity's shards in place — a mixed generation that every
722
+ // later load rejects (codex P1, PR #3148).
723
+ const diskIdentity = await this.identityFromDisk();
724
+ if (groups.size === 0) {
725
+ // An empty directory has no transport-visible deletion evidence.
726
+ // Publish a valid empty shard so a peer without a shared base also
727
+ // replaces its stale generation rather than preserving old vectors.
728
+ groups.set(0, {});
729
+ await this.publishReplacementGeneration(index, groups, limit, opts.fence);
730
+ return;
731
+ }
732
+ const identityChanged =
733
+ diskIdentity !== null &&
734
+ (diskIdentity.provider !== index.provider || diskIdentity.model !== index.model);
735
+ if (identityChanged) {
736
+ await this.publishReplacementGeneration(index, groups, limit, opts.fence);
737
+ return;
738
+ }
739
+ const dirtyShards = opts.touchedIds?.length
740
+ ? new Set(opts.touchedIds.map((id) => shardIndexOf(id, SHARD_COUNT)))
741
+ : null;
742
+ // Serialize every touched shard BEFORE writing anything so a capacity
743
+ // failure cannot leave a partial rewrite.
744
+ const targets = dirtyShards ? [...dirtyShards] : [...groups.keys()];
745
+ const payloads = targets
746
+ .filter((shardIndex) => groups.has(shardIndex))
747
+ .map((shardIndex) => ({
748
+ shardIndex,
749
+ body: serializeEmbeddingShard(index, shardIndex, groups.get(shardIndex)!, limit),
750
+ }));
751
+ // Serialization is done: reassert lock ownership immediately before
752
+ // the destructive live-generation writes.
753
+ await opts.fence?.();
754
+ for (const payload of payloads) {
755
+ await this.writeAtomicFile(
756
+ path.join(this.shardDir, shardFileName(payload.shardIndex)),
757
+ payload.body,
758
+ );
759
+ }
760
+ // A dirty shard with no remaining entries (its last entry was just
761
+ // removed) must be removed from disk, not silently skipped.
762
+ for (const shardIndex of targets) {
763
+ if (!groups.has(shardIndex)) {
764
+ await rm(path.join(this.shardDir, shardFileName(shardIndex)), { force: true });
765
+ }
766
+ }
767
+ return;
768
+ }
769
+
770
+ let whole: string | null;
771
+ try {
772
+ whole = JSON.stringify(index);
773
+ } catch (err) {
774
+ if (!isInvalidStringLengthError(err)) throw err;
775
+ whole = null;
776
+ }
777
+ const limit = resolveIndexFileCharLimit();
778
+ if (whole !== null && whole.length <= limit) {
779
+ // Serialized above: reassert lock ownership immediately before the
780
+ // destructive atomic write.
781
+ await opts.fence?.();
782
+ await this.writeAtomicFile(this.indexPath, whole);
783
+ return;
784
+ }
785
+
786
+ // One-way migration. Until the publish rename, the legacy file stays
787
+ // authoritative; afterwards the published directory is the only
788
+ // generation and the legacy file is demoted to a recovery artifact.
789
+ const stagingDir = path.join(
790
+ stateDir,
791
+ `embeddings.staging.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
792
+ );
793
+ await mkdir(stagingDir, { recursive: true });
794
+ try {
795
+ await this.stageShardSet(stagingDir, index, groups, limit);
796
+ // Atomic publish: the directory appearing IS the layout marker.
797
+ await this.publishSwappedGeneration(stagingDir, opts.fence);
798
+ } catch (err) {
799
+ await rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
800
+ throw err;
801
+ }
802
+ await rename(
803
+ this.indexPath,
804
+ `${this.indexPath}.pre-migration.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`,
805
+ ).catch(() => undefined);
806
+ }
807
+
808
+ /**
809
+ * Replace a published generation wholesale after a provider/model identity
810
+ * change (replacement semantics — the former entries are obsolete by
811
+ * definition). Crash-safe by the same construction as the migration: the
812
+ * COMPLETE new-identity set is staged and validated first (old bytes are
813
+ * not touched until staging succeeds), then published atomically through
814
+ * `publishSwappedGeneration`.
815
+ */
816
+ private async publishReplacementGeneration(
817
+ index: EmbeddingIndexFile,
818
+ groups: Map<number, Record<string, EmbeddingIndexEntry>>,
819
+ limit: number,
820
+ fence?: EmbeddingGenerationFence,
821
+ ): Promise<void> {
822
+ const stateDir = path.dirname(this.indexPath);
823
+ const stagingDir = path.join(
824
+ stateDir,
825
+ `embeddings.staging.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
826
+ );
827
+ await mkdir(stagingDir, { recursive: true });
828
+ try {
829
+ await this.stageShardSet(stagingDir, index, groups, limit);
830
+ await this.publishSwappedGeneration(stagingDir, fence);
831
+ } catch (err) {
832
+ await rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
833
+ throw err;
834
+ }
835
+ }
836
+
837
+ /**
838
+ * Write a complete shard set into a staging directory. Every shard is
839
+ * serialized before the first write, so a capacity failure cannot leave a
840
+ * partial stage behind.
841
+ */
842
+ private async stageShardSet(
843
+ stagingDir: string,
844
+ index: EmbeddingIndexFile,
845
+ groups: Map<number, Record<string, EmbeddingIndexEntry>>,
846
+ limit: number,
847
+ ): Promise<void> {
848
+ const payloads = [...groups]
849
+ .map(([shardIndex, entries]) => ({
850
+ shardIndex,
851
+ body: serializeEmbeddingShard(index, shardIndex, entries, limit),
852
+ }));
853
+ for (const payload of payloads) {
854
+ const finalAadFilePath = path.join(this.shardDir, shardFileName(payload.shardIndex));
855
+ await this.writeAtomicFile(
856
+ path.join(stagingDir, shardFileName(payload.shardIndex)),
857
+ payload.body,
858
+ { finalAadFilePath },
859
+ );
860
+ }
861
+ }
862
+
863
+ /**
864
+ * Atomically remove the published generation (offline-sync: the remote
865
+ * deleted the whole index) by publishing an EMPTY staged directory
866
+ * through the shared swap: the rename is atomic, so the former generation
867
+ * moves whole into the fixed backup and back — a failure before the
868
+ * commit rolls the former generation back intact, and a post-commit
869
+ * backup-cleanup failure is non-fatal (the empty published directory
870
+ * stays the authoritative one-way layout marker). The fixed backup is
871
+ * symlink-checked first (round 5).
872
+ */
873
+ async removePublishedGeneration(fence?: EmbeddingGenerationFence): Promise<void> {
874
+ const backupPath = replacementBackupPath(this.shardDir);
875
+ await this.assertBackupNotSymlink(backupPath);
876
+ await rm(backupPath, { recursive: true, force: true });
877
+ // Remove by publishing an EMPTY staged directory through the SAME swap
878
+ // state machine as every other publication: rename is atomic, so the
879
+ // former generation moves whole into the fixed backup and back — a
880
+ // partially-deleted generation can never be exposed. The empty
881
+ // published directory stays the one-way layout marker, so a stray
882
+ // legacy file can never become authoritative after removal. The
883
+ // demoted backup's post-commit cleanup is non-fatal (existing
884
+ // publishSwappedGeneration semantics).
885
+ await mkdir(path.dirname(this.shardDir), { recursive: true });
886
+ const emptyStaging = await mkdtemp(
887
+ path.join(path.dirname(this.shardDir), "embeddings.staging.tmp-empty-"),
888
+ );
889
+ try {
890
+ await this.publishSwappedGeneration(emptyStaging, fence);
891
+ } catch (err) {
892
+ await rm(emptyStaging, { recursive: true, force: true }).catch(() => undefined);
893
+ throw err;
894
+ }
895
+ }
896
+
897
+ /**
898
+ * Atomically publish a fully staged generation directory as THE published
899
+ * generation. The former published directory is demoted to the FIXED
900
+ * transaction backup and the staging dir is renamed in with a single
901
+ * rename; if the publish rename fails the former generation is rolled
902
+ * back, and if the process dies in the rename gap `detectLayout()` /
903
+ * `recoverIfInterrupted()` perform the same rollback on next use.
904
+ * Shared by the mutation-side migration/replacement and the offline-sync
905
+ * generation transaction (issue #3148, round 4) so there is exactly one
906
+ * swap state machine. Staging-dir cleanup stays with the caller.
907
+ */
908
+ async publishSwappedGeneration(stagingDir: string, fence?: EmbeddingGenerationFence): Promise<void> {
909
+ // The replacement is published: the backup is obsolete disk. A cleanup
910
+ // failure is non-fatal (the next replacement removes it) but must stay
911
+ // visible.
912
+ const backupPath = replacementBackupPath(this.shardDir);
913
+ await this.assertBackupNotSymlink(backupPath);
914
+ // Cheap ownership check before the destructive backup cleanup: a peer
915
+ // stale-break would otherwise see the backup vanish under it before we
916
+ // even reach the live renames (Kilo PRRT_kwDORJXyws6mZ8i5).
917
+ await fence?.();
918
+ await rm(backupPath, { recursive: true, force: true });
919
+ // Second ownership check immediately before the live renames: any
920
+ // delay between the cleanup and the rename still allows a stale-break,
921
+ // and publishing now would clobber the peer's write.
922
+ await fence?.();
923
+ let demoted = false;
924
+ try {
925
+ await stat(this.shardDir);
926
+ await rename(this.shardDir, backupPath);
927
+ demoted = true;
928
+ } catch (err) {
929
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
930
+ }
931
+ try {
932
+ await rename(stagingDir, this.shardDir);
933
+ } catch (err) {
934
+ if (demoted) await rename(backupPath, this.shardDir).catch(() => undefined);
935
+ throw err;
936
+ }
937
+ await rm(backupPath, { recursive: true, force: true }).catch((err) => {
938
+ log.warn(`embedding index: could not remove replacement backup ${backupPath}: ${err}`);
939
+ });
940
+ }
941
+
942
+ /** Remove staging dirs orphaned by crashed migrations. A foreign dir younger than the grace window may belong to a live writer mid-migration, so only older dirs are removed (issue #3148 review, round 3). */
943
+ private static readonly STAGING_CLEANUP_GRACE_MS = 10 * 60 * 1000;
944
+
945
+ private async cleanForeignStagingDirs(): Promise<void> {
946
+ const stateDir = path.dirname(this.indexPath);
947
+ let names: string[] = [];
948
+ try {
949
+ names = await readdir(stateDir);
950
+ } catch {
951
+ return;
952
+ }
953
+ const ownPrefix = `embeddings.staging.tmp-${process.pid}-`;
954
+ const cutoff = Date.now() - EmbeddingIndexFileStore.STAGING_CLEANUP_GRACE_MS;
955
+ for (const name of names) {
956
+ if (!name.startsWith("embeddings.staging.tmp-") || name.startsWith(ownPrefix)) continue;
957
+ try {
958
+ const stats = await stat(path.join(stateDir, name));
959
+ if (stats.mtimeMs > cutoff) continue;
960
+ } catch {
961
+ continue;
962
+ }
963
+ await rm(path.join(stateDir, name), { recursive: true, force: true }).catch(() => undefined);
964
+ }
965
+ }
966
+
967
+ /**
968
+ * Temp-file + rename write. The temp name carries `.tmp-` so offline sync
969
+ * ignores in-flight files under `state/`. With a wired io, the write goes
970
+ * through the canonical secure-store contract instead, optionally binding
971
+ * the ciphertext to the FINAL published path (staged shards).
972
+ */
973
+ private async writeAtomicFile(
974
+ filePath: string,
975
+ contents: string,
976
+ opts?: { finalAadFilePath?: string; bypassSecureIo?: boolean },
977
+ ): Promise<void> {
978
+ if (this.io && opts?.bypassSecureIo !== true) {
979
+ await this.io.writeUtf8(filePath, contents, opts?.finalAadFilePath === undefined ? undefined : { finalAadFilePath: opts.finalAadFilePath });
980
+ return;
981
+ }
982
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
983
+ try {
984
+ await writeFile(tempPath, contents, "utf-8");
985
+ await rename(tempPath, filePath);
986
+ } catch (err) {
987
+ await rm(tempPath, { force: true }).catch(() => undefined);
988
+ throw err;
989
+ }
990
+ }
991
+
992
+ /**
993
+ * Record the outcome of one index mutation durably. This is diagnostics,
994
+ * NOT recovery: write failures are recorded and then re-thrown by the
995
+ * caller so the extraction/consolidation task fails explicitly instead of
996
+ * declaring success over an index that could not be persisted (issue
997
+ * #3146).
998
+ */
999
+ async recordIndexWriteOutcome(err: unknown, memoryId?: string): Promise<void> {
1000
+ if (!err) {
1001
+ await this.recordIndexStatus({
1002
+ lastWriteFailure: null,
1003
+ lastSuccessAt: new Date().toISOString(),
1004
+ });
1005
+ return;
1006
+ }
1007
+ const kind: EmbeddingIndexWriteFailure["kind"] =
1008
+ err instanceof EmbeddingIndexCapacityError || isInvalidStringLengthError(err)
1009
+ ? "capacity"
1010
+ : "io";
1011
+ await this.recordIndexStatus({
1012
+ failureCountIncrement: 1,
1013
+ lastWriteFailure: {
1014
+ ts: new Date().toISOString(),
1015
+ kind,
1016
+ message: err instanceof Error ? err.message : String(err),
1017
+ ...(memoryId ? { memoryId } : {}),
1018
+ },
1019
+ });
1020
+ }
1021
+
1022
+ /**
1023
+ * Merge diagnostic state into `state/embedding-fallback-status.json`.
1024
+ * Best-effort: a diagnostics failure never masks the real outcome.
1025
+ */
1026
+ private async recordIndexStatus(
1027
+ patch: {
1028
+ failureCountIncrement?: number;
1029
+ lastWriteFailure?: EmbeddingIndexWriteFailure | null;
1030
+ lastReadRecovery?: { ts: string; message: string };
1031
+ lastSuccessAt?: string;
1032
+ },
1033
+ ): Promise<void> {
1034
+ try {
1035
+ let current: EmbeddingIndexStatusFile = { version: 1, failureCount: 0 };
1036
+ try {
1037
+ const parsed = JSON.parse(await readFile(this.statusPath, "utf-8")) as Partial<EmbeddingIndexStatusFile>;
1038
+ if (parsed && typeof parsed === "object" && typeof parsed.failureCount === "number") {
1039
+ current = { ...parsed, version: 1, failureCount: parsed.failureCount };
1040
+ }
1041
+ } catch {
1042
+ // Absent or unreadable status starts fresh; the index files own the truth.
1043
+ }
1044
+ const next: EmbeddingIndexStatusFile = { ...current };
1045
+ if (patch.failureCountIncrement) next.failureCount += patch.failureCountIncrement;
1046
+ if (patch.lastWriteFailure !== undefined) {
1047
+ if (patch.lastWriteFailure === null) delete next.lastWriteFailure;
1048
+ else next.lastWriteFailure = patch.lastWriteFailure;
1049
+ }
1050
+ if (patch.lastReadRecovery !== undefined) next.lastReadRecovery = patch.lastReadRecovery;
1051
+ if (patch.lastSuccessAt !== undefined) next.lastSuccessAt = patch.lastSuccessAt;
1052
+ // The status file is ALWAYS plain (non-secret diagnostics: failure
1053
+ // strings and counts only). It bypasses the secure io so console_state
1054
+ // keeps reading it on locked stores and failure counts never reset.
1055
+ await mkdir(path.dirname(this.statusPath), { recursive: true });
1056
+ await this.writeAtomicFile(this.statusPath, JSON.stringify(next), { bypassSecureIo: true });
1057
+ } catch (err) {
1058
+ log.debug(`embedding fallback status write failed: ${err}`);
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ function sameIndexIdentity(
1064
+ left: EmbeddingIndexIdentity,
1065
+ right: EmbeddingIndexIdentity,
1066
+ ): boolean {
1067
+ return left.provider === right.provider && left.model === right.model;
1068
+ }