@remnic/core 10.0.59 → 10.0.60
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 +49 -47
- package/dist/access-authorization-probe.js +50 -48
- package/dist/access-boundary.js +49 -47
- package/dist/access-cli.js +136 -134
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +49 -47
- package/dist/access-http-briefing.js +50 -48
- package/dist/access-http-query.js +51 -49
- package/dist/access-http.js +72 -70
- package/dist/access-identity-continuity-surface.js +49 -47
- package/dist/access-lcm-surface.js +49 -47
- package/dist/access-mcp.js +60 -58
- package/dist/access-namespace-preflight.js +49 -47
- package/dist/access-observe-write-category.js +8 -8
- package/dist/access-observe-write-surface.js +49 -47
- package/dist/access-offline-manifest.js +7 -7
- package/dist/access-operations-batch.js +52 -50
- package/dist/access-operations.js +59 -57
- package/dist/access-recall-concurrency.js +49 -47
- package/dist/access-recall-response.js +49 -47
- package/dist/access-recall-surface.js +49 -47
- package/dist/access-schema.js +7 -7
- package/dist/access-service.js +49 -47
- package/dist/access-surface-catalog.js +51 -49
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +23 -21
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/briefing.js +15 -13
- package/dist/buffer.js +5 -5
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +18 -16
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-XDKEHZUC.js → chunk-264B57GV.js} +2 -2
- package/dist/{chunk-5U2BBF7E.js → chunk-2EJAPJ5G.js} +20 -10
- package/dist/chunk-2EJAPJ5G.js.map +1 -0
- package/dist/{chunk-6BO27AWQ.js → chunk-2HVCTFKG.js} +2 -2
- package/dist/{chunk-3E3Z6UA3.js → chunk-3DG2YAAX.js} +86 -86
- package/dist/chunk-4LZ7SVKM.js +211 -0
- package/dist/chunk-4LZ7SVKM.js.map +1 -0
- package/dist/{chunk-2IVK5IDY.js → chunk-4O4VAEWH.js} +2 -2
- package/dist/{chunk-NRGQ5HJO.js → chunk-4P4VLPGB.js} +1 -1
- package/dist/{chunk-GIRWNTMV.js → chunk-4PG5R6BJ.js} +2 -2
- package/dist/{chunk-PM4PKC3Y.js → chunk-5XY46Z2W.js} +3 -3
- package/dist/{chunk-52II7PB6.js → chunk-645WOU5I.js} +4 -4
- package/dist/{chunk-RG3I2663.js → chunk-64JLBFCQ.js} +2 -2
- package/dist/{chunk-G3T6YMBO.js → chunk-64S6X4IU.js} +7 -7
- package/dist/{chunk-FB4JQEFS.js → chunk-66CC6L6R.js} +2 -2
- package/dist/{chunk-TYSD7UQO.js → chunk-6BKSUA6P.js} +2 -2
- package/dist/{chunk-6AEKSOWV.js → chunk-6I6DFULH.js} +8 -8
- package/dist/{chunk-OL7A7NKH.js → chunk-74Z2BYEK.js} +2 -2
- package/dist/{chunk-JD5F5MYN.js → chunk-7DHNBABU.js} +3 -3
- package/dist/{chunk-JZRQY7ZQ.js → chunk-ADK5TMQL.js} +2 -2
- package/dist/{chunk-EJFOGYF3.js → chunk-AWKADEMO.js} +3 -3
- package/dist/{chunk-VV73XT3A.js → chunk-BJLCV2P3.js} +2 -2
- package/dist/{chunk-Y5KESB7T.js → chunk-C5GTIRIO.js} +38 -38
- package/dist/{chunk-LLG5VNK7.js → chunk-DF4SYU4Z.js} +2 -2
- package/dist/{chunk-WBN66HPI.js → chunk-ELHJK7LM.js} +6 -6
- package/dist/{chunk-YDDLSGYE.js → chunk-F3GX76LV.js} +2 -2
- package/dist/{chunk-AAANTXCQ.js → chunk-FZVZYU5C.js} +2 -2
- package/dist/{chunk-ON77FQXH.js → chunk-G6GMZJQ4.js} +2 -2
- package/dist/{chunk-37VTMUAL.js → chunk-GGHX4YHM.js} +2 -2
- package/dist/{chunk-JY2BGCYN.js → chunk-GJF337YM.js} +2 -2
- package/dist/{chunk-GDYLZ6FR.js → chunk-HJ3UTGIW.js} +3 -3
- package/dist/{chunk-FXWX7VD3.js → chunk-HL643H2F.js} +12 -12
- package/dist/{chunk-CCL2OXFO.js → chunk-HSHLDSPX.js} +3 -3
- package/dist/{chunk-WXBVGKS6.js → chunk-I6UTOUBK.js} +35 -79
- package/dist/chunk-I6UTOUBK.js.map +1 -0
- package/dist/{chunk-MLUPSZFK.js → chunk-IA3XZG4V.js} +2 -2
- package/dist/{chunk-ISNPYOXR.js → chunk-IJFX4JRI.js} +7 -7
- package/dist/{chunk-RTJJPW3C.js → chunk-ILMB3X4X.js} +5 -5
- package/dist/{chunk-USNU3BEQ.js → chunk-J53IBVMF.js} +2 -2
- package/dist/{chunk-JEX2JL3U.js → chunk-JEDS2QYV.js} +2 -2
- package/dist/{chunk-RFT4OPYI.js → chunk-JFRO5HJY.js} +2 -2
- package/dist/{chunk-WZYAXOGO.js → chunk-JM2WJBP7.js} +2 -2
- package/dist/{chunk-DJFULNXK.js → chunk-JRY55I6Z.js} +2 -2
- package/dist/{chunk-YZNTAOPZ.js → chunk-KXDUSGPM.js} +2 -2
- package/dist/{chunk-YJHOVA42.js → chunk-L5RMURVI.js} +2 -2
- package/dist/{chunk-R4BR62UO.js → chunk-LGLLCWA2.js} +2 -2
- package/dist/{chunk-AMKIIRLL.js → chunk-M3GOWG2P.js} +2 -2
- package/dist/{chunk-4G2A7MH3.js → chunk-M7KAJBO2.js} +3 -3
- package/dist/{chunk-UGTYADB4.js → chunk-MHSUEA32.js} +3 -3
- package/dist/{chunk-ERMT5WQM.js → chunk-MKE5DP3S.js} +2 -2
- package/dist/{chunk-EH46DHBU.js → chunk-MOB4JHG6.js} +2 -2
- package/dist/{chunk-IQSRVR6Z.js → chunk-N5H22V75.js} +2 -2
- package/dist/{chunk-WASVDS6G.js → chunk-NF5EHQTJ.js} +4 -4
- package/dist/{chunk-RYB553SS.js → chunk-NJZY5LOC.js} +2 -2
- package/dist/{chunk-BPIAXMGC.js → chunk-PIVSUPF2.js} +2 -2
- package/dist/{chunk-2S6JADBB.js → chunk-PJLJN23N.js} +2 -2
- package/dist/{chunk-ZAZZOQAQ.js → chunk-PRSF7DWT.js} +2 -2
- package/dist/{chunk-QY24FSXT.js → chunk-PTJWUN4H.js} +45 -45
- package/dist/{chunk-GFAEKSFR.js → chunk-QBK3IM6W.js} +5 -5
- package/dist/{chunk-KRAMCBIW.js → chunk-QKXPOLQD.js} +4 -4
- package/dist/{chunk-VCLNU6MY.js → chunk-QWURVT5Z.js} +2 -2
- package/dist/{chunk-YDT3OEYF.js → chunk-R4ZHF4K2.js} +2 -2
- package/dist/{chunk-KMJDH4KX.js → chunk-RSYFSCKL.js} +16 -16
- package/dist/{chunk-UHH67NLU.js → chunk-T4SMMTGY.js} +2 -2
- package/dist/{chunk-DJR37Q37.js → chunk-TIS6T6UP.js} +2 -2
- package/dist/{chunk-RKRX4C4H.js → chunk-TPKB7T6Z.js} +2 -2
- package/dist/{chunk-JZCBMKR5.js → chunk-TUCEP2LS.js} +5 -5
- package/dist/{chunk-OMWQO7H6.js → chunk-UEDC2VGP.js} +2 -2
- package/dist/{chunk-KBOQQEVC.js → chunk-UIX4JLVV.js} +2 -2
- package/dist/{chunk-NGH74LDV.js → chunk-UP2Q7U4G.js} +9 -9
- package/dist/{chunk-HQUYENKK.js → chunk-UY67YP6Q.js} +2 -2
- package/dist/{chunk-U2A56LLF.js → chunk-VQ6H4DL6.js} +11 -11
- package/dist/{chunk-RHJ5VGB3.js → chunk-W37MS6SX.js} +2 -2
- package/dist/{chunk-KAL5SXPP.js → chunk-WJC5KGYC.js} +6 -6
- package/dist/{chunk-G6A3SGCE.js → chunk-WVEEVYMH.js} +8 -25
- package/dist/chunk-WVEEVYMH.js.map +1 -0
- package/dist/{chunk-4B73TQQV.js → chunk-X2TZWVCC.js} +2 -2
- package/dist/{chunk-UQBOPCDK.js → chunk-X4PAATLA.js} +2 -2
- package/dist/{chunk-24SIB3EW.js → chunk-XWE3EOWX.js} +2 -2
- package/dist/{chunk-Y77FXULH.js → chunk-XWXBXBOX.js} +2 -2
- package/dist/{chunk-VKQI47DJ.js → chunk-Y7427SL7.js} +4 -4
- package/dist/{chunk-TEH5MDX5.js → chunk-YH7HJNWV.js} +22 -22
- package/dist/chunk-YHTG64XV.js +117 -0
- package/dist/chunk-YHTG64XV.js.map +1 -0
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{chunk-2M36PR7R.js → chunk-Z2T5BPOZ.js} +2 -2
- package/dist/{chunk-7NT3FKAF.js → chunk-Z4EH5TSO.js} +2 -2
- package/dist/{chunk-ZE5N4JSY.js → chunk-ZRDEOC4X.js} +2 -2
- package/dist/{chunk-IEW6NDHX.js → chunk-ZXEPLZUQ.js} +9 -9
- package/dist/cli.js +94 -92
- package/dist/coding/export-okf-codegraph.js +14 -12
- package/dist/coding/export-okf-codegraph.js.map +1 -1
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +16 -14
- package/dist/config.js +23 -21
- package/dist/connectors/codex-materialize-runner.js +15 -13
- package/dist/connectors/index.js +15 -13
- package/dist/convergence-refresh.js +17 -15
- package/dist/corpus-scan-worker-host.d.ts +109 -0
- package/dist/corpus-scan-worker-host.d.ts.map +1 -0
- package/dist/corpus-scan-worker-host.js +15 -0
- package/dist/corpus-scan-worker.entry.d.ts +21 -0
- package/dist/corpus-scan-worker.entry.d.ts.map +1 -0
- package/dist/corpus-scan-worker.entry.js +60 -0
- package/dist/corpus-scan-worker.entry.js.map +1 -0
- package/dist/corpus-scan-worker.entry.mjs +176 -0
- package/dist/corpus-scan.d.ts +71 -0
- package/dist/corpus-scan.d.ts.map +1 -0
- package/dist/corpus-scan.js +16 -0
- package/dist/corpus-watermark.d.ts.map +1 -1
- package/dist/corpus-watermark.js +17 -15
- package/dist/dashboard-runtime.js +2 -2
- package/dist/deep-recall-http-glue.js +8 -8
- package/dist/embedding-fallback.js +2 -2
- package/dist/{entity-canonical-id-migration-F3JVG6K6.js → entity-canonical-id-migration-CUDCKWPF.js} +4 -2
- package/dist/entity-canonical-id-migration-CUDCKWPF.js.map +1 -0
- package/dist/entity-retrieval.js +17 -15
- package/dist/event-order-recall.js +3 -3
- package/dist/external-wiki-access.js +50 -48
- package/dist/extraction.js +20 -18
- package/dist/{graph-edge-decay-IU75NV42.js → graph-edge-decay-2BZTY2IM.js} +5 -5
- package/dist/graph-edge-decay-2BZTY2IM.js.map +1 -0
- package/dist/graph-jsonl.js +3 -3
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +2 -2
- package/dist/harmonic-retrieval.js +15 -13
- package/dist/index.js +164 -162
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +5 -5
- package/dist/location/index.js +17 -15
- package/dist/maintenance/memory-governance.js +15 -13
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +15 -13
- package/dist/maintenance/rebuild-memory-projection-runner.js +17 -15
- package/dist/maintenance/rebuild-memory-projection.js +16 -14
- package/dist/memory-browse-operations.js +50 -48
- package/dist/memory-browse.js +5 -5
- package/dist/namespaces/migrate.js +16 -14
- package/dist/namespaces/search.js +8 -8
- package/dist/namespaces/storage.js +15 -13
- package/dist/offline-sync-changeset-generations.js +4 -4
- package/dist/offline-sync-embedding-generation.js +3 -3
- package/dist/offline-sync-snapshot-stream.js +7 -7
- package/dist/offline-sync.js +6 -6
- package/dist/operator-doctor-corpus.js +18 -16
- package/dist/operator-toolkit.js +34 -32
- package/dist/orchestration/maintenance.js +25 -23
- package/dist/orchestrator.js +136 -134
- package/dist/qmd.js +4 -4
- package/dist/recall-navigation-operations.js +50 -48
- package/dist/recall-navigation.js +49 -47
- package/dist/recall-state-view-admission.js +5 -5
- package/dist/recall-why-service.js +5 -5
- package/dist/resume-bundles.js +24 -22
- package/dist/search/factory.js +7 -7
- package/dist/search/index.js +15 -15
- package/dist/semantic-consolidation.js +16 -14
- package/dist/semantic-rule-promotion.js +15 -13
- package/dist/semantic-rule-verifier.js +15 -13
- package/dist/source-agent-qualifier.js +18 -16
- package/dist/standup/build.js +15 -13
- package/dist/storage/entity-canonical-id-migration.d.ts.map +1 -1
- package/dist/storage/memory-read-store.d.ts +5 -9
- package/dist/storage/memory-read-store.d.ts.map +1 -1
- package/dist/storage.js +16 -14
- package/dist/support-passport/index.js +52 -50
- package/dist/transfer/export-okf.js +15 -13
- package/dist/verified-recall.js +15 -13
- package/dist/who-knows-cli.js +16 -14
- package/dist/who-knows.js +15 -13
- package/package.json +2 -2
- package/scripts/build-with-heap.mjs +8 -0
- package/src/corpus-scan-worker-host.ts +314 -0
- package/src/corpus-scan-worker.entry.mjs +176 -0
- package/src/corpus-scan-worker.entry.ts +95 -0
- package/src/corpus-scan.ts +215 -0
- package/src/corpus-watermark.ts +28 -12
- package/src/storage/entity-canonical-id-migration.ts +4 -26
- package/src/storage/memory-read-store.ts +17 -99
- package/dist/chunk-5U2BBF7E.js.map +0 -1
- package/dist/chunk-G6A3SGCE.js.map +0 -1
- package/dist/chunk-WXBVGKS6.js.map +0 -1
- /package/dist/{chunk-XDKEHZUC.js.map → chunk-264B57GV.js.map} +0 -0
- /package/dist/{chunk-6BO27AWQ.js.map → chunk-2HVCTFKG.js.map} +0 -0
- /package/dist/{chunk-3E3Z6UA3.js.map → chunk-3DG2YAAX.js.map} +0 -0
- /package/dist/{chunk-2IVK5IDY.js.map → chunk-4O4VAEWH.js.map} +0 -0
- /package/dist/{chunk-NRGQ5HJO.js.map → chunk-4P4VLPGB.js.map} +0 -0
- /package/dist/{chunk-GIRWNTMV.js.map → chunk-4PG5R6BJ.js.map} +0 -0
- /package/dist/{chunk-PM4PKC3Y.js.map → chunk-5XY46Z2W.js.map} +0 -0
- /package/dist/{chunk-52II7PB6.js.map → chunk-645WOU5I.js.map} +0 -0
- /package/dist/{chunk-RG3I2663.js.map → chunk-64JLBFCQ.js.map} +0 -0
- /package/dist/{chunk-G3T6YMBO.js.map → chunk-64S6X4IU.js.map} +0 -0
- /package/dist/{chunk-FB4JQEFS.js.map → chunk-66CC6L6R.js.map} +0 -0
- /package/dist/{chunk-TYSD7UQO.js.map → chunk-6BKSUA6P.js.map} +0 -0
- /package/dist/{chunk-6AEKSOWV.js.map → chunk-6I6DFULH.js.map} +0 -0
- /package/dist/{chunk-OL7A7NKH.js.map → chunk-74Z2BYEK.js.map} +0 -0
- /package/dist/{chunk-JD5F5MYN.js.map → chunk-7DHNBABU.js.map} +0 -0
- /package/dist/{chunk-JZRQY7ZQ.js.map → chunk-ADK5TMQL.js.map} +0 -0
- /package/dist/{chunk-EJFOGYF3.js.map → chunk-AWKADEMO.js.map} +0 -0
- /package/dist/{chunk-VV73XT3A.js.map → chunk-BJLCV2P3.js.map} +0 -0
- /package/dist/{chunk-Y5KESB7T.js.map → chunk-C5GTIRIO.js.map} +0 -0
- /package/dist/{chunk-LLG5VNK7.js.map → chunk-DF4SYU4Z.js.map} +0 -0
- /package/dist/{chunk-WBN66HPI.js.map → chunk-ELHJK7LM.js.map} +0 -0
- /package/dist/{chunk-YDDLSGYE.js.map → chunk-F3GX76LV.js.map} +0 -0
- /package/dist/{chunk-AAANTXCQ.js.map → chunk-FZVZYU5C.js.map} +0 -0
- /package/dist/{chunk-ON77FQXH.js.map → chunk-G6GMZJQ4.js.map} +0 -0
- /package/dist/{chunk-37VTMUAL.js.map → chunk-GGHX4YHM.js.map} +0 -0
- /package/dist/{chunk-JY2BGCYN.js.map → chunk-GJF337YM.js.map} +0 -0
- /package/dist/{chunk-GDYLZ6FR.js.map → chunk-HJ3UTGIW.js.map} +0 -0
- /package/dist/{chunk-FXWX7VD3.js.map → chunk-HL643H2F.js.map} +0 -0
- /package/dist/{chunk-CCL2OXFO.js.map → chunk-HSHLDSPX.js.map} +0 -0
- /package/dist/{chunk-MLUPSZFK.js.map → chunk-IA3XZG4V.js.map} +0 -0
- /package/dist/{chunk-ISNPYOXR.js.map → chunk-IJFX4JRI.js.map} +0 -0
- /package/dist/{chunk-RTJJPW3C.js.map → chunk-ILMB3X4X.js.map} +0 -0
- /package/dist/{chunk-USNU3BEQ.js.map → chunk-J53IBVMF.js.map} +0 -0
- /package/dist/{chunk-JEX2JL3U.js.map → chunk-JEDS2QYV.js.map} +0 -0
- /package/dist/{chunk-RFT4OPYI.js.map → chunk-JFRO5HJY.js.map} +0 -0
- /package/dist/{chunk-WZYAXOGO.js.map → chunk-JM2WJBP7.js.map} +0 -0
- /package/dist/{chunk-DJFULNXK.js.map → chunk-JRY55I6Z.js.map} +0 -0
- /package/dist/{chunk-YZNTAOPZ.js.map → chunk-KXDUSGPM.js.map} +0 -0
- /package/dist/{chunk-YJHOVA42.js.map → chunk-L5RMURVI.js.map} +0 -0
- /package/dist/{chunk-R4BR62UO.js.map → chunk-LGLLCWA2.js.map} +0 -0
- /package/dist/{chunk-AMKIIRLL.js.map → chunk-M3GOWG2P.js.map} +0 -0
- /package/dist/{chunk-4G2A7MH3.js.map → chunk-M7KAJBO2.js.map} +0 -0
- /package/dist/{chunk-UGTYADB4.js.map → chunk-MHSUEA32.js.map} +0 -0
- /package/dist/{chunk-ERMT5WQM.js.map → chunk-MKE5DP3S.js.map} +0 -0
- /package/dist/{chunk-EH46DHBU.js.map → chunk-MOB4JHG6.js.map} +0 -0
- /package/dist/{chunk-IQSRVR6Z.js.map → chunk-N5H22V75.js.map} +0 -0
- /package/dist/{chunk-WASVDS6G.js.map → chunk-NF5EHQTJ.js.map} +0 -0
- /package/dist/{chunk-RYB553SS.js.map → chunk-NJZY5LOC.js.map} +0 -0
- /package/dist/{chunk-BPIAXMGC.js.map → chunk-PIVSUPF2.js.map} +0 -0
- /package/dist/{chunk-2S6JADBB.js.map → chunk-PJLJN23N.js.map} +0 -0
- /package/dist/{chunk-ZAZZOQAQ.js.map → chunk-PRSF7DWT.js.map} +0 -0
- /package/dist/{chunk-QY24FSXT.js.map → chunk-PTJWUN4H.js.map} +0 -0
- /package/dist/{chunk-GFAEKSFR.js.map → chunk-QBK3IM6W.js.map} +0 -0
- /package/dist/{chunk-KRAMCBIW.js.map → chunk-QKXPOLQD.js.map} +0 -0
- /package/dist/{chunk-VCLNU6MY.js.map → chunk-QWURVT5Z.js.map} +0 -0
- /package/dist/{chunk-YDT3OEYF.js.map → chunk-R4ZHF4K2.js.map} +0 -0
- /package/dist/{chunk-KMJDH4KX.js.map → chunk-RSYFSCKL.js.map} +0 -0
- /package/dist/{chunk-UHH67NLU.js.map → chunk-T4SMMTGY.js.map} +0 -0
- /package/dist/{chunk-DJR37Q37.js.map → chunk-TIS6T6UP.js.map} +0 -0
- /package/dist/{chunk-RKRX4C4H.js.map → chunk-TPKB7T6Z.js.map} +0 -0
- /package/dist/{chunk-JZCBMKR5.js.map → chunk-TUCEP2LS.js.map} +0 -0
- /package/dist/{chunk-OMWQO7H6.js.map → chunk-UEDC2VGP.js.map} +0 -0
- /package/dist/{chunk-KBOQQEVC.js.map → chunk-UIX4JLVV.js.map} +0 -0
- /package/dist/{chunk-NGH74LDV.js.map → chunk-UP2Q7U4G.js.map} +0 -0
- /package/dist/{chunk-HQUYENKK.js.map → chunk-UY67YP6Q.js.map} +0 -0
- /package/dist/{chunk-U2A56LLF.js.map → chunk-VQ6H4DL6.js.map} +0 -0
- /package/dist/{chunk-RHJ5VGB3.js.map → chunk-W37MS6SX.js.map} +0 -0
- /package/dist/{chunk-KAL5SXPP.js.map → chunk-WJC5KGYC.js.map} +0 -0
- /package/dist/{chunk-4B73TQQV.js.map → chunk-X2TZWVCC.js.map} +0 -0
- /package/dist/{chunk-UQBOPCDK.js.map → chunk-X4PAATLA.js.map} +0 -0
- /package/dist/{chunk-24SIB3EW.js.map → chunk-XWE3EOWX.js.map} +0 -0
- /package/dist/{chunk-Y77FXULH.js.map → chunk-XWXBXBOX.js.map} +0 -0
- /package/dist/{chunk-VKQI47DJ.js.map → chunk-Y7427SL7.js.map} +0 -0
- /package/dist/{chunk-TEH5MDX5.js.map → chunk-YH7HJNWV.js.map} +0 -0
- /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
- /package/dist/{chunk-2M36PR7R.js.map → chunk-Z2T5BPOZ.js.map} +0 -0
- /package/dist/{chunk-7NT3FKAF.js.map → chunk-Z4EH5TSO.js.map} +0 -0
- /package/dist/{chunk-ZE5N4JSY.js.map → chunk-ZRDEOC4X.js.map} +0 -0
- /package/dist/{chunk-IEW6NDHX.js.map → chunk-ZXEPLZUQ.js.map} +0 -0
- /package/dist/{entity-canonical-id-migration-F3JVG6K6.js.map → corpus-scan-worker-host.js.map} +0 -0
- /package/dist/{graph-edge-decay-IU75NV42.js.map → corpus-scan.js.map} +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Corpus maintenance scans (the pure, read-only filesystem work the daemon
|
|
3
|
+
* runs periodically).
|
|
4
|
+
*
|
|
5
|
+
* Three scans live here, all of them corpus-sized:
|
|
6
|
+
* - the recall-corpus path walk (hot + cold tiers);
|
|
7
|
+
* - the entity-entry fingerprint the canonical-id migration reads on every
|
|
8
|
+
* entity unlock;
|
|
9
|
+
* - the watermark's freshness stat over the newest hot partition.
|
|
10
|
+
*
|
|
11
|
+
* They are plain functions over plain inputs so the corpus-scan worker can run
|
|
12
|
+
* them off the main event loop: `src/corpus-scan-worker.entry.ts` is bundled
|
|
13
|
+
* into a plain-JavaScript worker entry (`src/corpus-scan-worker.entry.mjs`,
|
|
14
|
+
* generated — see scripts/sync-corpus-scan-worker.mjs) and the host falls back
|
|
15
|
+
* to these functions inline when a worker cannot start. One implementation,
|
|
16
|
+
* two execution sites, identical results.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { Dirent, Stats } from "node:fs";
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import { lstat, readdir, realpath, stat } from "node:fs/promises";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { isErrnoCode } from "./utils/errno.js";
|
|
24
|
+
import { isMacOsReplicationJunkBasename } from "./utils/macos-replication-junk.js";
|
|
25
|
+
|
|
26
|
+
/** An errno-carrying filesystem failure. */
|
|
27
|
+
export type ErrnoFailure = NodeJS.ErrnoException;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A backend READ failure worth propagating from a strict (census) scan: an
|
|
31
|
+
* errno-coded fs error that is NOT `ENOENT`. `ENOENT` (an absent dir) and
|
|
32
|
+
* code-less rejections are expected and skipped; `EACCES`/`EIO`/… mean the
|
|
33
|
+
* corpus is unreadable and a divergence census must fail loud rather than
|
|
34
|
+
* silently publish a partial count (issue #2156 round-5). Non-strict (recall)
|
|
35
|
+
* callers ignore this and degrade to a best-effort partial scan.
|
|
36
|
+
*/
|
|
37
|
+
export function isPropagatableReadError(error: ErrnoFailure): boolean {
|
|
38
|
+
return error.code !== undefined && error.code !== "ENOENT";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CorpusScanWalkOptions {
|
|
42
|
+
/** Memory root the walk is contained in. */
|
|
43
|
+
baseDir: string;
|
|
44
|
+
/** Directories to walk (category roots, cold root, archive root). */
|
|
45
|
+
startDirs: readonly string[];
|
|
46
|
+
/** Strict mode: backend read failures propagate instead of being skipped. */
|
|
47
|
+
propagateReadErrors?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Cancellation check at every directory/entry boundary. The inline reader
|
|
50
|
+
* passes the corpus-read abort check; the worker passes its cancel flag.
|
|
51
|
+
*/
|
|
52
|
+
checkCancelled?(): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Recursively collect `*.md` paths under `startDirs`. Paths only — no
|
|
57
|
+
* frontmatter parse. Never throws for a missing root (an empty result), and
|
|
58
|
+
* throws in strict mode when a backend read failure makes the corpus
|
|
59
|
+
* unreadable.
|
|
60
|
+
*
|
|
61
|
+
* Containment is established once, not per file: the memory root is resolved
|
|
62
|
+
* with a single `realpath`, every directory the walk enters is `lstat`ed (a
|
|
63
|
+
* symlinked or non-directory entry is never followed), and the entry TYPE
|
|
64
|
+
* comes from `readdir(withFileTypes)` — so a symlinked entry, file or
|
|
65
|
+
* directory, is skipped without a per-entry `realpath`. Reads refuse
|
|
66
|
+
* symlinked files at open time with `O_NOFOLLOW`, so a path swapped to a
|
|
67
|
+
* symlink mid-scan is refused rather than followed.
|
|
68
|
+
*/
|
|
69
|
+
export async function collectContainedMarkdownPaths(options: CorpusScanWalkOptions): Promise<string[]> {
|
|
70
|
+
const { baseDir, startDirs, propagateReadErrors = false, checkCancelled } = options;
|
|
71
|
+
const filePaths: string[] = [];
|
|
72
|
+
|
|
73
|
+
// Resolve the memory root once: a missing root is an empty corpus and an
|
|
74
|
+
// unreadable root is a backend failure the strict census must surface.
|
|
75
|
+
try {
|
|
76
|
+
await realpath(baseDir);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// SAFETY: node:fs/promises rejects with an Error; the errno `code` is
|
|
79
|
+
// optional on that type, so this adds no unchecked claim.
|
|
80
|
+
const failure = error as ErrnoFailure;
|
|
81
|
+
// ENOENT: a not-yet-created root ⇒ empty. Any other errno is a backend
|
|
82
|
+
// read failure — propagate for strict census callers (never publish an
|
|
83
|
+
// unreadable corpus as empty); non-strict (recall) callers degrade.
|
|
84
|
+
if (propagateReadErrors && isPropagatableReadError(failure)) throw error;
|
|
85
|
+
return filePaths;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const collectPaths = async (dir: string): Promise<void> => {
|
|
89
|
+
checkCancelled?.();
|
|
90
|
+
// Directory-level guard, isolated from per-entry handling: skip symlinked
|
|
91
|
+
// or non-directory category dirs. A failure here means the whole subtree
|
|
92
|
+
// does not exist or escaped the store — fail closed by skipping it.
|
|
93
|
+
let dirStat: Stats;
|
|
94
|
+
try {
|
|
95
|
+
dirStat = await lstat(dir);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// SAFETY: node:fs/promises rejects with an Error; `code` is optional.
|
|
98
|
+
const failure = error as ErrnoFailure;
|
|
99
|
+
// ENOENT (absent category dir) is expected; a backend read error
|
|
100
|
+
// (EACCES/EIO/…) propagates in strict census mode.
|
|
101
|
+
if (propagateReadErrors && isPropagatableReadError(failure)) throw error;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (dirStat.isSymbolicLink()) return; // never follow symlinks out of the store (both modes)
|
|
105
|
+
if (!dirStat.isDirectory()) {
|
|
106
|
+
// A category root that EXISTS but is not a directory (e.g. `facts/`
|
|
107
|
+
// replaced by a regular file) is a layout/backend failure: a strict
|
|
108
|
+
// census must not silently publish the remaining categories as healthy.
|
|
109
|
+
// Non-strict (recall) stays best-effort and skips it (issue #2156 round-10).
|
|
110
|
+
if (propagateReadErrors) {
|
|
111
|
+
throw new Error(`corpus scan: expected a directory but found a non-directory at ${dir}`);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
let entries: Dirent[];
|
|
116
|
+
try {
|
|
117
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// SAFETY: node:fs/promises rejects with an Error; `code` is optional.
|
|
120
|
+
const failure = error as ErrnoFailure;
|
|
121
|
+
// A backend read error (EACCES …) propagates in strict census mode so a
|
|
122
|
+
// partial subtree is not silently counted. ENOENT (vanished mid-scan)
|
|
123
|
+
// and other non-propagatable failures skip the directory.
|
|
124
|
+
if (propagateReadErrors && isPropagatableReadError(failure)) throw error;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const subdirs: string[] = [];
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
// Outside the per-entry handling below, which deliberately swallows
|
|
131
|
+
// non-propagatable errors — a cancellation must never be swallowed with
|
|
132
|
+
// them (issue #2307).
|
|
133
|
+
checkCancelled?.();
|
|
134
|
+
// Never follow symlinked entries out of the store.
|
|
135
|
+
if (entry.isSymbolicLink()) continue;
|
|
136
|
+
if (isMacOsReplicationJunkBasename(entry.name)) continue;
|
|
137
|
+
const fullPath = path.join(dir, entry.name);
|
|
138
|
+
if (entry.isDirectory()) {
|
|
139
|
+
subdirs.push(fullPath);
|
|
140
|
+
} else if (entry.name.endsWith(".md")) {
|
|
141
|
+
filePaths.push(fullPath);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Recurse into real subdirectories regardless of any single poisoned entry
|
|
145
|
+
// above, so valid nested in-store memories are never dropped.
|
|
146
|
+
for (const subdir of subdirs) {
|
|
147
|
+
await collectPaths(subdir);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (const dir of startDirs) {
|
|
152
|
+
await collectPaths(dir);
|
|
153
|
+
}
|
|
154
|
+
// A signal that fired during the last directory read would otherwise return a
|
|
155
|
+
// truncated path list as if it were the whole corpus (issue #2307 review).
|
|
156
|
+
checkCancelled?.();
|
|
157
|
+
return filePaths;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Fingerprint of the entity entries a migration's completion verdict depends
|
|
162
|
+
* on: the sorted entry names plus each entry's identity and timestamps. Two
|
|
163
|
+
* runs over the same entries produce the same string, and an in-place edit of
|
|
164
|
+
* an entity file changes it.
|
|
165
|
+
*/
|
|
166
|
+
export async function fingerprintEntityEntries(entitiesDir: string): Promise<string> {
|
|
167
|
+
let names: string[];
|
|
168
|
+
try {
|
|
169
|
+
names = (await readdir(entitiesDir))
|
|
170
|
+
.filter((name) => path.extname(name) === ".md" && !isMacOsReplicationJunkBasename(name))
|
|
171
|
+
.sort();
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (isErrnoCode(error, "ENOENT")) return "missing";
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
const hash = createHash("sha256");
|
|
177
|
+
for (const name of names) {
|
|
178
|
+
hash.update(`${name.length}:${name}:`);
|
|
179
|
+
try {
|
|
180
|
+
const entry = await lstat(path.join(entitiesDir, name), { bigint: true });
|
|
181
|
+
hash.update(`${entry.dev}:${entry.ino}:${entry.size}:${entry.mtimeNs}:${entry.ctimeNs}`);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (!isErrnoCode(error, "ENOENT")) throw error;
|
|
184
|
+
hash.update("missing");
|
|
185
|
+
}
|
|
186
|
+
hash.update("\n");
|
|
187
|
+
}
|
|
188
|
+
return hash.digest("hex");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Stat a batch of files and return their mtimes, `null` for files that
|
|
193
|
+
* vanished (ENOENT). A backend read failure (EACCES/EIO/…) throws with its
|
|
194
|
+
* errno code intact so the caller can omit an unreadable corpus instead of
|
|
195
|
+
* publishing a stale freshness value.
|
|
196
|
+
*/
|
|
197
|
+
export async function statMtimes(paths: readonly string[]): Promise<Array<number | null>> {
|
|
198
|
+
const results: Array<number | null> = new Array(paths.length).fill(null);
|
|
199
|
+
// Fixed-size batches: an unbounded Promise.all submits one libuv operation
|
|
200
|
+
// per file at once, and the worker and main thread share that threadpool, so
|
|
201
|
+
// a large bulk import would delay every other filesystem operation.
|
|
202
|
+
const STAT_BATCH_SIZE = 256;
|
|
203
|
+
for (let start = 0; start < paths.length; start += STAT_BATCH_SIZE) {
|
|
204
|
+
await Promise.all(paths.slice(start, start + STAT_BATCH_SIZE).map(async (filePath, offset) => {
|
|
205
|
+
try {
|
|
206
|
+
const entry = await stat(filePath);
|
|
207
|
+
results[start + offset] = entry.mtimeMs;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (isErrnoCode(error, "ENOENT")) return;
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
return results;
|
|
215
|
+
}
|
package/src/corpus-watermark.ts
CHANGED
|
@@ -37,10 +37,11 @@
|
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import { createHash } from "node:crypto";
|
|
40
|
-
import { stat } from "node:fs/promises";
|
|
41
40
|
import path from "node:path";
|
|
41
|
+
import { setImmediate as yieldToEventLoop } from "node:timers/promises";
|
|
42
42
|
import { capabilityAllowsNamespace, type TokenCapabilities } from "./access-token-capabilities.js";
|
|
43
43
|
import { listNamespaces } from "./namespaces/migrate.js";
|
|
44
|
+
import { corpusScanWorker } from "./corpus-scan-worker-host.js";
|
|
44
45
|
import type { PluginConfig } from "./types.js";
|
|
45
46
|
|
|
46
47
|
export interface CorpusWatermark {
|
|
@@ -67,6 +68,9 @@ export const UNPARTITIONED_BUCKET = "unpartitioned";
|
|
|
67
68
|
/** Storage tier a memory file lives in. */
|
|
68
69
|
export type CorpusTier = "hot" | "cold";
|
|
69
70
|
|
|
71
|
+
/** Paths parsed between event-loop yields while building a census. */
|
|
72
|
+
const CENSUS_YIELD_EVERY = 4_096;
|
|
73
|
+
|
|
70
74
|
const DAY_PARTITION_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
71
75
|
/** Cold-tier root segment (relative to a namespace base dir): `<baseDir>/cold/...`. */
|
|
72
76
|
const COLD_TIER_DIR = "cold";
|
|
@@ -155,7 +159,17 @@ export async function computeCorpusWatermark(input: {
|
|
|
155
159
|
const census = new Map<string, number>();
|
|
156
160
|
let newestPartition: string | null = null;
|
|
157
161
|
const parsedByPath: ParsedPath[] = [];
|
|
162
|
+
let sinceYield = 0;
|
|
158
163
|
for (const filePath of paths) {
|
|
164
|
+
// Parsing one path is cheap, but a census covers the whole corpus (100k+
|
|
165
|
+
// paths): one uninterrupted pass holds the main thread for hundreds of
|
|
166
|
+
// milliseconds. Yield to the event loop every CENSUS_YIELD_EVERY paths so
|
|
167
|
+
// observe/recall interleave; the result is order-independent and identical.
|
|
168
|
+
if (sinceYield >= CENSUS_YIELD_EVERY) {
|
|
169
|
+
sinceYield = 0;
|
|
170
|
+
await yieldToEventLoop();
|
|
171
|
+
}
|
|
172
|
+
sinceYield += 1;
|
|
159
173
|
const parsed = parsePartition(baseDir, filePath);
|
|
160
174
|
parsedByPath.push(parsed);
|
|
161
175
|
census.set(parsed.bucket, (census.get(parsed.bucket) ?? 0) + 1);
|
|
@@ -171,19 +185,21 @@ export async function computeCorpusWatermark(input: {
|
|
|
171
185
|
|
|
172
186
|
let newestWriteAt: string | null = null;
|
|
173
187
|
if (newestPartition !== null) {
|
|
174
|
-
|
|
188
|
+
const newestPaths: string[] = [];
|
|
175
189
|
for (let i = 0; i < paths.length; i += 1) {
|
|
176
190
|
if (parsedByPath[i].tier !== "hot" || parsedByPath[i].date !== newestPartition) continue;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
newestPaths.push(paths[i]);
|
|
192
|
+
}
|
|
193
|
+
// The freshness sweep is one stat per file in the newest hot partition; it
|
|
194
|
+
// runs in the corpus-scan worker so the periodic census does not spend
|
|
195
|
+
// main-thread time on a per-file syscall sweep. A file that vanished
|
|
196
|
+
// (ENOENT) reports null; a backend read failure (EACCES/EIO/…) propagates
|
|
197
|
+
// with its errno code so the namespace is omitted, not published with a
|
|
198
|
+
// stale/null freshness (issue #2156 round-6).
|
|
199
|
+
const mtimes = await corpusScanWorker.request({ kind: "stat-mtimes", paths: newestPaths });
|
|
200
|
+
let maxMtimeMs = Number.NEGATIVE_INFINITY;
|
|
201
|
+
for (const mtimeMs of mtimes) {
|
|
202
|
+
if (mtimeMs !== null && mtimeMs > maxMtimeMs) maxMtimeMs = mtimeMs;
|
|
187
203
|
}
|
|
188
204
|
if (maxMtimeMs > Number.NEGATIVE_INFINITY) {
|
|
189
205
|
newestWriteAt = new Date(maxMtimeMs).toISOString();
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { lstatSync, readFileSync } from "node:fs";
|
|
15
15
|
import { z } from "zod";
|
|
16
|
+
import { corpusScanWorker } from "../corpus-scan-worker-host.js";
|
|
16
17
|
import { computeSupersessionKey, normalizeSupersessionKey } from "../temporal-supersession.js";
|
|
17
18
|
import type { EntityFile, MemoryFile } from "../types.js";
|
|
18
19
|
import { log } from "../logger.js";
|
|
@@ -102,31 +103,6 @@ async function fingerprintPath(filePath: string): Promise<string> {
|
|
|
102
103
|
: "missing";
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
async function fingerprintEntityEntries(entitiesDir: string): Promise<string> {
|
|
106
|
-
let names: string[];
|
|
107
|
-
try {
|
|
108
|
-
names = (await readdir(entitiesDir))
|
|
109
|
-
.filter((name) => path.extname(name) === ".md" && !isMacOsReplicationJunkBasename(name))
|
|
110
|
-
.sort();
|
|
111
|
-
} catch (error) {
|
|
112
|
-
if (isErrnoCode(error, "ENOENT")) return "missing";
|
|
113
|
-
throw error;
|
|
114
|
-
}
|
|
115
|
-
const hash = createHash("sha256");
|
|
116
|
-
for (const name of names) {
|
|
117
|
-
hash.update(`${name.length}:${name}:`);
|
|
118
|
-
try {
|
|
119
|
-
const entry = await lstat(path.join(entitiesDir, name), { bigint: true });
|
|
120
|
-
hash.update(`${entry.dev}:${entry.ino}:${entry.size}:${entry.mtimeNs}:${entry.ctimeNs}`);
|
|
121
|
-
} catch (error) {
|
|
122
|
-
if (!isErrnoCode(error, "ENOENT")) throw error;
|
|
123
|
-
hash.update("missing");
|
|
124
|
-
}
|
|
125
|
-
hash.update("\n");
|
|
126
|
-
}
|
|
127
|
-
return hash.digest("hex");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
106
|
/**
|
|
131
107
|
* Completion fingerprint for migration inputs only. Entity entry identities
|
|
132
108
|
* catch same-path edits that do not change the directory inode; the cooperative
|
|
@@ -146,7 +122,9 @@ export async function getFingerprint(
|
|
|
146
122
|
consumingFingerprint,
|
|
147
123
|
] = await Promise.all([
|
|
148
124
|
fingerprintPath(entitiesDir),
|
|
149
|
-
|
|
125
|
+
// The entry enumeration is one lstat per entity file — a corpus-sized
|
|
126
|
+
// syscall storm on the main thread. It runs in the corpus-scan worker.
|
|
127
|
+
corpusScanWorker.request({ kind: "entity-entries-fingerprint", entitiesDir }),
|
|
150
128
|
fingerprintPath(path.join(baseDir, "config", "aliases.json")),
|
|
151
129
|
fingerprintPath(path.join(baseDir, "state", ENTITY_CANONICAL_ID_RECONCILE_MARKER)),
|
|
152
130
|
fingerprintPath(path.join(baseDir, "state", ENTITY_CANONICAL_ID_RECONCILE_CONSUMING_MARKER)),
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { Dirent, Stats } from "node:fs";
|
|
13
|
-
import { lstat, readdir,
|
|
13
|
+
import { lstat, readdir, stat } from "node:fs/promises";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { toMemoryPathRel } from "../memory-lifecycle-ledger-utils.js";
|
|
16
16
|
import { SecureStoreLockedError, readMaybeEncryptedFile } from "../secure-store/secure-fs.js";
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
StorageManager,
|
|
31
31
|
} from "../storage.js";
|
|
32
32
|
import { rememberRawFrontmatter } from "./memory-frontmatter-metadata.js";
|
|
33
|
+
import { corpusScanWorker } from "../corpus-scan-worker-host.js";
|
|
33
34
|
|
|
34
35
|
export interface MemoryReadStoreDeps {
|
|
35
36
|
/** Live class object of the host instance — shared static caches (see storage.ts storageManagerClass). */
|
|
@@ -85,19 +86,6 @@ export interface MemoryReadStoreDeps {
|
|
|
85
86
|
readonly wearablesDir: string;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
/**
|
|
89
|
-
* A backend READ failure worth propagating from a strict (census) scan: an
|
|
90
|
-
* errno-coded fs error that is NOT `ENOENT`. `ENOENT` (an absent dir) and
|
|
91
|
-
* code-less rejections are expected and skipped; `EACCES`/`EIO`/… mean the
|
|
92
|
-
* corpus is unreadable and a divergence census must fail loud rather than
|
|
93
|
-
* silently publish a partial count (issue #2156 round-5). Non-strict (recall)
|
|
94
|
-
* callers ignore this and degrade to a best-effort partial scan.
|
|
95
|
-
*/
|
|
96
|
-
function isPropagatableReadError(err: unknown): boolean {
|
|
97
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
98
|
-
return typeof code === "string" && code !== "ENOENT";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
89
|
/**
|
|
102
90
|
* Markdown under this store's `entities/` directory. The first relative
|
|
103
91
|
* segment must be `entities`; a memory root whose absolute path contains
|
|
@@ -233,15 +221,11 @@ export class MemoryReadStore {
|
|
|
233
221
|
* escape: a category dir symlinked outside memoryDir (e.g. decisions/ -> an
|
|
234
222
|
* external dir) must NOT pull out-of-store files into a scan (info leak).
|
|
235
223
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* `realpath` cost one syscall per markdown file (hundreds of thousands per
|
|
242
|
-
* corpus scan) to re-derive a fact the directory-level checks already
|
|
243
|
-
* establish; reads refuse symlinked files at open time with `O_NOFOLLOW`, so
|
|
244
|
-
* a path swapped to a symlink mid-scan is refused rather than followed.
|
|
224
|
+
* The walk runs in the corpus-scan worker: it is one `readdir` + `lstat` per
|
|
225
|
+
* directory over the whole corpus, and on the main event loop that starves
|
|
226
|
+
* observe, recall and the extraction queue behind it. The implementation is
|
|
227
|
+
* shared (src/corpus-scan.ts) and the host runs it inline when no worker
|
|
228
|
+
* can start, so the collected path set is the same either way.
|
|
245
229
|
* Paths only — no frontmatter parse.
|
|
246
230
|
*/
|
|
247
231
|
private async collectContainedMarkdownPaths(
|
|
@@ -249,82 +233,16 @@ export class MemoryReadStore {
|
|
|
249
233
|
propagateReadErrors = false,
|
|
250
234
|
options?: CorpusReadOptions,
|
|
251
235
|
): Promise<string[]> {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (propagateReadErrors && isPropagatableReadError(err)) throw err;
|
|
263
|
-
return filePaths;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const collectPaths = async (dir: string): Promise<void> => {
|
|
267
|
-
checkCorpusReadAbort(options);
|
|
268
|
-
// Directory-level guard, isolated from per-entry handling: skip symlinked
|
|
269
|
-
// or non-directory category dirs. A failure here means the whole subtree
|
|
270
|
-
// does not exist or escaped the store — fail closed by skipping it.
|
|
271
|
-
let dirStat: Stats;
|
|
272
|
-
try {
|
|
273
|
-
dirStat = await lstat(dir);
|
|
274
|
-
} catch (err) {
|
|
275
|
-
// ENOENT (absent category dir) is expected; a backend read error
|
|
276
|
-
// (EACCES/EIO/…) propagates in strict census mode.
|
|
277
|
-
if (propagateReadErrors && isPropagatableReadError(err)) throw err;
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
if (dirStat.isSymbolicLink()) return; // never follow symlinks out of the store (both modes)
|
|
281
|
-
if (!dirStat.isDirectory()) {
|
|
282
|
-
// A category root that EXISTS but is not a directory (e.g. `facts/`
|
|
283
|
-
// replaced by a regular file) is a layout/backend failure: a strict
|
|
284
|
-
// census must not silently publish the remaining categories as healthy.
|
|
285
|
-
// Non-strict (recall) stays best-effort and skips it (issue #2156 round-10).
|
|
286
|
-
if (propagateReadErrors) {
|
|
287
|
-
throw new Error(`corpus scan: expected a directory but found a non-directory at ${dir}`);
|
|
288
|
-
}
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
let entries: Dirent[];
|
|
292
|
-
try {
|
|
293
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
294
|
-
} catch (err) {
|
|
295
|
-
// A backend read error (EACCES …) propagates in strict census mode so a
|
|
296
|
-
// partial subtree is not silently counted. ENOENT (vanished mid-scan)
|
|
297
|
-
// and other non-propagatable failures skip the directory.
|
|
298
|
-
if (propagateReadErrors && isPropagatableReadError(err)) throw err;
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const subdirs: string[] = [];
|
|
303
|
-
for (const entry of entries) {
|
|
304
|
-
// Outside the per-entry handling below, which deliberately swallows
|
|
305
|
-
// non-propagatable errors — a cancellation must never be swallowed with
|
|
306
|
-
// them (issue #2307).
|
|
307
|
-
checkCorpusReadAbort(options);
|
|
308
|
-
// Never follow symlinked entries out of the store.
|
|
309
|
-
if (entry.isSymbolicLink()) continue;
|
|
310
|
-
if (isMacOsReplicationJunkBasename(entry.name)) continue;
|
|
311
|
-
const fullPath = path.join(dir, entry.name);
|
|
312
|
-
if (entry.isDirectory()) {
|
|
313
|
-
subdirs.push(fullPath);
|
|
314
|
-
} else if (entry.name.endsWith(".md")) {
|
|
315
|
-
filePaths.push(fullPath);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
// Recurse into real subdirectories regardless of any single poisoned entry
|
|
319
|
-
// above, so valid nested in-store memories are never dropped.
|
|
320
|
-
for (const subdir of subdirs) {
|
|
321
|
-
await collectPaths(subdir);
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
for (const dir of startDirs) {
|
|
326
|
-
await collectPaths(dir);
|
|
327
|
-
}
|
|
236
|
+
checkCorpusReadAbort(options);
|
|
237
|
+
const filePaths = await corpusScanWorker.request(
|
|
238
|
+
{
|
|
239
|
+
kind: "walk",
|
|
240
|
+
baseDir: this.deps.baseDir,
|
|
241
|
+
startDirs: [...startDirs],
|
|
242
|
+
propagateReadErrors,
|
|
243
|
+
},
|
|
244
|
+
options?.abortSignal,
|
|
245
|
+
);
|
|
328
246
|
// A signal that fired during the last directory read would otherwise return a
|
|
329
247
|
// truncated path list as if it were the whole corpus (issue #2307 review).
|
|
330
248
|
checkCorpusReadAbort(options);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/corpus-watermark.ts"],"sourcesContent":["/**\n * Corpus watermark (issue #2149) — a cheap, comparable fingerprint of a\n * daemon's active-memory corpus.\n *\n * Deployment shape this addresses: two daemons behind an active/backup VIP,\n * each owning its own memory directory. Nothing compared the two corpora, so a\n * pair could diverge for months (e.g. one holding ~190k files, its backup\n * ~340k with different histories) with no signal until a manual audit. This\n * module ships the WATERMARK primitive and the read surfaces that expose it;\n * peer polling and divergence alerting are a follow-up.\n *\n * The digest is a CENSUS fingerprint (per day-partition file counts), NOT a\n * content hash — two daemons that agree on how many memory FILES live in\n * each `<tier>:<category>/<day>` partition produce the same digest, so a\n * differing digest is a cheap divergence signal without reading or hashing file\n * bodies. Buckets are tier-aware (`hot:` / `cold:`): demoted memories stay\n * reachable via cold recall, so the census counts BOTH tiers and a hot/cold\n * split shows in the digest rather than being silently folded together (issue\n * #2156 finding D). `newestWriteAt` stays scoped to the newest HOT partition\n * only, so the freshness probe stays O(one day's active files) even on a 100k+\n * corpus.\n *\n * Robustness (issue #2156 review rounds):\n * - `/health` and `remnic doctor` resolve their namespace set through the ONE\n * {@link resolveCorpusNamespaceRoots} helper — config-driven enumeration\n * unioned with the PERSISTED namespace catalog read from config — so both\n * surfaces enumerate the same tenants and cannot drift.\n * - The `/health` builder serves through a stale-while-revalidate\n * {@link CorpusWatermarkCache}, so a probe NEVER awaits the recursive corpus\n * scan (it returns the cached/stale value and refreshes in the background).\n * - Each namespace's census brackets its hot+cold walk with a corpus-mutation\n * sentinel and retries when a tier migration races it, so a transient\n * double-count/miss is never cached as a false divergence.\n * - An unreadable corpus root (EACCES) is OMITTED, not published as a false\n * empty; a missing root (ENOENT — a not-yet-created namespace) is a genuine\n * empty corpus.\n */\n\nimport { createHash } from \"node:crypto\";\nimport { stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { capabilityAllowsNamespace, type TokenCapabilities } from \"./access-token-capabilities.js\";\nimport { listNamespaces } from \"./namespaces/migrate.js\";\nimport type { PluginConfig } from \"./types.js\";\n\nexport interface CorpusWatermark {\n namespace: string;\n /**\n * Count of memory files under the census scan roots (hot + cold). This is a\n * FILE census, not a status-filtered active count: reading each file's\n * frontmatter status would defeat the cheap-probe design, so an in-place\n * status change (e.g. archived-in-place) is not reflected here.\n */\n memoryFileCount: number;\n /** Newest `YYYY-MM-DD` day-partition seen in the HOT tier, or null when none is dated. */\n newestPartition: string | null;\n /** ISO max mtime within the newest HOT partition, or null when it has no files. */\n newestWriteAt: string | null;\n /** sha256 hex over the deterministic per-partition census. */\n digest: string;\n computedAt: string;\n}\n\n/** Explicit bucket for paths that do not match the `<category>/<day>` shape. */\nexport const UNPARTITIONED_BUCKET = \"unpartitioned\";\n\n/** Storage tier a memory file lives in. */\nexport type CorpusTier = \"hot\" | \"cold\";\n\nconst DAY_PARTITION_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n/** Cold-tier root segment (relative to a namespace base dir): `<baseDir>/cold/...`. */\nconst COLD_TIER_DIR = \"cold\";\n\n/**\n * True when `token` is not just `YYYY-MM-DD`-shaped but a REAL calendar day.\n * The regex alone accepts impossible dates (e.g. `2026-99-99`) that would then\n * sort lexically above every valid partition and corrupt `newestPartition`\n * (issue #2156 round-6); round-tripping through a UTC Date rejects month/day\n * overflow. A malformed dir falls back to the unpartitioned bucket.\n */\nfunction isRealCalendarDay(token: string): boolean {\n if (!DAY_PARTITION_RE.test(token)) return false;\n const [year, month, day] = token.split(\"-\").map(Number);\n const date = new Date(Date.UTC(year, month - 1, day));\n return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;\n}\n\ninterface ParsedPath {\n readonly bucket: string;\n readonly date: string | null;\n readonly tier: CorpusTier;\n}\n\nfunction parsePartition(baseDir: string, filePath: string): ParsedPath {\n const rel = path.relative(baseDir, filePath).split(path.sep).join(\"/\");\n const parts = rel.split(\"/\");\n // Cold-tier files live under `<baseDir>/cold/<category>/<day>/...`; hot files\n // sit directly under `<baseDir>/<category>/<day>/...`. Strip the leading\n // `cold/` segment so the category/day parse is tier-agnostic, then prefix the\n // bucket with the tier so a hot and cold partition of the same day never\n // collide (they must diverge the digest, not fold together).\n const tier: CorpusTier = parts[0] === COLD_TIER_DIR ? \"cold\" : \"hot\";\n const categoryParts = tier === \"cold\" ? parts.slice(1) : parts;\n if (categoryParts.length >= 2 && isRealCalendarDay(categoryParts[1])) {\n return { bucket: `${tier}:${categoryParts[0]}/${categoryParts[1]}`, date: categoryParts[1], tier };\n }\n return { bucket: `${tier}:${UNPARTITIONED_BUCKET}`, date: null, tier };\n}\n\n/**\n * Build the per-partition census: bucket key -> file count. Bucket keys are\n * tier-aware (`hot:`/`cold:`). Paths not matching a day-partition land in the\n * explicit tier {@link UNPARTITIONED_BUCKET}; they are never silently dropped.\n * Pure — no filesystem access.\n */\nexport function buildPartitionCensus(paths: readonly string[], baseDir: string): Map<string, number> {\n const census = new Map<string, number>();\n for (const filePath of paths) {\n const { bucket } = parsePartition(baseDir, filePath);\n census.set(bucket, (census.get(bucket) ?? 0) + 1);\n }\n return census;\n}\n\n/**\n * Deterministic census -> digest. Bucket keys are SORTED before serialization\n * (AGENTS.md pattern 26: insertion order must not affect a hash), then emitted\n * as `\"<bucket>:<count>\"` lines joined by `\\n` and hashed with sha256. Pure —\n * separately exported so it can be unit-tested without a filesystem.\n */\nexport function digestPartitionCensus(census: ReadonlyMap<string, number> | Record<string, number>): string {\n const entries = census instanceof Map ? [...census.entries()] : Object.entries(census);\n const serialized = entries\n .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))\n .map(([bucket, count]) => `${bucket}:${count}`)\n .join(\"\\n\");\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n}\n\n/**\n * Compute a watermark for a single namespace from an already-collected path\n * list (both tiers, under `baseDir`) plus its memory base directory. Never\n * throws on an empty list — an empty corpus yields the digest of the empty\n * census and null timestamps. Only the newest HOT partition's files are\n * `stat`-ed, so freshness stays cheap even on a 100k+ corpus.\n */\nexport async function computeCorpusWatermark(input: {\n namespace: string;\n paths: readonly string[];\n baseDir: string;\n now?: Date;\n}): Promise<CorpusWatermark> {\n const { namespace, paths, baseDir } = input;\n\n const census = new Map<string, number>();\n let newestPartition: string | null = null;\n const parsedByPath: ParsedPath[] = [];\n for (const filePath of paths) {\n const parsed = parsePartition(baseDir, filePath);\n parsedByPath.push(parsed);\n census.set(parsed.bucket, (census.get(parsed.bucket) ?? 0) + 1);\n // Freshness is the newest ACTIVE (hot) write. Cold memories are demoted and\n // keep their original (older) partition date, so they never advance the\n // freshness probe and their files are never stat-ed below.\n if (parsed.tier === \"hot\" && parsed.date !== null && (newestPartition === null || parsed.date > newestPartition)) {\n newestPartition = parsed.date;\n }\n }\n\n const digest = digestPartitionCensus(census);\n\n let newestWriteAt: string | null = null;\n if (newestPartition !== null) {\n let maxMtimeMs = Number.NEGATIVE_INFINITY;\n for (let i = 0; i < paths.length; i += 1) {\n if (parsedByPath[i].tier !== \"hot\" || parsedByPath[i].date !== newestPartition) continue;\n try {\n const st = await stat(paths[i]);\n if (st.mtimeMs > maxMtimeMs) maxMtimeMs = st.mtimeMs;\n } catch (err) {\n // A file vanishing between scan and stat (ENOENT) is an expected race —\n // skip it. A backend read failure (EACCES/EIO/…) means the corpus is\n // unreadable: propagate so the namespace is omitted, not published with\n // a stale/null freshness (issue #2156 round-6).\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\n }\n }\n if (maxMtimeMs > Number.NEGATIVE_INFINITY) {\n newestWriteAt = new Date(maxMtimeMs).toISOString();\n }\n }\n\n // Stamp AFTER the (possibly slow) stat loop so an age-based HA consumer never\n // sees a timestamp earlier than when the scan actually finished (finding\n // round-4). An explicit `now` (tests / determinism) still wins.\n const computedAt = (input.now ?? new Date()).toISOString();\n\n return {\n namespace,\n memoryFileCount: paths.length,\n newestPartition,\n newestWriteAt,\n digest,\n computedAt,\n };\n}\n\n/** Minimal storage surface a watermark needs — satisfied by StorageManager. */\nexport interface CorpusStorage {\n readonly dir: string;\n /** Hot-tier active memory paths (the recall corpus). */\n collectActiveMemoryPaths(options?: { propagateReadErrors?: boolean }): Promise<string[]>;\n /** Cold-tier (demoted-but-reachable) memory paths (issue #2156 finding D). */\n collectColdMemoryPaths(options?: { propagateReadErrors?: boolean }): Promise<string[]>;\n /**\n * Optional combined hot+cold corpus-mutation sentinel. When present, the\n * census brackets its scan with it and retries on change so a tier migration\n * racing the walkers is not cached as a false divergence. Absent (test fakes)\n * ⇒ single-pass.\n */\n getCorpusScanVersion?(): string | Promise<string>;\n}\n\n/** Max census re-scans when a corpus mutation keeps racing the walkers. */\nconst CENSUS_RACE_MAX_ATTEMPTS = 3;\n\n/**\n * Compute one namespace's watermark from its storage, spanning BOTH tiers so a\n * cold-tier divergence is caught. Reads are STRICT: the collectors propagate a\n * backend read failure (EACCES on the root OR any nested category dir, ENOTDIR,\n * …) so an unreadable corpus is OMITTED by the caller rather than published as a\n * false empty census; a missing root (ENOENT — a not-yet-created namespace)\n * stays a genuine empty corpus. The hot+cold walk is bracketed by the\n * corpus-mutation sentinel (when the storage exposes one) and retried on change;\n * if it never stabilizes (sustained tier churn) this THROWS rather than caching\n * a transient double-count/miss as a false divergence.\n */\nasync function computeNamespaceWatermark(\n namespace: string,\n storage: CorpusStorage,\n now?: Date,\n): Promise<CorpusWatermark> {\n const readVersion = storage.getCorpusScanVersion?.bind(storage);\n const maxAttempts = readVersion ? CENSUS_RACE_MAX_ATTEMPTS : 1;\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n const before = readVersion ? await readVersion() : null;\n const [hotPaths, coldPaths] = await Promise.all([\n storage.collectActiveMemoryPaths({ propagateReadErrors: true }),\n storage.collectColdMemoryPaths({ propagateReadErrors: true }),\n ]);\n const result = await computeCorpusWatermark({ namespace, paths: [...hotPaths, ...coldPaths], baseDir: storage.dir, now });\n const after = readVersion ? await readVersion() : null;\n if (before === after) return result;\n // A tier write (e.g. hot→cold migration) raced the two walkers, so the\n // count/digest may double-count or miss the in-flight memory. Retry for a\n // consistent snapshot.\n }\n throw new Error(`corpus census for namespace \"${namespace}\" did not stabilize after ${maxAttempts} attempts`);\n}\n\n/**\n * Compute watermarks for a set of namespaces. A namespace whose storage is\n * unavailable (or unreadable) is skipped rather than failing the whole read, so\n * a single bad namespace never blanks the health/doctor payload.\n */\nexport async function computeCorpusWatermarks(\n namespaces: readonly string[],\n storageFor: (namespace: string) => CorpusStorage | Promise<CorpusStorage>,\n now?: Date,\n): Promise<CorpusWatermark[]> {\n const watermarks: CorpusWatermark[] = [];\n for (const namespace of namespaces) {\n try {\n watermarks.push(await computeNamespaceWatermark(namespace, await storageFor(namespace), now));\n } catch {\n // Storage unavailable/unreadable for this namespace — degrade gracefully.\n }\n }\n return watermarks;\n}\n\n/** A namespace and the memory root its corpus lives under. */\nexport interface CorpusNamespaceRoot {\n /** Representative namespace label for this root (default wins on collapse). */\n readonly namespace: string;\n readonly rootDir: string;\n /**\n * Every configured namespace name that resolves to `rootDir` (aliases that\n * collapsed onto one physical root, e.g. `global` + `shared` when namespaces\n * are disabled). `/health` filters visibility against ALL of these so a token\n * scoped to a non-representative alias still sees the shared corpus (issue\n * #2156 round-10). Always includes `namespace`.\n */\n readonly namespaces: readonly string[];\n}\n\n/**\n * Resolve the namespace set the corpus census covers, shared by the `/health`\n * builder AND the `remnic doctor` corpus check (issue #2156 finding C) so the\n * two surfaces enumerate the SAME tenants — and, critically, resolve each\n * namespace's ROOT the SAME way — and cannot drift. Enumeration is purely\n * config-driven (`listNamespaces({ config })`): it discovers the default,\n * shared, policy, and on-disk `namespaces/<ns>` tenants and resolves every root\n * through the namespace storage router, exactly as the live daemon's\n * `getStorage(namespace)` does. It still works when the namespace catalog is\n * opted out. The catalog is deliberately NOT unioned in: a catalog-registered\n * tenant with no directory has an empty corpus (nothing to fingerprint), and\n * trusting `catalog.storageDir` here could make the doctor scan a different root\n * than `/health` for the same namespace (issue #2156 round-8). Deduped by\n * resolved root so a namespaces-disabled / flat-root deployment reports its\n * single shared corpus once, under the default-namespace label.\n */\nexport async function resolveCorpusNamespaceRoots(options: {\n config: PluginConfig;\n propagateDiscoveryErrors?: boolean;\n}): Promise<CorpusNamespaceRoot[]> {\n const { config } = options;\n const configDriven = await listNamespaces({\n config,\n propagateDiscoveryErrors: options.propagateDiscoveryErrors,\n });\n // Default namespace first so it wins the representative label when several\n // configured names collapse onto one root (namespaces disabled → memoryDir).\n const ordered = [\n ...configDriven.filter((entry) => entry.namespace === config.defaultNamespace),\n ...configDriven.filter((entry) => entry.namespace !== config.defaultNamespace),\n ];\n const seenNamespaces = new Set<string>();\n const byRoot = new Map<string, { namespace: string; rootDir: string; namespaces: string[] }>();\n for (const entry of ordered) {\n if (seenNamespaces.has(entry.namespace)) continue;\n seenNamespaces.add(entry.namespace);\n const existing = byRoot.get(entry.rootDir);\n if (existing) {\n existing.namespaces.push(entry.namespace); // an alias collapsing onto a seen root\n } else {\n byRoot.set(entry.rootDir, { namespace: entry.namespace, rootDir: entry.rootDir, namespaces: [entry.namespace] });\n }\n }\n const roots: CorpusNamespaceRoot[] = [...byRoot.values()];\n if (roots.length === 0) {\n roots.push({ namespace: config.defaultNamespace, rootDir: config.memoryDir, namespaces: [config.defaultNamespace] });\n }\n return roots;\n}\n\n/**\n * Divergence detection tolerates a watermark that is stale by up to a minute\n * (peers are compared on a slow cadence); a health/readiness probe does NOT\n * tolerate a full corpus scan (a recursive realpath-per-Markdown-file walk) on\n * every request. This TTL is that trade-off: recompute a namespace's watermark\n * at most once per window; probes in between serve the cached census (whose\n * `computedAt` shows its staleness).\n */\nexport const WATERMARK_CACHE_TTL_MS = 60_000;\n\n/**\n * Cap on concurrent background corpus scans. A probe that finds many namespaces\n * cold/expired would otherwise fan out one recursive hot/cold walk PER namespace\n * at once, a periodic unbounded filesystem-work burst that can starve recall and\n * storage (issue #2156 round-9). Excess refreshes queue and start as slots free;\n * single-flight still dedupes duplicate requests for the same namespace.\n */\nconst MAX_CONCURRENT_WATERMARK_REFRESHES = 4;\n\n/**\n * Instance-scoped (AGENTS.md pattern 5 — never a bare module global)\n * stale-while-revalidate cache in front of the per-namespace corpus scan.\n * {@link get} NEVER awaits the scan: it returns the freshest already-computed\n * watermark (stale allowed, or undefined for a cold namespace) and triggers a\n * single-flight background refresh when the entry is missing or expired. So a\n * `/health` probe is always O(1) — it never blocks on a recursive corpus walk,\n * even on the first request after startup or after a TTL expiry. Owned by the\n * long-lived access service instance.\n */\nexport class CorpusWatermarkCache {\n private readonly entries = new Map<string, { value: CorpusWatermark; expiresAt: number }>();\n private readonly inFlight = new Map<string, Promise<void>>();\n private readonly ttlMs: number;\n private readonly clock: () => number;\n private rootsEntry: { value: CorpusNamespaceRoot[]; expiresAt: number } | undefined;\n private rootsInFlight: Promise<void> | undefined;\n private rootsRefreshError: unknown;\n private readonly queued = new Map<string, () => void>();\n private activeRefreshes = 0;\n private readonly maxConcurrentRefreshes: number;\n\n constructor(options: { ttlMs?: number; clock?: () => number; maxConcurrentRefreshes?: number } = {}) {\n this.ttlMs = options.ttlMs ?? WATERMARK_CACHE_TTL_MS;\n this.clock = options.clock ?? Date.now;\n this.maxConcurrentRefreshes = Math.max(1, options.maxConcurrentRefreshes ?? MAX_CONCURRENT_WATERMARK_REFRESHES);\n }\n\n get(namespace: string, compute: () => Promise<CorpusWatermark>): CorpusWatermark | undefined {\n const cached = this.entries.get(namespace);\n const fresh = cached !== undefined && this.clock() < cached.expiresAt;\n if (!fresh) this.refresh(namespace, compute);\n return cached?.value;\n }\n\n private refresh(namespace: string, compute: () => Promise<CorpusWatermark>): void {\n // Single-flight + not-already-queued: one background scan per namespace.\n if (this.inFlight.has(namespace) || this.queued.has(namespace)) return;\n const run = (): void => {\n this.activeRefreshes += 1;\n const pending = compute()\n .then((value) => {\n this.entries.set(namespace, { value, expiresAt: this.clock() + this.ttlMs });\n })\n .catch(() => {\n // Failed/unreadable scan: never cache it; keep serving any stale value.\n })\n .finally(() => {\n this.inFlight.delete(namespace);\n this.activeRefreshes -= 1;\n this.drainQueue();\n });\n this.inFlight.set(namespace, pending);\n };\n // Bound the concurrent fan-out: run now if a slot is free, else queue.\n if (this.activeRefreshes < this.maxConcurrentRefreshes) {\n run();\n } else {\n this.queued.set(namespace, run);\n }\n }\n\n private drainQueue(): void {\n while (this.activeRefreshes < this.maxConcurrentRefreshes && this.queued.size > 0) {\n const next = this.queued.entries().next().value;\n if (!next) break;\n const [namespace, run] = next;\n this.queued.delete(namespace);\n run();\n }\n }\n\n\n /**\n * Stale-while-revalidate the resolved namespace roots (issue #2156 round-7).\n * Namespace enumeration (config scan + persisted-catalog parse) is O(tenants)\n * filesystem work; without this it ran on every probe ahead of the watermark\n * cache. NEVER awaits: returns the cached/stale roots (or undefined when cold)\n * and single-flights a background refresh.\n */\n getResolvedRoots(compute: () => Promise<CorpusNamespaceRoot[]>): CorpusNamespaceRoot[] | undefined {\n const fresh = this.rootsEntry !== undefined && this.clock() < this.rootsEntry.expiresAt;\n if (!fresh && this.rootsInFlight === undefined) {\n this.rootsInFlight = compute()\n .then((value) => {\n this.rootsEntry = { value, expiresAt: this.clock() + this.ttlMs };\n this.rootsRefreshError = undefined;\n })\n .catch((error: unknown) => {\n this.rootsRefreshError = error;\n // Keep serving any stale roots; callers that require completeness\n // inspect the refresh status before trusting them.\n })\n .finally(() => {\n this.rootsInFlight = undefined;\n });\n }\n return this.rootsEntry?.value;\n }\n\n getResolvedRootsStatus(compute: () => Promise<CorpusNamespaceRoot[]>): {\n roots: CorpusNamespaceRoot[] | undefined;\n refreshError: unknown;\n } {\n const roots = this.getResolvedRoots(compute);\n return { roots, refreshError: this.rootsRefreshError };\n }\n /** Await every background refresh — in-flight AND queued (issue #2156 round-9) —\n * plus any roots refresh (shutdown / deterministic tests). */\n async whenIdle(): Promise<void> {\n while (this.inFlight.size > 0 || this.rootsInFlight !== undefined || this.queued.size > 0) {\n const pending: Promise<unknown>[] = [...this.inFlight.values()];\n if (this.rootsInFlight) pending.push(this.rootsInFlight);\n if (pending.length === 0) {\n this.drainQueue(); // queued work with no running peer — start it\n await Promise.resolve();\n continue;\n }\n await Promise.all(pending);\n }\n }\n}\n\n/** Orchestrator surface the service watermark builder reads — satisfied by Orchestrator. */\nexport interface CorpusWatermarkHost {\n config: PluginConfig;\n getStorage(namespace: string): CorpusStorage | Promise<CorpusStorage>;\n}\n\nexport interface ServiceCorpusWatermarkOptions {\n /** Stale-while-revalidate cache so probes never await the corpus scan. */\n cache?: CorpusWatermarkCache;\n /**\n * Presenting token capabilities. When the token is namespace-restricted the\n * fleet view is filtered to the namespaces it may access (issue #2156 finding\n * B); an unrestricted/legacy token keeps the full fleet view.\n */\n caps?: TokenCapabilities | null;\n now?: Date;\n}\n\n/**\n * Build the per-namespace watermark array served on `/health`. Namespaces are\n * resolved through {@link resolveCorpusNamespaceRoots} (shared with the doctor),\n * filtered to the caller's capabilities, then served through the (optional)\n * stale-while-revalidate cache so a probe never blocks on a scan. Degrades to an\n * empty array — never throws — if enumeration is unavailable; an individual\n * namespace whose scan fails is omitted, not allowed to blank the payload.\n */\nexport async function computeServiceCorpusWatermarks(\n host: CorpusWatermarkHost,\n options: ServiceCorpusWatermarkOptions = {},\n): Promise<CorpusWatermark[]> {\n return (await computeServiceCorpusCensus(host, options)).watermarks;\n}\n\n/**\n * Watermarks PLUS whether every configured namespace made it into the set.\n *\n * Completeness is returned ATOMICALLY with the data rather than stashed in a\n * side channel: every early exit here (enumeration threw, roots still warming,\n * a per-namespace scan failed, a cache entry not yet populated) yields an\n * incomplete set, and a caller that certifies convergence against a partial\n * census would hide the divergence of whatever was dropped (issue #2149).\n */\nexport async function computeServiceCorpusCensus(\n host: CorpusWatermarkHost,\n options: ServiceCorpusWatermarkOptions = {},\n): Promise<{ watermarks: CorpusWatermark[]; complete: boolean }> {\n let roots: CorpusNamespaceRoot[] | undefined;\n if (options.cache) {\n const { roots: cachedRoots, refreshError } = options.cache.getResolvedRootsStatus(() =>\n resolveCorpusNamespaceRoots({ config: host.config, propagateDiscoveryErrors: true }),\n );\n if (refreshError !== undefined) return { watermarks: [], complete: false };\n roots = cachedRoots;\n } else {\n try {\n roots = await resolveCorpusNamespaceRoots({\n config: host.config,\n propagateDiscoveryErrors: true,\n });\n } catch {\n return { watermarks: [], complete: false }; // never enumerated — not an empty corpus\n }\n }\n // Enumeration still warming (cold): served on a later probe, and NOT complete.\n if (!roots) return { watermarks: [], complete: false };\n const visible = roots.filter((root) => root.namespaces.some((ns) => capabilityAllowsNamespace(options.caps, ns)));\n const watermarks: CorpusWatermark[] = [];\n let complete = true;\n for (const { namespace } of visible) {\n const compute = async (): Promise<CorpusWatermark> =>\n computeNamespaceWatermark(namespace, await host.getStorage(namespace), options.now);\n if (options.cache) {\n const cached = options.cache.get(namespace, compute);\n if (cached) watermarks.push(cached);\n else complete = false; // still warming — the set is not the full census yet\n } else {\n try {\n watermarks.push(await compute());\n } catch {\n // One tenant's storage/scan failed — omit just that namespace.\n complete = false;\n }\n }\n }\n return { watermarks, complete };\n}\n\n\n"],"mappings":"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","names":[]}
|