@remnic/core 10.0.48 → 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.
- package/dist/access-admin-ops-surface.js +73 -70
- package/dist/access-audit.js +2 -2
- package/dist/access-authorization-probe.js +74 -71
- package/dist/access-boundary.js +73 -70
- package/dist/access-cli.js +170 -167
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +73 -70
- package/dist/access-http-briefing.d.ts +2 -0
- package/dist/access-http-briefing.d.ts.map +1 -1
- package/dist/access-http-briefing.js +74 -71
- package/dist/access-http-query.js +75 -72
- package/dist/access-http.d.ts.map +1 -1
- package/dist/access-http.js +96 -93
- package/dist/access-identity-continuity-surface.js +73 -70
- package/dist/access-lcm-surface.js +73 -70
- package/dist/access-mcp.js +85 -82
- package/dist/access-namespace-preflight.js +73 -70
- package/dist/access-observe-write-category.d.ts +1 -1
- package/dist/access-observe-write-category.d.ts.map +1 -1
- package/dist/access-observe-write-category.js +15 -14
- package/dist/access-observe-write-surface.d.ts.map +1 -1
- package/dist/access-observe-write-surface.js +73 -70
- package/dist/access-offline-manifest.js +9 -8
- package/dist/access-operations-batch.js +76 -73
- package/dist/access-operations.js +83 -80
- package/dist/access-recall-concurrency.js +73 -70
- package/dist/access-recall-response.js +73 -70
- package/dist/access-recall-surface.js +73 -70
- package/dist/access-schema.js +11 -10
- package/dist/access-service.d.ts +5 -0
- package/dist/access-service.d.ts.map +1 -1
- package/dist/access-service.js +73 -70
- package/dist/access-surface-catalog.js +75 -72
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +32 -30
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-BNCBMU6H.js → auto-sync-UQQ6YJSB.js} +3 -3
- package/dist/briefing-deadline.d.ts +7 -0
- package/dist/briefing-deadline.d.ts.map +1 -0
- package/dist/briefing-deadline.js +13 -0
- package/dist/briefing-window.d.ts +12 -4
- package/dist/briefing-window.d.ts.map +1 -1
- package/dist/briefing-window.js +3 -1
- package/dist/briefing.d.ts +2 -0
- package/dist/briefing.d.ts.map +1 -1
- package/dist/briefing.js +22 -20
- package/dist/buffer.js +6 -6
- package/dist/calibration.js +2 -2
- package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/{capsule-merge-RNQUP2VL.js → capsule-merge-D4BTIJGK.js} +3 -3
- package/dist/capture-write-lock-scope.d.ts +7 -0
- package/dist/capture-write-lock-scope.d.ts.map +1 -0
- package/dist/capture-write-lock-scope.js +10 -0
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +23 -22
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +3 -3
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-FAKRTS3Y.js → chunk-2AKIVSHP.js} +2 -2
- package/dist/{chunk-EIYJK7OD.js → chunk-2M36PR7R.js} +2 -2
- package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
- package/dist/{chunk-6P6MZDBL.js → chunk-3NL4ALUP.js} +2 -2
- package/dist/{chunk-6CC2YCKG.js → chunk-43TUWQNR.js} +3 -3
- package/dist/{chunk-H7BZBSP6.js → chunk-52II7PB6.js} +4 -4
- package/dist/{chunk-2NLCDNGA.js → chunk-564YP3DW.js} +2 -2
- package/dist/{chunk-5KGYR5VN.js → chunk-5DVOBBTV.js} +2 -2
- package/dist/{chunk-RT2GN6KQ.js → chunk-5FUDSK6Z.js} +2 -2
- package/dist/{chunk-B4XSXFKI.js → chunk-5G53JSRO.js} +2 -2
- package/dist/{chunk-BGK3SFCC.js → chunk-5X2H3X4J.js} +2 -2
- package/dist/{chunk-OXSTKCYK.js → chunk-6AEKSOWV.js} +33 -5
- package/dist/chunk-6AEKSOWV.js.map +1 -0
- package/dist/{chunk-4J7BNL7B.js → chunk-6ZQX3R66.js} +2 -2
- package/dist/{chunk-JNLYHJG7.js → chunk-74AY3S7U.js} +4 -4
- package/dist/{chunk-RUAIO5UA.js → chunk-7J6A5MV2.js} +2 -2
- package/dist/{chunk-A45QXZMU.js → chunk-7SAE6T36.js} +2 -2
- package/dist/chunk-7VLBZKNB.js +26 -0
- package/dist/chunk-7VLBZKNB.js.map +1 -0
- package/dist/{chunk-QE2XQM3S.js → chunk-7YVER7ZO.js} +2 -2
- package/dist/{chunk-IJH6METJ.js → chunk-A3UFV4K3.js} +4 -4
- package/dist/{chunk-LWRIGKFM.js → chunk-A5L5KGDI.js} +19 -1
- package/dist/chunk-A5L5KGDI.js.map +1 -0
- package/dist/{chunk-LXN737FX.js → chunk-AAANTXCQ.js} +2 -2
- package/dist/{chunk-I3IPXGFQ.js → chunk-AJJPXUE6.js} +4 -4
- package/dist/{chunk-2DPJFJFH.js → chunk-BNJAVRNJ.js} +11 -11
- package/dist/{chunk-YL564QS2.js → chunk-CCL2OXFO.js} +3 -3
- package/dist/{chunk-CHSRA76M.js → chunk-CRN3EPE2.js} +4 -3
- package/dist/chunk-CRN3EPE2.js.map +1 -0
- package/dist/{chunk-ZX5N5D6M.js → chunk-DAOMWGO2.js} +4 -4
- package/dist/{chunk-YUE7J2HZ.js → chunk-DMKTOCC7.js} +2 -2
- package/dist/chunk-DMKTOCC7.js.map +1 -0
- package/dist/{chunk-FRXS6SC7.js → chunk-EKUI7I2X.js} +20 -4
- package/dist/chunk-EKUI7I2X.js.map +1 -0
- package/dist/{chunk-PKPOO6X5.js → chunk-EKYWZYZJ.js} +25 -17
- package/dist/chunk-EKYWZYZJ.js.map +1 -0
- package/dist/{chunk-23ZMQONL.js → chunk-ERMT5WQM.js} +2 -2
- package/dist/{chunk-VTCNVGM6.js → chunk-EWBXMGXR.js} +2 -2
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-XTN54NML.js → chunk-G3T6YMBO.js} +4 -4
- package/dist/{chunk-2JNBPOZ2.js → chunk-GDYLZ6FR.js} +3 -3
- package/dist/{chunk-DZPTR2XQ.js → chunk-GFAEKSFR.js} +2 -2
- package/dist/{chunk-L46MG2GE.js → chunk-GQIYL7OD.js} +400 -301
- package/dist/chunk-GQIYL7OD.js.map +1 -0
- package/dist/{chunk-M7LA4QBY.js → chunk-GXVXBYRA.js} +3 -3
- package/dist/{chunk-UXAF4RQM.js → chunk-HKOMHP6D.js} +27 -15
- package/dist/chunk-HKOMHP6D.js.map +1 -0
- package/dist/{chunk-JGMJZBFV.js → chunk-HPNXS6W6.js} +2 -2
- package/dist/{chunk-BFPF5N3H.js → chunk-HXFIILGF.js} +4 -4
- package/dist/{chunk-YBKQAVPZ.js → chunk-IMKOJVD4.js} +101 -81
- package/dist/chunk-IMKOJVD4.js.map +1 -0
- package/dist/{chunk-CMNL3QSL.js → chunk-IN6U2BS5.js} +2 -2
- package/dist/{chunk-BWRX322G.js → chunk-IUYXCTA7.js} +2 -2
- package/dist/{chunk-LU57EEJW.js → chunk-JBWD3J5U.js} +3 -3
- package/dist/chunk-JNA7LFMQ.js +41 -0
- package/dist/chunk-JNA7LFMQ.js.map +1 -0
- package/dist/{chunk-2V664H6O.js → chunk-JS44PBCM.js} +5 -5
- package/dist/{chunk-XKTHJHSQ.js → chunk-JZCBMKR5.js} +3 -3
- package/dist/{chunk-MXPVNDGO.js → chunk-K5QQXVG6.js} +12 -12
- package/dist/{chunk-BUKK5SWA.js → chunk-KQAFEZQX.js} +2 -2
- package/dist/{chunk-WUBGMV3G.js → chunk-KRAMCBIW.js} +9 -9
- package/dist/{chunk-NDMPRT3Z.js → chunk-KW5YZELC.js} +2 -2
- package/dist/{chunk-2RQR4TXV.js → chunk-LIBVANOF.js} +2 -2
- package/dist/{chunk-M7L5A2AT.js → chunk-LMMBV7QC.js} +4 -4
- package/dist/{chunk-PJBYVJKO.js → chunk-LXXIV7Y2.js} +2 -2
- package/dist/{chunk-MZCTS3ZG.js → chunk-M2DOJBH7.js} +2 -2
- package/dist/{chunk-WOPZ2P5Z.js → chunk-M4HRYLOA.js} +2 -2
- package/dist/{chunk-JCYEDMOY.js → chunk-M6TP6UYD.js} +3 -3
- package/dist/{chunk-KAUCVTAW.js → chunk-MLNXATFG.js} +2 -2
- package/dist/{chunk-2NX7JTST.js → chunk-MPWOAIIH.js} +2 -2
- package/dist/{chunk-RQLTCHUL.js → chunk-MWURUSRE.js} +13 -13
- package/dist/{chunk-3EGL7XZO.js → chunk-N74WJ7B2.js} +2 -2
- package/dist/{chunk-DRXTMDAG.js → chunk-N7FNCE7F.js} +2 -2
- package/dist/{chunk-6CWAY4R7.js → chunk-NPLM7AFU.js} +2 -2
- package/dist/{chunk-VECRSYQG.js → chunk-NRGQ5HJO.js} +1 -1
- package/dist/{chunk-PPDNG7WC.js → chunk-NSP5PIQH.js} +2 -2
- package/dist/{chunk-6HVLHIAB.js → chunk-P7OVFGMU.js} +2 -2
- package/dist/{chunk-EX27VKU6.js → chunk-PDTNQLPC.js} +2 -2
- package/dist/{chunk-ME5LVISI.js → chunk-PJCVI44H.js} +2 -2
- package/dist/{chunk-LJCHDFLP.js → chunk-Q3PNQYAC.js} +2 -2
- package/dist/{chunk-YFJHFEET.js → chunk-QS765SBL.js} +2 -2
- package/dist/{chunk-FJ264QBP.js → chunk-QTDZ2NYC.js} +11 -11
- package/dist/{chunk-Q6UZREJK.js → chunk-RKRX4C4H.js} +2 -2
- package/dist/{chunk-YFXRLRKP.js → chunk-S4OZKPBS.js} +7 -11
- package/dist/{chunk-YFXRLRKP.js.map → chunk-S4OZKPBS.js.map} +1 -1
- package/dist/{chunk-GLZWOIU3.js → chunk-SJTSWG2Z.js} +3 -3
- package/dist/{chunk-Y57GJDG5.js → chunk-SWRRZNHM.js} +14 -17
- package/dist/chunk-SWRRZNHM.js.map +1 -0
- package/dist/{chunk-CCTLRRV6.js → chunk-TEH5MDX5.js} +55 -34
- package/dist/chunk-TEH5MDX5.js.map +1 -0
- package/dist/{chunk-XM747YJF.js → chunk-TFWJXWL6.js} +2 -2
- package/dist/{chunk-QFQ4UG3I.js → chunk-TXKQBQYW.js} +4 -4
- package/dist/{chunk-BMOJFGVX.js → chunk-UALXWWGN.js} +2 -2
- package/dist/{chunk-B44NFHWI.js → chunk-UNOAVHV4.js} +2 -2
- package/dist/{chunk-TF5NZGPS.js → chunk-UQBOPCDK.js} +2 -2
- package/dist/{chunk-SUKPM6DV.js → chunk-UWMZT2TY.js} +47 -4
- package/dist/chunk-UWMZT2TY.js.map +1 -0
- package/dist/{chunk-5R2IF2KI.js → chunk-UXGXJUZ5.js} +7 -7
- package/dist/{chunk-FWV2SXSL.js → chunk-UZVQFGAY.js} +101 -98
- package/dist/chunk-UZVQFGAY.js.map +1 -0
- package/dist/{chunk-J6B6FK2X.js → chunk-VCOMACR6.js} +53 -53
- package/dist/{chunk-WCGJK4NQ.js → chunk-VKQI47DJ.js} +7 -7
- package/dist/{chunk-MPJDQC4G.js → chunk-WJCXUE2J.js} +2 -2
- package/dist/{chunk-BCDECPVO.js → chunk-WV4UYDP6.js} +2 -2
- package/dist/{chunk-VWJ56T63.js → chunk-WXJC2L66.js} +2 -2
- package/dist/chunk-X2KO4FK5.js +40 -0
- package/dist/chunk-X2KO4FK5.js.map +1 -0
- package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
- package/dist/{chunk-FPANDKMQ.js → chunk-XC3H7KON.js} +2 -2
- package/dist/{chunk-OOOX5C4X.js → chunk-XL2EE5W3.js} +4 -4
- package/dist/{chunk-3DKKZVXS.js → chunk-XTXWR6KQ.js} +26 -11
- package/dist/chunk-XTXWR6KQ.js.map +1 -0
- package/dist/{chunk-3SL52JKD.js → chunk-XVZA6VXC.js} +4 -4
- package/dist/{chunk-CZVNOTLR.js → chunk-XXR4A33N.js} +2 -2
- package/dist/{chunk-2AV2U2SZ.js → chunk-YDIOMDBW.js} +2 -2
- package/dist/{chunk-XTVH2NAW.js → chunk-YECEEJUU.js} +3 -3
- package/dist/{chunk-QR6W3FSW.js → chunk-Z5277JWF.js} +3 -3
- package/dist/{chunk-CTHYR4QK.js → chunk-ZFNXJEUG.js} +2 -2
- package/dist/{chunk-6Q56WDHG.js → chunk-ZLCBLDJO.js} +2 -2
- package/dist/{chunk-VY6BD5KB.js → chunk-ZMO2S5RT.js} +12 -10
- package/dist/chunk-ZMO2S5RT.js.map +1 -0
- package/dist/{chunk-KRACAE73.js → chunk-ZNRTHYYW.js} +6 -6
- package/dist/{chunk-7AIPWNM7.js → chunk-ZSMA67FV.js} +2 -2
- package/dist/cli.js +123 -120
- package/dist/coding/export-okf-codegraph.js +19 -18
- package/dist/coding/export-okf-codegraph.js.map +1 -1
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +21 -20
- package/dist/config.js +32 -30
- package/dist/connectors/codex-materialize-runner.js +20 -19
- package/dist/connectors/index.js +20 -19
- package/dist/consolidation-provenance-check.js +4 -4
- package/dist/consolidation-undo.js +2 -2
- package/dist/convergence-refresh.js +22 -21
- package/dist/corpus-watermark.js +22 -21
- package/dist/correction/passive-correction-notifications.d.ts +1 -1
- package/dist/correction/passive-correction-notifications.d.ts.map +1 -1
- package/dist/dashboard-runtime.js +2 -2
- package/dist/deep-recall-http-glue.js +12 -11
- package/dist/deep-recall.js +2 -2
- package/dist/embedding-fallback.js +3 -2
- package/dist/embedding-generation-marker.d.ts +16 -0
- package/dist/embedding-generation-marker.d.ts.map +1 -0
- package/dist/embedding-generation-marker.js +19 -0
- package/dist/embedding-generation-marker.js.map +1 -0
- package/dist/embedding-index-storage.d.ts +2 -4
- package/dist/embedding-index-storage.d.ts.map +1 -1
- package/dist/embedding-index-storage.js +2 -1
- package/dist/{entity-canonical-id-migration-2YOLMOFC.js → entity-canonical-id-migration-ZBNRGUNV.js} +3 -3
- package/dist/entity-canonical-id-migration-ZBNRGUNV.js.map +1 -0
- package/dist/entity-retrieval.js +22 -21
- package/dist/event-order-recall.js +2 -2
- package/dist/explicit-capture.js +3 -3
- package/dist/external-wiki-access.js +74 -71
- package/dist/extraction-span-materialize.js +2 -2
- package/dist/extraction.js +30 -29
- package/dist/{graph-edge-decay-3HVLMT6U.js → graph-edge-decay-IU75NV42.js} +6 -6
- package/dist/graph-edge-decay-IU75NV42.js.map +1 -0
- package/dist/graph-jsonl.js +4 -4
- package/dist/graph-snapshot.js +4 -4
- package/dist/graph.js +5 -5
- package/dist/harmonic-retrieval.js +20 -19
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +230 -218
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +5 -5
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +25 -24
- package/dist/location/tagging.d.ts +7 -5
- package/dist/location/tagging.d.ts.map +1 -1
- package/dist/maintenance/memory-governance.js +20 -19
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +20 -19
- package/dist/maintenance/rebuild-memory-projection-runner.js +22 -21
- package/dist/maintenance/rebuild-memory-projection.js +21 -20
- package/dist/memory-browse-operations.js +74 -71
- package/dist/memory-browse.js +5 -5
- package/dist/namespaces/migrate.js +21 -20
- package/dist/namespaces/search.js +6 -6
- package/dist/namespaces/storage.js +20 -19
- package/dist/offline-sync-changeset-generations.js +5 -4
- package/dist/offline-sync-embedding-generation.d.ts +12 -2
- package/dist/offline-sync-embedding-generation.d.ts.map +1 -1
- package/dist/offline-sync-embedding-generation.js +6 -3
- package/dist/offline-sync-file-io.d.ts +15 -0
- package/dist/offline-sync-file-io.d.ts.map +1 -1
- package/dist/offline-sync-file-io.js +9 -1
- package/dist/offline-sync-snapshot-stream.js +9 -8
- package/dist/offline-sync-upload-staging.js +2 -2
- package/dist/offline-sync.d.ts.map +1 -1
- package/dist/offline-sync.js +10 -9
- package/dist/operator-doctor-corpus.js +23 -22
- package/dist/operator-toolkit.js +45 -43
- package/dist/orchestration/maintenance.js +33 -32
- package/dist/orchestrator.d.ts +1 -0
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +170 -167
- package/dist/page-versioning.js +2 -2
- package/dist/qmd.js +2 -2
- package/dist/recall-navigation-operations.js +74 -71
- package/dist/recall-navigation.js +73 -70
- package/dist/recall-state-view-admission.js +5 -5
- package/dist/recall-why-service.js +5 -5
- package/dist/resume-bundles.js +33 -31
- package/dist/search/factory.js +5 -5
- package/dist/search/index.js +9 -9
- package/dist/semantic-consolidation.js +25 -24
- package/dist/semantic-rule-promotion.js +20 -19
- package/dist/semantic-rule-verifier.js +20 -19
- package/dist/source-agent-qualifier.js +24 -23
- package/dist/standup/build.js +20 -19
- package/dist/storage/entity-canonical-id-migration-adapter.d.ts +1 -0
- package/dist/storage/entity-canonical-id-migration-adapter.d.ts.map +1 -1
- package/dist/storage/entity-canonical-id-migration.d.ts +7 -0
- package/dist/storage/entity-canonical-id-migration.d.ts.map +1 -1
- package/dist/storage/entity-canonical-id-references.d.ts +2 -0
- package/dist/storage/entity-canonical-id-references.d.ts.map +1 -1
- package/dist/storage/entity-migration-completion-cache.d.ts +46 -0
- package/dist/storage/entity-migration-completion-cache.d.ts.map +1 -0
- package/dist/storage/entity-store.d.ts +1 -8
- package/dist/storage/entity-store.d.ts.map +1 -1
- package/dist/storage/memory-read-store.d.ts +3 -2
- package/dist/storage/memory-read-store.d.ts.map +1 -1
- package/dist/storage/windowed-memory-read.d.ts +6 -0
- package/dist/storage/windowed-memory-read.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +19 -18
- package/dist/summarizer.js +3 -3
- package/dist/support-passport/index.js +76 -73
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +4 -4
- package/dist/transfer/export-okf.js +20 -19
- package/dist/verified-recall.js +20 -19
- package/dist/who-knows-cli.js +21 -20
- package/dist/who-knows.js +20 -19
- package/package.json +2 -2
- package/src/access-http-briefing.ts +4 -0
- package/src/access-http.test.ts +40 -11
- package/src/access-http.ts +6 -0
- package/src/access-observe-write-category.ts +1 -1
- package/src/access-observe-write-surface.ts +24 -19
- package/src/access-operations-batch.ts +18 -2
- package/src/access-service-peer-capture-backoff.test.ts +236 -0
- package/src/access-service.ts +11 -2
- package/src/briefing-abort.test.ts +173 -0
- package/src/briefing-deadline.test.ts +84 -0
- package/src/briefing-deadline.ts +33 -0
- package/src/briefing-window-abort.test.ts +47 -0
- package/src/briefing-window.ts +33 -4
- package/src/briefing.ts +23 -16
- package/src/capture-write-lock-scope.test.ts +32 -0
- package/src/capture-write-lock-scope.ts +30 -0
- package/src/correction/passive-correction-notifications-abort.test.ts +42 -0
- package/src/correction/passive-correction-notifications.ts +4 -0
- package/src/embedding-generation-marker.ts +51 -0
- package/src/embedding-index-storage.ts +13 -29
- package/src/entity-lazy-compatibility.test.ts +229 -0
- package/src/entity-migration-completion-notification.test.ts +148 -0
- package/src/index.ts +2 -2
- package/src/location/tagging.ts +14 -12
- package/src/offline-sync-embedding-delete-digest.test.ts +58 -0
- package/src/offline-sync-embedding-generation-marker.test.ts +245 -0
- package/src/offline-sync-embedding-generation.ts +47 -2
- package/src/offline-sync-embedding-lock-loss.test.ts +42 -0
- package/src/offline-sync-file-io.ts +61 -2
- package/src/offline-sync.test.ts +313 -20
- package/src/offline-sync.ts +47 -22
- package/src/orchestrator-init.test.ts +49 -1
- package/src/orchestrator.ts +9 -8
- package/src/storage/entity-canonical-id-migration-adapter.ts +16 -1
- package/src/storage/entity-canonical-id-migration.ts +17 -8
- package/src/storage/entity-canonical-id-references.ts +24 -0
- package/src/storage/entity-migration-completion-cache.nofollow.test.ts +223 -0
- package/src/storage/entity-migration-completion-cache.ts +153 -0
- package/src/storage/entity-store.ts +58 -86
- package/src/storage/memory-read-store.ts +9 -3
- package/src/storage/windowed-memory-read.ts +8 -0
- package/src/storage-startup-nonblocking-migration.test.ts +183 -0
- package/src/storage.ts +9 -3
- package/src/testing/subjects/entity-migration-completion-cache.test.ts +90 -0
- package/dist/chunk-3DKKZVXS.js.map +0 -1
- package/dist/chunk-CCTLRRV6.js.map +0 -1
- package/dist/chunk-CHSRA76M.js.map +0 -1
- package/dist/chunk-FRXS6SC7.js.map +0 -1
- package/dist/chunk-FWV2SXSL.js.map +0 -1
- package/dist/chunk-L46MG2GE.js.map +0 -1
- package/dist/chunk-LWRIGKFM.js.map +0 -1
- package/dist/chunk-OXSTKCYK.js.map +0 -1
- package/dist/chunk-PKPOO6X5.js.map +0 -1
- package/dist/chunk-SUKPM6DV.js.map +0 -1
- package/dist/chunk-UXAF4RQM.js.map +0 -1
- package/dist/chunk-VY6BD5KB.js.map +0 -1
- package/dist/chunk-Y57GJDG5.js.map +0 -1
- package/dist/chunk-YBKQAVPZ.js.map +0 -1
- package/dist/chunk-YUE7J2HZ.js.map +0 -1
- /package/dist/{auto-sync-BNCBMU6H.js.map → auto-sync-UQQ6YJSB.js.map} +0 -0
- /package/dist/{capsule-crypto-GWVG7LGC.js.map → briefing-deadline.js.map} +0 -0
- /package/dist/{entity-canonical-id-migration-2YOLMOFC.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
- /package/dist/{capsule-merge-RNQUP2VL.js.map → capsule-merge-D4BTIJGK.js.map} +0 -0
- /package/dist/{graph-edge-decay-3HVLMT6U.js.map → capture-write-lock-scope.js.map} +0 -0
- /package/dist/{chunk-FAKRTS3Y.js.map → chunk-2AKIVSHP.js.map} +0 -0
- /package/dist/{chunk-EIYJK7OD.js.map → chunk-2M36PR7R.js.map} +0 -0
- /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /package/dist/{chunk-6P6MZDBL.js.map → chunk-3NL4ALUP.js.map} +0 -0
- /package/dist/{chunk-6CC2YCKG.js.map → chunk-43TUWQNR.js.map} +0 -0
- /package/dist/{chunk-H7BZBSP6.js.map → chunk-52II7PB6.js.map} +0 -0
- /package/dist/{chunk-2NLCDNGA.js.map → chunk-564YP3DW.js.map} +0 -0
- /package/dist/{chunk-5KGYR5VN.js.map → chunk-5DVOBBTV.js.map} +0 -0
- /package/dist/{chunk-RT2GN6KQ.js.map → chunk-5FUDSK6Z.js.map} +0 -0
- /package/dist/{chunk-B4XSXFKI.js.map → chunk-5G53JSRO.js.map} +0 -0
- /package/dist/{chunk-BGK3SFCC.js.map → chunk-5X2H3X4J.js.map} +0 -0
- /package/dist/{chunk-4J7BNL7B.js.map → chunk-6ZQX3R66.js.map} +0 -0
- /package/dist/{chunk-JNLYHJG7.js.map → chunk-74AY3S7U.js.map} +0 -0
- /package/dist/{chunk-RUAIO5UA.js.map → chunk-7J6A5MV2.js.map} +0 -0
- /package/dist/{chunk-A45QXZMU.js.map → chunk-7SAE6T36.js.map} +0 -0
- /package/dist/{chunk-QE2XQM3S.js.map → chunk-7YVER7ZO.js.map} +0 -0
- /package/dist/{chunk-IJH6METJ.js.map → chunk-A3UFV4K3.js.map} +0 -0
- /package/dist/{chunk-LXN737FX.js.map → chunk-AAANTXCQ.js.map} +0 -0
- /package/dist/{chunk-I3IPXGFQ.js.map → chunk-AJJPXUE6.js.map} +0 -0
- /package/dist/{chunk-2DPJFJFH.js.map → chunk-BNJAVRNJ.js.map} +0 -0
- /package/dist/{chunk-YL564QS2.js.map → chunk-CCL2OXFO.js.map} +0 -0
- /package/dist/{chunk-ZX5N5D6M.js.map → chunk-DAOMWGO2.js.map} +0 -0
- /package/dist/{chunk-23ZMQONL.js.map → chunk-ERMT5WQM.js.map} +0 -0
- /package/dist/{chunk-VTCNVGM6.js.map → chunk-EWBXMGXR.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-XTN54NML.js.map → chunk-G3T6YMBO.js.map} +0 -0
- /package/dist/{chunk-2JNBPOZ2.js.map → chunk-GDYLZ6FR.js.map} +0 -0
- /package/dist/{chunk-DZPTR2XQ.js.map → chunk-GFAEKSFR.js.map} +0 -0
- /package/dist/{chunk-M7LA4QBY.js.map → chunk-GXVXBYRA.js.map} +0 -0
- /package/dist/{chunk-JGMJZBFV.js.map → chunk-HPNXS6W6.js.map} +0 -0
- /package/dist/{chunk-BFPF5N3H.js.map → chunk-HXFIILGF.js.map} +0 -0
- /package/dist/{chunk-CMNL3QSL.js.map → chunk-IN6U2BS5.js.map} +0 -0
- /package/dist/{chunk-BWRX322G.js.map → chunk-IUYXCTA7.js.map} +0 -0
- /package/dist/{chunk-LU57EEJW.js.map → chunk-JBWD3J5U.js.map} +0 -0
- /package/dist/{chunk-2V664H6O.js.map → chunk-JS44PBCM.js.map} +0 -0
- /package/dist/{chunk-XKTHJHSQ.js.map → chunk-JZCBMKR5.js.map} +0 -0
- /package/dist/{chunk-MXPVNDGO.js.map → chunk-K5QQXVG6.js.map} +0 -0
- /package/dist/{chunk-BUKK5SWA.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-WUBGMV3G.js.map → chunk-KRAMCBIW.js.map} +0 -0
- /package/dist/{chunk-NDMPRT3Z.js.map → chunk-KW5YZELC.js.map} +0 -0
- /package/dist/{chunk-2RQR4TXV.js.map → chunk-LIBVANOF.js.map} +0 -0
- /package/dist/{chunk-M7L5A2AT.js.map → chunk-LMMBV7QC.js.map} +0 -0
- /package/dist/{chunk-PJBYVJKO.js.map → chunk-LXXIV7Y2.js.map} +0 -0
- /package/dist/{chunk-MZCTS3ZG.js.map → chunk-M2DOJBH7.js.map} +0 -0
- /package/dist/{chunk-WOPZ2P5Z.js.map → chunk-M4HRYLOA.js.map} +0 -0
- /package/dist/{chunk-JCYEDMOY.js.map → chunk-M6TP6UYD.js.map} +0 -0
- /package/dist/{chunk-KAUCVTAW.js.map → chunk-MLNXATFG.js.map} +0 -0
- /package/dist/{chunk-2NX7JTST.js.map → chunk-MPWOAIIH.js.map} +0 -0
- /package/dist/{chunk-RQLTCHUL.js.map → chunk-MWURUSRE.js.map} +0 -0
- /package/dist/{chunk-3EGL7XZO.js.map → chunk-N74WJ7B2.js.map} +0 -0
- /package/dist/{chunk-DRXTMDAG.js.map → chunk-N7FNCE7F.js.map} +0 -0
- /package/dist/{chunk-6CWAY4R7.js.map → chunk-NPLM7AFU.js.map} +0 -0
- /package/dist/{chunk-VECRSYQG.js.map → chunk-NRGQ5HJO.js.map} +0 -0
- /package/dist/{chunk-PPDNG7WC.js.map → chunk-NSP5PIQH.js.map} +0 -0
- /package/dist/{chunk-6HVLHIAB.js.map → chunk-P7OVFGMU.js.map} +0 -0
- /package/dist/{chunk-EX27VKU6.js.map → chunk-PDTNQLPC.js.map} +0 -0
- /package/dist/{chunk-ME5LVISI.js.map → chunk-PJCVI44H.js.map} +0 -0
- /package/dist/{chunk-LJCHDFLP.js.map → chunk-Q3PNQYAC.js.map} +0 -0
- /package/dist/{chunk-YFJHFEET.js.map → chunk-QS765SBL.js.map} +0 -0
- /package/dist/{chunk-FJ264QBP.js.map → chunk-QTDZ2NYC.js.map} +0 -0
- /package/dist/{chunk-Q6UZREJK.js.map → chunk-RKRX4C4H.js.map} +0 -0
- /package/dist/{chunk-GLZWOIU3.js.map → chunk-SJTSWG2Z.js.map} +0 -0
- /package/dist/{chunk-XM747YJF.js.map → chunk-TFWJXWL6.js.map} +0 -0
- /package/dist/{chunk-QFQ4UG3I.js.map → chunk-TXKQBQYW.js.map} +0 -0
- /package/dist/{chunk-BMOJFGVX.js.map → chunk-UALXWWGN.js.map} +0 -0
- /package/dist/{chunk-B44NFHWI.js.map → chunk-UNOAVHV4.js.map} +0 -0
- /package/dist/{chunk-TF5NZGPS.js.map → chunk-UQBOPCDK.js.map} +0 -0
- /package/dist/{chunk-5R2IF2KI.js.map → chunk-UXGXJUZ5.js.map} +0 -0
- /package/dist/{chunk-J6B6FK2X.js.map → chunk-VCOMACR6.js.map} +0 -0
- /package/dist/{chunk-WCGJK4NQ.js.map → chunk-VKQI47DJ.js.map} +0 -0
- /package/dist/{chunk-MPJDQC4G.js.map → chunk-WJCXUE2J.js.map} +0 -0
- /package/dist/{chunk-BCDECPVO.js.map → chunk-WV4UYDP6.js.map} +0 -0
- /package/dist/{chunk-VWJ56T63.js.map → chunk-WXJC2L66.js.map} +0 -0
- /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
- /package/dist/{chunk-FPANDKMQ.js.map → chunk-XC3H7KON.js.map} +0 -0
- /package/dist/{chunk-OOOX5C4X.js.map → chunk-XL2EE5W3.js.map} +0 -0
- /package/dist/{chunk-3SL52JKD.js.map → chunk-XVZA6VXC.js.map} +0 -0
- /package/dist/{chunk-CZVNOTLR.js.map → chunk-XXR4A33N.js.map} +0 -0
- /package/dist/{chunk-2AV2U2SZ.js.map → chunk-YDIOMDBW.js.map} +0 -0
- /package/dist/{chunk-XTVH2NAW.js.map → chunk-YECEEJUU.js.map} +0 -0
- /package/dist/{chunk-QR6W3FSW.js.map → chunk-Z5277JWF.js.map} +0 -0
- /package/dist/{chunk-CTHYR4QK.js.map → chunk-ZFNXJEUG.js.map} +0 -0
- /package/dist/{chunk-6Q56WDHG.js.map → chunk-ZLCBLDJO.js.map} +0 -0
- /package/dist/{chunk-KRACAE73.js.map → chunk-ZNRTHYYW.js.map} +0 -0
- /package/dist/{chunk-7AIPWNM7.js.map → chunk-ZSMA67FV.js.map} +0 -0
|
@@ -10,11 +10,17 @@
|
|
|
10
10
|
* - fragmented-entity merge (consolidation)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { lstat, readdir,
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
663
|
-
|
|
664
|
-
|
|
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.
|
|
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.
|
|
727
|
+
return await readEntityFileResolvingRename(this.deps, entryPath, entityId);
|
|
757
728
|
}
|
|
758
729
|
return null;
|
|
759
730
|
}
|
|
760
|
-
return await this.
|
|
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
|
|
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
|
@@ -17,6 +17,7 @@ import { computeLegacyContentHash, normalizeContent } from "./content-hash.js";
|
|
|
17
17
|
import { parseDriftProvenance, serializeDriftProvenance } from "./preferences/drift-types.js";
|
|
18
18
|
import { isAbortError, raceAbort } from "./abort-error.js";
|
|
19
19
|
import { checkCorpusReadAbort, type CorpusReadOptions } from "./corpus-read-cancellation.js";
|
|
20
|
+
import { scopedCaptureWriteLockLimits } from "./capture-write-lock-scope.js";
|
|
20
21
|
import { selectArtifactMatches, type ArtifactSearchOptions } from "./artifact-search.js";
|
|
21
22
|
import path from "node:path";
|
|
22
23
|
import { resolveSafeStoragePath } from "./storage-paths.js";
|
|
@@ -1922,6 +1923,8 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
1922
1923
|
onCatalogWrite?: () => void;
|
|
1923
1924
|
/** Post-write embedding hook (#2019). Installed by the orchestrator; fire-and-forget. */
|
|
1924
1925
|
onMemoryWrite?: (filePath: string) => void;
|
|
1926
|
+
/** Request search maintenance after a completed entity replay. */
|
|
1927
|
+
onEntityMigrationComplete?: () => void;
|
|
1925
1928
|
private notifyMemoryWrite(filePath: string): void {
|
|
1926
1929
|
try {
|
|
1927
1930
|
this.onMemoryWrite?.(filePath);
|
|
@@ -3241,7 +3244,7 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
3241
3244
|
* resolves to the legacy short bounds instead of the converge deadline.
|
|
3242
3245
|
*/
|
|
3243
3246
|
private captureLockLimitSource(): ContentHashIndexLockOptions | undefined {
|
|
3244
|
-
const options = this.factHashIndexLockOptions;
|
|
3247
|
+
const options = { ...this.factHashIndexLockOptions, ...scopedCaptureWriteLockLimits(this) };
|
|
3245
3248
|
const keys = ["captureWriteLockInitialDelayMs", "captureWriteLockMaxDelayMs", "captureWriteLockDeadlineMs", "captureWriteLockSignal"] as const;
|
|
3246
3249
|
return keys.some((key) => options[key] !== undefined) ? options : undefined;
|
|
3247
3250
|
}
|
|
@@ -4833,13 +4836,16 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
4833
4836
|
candidateBatchPaths: string[],
|
|
4834
4837
|
remainingSlots: number,
|
|
4835
4838
|
remainingInspectionBudget: number,
|
|
4836
|
-
readBatchSize: number
|
|
4839
|
+
readBatchSize: number,
|
|
4840
|
+
options?: CorpusReadOptions
|
|
4837
4841
|
): Promise<{ memories: MemoryFile[]; filePaths: string[] }> {
|
|
4838
4842
|
const memories: MemoryFile[] = [];
|
|
4839
4843
|
const filePaths: string[] = [];
|
|
4840
4844
|
const normalizedReadBatchSize = this.normalizeMemoryReadBatchSize(readBatchSize);
|
|
4841
4845
|
|
|
4842
4846
|
for (let index = 0; index < candidateBatchPaths.length; ) {
|
|
4847
|
+
// Per batch: an abandoned caller must not keep the scan running (#3139).
|
|
4848
|
+
checkCorpusReadAbort(options);
|
|
4843
4849
|
if (memories.length >= remainingSlots || filePaths.length >= remainingInspectionBudget) break;
|
|
4844
4850
|
const availableSlots = remainingSlots - memories.length;
|
|
4845
4851
|
const availableInspectionBudget = remainingInspectionBudget - filePaths.length;
|
|
@@ -4852,7 +4858,7 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
4852
4858
|
index += candidatePaths.length;
|
|
4853
4859
|
if (candidatePaths.length === 0) break;
|
|
4854
4860
|
filePaths.push(...candidatePaths);
|
|
4855
|
-
const parsedMemories = await this.readParsedMemoriesFromPaths(candidatePaths, candidatePaths.length);
|
|
4861
|
+
const parsedMemories = await this.readParsedMemoriesFromPaths(candidatePaths, candidatePaths.length, options);
|
|
4856
4862
|
if (parsedMemories.length === 0) continue;
|
|
4857
4863
|
memories.push(...parsedMemories.slice(0, availableSlots));
|
|
4858
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);
|