@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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Node-local warm-cache generation marker (`<stateDir>/embeddings.generation`).
|
|
2
|
+
// Two-phase publication barrier: in-flight before a publication's first
|
|
3
|
+
// destructive write, fresh unique value after its last write (PR #3176), so a
|
|
4
|
+
// completed stamp is only ever stable while generation bytes are unchanged.
|
|
5
|
+
// Single owner of the atomic protocol and logging — the store's own mutations
|
|
6
|
+
// and the offline-sync direct write/delete paths (issue #3182) share it.
|
|
7
|
+
// Plain fs always (tmp + rename): the marker is node-local raw bytes, never
|
|
8
|
+
// sync traffic or secure-store content.
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { rename, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
import { log } from "./logger.js";
|
|
14
|
+
|
|
15
|
+
export const GENERATION_MARKER_BASENAME = "embeddings.generation";
|
|
16
|
+
|
|
17
|
+
/** Reserved in-flight marker value; never equals a completion marker. */
|
|
18
|
+
export const GENERATION_MARKER_IN_FLIGHT = "in-flight";
|
|
19
|
+
|
|
20
|
+
export function generationMarkerPath(stateDirAbs: string): string {
|
|
21
|
+
return path.join(stateDirAbs, GENERATION_MARKER_BASENAME);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Strict atomic marker write; recovery uses it for a fresh completed value. */
|
|
25
|
+
export async function writeGenerationMarkerRaw(markerAbs: string, value: string): Promise<void> {
|
|
26
|
+
const tempPath = `${markerAbs}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
27
|
+
try {
|
|
28
|
+
await writeFile(tempPath, value, "utf-8");
|
|
29
|
+
await rename(tempPath, markerAbs);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
await rm(tempPath, { force: true }).catch(() => undefined);
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Barrier first half: strict, so bytes can never change under a stable stamp. */
|
|
37
|
+
export async function beginGenerationMarker(markerAbs: string): Promise<void> {
|
|
38
|
+
await writeGenerationMarkerRaw(markerAbs, GENERATION_MARKER_IN_FLIGHT);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Barrier second half: written after the last content write. Best-effort:
|
|
43
|
+
* a failure leaves the in-flight marker, whose stamp identityStamp()
|
|
44
|
+
* never repeats, so cached probes revalidate on every search until a
|
|
45
|
+
* publication completes.
|
|
46
|
+
*/
|
|
47
|
+
export async function completeGenerationMarker(markerAbs: string): Promise<void> {
|
|
48
|
+
await writeGenerationMarkerRaw(markerAbs, randomUUID()).catch((err) => {
|
|
49
|
+
log.warn(`embedding index: could not write generation completion marker: ${err instanceof Error ? err.message : String(err)}`);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -18,6 +18,13 @@ import { randomUUID } from "node:crypto";
|
|
|
18
18
|
import { constants as bufferConstants } from "node:buffer";
|
|
19
19
|
import { lstat, mkdir, mkdtemp, realpath, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
20
20
|
import { log } from "./logger.js";
|
|
21
|
+
import {
|
|
22
|
+
GENERATION_MARKER_IN_FLIGHT,
|
|
23
|
+
beginGenerationMarker as writeGenerationMarkerInFlight,
|
|
24
|
+
completeGenerationMarker as writeGenerationMarkerComplete,
|
|
25
|
+
generationMarkerPath,
|
|
26
|
+
writeGenerationMarkerRaw,
|
|
27
|
+
} from "./embedding-generation-marker.js";
|
|
21
28
|
import { readEnvVar } from "./runtime/env.js";
|
|
22
29
|
import type { EmbeddingGenerationFence } from "./embedding-generation-lock.js";
|
|
23
30
|
|
|
@@ -166,21 +173,6 @@ function replacementBackupPath(shardDir: string): string {
|
|
|
166
173
|
return path.join(path.dirname(shardDir), "embeddings.pre-replace.tmp");
|
|
167
174
|
}
|
|
168
175
|
|
|
169
|
-
/**
|
|
170
|
-
* Warm-cache generation marker (`<stateDir>/embeddings.generation`).
|
|
171
|
-
* Two-phase publication barrier: in-flight before a publication's first
|
|
172
|
-
* destructive write, fresh unique value after its last write. A stamp is
|
|
173
|
-
* therefore stable only for a completed generation — readers can never
|
|
174
|
-
* cache a stamp that stays stable while generation bytes change, and an
|
|
175
|
-
* in-flight stamp never repeats, so probes cached during a failed
|
|
176
|
-
* completion keep revalidating (PR #3176 codex P2). Plain fs like the
|
|
177
|
-
* status file.
|
|
178
|
-
*/
|
|
179
|
-
const GENERATION_MARKER_BASENAME = "embeddings.generation";
|
|
180
|
-
|
|
181
|
-
/** Reserved in-flight marker value; never equals a completion marker. */
|
|
182
|
-
const GENERATION_MARKER_IN_FLIGHT = "in-flight";
|
|
183
|
-
|
|
184
176
|
/**
|
|
185
177
|
* Stable FNV-1a shard assignment: an entry always hashes to the same shard,
|
|
186
178
|
* so an update or removal never needs a manifest to find its file.
|
|
@@ -535,34 +527,26 @@ export class EmbeddingIndexFileStore {
|
|
|
535
527
|
|
|
536
528
|
/** Barrier first half: strict, so bytes can never change under a stable stamp. */
|
|
537
529
|
private async beginGenerationMarker(): Promise<void> {
|
|
538
|
-
await this.
|
|
530
|
+
await writeGenerationMarkerInFlight(generationMarkerPath(path.dirname(this.shardDir)));
|
|
539
531
|
}
|
|
540
532
|
|
|
541
533
|
/**
|
|
542
|
-
* Barrier second half: written after the last content write. Best-effort
|
|
543
|
-
*
|
|
544
|
-
* never repeats, so cached probes revalidate on every search until a
|
|
545
|
-
* publication completes.
|
|
534
|
+
* Barrier second half: written after the last content write. Best-effort;
|
|
535
|
+
* the in-flight fallback semantics live in the shared marker module.
|
|
546
536
|
*/
|
|
547
537
|
private async completeGenerationMarker(): Promise<void> {
|
|
548
|
-
await
|
|
549
|
-
log.warn(`embedding index: could not write generation completion marker: ${err instanceof Error ? err.message : String(err)}`);
|
|
550
|
-
});
|
|
538
|
+
await writeGenerationMarkerComplete(generationMarkerPath(path.dirname(this.shardDir)));
|
|
551
539
|
}
|
|
552
540
|
|
|
553
541
|
private async writeGenerationMarker(value: string): Promise<void> {
|
|
554
|
-
await this.
|
|
555
|
-
path.join(path.dirname(this.shardDir), GENERATION_MARKER_BASENAME),
|
|
556
|
-
value,
|
|
557
|
-
{ bypassSecureIo: true },
|
|
558
|
-
);
|
|
542
|
+
await writeGenerationMarkerRaw(generationMarkerPath(path.dirname(this.shardDir)), value);
|
|
559
543
|
}
|
|
560
544
|
|
|
561
545
|
/** Marker value, or null when absent/unreadable (pre-marker stores keep the stat stamp). */
|
|
562
546
|
private async readGenerationMarker(): Promise<string | null> {
|
|
563
547
|
try {
|
|
564
548
|
const raw = await readFile(
|
|
565
|
-
|
|
549
|
+
generationMarkerPath(path.dirname(this.shardDir)),
|
|
566
550
|
"utf-8",
|
|
567
551
|
);
|
|
568
552
|
const value = raw.trim();
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { StorageManager, parseEntityFile } from "./storage.js";
|
|
7
|
+
import { sleep } from "./utils/sleep.js";
|
|
8
|
+
|
|
9
|
+
async function legacyStore() {
|
|
10
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-lazy-compat-"));
|
|
11
|
+
const seed = new StorageManager(dir);
|
|
12
|
+
const id = await seed.writeEntity("Legacy Person", "person", ["Keeps a synthetic task."]);
|
|
13
|
+
await rename(path.join(dir, "entities", `${id}.md`), path.join(dir, "entities", "legacy-person.md"));
|
|
14
|
+
const journal = path.join(dir, "state", "entity-canonical-id-migration-v1.json");
|
|
15
|
+
await rm(journal, { force: true });
|
|
16
|
+
const lock = path.join(dir, "state", "entity-canonical-id-migration.lock");
|
|
17
|
+
await writeFile(lock, `${process.pid} test-holder ${new Date().toISOString()}\n`);
|
|
18
|
+
const storage = new StorageManager(dir);
|
|
19
|
+
await storage.ensureDirectories({ waitForEntityMigration: false });
|
|
20
|
+
return { dir, id, journal, lock, storage };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("canonical reads find a legacy file without a journal or migration", async () => {
|
|
24
|
+
const fixture = await legacyStore();
|
|
25
|
+
try {
|
|
26
|
+
assert.match(await fixture.storage.readEntity(fixture.id), /Keeps a synthetic task/);
|
|
27
|
+
await assert.rejects(() => readFile(fixture.journal), { code: "ENOENT" });
|
|
28
|
+
assert.ok(await readFile(fixture.lock));
|
|
29
|
+
} finally {
|
|
30
|
+
StorageManager.clearAllStaticCaches();
|
|
31
|
+
await rm(fixture.dir, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("ambiguous legacy canonical matches are refused rather than picking a claimant", async () => {
|
|
36
|
+
const fixture = await legacyStore();
|
|
37
|
+
try {
|
|
38
|
+
const raw = await readFile(path.join(fixture.dir, "entities", "legacy-person.md"));
|
|
39
|
+
await writeFile(path.join(fixture.dir, "entities", "other-legacy-person.md"), raw);
|
|
40
|
+
await assert.rejects(() => fixture.storage.readEntity(fixture.id), /ambiguous/i);
|
|
41
|
+
} finally {
|
|
42
|
+
StorageManager.clearAllStaticCaches();
|
|
43
|
+
await rm(fixture.dir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("completed migration skips corpus reads on repeated misses in a fresh read-only store", async () => {
|
|
48
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-completed-miss-"));
|
|
49
|
+
try {
|
|
50
|
+
const seed = new StorageManager(dir);
|
|
51
|
+
await seed.writeEntity("Existing Person", "person", ["Synthetic fact."]);
|
|
52
|
+
await seed.ensureDirectories();
|
|
53
|
+
const reader = new StorageManager(dir);
|
|
54
|
+
await reader.ensureDirectories({ waitForEntityMigration: false });
|
|
55
|
+
// Bracket access is TypeScript's typed escape hatch for the private reader.
|
|
56
|
+
const originalRead = reader["readStorageSecureFile"].bind(reader);
|
|
57
|
+
const reads: string[] = [];
|
|
58
|
+
reader["readStorageSecureFile"] = async (filePath: string) => {
|
|
59
|
+
reads.push(path.basename(filePath));
|
|
60
|
+
return originalRead(filePath);
|
|
61
|
+
};
|
|
62
|
+
assert.equal(await reader.readEntity("person-missing"), "");
|
|
63
|
+
assert.equal(await reader.readEntity("person-missing"), "");
|
|
64
|
+
assert.deepEqual(reads, ["person-missing.md", "person-missing.md"], "completed-store misses must not scan entity contents");
|
|
65
|
+
} finally {
|
|
66
|
+
StorageManager.clearAllStaticCaches();
|
|
67
|
+
await rm(dir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("an external entities change invalidates the amortized completion verdict on the same reader", async () => {
|
|
72
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-completed-cache-"));
|
|
73
|
+
try {
|
|
74
|
+
const seed = new StorageManager(dir);
|
|
75
|
+
const id = await seed.writeEntity("Café", "project", ["Synthetic legacy fact."]);
|
|
76
|
+
await seed.ensureDirectories();
|
|
77
|
+
const reader = new StorageManager(dir);
|
|
78
|
+
await reader.ensureDirectories({ waitForEntityMigration: false });
|
|
79
|
+
const canonical = reader.normalizeEntityName("Café", "project");
|
|
80
|
+
// First miss computes and caches the completion verdict.
|
|
81
|
+
assert.equal(await reader.readEntity("project-missing"), "");
|
|
82
|
+
// An external rename changes the entities directory; the cached verdict
|
|
83
|
+
// must not keep hiding the now-legacy file from the same reader.
|
|
84
|
+
await rename(path.join(dir, "entities", `${id}.md`), path.join(dir, "entities", "legacy-cafe.md"));
|
|
85
|
+
assert.match(await reader.readEntity(canonical), /Synthetic legacy fact/);
|
|
86
|
+
} finally {
|
|
87
|
+
StorageManager.clearAllStaticCaches();
|
|
88
|
+
await rm(dir, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("an external aliases change invalidates the amortized completion verdict on the same reader", async () => {
|
|
93
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-completed-alias-cache-"));
|
|
94
|
+
try {
|
|
95
|
+
const seed = new StorageManager(dir);
|
|
96
|
+
const previousId = await seed.writeEntity("Café", "project", ["Synthetic legacy fact."]);
|
|
97
|
+
await seed.ensureDirectories();
|
|
98
|
+
const journalPath = path.join(dir, "state", "entity-canonical-id-migration-v1.json");
|
|
99
|
+
const journal = await readFile(journalPath, "utf8");
|
|
100
|
+
assert.equal(JSON.parse(journal).complete, true);
|
|
101
|
+
|
|
102
|
+
const reader = new StorageManager(dir);
|
|
103
|
+
await reader.ensureDirectories({ waitForEntityMigration: false });
|
|
104
|
+
// Warm amortized completion (and any same-reader completion memo) on a miss.
|
|
105
|
+
assert.equal(await reader.readEntity("project-missing"), "");
|
|
106
|
+
|
|
107
|
+
// Block migration repair so fingerprint drift stays visible to reads.
|
|
108
|
+
const lock = path.join(dir, "state", "entity-canonical-id-migration.lock");
|
|
109
|
+
await writeFile(lock, `${process.pid} test-holder ${new Date().toISOString()}\n`);
|
|
110
|
+
|
|
111
|
+
// External aliases.json is a getFingerprint input. It must invalidate a
|
|
112
|
+
// warm completed verdict without entities membership or mutation-version change.
|
|
113
|
+
// The entity file stays at previousId; only the alias-derived canonical id moves.
|
|
114
|
+
await mkdir(path.join(dir, "config"), { recursive: true });
|
|
115
|
+
await writeFile(path.join(dir, "config", "aliases.json"), JSON.stringify({ café: "coffee" }));
|
|
116
|
+
await reader.loadAliases();
|
|
117
|
+
const canonical = reader.normalizeEntityName("Café", "project");
|
|
118
|
+
assert.notEqual(canonical, previousId);
|
|
119
|
+
|
|
120
|
+
assert.match(await reader.readEntity(canonical), /Synthetic legacy fact/);
|
|
121
|
+
assert.equal(await readFile(journalPath, "utf8"), journal, "read compatibility must not replay migration");
|
|
122
|
+
assert.ok(await readFile(lock));
|
|
123
|
+
} finally {
|
|
124
|
+
StorageManager.clearAllStaticCaches();
|
|
125
|
+
await rm(dir, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
for (const change of ["aliases", "raw legacy write"]) {
|
|
131
|
+
test(`stale completed journals do not hide canonical reads after ${change}`, async () => {
|
|
132
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-stale-completion-"));
|
|
133
|
+
try {
|
|
134
|
+
const seed = new StorageManager(dir);
|
|
135
|
+
const previousId = await seed.writeEntity("Café", "project", ["Synthetic legacy fact."]);
|
|
136
|
+
await seed.ensureDirectories();
|
|
137
|
+
const journalPath = path.join(dir, "state", "entity-canonical-id-migration-v1.json");
|
|
138
|
+
const journal = await readFile(journalPath, "utf8");
|
|
139
|
+
assert.equal(JSON.parse(journal).complete, true);
|
|
140
|
+
if (change === "aliases") {
|
|
141
|
+
await mkdir(path.join(dir, "config"), { recursive: true });
|
|
142
|
+
await writeFile(path.join(dir, "config", "aliases.json"), JSON.stringify({ café: "coffee" }));
|
|
143
|
+
} else {
|
|
144
|
+
await rename(path.join(dir, "entities", `${previousId}.md`), path.join(dir, "entities", "legacy-cafe.md"));
|
|
145
|
+
await writeFile(path.join(dir, "state", "entity-canonical-id-reconcile.pending"), "synthetic raw writer\n");
|
|
146
|
+
}
|
|
147
|
+
const lock = path.join(dir, "state", "entity-canonical-id-migration.lock");
|
|
148
|
+
await writeFile(lock, `${process.pid} test-holder ${new Date().toISOString()}\n`);
|
|
149
|
+
const reader = new StorageManager(dir);
|
|
150
|
+
await reader.ensureDirectories({ waitForEntityMigration: false });
|
|
151
|
+
const canonical = reader.normalizeEntityName("Café", "project");
|
|
152
|
+
if (change === "aliases") assert.notEqual(canonical, previousId);
|
|
153
|
+
assert.match(await reader.readEntity(canonical), /Synthetic legacy fact/);
|
|
154
|
+
assert.equal(await readFile(journalPath, "utf8"), journal, "read compatibility must not replay migration");
|
|
155
|
+
assert.ok(await readFile(lock));
|
|
156
|
+
} finally {
|
|
157
|
+
StorageManager.clearAllStaticCaches();
|
|
158
|
+
await rm(dir, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const mutations = [
|
|
164
|
+
{
|
|
165
|
+
name: "relationship",
|
|
166
|
+
run: (s: StorageManager, id: string) => s.addEntityRelationship(id, { target: "person-peer", label: "knows" }),
|
|
167
|
+
check: (raw: string) => assert.ok(parseEntityFile(raw).relationships.some((r) => r.label === "knows")),
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "activity",
|
|
171
|
+
run: (s: StorageManager, id: string) => s.addEntityActivity(id, { date: "2026-01-01", note: "Synthetic activity" }, 10),
|
|
172
|
+
check: (raw: string) => assert.ok(parseEntityFile(raw).activity.some((a) => a.note === "Synthetic activity")),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "alias",
|
|
176
|
+
run: (s: StorageManager, id: string) => s.addEntityAlias(id, "synthetic-alias"),
|
|
177
|
+
check: (raw: string) => assert.ok(parseEntityFile(raw).aliases.includes("synthetic-alias")),
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "synthesis",
|
|
181
|
+
run: (s: StorageManager, id: string) => s.updateEntitySynthesis(id, "Synthetic synthesis"),
|
|
182
|
+
check: (raw: string) => assert.equal(parseEntityFile(raw).synthesis, "Synthetic synthesis"),
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "fragment merge",
|
|
186
|
+
run: (s: StorageManager) => s.mergeFragmentedEntities(),
|
|
187
|
+
check: (raw: string) => assert.match(raw, /Keeps a synthetic task/),
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "timeline migration",
|
|
191
|
+
run: (s: StorageManager) => s.migrateEntityFilesToCompiledTruthTimeline(),
|
|
192
|
+
check: (raw: string) => assert.match(raw, /Keeps a synthetic task/),
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
for (const caller of ["canonical", "legacy"]) {
|
|
197
|
+
for (const mutation of mutations) {
|
|
198
|
+
test(`${mutation.name} awaits migration for a ${caller} caller`, async () => {
|
|
199
|
+
const fixture = await legacyStore();
|
|
200
|
+
let settled = false;
|
|
201
|
+
let failure: Error | undefined;
|
|
202
|
+
const operation = mutation.run(fixture.storage, caller === "legacy" ? "legacy-person" : fixture.id).then(
|
|
203
|
+
() => { settled = true; },
|
|
204
|
+
(error: Error) => { failure = error; settled = true; },
|
|
205
|
+
);
|
|
206
|
+
let probeFailure: unknown;
|
|
207
|
+
try {
|
|
208
|
+
await sleep(100);
|
|
209
|
+
assert.equal(settled, false, "a public entity mutation must wait behind the migration lock");
|
|
210
|
+
} catch (error) {
|
|
211
|
+
probeFailure = error;
|
|
212
|
+
} finally {
|
|
213
|
+
await rm(fixture.lock, { force: true });
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
await operation;
|
|
217
|
+
if (probeFailure) throw probeFailure;
|
|
218
|
+
assert.equal(failure, undefined);
|
|
219
|
+
const raw = await readFile(path.join(fixture.dir, "entities", `${fixture.id}.md`), "utf8");
|
|
220
|
+
mutation.check(raw);
|
|
221
|
+
const journal = JSON.parse(await readFile(fixture.journal, "utf8"));
|
|
222
|
+
assert.equal(journal.complete, true);
|
|
223
|
+
} finally {
|
|
224
|
+
StorageManager.clearAllStaticCaches();
|
|
225
|
+
await rm(fixture.dir, { recursive: true, force: true });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rename, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { parseConfig } from "./config.js";
|
|
8
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
9
|
+
import { StorageManager } from "./storage.js";
|
|
10
|
+
|
|
11
|
+
// #3240 follow-up: the host's onEntityMigrationComplete slot must fire once
|
|
12
|
+
// per migration pass that rewrote entities (never on no-op replays or ensure() no-ops), fail open on a
|
|
13
|
+
// throwing hook, reach router-created storages independent of auto-embed, and
|
|
14
|
+
// be inert after Orchestrator.destroy(). No direct QMD calls; the re-arm is
|
|
15
|
+
// observed through a requestQmdMaintenance spy.
|
|
16
|
+
|
|
17
|
+
async function legacyEntityStore(prefix: string): Promise<string> {
|
|
18
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), prefix));
|
|
19
|
+
const seed = new StorageManager(dir);
|
|
20
|
+
const id = await seed.writeEntity("Legacy Person", "person", ["Keeps a synthetic task."]);
|
|
21
|
+
await rename(path.join(dir, "entities", `${id}.md`), path.join(dir, "entities", "legacy-person.md"));
|
|
22
|
+
await rm(path.join(dir, "state", "entity-canonical-id-migration-v1.json"), { force: true });
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("real storage fires the completion slot only for a pass that rewrote entities, and fails open on a throwing hook", async () => {
|
|
27
|
+
const dir = await legacyEntityStore("remnic-migration-complete-");
|
|
28
|
+
const emptyDir = await mkdtemp(path.join(os.tmpdir(), "remnic-migration-noop-"));
|
|
29
|
+
const throwingDir = await legacyEntityStore("remnic-migration-throwing-");
|
|
30
|
+
try {
|
|
31
|
+
const storage = new StorageManager(dir);
|
|
32
|
+
let completions = 0;
|
|
33
|
+
storage.onEntityMigrationComplete = () => {
|
|
34
|
+
completions += 1;
|
|
35
|
+
};
|
|
36
|
+
await storage.ensureDirectories();
|
|
37
|
+
assert.equal(completions, 1, "a migration pass that renamed a legacy entity must fire the completion slot");
|
|
38
|
+
await storage.ensureDirectories();
|
|
39
|
+
assert.equal(completions, 1, "a second ensureDirectories must not re-fire the slot");
|
|
40
|
+
|
|
41
|
+
const noop = new StorageManager(emptyDir);
|
|
42
|
+
let noopCompletions = 0;
|
|
43
|
+
noop.onEntityMigrationComplete = () => {
|
|
44
|
+
noopCompletions += 1;
|
|
45
|
+
};
|
|
46
|
+
await noop.ensureDirectories();
|
|
47
|
+
assert.equal(noopCompletions, 0, "a no-op replay must not ask search maintenance to run");
|
|
48
|
+
|
|
49
|
+
const throwing = new StorageManager(throwingDir);
|
|
50
|
+
let hookCalls = 0;
|
|
51
|
+
throwing.onEntityMigrationComplete = () => {
|
|
52
|
+
hookCalls += 1;
|
|
53
|
+
throw new Error("consumer hook exploded");
|
|
54
|
+
};
|
|
55
|
+
await throwing.ensureDirectories();
|
|
56
|
+
assert.equal(hookCalls, 1, "the throwing hook must still be invoked");
|
|
57
|
+
} finally {
|
|
58
|
+
StorageManager.clearAllStaticCaches();
|
|
59
|
+
await rm(dir, { recursive: true, force: true });
|
|
60
|
+
await rm(emptyDir, { recursive: true, force: true });
|
|
61
|
+
await rm(throwingDir, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("orchestrator wires completion with auto-embed off and blocks it from teardown admission", async () => {
|
|
66
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-migration-complete-orch-"));
|
|
67
|
+
const orchestrator = new Orchestrator(
|
|
68
|
+
parseConfig({
|
|
69
|
+
memoryDir: dir,
|
|
70
|
+
workspaceDir: path.join(dir, "workspace"),
|
|
71
|
+
qmdEnabled: false,
|
|
72
|
+
qmdAutoEmbedEnabled: false,
|
|
73
|
+
namespacesEnabled: true,
|
|
74
|
+
namespaceCatalogEnabled: true,
|
|
75
|
+
defaultNamespace: "default",
|
|
76
|
+
sharedNamespace: "shared",
|
|
77
|
+
namespacePolicies: [],
|
|
78
|
+
embeddingFallbackEnabled: false,
|
|
79
|
+
identityContinuityEnabled: false,
|
|
80
|
+
transcriptEnabled: false,
|
|
81
|
+
hourlySummariesEnabled: false,
|
|
82
|
+
compoundingEnabled: false,
|
|
83
|
+
knowledgeIndexEnabled: false,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
// Test-only access to the Orchestrator's private wiring fields: bracket access
|
|
87
|
+
// is TypeScript's typed escape hatch for private members, so no assertion.
|
|
88
|
+
const internals = {
|
|
89
|
+
get storage() { return orchestrator["storage"]; },
|
|
90
|
+
get storageRouter() { return orchestrator["storageRouter"]; },
|
|
91
|
+
get maintenanceScheduler() { return orchestrator["maintenanceScheduler"]; },
|
|
92
|
+
set requestQmdMaintenance(fn: () => void) { orchestrator["requestQmdMaintenance"] = fn; },
|
|
93
|
+
};
|
|
94
|
+
try {
|
|
95
|
+
// Wiring must exist independent of auto-embed.
|
|
96
|
+
const onMigrationComplete = internals.storage.onEntityMigrationComplete;
|
|
97
|
+
assert.ok(
|
|
98
|
+
onMigrationComplete instanceof Function,
|
|
99
|
+
"the default storage must carry the migration completion slot without auto-embed",
|
|
100
|
+
);
|
|
101
|
+
const namespaceStorage = await internals.storageRouter.storageFor("proj-a");
|
|
102
|
+
const onNamespaceMigrationComplete = namespaceStorage.onEntityMigrationComplete;
|
|
103
|
+
assert.ok(
|
|
104
|
+
onNamespaceMigrationComplete instanceof Function,
|
|
105
|
+
"a router-created namespace storage must carry the slot without auto-embed",
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const rearmRequests: string[] = [];
|
|
109
|
+
internals.requestQmdMaintenance = () => {
|
|
110
|
+
rearmRequests.push("rearm");
|
|
111
|
+
};
|
|
112
|
+
assert.ok(onMigrationComplete);
|
|
113
|
+
onMigrationComplete();
|
|
114
|
+
assert.ok(onNamespaceMigrationComplete);
|
|
115
|
+
onNamespaceMigrationComplete();
|
|
116
|
+
assert.deepEqual(rearmRequests, ["rearm", "rearm"], "a completion must re-arm QMD maintenance");
|
|
117
|
+
|
|
118
|
+
const { promise: disposeGate, resolve: releaseDispose } = Promise.withResolvers<void>();
|
|
119
|
+
const { promise: disposing, resolve: signalDispose } = Promise.withResolvers<void>();
|
|
120
|
+
const originalDispose = internals.maintenanceScheduler.dispose.bind(internals.maintenanceScheduler);
|
|
121
|
+
internals.maintenanceScheduler.dispose = async () => {
|
|
122
|
+
signalDispose();
|
|
123
|
+
await disposeGate;
|
|
124
|
+
await originalDispose();
|
|
125
|
+
};
|
|
126
|
+
const teardown = orchestrator.destroy();
|
|
127
|
+
try {
|
|
128
|
+
await disposing;
|
|
129
|
+
onMigrationComplete();
|
|
130
|
+
onNamespaceMigrationComplete();
|
|
131
|
+
assert.deepEqual(rearmRequests, ["rearm", "rearm"], "teardown admission must stop completion re-arms before drains");
|
|
132
|
+
} finally {
|
|
133
|
+
releaseDispose();
|
|
134
|
+
await teardown;
|
|
135
|
+
}
|
|
136
|
+
onMigrationComplete();
|
|
137
|
+
onNamespaceMigrationComplete();
|
|
138
|
+
assert.deepEqual(
|
|
139
|
+
rearmRequests,
|
|
140
|
+
["rearm", "rearm"],
|
|
141
|
+
"a completion after destroy must not re-arm maintenance",
|
|
142
|
+
);
|
|
143
|
+
} finally {
|
|
144
|
+
await orchestrator.destroy().catch(() => undefined);
|
|
145
|
+
StorageManager.clearAllStaticCaches();
|
|
146
|
+
await rm(dir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -657,7 +657,7 @@ export {
|
|
|
657
657
|
type ParsedBriefingWindow,
|
|
658
658
|
type BriefingFormatValue,
|
|
659
659
|
} from "./briefing.js";
|
|
660
|
-
|
|
660
|
+
export { withBriefingDeadline, BriefingTimeoutError, BRIEFING_DEADLINE_MS } from "./briefing-deadline.js";
|
|
661
661
|
// ---------------------------------------------------------------------------
|
|
662
662
|
// Binary lifecycle management (#367)
|
|
663
663
|
// ---------------------------------------------------------------------------
|
|
@@ -880,7 +880,7 @@ export {
|
|
|
880
880
|
type OfflineSyncSnapshot,
|
|
881
881
|
type OfflineSyncState,
|
|
882
882
|
} from "./offline-sync.js";
|
|
883
|
-
export { embeddingGenerationMembership, divergedEmbeddingGenerationDeferrals } from "./offline-sync-embedding-generation.js";
|
|
883
|
+
export { advanceEmbeddingGenerationMarker, embeddingGenerationMembership, divergedEmbeddingGenerationDeferrals } from "./offline-sync-embedding-generation.js";
|
|
884
884
|
export { EmbeddingIndexCapacityError, EmbeddingIndexFileStore, EmbeddingIndexStorageError, parseEmbeddingIndexDocument, serializeEmbeddingShard, shardEntriesForIndex, shardIndexOf, validateShardMembership } from "./embedding-index-storage.js";
|
|
885
885
|
|
|
886
886
|
// ---------------------------------------------------------------------------
|
package/src/location/tagging.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import type { StorageManager } from "../index.js";
|
|
23
23
|
import { log } from "../logger.js";
|
|
24
|
-
import type { MemoryFile } from "../types.js";
|
|
24
|
+
import type { BriefingResult, MemoryFile } from "../types.js";
|
|
25
25
|
import type { WearableConversation } from "../wearables/types.js";
|
|
26
26
|
import { localDayKey, placeDurations } from "./intervals.js";
|
|
27
27
|
import {
|
|
@@ -294,23 +294,25 @@ export async function briefingLocationSection(
|
|
|
294
294
|
);
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Append the opt-in location section to a briefing. The section is read BEFORE
|
|
299
|
+
* `build` runs: buildBriefing drains correction notifications at most once, so
|
|
300
|
+
* a deadline during this read must not lose them.
|
|
301
|
+
*/
|
|
302
|
+
export async function buildWithBriefingLocation<T extends Pick<BriefingResult, "markdown" | "json">>(
|
|
301
303
|
includeLocation: boolean | undefined,
|
|
302
304
|
memoryDir: string,
|
|
303
305
|
windowStartUtc: string,
|
|
304
306
|
location: LocationConfig,
|
|
307
|
+
build: () => Promise<T>,
|
|
305
308
|
): Promise<T> {
|
|
306
|
-
|
|
307
|
-
|
|
309
|
+
const section =
|
|
310
|
+
includeLocation === true && locationTaggingEnabled(location)
|
|
311
|
+
? await briefingLocationSection(memoryDir, windowStartUtc, location)
|
|
312
|
+
: null;
|
|
313
|
+
const result = await build();
|
|
308
314
|
if (!section) return result;
|
|
309
|
-
return {
|
|
310
|
-
...result,
|
|
311
|
-
markdown: `${result.markdown}\n\n${section}\n`,
|
|
312
|
-
json: { ...result.json, locationContext: section },
|
|
313
|
-
};
|
|
315
|
+
return { ...result, markdown: `${result.markdown}\n\n${section}\n`, json: { ...result.json, locationContext: section } };
|
|
314
316
|
}
|
|
315
317
|
|
|
316
318
|
/**
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { withEmbeddingGenerationLock } from "./embedding-generation-lock.js";
|
|
8
|
+
import { advanceEmbeddingGenerationMarker, embeddingGenerationMembership } from "./offline-sync-embedding-generation.js";
|
|
9
|
+
|
|
10
|
+
// Issue #3182 review: a peer can publish a newer shard while converge waits
|
|
11
|
+
// for the generation lock; the stale pre-lock digest must not delete it.
|
|
12
|
+
test("a stale-digest deletion is rejected under the generation lock", async (t) => {
|
|
13
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-3182-delete-digest-"));
|
|
14
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
15
|
+
const stateDir = path.join(root, "state");
|
|
16
|
+
const relative = "state/embeddings/shard-0000.json";
|
|
17
|
+
const target = path.join(root, relative);
|
|
18
|
+
const marker = path.join(stateDir, "embeddings.generation");
|
|
19
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
20
|
+
await writeFile(target, "older shard");
|
|
21
|
+
await writeFile(marker, "peer completed generation");
|
|
22
|
+
const staleSha256 = createHash("sha256").update("older shard").digest("hex");
|
|
23
|
+
|
|
24
|
+
let signalHeld: () => void;
|
|
25
|
+
const held = new Promise<void>((resolve) => { signalHeld = resolve; });
|
|
26
|
+
let releaseWriter: (() => void) | undefined;
|
|
27
|
+
const release = new Promise<void>((resolve) => { releaseWriter = resolve; });
|
|
28
|
+
assert.ok(releaseWriter);
|
|
29
|
+
const writer = withEmbeddingGenerationLock(stateDir, async () => {
|
|
30
|
+
signalHeld();
|
|
31
|
+
await release;
|
|
32
|
+
await writeFile(target, "newer peer shard");
|
|
33
|
+
});
|
|
34
|
+
await held;
|
|
35
|
+
|
|
36
|
+
let validated = false;
|
|
37
|
+
const deletion = advanceEmbeddingGenerationMarker(
|
|
38
|
+
root,
|
|
39
|
+
embeddingGenerationMembership(relative),
|
|
40
|
+
async () => {
|
|
41
|
+
assert.fail("deletion must never run when the digest is stale");
|
|
42
|
+
},
|
|
43
|
+
async () => {
|
|
44
|
+
validated = true;
|
|
45
|
+
if ((await readFile(target, "utf8")) !== "older shard") {
|
|
46
|
+
throw new Error("converge file changed before deletion");
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
const rejected = assert.rejects(deletion, /changed before deletion/);
|
|
51
|
+
releaseWriter();
|
|
52
|
+
await writer;
|
|
53
|
+
await rejected;
|
|
54
|
+
|
|
55
|
+
assert.equal(validated, true, "the digest precondition must run inside the lock");
|
|
56
|
+
assert.equal(await readFile(target, "utf8"), "newer peer shard");
|
|
57
|
+
assert.equal(await readFile(marker, "utf8"), "peer completed generation");
|
|
58
|
+
});
|