@remnic/core 10.0.0 → 10.0.2
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 +46 -43
- package/dist/access-authorization-probe.js +47 -44
- package/dist/access-boundary.js +46 -43
- package/dist/access-cli.js +111 -108
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +46 -43
- package/dist/access-http-briefing.js +47 -44
- package/dist/access-http-query.js +48 -45
- package/dist/access-http.js +66 -63
- package/dist/access-identity-continuity-surface.js +46 -43
- package/dist/access-lcm-surface.js +46 -43
- package/dist/access-mcp.js +55 -52
- package/dist/access-namespace-preflight.js +46 -43
- package/dist/access-observe-write-category.js +9 -9
- package/dist/access-observe-write-surface.js +46 -43
- package/dist/access-offline-manifest.js +6 -6
- package/dist/access-operations-batch.js +47 -44
- package/dist/access-operations.js +54 -51
- package/dist/access-recall-concurrency.js +46 -43
- package/dist/access-recall-response.js +46 -43
- package/dist/access-recall-surface.js +46 -43
- package/dist/access-schema.js +8 -8
- package/dist/access-service.js +46 -43
- package/dist/access-surface-catalog.js +48 -45
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +18 -16
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/briefing.js +12 -10
- package/dist/buffer.js +7 -5
- package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/causal-consolidation.js +13 -11
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-JWFTPRYD.js → chunk-2EZB2DL5.js} +2 -2
- package/dist/{chunk-J3ZJBX7I.js → chunk-2J3M2KV4.js} +2 -2
- package/dist/{chunk-BXLOS5AJ.js → chunk-2NLLXCJG.js} +2 -2
- package/dist/{chunk-SWXPR5SE.js → chunk-2SYRHFUG.js} +2 -2
- package/dist/{chunk-QKCGOECM.js → chunk-34G4U6HS.js} +2 -2
- package/dist/{chunk-QX3K3INR.js → chunk-36BUOTVN.js} +2 -2
- package/dist/{chunk-CNBGAFDY.js → chunk-3J54RR3A.js} +2 -2
- package/dist/{chunk-XLWMNDQR.js → chunk-4A2UMSNB.js} +7 -5
- package/dist/chunk-4A2UMSNB.js.map +1 -0
- package/dist/{chunk-OWV6LKNZ.js → chunk-4G2COUSD.js} +2 -2
- package/dist/{chunk-HWGFTTPS.js → chunk-5FLFPTDK.js} +2 -2
- package/dist/{chunk-WR66CTVU.js → chunk-7IGNDEUJ.js} +4 -4
- package/dist/chunk-7PZHH4KC.js +63 -0
- package/dist/chunk-7PZHH4KC.js.map +1 -0
- package/dist/{chunk-VO2BNAA2.js → chunk-A5N6ZEUW.js} +2 -2
- package/dist/{chunk-XQWKB6PY.js → chunk-AALVXJJ7.js} +2 -2
- package/dist/{chunk-KFHEZJQZ.js → chunk-BAREPCLY.js} +2 -2
- package/dist/{chunk-QAIVTLL2.js → chunk-BLHPADUX.js} +2 -2
- package/dist/{chunk-2TCNEJ36.js → chunk-BNMI3JBV.js} +3 -3
- package/dist/{chunk-QUNCLKHM.js → chunk-CAXMG4JI.js} +2 -2
- package/dist/{chunk-KV7LFOPF.js → chunk-CQBFEGLF.js} +2 -2
- package/dist/{chunk-KMN2JE4D.js → chunk-CVQWNACA.js} +2 -2
- package/dist/{chunk-VV2MVHCJ.js → chunk-DAVONFNK.js} +2 -2
- package/dist/{chunk-KYUVFCT2.js → chunk-DQQ4IM5L.js} +2 -2
- package/dist/{chunk-B454SHPU.js → chunk-E7FN5N5E.js} +2 -2
- package/dist/{chunk-CGBE3N7T.js → chunk-EB3RXN2L.js} +2 -2
- package/dist/{chunk-AWSP3WNM.js → chunk-EJJMDLVB.js} +2 -2
- package/dist/{chunk-A6WNTVA7.js → chunk-FNVSBUC3.js} +2 -2
- package/dist/{chunk-XTN54NML.js → chunk-G3T6YMBO.js} +4 -4
- package/dist/{chunk-BKVCTTEN.js → chunk-GUD6FYQG.js} +2 -2
- package/dist/{chunk-2MMBJE53.js → chunk-H5YACCE3.js} +7 -7
- package/dist/{chunk-JC7QQ2DJ.js → chunk-HRZZJ4XB.js} +2 -2
- package/dist/{chunk-LHDSCWDK.js → chunk-I2VQ7WBJ.js} +16 -16
- package/dist/{chunk-B3OFQP5Y.js → chunk-IB63OVUB.js} +24 -249
- package/dist/chunk-IB63OVUB.js.map +1 -0
- package/dist/chunk-IM2GO42H.js +312 -0
- package/dist/chunk-IM2GO42H.js.map +1 -0
- package/dist/{chunk-PRNR47DI.js → chunk-IR5LZCE6.js} +2 -2
- package/dist/{chunk-YI6XIFQV.js → chunk-J5CUQRST.js} +2 -2
- package/dist/{chunk-6WRHNA4I.js → chunk-JFDKPW54.js} +35 -35
- package/dist/{chunk-VYIBRCVW.js → chunk-JOBE47EJ.js} +2 -2
- package/dist/{chunk-SFDBLCZV.js → chunk-K4TWTUEY.js} +41 -41
- package/dist/{chunk-UHESJ6TQ.js → chunk-KEGUXZNY.js} +3 -3
- package/dist/{chunk-UHESJ6TQ.js.map → chunk-KEGUXZNY.js.map} +1 -1
- package/dist/{chunk-T6RQNV7E.js → chunk-KHOINMCN.js} +2 -2
- package/dist/{chunk-VDX2J7OX.js → chunk-KQAFEZQX.js} +2 -2
- package/dist/{chunk-E4XLFYHV.js → chunk-LHOIYLKH.js} +2 -2
- package/dist/chunk-LHOIYLKH.js.map +1 -0
- package/dist/{chunk-PQKZLBCJ.js → chunk-LMZV3H4W.js} +127 -6
- package/dist/chunk-LMZV3H4W.js.map +1 -0
- package/dist/{chunk-MZOQ26LH.js → chunk-LQGX5TCI.js} +2 -2
- package/dist/{chunk-TRN56SPQ.js → chunk-M2OPC4BD.js} +2 -2
- package/dist/{chunk-LXJZUNLX.js → chunk-NBK24POV.js} +2 -2
- package/dist/{chunk-7X7A7KVN.js → chunk-NLYOOGZ7.js} +2 -2
- package/dist/{chunk-G5I5ZT6O.js → chunk-OSDAF7HH.js} +2 -2
- package/dist/{chunk-EZ3F4YIN.js → chunk-OV7EYM6M.js} +6 -6
- package/dist/{chunk-AU43D3IT.js → chunk-P57VR7AI.js} +4 -4
- package/dist/{chunk-NT2Q6AHC.js → chunk-PCO3P7WJ.js} +2 -2
- package/dist/{chunk-73R4HOOK.js → chunk-PT6DQK5T.js} +3 -3
- package/dist/{chunk-O43AAJFQ.js → chunk-PTGPU4DC.js} +2 -2
- package/dist/{chunk-STQC2FYQ.js → chunk-PWVBVIOP.js} +6 -6
- package/dist/{chunk-TFA6HPOA.js → chunk-QCP75JGO.js} +5 -5
- package/dist/{chunk-SIGDETMB.js → chunk-R4NGLTUS.js} +2 -2
- package/dist/{chunk-S2GR3T7W.js → chunk-RNYVAQX2.js} +2 -2
- package/dist/{chunk-PZ2QQULY.js → chunk-RU744TNX.js} +8 -8
- package/dist/{chunk-2OFDC3AK.js → chunk-RVQVWDSN.js} +2 -2
- package/dist/{chunk-ZLQ37WJX.js → chunk-SIGAGKNE.js} +8 -2
- package/dist/{chunk-ZLQ37WJX.js.map → chunk-SIGAGKNE.js.map} +1 -1
- package/dist/{chunk-OFGTRDWX.js → chunk-SIYZDT2V.js} +3 -3
- package/dist/{chunk-PQ7EB5AD.js → chunk-SQPC6MUM.js} +3 -3
- package/dist/{chunk-5R4YPRAR.js → chunk-TGF43K66.js} +18 -18
- package/dist/chunk-TPSLVZHS.js +102 -0
- package/dist/chunk-TPSLVZHS.js.map +1 -0
- package/dist/{chunk-ZRUC7FD3.js → chunk-U4SG4HKQ.js} +2 -2
- package/dist/{chunk-ATFFWIKL.js → chunk-U6HWKOGD.js} +2 -2
- package/dist/{chunk-IOMLGJHG.js → chunk-UK5EH7AI.js} +2 -2
- package/dist/{chunk-DB6XFMOF.js → chunk-VC4J4UTI.js} +5 -5
- package/dist/{chunk-T32YYUT2.js → chunk-VQTPEL7O.js} +2 -2
- package/dist/{chunk-7YQR6XHY.js → chunk-W6K6BC3H.js} +57 -57
- package/dist/{chunk-FYX2LXIX.js → chunk-WI37MWRZ.js} +2 -2
- package/dist/{chunk-7NBUE5VY.js → chunk-WPMDNCGB.js} +2 -2
- package/dist/{chunk-GZ2KV2RO.js → chunk-WWLAKRMX.js} +4 -4
- package/dist/{chunk-EWWK65CI.js → chunk-WY4IV62O.js} +2 -2
- package/dist/{chunk-DQEMWVMT.js → chunk-X7Y7WX73.js} +1 -1
- package/dist/{chunk-GQ6ZF4LD.js → chunk-XG4XXI66.js} +2 -2
- package/dist/{chunk-Z6EZCN6Z.js → chunk-Y7427SL7.js} +10 -10
- package/dist/{chunk-TXTRHF3B.js → chunk-YAPTJQI7.js} +2 -2
- package/dist/{chunk-JRDL3FGM.js → chunk-YFET6A4I.js} +3 -3
- package/dist/{chunk-ISX7GVZE.js → chunk-YRULQW5J.js} +4 -4
- package/dist/{chunk-VDVHIRCK.js → chunk-YVHNBOK2.js} +2 -2
- package/dist/cli.js +85 -82
- package/dist/coding/export-okf-codegraph.js +11 -9
- package/dist/coding/export-okf-codegraph.js.map +1 -1
- package/dist/compounding/engine.js +12 -10
- package/dist/config-qmd.js +1 -1
- package/dist/config.js +18 -16
- package/dist/connectors/codex-materialize-runner.js +12 -10
- package/dist/connectors/index.js +12 -10
- package/dist/convergence-refresh.js +14 -12
- package/dist/conversation-index/backend.js +2 -2
- package/dist/corpus-watermark.js +14 -12
- package/dist/deep-recall-http-glue.js +9 -9
- package/dist/deep-recall.js +2 -2
- package/dist/entity-retrieval.js +12 -10
- package/dist/external-wiki-access.js +47 -44
- package/dist/extraction.js +16 -14
- package/dist/harmonic-retrieval.d.ts.map +1 -1
- package/dist/harmonic-retrieval.js +91 -1
- package/dist/index.js +136 -133
- package/dist/lcm/engine.js +4 -4
- package/dist/lcm/index.js +10 -10
- package/dist/location/index.js +14 -12
- package/dist/maintenance/memory-governance.js +12 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +12 -10
- package/dist/maintenance/rebuild-memory-projection.d.ts.map +1 -1
- package/dist/maintenance/rebuild-memory-projection.js +13 -11
- package/dist/memory-browse-operations.js +47 -44
- package/dist/memory-browse.js +7 -5
- package/dist/memory-projection-batch.d.ts +20 -0
- package/dist/memory-projection-batch.d.ts.map +1 -0
- package/dist/memory-projection-batch.js +11 -0
- package/dist/memory-projection-batch.js.map +1 -0
- package/dist/namespaces/migrate.js +13 -11
- package/dist/namespaces/search.js +10 -8
- package/dist/namespaces/storage.js +12 -10
- package/dist/offline-sync-changeset-generations.js +3 -3
- package/dist/offline-sync-embedding-generation.js +2 -2
- package/dist/offline-sync-snapshot-stream.js +6 -6
- package/dist/offline-sync.js +5 -5
- package/dist/operator-doctor-corpus.js +15 -13
- package/dist/operator-toolkit.js +27 -25
- package/dist/orchestration/maintenance.js +20 -18
- package/dist/orchestrator.js +111 -108
- package/dist/qmd-daemon-session.d.ts +45 -0
- package/dist/qmd-daemon-session.d.ts.map +1 -0
- package/dist/qmd-daemon-session.js +12 -0
- package/dist/qmd-daemon-session.js.map +1 -0
- package/dist/qmd-mcp-cancel.d.ts +21 -0
- package/dist/qmd-mcp-cancel.d.ts.map +1 -0
- package/dist/qmd-mcp-cancel.js +11 -0
- package/dist/qmd-mcp-cancel.js.map +1 -0
- package/dist/qmd-preflight.d.ts +3 -1
- package/dist/qmd-preflight.d.ts.map +1 -1
- package/dist/qmd-preflight.js +3 -1
- package/dist/qmd.d.ts +5 -29
- package/dist/qmd.d.ts.map +1 -1
- package/dist/qmd.js +6 -2
- package/dist/recall-navigation-operations.js +47 -44
- package/dist/recall-navigation.js +46 -43
- package/dist/recall-state-view-admission.js +7 -5
- package/dist/recall-why-service.js +7 -5
- package/dist/resume-bundles.js +19 -17
- package/dist/search/factory.js +9 -7
- package/dist/search/index.js +13 -11
- package/dist/semantic-consolidation.js +13 -11
- package/dist/semantic-rule-promotion.js +12 -10
- package/dist/semantic-rule-verifier.js +12 -10
- package/dist/source-agent-qualifier.js +15 -13
- package/dist/standup/build.js +12 -10
- package/dist/storage.d.ts +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +11 -9
- package/dist/support-passport/index.js +49 -46
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/export-okf.js +12 -10
- package/dist/verified-recall.js +12 -10
- package/dist/who-knows-cli.js +13 -11
- package/dist/who-knows.js +12 -10
- package/package.json +2 -2
- package/src/config-qmd.ts +2 -2
- package/src/harmonic-retrieval-index.test.ts +387 -0
- package/src/harmonic-retrieval.ts +181 -15
- package/src/maintenance/rebuild-memory-projection.ts +3 -0
- package/src/memory-projection-batch.ts +82 -0
- package/src/qmd-daemon-session.ts +369 -0
- package/src/qmd-mcp-cancel.test.ts +408 -0
- package/src/qmd-mcp-cancel.ts +130 -0
- package/src/qmd-preflight.ts +10 -1
- package/src/qmd-rank-compare.test.ts +100 -0
- package/src/qmd.test.ts +16 -1
- package/src/qmd.ts +18 -297
- package/src/storage.ts +1 -1
- package/dist/chunk-B3OFQP5Y.js.map +0 -1
- package/dist/chunk-E4XLFYHV.js.map +0 -1
- package/dist/chunk-PQKZLBCJ.js.map +0 -1
- package/dist/chunk-XLWMNDQR.js.map +0 -1
- /package/dist/{capsule-crypto-YO5QJ6L3.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
- /package/dist/{chunk-JWFTPRYD.js.map → chunk-2EZB2DL5.js.map} +0 -0
- /package/dist/{chunk-J3ZJBX7I.js.map → chunk-2J3M2KV4.js.map} +0 -0
- /package/dist/{chunk-BXLOS5AJ.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /package/dist/{chunk-SWXPR5SE.js.map → chunk-2SYRHFUG.js.map} +0 -0
- /package/dist/{chunk-QKCGOECM.js.map → chunk-34G4U6HS.js.map} +0 -0
- /package/dist/{chunk-QX3K3INR.js.map → chunk-36BUOTVN.js.map} +0 -0
- /package/dist/{chunk-CNBGAFDY.js.map → chunk-3J54RR3A.js.map} +0 -0
- /package/dist/{chunk-OWV6LKNZ.js.map → chunk-4G2COUSD.js.map} +0 -0
- /package/dist/{chunk-HWGFTTPS.js.map → chunk-5FLFPTDK.js.map} +0 -0
- /package/dist/{chunk-WR66CTVU.js.map → chunk-7IGNDEUJ.js.map} +0 -0
- /package/dist/{chunk-VO2BNAA2.js.map → chunk-A5N6ZEUW.js.map} +0 -0
- /package/dist/{chunk-XQWKB6PY.js.map → chunk-AALVXJJ7.js.map} +0 -0
- /package/dist/{chunk-KFHEZJQZ.js.map → chunk-BAREPCLY.js.map} +0 -0
- /package/dist/{chunk-QAIVTLL2.js.map → chunk-BLHPADUX.js.map} +0 -0
- /package/dist/{chunk-2TCNEJ36.js.map → chunk-BNMI3JBV.js.map} +0 -0
- /package/dist/{chunk-QUNCLKHM.js.map → chunk-CAXMG4JI.js.map} +0 -0
- /package/dist/{chunk-KV7LFOPF.js.map → chunk-CQBFEGLF.js.map} +0 -0
- /package/dist/{chunk-KMN2JE4D.js.map → chunk-CVQWNACA.js.map} +0 -0
- /package/dist/{chunk-VV2MVHCJ.js.map → chunk-DAVONFNK.js.map} +0 -0
- /package/dist/{chunk-KYUVFCT2.js.map → chunk-DQQ4IM5L.js.map} +0 -0
- /package/dist/{chunk-B454SHPU.js.map → chunk-E7FN5N5E.js.map} +0 -0
- /package/dist/{chunk-CGBE3N7T.js.map → chunk-EB3RXN2L.js.map} +0 -0
- /package/dist/{chunk-AWSP3WNM.js.map → chunk-EJJMDLVB.js.map} +0 -0
- /package/dist/{chunk-A6WNTVA7.js.map → chunk-FNVSBUC3.js.map} +0 -0
- /package/dist/{chunk-XTN54NML.js.map → chunk-G3T6YMBO.js.map} +0 -0
- /package/dist/{chunk-BKVCTTEN.js.map → chunk-GUD6FYQG.js.map} +0 -0
- /package/dist/{chunk-2MMBJE53.js.map → chunk-H5YACCE3.js.map} +0 -0
- /package/dist/{chunk-JC7QQ2DJ.js.map → chunk-HRZZJ4XB.js.map} +0 -0
- /package/dist/{chunk-LHDSCWDK.js.map → chunk-I2VQ7WBJ.js.map} +0 -0
- /package/dist/{chunk-PRNR47DI.js.map → chunk-IR5LZCE6.js.map} +0 -0
- /package/dist/{chunk-YI6XIFQV.js.map → chunk-J5CUQRST.js.map} +0 -0
- /package/dist/{chunk-6WRHNA4I.js.map → chunk-JFDKPW54.js.map} +0 -0
- /package/dist/{chunk-VYIBRCVW.js.map → chunk-JOBE47EJ.js.map} +0 -0
- /package/dist/{chunk-SFDBLCZV.js.map → chunk-K4TWTUEY.js.map} +0 -0
- /package/dist/{chunk-T6RQNV7E.js.map → chunk-KHOINMCN.js.map} +0 -0
- /package/dist/{chunk-VDX2J7OX.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-MZOQ26LH.js.map → chunk-LQGX5TCI.js.map} +0 -0
- /package/dist/{chunk-TRN56SPQ.js.map → chunk-M2OPC4BD.js.map} +0 -0
- /package/dist/{chunk-LXJZUNLX.js.map → chunk-NBK24POV.js.map} +0 -0
- /package/dist/{chunk-7X7A7KVN.js.map → chunk-NLYOOGZ7.js.map} +0 -0
- /package/dist/{chunk-G5I5ZT6O.js.map → chunk-OSDAF7HH.js.map} +0 -0
- /package/dist/{chunk-EZ3F4YIN.js.map → chunk-OV7EYM6M.js.map} +0 -0
- /package/dist/{chunk-AU43D3IT.js.map → chunk-P57VR7AI.js.map} +0 -0
- /package/dist/{chunk-NT2Q6AHC.js.map → chunk-PCO3P7WJ.js.map} +0 -0
- /package/dist/{chunk-73R4HOOK.js.map → chunk-PT6DQK5T.js.map} +0 -0
- /package/dist/{chunk-O43AAJFQ.js.map → chunk-PTGPU4DC.js.map} +0 -0
- /package/dist/{chunk-STQC2FYQ.js.map → chunk-PWVBVIOP.js.map} +0 -0
- /package/dist/{chunk-TFA6HPOA.js.map → chunk-QCP75JGO.js.map} +0 -0
- /package/dist/{chunk-SIGDETMB.js.map → chunk-R4NGLTUS.js.map} +0 -0
- /package/dist/{chunk-S2GR3T7W.js.map → chunk-RNYVAQX2.js.map} +0 -0
- /package/dist/{chunk-PZ2QQULY.js.map → chunk-RU744TNX.js.map} +0 -0
- /package/dist/{chunk-2OFDC3AK.js.map → chunk-RVQVWDSN.js.map} +0 -0
- /package/dist/{chunk-OFGTRDWX.js.map → chunk-SIYZDT2V.js.map} +0 -0
- /package/dist/{chunk-PQ7EB5AD.js.map → chunk-SQPC6MUM.js.map} +0 -0
- /package/dist/{chunk-5R4YPRAR.js.map → chunk-TGF43K66.js.map} +0 -0
- /package/dist/{chunk-ZRUC7FD3.js.map → chunk-U4SG4HKQ.js.map} +0 -0
- /package/dist/{chunk-ATFFWIKL.js.map → chunk-U6HWKOGD.js.map} +0 -0
- /package/dist/{chunk-IOMLGJHG.js.map → chunk-UK5EH7AI.js.map} +0 -0
- /package/dist/{chunk-DB6XFMOF.js.map → chunk-VC4J4UTI.js.map} +0 -0
- /package/dist/{chunk-T32YYUT2.js.map → chunk-VQTPEL7O.js.map} +0 -0
- /package/dist/{chunk-7YQR6XHY.js.map → chunk-W6K6BC3H.js.map} +0 -0
- /package/dist/{chunk-FYX2LXIX.js.map → chunk-WI37MWRZ.js.map} +0 -0
- /package/dist/{chunk-7NBUE5VY.js.map → chunk-WPMDNCGB.js.map} +0 -0
- /package/dist/{chunk-GZ2KV2RO.js.map → chunk-WWLAKRMX.js.map} +0 -0
- /package/dist/{chunk-EWWK65CI.js.map → chunk-WY4IV62O.js.map} +0 -0
- /package/dist/{chunk-DQEMWVMT.js.map → chunk-X7Y7WX73.js.map} +0 -0
- /package/dist/{chunk-GQ6ZF4LD.js.map → chunk-XG4XXI66.js.map} +0 -0
- /package/dist/{chunk-Z6EZCN6Z.js.map → chunk-Y7427SL7.js.map} +0 -0
- /package/dist/{chunk-TXTRHF3B.js.map → chunk-YAPTJQI7.js.map} +0 -0
- /package/dist/{chunk-JRDL3FGM.js.map → chunk-YFET6A4I.js.map} +0 -0
- /package/dist/{chunk-ISX7GVZE.js.map → chunk-YRULQW5J.js.map} +0 -0
- /package/dist/{chunk-VDVHIRCK.js.map → chunk-YVHNBOK2.js.map} +0 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
import { abortError } from "./abort-error.js";
|
|
6
|
+
import { QmdDaemonSession } from "./qmd-daemon-session.js";
|
|
7
|
+
import { createInflightJoiner, rankDocumentsUntilAbortParallel } from "./qmd-mcp-cancel.js";
|
|
8
|
+
import { QmdClient } from "./qmd.js";
|
|
9
|
+
import type { CommandChildProcess } from "./runtime/child-process.js";
|
|
10
|
+
import type { QmdSearchResult } from "./types.js";
|
|
11
|
+
|
|
12
|
+
function delay(ms: number): Promise<void> {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test("in-flight joiner runs identical work once", async () => {
|
|
17
|
+
const joiner = createInflightJoiner<string>();
|
|
18
|
+
let runs = 0;
|
|
19
|
+
const run = () => {
|
|
20
|
+
runs += 1;
|
|
21
|
+
return delay(20).then(() => "shared");
|
|
22
|
+
};
|
|
23
|
+
const [a, b] = await Promise.all([joiner.join("same", undefined, run), joiner.join("same", undefined, run)]);
|
|
24
|
+
assert.equal(a, "shared");
|
|
25
|
+
assert.equal(b, "shared");
|
|
26
|
+
assert.equal(runs, 1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("aborting one of two waiters does not cancel the shared call", async () => {
|
|
30
|
+
const joiner = createInflightJoiner<string>();
|
|
31
|
+
let runs = 0;
|
|
32
|
+
let sharedAborted = false;
|
|
33
|
+
const run = (signal: AbortSignal) => {
|
|
34
|
+
runs += 1;
|
|
35
|
+
signal.addEventListener("abort", () => {
|
|
36
|
+
sharedAborted = true;
|
|
37
|
+
});
|
|
38
|
+
return delay(30).then(() => "kept");
|
|
39
|
+
};
|
|
40
|
+
const ac = new AbortController();
|
|
41
|
+
const leaving = joiner.join("same", ac.signal, run);
|
|
42
|
+
const staying = joiner.join("same", undefined, run);
|
|
43
|
+
await delay(5);
|
|
44
|
+
ac.abort();
|
|
45
|
+
await assert.rejects(leaving, (err: Error) => err.name === "AbortError");
|
|
46
|
+
assert.equal(await staying, "kept");
|
|
47
|
+
assert.equal(runs, 1);
|
|
48
|
+
assert.equal(sharedAborted, false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("aborting the only waiter aborts the shared signal", async () => {
|
|
52
|
+
const joiner = createInflightJoiner<string>();
|
|
53
|
+
let shared: AbortSignal | undefined;
|
|
54
|
+
const run = (signal: AbortSignal) => {
|
|
55
|
+
shared = signal;
|
|
56
|
+
return new Promise<string>((_resolve, reject) => {
|
|
57
|
+
signal.addEventListener("abort", () => reject(abortError("run aborted")));
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const ac = new AbortController();
|
|
61
|
+
const pending = joiner.join("only", ac.signal, run);
|
|
62
|
+
await delay(5);
|
|
63
|
+
ac.abort();
|
|
64
|
+
await assert.rejects(pending, (err: Error) => err.name === "AbortError");
|
|
65
|
+
assert.equal(shared?.aborted, true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a pre-aborted signal does not start work", async () => {
|
|
69
|
+
const joiner = createInflightJoiner<string>();
|
|
70
|
+
let runs = 0;
|
|
71
|
+
const ac = new AbortController();
|
|
72
|
+
ac.abort();
|
|
73
|
+
await assert.rejects(
|
|
74
|
+
joiner.join("pre", ac.signal, async () => {
|
|
75
|
+
runs += 1;
|
|
76
|
+
return "no";
|
|
77
|
+
}),
|
|
78
|
+
(err: Error) => err.name === "AbortError"
|
|
79
|
+
);
|
|
80
|
+
assert.equal(runs, 0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("rank loop stops between documents and the next batch is not charged the remainder", async () => {
|
|
84
|
+
const ac = new AbortController();
|
|
85
|
+
let calls = 0;
|
|
86
|
+
const abandoned = rankDocumentsUntilAbortParallel(5, 1, ac.signal, async () => {
|
|
87
|
+
calls += 1;
|
|
88
|
+
await delay(40);
|
|
89
|
+
return 1;
|
|
90
|
+
});
|
|
91
|
+
await delay(50);
|
|
92
|
+
ac.abort();
|
|
93
|
+
const stoppedAt = Date.now();
|
|
94
|
+
await assert.rejects(abandoned, (err: Error) => err.name === "AbortError");
|
|
95
|
+
const stopMs = Date.now() - stoppedAt;
|
|
96
|
+
assert.ok(calls < 5, `cancelled rerank still scored ${calls} documents`);
|
|
97
|
+
assert.ok(stopMs < 120, `worker kept scoring for ${stopMs}ms after abort`);
|
|
98
|
+
|
|
99
|
+
const followStarted = Date.now();
|
|
100
|
+
const scores = await rankDocumentsUntilAbortParallel(5, 1, undefined, async (index) => {
|
|
101
|
+
await delay(5);
|
|
102
|
+
return index;
|
|
103
|
+
});
|
|
104
|
+
const followMs = Date.now() - followStarted;
|
|
105
|
+
assert.deepEqual(scores, [0, 1, 2, 3, 4]);
|
|
106
|
+
assert.ok(followMs < 150, `following batch took ${followMs}ms`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
type Reply = (payload: unknown) => void;
|
|
110
|
+
|
|
111
|
+
function createScriptedChild(onLine: (line: string, reply: Reply) => void): CommandChildProcess {
|
|
112
|
+
const stdout = new EventEmitter();
|
|
113
|
+
const stderr = new EventEmitter();
|
|
114
|
+
const child = new EventEmitter() as EventEmitter & {
|
|
115
|
+
killed: boolean;
|
|
116
|
+
pid: number;
|
|
117
|
+
stdin: {
|
|
118
|
+
destroyed: boolean;
|
|
119
|
+
write: (chunk: string, cb?: (err?: Error | null) => void) => boolean;
|
|
120
|
+
on: (event: string, listener: (...args: unknown[]) => void) => unknown;
|
|
121
|
+
};
|
|
122
|
+
stdout: EventEmitter;
|
|
123
|
+
stderr: EventEmitter;
|
|
124
|
+
kill: (signal?: string) => boolean;
|
|
125
|
+
};
|
|
126
|
+
const reply: Reply = (payload) => {
|
|
127
|
+
stdout.emit("data", Buffer.from(`${JSON.stringify(payload)}\n`));
|
|
128
|
+
};
|
|
129
|
+
child.killed = false;
|
|
130
|
+
child.pid = 4242;
|
|
131
|
+
child.stdout = stdout;
|
|
132
|
+
child.stderr = stderr;
|
|
133
|
+
child.stdin = {
|
|
134
|
+
destroyed: false,
|
|
135
|
+
on() {
|
|
136
|
+
return child.stdin;
|
|
137
|
+
},
|
|
138
|
+
write(chunk, cb) {
|
|
139
|
+
queueMicrotask(() => {
|
|
140
|
+
for (const line of String(chunk).split("\n")) {
|
|
141
|
+
if (line.trim()) onLine(line, reply);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
cb?.(null);
|
|
145
|
+
return true;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
child.kill = () => {
|
|
149
|
+
child.killed = true;
|
|
150
|
+
child.emit("close", 0);
|
|
151
|
+
return true;
|
|
152
|
+
};
|
|
153
|
+
return child as unknown as CommandChildProcess;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function scriptedQueryServer(lines: string[]) {
|
|
157
|
+
let held: number | null = null;
|
|
158
|
+
const queue: number[] = [];
|
|
159
|
+
const holdTimers = new Map<number, ReturnType<typeof setTimeout>>();
|
|
160
|
+
|
|
161
|
+
const serveNext = (reply: Reply) => {
|
|
162
|
+
if (held !== null || queue.length === 0) return;
|
|
163
|
+
const id = queue.shift();
|
|
164
|
+
if (id === undefined) return;
|
|
165
|
+
held = id;
|
|
166
|
+
const timer = setTimeout(() => {
|
|
167
|
+
if (held !== id) return;
|
|
168
|
+
reply({
|
|
169
|
+
jsonrpc: "2.0",
|
|
170
|
+
id,
|
|
171
|
+
result: { structuredContent: { results: [] } },
|
|
172
|
+
});
|
|
173
|
+
held = null;
|
|
174
|
+
serveNext(reply);
|
|
175
|
+
}, 2_000);
|
|
176
|
+
holdTimers.set(id, timer);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return (line: string, reply: Reply) => {
|
|
180
|
+
lines.push(line);
|
|
181
|
+
const msg = JSON.parse(line) as {
|
|
182
|
+
id?: number;
|
|
183
|
+
method?: string;
|
|
184
|
+
params?: { requestId?: number };
|
|
185
|
+
};
|
|
186
|
+
if (msg.method === "initialize" && msg.id !== undefined) {
|
|
187
|
+
reply({
|
|
188
|
+
jsonrpc: "2.0",
|
|
189
|
+
id: msg.id,
|
|
190
|
+
result: {
|
|
191
|
+
protocolVersion: "2024-11-05",
|
|
192
|
+
capabilities: {},
|
|
193
|
+
serverInfo: { name: "fake-qmd", version: "2.5.3" },
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (msg.method === "notifications/cancelled") {
|
|
199
|
+
const requestId = msg.params?.requestId;
|
|
200
|
+
if (requestId !== undefined && held === requestId) {
|
|
201
|
+
const timer = holdTimers.get(requestId);
|
|
202
|
+
if (timer) clearTimeout(timer);
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
if (held === requestId) held = null;
|
|
205
|
+
const next = queue.shift();
|
|
206
|
+
if (next !== undefined) {
|
|
207
|
+
reply({
|
|
208
|
+
jsonrpc: "2.0",
|
|
209
|
+
id: next,
|
|
210
|
+
result: { structuredContent: { results: [{ docid: "next" }] } },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}, 40);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (msg.method === "tools/call" && msg.id !== undefined) {
|
|
218
|
+
queue.push(msg.id);
|
|
219
|
+
serveNext(reply);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parsedLines(lines: string[]): Array<Record<string, unknown>> {
|
|
225
|
+
return lines.map((line) => JSON.parse(line) as Record<string, unknown>);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function startedSession(lines: string[]): Promise<QmdDaemonSession> {
|
|
229
|
+
const session = new QmdDaemonSession("qmd", {}, undefined, () => createScriptedChild(scriptedQueryServer(lines)));
|
|
230
|
+
assert.equal(await session.start(), true);
|
|
231
|
+
return session;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
test("an aborted tool call cancels the worker and the next call is not stuck behind it", async () => {
|
|
235
|
+
const lines: string[] = [];
|
|
236
|
+
const session = await startedSession(lines);
|
|
237
|
+
const ac = new AbortController();
|
|
238
|
+
const first = session.callTool("query", { query: "abandoned" }, 30_000, ac.signal);
|
|
239
|
+
await delay(15);
|
|
240
|
+
ac.abort();
|
|
241
|
+
await assert.rejects(first, (err: Error) => err.name === "AbortError");
|
|
242
|
+
|
|
243
|
+
const firstTool = parsedLines(lines).find((msg) => msg.method === "tools/call");
|
|
244
|
+
const cancel = parsedLines(lines).find((msg) => msg.method === "notifications/cancelled");
|
|
245
|
+
assert.ok(firstTool);
|
|
246
|
+
assert.ok(cancel);
|
|
247
|
+
const params = cancel?.params as { requestId?: number; reason?: string };
|
|
248
|
+
assert.equal(params.requestId, firstTool?.id);
|
|
249
|
+
assert.equal(params.reason, "client aborted");
|
|
250
|
+
|
|
251
|
+
const started = Date.now();
|
|
252
|
+
const second = await session.callTool("query", { query: "next" }, 30_000);
|
|
253
|
+
const elapsed = Date.now() - started;
|
|
254
|
+
assert.ok(elapsed < 400, `following call took ${elapsed}ms; it queued behind the abandoned rerank`);
|
|
255
|
+
const structured = (second as { structuredContent?: { results?: Array<{ docid?: string }> } }).structuredContent;
|
|
256
|
+
assert.equal(structured?.results?.[0]?.docid, "next");
|
|
257
|
+
await session.close();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("a timed-out tool call cancels the worker and the next call is not stuck behind it", async () => {
|
|
261
|
+
const lines: string[] = [];
|
|
262
|
+
const session = await startedSession(lines);
|
|
263
|
+
await assert.rejects(session.callTool("query", { query: "slow" }, 80), /timed out/);
|
|
264
|
+
|
|
265
|
+
const firstTool = parsedLines(lines).find((msg) => msg.method === "tools/call");
|
|
266
|
+
const cancel = parsedLines(lines).find((msg) => msg.method === "notifications/cancelled");
|
|
267
|
+
const params = cancel?.params as { requestId?: number; reason?: string };
|
|
268
|
+
assert.equal(params?.requestId, firstTool?.id);
|
|
269
|
+
assert.match(String(params?.reason), /timed out/);
|
|
270
|
+
|
|
271
|
+
const started = Date.now();
|
|
272
|
+
await session.callTool("query", { query: "next" }, 30_000);
|
|
273
|
+
const elapsed = Date.now() - started;
|
|
274
|
+
assert.ok(elapsed < 400, `following call took ${elapsed}ms; it queued behind the timed-out rerank`);
|
|
275
|
+
await session.close();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
type SearchInternals = {
|
|
279
|
+
available: boolean;
|
|
280
|
+
daemonAvailable: boolean;
|
|
281
|
+
maybeProbeDaemon: () => Promise<void>;
|
|
282
|
+
searchViaDaemon: (
|
|
283
|
+
query: string,
|
|
284
|
+
collection: string | undefined,
|
|
285
|
+
maxResults: number,
|
|
286
|
+
options: unknown,
|
|
287
|
+
signal?: AbortSignal
|
|
288
|
+
) => Promise<QmdSearchResult[] | null>;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
function daemonClient(): { client: QmdClient; internals: SearchInternals } {
|
|
292
|
+
const client = new QmdClient("memories", 5, { daemonRecheckIntervalMs: 60_000 });
|
|
293
|
+
const internals = client as unknown as SearchInternals;
|
|
294
|
+
internals.maybeProbeDaemon = async () => {
|
|
295
|
+
internals.available = true;
|
|
296
|
+
internals.daemonAvailable = true;
|
|
297
|
+
};
|
|
298
|
+
return { client, internals };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const hit = (docid: string): QmdSearchResult => ({
|
|
302
|
+
docid,
|
|
303
|
+
path: "memory.md",
|
|
304
|
+
snippet: "",
|
|
305
|
+
score: 1,
|
|
306
|
+
transport: "daemon",
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("identical search() calls share one daemon query until the last waiter aborts", async () => {
|
|
310
|
+
const { client, internals } = daemonClient();
|
|
311
|
+
let calls = 0;
|
|
312
|
+
let shared: AbortSignal | undefined;
|
|
313
|
+
let release: (value: QmdSearchResult[]) => void = () => {};
|
|
314
|
+
internals.searchViaDaemon = (_query, _collection, _maxResults, _options, signal) => {
|
|
315
|
+
calls += 1;
|
|
316
|
+
shared = signal;
|
|
317
|
+
return new Promise((resolve) => {
|
|
318
|
+
release = resolve;
|
|
319
|
+
});
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const query = "joiner-share-recall-plan";
|
|
323
|
+
const first = client.search(query, "memories", 5);
|
|
324
|
+
const second = client.search(query, "memories", 5);
|
|
325
|
+
await delay(10);
|
|
326
|
+
assert.equal(calls, 1);
|
|
327
|
+
|
|
328
|
+
const ac = new AbortController();
|
|
329
|
+
const leaving = client.search(query, "memories", 5, undefined, { signal: ac.signal });
|
|
330
|
+
await delay(5);
|
|
331
|
+
assert.equal(calls, 1);
|
|
332
|
+
ac.abort();
|
|
333
|
+
await assert.rejects(leaving, (err: Error) => err.name === "AbortError");
|
|
334
|
+
assert.equal(shared?.aborted, false);
|
|
335
|
+
|
|
336
|
+
release([hit("kept")]);
|
|
337
|
+
const [a, b] = await Promise.all([first, second]);
|
|
338
|
+
assert.equal(a[0]?.docid, "kept");
|
|
339
|
+
assert.equal(b[0]?.docid, "kept");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("the only in-flight search() abort cancels the shared daemon call", async () => {
|
|
343
|
+
const { client, internals } = daemonClient();
|
|
344
|
+
let calls = 0;
|
|
345
|
+
let shared: AbortSignal | undefined;
|
|
346
|
+
internals.searchViaDaemon = (_query, _collection, _maxResults, _options, signal) => {
|
|
347
|
+
calls += 1;
|
|
348
|
+
shared = signal;
|
|
349
|
+
return new Promise((_resolve, reject) => {
|
|
350
|
+
signal?.addEventListener("abort", () => reject(abortError("daemon aborted")));
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const ac = new AbortController();
|
|
355
|
+
const pending = client.search("joiner-sole-abort", "memories", 5, undefined, { signal: ac.signal });
|
|
356
|
+
await delay(10);
|
|
357
|
+
assert.equal(calls, 1);
|
|
358
|
+
ac.abort();
|
|
359
|
+
await assert.rejects(pending, (err: Error) => err.name === "AbortError");
|
|
360
|
+
assert.equal(shared?.aborted, true);
|
|
361
|
+
|
|
362
|
+
const pre = new AbortController();
|
|
363
|
+
pre.abort();
|
|
364
|
+
await assert.rejects(
|
|
365
|
+
client.search("joiner-pre-aborted", "memories", 5, undefined, { signal: pre.signal }),
|
|
366
|
+
(err: Error) => err.name === "AbortError"
|
|
367
|
+
);
|
|
368
|
+
assert.equal(calls, 1);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("a caller joining after the last waiter aborted starts a fresh call", async () => {
|
|
372
|
+
const joiner = createInflightJoiner<string>();
|
|
373
|
+
let runs = 0;
|
|
374
|
+
const run = (signal: AbortSignal) => {
|
|
375
|
+
runs += 1;
|
|
376
|
+
const id = runs;
|
|
377
|
+
return new Promise<string>((resolve, reject) => {
|
|
378
|
+
// The aborted call settles late, so its slot is still in the map when the
|
|
379
|
+
// second caller joins.
|
|
380
|
+
signal.addEventListener("abort", () => setTimeout(() => reject(abortError("run aborted")), 40));
|
|
381
|
+
setTimeout(() => resolve(`run${id}`), 80);
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
const ac = new AbortController();
|
|
385
|
+
const first = joiner.join("same", ac.signal, run);
|
|
386
|
+
await delay(5);
|
|
387
|
+
ac.abort();
|
|
388
|
+
await assert.rejects(first, (err: Error) => err.name === "AbortError");
|
|
389
|
+
assert.equal(await joiner.join("same", undefined, run), "run2");
|
|
390
|
+
assert.equal(runs, 2);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("blank stdout lines are skipped and later replies still parse", async () => {
|
|
394
|
+
const lines: string[] = [];
|
|
395
|
+
let child: { stdout: EventEmitter } | undefined;
|
|
396
|
+
const session = new QmdDaemonSession("qmd", {}, undefined, () => {
|
|
397
|
+
const created = createScriptedChild(scriptedQueryServer(lines));
|
|
398
|
+
child = created as unknown as { stdout: EventEmitter };
|
|
399
|
+
return created;
|
|
400
|
+
});
|
|
401
|
+
assert.equal(await session.start(), true);
|
|
402
|
+
child?.stdout.emit("data", Buffer.from("\n \n\r\n"));
|
|
403
|
+
const reply = (await session.callTool("query", { query: "after-blank" }, 30_000)) as {
|
|
404
|
+
structuredContent?: { results?: unknown[] };
|
|
405
|
+
};
|
|
406
|
+
assert.deepEqual(reply.structuredContent?.results, []);
|
|
407
|
+
await session.close();
|
|
408
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { abortError } from "./abort-error.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One in-flight daemon search shared by identical callers.
|
|
5
|
+
* The shared signal aborts only when every waiter has aborted, which is
|
|
6
|
+
* what tells the QMD process to stop. One caller giving up must not cancel
|
|
7
|
+
* a sibling that still wants the result.
|
|
8
|
+
*/
|
|
9
|
+
export type InflightJoiner<T> = {
|
|
10
|
+
join(key: string, signal: AbortSignal | undefined, run: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type InflightSlot<T> = {
|
|
14
|
+
controller: AbortController;
|
|
15
|
+
refs: number;
|
|
16
|
+
promise: Promise<T>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function createInflightJoiner<T>(): InflightJoiner<T> {
|
|
20
|
+
const slots = new Map<string, InflightSlot<T>>();
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
join(key, signal, run) {
|
|
24
|
+
if (signal?.aborted) {
|
|
25
|
+
return Promise.reject(abortError("QMD inflight search aborted"));
|
|
26
|
+
}
|
|
27
|
+
let slot = slots.get(key);
|
|
28
|
+
// A slot whose last waiter already aborted is doomed; its promise settles
|
|
29
|
+
// late. Never hand it to a new caller.
|
|
30
|
+
if (slot?.controller.signal.aborted) slot = undefined;
|
|
31
|
+
if (!slot) {
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const created: InflightSlot<T> = {
|
|
34
|
+
controller,
|
|
35
|
+
refs: 0,
|
|
36
|
+
promise: Promise.resolve() as Promise<T>,
|
|
37
|
+
};
|
|
38
|
+
slots.set(key, created);
|
|
39
|
+
created.promise = new Promise<T>((resolve, reject) => {
|
|
40
|
+
try {
|
|
41
|
+
resolve(run(controller.signal));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
reject(err);
|
|
44
|
+
}
|
|
45
|
+
}).finally(() => {
|
|
46
|
+
if (slots.get(key) === created) slots.delete(key);
|
|
47
|
+
});
|
|
48
|
+
// A cancel with no remaining waiter rejects this promise after the
|
|
49
|
+
// waiter has already settled. Swallow that so it is not unhandled.
|
|
50
|
+
created.promise.catch(() => {});
|
|
51
|
+
slot = created;
|
|
52
|
+
}
|
|
53
|
+
return attachWaiter(slot, signal);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function attachWaiter<T>(slot: InflightSlot<T>, signal?: AbortSignal): Promise<T> {
|
|
59
|
+
if (signal?.aborted) {
|
|
60
|
+
return Promise.reject(abortError("QMD inflight search aborted"));
|
|
61
|
+
}
|
|
62
|
+
slot.refs += 1;
|
|
63
|
+
let settled = false;
|
|
64
|
+
|
|
65
|
+
return new Promise<T>((resolve, reject) => {
|
|
66
|
+
const finish = (abortShared: boolean, settle: () => void) => {
|
|
67
|
+
if (settled) return;
|
|
68
|
+
settled = true;
|
|
69
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
70
|
+
slot.refs -= 1;
|
|
71
|
+
if (slot.refs < 0) slot.refs = 0;
|
|
72
|
+
if (abortShared && slot.refs === 0) {
|
|
73
|
+
slot.controller.abort();
|
|
74
|
+
}
|
|
75
|
+
settle();
|
|
76
|
+
};
|
|
77
|
+
const onAbort = () => {
|
|
78
|
+
finish(true, () => reject(abortError("QMD inflight search aborted")));
|
|
79
|
+
};
|
|
80
|
+
if (signal) {
|
|
81
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
82
|
+
if (signal.aborted) {
|
|
83
|
+
onAbort();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
slot.promise.then(
|
|
88
|
+
(value) => finish(false, () => resolve(value)),
|
|
89
|
+
(err) => finish(false, () => reject(err))
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Score documents on a shared cursor and stop between documents when
|
|
96
|
+
* `signal` aborts. In-flight `rankIndex` calls finish; later indexes
|
|
97
|
+
* are not started. A cancelled batch throws and does not return scores.
|
|
98
|
+
*
|
|
99
|
+
* `docs/patches/qmd-2.8.3-mcp-cancel.patch` uses the same loop inside
|
|
100
|
+
* QMD's reranker. `rank()` and `rankAll()` share one evaluate path, so
|
|
101
|
+
* a document that finishes has the same score either way.
|
|
102
|
+
*/
|
|
103
|
+
export async function rankDocumentsUntilAbortParallel<T>(
|
|
104
|
+
count: number,
|
|
105
|
+
parallelism: number,
|
|
106
|
+
signal: AbortSignal | undefined,
|
|
107
|
+
rankIndex: (index: number) => Promise<T>
|
|
108
|
+
): Promise<T[]> {
|
|
109
|
+
if (signal?.aborted) {
|
|
110
|
+
throw abortError("qmd rerank aborted");
|
|
111
|
+
}
|
|
112
|
+
if (count <= 0) return [];
|
|
113
|
+
const scores = new Array<T>(count);
|
|
114
|
+
let cursor = 0;
|
|
115
|
+
const workers = Math.max(1, Math.min(Math.floor(parallelism) || 1, count));
|
|
116
|
+
await Promise.all(
|
|
117
|
+
Array.from({ length: workers }, async () => {
|
|
118
|
+
while (true) {
|
|
119
|
+
if (signal?.aborted) return;
|
|
120
|
+
const index = cursor++;
|
|
121
|
+
if (index >= count) return;
|
|
122
|
+
scores[index] = await rankIndex(index);
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
if (signal?.aborted) {
|
|
127
|
+
throw abortError("qmd rerank aborted");
|
|
128
|
+
}
|
|
129
|
+
return scores;
|
|
130
|
+
}
|
package/src/qmd-preflight.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const QMD_PROBE_TIMEOUT_MS = 8_000;
|
|
|
29
29
|
// Length == number of retries after the initial attempt. Issue #1841.
|
|
30
30
|
export const QMD_PROBE_RETRY_BACKOFF_MS = [300, 800];
|
|
31
31
|
|
|
32
|
-
export const QMD_SUPPORTED_VERSION = "2.
|
|
32
|
+
export const QMD_SUPPORTED_VERSION = "2.8.3";
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Classify a `qmd --version` probe failure so the preflight can distinguish a
|
|
@@ -148,6 +148,15 @@ export function shouldAutoUpgradeQmd(
|
|
|
148
148
|
return compareQmdVersions(installed, supported) < 0;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/** First QMD release that needs the pinned, patched build (docs/qmd-2.8.3.md); stock npm lacks the patches. */
|
|
152
|
+
const QMD_PATCHED_BUILD_MIN: QmdVersionTuple = [2, 8, 3];
|
|
153
|
+
|
|
154
|
+
/** False when `npm install -g @tobilu/qmd@<supported>` would install stock QMD instead of the patched build. */
|
|
155
|
+
export function autoUpgradeInstallsStockPackage(supportedVersion: string): boolean {
|
|
156
|
+
const supported = parseQmdVersion(supportedVersion);
|
|
157
|
+
return supported !== null && compareQmdVersions(supported, QMD_PATCHED_BUILD_MIN) < 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
export function getQmdPostInstallProbeTargets(
|
|
152
161
|
qmdPath: string,
|
|
153
162
|
qmdPathSource: "configured" | "auto-path" | "auto-fallback"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { compareRankedLists, compareDirectories, compareRerankOff, formatComparison, spearman } from "../../../scripts/recall-qmd-compare.mjs";
|
|
7
|
+
|
|
8
|
+
const row = (docid: string) => ({ docid, score: 1 });
|
|
9
|
+
|
|
10
|
+
test("identical rankings match on top-1, overlap, and correlation", () => {
|
|
11
|
+
const ranked = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"].map(row);
|
|
12
|
+
const compared = compareRankedLists(ranked, ranked);
|
|
13
|
+
assert.equal(compared.top1Match, true);
|
|
14
|
+
assert.equal(compared.top10Overlap, 10);
|
|
15
|
+
assert.ok(Math.abs((compared.spearman ?? NaN) - 1) < 1e-9);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("a swap keeps top-1 and reports the rank correlation", () => {
|
|
19
|
+
const before = ["a", "b", "c"].map(row);
|
|
20
|
+
const after = ["a", "c", "b"].map(row);
|
|
21
|
+
const compared = compareRankedLists(before, after);
|
|
22
|
+
assert.equal(compared.top1Match, true);
|
|
23
|
+
assert.equal(compared.top10Overlap, 3);
|
|
24
|
+
assert.ok(Math.abs((compared.spearman ?? NaN) - 0.5) < 1e-9);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("disjoint top-10 lists do not match and correlation is negative", () => {
|
|
28
|
+
const before = ["a", "b", "c"].map(row);
|
|
29
|
+
const after = ["d", "e", "f"].map(row);
|
|
30
|
+
const compared = compareRankedLists(before, after);
|
|
31
|
+
assert.equal(compared.top1Match, false);
|
|
32
|
+
assert.equal(compared.top10Overlap, 0);
|
|
33
|
+
assert.ok((compared.spearman ?? 0) < 0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("spearman is 1 for tied constant ranks and -1 for a reversal", () => {
|
|
37
|
+
assert.equal(spearman([1, 1, 1], [4, 4, 4]), 1);
|
|
38
|
+
assert.ok(Math.abs((spearman([1, 2, 3], [3, 2, 1]) ?? NaN) + 1) < 1e-9);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("an empty ranking against a non-empty one is not perfect correlation", () => {
|
|
42
|
+
const compared = compareRankedLists([], ["a", "b", "c"].map(row));
|
|
43
|
+
assert.equal(compared.top10Overlap, 0);
|
|
44
|
+
assert.equal(compared.spearman, null);
|
|
45
|
+
assert.equal(spearman([11, 11], [1, 2]), null);
|
|
46
|
+
assert.equal(spearman([1], [1]), 1);
|
|
47
|
+
assert.equal(spearman([1], [11]), null);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function resultFile(dir: string, name: string, docids: string[]): void {
|
|
51
|
+
writeFileSync(join(dir, name), JSON.stringify({ results: docids.map(row) }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test("a missing or unreadable counterpart is reported, not dropped", () => {
|
|
55
|
+
const before = mkdtempSync(join(tmpdir(), "qmd-cmp-b-"));
|
|
56
|
+
const after = mkdtempSync(join(tmpdir(), "qmd-cmp-a-"));
|
|
57
|
+
resultFile(before, "q1.json", ["a", "b"]);
|
|
58
|
+
resultFile(before, "q2.json", ["a", "b"]);
|
|
59
|
+
resultFile(before, "q3.json", ["a", "b"]);
|
|
60
|
+
resultFile(after, "q1.json", ["a", "b"]);
|
|
61
|
+
writeFileSync(join(after, "q3.json"), "{not json");
|
|
62
|
+
const rows = compareDirectories(before, after);
|
|
63
|
+
assert.deepEqual(rows.filter((r) => r.missing).map((r) => r.name), ["q2.json", "q3.json"]);
|
|
64
|
+
assert.match(formatComparison(rows), /summary queries=3 compared=1 missing=2 empty_side=0/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("rerank-off reports a daemon file with no norerank twin instead of throwing", () => {
|
|
68
|
+
const dir = mkdtempSync(join(tmpdir(), "qmd-cmp-r-"));
|
|
69
|
+
resultFile(dir, "q1-daemon.json", ["a", "b"]);
|
|
70
|
+
resultFile(dir, "q1-norerank.json", ["a", "b"]);
|
|
71
|
+
resultFile(dir, "q2-daemon.json", ["a", "b"]);
|
|
72
|
+
const rows = compareRerankOff(dir);
|
|
73
|
+
assert.equal(rows.length, 2);
|
|
74
|
+
assert.equal(rows.filter((r) => r.missing).length, 1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("files present on only one side, an invalid results field, and a one-sided empty ranking are all reported", () => {
|
|
78
|
+
const before = mkdtempSync(join(tmpdir(), "qmd-cmp-b2-"));
|
|
79
|
+
const after = mkdtempSync(join(tmpdir(), "qmd-cmp-a2-"));
|
|
80
|
+
resultFile(before, "q1.json", ["a", "b"]);
|
|
81
|
+
writeFileSync(join(after, "q1.json"), "{}");
|
|
82
|
+
resultFile(after, "q2.json", ["a"]);
|
|
83
|
+
resultFile(before, "q3.json", ["a", "b"]);
|
|
84
|
+
resultFile(after, "q3.json", []);
|
|
85
|
+
const rows = compareDirectories(before, after);
|
|
86
|
+
assert.deepEqual(rows.map((r) => r.name), ["q1.json", "q2.json", "q3.json"]);
|
|
87
|
+
assert.deepEqual(rows.map((r) => Boolean(r.missing)), [true, true, false]);
|
|
88
|
+
assert.equal(rows[2]?.oneSidedEmpty, true);
|
|
89
|
+
assert.match(formatComparison(rows), /missing=2 empty_side=1/);
|
|
90
|
+
// an empty before dir with captures only in after must not look clean
|
|
91
|
+
const emptyBefore = mkdtempSync(join(tmpdir(), "qmd-cmp-b3-"));
|
|
92
|
+
assert.equal(compareDirectories(emptyBefore, after).every((r) => r.missing), true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("rerank-off reports a norerank capture that has no daemon twin", () => {
|
|
96
|
+
const dir = mkdtempSync(join(tmpdir(), "qmd-cmp-r2-"));
|
|
97
|
+
resultFile(dir, "q1-norerank.json", ["a", "b"]);
|
|
98
|
+
const rows = compareRerankOff(dir);
|
|
99
|
+
assert.deepEqual(rows.map((r) => [r.name, Boolean(r.missing)]), [["q1-daemon.json", true]]);
|
|
100
|
+
});
|