@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
@@ -746,6 +746,7 @@ export class Orchestrator {
746
746
  private _dependencyPropagationDelivery: DependencyPropagationDelivery | undefined;
747
747
  private _dependencyPropagationRecovery: Promise<void> | null = null;
748
748
  private destroyed = false;
749
+ private acceptingEntityMigrationMaintenance = true;
749
750
 
750
751
  private get dependencyPropagationDelivery(): DependencyPropagationDelivery {
751
752
  if (this.destroyed) throw new Error("orchestrator has been destroyed");
@@ -967,6 +968,7 @@ export class Orchestrator {
967
968
  * commands should call it before returning to let Node exit naturally.
968
969
  */
969
970
  async destroy(): Promise<void> {
971
+ this.acceptingEntityMigrationMaintenance = false;
970
972
  this._orchestratorInitCoordinator?.retireStartupEpoch();
971
973
  this.abortDeferredInit();
972
974
  this.extractionQueueCoordinator.stopAccepting();
@@ -1499,14 +1501,6 @@ export class Orchestrator {
1499
1501
  (namespace) => this.namespaceSearchRouter.backendForNamespace(namespace),
1500
1502
  (msg) => log.debug(msg),
1501
1503
  );
1502
- const defaultNs = this.config.defaultNamespace;
1503
- this.storage.onMemoryWrite = (filePath) => this.prioritizedEmbedding?.enqueue(filePath, defaultNs);
1504
- this.storageRouter.onStorageCreated = (storage, namespace) => {
1505
- storage.onMemoryWrite = (filePath) => this.prioritizedEmbedding?.enqueue(filePath, namespace);
1506
- };
1507
- this.storageRouter.forEachCachedStorage((storage, namespace) => {
1508
- storage.onMemoryWrite = (filePath) => this.prioritizedEmbedding?.enqueue(filePath, namespace);
1509
- });
1510
1504
  }
1511
1505
  this.maintenanceScheduler = new MaintenanceScheduler({
1512
1506
  config,
@@ -1516,6 +1510,13 @@ export class Orchestrator {
1516
1510
  getStorage: () => this.storage, storageForNamespace: (namespace) => this.storageRouter.storageFor(namespace),
1517
1511
  onActivitySynced: (summary) => { if (!this.config.meetings.enabled) return; void this.getMeetingsService().then((s) => s.requestBuildForActivitySync(summary.ranAt, this.config.activity.timezone, summary.touchedDates)).catch((err) => log.warn(`meetings: activity tail-step build failed to schedule: ${err instanceof Error ? err.message : String(err)}`)); },
1518
1512
  });
1513
+ const bindStorage = (storage: StorageManager, namespace: string) => {
1514
+ storage.onEntityMigrationComplete = () => { if (this.acceptingEntityMigrationMaintenance) this.requestQmdMaintenance(); };
1515
+ if (this.prioritizedEmbedding) storage.onMemoryWrite = (filePath) => this.prioritizedEmbedding?.enqueue(filePath, namespace);
1516
+ };
1517
+ bindStorage(this.storage, this.config.defaultNamespace);
1518
+ this.storageRouter.onStorageCreated = bindStorage;
1519
+ this.storageRouter.forEachCachedStorage(bindStorage);
1519
1520
  // Issue #1526: background extraction queue lives on its own coordinator.
1520
1521
  this.extractionQueueCoordinator = new ExtractionQueueCoordinator();
1521
1522
 
@@ -1,3 +1,4 @@
1
+ import { log } from "../logger.js";
1
2
  import type { EntityFile, MemoryFile } from "../types.js";
2
3
  import {
3
4
  getFingerprint,
@@ -30,6 +31,7 @@ export interface EntityCanonicalIdMigrationHost {
30
31
  invalidateColdMemoriesCache(): void;
31
32
  bumpMemoryStatusVersion(): void;
32
33
  getEntityMutationVersion(): number;
34
+ onEntityMigrationComplete?: () => void;
33
35
  }
34
36
 
35
37
  export async function runLegacyEntityCanonicalIdMigration(
@@ -40,6 +42,9 @@ export async function runLegacyEntityCanonicalIdMigration(
40
42
  ): Promise<string | undefined> {
41
43
  const readMigrationFingerprint = () =>
42
44
  getFingerprint(host.baseDir, host.entitiesDir, () => String(host.getEntityMutationVersion()));
45
+ // The migration bumps the status version only after a pass that rewrote
46
+ // references; a no-op replay must not ask search maintenance to run.
47
+ let rewroteReferences = false;
43
48
  const completionFingerprint = await migrateLegacyEntityCanonicalIds({
44
49
  stateDir: host.stateDir,
45
50
  entitiesDir: host.entitiesDir,
@@ -65,9 +70,19 @@ export async function runLegacyEntityCanonicalIdMigration(
65
70
  invalidateKnowledgeIndexCache: host.invalidateKnowledgeIndexCache.bind(host),
66
71
  invalidateAllMemoriesCache: host.invalidateAllMemoriesCache.bind(host),
67
72
  invalidateColdMemoriesCache: host.invalidateColdMemoriesCache.bind(host),
68
- bumpMemoryStatusVersion: host.bumpMemoryStatusVersion.bind(host),
73
+ bumpMemoryStatusVersion: () => {
74
+ rewroteReferences = true;
75
+ host.bumpMemoryStatusVersion();
76
+ },
69
77
  readMigrationFingerprint,
70
78
  } satisfies EntityCanonicalIdMigrationDependencies);
79
+ if (completionFingerprint !== undefined && rewroteReferences) {
80
+ try {
81
+ host.onEntityMigrationComplete?.();
82
+ } catch {
83
+ log.warn("entity migration: search maintenance scheduling failed");
84
+ }
85
+ }
71
86
  // An undefined completion fingerprint is a deliberate signal (issue #2213):
72
87
  // a reconcile marker landed while the run scanned, and recomputing the
73
88
  // fingerprint here would seal that pending request into the runner's
@@ -47,6 +47,7 @@ const EntityCanonicalIdMigrationStateSchema = z.object({
47
47
  version: z.literal(ENTITY_CANONICAL_ID_MIGRATION_VERSION),
48
48
  complete: z.boolean(),
49
49
  mappings: z.record(z.string().min(1)),
50
+ completionFingerprint: z.string().optional(),
50
51
  /**
51
52
  * Pairs the migration refused to resolve, PARKED rather than dropped.
52
53
  * Deleting the record would strand the collision: once an operator removes
@@ -370,6 +371,13 @@ async function assertEntityContentUnchanged(
370
371
  }
371
372
  }
372
373
 
374
+ /**
375
+ * Fail-closed entity-entry validation for the startup path: refuses a
376
+ * symlinked or non-directory entities root and any symlinked entry, without
377
+ * taking the migration lock. Exported so `ensureDirectories()` can run it
378
+ * before deferring the migration (#3139) — the same check
379
+ * `migrateLegacyEntityCanonicalIds` performs under the lock.
380
+ */
373
381
  export async function readSafeEntityEntries(entitiesDir: string): Promise<string[]> {
374
382
  const rootStat = await lstat(entitiesDir);
375
383
  if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
@@ -887,14 +895,11 @@ export async function migrateLegacyEntityCanonicalIds(
887
895
  if (!isErrnoCode(error, "ENOENT")) throw error;
888
896
  }
889
897
  }
890
- let state = await readState(deps);
891
- if (!state) {
892
- state = {
893
- version: ENTITY_CANONICAL_ID_MIGRATION_VERSION,
894
- complete: false,
895
- mappings: {},
896
- };
897
- }
898
+ let state: EntityCanonicalIdMigrationState = await readState(deps) ?? {
899
+ version: ENTITY_CANONICAL_ID_MIGRATION_VERSION,
900
+ complete: false,
901
+ mappings: {},
902
+ };
898
903
  const blocked: BlockedEntityPairs = new Map();
899
904
  const scanNotes: EntityScanNotes = { junk: new Set(), malformed: new Set() };
900
905
  // Parks for pairs whose files are GONE, tracked separately from
@@ -951,6 +956,10 @@ export async function migrateLegacyEntityCanonicalIds(
951
956
  // fingerprint could SEAL a marker the check never saw.
952
957
  const completionFingerprint = await deps.readMigrationFingerprint?.();
953
958
  if (await fileExists(reconcileMarkerPath)) return undefined;
959
+ if (completionFingerprint !== undefined && state.completionFingerprint !== completionFingerprint) {
960
+ state = { ...state, complete: true, completionFingerprint };
961
+ await publishState(deps, state);
962
+ }
954
963
  return completionFingerprint;
955
964
  };
956
965
  /**
@@ -16,6 +16,7 @@ import { statSync } from "node:fs";
16
16
  * reconciliation pass instead ({@link requestEntityCanonicalIdReconcile}).
17
17
  */
18
18
  import path from "node:path";
19
+ import { z } from "zod";
19
20
  import {
20
21
  closeSync,
21
22
  constants,
@@ -112,6 +113,29 @@ function readJournalFileNoFollow(statePath: string): JournalRead {
112
113
  }
113
114
  }
114
115
 
116
+ /** The journal fields completion attestation needs; extra journal fields are ignored. */
117
+ const CompletedJournalAttestationSchema = z.object({
118
+ version: z.literal(1),
119
+ complete: z.literal(true),
120
+ completionFingerprint: z.string(),
121
+ });
122
+
123
+ /** Skip compatibility scans only when journal completion attests to the live inputs. */
124
+ export async function entityCanonicalIdMigrationCompleted(
125
+ stateDir: string,
126
+ readFingerprint: () => Promise<string>,
127
+ ): Promise<boolean> {
128
+ const result = readJournalFileNoFollow(path.join(stateDir, ENTITY_CANONICAL_ID_MIGRATION_FILE));
129
+ if (result.kind !== "ok") return false;
130
+ try {
131
+ const parsed = CompletedJournalAttestationSchema.safeParse(JSON.parse(result.raw));
132
+ if (!parsed.success) return false;
133
+ return parsed.data.completionFingerprint === await readFingerprint();
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+
115
139
  export function loadHistoricalEntityCanonicalIds(stateDir: string): Readonly<Record<string, string>> {
116
140
  const result = readJournalFileNoFollow(path.join(stateDir, ENTITY_CANONICAL_ID_MIGRATION_FILE));
117
141
  if (result.kind !== "ok") return {};
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Regression: amortized completion must refuse a symlinked entities root
3
+ * before getFingerprint → fingerprintEntityEntries readdir.
4
+ *
5
+ * inputSig already labels the root "L", but isCompleted still runs the full
6
+ * fingerprint (external tree walk) before any isTrustedEntitiesRoot-style
7
+ * guard. A planted complete journal makes that path reachable after a
8
+ * missing-id ENOENT.
9
+ *
10
+ * Instrumentation (deterministic): spawn a plain child (not nested --test —
11
+ * node skips recursive test runs) with --experimental-test-module-mocks that
12
+ * wraps node:fs/promises.readdir via mock.module, dynamic-imports the real
13
+ * EntityMigrationCompletionCache, and records every readdir whose realpath is
14
+ * the external canary (or whose path is the entities symlink). Assert
15
+ * externalCount === 0 — not merely completed === false. chmod/EACCES is not
16
+ * used (false-green on this host).
17
+ *
18
+ * Dynamic import in the probe is intentional: mock.module must install before
19
+ * the cache module loads fs.promises (module-loading boundary).
20
+ *
21
+ * Temp roots only. Expected today: RED (externalCount >= 1).
22
+ * After root nofollow guard: externalCount === 0 and completed === false.
23
+ */
24
+ import assert from "node:assert/strict";
25
+ import { spawnSync } from "node:child_process";
26
+ import { existsSync } from "node:fs";
27
+ import {
28
+ lstat,
29
+ mkdir,
30
+ mkdtemp,
31
+ readFile,
32
+ realpath,
33
+ rm,
34
+ symlink,
35
+ writeFile,
36
+ } from "node:fs/promises";
37
+ import os from "node:os";
38
+ import path from "node:path";
39
+ import test from "node:test";
40
+ import { fileURLToPath } from "node:url";
41
+
42
+ import { ENTITY_CANONICAL_ID_MIGRATION_FILE } from "./entity-canonical-id-references.js";
43
+
44
+ function findRepoRoot(startDir: string): string {
45
+ let dir = startDir;
46
+ for (;;) {
47
+ if (existsSync(path.join(dir, "packages", "remnic-core", "package.json"))) return dir;
48
+ const parent = path.dirname(dir);
49
+ if (parent === dir) {
50
+ throw new Error(`remnic repo root not found from ${startDir}`);
51
+ }
52
+ dir = parent;
53
+ }
54
+ }
55
+
56
+ const REPO_ROOT = findRepoRoot(path.dirname(fileURLToPath(import.meta.url)));
57
+ const CACHE_MODULE = path.join(
58
+ REPO_ROOT,
59
+ "packages",
60
+ "remnic-core",
61
+ "src",
62
+ "storage",
63
+ "entity-migration-completion-cache.ts",
64
+ );
65
+
66
+ type ProbeResult = {
67
+ completed: boolean | null;
68
+ err: string | null;
69
+ externalCount: number;
70
+ seen: Array<{ raw: string; real: string }>;
71
+ canaryReal: string;
72
+ };
73
+
74
+ test("isCompleted must not readdir through a symlinked entities root", async () => {
75
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-entities-root-nofollow-"));
76
+ const memoryDir = path.join(root, "memory");
77
+ const canaryDir = path.join(root, "canary-external");
78
+ const entitiesDir = path.join(memoryDir, "entities");
79
+ const stateDir = path.join(memoryDir, "state");
80
+ const probeFile = path.join(root, "completion-cache-nofollow.probe.mts");
81
+ const resultFile = path.join(root, "probe-result.json");
82
+
83
+ try {
84
+ await mkdir(stateDir, { recursive: true });
85
+ await mkdir(path.join(memoryDir, "config"), { recursive: true });
86
+ await mkdir(canaryDir, { recursive: true });
87
+ await writeFile(path.join(canaryDir, "outside-only.md"), "canary external entity body\n");
88
+
89
+ await symlink(canaryDir, entitiesDir);
90
+ assert.equal(
91
+ (await lstat(entitiesDir)).isSymbolicLink(),
92
+ true,
93
+ "fixture: entities/ must be a symlink to the external canary directory",
94
+ );
95
+ const canaryReal = await realpath(canaryDir);
96
+
97
+ // Regular complete journal — completionFingerprint compare invokes getFingerprint.
98
+ await writeFile(
99
+ path.join(stateDir, ENTITY_CANONICAL_ID_MIGRATION_FILE),
100
+ JSON.stringify({
101
+ version: 1,
102
+ complete: true,
103
+ mappings: {},
104
+ completionFingerprint: "planted-complete-journal-for-symlink-root-regression",
105
+ }),
106
+ );
107
+
108
+ // Plain probe script (no nested --test: parent already runs under the test
109
+ // runner, and node skips recursive test-file execution).
110
+ // Dynamic import loads cache after mock.module wraps readdir.
111
+ await writeFile(
112
+ probeFile,
113
+ `import { mock } from "node:test";
114
+ import path from "node:path";
115
+ import { writeFile } from "node:fs/promises";
116
+ import { readdir as realReaddir, realpath } from "node:fs/promises";
117
+
118
+ const canaryReal = ${JSON.stringify(canaryReal)};
119
+ const memoryDir = ${JSON.stringify(memoryDir)};
120
+ const entitiesDir = ${JSON.stringify(entitiesDir)};
121
+ const cacheModule = ${JSON.stringify(CACHE_MODULE)};
122
+ const resultFile = ${JSON.stringify(resultFile)};
123
+ const seen = [];
124
+
125
+ const fsp = await import("node:fs/promises");
126
+ mock.module("node:fs/promises", {
127
+ namedExports: {
128
+ ...fsp,
129
+ readdir: async (p, ...a) => {
130
+ const raw = path.resolve(String(p));
131
+ let real = raw;
132
+ try { real = await realpath(raw); } catch { /* keep raw */ }
133
+ seen.push({ raw, real });
134
+ return realReaddir(p, ...a);
135
+ },
136
+ },
137
+ });
138
+
139
+ // Dynamic import: mock.module must wrap fs.promises before the cache loads it.
140
+ const { EntityMigrationCompletionCache } = await import(cacheModule);
141
+ const cache = new EntityMigrationCompletionCache();
142
+ let completed = null;
143
+ let err = null;
144
+ try {
145
+ completed = await cache.isCompleted({
146
+ baseDir: memoryDir,
147
+ entitiesDir,
148
+ getEntityMutationVersion: () => 0,
149
+ });
150
+ } catch (e) {
151
+ err = e && typeof e === "object" && "code" in e ? String(e.code) : String(e);
152
+ }
153
+ const entitiesResolved = path.resolve(entitiesDir);
154
+ const external = seen.filter((s) => s.real === canaryReal || s.raw === entitiesResolved);
155
+ const payload = {
156
+ completed,
157
+ err,
158
+ seen,
159
+ externalCount: external.length,
160
+ canaryReal,
161
+ };
162
+ await writeFile(resultFile, JSON.stringify(payload), "utf8");
163
+ console.log(JSON.stringify(payload));
164
+ mock.reset();
165
+ `,
166
+ );
167
+
168
+ const probe = spawnSync(
169
+ process.execPath,
170
+ [
171
+ "--experimental-test-module-mocks",
172
+ "--import",
173
+ "tsx",
174
+ "--conditions=remnic-source",
175
+ probeFile,
176
+ ],
177
+ {
178
+ encoding: "utf8",
179
+ cwd: REPO_ROOT,
180
+ env: {
181
+ ...process.env,
182
+ NODE_PATH: path.join(REPO_ROOT, "node_modules"),
183
+ },
184
+ timeout: 60_000,
185
+ },
186
+ );
187
+
188
+ if (probe.error) {
189
+ assert.fail(`probe failed to start: ${probe.error.message}`);
190
+ }
191
+
192
+ let result: ProbeResult;
193
+ try {
194
+ // SAFETY: the child probe in this file writes `resultFile` as a ProbeResult before exiting.
195
+ result = JSON.parse(await readFile(resultFile, "utf8")) as ProbeResult;
196
+ } catch {
197
+ assert.fail(
198
+ [
199
+ "probe did not write result file",
200
+ `status=${String(probe.status)} signal=${String(probe.signal)}`,
201
+ `stdout:\n${probe.stdout ?? ""}`,
202
+ `stderr:\n${probe.stderr ?? ""}`,
203
+ ].join("\n"),
204
+ );
205
+ return;
206
+ }
207
+
208
+ assert.equal(
209
+ result.externalCount,
210
+ 0,
211
+ [
212
+ "isCompleted must not readdir a symlinked entities root (external canary).",
213
+ `externalCount=${result.externalCount} canaryReal=${result.canaryReal}`,
214
+ `seen=${JSON.stringify(result.seen)}`,
215
+ result.err ? `isCompleted err=${result.err}` : `completed=${String(result.completed)}`,
216
+ ].join(" "),
217
+ );
218
+ assert.equal(result.err, null, "untrusted root must fail closed without throwing through external IO");
219
+ assert.equal(result.completed, false, "untrusted entities root must not report migration complete");
220
+ } finally {
221
+ await rm(root, { recursive: true, force: true });
222
+ }
223
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Amortized entity canonical-id migration completion verdict for entity reads.
3
+ *
4
+ * Key covers every getFingerprint input that is cheap to lstat: entity mutation
5
+ * version, entities/ dir, config/aliases.json, the journal, and reconcile
6
+ * markers. Full fingerprint (including entity entry enumeration) runs only on
7
+ * key miss.
8
+ *
9
+ * ponytail: in-place entity *content* edits still skip revalidation.
10
+ */
11
+
12
+ import { lstat, realpath } from "node:fs/promises";
13
+ import path from "node:path";
14
+ import { SecureStoreLockedError } from "../secure-store/secure-fs.js";
15
+ import { isErrnoCode } from "../utils/errno.js";
16
+ import { pathIsInside } from "../utils/path-containment.js";
17
+ import { getFingerprint } from "./entity-canonical-id-migration.js";
18
+ import {
19
+ ENTITY_CANONICAL_ID_MIGRATION_FILE,
20
+ ENTITY_CANONICAL_ID_RECONCILE_CONSUMING_MARKER,
21
+ ENTITY_CANONICAL_ID_RECONCILE_MARKER,
22
+ entityCanonicalIdMigrationCompleted,
23
+ resolveHistoricalEntityCanonicalId,
24
+ } from "./entity-canonical-id-references.js";
25
+
26
+ export type EntityMigrationCompletionCacheDeps = {
27
+ readonly baseDir: string;
28
+ readonly entitiesDir: string;
29
+ getEntityMutationVersion(): number;
30
+ };
31
+
32
+ /**
33
+ * True when `entitiesDir` is a real directory inside `baseDir`.
34
+ * lstat first so a symlinked root is never realpath'd into trust.
35
+ */
36
+ export async function isTrustedEntitiesRoot(
37
+ baseDir: string,
38
+ entitiesDir: string,
39
+ ): Promise<boolean> {
40
+ let rootStat;
41
+ try {
42
+ rootStat = await lstat(entitiesDir);
43
+ } catch (err) {
44
+ if (isErrnoCode(err, "ENOENT")) return false;
45
+ throw err;
46
+ }
47
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return false;
48
+ let memoryRoot: string;
49
+ let entitiesReal: string;
50
+ try {
51
+ memoryRoot = await realpath(baseDir);
52
+ entitiesReal = await realpath(entitiesDir);
53
+ } catch (err) {
54
+ if (isErrnoCode(err, "ENOENT")) return false;
55
+ throw err;
56
+ }
57
+ return pathIsInside(memoryRoot, entitiesReal);
58
+ }
59
+
60
+ export class EntityMigrationCompletionCache {
61
+ private cache: { version: number; inputSig: string; completed: boolean } | undefined;
62
+
63
+ /** lstat identity for one fingerprint input path; nofollow; missing → stable token. */
64
+ private async inputPathSig(filePath: string): Promise<string> {
65
+ try {
66
+ const s = await lstat(filePath);
67
+ return `${s.dev}:${s.ino}:${s.mtimeMs}:${s.ctimeMs}:${s.size}:${s.isSymbolicLink() ? "L" : s.isDirectory() ? "D" : "F"}`;
68
+ } catch {
69
+ return "missing";
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Cheap coherence key. entities/ absence is uncacheable (random suffix) so
75
+ * we never pin a false complete.
76
+ */
77
+ private async inputSig(deps: EntityMigrationCompletionCacheDeps): Promise<string> {
78
+ const stateDir = path.join(deps.baseDir, "state");
79
+ const entitiesSig = await this.inputPathSig(deps.entitiesDir);
80
+ if (entitiesSig === "missing") return `entities-missing:${Math.random()}`;
81
+ const [aliasesSig, journalSig, reconcileSig, consumingSig] = await Promise.all([
82
+ this.inputPathSig(path.join(deps.baseDir, "config", "aliases.json")),
83
+ this.inputPathSig(path.join(stateDir, ENTITY_CANONICAL_ID_MIGRATION_FILE)),
84
+ this.inputPathSig(path.join(stateDir, ENTITY_CANONICAL_ID_RECONCILE_MARKER)),
85
+ this.inputPathSig(path.join(stateDir, ENTITY_CANONICAL_ID_RECONCILE_CONSUMING_MARKER)),
86
+ ]);
87
+ return [entitiesSig, aliasesSig, journalSig, reconcileSig, consumingSig].join("|");
88
+ }
89
+
90
+ /**
91
+ * True when journal completion attests to live fingerprint inputs.
92
+ * Untrusted entities roots (symlink / outside base) never reach getFingerprint.
93
+ */
94
+ async isCompleted(deps: EntityMigrationCompletionCacheDeps): Promise<boolean> {
95
+ const version = deps.getEntityMutationVersion();
96
+ const inputSig = await this.inputSig(deps);
97
+ const cached = this.cache;
98
+ if (cached && cached.version === version && cached.inputSig === inputSig) return cached.completed;
99
+ if (!(await isTrustedEntitiesRoot(deps.baseDir, deps.entitiesDir))) {
100
+ this.cache = { version, inputSig, completed: false };
101
+ return false;
102
+ }
103
+ const completed = await entityCanonicalIdMigrationCompleted(path.join(deps.baseDir, "state"), () =>
104
+ getFingerprint(deps.baseDir, deps.entitiesDir, () => String(version)));
105
+ this.cache = { version, inputSig, completed };
106
+ return completed;
107
+ }
108
+ }
109
+
110
+ export type ReadEntityRenameDeps = {
111
+ readonly entitiesDir: string;
112
+ currentHistoricalIds(): Readonly<Record<string, string>>;
113
+ readStorageSecureFile(filePath: string): Promise<string>;
114
+ };
115
+
116
+ /** Resolve a physical entity path through historical id rename candidates. */
117
+ export async function readEntityFileResolvingRename(
118
+ deps: ReadEntityRenameDeps,
119
+ firstPath: string,
120
+ entityId: string,
121
+ ): Promise<{ id: string; content: string } | null> {
122
+ try {
123
+ return { id: entityId, content: await deps.readStorageSecureFile(firstPath) };
124
+ } catch (err) {
125
+ if (err instanceof SecureStoreLockedError) throw err;
126
+ // ENOENT: renamed away (retry below). ELOOP: planted symlink — skip.
127
+ if (isErrnoCode(err, "ELOOP")) return null;
128
+ if (!isErrnoCode(err, "ENOENT")) throw err;
129
+ }
130
+ const historicalIds = deps.currentHistoricalIds();
131
+ const canonical = resolveHistoricalEntityCanonicalId(entityId, historicalIds);
132
+ const candidates = new Set([canonical]);
133
+ for (const legacy of Object.keys(historicalIds)) {
134
+ if (resolveHistoricalEntityCanonicalId(legacy, historicalIds) === canonical) candidates.add(legacy);
135
+ }
136
+ candidates.delete(entityId);
137
+ for (const candidate of candidates) {
138
+ try {
139
+ return {
140
+ id: candidate,
141
+ content: await deps.readStorageSecureFile(
142
+ path.join(deps.entitiesDir, `${candidate}.md`),
143
+ ),
144
+ };
145
+ } catch (err) {
146
+ if (err instanceof SecureStoreLockedError) throw err;
147
+ // ENOENT/ELOOP: gone or a planted symlink — try the next candidate.
148
+ if (isErrnoCode(err, "ENOENT") || isErrnoCode(err, "ELOOP")) continue;
149
+ throw err;
150
+ }
151
+ }
152
+ return null;
153
+ }