@remnic/core 10.0.49 → 10.0.50

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 (338) hide show
  1. package/dist/access-admin-ops-surface.js +41 -40
  2. package/dist/access-authorization-probe.js +42 -41
  3. package/dist/access-boundary.js +41 -40
  4. package/dist/access-cli.js +107 -106
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-extraction-force-flush.js +41 -40
  7. package/dist/access-http-briefing.d.ts +2 -0
  8. package/dist/access-http-briefing.d.ts.map +1 -1
  9. package/dist/access-http-briefing.js +42 -41
  10. package/dist/access-http-query.js +43 -42
  11. package/dist/access-http.d.ts.map +1 -1
  12. package/dist/access-http.js +59 -58
  13. package/dist/access-identity-continuity-surface.js +41 -40
  14. package/dist/access-lcm-surface.js +41 -40
  15. package/dist/access-mcp.js +51 -50
  16. package/dist/access-namespace-preflight.js +41 -40
  17. package/dist/access-observe-write-category.d.ts +1 -1
  18. package/dist/access-observe-write-category.d.ts.map +1 -1
  19. package/dist/access-observe-write-category.js +10 -10
  20. package/dist/access-observe-write-surface.d.ts.map +1 -1
  21. package/dist/access-observe-write-surface.js +41 -40
  22. package/dist/access-offline-manifest.js +4 -4
  23. package/dist/access-operations-batch.js +42 -41
  24. package/dist/access-operations.js +49 -48
  25. package/dist/access-recall-concurrency.js +41 -40
  26. package/dist/access-recall-response.js +41 -40
  27. package/dist/access-recall-surface.js +41 -40
  28. package/dist/access-schema.js +6 -6
  29. package/dist/access-service.d.ts +5 -0
  30. package/dist/access-service.d.ts.map +1 -1
  31. package/dist/access-service.js +41 -40
  32. package/dist/access-surface-catalog.js +43 -42
  33. package/dist/access-surface-catalog.js.map +1 -1
  34. package/dist/active-recall.js +14 -14
  35. package/dist/{auto-sync-BNCBMU6H.js → auto-sync-UQQ6YJSB.js} +3 -3
  36. package/dist/briefing-deadline.d.ts +7 -0
  37. package/dist/briefing-deadline.d.ts.map +1 -0
  38. package/dist/briefing-deadline.js +13 -0
  39. package/dist/briefing-window.d.ts +12 -4
  40. package/dist/briefing-window.d.ts.map +1 -1
  41. package/dist/briefing-window.js +3 -1
  42. package/dist/briefing.d.ts +2 -0
  43. package/dist/briefing.d.ts.map +1 -1
  44. package/dist/briefing.js +12 -11
  45. package/dist/buffer.js +3 -3
  46. package/dist/calibration.js +2 -2
  47. package/dist/{capsule-merge-RNQUP2VL.js → capsule-merge-D4BTIJGK.js} +3 -3
  48. package/dist/causal-consolidation.js +11 -11
  49. package/dist/{chunk-ESDOQEJM.js → chunk-2AKIVSHP.js} +2 -2
  50. package/dist/{chunk-MCS4LIXP.js → chunk-43TUWQNR.js} +2 -2
  51. package/dist/{chunk-2NLCDNGA.js → chunk-564YP3DW.js} +2 -2
  52. package/dist/{chunk-PBWJVRKE.js → chunk-5FUDSK6Z.js} +2 -2
  53. package/dist/{chunk-V4GPFCJ7.js → chunk-5G53JSRO.js} +2 -2
  54. package/dist/{chunk-WJCAAVZX.js → chunk-5X2H3X4J.js} +2 -2
  55. package/dist/{chunk-CPBYKOTC.js → chunk-6ZQX3R66.js} +2 -2
  56. package/dist/{chunk-AF4UIHON.js → chunk-74AY3S7U.js} +9 -9
  57. package/dist/{chunk-XSJL62LE.js → chunk-7J6A5MV2.js} +2 -2
  58. package/dist/{chunk-A3WXV7YU.js → chunk-7SAE6T36.js} +2 -2
  59. package/dist/{chunk-BWF3KNSQ.js → chunk-7YVER7ZO.js} +2 -2
  60. package/dist/{chunk-5IDQVE4U.js → chunk-A3UFV4K3.js} +6 -6
  61. package/dist/{chunk-LWRIGKFM.js → chunk-A5L5KGDI.js} +19 -1
  62. package/dist/chunk-A5L5KGDI.js.map +1 -0
  63. package/dist/{chunk-6TIVVOVU.js → chunk-BNJAVRNJ.js} +9 -9
  64. package/dist/{chunk-CMOYD5CA.js → chunk-CCL2OXFO.js} +2 -2
  65. package/dist/{chunk-7XFQSQYS.js → chunk-CRN3EPE2.js} +4 -3
  66. package/dist/chunk-CRN3EPE2.js.map +1 -0
  67. package/dist/{chunk-3NYIXVCT.js → chunk-DMKTOCC7.js} +2 -2
  68. package/dist/chunk-DMKTOCC7.js.map +1 -0
  69. package/dist/{chunk-FRXS6SC7.js → chunk-EKUI7I2X.js} +20 -4
  70. package/dist/chunk-EKUI7I2X.js.map +1 -0
  71. package/dist/{chunk-N6EM7ESC.js → chunk-EKYWZYZJ.js} +22 -14
  72. package/dist/chunk-EKYWZYZJ.js.map +1 -0
  73. package/dist/{chunk-FOKTW6GY.js → chunk-EWBXMGXR.js} +2 -2
  74. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  75. package/dist/{chunk-6W3QH3QW.js → chunk-GQIYL7OD.js} +388 -292
  76. package/dist/chunk-GQIYL7OD.js.map +1 -0
  77. package/dist/{chunk-E47UQBYW.js → chunk-GXVXBYRA.js} +3 -3
  78. package/dist/{chunk-RZDDNH63.js → chunk-HKOMHP6D.js} +27 -15
  79. package/dist/chunk-HKOMHP6D.js.map +1 -0
  80. package/dist/{chunk-AGCEJDG2.js → chunk-HPNXS6W6.js} +2 -2
  81. package/dist/{chunk-O4CA53UY.js → chunk-IMKOJVD4.js} +69 -63
  82. package/dist/chunk-IMKOJVD4.js.map +1 -0
  83. package/dist/{chunk-N5PISU26.js → chunk-IN6U2BS5.js} +2 -2
  84. package/dist/{chunk-HI53EK3Y.js → chunk-IUYXCTA7.js} +2 -2
  85. package/dist/{chunk-VGRBKEBH.js → chunk-JBWD3J5U.js} +3 -3
  86. package/dist/{chunk-YB3WN6XA.js → chunk-K5QQXVG6.js} +12 -12
  87. package/dist/{chunk-7LCGFUMB.js → chunk-KQAFEZQX.js} +4 -4
  88. package/dist/{chunk-HDT3DMUF.js → chunk-KW5YZELC.js} +2 -2
  89. package/dist/{chunk-DM6SOROF.js → chunk-LIBVANOF.js} +2 -2
  90. package/dist/{chunk-VKUMKVEU.js → chunk-LMMBV7QC.js} +4 -4
  91. package/dist/{chunk-NMOEOQV4.js → chunk-M2DOJBH7.js} +2 -2
  92. package/dist/{chunk-3INLTKTW.js → chunk-M4HRYLOA.js} +2 -2
  93. package/dist/{chunk-YY6PWFUC.js → chunk-M6TP6UYD.js} +3 -3
  94. package/dist/{chunk-BJTFLP3J.js → chunk-MLNXATFG.js} +2 -2
  95. package/dist/{chunk-C7WDWD35.js → chunk-MWURUSRE.js} +5 -5
  96. package/dist/{chunk-WIBWONL4.js → chunk-N74WJ7B2.js} +2 -2
  97. package/dist/{chunk-LCHFINL4.js → chunk-N7FNCE7F.js} +2 -2
  98. package/dist/{chunk-QZK6HG4J.js → chunk-NPLM7AFU.js} +2 -2
  99. package/dist/{chunk-CRJX4OMN.js → chunk-NSP5PIQH.js} +2 -2
  100. package/dist/{chunk-X52SXKVC.js → chunk-P7OVFGMU.js} +2 -2
  101. package/dist/{chunk-2J6BIOVN.js → chunk-PJCVI44H.js} +2 -2
  102. package/dist/{chunk-Q6PYCPHO.js → chunk-Q3PNQYAC.js} +2 -2
  103. package/dist/{chunk-D76AHITY.js → chunk-QS765SBL.js} +2 -2
  104. package/dist/{chunk-KNCR7APE.js → chunk-QTDZ2NYC.js} +7 -7
  105. package/dist/{chunk-YFXRLRKP.js → chunk-S4OZKPBS.js} +7 -11
  106. package/dist/{chunk-YFXRLRKP.js.map → chunk-S4OZKPBS.js.map} +1 -1
  107. package/dist/{chunk-HCPVL6SF.js → chunk-SJTSWG2Z.js} +3 -3
  108. package/dist/{chunk-SA6P2KS3.js → chunk-TEH5MDX5.js} +33 -17
  109. package/dist/chunk-TEH5MDX5.js.map +1 -0
  110. package/dist/{chunk-XFND63OB.js → chunk-TFWJXWL6.js} +2 -2
  111. package/dist/{chunk-SVJ5JVZO.js → chunk-TXKQBQYW.js} +3 -3
  112. package/dist/{chunk-WQFHHJWK.js → chunk-UALXWWGN.js} +2 -2
  113. package/dist/{chunk-3PVKV3XL.js → chunk-UNOAVHV4.js} +2 -2
  114. package/dist/{chunk-FYZXZUGU.js → chunk-UQBOPCDK.js} +2 -2
  115. package/dist/{chunk-SUKPM6DV.js → chunk-UWMZT2TY.js} +47 -4
  116. package/dist/chunk-UWMZT2TY.js.map +1 -0
  117. package/dist/{chunk-OD7ZXCUF.js → chunk-UXGXJUZ5.js} +7 -7
  118. package/dist/{chunk-NH37SP6V.js → chunk-UZVQFGAY.js} +81 -78
  119. package/dist/chunk-UZVQFGAY.js.map +1 -0
  120. package/dist/{chunk-2BY56HE6.js → chunk-VCOMACR6.js} +35 -35
  121. package/dist/{chunk-5FNZ3G6X.js → chunk-VKQI47DJ.js} +4 -4
  122. package/dist/{chunk-MPJDQC4G.js → chunk-WJCXUE2J.js} +2 -2
  123. package/dist/{chunk-C3HRYVGD.js → chunk-WV4UYDP6.js} +2 -2
  124. package/dist/{chunk-E3DL3CAL.js → chunk-WXJC2L66.js} +2 -2
  125. package/dist/chunk-X2KO4FK5.js +40 -0
  126. package/dist/chunk-X2KO4FK5.js.map +1 -0
  127. package/dist/{chunk-O7GNSHFF.js → chunk-XC3H7KON.js} +2 -2
  128. package/dist/{chunk-OOOX5C4X.js → chunk-XL2EE5W3.js} +4 -4
  129. package/dist/{chunk-NUK5XU3P.js → chunk-XTXWR6KQ.js} +25 -10
  130. package/dist/chunk-XTXWR6KQ.js.map +1 -0
  131. package/dist/{chunk-CZVNOTLR.js → chunk-XXR4A33N.js} +2 -2
  132. package/dist/{chunk-2AV2U2SZ.js → chunk-YDIOMDBW.js} +2 -2
  133. package/dist/{chunk-QFTNWDJ6.js → chunk-YECEEJUU.js} +2 -2
  134. package/dist/{chunk-BMG3SAXA.js → chunk-Z5277JWF.js} +3 -3
  135. package/dist/{chunk-SW3WHVKN.js → chunk-ZFNXJEUG.js} +2 -2
  136. package/dist/{chunk-VY6BD5KB.js → chunk-ZMO2S5RT.js} +12 -10
  137. package/dist/chunk-ZMO2S5RT.js.map +1 -0
  138. package/dist/{chunk-KMTMMBEB.js → chunk-ZNRTHYYW.js} +6 -6
  139. package/dist/{chunk-VCBS5GRQ.js → chunk-ZSMA67FV.js} +2 -2
  140. package/dist/cli.js +80 -79
  141. package/dist/coding/export-okf-codegraph.js +9 -9
  142. package/dist/compounding/engine.js +11 -11
  143. package/dist/config.js +14 -14
  144. package/dist/connectors/codex-materialize-runner.js +10 -10
  145. package/dist/connectors/index.js +10 -10
  146. package/dist/consolidation-provenance-check.js +4 -4
  147. package/dist/consolidation-undo.js +2 -2
  148. package/dist/convergence-refresh.js +12 -12
  149. package/dist/corpus-watermark.js +12 -12
  150. package/dist/correction/passive-correction-notifications.d.ts +1 -1
  151. package/dist/correction/passive-correction-notifications.d.ts.map +1 -1
  152. package/dist/dashboard-runtime.js +2 -2
  153. package/dist/deep-recall-http-glue.js +7 -7
  154. package/dist/{entity-canonical-id-migration-2YOLMOFC.js → entity-canonical-id-migration-ZBNRGUNV.js} +3 -3
  155. package/dist/entity-canonical-id-migration-ZBNRGUNV.js.map +1 -0
  156. package/dist/entity-retrieval.js +11 -11
  157. package/dist/explicit-capture.js +2 -2
  158. package/dist/external-wiki-access.js +42 -41
  159. package/dist/extraction.js +16 -16
  160. package/dist/{first-start-migration-QVD22WWS.js → first-start-migration-A6PHIBUQ.js} +4 -4
  161. package/dist/harmonic-retrieval.js +10 -10
  162. package/dist/index.d.ts +1 -0
  163. package/dist/index.d.ts.map +1 -1
  164. package/dist/index.js +130 -122
  165. package/dist/lcm/engine.js +2 -2
  166. package/dist/lcm/index.js +5 -5
  167. package/dist/local-llm.js +2 -2
  168. package/dist/location/index.js +15 -15
  169. package/dist/location/tagging.d.ts +7 -5
  170. package/dist/location/tagging.d.ts.map +1 -1
  171. package/dist/maintenance/memory-governance.js +10 -10
  172. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
  173. package/dist/maintenance/rebuild-memory-projection-runner.js +12 -12
  174. package/dist/maintenance/rebuild-memory-projection.js +11 -11
  175. package/dist/memory-browse-operations.js +42 -41
  176. package/dist/namespaces/migrate.js +11 -11
  177. package/dist/namespaces/storage.js +10 -10
  178. package/dist/offline-sync-file-io.d.ts +15 -0
  179. package/dist/offline-sync-file-io.d.ts.map +1 -1
  180. package/dist/offline-sync-file-io.js +9 -1
  181. package/dist/offline-sync-snapshot-stream.js +4 -4
  182. package/dist/offline-sync-upload-staging.js +2 -2
  183. package/dist/offline-sync.d.ts.map +1 -1
  184. package/dist/offline-sync.js +3 -3
  185. package/dist/operator-doctor-corpus.js +13 -13
  186. package/dist/operator-toolkit.js +24 -24
  187. package/dist/orchestration/maintenance.js +23 -23
  188. package/dist/orchestrator.d.ts +1 -0
  189. package/dist/orchestrator.d.ts.map +1 -1
  190. package/dist/orchestrator.js +109 -108
  191. package/dist/page-versioning.js +2 -2
  192. package/dist/recall-navigation-operations.js +42 -41
  193. package/dist/recall-navigation.js +41 -40
  194. package/dist/resume-bundles.js +15 -15
  195. package/dist/semantic-consolidation.js +15 -15
  196. package/dist/semantic-rule-promotion.js +10 -10
  197. package/dist/semantic-rule-verifier.js +10 -10
  198. package/dist/source-agent-qualifier.js +13 -13
  199. package/dist/standup/build.js +10 -10
  200. package/dist/storage/entity-canonical-id-migration-adapter.d.ts +1 -0
  201. package/dist/storage/entity-canonical-id-migration-adapter.d.ts.map +1 -1
  202. package/dist/storage/entity-canonical-id-migration.d.ts +7 -0
  203. package/dist/storage/entity-canonical-id-migration.d.ts.map +1 -1
  204. package/dist/storage/entity-canonical-id-references.d.ts +2 -0
  205. package/dist/storage/entity-canonical-id-references.d.ts.map +1 -1
  206. package/dist/storage/entity-migration-completion-cache.d.ts +46 -0
  207. package/dist/storage/entity-migration-completion-cache.d.ts.map +1 -0
  208. package/dist/storage/entity-store.d.ts +1 -8
  209. package/dist/storage/entity-store.d.ts.map +1 -1
  210. package/dist/storage/memory-read-store.d.ts +3 -2
  211. package/dist/storage/memory-read-store.d.ts.map +1 -1
  212. package/dist/storage/windowed-memory-read.d.ts +6 -0
  213. package/dist/storage/windowed-memory-read.d.ts.map +1 -1
  214. package/dist/storage.d.ts +2 -0
  215. package/dist/storage.d.ts.map +1 -1
  216. package/dist/storage.js +9 -9
  217. package/dist/summarizer.js +3 -3
  218. package/dist/support-passport/index.js +44 -43
  219. package/dist/transfer/capsule-export.js +2 -2
  220. package/dist/transfer/capsule-import.js +4 -4
  221. package/dist/transfer/export-okf.js +10 -10
  222. package/dist/verified-recall.js +10 -10
  223. package/dist/who-knows-cli.js +11 -11
  224. package/dist/who-knows.js +10 -10
  225. package/package.json +2 -2
  226. package/src/access-http-briefing.ts +4 -0
  227. package/src/access-http.test.ts +40 -11
  228. package/src/access-http.ts +6 -0
  229. package/src/access-observe-write-category.ts +1 -1
  230. package/src/access-observe-write-surface.ts +24 -19
  231. package/src/access-operations-batch.ts +18 -2
  232. package/src/access-service.ts +5 -0
  233. package/src/briefing-abort.test.ts +173 -0
  234. package/src/briefing-deadline.test.ts +84 -0
  235. package/src/briefing-deadline.ts +33 -0
  236. package/src/briefing-window-abort.test.ts +47 -0
  237. package/src/briefing-window.ts +33 -4
  238. package/src/briefing.ts +23 -16
  239. package/src/correction/passive-correction-notifications-abort.test.ts +42 -0
  240. package/src/correction/passive-correction-notifications.ts +4 -0
  241. package/src/entity-lazy-compatibility.test.ts +229 -0
  242. package/src/entity-migration-completion-notification.test.ts +148 -0
  243. package/src/index.ts +1 -1
  244. package/src/location/tagging.ts +14 -12
  245. package/src/offline-sync-file-io.ts +61 -2
  246. package/src/offline-sync.test.ts +313 -20
  247. package/src/offline-sync.ts +43 -21
  248. package/src/orchestrator-init.test.ts +49 -1
  249. package/src/orchestrator.ts +9 -8
  250. package/src/storage/entity-canonical-id-migration-adapter.ts +16 -1
  251. package/src/storage/entity-canonical-id-migration.ts +17 -8
  252. package/src/storage/entity-canonical-id-references.ts +24 -0
  253. package/src/storage/entity-migration-completion-cache.nofollow.test.ts +223 -0
  254. package/src/storage/entity-migration-completion-cache.ts +153 -0
  255. package/src/storage/entity-store.ts +58 -86
  256. package/src/storage/memory-read-store.ts +9 -3
  257. package/src/storage/windowed-memory-read.ts +8 -0
  258. package/src/storage-startup-nonblocking-migration.test.ts +183 -0
  259. package/src/storage.ts +7 -2
  260. package/src/testing/subjects/entity-migration-completion-cache.test.ts +90 -0
  261. package/dist/chunk-3NYIXVCT.js.map +0 -1
  262. package/dist/chunk-6W3QH3QW.js.map +0 -1
  263. package/dist/chunk-7XFQSQYS.js.map +0 -1
  264. package/dist/chunk-FRXS6SC7.js.map +0 -1
  265. package/dist/chunk-LWRIGKFM.js.map +0 -1
  266. package/dist/chunk-N6EM7ESC.js.map +0 -1
  267. package/dist/chunk-NH37SP6V.js.map +0 -1
  268. package/dist/chunk-NUK5XU3P.js.map +0 -1
  269. package/dist/chunk-O4CA53UY.js.map +0 -1
  270. package/dist/chunk-RZDDNH63.js.map +0 -1
  271. package/dist/chunk-SA6P2KS3.js.map +0 -1
  272. package/dist/chunk-SUKPM6DV.js.map +0 -1
  273. package/dist/chunk-VY6BD5KB.js.map +0 -1
  274. /package/dist/{auto-sync-BNCBMU6H.js.map → auto-sync-UQQ6YJSB.js.map} +0 -0
  275. /package/dist/{entity-canonical-id-migration-2YOLMOFC.js.map → briefing-deadline.js.map} +0 -0
  276. /package/dist/{capsule-merge-RNQUP2VL.js.map → capsule-merge-D4BTIJGK.js.map} +0 -0
  277. /package/dist/{chunk-ESDOQEJM.js.map → chunk-2AKIVSHP.js.map} +0 -0
  278. /package/dist/{chunk-MCS4LIXP.js.map → chunk-43TUWQNR.js.map} +0 -0
  279. /package/dist/{chunk-2NLCDNGA.js.map → chunk-564YP3DW.js.map} +0 -0
  280. /package/dist/{chunk-PBWJVRKE.js.map → chunk-5FUDSK6Z.js.map} +0 -0
  281. /package/dist/{chunk-V4GPFCJ7.js.map → chunk-5G53JSRO.js.map} +0 -0
  282. /package/dist/{chunk-WJCAAVZX.js.map → chunk-5X2H3X4J.js.map} +0 -0
  283. /package/dist/{chunk-CPBYKOTC.js.map → chunk-6ZQX3R66.js.map} +0 -0
  284. /package/dist/{chunk-AF4UIHON.js.map → chunk-74AY3S7U.js.map} +0 -0
  285. /package/dist/{chunk-XSJL62LE.js.map → chunk-7J6A5MV2.js.map} +0 -0
  286. /package/dist/{chunk-A3WXV7YU.js.map → chunk-7SAE6T36.js.map} +0 -0
  287. /package/dist/{chunk-BWF3KNSQ.js.map → chunk-7YVER7ZO.js.map} +0 -0
  288. /package/dist/{chunk-5IDQVE4U.js.map → chunk-A3UFV4K3.js.map} +0 -0
  289. /package/dist/{chunk-6TIVVOVU.js.map → chunk-BNJAVRNJ.js.map} +0 -0
  290. /package/dist/{chunk-CMOYD5CA.js.map → chunk-CCL2OXFO.js.map} +0 -0
  291. /package/dist/{chunk-FOKTW6GY.js.map → chunk-EWBXMGXR.js.map} +0 -0
  292. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  293. /package/dist/{chunk-E47UQBYW.js.map → chunk-GXVXBYRA.js.map} +0 -0
  294. /package/dist/{chunk-AGCEJDG2.js.map → chunk-HPNXS6W6.js.map} +0 -0
  295. /package/dist/{chunk-N5PISU26.js.map → chunk-IN6U2BS5.js.map} +0 -0
  296. /package/dist/{chunk-HI53EK3Y.js.map → chunk-IUYXCTA7.js.map} +0 -0
  297. /package/dist/{chunk-VGRBKEBH.js.map → chunk-JBWD3J5U.js.map} +0 -0
  298. /package/dist/{chunk-YB3WN6XA.js.map → chunk-K5QQXVG6.js.map} +0 -0
  299. /package/dist/{chunk-7LCGFUMB.js.map → chunk-KQAFEZQX.js.map} +0 -0
  300. /package/dist/{chunk-HDT3DMUF.js.map → chunk-KW5YZELC.js.map} +0 -0
  301. /package/dist/{chunk-DM6SOROF.js.map → chunk-LIBVANOF.js.map} +0 -0
  302. /package/dist/{chunk-VKUMKVEU.js.map → chunk-LMMBV7QC.js.map} +0 -0
  303. /package/dist/{chunk-NMOEOQV4.js.map → chunk-M2DOJBH7.js.map} +0 -0
  304. /package/dist/{chunk-3INLTKTW.js.map → chunk-M4HRYLOA.js.map} +0 -0
  305. /package/dist/{chunk-YY6PWFUC.js.map → chunk-M6TP6UYD.js.map} +0 -0
  306. /package/dist/{chunk-BJTFLP3J.js.map → chunk-MLNXATFG.js.map} +0 -0
  307. /package/dist/{chunk-C7WDWD35.js.map → chunk-MWURUSRE.js.map} +0 -0
  308. /package/dist/{chunk-WIBWONL4.js.map → chunk-N74WJ7B2.js.map} +0 -0
  309. /package/dist/{chunk-LCHFINL4.js.map → chunk-N7FNCE7F.js.map} +0 -0
  310. /package/dist/{chunk-QZK6HG4J.js.map → chunk-NPLM7AFU.js.map} +0 -0
  311. /package/dist/{chunk-CRJX4OMN.js.map → chunk-NSP5PIQH.js.map} +0 -0
  312. /package/dist/{chunk-X52SXKVC.js.map → chunk-P7OVFGMU.js.map} +0 -0
  313. /package/dist/{chunk-2J6BIOVN.js.map → chunk-PJCVI44H.js.map} +0 -0
  314. /package/dist/{chunk-Q6PYCPHO.js.map → chunk-Q3PNQYAC.js.map} +0 -0
  315. /package/dist/{chunk-D76AHITY.js.map → chunk-QS765SBL.js.map} +0 -0
  316. /package/dist/{chunk-KNCR7APE.js.map → chunk-QTDZ2NYC.js.map} +0 -0
  317. /package/dist/{chunk-HCPVL6SF.js.map → chunk-SJTSWG2Z.js.map} +0 -0
  318. /package/dist/{chunk-XFND63OB.js.map → chunk-TFWJXWL6.js.map} +0 -0
  319. /package/dist/{chunk-SVJ5JVZO.js.map → chunk-TXKQBQYW.js.map} +0 -0
  320. /package/dist/{chunk-WQFHHJWK.js.map → chunk-UALXWWGN.js.map} +0 -0
  321. /package/dist/{chunk-3PVKV3XL.js.map → chunk-UNOAVHV4.js.map} +0 -0
  322. /package/dist/{chunk-FYZXZUGU.js.map → chunk-UQBOPCDK.js.map} +0 -0
  323. /package/dist/{chunk-OD7ZXCUF.js.map → chunk-UXGXJUZ5.js.map} +0 -0
  324. /package/dist/{chunk-2BY56HE6.js.map → chunk-VCOMACR6.js.map} +0 -0
  325. /package/dist/{chunk-5FNZ3G6X.js.map → chunk-VKQI47DJ.js.map} +0 -0
  326. /package/dist/{chunk-MPJDQC4G.js.map → chunk-WJCXUE2J.js.map} +0 -0
  327. /package/dist/{chunk-C3HRYVGD.js.map → chunk-WV4UYDP6.js.map} +0 -0
  328. /package/dist/{chunk-E3DL3CAL.js.map → chunk-WXJC2L66.js.map} +0 -0
  329. /package/dist/{chunk-O7GNSHFF.js.map → chunk-XC3H7KON.js.map} +0 -0
  330. /package/dist/{chunk-OOOX5C4X.js.map → chunk-XL2EE5W3.js.map} +0 -0
  331. /package/dist/{chunk-CZVNOTLR.js.map → chunk-XXR4A33N.js.map} +0 -0
  332. /package/dist/{chunk-2AV2U2SZ.js.map → chunk-YDIOMDBW.js.map} +0 -0
  333. /package/dist/{chunk-QFTNWDJ6.js.map → chunk-YECEEJUU.js.map} +0 -0
  334. /package/dist/{chunk-BMG3SAXA.js.map → chunk-Z5277JWF.js.map} +0 -0
  335. /package/dist/{chunk-SW3WHVKN.js.map → chunk-ZFNXJEUG.js.map} +0 -0
  336. /package/dist/{chunk-KMTMMBEB.js.map → chunk-ZNRTHYYW.js.map} +0 -0
  337. /package/dist/{chunk-VCBS5GRQ.js.map → chunk-ZSMA67FV.js.map} +0 -0
  338. /package/dist/{first-start-migration-QVD22WWS.js.map → first-start-migration-A6PHIBUQ.js.map} +0 -0
@@ -77,9 +77,16 @@ export async function plainFileDigest(filePath: string): Promise<string> {
77
77
  return hash.digest("hex");
78
78
  }
79
79
 
80
- import { lstat, utimes } from "node:fs/promises";
80
+ import { lstat, realpath, utimes } from "node:fs/promises";
81
81
  import { isCensusMtimeMs } from "./census-validation.js";
82
- import { resolveSafeArchiveTarget, type SafeArchiveRoot } from "./transfer/fs-utils.js";
82
+ import path from "node:path";
83
+ import {
84
+ fromPosixRelPath,
85
+ isPathInsideRoot,
86
+ resolveSafeArchiveTarget,
87
+ type SafeArchiveRoot,
88
+ validateArchiveRelativePath,
89
+ } from "./transfer/fs-utils.js";
83
90
 
84
91
  /** Sync-internal directory spooling partial uploads and coordination state. */
85
92
  export const SYNC_INTERNAL_DIR = ".offline-sync";
@@ -124,3 +131,55 @@ export async function setSafeFileMtime(
124
131
  await utimes(target, mtime, mtime);
125
132
  return true;
126
133
  }
134
+
135
+ /**
136
+ * True when a regular (non-symlink) file exists at the archive-relative path.
137
+ * A symlink is not a regular local file and reads as absent, so callers treat
138
+ * it like any other missing path instead of failing the whole push.
139
+ */
140
+ export async function offlineSyncRegularFilePresent(
141
+ root: SafeArchiveRoot,
142
+ relPath: string,
143
+ ): Promise<boolean> {
144
+ const target = path.resolve(root.abs, fromPosixRelPath(validateArchiveRelativePath(relPath, root.errorPrefix)));
145
+ if (!isPathInsideRoot(root.abs, target)) {
146
+ throw new Error(`${root.errorPrefix}: record path escapes target root: ${relPath}`);
147
+ }
148
+ const st = await lstat(target).catch((error: NodeJS.ErrnoException) => {
149
+ // SAFETY: Node fs errors carry `code`; only ENOENT means absent, all other
150
+ // errno codes must propagate so census uncertainty fails closed.
151
+ if (error.code === "ENOENT") return null;
152
+ throw error;
153
+ });
154
+ if (!st || st.isSymbolicLink() || !st.isFile()) return false;
155
+ // A regular file still has to resolve inside the root (a symlinked parent directory escapes it).
156
+ await resolveSafeArchiveTarget(root, relPath);
157
+ // The census skips a symlinked directory, so a file reached only through one is absent here too.
158
+ return (await realpath(target)) === path.join(root.real, path.relative(root.abs, target));
159
+ }
160
+
161
+ /** A path the census observed vanished before its digest was read: a retryable race, never a confirmed delete. */
162
+ export class OfflineSyncCensusRaceError extends Error {
163
+ constructor(readonly relPath: string, cause?: unknown) {
164
+ super(
165
+ `offline sync census race: ${relPath} vanished during digest; retry the snapshot`,
166
+ cause === undefined ? undefined : { cause },
167
+ );
168
+ this.name = "OfflineSyncCensusRaceError";
169
+ }
170
+ }
171
+
172
+ /** The message the CLI push retry loop matches: it defers the path and rebuilds the census. */
173
+ export function changedWhileBuildingChangesetError(relPath: string, cause?: unknown): Error {
174
+ return new Error(
175
+ `offline sync file changed while building changeset: ${relPath}`,
176
+ cause === undefined ? undefined : { cause },
177
+ );
178
+ }
179
+
180
+ /** A `.catch` handler: a census race while building a changeset upsert stays retryable. */
181
+ export function censusRaceAsChangedWhileBuilding(relPath: string): (error: Error) => never {
182
+ return (error) => {
183
+ throw error instanceof OfflineSyncCensusRaceError ? changedWhileBuildingChangesetError(relPath, error) : error;
184
+ };
185
+ }
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { createHash } from "node:crypto";
3
- import { chmod, mkdir, mkdtemp, readdir, readFile, rm, stat, utimes, writeFile } from "node:fs/promises";
3
+ import { chmod, mkdir, mkdtemp, readdir, readFile, rm, stat, symlink, utimes, writeFile } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import test from "node:test";
@@ -3714,33 +3714,37 @@ test("applyOfflineSyncSnapshot consumes readIncomingFile for records without inl
3714
3714
  }
3715
3715
  });
3716
3716
 
3717
- test("buildOfflineSyncSnapshot skips a file that vanishes during the walk", async () => {
3717
+ test("buildOfflineSyncSnapshot fails closed when a file vanishes during digest", async () => {
3718
3718
  const root = await tempDir("remnic-offline-vanish");
3719
3719
  try {
3720
3720
  await write(root, "facts/keep.md", "keep");
3721
3721
  await write(root, "facts/gone.md", "gone");
3722
- const snapshot = await buildOfflineSyncSnapshot({
3723
- root,
3724
- sourceId: "local",
3725
- includeContent: false,
3726
- readFileDigest: async ({ path: relPath, filePath }) => {
3727
- if (relPath.endsWith("gone.md")) {
3728
- await rm(filePath);
3729
- throw Object.assign(new Error("ENOENT: vanished"), { code: "ENOENT" });
3730
- }
3731
- const content = await readFile(filePath);
3732
- return {
3733
- sha256: createHash("sha256").update(content).digest("hex"),
3734
- bytes: content.length,
3735
- };
3736
- },
3737
- });
3738
- assert.deepEqual(snapshot.files.map((file) => file.path), ["facts/keep.md"]);
3722
+ await assert.rejects(
3723
+ () => buildOfflineSyncSnapshot({
3724
+ root,
3725
+ sourceId: "local",
3726
+ includeContent: false,
3727
+ readFileDigest: async ({ path: relPath, filePath }) => {
3728
+ if (relPath.endsWith("gone.md")) {
3729
+ await rm(filePath);
3730
+ throw Object.assign(new Error("ENOENT: vanished"), { code: "ENOENT" });
3731
+ }
3732
+ const content = await readFile(filePath);
3733
+ return {
3734
+ sha256: createHash("sha256").update(content).digest("hex"),
3735
+ bytes: content.length,
3736
+ };
3737
+ },
3738
+ }),
3739
+ /census race|vanished during digest|retry the snapshot/i,
3740
+ );
3741
+ assert.equal(await readUtf8(root, "facts/keep.md"), "keep");
3739
3742
  } finally {
3740
3743
  await rm(root, { recursive: true, force: true });
3741
3744
  }
3742
3745
  });
3743
3746
 
3747
+
3744
3748
  test("base snapshot fails when a base-known file vanishes before the fast-base stat", async () => {
3745
3749
  const root = await tempDir("remnic-offline-base-vanish");
3746
3750
  try {
@@ -3770,7 +3774,7 @@ test("base snapshot fails when a base-known file vanishes before the fast-base s
3770
3774
  }
3771
3775
  });
3772
3776
 
3773
- test("base snapshot omits a brand-new file that vanishes during the walk", async () => {
3777
+ test("base snapshot omits a brand-new file that vanishes before the fast-base stat", async () => {
3774
3778
  const root = await tempDir("remnic-offline-new-vanish");
3775
3779
  try {
3776
3780
  await write(root, "facts/keep.md", "keep");
@@ -3798,6 +3802,7 @@ test("base snapshot omits a brand-new file that vanishes during the walk", async
3798
3802
  }
3799
3803
  });
3800
3804
 
3805
+
3801
3806
  test("snapshot fails when an embedding generation member vanishes during the walk", async () => {
3802
3807
  const root = await tempDir("remnic-offline-shard-vanish");
3803
3808
  try {
@@ -3822,3 +3827,291 @@ test("snapshot fails when an embedding generation member vanishes during the wal
3822
3827
  await rm(root, { recursive: true, force: true });
3823
3828
  }
3824
3829
  });
3830
+
3831
+ test("a transient snapshot omission must not emit a delete for a shared-base file", async () => {
3832
+ const root = await tempDir("remnic-offline-uncertain-delete");
3833
+ try {
3834
+ await write(root, "facts/keep.md", "keep");
3835
+ await write(root, "facts/race.md", "race-v1");
3836
+ const base = await buildOfflineSyncSnapshot({
3837
+ root,
3838
+ sourceId: "local",
3839
+ includeContent: false,
3840
+ });
3841
+ // Live census fail-closes on digest ENOENT. A stale/incomplete currentFiles
3842
+ // list is still possible from an older client cache — that path must not
3843
+ // emit a confirmed delete when the file is back on disk.
3844
+ const incompleteFiles = base.files.filter((file) => file.path !== "facts/race.md");
3845
+ assert.deepEqual(
3846
+ incompleteFiles.map((file) => file.path),
3847
+ ["facts/keep.md"],
3848
+ "fixture: the race path is absent from the incomplete currentFiles view",
3849
+ );
3850
+ await write(root, "facts/race.md", "race-v1");
3851
+ const changeset = await buildOfflineSyncChangesetFromSnapshot({
3852
+ root,
3853
+ sourceId: "local",
3854
+ currentFiles: incompleteFiles,
3855
+ baseFiles: base.files,
3856
+ });
3857
+ assert.equal(
3858
+ changeset.changes.filter((change) => change.path === "facts/race.md" && change.type === "delete").length,
3859
+ 0,
3860
+ "an unreadable/recreated path must not become a confirmed delete against the shared base",
3861
+ );
3862
+ } finally {
3863
+ await rm(root, { recursive: true, force: true });
3864
+ }
3865
+ });
3866
+
3867
+ test("apply must not overwrite a recreated local file omitted from the local census", async () => {
3868
+ const root = await tempDir("remnic-offline-uncertain-overwrite");
3869
+ try {
3870
+ const recreated = "recreated local bytes";
3871
+ const incoming = "incoming remote bytes";
3872
+ await write(root, "facts/race.md", recreated);
3873
+ const incomingSha = createHash("sha256").update(incoming).digest("hex");
3874
+ // Local census omitted the recreated path (stale/racy currentFiles).
3875
+ // Incoming is base-less, so the apply path would treat it as a pure create.
3876
+ const result = await applyOfflineSyncSnapshot({
3877
+ root,
3878
+ currentFiles: [],
3879
+ baseFiles: [],
3880
+ snapshot: {
3881
+ format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
3882
+ schemaVersion: 1,
3883
+ createdAt: new Date().toISOString(),
3884
+ sourceId: "remote",
3885
+ includeTranscripts: false,
3886
+ files: [{
3887
+ path: "facts/race.md",
3888
+ sha256: incomingSha,
3889
+ bytes: Buffer.byteLength(incoming),
3890
+ mtimeMs: 2,
3891
+ contentBase64: Buffer.from(incoming).toString("base64"),
3892
+ }],
3893
+ },
3894
+ });
3895
+ assert.equal(
3896
+ await readUtf8(root, "facts/race.md"),
3897
+ recreated,
3898
+ "recreated local content must survive when the local census omitted it",
3899
+ );
3900
+ assert.equal(result.upserted, 0, "must not publish over an uncertain local path");
3901
+ } finally {
3902
+ await rm(root, { recursive: true, force: true });
3903
+ }
3904
+ });
3905
+
3906
+ test("changeset apply must not overwrite a recreated local file omitted by controlled digest census", async () => {
3907
+ // Public applyOfflineSyncChangeset builds local currentFiles via
3908
+ // buildOfflineSyncSnapshotForPaths when currentFiles is omitted. A controlled
3909
+ // digest that recreates local bytes then throws ENOENT must fail closed at
3910
+ // the shared census boundary — never export confirmed absence and overwrite.
3911
+ const root = await tempDir("remnic-offline-changeset-uncertain-overwrite");
3912
+ try {
3913
+ const recreated = "recreated local bytes";
3914
+ const incoming = "incoming remote bytes";
3915
+ const incomingSha = createHash("sha256").update(incoming).digest("hex");
3916
+ await write(root, "facts/race.md", recreated);
3917
+
3918
+ await assert.rejects(
3919
+ () => applyOfflineSyncChangeset({
3920
+ root,
3921
+ // No currentFiles: exercise the public snapshotForPaths census path.
3922
+ returnCurrentFiles: false,
3923
+ writeConflictCopies: false,
3924
+ readFileDigest: async ({ path: relPath, filePath }) => {
3925
+ if (relPath === "facts/race.md") {
3926
+ await writeFile(filePath, recreated);
3927
+ throw Object.assign(new Error("ENOENT: vanished during census"), { code: "ENOENT" });
3928
+ }
3929
+ const content = await readFile(filePath);
3930
+ return {
3931
+ sha256: createHash("sha256").update(content).digest("hex"),
3932
+ bytes: content.length,
3933
+ };
3934
+ },
3935
+ changeset: {
3936
+ format: "remnic.offline-sync.changeset.v1",
3937
+ schemaVersion: 1,
3938
+ createdAt: new Date().toISOString(),
3939
+ sourceId: "remote",
3940
+ includeTranscripts: false,
3941
+ changes: [{
3942
+ type: "upsert",
3943
+ path: "facts/race.md",
3944
+ file: {
3945
+ path: "facts/race.md",
3946
+ sha256: incomingSha,
3947
+ bytes: Buffer.byteLength(incoming),
3948
+ mtimeMs: 2,
3949
+ contentBase64: Buffer.from(incoming).toString("base64"),
3950
+ },
3951
+ }],
3952
+ },
3953
+ }),
3954
+ /census race|vanished during digest|retry the snapshot/i,
3955
+ );
3956
+
3957
+ assert.equal(
3958
+ await readUtf8(root, "facts/race.md"),
3959
+ recreated,
3960
+ "recreated local content must survive a fail-closed public changeset census race",
3961
+ );
3962
+ } finally {
3963
+ await rm(root, { recursive: true, force: true });
3964
+ }
3965
+ });
3966
+
3967
+ test("sender snapshot omission from digest race must not delete receiver shared-base file", async () => {
3968
+ // Root premise: sender digest race must not export an apparently-complete
3969
+ // snapshot that omits the path. Fail closed at the shared census boundary so
3970
+ // no consumer can assign confirmed absence. Sender and receiver bytes stay.
3971
+ const senderRoot = await tempDir("remnic-offline-sender-uncertain-omit");
3972
+ const receiverRoot = await tempDir("remnic-offline-receiver-shared-base");
3973
+ try {
3974
+ const good = "unchanged good fact bytes";
3975
+ const keep = "keep peer";
3976
+ await write(senderRoot, "facts/race.md", good);
3977
+ await write(senderRoot, "facts/keep.md", keep);
3978
+ await write(receiverRoot, "facts/race.md", good);
3979
+ await write(receiverRoot, "facts/keep.md", keep);
3980
+
3981
+ const sharedBase = await buildOfflineSyncSnapshot({
3982
+ root: receiverRoot,
3983
+ sourceId: "shared",
3984
+ includeContent: false,
3985
+ });
3986
+ assert.ok(
3987
+ sharedBase.files.some((file) => file.path === "facts/race.md"),
3988
+ "fixture: shared base includes the good race path",
3989
+ );
3990
+
3991
+ await assert.rejects(
3992
+ () => buildOfflineSyncSnapshot({
3993
+ root: senderRoot,
3994
+ sourceId: "sender",
3995
+ includeContent: false,
3996
+ readFileDigest: async ({ path: relPath, filePath }) => {
3997
+ if (relPath === "facts/race.md") {
3998
+ await writeFile(filePath, good);
3999
+ throw Object.assign(new Error("ENOENT: vanished during sender census"), { code: "ENOENT" });
4000
+ }
4001
+ const content = await readFile(filePath);
4002
+ return {
4003
+ sha256: createHash("sha256").update(content).digest("hex"),
4004
+ bytes: content.length,
4005
+ };
4006
+ },
4007
+ }),
4008
+ /census race|vanished during digest|retry the snapshot/i,
4009
+ );
4010
+
4011
+ assert.equal(await readUtf8(senderRoot, "facts/race.md"), good, "sender bytes preserved");
4012
+ assert.equal(await readUtf8(receiverRoot, "facts/race.md"), good, "receiver bytes preserved without a false snapshot");
4013
+ assert.equal(await readUtf8(receiverRoot, "facts/keep.md"), keep);
4014
+ } finally {
4015
+ await rm(senderRoot, { recursive: true, force: true });
4016
+ await rm(receiverRoot, { recursive: true, force: true });
4017
+ }
4018
+ });
4019
+
4020
+
4021
+ test("a file vanishing during the changeset upsert read keeps the retryable changed-while-building message", async () => {
4022
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-changeset-vanish-"));
4023
+ try {
4024
+ const rel = "facts/volatile.md";
4025
+ const bytes = Buffer.from("volatile bytes\n");
4026
+ await mkdir(path.join(root, "facts"), { recursive: true });
4027
+ await writeFile(path.join(root, rel), bytes);
4028
+ const currentFile = {
4029
+ path: rel,
4030
+ sha256: createHash("sha256").update(bytes).digest("hex"),
4031
+ bytes: bytes.byteLength,
4032
+ mtimeMs: 1,
4033
+ };
4034
+ await assert.rejects(
4035
+ buildOfflineSyncChangesetFromSnapshot({
4036
+ root,
4037
+ sourceId: "laptop",
4038
+ baseFiles: [],
4039
+ currentFiles: [currentFile],
4040
+ readFile: async () => {
4041
+ await rm(path.join(root, rel), { force: true });
4042
+ throw Object.assign(new Error("ENOENT: file vanished"), { code: "ENOENT" });
4043
+ },
4044
+ }),
4045
+ (error: Error) => {
4046
+ // The CLI retry loop matches this exact prefix to defer the path and rebuild the census.
4047
+ assert.equal(error.message, `offline sync file changed while building changeset: ${rel}`);
4048
+ return true;
4049
+ },
4050
+ );
4051
+ } finally {
4052
+ await rm(root, { recursive: true, force: true });
4053
+ }
4054
+ });
4055
+
4056
+ test("a tracked file replaced by a symlink is a local delete, not a push-blocking error", async () => {
4057
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-symlink-replaced-"));
4058
+ const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-symlink-target-"));
4059
+ try {
4060
+ const rel = "facts/tracked.md";
4061
+ const bytes = Buffer.from("tracked bytes\n");
4062
+ await mkdir(path.join(root, "facts"), { recursive: true });
4063
+ await writeFile(path.join(outside, "target.md"), "outside\n");
4064
+ await symlink(path.join(outside, "target.md"), path.join(root, rel));
4065
+ const baseFile = {
4066
+ path: rel,
4067
+ sha256: createHash("sha256").update(bytes).digest("hex"),
4068
+ bytes: bytes.byteLength,
4069
+ mtimeMs: 1,
4070
+ };
4071
+ const changeset = await buildOfflineSyncChangesetFromSnapshot({
4072
+ root,
4073
+ sourceId: "laptop",
4074
+ baseFiles: [baseFile],
4075
+ currentFiles: [],
4076
+ });
4077
+ assert.deepEqual(
4078
+ changeset.changes.map((change) => [change.type, change.path]),
4079
+ [["delete", rel]],
4080
+ "a symlink is not a regular local file: it reads as a delete, as before the census change",
4081
+ );
4082
+ } finally {
4083
+ await rm(root, { recursive: true, force: true });
4084
+ await rm(outside, { recursive: true, force: true });
4085
+ }
4086
+ });
4087
+
4088
+ test("a tracked file under a directory replaced by an in-root symlink is a local delete", async () => {
4089
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-symlink-parent-"));
4090
+ try {
4091
+ const rel = "facts/tracked.md";
4092
+ const bytes = Buffer.from("tracked bytes\n");
4093
+ await mkdir(path.join(root, "elsewhere"), { recursive: true });
4094
+ await writeFile(path.join(root, "elsewhere", "tracked.md"), bytes);
4095
+ // The census skips a symlinked directory, so facts/tracked.md is not in it.
4096
+ await symlink(path.join(root, "elsewhere"), path.join(root, "facts"));
4097
+ const baseFile = {
4098
+ path: rel,
4099
+ sha256: createHash("sha256").update(bytes).digest("hex"),
4100
+ bytes: bytes.byteLength,
4101
+ mtimeMs: 1,
4102
+ };
4103
+ const changeset = await buildOfflineSyncChangesetFromSnapshot({
4104
+ root,
4105
+ sourceId: "laptop",
4106
+ baseFiles: [baseFile],
4107
+ currentFiles: [],
4108
+ });
4109
+ assert.deepEqual(
4110
+ changeset.changes.map((change) => [change.type, change.path]),
4111
+ [["delete", rel]],
4112
+ "a path reached only through a symlinked parent is absent, as the census sees it",
4113
+ );
4114
+ } finally {
4115
+ await rm(root, { recursive: true, force: true });
4116
+ }
4117
+ });
@@ -70,6 +70,10 @@ import {
70
70
  setSafeFileMtime,
71
71
  assertNonNegativeFinite,
72
72
  assertOfflineSyncMtimeMs,
73
+ offlineSyncRegularFilePresent,
74
+ OfflineSyncCensusRaceError,
75
+ censusRaceAsChangedWhileBuilding,
76
+ changedWhileBuildingChangesetError,
73
77
  OFFLINE_SYNC_FAST_BASE_MTIME_TOLERANCE_MS,
74
78
  } from "./offline-sync-file-io.js";
75
79
  import { CENSUS_MAX_MTIME_MS, isSha256Hex } from "./census-validation.js";
@@ -793,19 +797,29 @@ async function canReuseFastBaseFileStateFromDisk(
793
797
 
794
798
  async function readOfflineSyncFileRecord(
795
799
  options: OfflineSyncFileRecordOptions,
796
- ): Promise<OfflineSyncFileRecord | null> {
800
+ ): Promise<OfflineSyncFileRecord> {
797
801
  try {
798
802
  return await readPresentOfflineSyncFileRecord(options);
799
803
  } catch (error) {
800
- // A file removed between readdir and stat/hash is a race, not a corrupt corpus.
801
- if (isNotFoundError(error)) return null;
804
+ // Caller already observed the path (readdir/lstat). Digest ENOENT is an
805
+ // ambiguous race: exporting a silent omission assigns confirmed absence to
806
+ // every consumer (full snapshot apply + changeset apply). Fail closed.
807
+ if (isNotFoundError(error)) {
808
+ if (embeddingGenerationMembership(options.relPath) !== null) {
809
+ throw new Error(
810
+ `offline sync embedding generation member vanished during snapshot walk: ${options.relPath}`,
811
+ );
812
+ }
813
+ throw new OfflineSyncCensusRaceError(options.relPath, error);
814
+ }
802
815
  throw error;
803
816
  }
804
817
  }
805
818
 
806
- // A vanished path may only be omitted when nothing can misread the gap:
807
- // embedding generations travel and replace as one unit (issue #3148), and a
808
- // base-known file omitted here would fabricate a delete in
819
+
820
+ // Pre-stat vanish (readdir then lstat/stat ENOENT) may omit only when nothing
821
+ // can misread the gap: embedding generations travel as one unit (issue #3148),
822
+ // and a base-known file omitted here would fabricate a delete in
809
823
  // buildOfflineSyncChangesetFromSnapshot.
810
824
  function assertSnapshotVanishIsSafe(relPath: string, base: ReadonlyMap<string, unknown> | undefined): void {
811
825
  if (embeddingGenerationMembership(relPath) !== null) {
@@ -816,6 +830,7 @@ function assertSnapshotVanishIsSafe(relPath: string, base: ReadonlyMap<string, u
816
830
  }
817
831
  }
818
832
 
833
+
819
834
  async function readPresentOfflineSyncFileRecord(
820
835
  options: OfflineSyncFileRecordOptions,
821
836
  ): Promise<OfflineSyncFileRecord> {
@@ -923,7 +938,7 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
923
938
  path: relPosix,
924
939
  filePath: abs,
925
940
  })) continue;
926
- const record = await readOfflineSyncFileRecord({
941
+ yield await readOfflineSyncFileRecord({
927
942
  root,
928
943
  relPath: relPosix,
929
944
  filePath: abs,
@@ -932,8 +947,7 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
932
947
  readFileDigest: options.readFileDigest,
933
948
  signal: options.signal,
934
949
  });
935
- if (record) yield record;
936
- else assertSnapshotVanishIsSafe(relPosix, undefined);
950
+
937
951
  }
938
952
  }
939
953
 
@@ -1094,8 +1108,7 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
1094
1108
  try {
1095
1109
  st = await stat(abs);
1096
1110
  } catch (error) {
1097
- // Same race readOfflineSyncFileRecord tolerates, unless omitting the
1098
- // path could fabricate a delete or downgrade a generation.
1111
+ // Pre-stat vanish after readdir: omit only when assertSnapshotVanishIsSafe allows.
1099
1112
  if (isNotFoundError(error)) {
1100
1113
  assertSnapshotVanishIsSafe(relPosix, base);
1101
1114
  continue;
@@ -1112,7 +1125,7 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
1112
1125
  files.push(baseEntry);
1113
1126
  continue;
1114
1127
  }
1115
- const record = await readOfflineSyncFileRecord({
1128
+ files.push(await readOfflineSyncFileRecord({
1116
1129
  root,
1117
1130
  relPath: relPosix,
1118
1131
  filePath: abs,
@@ -1120,9 +1133,7 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
1120
1133
  readFile: options.readFile,
1121
1134
  readFileDigest: options.readFileDigest,
1122
1135
  signal: options.signal,
1123
- });
1124
- if (record) files.push(record);
1125
- else assertSnapshotVanishIsSafe(relPosix, base);
1136
+ }));
1126
1137
  }
1127
1138
  }
1128
1139
 
@@ -1208,7 +1219,7 @@ export async function buildOfflineSyncSnapshotForPaths(options: {
1208
1219
  })) {
1209
1220
  throw new Error(`offline sync snapshot path is excluded: ${relPath}`);
1210
1221
  }
1211
- const record = await readOfflineSyncFileRecord({
1222
+ files.push(await readOfflineSyncFileRecord({
1212
1223
  root,
1213
1224
  relPath,
1214
1225
  filePath,
@@ -1216,9 +1227,7 @@ export async function buildOfflineSyncSnapshotForPaths(options: {
1216
1227
  readFile: options.readFile,
1217
1228
  readFileDigest: options.readFileDigest,
1218
1229
  signal: options.signal,
1219
- });
1220
- if (record) files.push(record);
1221
- else assertSnapshotVanishIsSafe(relPath, undefined);
1230
+ }));
1222
1231
  }
1223
1232
  throwIfOfflineSyncAborted(options.signal);
1224
1233
 
@@ -1414,6 +1423,11 @@ export async function buildOfflineSyncChangesetFromSnapshot(options: {
1414
1423
  excludeNodeLocalState?: boolean;
1415
1424
  readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
1416
1425
  }): Promise<OfflineSyncChangeset> {
1426
+ const root = await prepareSafeArchiveRoot(
1427
+ path.resolve(options.root),
1428
+ "buildOfflineSyncChangesetFromSnapshot",
1429
+ "root",
1430
+ );
1417
1431
  const includeTranscripts = options.includeTranscripts !== false;
1418
1432
  const excludedPaths = new Set(
1419
1433
  (options.excludePaths ?? []).map((relPath) => normalizeRelativePath(relPath, "excludePaths[]")),
@@ -1453,10 +1467,10 @@ export async function buildOfflineSyncChangesetFromSnapshot(options: {
1453
1467
  now: options.now,
1454
1468
  userExcludeRegexps: options.userExcludeRegexps,
1455
1469
  readFile: options.readFile,
1456
- });
1470
+ }).catch(censusRaceAsChangedWhileBuilding(relPath));
1457
1471
  const record = file.files[0];
1458
1472
  if (!record || typeof record.contentBase64 !== "string" || record.sha256 !== currentEntry.sha256) {
1459
- throw new Error(`offline sync file changed while building changeset: ${relPath}`);
1473
+ throw changedWhileBuildingChangesetError(relPath);
1460
1474
  }
1461
1475
  changes.push({
1462
1476
  type: "upsert",
@@ -1467,6 +1481,8 @@ export async function buildOfflineSyncChangesetFromSnapshot(options: {
1467
1481
  continue;
1468
1482
  }
1469
1483
  if (!currentEntry && baseEntry) {
1484
+ // Census miss is not confirmed delete — recheck disk (transient ENOENT / recreate).
1485
+ if (await offlineSyncRegularFilePresent(root, relPath)) continue;
1470
1486
  const mtimeMs = deletionMtimeByPath?.get(relPath);
1471
1487
  changes.push({
1472
1488
  type: "delete",
@@ -1808,6 +1824,12 @@ export async function applyOfflineSyncSnapshot(options: {
1808
1824
  continue;
1809
1825
  }
1810
1826
  if (!currentEntry && !base) {
1827
+ // Census miss with on-disk file: uncertain local presence, do not overwrite.
1828
+ if (await offlineSyncRegularFilePresent(root, relPath)) {
1829
+ pendingLocal += 1;
1830
+ skipped += 1;
1831
+ continue;
1832
+ }
1811
1833
  await writeSafeFile(root, relPath, requiredBuffer(incomingBuffers, relPath), options.writeFile, incoming.mtimeMs);
1812
1834
  nextBase.set(relPath, toFileState(incoming));
1813
1835
  upserted += 1;
@@ -1,5 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
6
  import test from "node:test";
@@ -1547,3 +1548,50 @@ test("startupSearchSync composes its caller abort into the QMD probe", async ()
1547
1548
  await rm(memoryDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
1548
1549
  }
1549
1550
  });
1551
+
1552
+ // Both startup call sites must open read readiness without migration replay.
1553
+ for (const namespace of ["default", "shared"]) {
1554
+ test(`startup defers entity migration for ${namespace} storage`, async () => {
1555
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-init-migration-defer-"));
1556
+ const orchestrator = new Orchestrator(
1557
+ parseConfig({
1558
+ memoryDir,
1559
+ workspaceDir: path.join(memoryDir, "workspace"),
1560
+ qmdEnabled: false,
1561
+ namespacesEnabled: namespace === "shared",
1562
+ namespaceCatalogEnabled: false,
1563
+ defaultNamespace: "default",
1564
+ sharedNamespace: "shared",
1565
+ namespacePolicies: [],
1566
+ embeddingFallbackEnabled: false,
1567
+ identityContinuityEnabled: false,
1568
+ transcriptEnabled: false,
1569
+ hourlySummariesEnabled: false,
1570
+ compoundingEnabled: false,
1571
+ knowledgeIndexEnabled: false,
1572
+ }),
1573
+ );
1574
+ const storage = await orchestrator.getStorage(namespace);
1575
+ const lockPath = path.join(storage.dir, "state", "entity-canonical-id-migration.lock");
1576
+ const journalPath = path.join(storage.dir, "state", "entity-canonical-id-migration-v1.json");
1577
+ try {
1578
+ await mkdir(path.dirname(lockPath), { recursive: true });
1579
+ await writeFile(lockPath, `${process.pid} init-migration-test ${new Date().toISOString()}\n`);
1580
+ const outcome = await withTestDeadline(
1581
+ orchestrator.initialize().then(() => "settled" as const, () => "rejected" as const),
1582
+ );
1583
+ assert.equal(outcome, "settled", "startup must succeed while the migration lock is held");
1584
+ assert.equal(existsSync(lockPath), true);
1585
+ assert.equal(existsSync(journalPath), false, "startup must not run the migration");
1586
+ await rm(lockPath);
1587
+ await storage.ensureDirectories();
1588
+ // SAFETY: the journal is written by the migration under test and carries an optional boolean `complete`.
1589
+ const state = JSON.parse(await readFile(journalPath, "utf8")) as { complete?: boolean };
1590
+ assert.equal(state.complete, true, "default setup must still run migration");
1591
+ } finally {
1592
+ await rm(lockPath, { force: true });
1593
+ await orchestrator.destroy();
1594
+ await rm(memoryDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
1595
+ }
1596
+ });
1597
+ }