@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
@@ -10,11 +10,17 @@
10
10
  * - fragmented-entity merge (consolidation)
11
11
  */
12
12
 
13
- import { lstat, readdir, realpath, unlink } from "node:fs/promises";
13
+ import { lstat, readdir, unlink } from "node:fs/promises";
14
14
  import path from "node:path";
15
15
  import { checkCorpusReadAbort, type CorpusReadOptions } from "../corpus-read-cancellation.js";
16
16
  import { normalizeEntityStructuredSection, sortStructuredSectionsBySchema } from "../entity-schema.js";
17
17
  import { withEntityCanonicalMutationLock } from "./entity-canonical-id-lock.js";
18
+ import { assertSafeEntityId } from "./entity-id-safety.js";
19
+ import {
20
+ EntityMigrationCompletionCache,
21
+ isTrustedEntitiesRoot,
22
+ readEntityFileResolvingRename,
23
+ } from "./entity-migration-completion-cache.js";
18
24
  import { isMalformedEntityRecord } from "../utils/malformed-entity-record.js";
19
25
  import { log } from "../logger.js";
20
26
  import {
@@ -33,7 +39,6 @@ import type {
33
39
  ScoredEntity,
34
40
  } from "../types.js";
35
41
  import { isErrnoCode } from "../utils/errno.js";
36
- import { pathIsInside } from "../utils/path-containment.js";
37
42
  import {
38
43
  reconcileIfJournalMovedSync,
39
44
  requestEntityCanonicalIdReconcileSync,
@@ -89,6 +94,7 @@ export interface EntityStoreDeps {
89
94
  export class EntityStore {
90
95
  /** Filename → file identity of the last indexable check. Unchanged files are not reread. */
91
96
  private readonly entityIndexableCache = new Map<string, { identity: string; ok: boolean }>();
97
+ private readonly migrationCompletion = new EntityMigrationCompletionCache();
92
98
 
93
99
  constructor(
94
100
  private readonly deps: EntityStoreDeps,
@@ -111,6 +117,7 @@ export class EntityStore {
111
117
  method: string,
112
118
  historicalIds: Readonly<Record<string, string>>,
113
119
  ): Promise<{ filePath: string; entity: EntityFile } | null> {
120
+ assertSafeEntityId(name);
114
121
  const canonical = resolveHistoricalEntityCanonicalId(name, historicalIds);
115
122
  for (const candidate of canonical === name ? [name] : [canonical, name]) {
116
123
  const filePath = path.join(this.deps.entitiesDir, `${candidate}.md`);
@@ -143,6 +150,7 @@ export class EntityStore {
143
150
  method: string,
144
151
  mutate: (entity: EntityFile, historicalIds: Readonly<Record<string, string>>) => boolean,
145
152
  ): Promise<boolean> {
153
+ await this.deps.ensureDirectories();
146
154
  const stateDir = path.join(this.deps.baseDir, "state");
147
155
  return withEntityCanonicalMutationLock(stateDir, async () => {
148
156
  for (let attempt = 0; attempt < 3; attempt += 1) {
@@ -375,83 +383,46 @@ export class EntityStore {
375
383
  return normalized;
376
384
  }
377
385
 
378
- private async readEntityFileResolvingRename(firstPath: string, entityId: string) {
379
- try {
380
- return { id: entityId, content: await this.deps.readStorageSecureFile(firstPath) };
381
- } catch (err) {
382
- if (err instanceof SecureStoreLockedError) throw err;
383
- // ENOENT: renamed away (retry below). ELOOP: planted symlink — skip.
384
- if (isErrnoCode(err, "ELOOP")) return null;
385
- if (!isErrnoCode(err, "ENOENT")) throw err;
386
- }
387
- const historicalIds = this.deps.currentHistoricalIds();
388
- const canonical = resolveHistoricalEntityCanonicalId(entityId, historicalIds);
389
- const candidates = new Set([canonical]);
390
- for (const legacy of Object.keys(historicalIds)) {
391
- if (resolveHistoricalEntityCanonicalId(legacy, historicalIds) === canonical) candidates.add(legacy);
392
- }
393
- candidates.delete(entityId);
394
- for (const candidate of candidates) {
386
+ async readEntity(name: string): Promise<string> {
387
+ const filePath = this.deps.resolveEntityFilePath(name);
388
+ if (filePath === null) return "";
389
+ // Physical hit wins. Miss skips rename/body-scan only when amortized
390
+ // completion attests live fingerprint inputs (not bare journal.complete).
391
+ const readDirect = async (): Promise<string | undefined> => {
395
392
  try {
396
- return { id: candidate, content: await this.deps.readStorageSecureFile(
397
- path.join(this.deps.entitiesDir, `${candidate}.md`),
398
- ) };
393
+ return await this.deps.readStorageSecureFile(filePath);
399
394
  } catch (err) {
400
395
  if (err instanceof SecureStoreLockedError) throw err;
401
- // ENOENT/ELOOP: gone or a planted symlink — try the next candidate.
402
- if (isErrnoCode(err, "ENOENT") || isErrnoCode(err, "ELOOP")) continue;
396
+ if (isErrnoCode(err, "ENOENT") || isErrnoCode(err, "ELOOP")) return undefined;
403
397
  throw err;
404
398
  }
405
- }
406
- return null;
407
- }
408
-
409
- async readEntity(name: string): Promise<string> {
410
- const filePath = this.deps.resolveEntityFilePath(name);
411
- if (filePath === null) {
412
- return "";
413
- }
414
- // Batch reads retry renames unconditionally (a listed file that ENOENTs
415
- // is definitionally a race), but a DIRECT lookup of a legacy id must stop
416
- // resolving once the journal says migration completed and its file is gone.
417
- if (this.deps.entityCanonicalMigrationComplete()) {
399
+ };
400
+ const direct = await readDirect();
401
+ if (direct !== undefined) return direct;
402
+ if (await this.migrationCompletion.isCompleted(this.deps)) return "";
403
+ const renamed = await readEntityFileResolvingRename(this.deps, filePath, name);
404
+ if (renamed !== null) return renamed.content;
405
+ // Deferred / fingerprint-stale: match legacy filename by body id.
406
+ let legacy: string | undefined;
407
+ for (const file of await this.readEntityDirectoryNames()) {
408
+ if (!file.endsWith(".md") || !(await this.isContainedRegularEntityFile(file))) continue;
409
+ let content: string;
418
410
  try {
419
- return await this.deps.readStorageSecureFile(filePath);
411
+ content = await this.deps.readStorageSecureFile(path.join(this.deps.entitiesDir, file));
420
412
  } catch (err) {
421
413
  if (err instanceof SecureStoreLockedError) throw err;
422
- // ENOENT: gone. ELOOP: a planted symlink the open refused to follow.
423
- if (isErrnoCode(err, "ENOENT") || isErrnoCode(err, "ELOOP")) return "";
414
+ if (isErrnoCode(err, "ENOENT") || isErrnoCode(err, "ELOOP")) continue;
424
415
  throw err;
425
416
  }
417
+ if (isMalformedEntityRecord(content)) continue;
418
+ const entity = parseEntityFile(content, this.deps.entitySchemas);
419
+ if (!entity.name.trim() || !entity.type.trim()) continue;
420
+ if (this.deps.normalizeEntityName(entity.name, entity.type) !== name) continue;
421
+ if (legacy !== undefined) throw new Error(`Ambiguous legacy entity id: ${name}`);
422
+ legacy = content;
426
423
  }
427
- return (await this.readEntityFileResolvingRename(filePath, name))?.content ?? "";
428
- }
429
-
430
- /**
431
- * True when `entities/` is a real directory inside this memory root.
432
- * `realpath` on a symlinked root would treat the link target as trusted,
433
- * so the check uses `lstat` first and refuses the scan when the root
434
- * itself is a symlink.
435
- */
436
- private async isTrustedEntitiesRoot(): Promise<boolean> {
437
- let rootStat;
438
- try {
439
- rootStat = await lstat(this.deps.entitiesDir);
440
- } catch (err) {
441
- if (isErrnoCode(err, "ENOENT")) return false;
442
- throw err;
443
- }
444
- if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return false;
445
- let memoryRoot: string;
446
- let entitiesReal: string;
447
- try {
448
- memoryRoot = await realpath(this.deps.baseDir);
449
- entitiesReal = await realpath(this.deps.entitiesDir);
450
- } catch (err) {
451
- if (isErrnoCode(err, "ENOENT")) return false;
452
- throw err;
453
- }
454
- return pathIsInside(memoryRoot, entitiesReal);
424
+ // A peer may have renamed the legacy entry after the directory listing.
425
+ return (await readDirect()) ?? legacy ?? "";
455
426
  }
456
427
 
457
428
  /**
@@ -460,7 +431,7 @@ export class EntityStore {
460
431
  * A symlinked entities root yields an empty list and is not read.
461
432
  */
462
433
  private async readEntityDirectoryNames(): Promise<string[]> {
463
- if (!(await this.isTrustedEntitiesRoot())) return [];
434
+ if (!(await isTrustedEntitiesRoot(this.deps.baseDir, this.deps.entitiesDir))) return [];
464
435
  const entries = await readdir(this.deps.entitiesDir);
465
436
  const { kept, ignored } = withoutMacOsReplicationJunk(entries, (name) => name);
466
437
  warnIgnoredMacOsReplicationJunk(this.deps.entitiesDir, ignored, (message) => {
@@ -595,6 +566,7 @@ export class EntityStore {
595
566
 
596
567
  /** Re-serialize every entity file into the compiled-truth timeline shape. */
597
568
  async migrateEntityFilesToCompiledTruthTimeline(): Promise<{ total: number; migrated: number }> {
569
+ await this.deps.ensureDirectories();
598
570
  const entityNames = await this.listEntityNames();
599
571
  const stateDir = path.join(this.deps.baseDir, "state");
600
572
  let migrated = 0;
@@ -605,14 +577,20 @@ export class EntityStore {
605
577
  // path bumps only the status version, which never re-triggers the
606
578
  // completed migration's reference scan.
607
579
  const wrote = await withEntityCanonicalMutationLock(stateDir, async () => {
608
- const raw = await this.readEntity(entityName);
580
+ const filePath = this.deps.resolveEntityFilePath(entityName);
581
+ if (filePath === null) return false;
582
+ let raw: string;
583
+ try {
584
+ raw = await this.deps.readStorageSecureFile(filePath);
585
+ } catch (err) {
586
+ if (err instanceof SecureStoreLockedError || !isErrnoCode(err, "ENOENT")) throw err;
587
+ return false;
588
+ }
609
589
  if (!raw || isMalformedEntityRecord(raw)) return false;
610
590
  const parsed = parseEntityFile(raw, this.deps.entitySchemas);
611
591
  if (!parsed.name.trim() || !parsed.type.trim()) return false;
612
592
  const serialized = serializeEntityFile(parsed, this.deps.entitySchemas, this.deps.okfConformanceEnabled());
613
593
  if (raw.trimEnd() === serialized.trimEnd()) return false;
614
- const filePath = this.deps.resolveEntityFilePath(entityName);
615
- if (filePath === null) return false;
616
594
  await this.deps.writeStorageSecureFile(filePath, serialized);
617
595
  this.deps.bumpEntityMutationVersion();
618
596
  return true;
@@ -641,6 +619,7 @@ export class EntityStore {
641
619
  incrementVersion?: boolean;
642
620
  } = {},
643
621
  ): Promise<void> {
622
+ await this.deps.ensureDirectories();
644
623
  return withEntityCanonicalMutationLock(
645
624
  path.join(this.deps.baseDir, "state"),
646
625
  () => this.updateEntitySynthesisUnlocked(name, synthesis, options),
@@ -659,17 +638,9 @@ export class EntityStore {
659
638
  incrementVersion?: boolean;
660
639
  } = {},
661
640
  ): Promise<void> {
662
- const filePath = path.join(this.deps.entitiesDir, `${name}.md`);
663
- let entity: EntityFile;
664
- try {
665
- const content = await this.deps.readStorageSecureFile(filePath);
666
- entity = parseEntityFile(content, this.deps.entitySchemas);
667
- } catch (err) {
668
- if (err instanceof SecureStoreLockedError) throw err;
669
- if (!isErrnoCode(err, "ENOENT")) throw err;
670
- log.debug(`updateEntitySynthesis: entity file ${name}.md not found`);
671
- return;
672
- }
641
+ const located = await this.readEntityForMutation(name, "updateEntitySynthesis", this.deps.currentHistoricalIds());
642
+ if (!located) return;
643
+ const { filePath, entity } = located;
673
644
 
674
645
  const updatedAt = options.updatedAt?.trim() || entity.synthesisUpdatedAt?.trim() || undefined;
675
646
  const entityUpdatedAt = options.entityUpdatedAt?.trim() || updatedAt || entity.updated || new Date().toISOString();
@@ -721,7 +692,7 @@ export class EntityStore {
721
692
  // #3207 semantics): resolve empty rather than following it. Within a
722
693
  // trusted root, readdir directly so planted entries reach the secure
723
694
  // read instead of being swallowed by the listing guards.
724
- if (!(await this.isTrustedEntitiesRoot())) return [];
695
+ if (!(await isTrustedEntitiesRoot(this.deps.baseDir, this.deps.entitiesDir))) return [];
725
696
  const listed = await readdir(this.deps.entitiesDir);
726
697
  const { kept: entries, ignored } = withoutMacOsReplicationJunk(listed, (name) => name);
727
698
  warnIgnoredMacOsReplicationJunk(this.deps.entitiesDir, ignored, (message) => {
@@ -753,11 +724,11 @@ export class EntityStore {
753
724
  // rename: resolve through historical ids like a direct read.
754
725
  // Still-present symlinks/dirs stay skipped.
755
726
  if (await this.isMissingEntry(entry)) {
756
- return await this.readEntityFileResolvingRename(entryPath, entityId);
727
+ return await readEntityFileResolvingRename(this.deps, entryPath, entityId);
757
728
  }
758
729
  return null;
759
730
  }
760
- return await this.readEntityFileResolvingRename(entryPath, entityId);
731
+ return await readEntityFileResolvingRename(this.deps, entryPath, entityId);
761
732
  } catch (err) {
762
733
  // Lock and decrypt failures abort the corpus read (the store
763
734
  // is unusable or tampered); anything else isolates to this entry.
@@ -873,6 +844,7 @@ export class EntityStore {
873
844
  * Returns count of files merged.
874
845
  */
875
846
  async mergeFragmentedEntities(): Promise<number> {
847
+ await this.deps.ensureDirectories();
876
848
  return withEntityCanonicalMutationLock(
877
849
  path.join(this.deps.baseDir, "state"),
878
850
  () => this.mergeFragmentedEntitiesUnlocked(),
@@ -41,7 +41,7 @@ export interface MemoryReadStoreDeps {
41
41
  readonly artifactsDir: string;
42
42
  readonly baseDir: string;
43
43
  readonly bufferSurpriseLedgerPath: string;
44
- collectActiveMemoryPaths(): Promise<string[]>;
44
+ collectActiveMemoryPaths(options?: CorpusReadOptions): Promise<string[]>;
45
45
  readonly correctionsDir: string;
46
46
  ensureDirectories(): Promise<void>;
47
47
  readonly entitySchemas: PluginConfig["entitySchemas"] | undefined;
@@ -79,6 +79,7 @@ export interface MemoryReadStoreDeps {
79
79
  remainingSlots: number,
80
80
  remainingInspectionBudget: number,
81
81
  readBatchSize: number,
82
+ options?: CorpusReadOptions,
82
83
  ): Promise<{ memories: MemoryFile[]; filePaths: string[] }>;
83
84
  readonly reasoningTracesDir: string;
84
85
  resolveTierRootDir(tier: "hot" | "cold"): string;
@@ -408,8 +409,10 @@ export class MemoryReadStore {
408
409
  maxMemories?: number;
409
410
  batchSize?: number;
410
411
  updatedAfter?: Date;
412
+ abortSignal?: AbortSignal;
411
413
  } = {}): Promise<{ memories: MemoryFile[]; filePaths: string[] }> {
412
- const allPaths = await this.deps.collectActiveMemoryPaths();
414
+ const readOptions: CorpusReadOptions = { abortSignal: options.abortSignal };
415
+ const allPaths = await this.deps.collectActiveMemoryPaths(readOptions);
413
416
  const sortedPaths = this.deps.orderWindowPaths(allPaths);
414
417
  const maxMemories =
415
418
  typeof options.maxMemories === "number" && Number.isFinite(options.maxMemories)
@@ -422,6 +425,8 @@ export class MemoryReadStore {
422
425
  const selectedPaths: string[] = [];
423
426
 
424
427
  for (let i = 0; i < sortedPaths.length; i += normalizedBatchSize) {
428
+ // Per batch: an abandoned caller must not keep the scan running (#3139).
429
+ checkCorpusReadAbort(readOptions);
425
430
  if (
426
431
  maxMemories !== undefined
427
432
  && (memories.length >= maxMemories || (maxCandidatePaths !== undefined && selectedPaths.length >= maxCandidatePaths))
@@ -436,7 +441,7 @@ export class MemoryReadStore {
436
441
  const remainingInspectionBudget = maxCandidatePaths === undefined ? undefined : Math.max(0, maxCandidatePaths - selectedPaths.length);
437
442
  const { memories: batchMemories, filePaths: parsedCandidatePaths } = remainingSlots === undefined
438
443
  ? {
439
- memories: await this.deps.readParsedMemoriesFromPaths(candidateBatchPaths, normalizedBatchSize),
444
+ memories: await this.deps.readParsedMemoriesFromPaths(candidateBatchPaths, normalizedBatchSize, readOptions),
440
445
  filePaths: candidateBatchPaths,
441
446
  }
442
447
  : await this.deps.readWindowBoundedBatch(
@@ -444,6 +449,7 @@ export class MemoryReadStore {
444
449
  remainingSlots,
445
450
  remainingInspectionBudget ?? remainingSlots,
446
451
  normalizedBatchSize,
452
+ readOptions,
447
453
  );
448
454
  selectedPaths.push(...parsedCandidatePaths);
449
455
  for (const memory of batchMemories) {
@@ -1,5 +1,6 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { throwIfAborted } from "../abort-error.js";
3
4
  import { getCachedMemories } from "../memory-cache.js";
4
5
  import { readRawMemoryDocument } from "./memory-frontmatter-metadata.js";
5
6
  import type { MemoryFile } from "../types.js";
@@ -11,6 +12,12 @@ export interface WindowedMemoryReadOptions {
11
12
  updatedAfter?: Date;
12
13
  /** Require candidate paths so malformed files can be reported by governance. */
13
14
  includeMalformedPaths?: boolean;
15
+ /**
16
+ * Caller cancellation. The windowed scan stops at its next batch boundary
17
+ * when this aborts, so a cancelled read leaves no work running behind it
18
+ * (issue #3139).
19
+ */
20
+ abortSignal?: AbortSignal;
14
21
  }
15
22
 
16
23
  export interface WindowedMemoryReadResult {
@@ -62,6 +69,7 @@ export async function readWindowedMemories(
62
69
  })
63
70
  )
64
71
  ).filter((memory): memory is MemoryFile => memory !== null);
72
+ throwIfAborted(options.abortSignal, "windowed memory read aborted");
65
73
  const memories = orderWindowMemories(selected, hotCache.baseDir);
66
74
  return { memories: rememberMemorySnapshots(memories), filePaths: memories.map((memory) => memory.path) };
67
75
  }
@@ -0,0 +1,183 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, mkdtemp, readFile, readdir, rm, symlink, unlink, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import test from "node:test";
7
+
8
+ import { StorageManager } from "./storage.js";
9
+ import { sleep } from "./utils/sleep.js";
10
+
11
+ // Issue #3139 (Q2 briefing 60s ceiling): the CLI briefing path awaits
12
+ // orchestrator.initialize(), which awaited storage.ensureDirectories(), which
13
+ // awaited the entity canonical-id migration lock/replay (up to a 300s bounded
14
+ // wait on a busy lock). Startup READ readiness must not sit behind that
15
+ // migration; writes must. These tests pin the boundary with the real on-disk
16
+ // migration lock and a real temp corpus — no orchestrator stubs. Every wait is
17
+ // bounded so a regression fails fast instead of stalling the run.
18
+
19
+ const MIGRATION_LOCK_FILE = path.join("state", "entity-canonical-id-migration.lock");
20
+ const MIGRATION_JOURNAL_FILE = path.join("state", "entity-canonical-id-migration-v1.json");
21
+ const READINESS_BUDGET_MS = 500;
22
+ const BARRIER_PROBE_MS = 250;
23
+
24
+ async function withMemoryDir(run: (dir: string) => Promise<void>): Promise<void> {
25
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-startup-migration-"));
26
+ try {
27
+ await run(dir);
28
+ } finally {
29
+ StorageManager.clearAllStaticCaches();
30
+ await rm(dir, { recursive: true, force: true });
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Hold the migration's own cross-process lock (the exact file
36
+ * migrateLegacyEntityCanonicalIds acquires via withHeldFileLock: poll 50ms,
37
+ * stale 60s — a hold for the lifetime of one test). Returns the release.
38
+ */
39
+ async function gateMigration(dir: string): Promise<() => Promise<void>> {
40
+ const lockPath = path.join(dir, MIGRATION_LOCK_FILE);
41
+ await mkdir(path.dirname(lockPath), { recursive: true });
42
+ await writeFile(lockPath, `${process.pid} startup-migration-test ${new Date().toISOString()}\n`);
43
+ return () => unlink(lockPath);
44
+ }
45
+
46
+ async function journalComplete(dir: string): Promise<boolean> {
47
+ try {
48
+ // SAFETY: the journal is written by this test fixture and carries an optional boolean `complete`.
49
+ const state = JSON.parse(await readFile(path.join(dir, MIGRATION_JOURNAL_FILE), "utf8")) as {
50
+ complete?: boolean;
51
+ };
52
+ return state.complete === true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ test("startup read readiness can defer the entity canonical-id migration", async () => {
59
+ await withMemoryDir(async (dir) => {
60
+ const storage = new StorageManager(dir);
61
+ const releaseGate = await gateMigration(dir);
62
+ const readiness = storage.ensureDirectories({ waitForEntityMigration: false });
63
+ // Regression: on the awaited path this call sits behind the held lock and
64
+ // only surfaces after the 300s bounded wait. The startup option must
65
+ // settle while the lock is still held — asserted against a bounded race
66
+ // so main fails behaviorally in ~500ms instead of stalling the job.
67
+ let failure: unknown;
68
+ try {
69
+ const outcome = await Promise.race([readiness, sleep(READINESS_BUDGET_MS).then(() => "timeout")]);
70
+ assert.notEqual(outcome, "timeout", "startup read readiness must not wait for the gated migration");
71
+ assert.equal(existsSync(path.join(dir, "entities")), true);
72
+ assert.equal(
73
+ existsSync(path.join(dir, MIGRATION_LOCK_FILE)),
74
+ true,
75
+ "the deferred startup must not take the migration lock",
76
+ );
77
+ assert.equal(
78
+ existsSync(path.join(dir, MIGRATION_JOURNAL_FILE)),
79
+ false,
80
+ "the deferred startup must not run the migration",
81
+ );
82
+ } catch (err) {
83
+ failure = err;
84
+ } finally {
85
+ await releaseGate();
86
+ }
87
+ // The write-side ensure still runs the migration and awaits it.
88
+ await readiness;
89
+ await storage.ensureDirectories();
90
+ if (failure !== undefined) throw failure;
91
+ assert.equal(await journalComplete(dir), true);
92
+ // Normal entity writes still work after the deferred migration settles.
93
+ await storage.writeEntity("Alice", "person", ["Alice works on Remnic"]);
94
+ });
95
+ });
96
+
97
+ test("default ensureDirectories still waits behind a held migration lock", async () => {
98
+ await withMemoryDir(async (dir) => {
99
+ const storage = new StorageManager(dir);
100
+ const releaseGate = await gateMigration(dir);
101
+ let settled = false;
102
+ let setupError: Error | undefined;
103
+ const gatedSetup = storage.ensureDirectories().then(
104
+ () => {
105
+ settled = true;
106
+ },
107
+ (err: Error) => {
108
+ setupError = err;
109
+ settled = true;
110
+ },
111
+ );
112
+ let failure: unknown;
113
+ try {
114
+ await sleep(BARRIER_PROBE_MS);
115
+ assert.equal(settled, false, "default ensureDirectories must not resolve while the migration lock is held");
116
+ } catch (err) {
117
+ failure = err;
118
+ } finally {
119
+ await releaseGate();
120
+ }
121
+ await gatedSetup;
122
+ assert.equal(setupError, undefined);
123
+ if (failure !== undefined) throw failure;
124
+ assert.equal(await journalComplete(dir), true);
125
+ });
126
+ });
127
+
128
+ test("deferred startup still refuses unsafe entity entries before serving reads", async () => {
129
+ const outside = await mkdtemp(path.join(os.tmpdir(), "remnic-startup-migration-outside-"));
130
+ try {
131
+ await withMemoryDir(async (dir) => {
132
+ const storage = new StorageManager(dir);
133
+ await mkdir(path.join(dir, "entities"), { recursive: true });
134
+ await symlink(path.join(outside, "leaked.md"), path.join(dir, "entities", "leaked.md"));
135
+ await assert.rejects(
136
+ () => storage.ensureDirectories({ waitForEntityMigration: false }),
137
+ /Refusing entity canonical-id migration through symlink/,
138
+ );
139
+ });
140
+ } finally {
141
+ await rm(outside, { recursive: true, force: true });
142
+ }
143
+ });
144
+
145
+ test("entity writes queue behind the migration they trigger", async () => {
146
+ await withMemoryDir(async (dir) => {
147
+ const storage = new StorageManager(dir);
148
+ const releaseGate = await gateMigration(dir);
149
+ const readiness = storage.ensureDirectories({ waitForEntityMigration: false });
150
+ const { promise: written, resolve: writeSettled } = Promise.withResolvers<void>();
151
+ let writeError: Error | undefined;
152
+ let failure: unknown;
153
+ try {
154
+ const outcome = await Promise.race([readiness, sleep(READINESS_BUDGET_MS).then(() => "timeout")]);
155
+ assert.notEqual(outcome, "timeout", "startup read readiness must not wait for the gated migration");
156
+ // A real entity write must queue behind the migration it triggers — the
157
+ // deferred startup opens READ readiness only.
158
+ storage.writeEntity("Alice", "person", ["Alice works on Remnic"]).then(
159
+ () => writeSettled(),
160
+ (err: Error) => {
161
+ writeError = err;
162
+ writeSettled();
163
+ },
164
+ );
165
+ const writeProbe = await Promise.race([written, sleep(BARRIER_PROBE_MS).then(() => "pending")]);
166
+ assert.equal(writeProbe, "pending", "entity writes must queue behind the migration they trigger");
167
+ } catch (err) {
168
+ failure = err;
169
+ } finally {
170
+ await releaseGate();
171
+ }
172
+ await readiness;
173
+ await storage.ensureDirectories();
174
+ if (failure === undefined) {
175
+ const settled = await Promise.race([written, sleep(10_000).then(() => "deadline")]);
176
+ assert.notEqual(settled, "deadline", "entity write must complete once the gate releases");
177
+ assert.equal(writeError, undefined);
178
+ const entries = await readdir(path.join(dir, "entities"));
179
+ assert.equal(entries.some((name) => name.endsWith(".md")), true, "entity file must land after the migration settles");
180
+ }
181
+ if (failure !== undefined) throw failure;
182
+ });
183
+ });
package/src/storage.ts CHANGED
@@ -1923,6 +1923,8 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
1923
1923
  onCatalogWrite?: () => void;
1924
1924
  /** Post-write embedding hook (#2019). Installed by the orchestrator; fire-and-forget. */
1925
1925
  onMemoryWrite?: (filePath: string) => void;
1926
+ /** Request search maintenance after a completed entity replay. */
1927
+ onEntityMigrationComplete?: () => void;
1926
1928
  private notifyMemoryWrite(filePath: string): void {
1927
1929
  try {
1928
1930
  this.onMemoryWrite?.(filePath);
@@ -4834,13 +4836,16 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
4834
4836
  candidateBatchPaths: string[],
4835
4837
  remainingSlots: number,
4836
4838
  remainingInspectionBudget: number,
4837
- readBatchSize: number
4839
+ readBatchSize: number,
4840
+ options?: CorpusReadOptions
4838
4841
  ): Promise<{ memories: MemoryFile[]; filePaths: string[] }> {
4839
4842
  const memories: MemoryFile[] = [];
4840
4843
  const filePaths: string[] = [];
4841
4844
  const normalizedReadBatchSize = this.normalizeMemoryReadBatchSize(readBatchSize);
4842
4845
 
4843
4846
  for (let index = 0; index < candidateBatchPaths.length; ) {
4847
+ // Per batch: an abandoned caller must not keep the scan running (#3139).
4848
+ checkCorpusReadAbort(options);
4844
4849
  if (memories.length >= remainingSlots || filePaths.length >= remainingInspectionBudget) break;
4845
4850
  const availableSlots = remainingSlots - memories.length;
4846
4851
  const availableInspectionBudget = remainingInspectionBudget - filePaths.length;
@@ -4853,7 +4858,7 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
4853
4858
  index += candidatePaths.length;
4854
4859
  if (candidatePaths.length === 0) break;
4855
4860
  filePaths.push(...candidatePaths);
4856
- const parsedMemories = await this.readParsedMemoriesFromPaths(candidatePaths, candidatePaths.length);
4861
+ const parsedMemories = await this.readParsedMemoriesFromPaths(candidatePaths, candidatePaths.length, options);
4857
4862
  if (parsedMemories.length === 0) continue;
4858
4863
  memories.push(...parsedMemories.slice(0, availableSlots));
4859
4864
  }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Lifecycle matrix subject for the amortized entity migration-completion
3
+ * cache. Exercises the helper only through StorageManager.readEntity — never
4
+ * a direct cache test double.
5
+ */
6
+ import assert from "node:assert/strict";
7
+ import { mkdir, rm, writeFile } from "node:fs/promises";
8
+ import path from "node:path";
9
+
10
+ import { StorageManager } from "../../storage.js";
11
+ import { type LifecycleSubject, type MatrixRow, runLifecycleMatrix } from "../lifecycle-matrix.js";
12
+ import { mkTempMemoryDir } from "../orchestrator-lite.js";
13
+
14
+ interface CompletionCacheState {
15
+ dir: string;
16
+ previousId: string;
17
+ reader: StorageManager;
18
+ warmMiss: string;
19
+ afterAlias?: string;
20
+ afterRestart?: string;
21
+ restarted?: StorageManager;
22
+ }
23
+
24
+ const subject: LifecycleSubject<CompletionCacheState> = {
25
+ async setup(row: MatrixRow): Promise<CompletionCacheState> {
26
+ const dir = await mkTempMemoryDir(`entity-mig-complete-${row.id}`);
27
+ try {
28
+ const seed = new StorageManager(dir);
29
+ const previousId = await seed.writeEntity("Café", "project", ["Synthetic legacy fact."]);
30
+ await seed.ensureDirectories();
31
+ const reader = new StorageManager(dir);
32
+ await reader.ensureDirectories({ waitForEntityMigration: false });
33
+ return { dir, previousId, reader, warmMiss: "" };
34
+ } catch (error) {
35
+ await rm(dir, { recursive: true, force: true });
36
+ throw error;
37
+ }
38
+ },
39
+
40
+ async exercise(state: CompletionCacheState, row: MatrixRow): Promise<void> {
41
+ // Warm the amortized completion verdict on a miss (key + fingerprint path).
42
+ state.warmMiss = await state.reader.readEntity("project-missing");
43
+
44
+ await mkdir(path.join(state.dir, "config"), { recursive: true });
45
+ await writeFile(path.join(state.dir, "config", "aliases.json"), JSON.stringify({ café: "coffee" }));
46
+ await state.reader.loadAliases();
47
+ const canonical = state.reader.normalizeEntityName("Café", "project");
48
+ assert.notEqual(canonical, state.previousId, `${row.id}: alias must move the canonical id`);
49
+ state.afterAlias = await state.reader.readEntity(canonical);
50
+
51
+ if (row.dimensions.restart) {
52
+ const restarted = new StorageManager(state.dir);
53
+ state.restarted = restarted;
54
+ await restarted.ensureDirectories({ waitForEntityMigration: false });
55
+ await restarted.loadAliases();
56
+ state.afterRestart = await restarted.readEntity(restarted.normalizeEntityName("Café", "project"));
57
+ }
58
+
59
+ if (row.dimensions.dedupeOrReplay) {
60
+ // Second miss with stable inputs must stay a cheap empty miss (no throw).
61
+ assert.equal(await state.reader.readEntity("project-missing-again"), "");
62
+ }
63
+ },
64
+
65
+ async invariants(state: CompletionCacheState, row: MatrixRow): Promise<void> {
66
+ assert.equal(state.warmMiss, "", `${row.id}: warm miss must return empty`);
67
+ assert.match(
68
+ state.afterAlias ?? "",
69
+ /Synthetic legacy fact/,
70
+ `${row.id}: alias drift must not hide the entity behind a stale completion cache`,
71
+ );
72
+ if (row.dimensions.restart) {
73
+ assert.match(
74
+ state.afterRestart ?? "",
75
+ /Synthetic legacy fact/,
76
+ `${row.id}: restarted reader must still resolve the alias-moved entity`,
77
+ );
78
+ }
79
+ },
80
+
81
+ async teardown(state: CompletionCacheState): Promise<void> {
82
+ // The deferred startup replay runs in the background; let it finish before the directory goes.
83
+ await state.reader.drainEntityCanonicalIdMigration();
84
+ await state.restarted?.drainEntityCanonicalIdMigration();
85
+ StorageManager.clearAllStaticCaches();
86
+ await rm(state.dir, { recursive: true, force: true });
87
+ },
88
+ };
89
+
90
+ runLifecycleMatrix("entity-migration-completion-cache", subject);
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/access-observe-write-category.ts"],"sourcesContent":["/**\n * Category-alias split + replay restamp for observe/write (#2829).\n * Extracted from access-observe-write-surface.ts to stay under the 1200-line cap.\n *\n * The wire schema already maps an alias to \"fact\". `rawCategory` is the\n * caller's spelling: it must not reach the write candidate or fingerprint,\n * and a replay must rebuild the note from THIS request's spelling.\n */\nimport {\n canonicalCategoryForAlias,\n categoryAliasCoercion,\n reapplyCategoryCoercion,\n type CategoryAliasCoercion,\n type MemoryCategoryAlias,\n} from \"./access-schema.js\";\n\nexport { reapplyCategoryCoercion };\nexport { applyBriefingLocationContext } from \"./location/tagging.js\";\n\nexport function splitCanonicalWriteRequest<T extends { rawCategory?: string; category?: string }>(\n request: T,\n): {\n canonical: Omit<T, \"rawCategory\">;\n categoryCoercion: CategoryAliasCoercion | undefined;\n} {\n const { rawCategory, ...canonical } = request;\n // #2962: a direct service caller bypasses the wire schema, so an accepted\n // alias can still sit on `category`. Canonicalize it at the same boundary\n // the schema's transform covers for HTTP/MCP/CLI, minting the retained\n // spelling from the request itself. A schema-minted `rawCategory` always\n // wins (it names the spelling the transform actually canonicalized);\n // near-misses pass through untouched and reject at the write candidate\n // with the attempted category echoed.\n if (rawCategory === undefined && typeof canonical.category === \"string\") {\n const mapped = canonicalCategoryForAlias(canonical.category);\n if (mapped !== undefined) {\n const from = canonical.category as MemoryCategoryAlias;\n return {\n canonical: { ...canonical, category: mapped } as Omit<T, \"rawCategory\">,\n categoryCoercion: { from, to: mapped },\n };\n }\n }\n return { canonical, categoryCoercion: categoryAliasCoercion(rawCategory) };\n}\n"],"mappings":";;;;;;AAmBO,SAAS,2BACd,SAIA;AACA,QAAM,EAAE,aAAa,GAAG,UAAU,IAAI;AAQtC,MAAI,gBAAgB,UAAa,OAAO,UAAU,aAAa,UAAU;AACvE,UAAM,SAAS,0BAA0B,UAAU,QAAQ;AAC3D,QAAI,WAAW,QAAW;AACxB,YAAM,OAAO,UAAU;AACvB,aAAO;AAAA,QACL,WAAW,EAAE,GAAG,WAAW,UAAU,OAAO;AAAA,QAC5C,kBAAkB,EAAE,MAAM,IAAI,OAAO;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,WAAW,kBAAkB,sBAAsB,WAAW,EAAE;AAC3E;","names":[]}