@remnic/core 9.4.0 → 9.4.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.d.ts +5 -5
- package/dist/access-admin-ops-surface.js +22 -21
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +23 -22
- package/dist/access-cli.js +63 -62
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +31 -30
- package/dist/access-identity-continuity-surface.d.ts +4 -4
- package/dist/access-identity-continuity-surface.js +22 -21
- package/dist/access-lcm-surface.d.ts +5 -5
- package/dist/access-lcm-surface.js +22 -21
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +27 -26
- package/dist/access-observe-write-surface.d.ts +5 -5
- package/dist/access-observe-write-surface.js +22 -21
- package/dist/access-operations-batch.js +24 -23
- package/dist/access-operations.d.ts +9 -9
- package/dist/access-operations.js +26 -25
- package/dist/access-recall-surface.d.ts +5 -5
- package/dist/access-recall-surface.js +22 -21
- package/dist/access-schema.d.ts +68 -68
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-CdW6B2qQ.d.ts → access-service-m-2VO_Ig.d.ts} +3 -3
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +22 -21
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-M47VTCFI.js → auto-sync-HMHQHUAG.js} +4 -4
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +5 -5
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-bflYNWsM.d.ts → catalog-BxWokdC2.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +6 -6
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-TKUQSXU4.js → chunk-27FLVMDC.js} +12 -12
- package/dist/{chunk-XXEOGY77.js → chunk-2FOD2YY3.js} +7 -7
- package/dist/{chunk-6Y4JTNX7.js → chunk-2MR3MFQB.js} +2 -2
- package/dist/{chunk-AU7Q3LSC.js → chunk-2QSZNTDO.js} +4 -4
- package/dist/{chunk-64ZYXSCO.js → chunk-2U2A5HF4.js} +10 -4
- package/dist/chunk-2U2A5HF4.js.map +1 -0
- package/dist/{chunk-RBGL4MKQ.js → chunk-3F4N4WDH.js} +75 -110
- package/dist/chunk-3F4N4WDH.js.map +1 -0
- package/dist/{chunk-4NMATTXV.js → chunk-4BFRBP45.js} +2 -2
- package/dist/{chunk-L4UZBJNM.js → chunk-4P4626RY.js} +4 -4
- package/dist/{chunk-XZJMIJLW.js → chunk-5QZDMSSN.js} +29 -29
- package/dist/chunk-64UDOIMI.js +121 -0
- package/dist/chunk-64UDOIMI.js.map +1 -0
- package/dist/{chunk-H2EJNV2Z.js → chunk-66FLU5BT.js} +2 -2
- package/dist/{chunk-MWH5MAFP.js → chunk-6BISDEN6.js} +5 -5
- package/dist/{chunk-VBVSN2TV.js → chunk-6ZIETXZ2.js} +2 -2
- package/dist/{chunk-42BBIR33.js → chunk-7N2RUBAP.js} +2 -2
- package/dist/{chunk-UQI2NHTY.js → chunk-7QWMTOIF.js} +37 -37
- package/dist/{chunk-QICQ35K5.js → chunk-7Z2DZNWN.js} +17 -8
- package/dist/{chunk-QICQ35K5.js.map → chunk-7Z2DZNWN.js.map} +1 -1
- package/dist/{chunk-JI5AVXYO.js → chunk-AKUCU7CI.js} +5 -5
- package/dist/{chunk-FT37KV7R.js → chunk-BGTAL46Y.js} +2 -2
- package/dist/{chunk-7VFZTJ7K.js → chunk-BV2TIZ4F.js} +2 -2
- package/dist/{chunk-FWPHPN3V.js → chunk-CLX5VDNY.js} +5 -5
- package/dist/{chunk-TH2KIPP7.js → chunk-CZAA5N5S.js} +2 -2
- package/dist/{chunk-7BAJAXGB.js → chunk-CZAZX7JP.js} +4 -4
- package/dist/{chunk-APMGGGTY.js → chunk-DGYCWCQ7.js} +2 -2
- package/dist/{chunk-GEVIE5NV.js → chunk-DR6PEJU3.js} +4 -4
- package/dist/{chunk-2TCZE3C2.js → chunk-EPGWWCZF.js} +2 -2
- package/dist/{chunk-OUXPVWTZ.js → chunk-FLVL4L3L.js} +5 -5
- package/dist/{chunk-YPL66L3U.js → chunk-FZGGZAAU.js} +2 -2
- package/dist/{chunk-XOE5DYQL.js → chunk-HRCACVHL.js} +2 -2
- package/dist/{chunk-KS4PRHRD.js → chunk-IC6QX5TC.js} +2 -2
- package/dist/{chunk-H25TAODC.js → chunk-ILFJ7AVS.js} +4 -4
- package/dist/{chunk-UQMCR5RW.js → chunk-JLR3JSTL.js} +3 -3
- package/dist/{chunk-5H56JAHW.js → chunk-KCJ5H6XQ.js} +2 -2
- package/dist/{chunk-H5IT5JL7.js → chunk-LOR6ZM5R.js} +3 -3
- package/dist/{chunk-VBBMTSOV.js → chunk-M7GGMGMZ.js} +6 -6
- package/dist/{chunk-G47FEGGK.js → chunk-NG7YBCJO.js} +4 -4
- package/dist/{chunk-QGS7Z7YU.js → chunk-OHDJ34CC.js} +2 -2
- package/dist/{chunk-UAOZJX5J.js → chunk-P6SYWE4G.js} +53 -6
- package/dist/chunk-P6SYWE4G.js.map +1 -0
- package/dist/{chunk-LLMZK53N.js → chunk-PUBTHW5O.js} +2 -2
- package/dist/{chunk-AGNBY3VG.js → chunk-PYHOAXTI.js} +2 -2
- package/dist/{chunk-SXN3LCUT.js → chunk-RHOZ5LLO.js} +100 -4
- package/dist/chunk-RHOZ5LLO.js.map +1 -0
- package/dist/{chunk-6KYMPV2O.js → chunk-RL6NEVXT.js} +45 -2
- package/dist/chunk-RL6NEVXT.js.map +1 -0
- package/dist/{chunk-7WV3F5DQ.js → chunk-V7IZKJI3.js} +2 -2
- package/dist/{chunk-IGF4DII6.js → chunk-X4A62ET7.js} +9 -9
- package/dist/{chunk-OH6BKCPB.js → chunk-XN3TUUX2.js} +2 -2
- package/dist/{chunk-5K5VEDFY.js → chunk-YDSWRD33.js} +4 -4
- package/dist/{chunk-ABGDGBDT.js → chunk-YPEMNNNY.js} +2 -2
- package/dist/{chunk-YM7IEUO7.js → chunk-ZPOROUTX.js} +2 -2
- package/dist/{cli-Dfnc66gs.d.ts → cli-EiBBOt3r.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +51 -50
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +5 -5
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +5 -5
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +5 -5
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +5 -5
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +17 -12
- package/dist/index.js +79 -78
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +4 -4
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +6 -6
- package/dist/lcm/schema.js +2 -2
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +5 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +6 -6
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +23 -3
- package/dist/memory-projection-store.js +9 -2
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- package/dist/namespaces/migrate.js +17 -16
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +11 -10
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +5 -5
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +11 -2
- package/dist/operator-toolkit.js +23 -20
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +7 -7
- package/dist/{orchestrator-Y9wyG4jK.d.ts → orchestrator-B2Y28Z9t.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +63 -62
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{purge-IHRX4Z7X.js → purge-NDWV6WYD.js} +4 -3
- package/dist/{purge-IHRX4Z7X.js.map → purge-NDWV6WYD.js.map} +1 -1
- package/dist/qmd-Db9mOkzB.d.ts +330 -0
- package/dist/qmd-preflight.d.ts +5 -0
- package/dist/qmd-preflight.js +31 -0
- package/dist/qmd-preflight.js.map +1 -0
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +3 -305
- package/dist/qmd.js +10 -8
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/runtime/better-sqlite.d.ts +17 -1
- package/dist/runtime/better-sqlite.js +5 -3
- package/dist/schemas.d.ts +84 -84
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +10 -9
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +15 -14
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-bfXJNKVf.d.ts → semantic-consolidation-BDVIbLRw.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +5 -5
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +5 -5
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/session-transcript-migration.js +2 -2
- package/dist/shared-context/manager.d.ts +9 -9
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +4 -4
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/export-sqlite.js +2 -2
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/transfer/types.d.ts +66 -66
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-D1iBqhJA.d.ts → types-ljkd82rn.d.ts} +13 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +5 -5
- package/package.json +2 -2
- package/src/memory-projection-open-failure.test.ts +322 -0
- package/src/memory-projection-store.ts +179 -2
- package/src/operator-toolkit.ts +64 -0
- package/src/qmd-preflight.test.ts +197 -0
- package/src/qmd-preflight.ts +167 -0
- package/src/qmd.ts +115 -129
- package/src/runtime/better-sqlite.test.ts +54 -0
- package/src/runtime/better-sqlite.ts +83 -3
- package/src/wearables/cleanup.test.ts +56 -0
- package/src/wearables/cleanup.ts +22 -1
- package/src/wearables/config.test.ts +25 -0
- package/src/wearables/config.ts +31 -4
- package/src/wearables/types.ts +12 -1
- package/dist/chunk-64ZYXSCO.js.map +0 -1
- package/dist/chunk-6KYMPV2O.js.map +0 -1
- package/dist/chunk-RBGL4MKQ.js.map +0 -1
- package/dist/chunk-SXN3LCUT.js.map +0 -1
- package/dist/chunk-UAOZJX5J.js.map +0 -1
- /package/dist/{auto-sync-M47VTCFI.js.map → auto-sync-HMHQHUAG.js.map} +0 -0
- /package/dist/{chunk-TKUQSXU4.js.map → chunk-27FLVMDC.js.map} +0 -0
- /package/dist/{chunk-XXEOGY77.js.map → chunk-2FOD2YY3.js.map} +0 -0
- /package/dist/{chunk-6Y4JTNX7.js.map → chunk-2MR3MFQB.js.map} +0 -0
- /package/dist/{chunk-AU7Q3LSC.js.map → chunk-2QSZNTDO.js.map} +0 -0
- /package/dist/{chunk-4NMATTXV.js.map → chunk-4BFRBP45.js.map} +0 -0
- /package/dist/{chunk-L4UZBJNM.js.map → chunk-4P4626RY.js.map} +0 -0
- /package/dist/{chunk-XZJMIJLW.js.map → chunk-5QZDMSSN.js.map} +0 -0
- /package/dist/{chunk-H2EJNV2Z.js.map → chunk-66FLU5BT.js.map} +0 -0
- /package/dist/{chunk-MWH5MAFP.js.map → chunk-6BISDEN6.js.map} +0 -0
- /package/dist/{chunk-VBVSN2TV.js.map → chunk-6ZIETXZ2.js.map} +0 -0
- /package/dist/{chunk-42BBIR33.js.map → chunk-7N2RUBAP.js.map} +0 -0
- /package/dist/{chunk-UQI2NHTY.js.map → chunk-7QWMTOIF.js.map} +0 -0
- /package/dist/{chunk-JI5AVXYO.js.map → chunk-AKUCU7CI.js.map} +0 -0
- /package/dist/{chunk-FT37KV7R.js.map → chunk-BGTAL46Y.js.map} +0 -0
- /package/dist/{chunk-7VFZTJ7K.js.map → chunk-BV2TIZ4F.js.map} +0 -0
- /package/dist/{chunk-FWPHPN3V.js.map → chunk-CLX5VDNY.js.map} +0 -0
- /package/dist/{chunk-TH2KIPP7.js.map → chunk-CZAA5N5S.js.map} +0 -0
- /package/dist/{chunk-7BAJAXGB.js.map → chunk-CZAZX7JP.js.map} +0 -0
- /package/dist/{chunk-APMGGGTY.js.map → chunk-DGYCWCQ7.js.map} +0 -0
- /package/dist/{chunk-GEVIE5NV.js.map → chunk-DR6PEJU3.js.map} +0 -0
- /package/dist/{chunk-2TCZE3C2.js.map → chunk-EPGWWCZF.js.map} +0 -0
- /package/dist/{chunk-OUXPVWTZ.js.map → chunk-FLVL4L3L.js.map} +0 -0
- /package/dist/{chunk-YPL66L3U.js.map → chunk-FZGGZAAU.js.map} +0 -0
- /package/dist/{chunk-XOE5DYQL.js.map → chunk-HRCACVHL.js.map} +0 -0
- /package/dist/{chunk-KS4PRHRD.js.map → chunk-IC6QX5TC.js.map} +0 -0
- /package/dist/{chunk-H25TAODC.js.map → chunk-ILFJ7AVS.js.map} +0 -0
- /package/dist/{chunk-UQMCR5RW.js.map → chunk-JLR3JSTL.js.map} +0 -0
- /package/dist/{chunk-5H56JAHW.js.map → chunk-KCJ5H6XQ.js.map} +0 -0
- /package/dist/{chunk-H5IT5JL7.js.map → chunk-LOR6ZM5R.js.map} +0 -0
- /package/dist/{chunk-VBBMTSOV.js.map → chunk-M7GGMGMZ.js.map} +0 -0
- /package/dist/{chunk-G47FEGGK.js.map → chunk-NG7YBCJO.js.map} +0 -0
- /package/dist/{chunk-QGS7Z7YU.js.map → chunk-OHDJ34CC.js.map} +0 -0
- /package/dist/{chunk-LLMZK53N.js.map → chunk-PUBTHW5O.js.map} +0 -0
- /package/dist/{chunk-AGNBY3VG.js.map → chunk-PYHOAXTI.js.map} +0 -0
- /package/dist/{chunk-7WV3F5DQ.js.map → chunk-V7IZKJI3.js.map} +0 -0
- /package/dist/{chunk-IGF4DII6.js.map → chunk-X4A62ET7.js.map} +0 -0
- /package/dist/{chunk-OH6BKCPB.js.map → chunk-XN3TUUX2.js.map} +0 -0
- /package/dist/{chunk-5K5VEDFY.js.map → chunk-YDSWRD33.js.map} +0 -0
- /package/dist/{chunk-ABGDGBDT.js.map → chunk-YPEMNNNY.js.map} +0 -0
- /package/dist/{chunk-YM7IEUO7.js.map → chunk-ZPOROUTX.js.map} +0 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { abortError } from "./abort-error.js";
|
|
4
|
+
import { QmdClient } from "./qmd.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internals exposed only for preflight testing. Cast once to a NAMED type (not
|
|
8
|
+
* an inline shape) so the compiler never fabricates a one-off member type; the
|
|
9
|
+
* unchecked access is scoped to these deliberately-injected seams. Issue #1841.
|
|
10
|
+
*/
|
|
11
|
+
type QmdPreflightInternals = {
|
|
12
|
+
configuredQmdPath: string | undefined;
|
|
13
|
+
runVersionProbe: (qmdPath: string, signal?: AbortSignal) => Promise<{ stdout: string; stderr: string }>;
|
|
14
|
+
logCliProbeWarning: (message: string) => void;
|
|
15
|
+
probeCli: (options?: {
|
|
16
|
+
allowAutoUpgrade?: boolean;
|
|
17
|
+
preserveStateOnFailure?: boolean;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}) => Promise<boolean>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const CONFIGURED_PATH = "/opt/qmd/bin/qmd";
|
|
23
|
+
|
|
24
|
+
/** Build the Node spawn ENOENT error shape (Error carrying `code: "ENOENT"`). */
|
|
25
|
+
function spawnENOENT(qmdPath: string): Error & { code: string } {
|
|
26
|
+
return Object.assign(new Error(`spawn ${qmdPath} ENOENT`), {
|
|
27
|
+
code: "ENOENT" as const,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function timeoutError(): Error & { timedOut: true } {
|
|
32
|
+
// Mirrors the error runCommandWithTimeout rejects with on deadline: a plain
|
|
33
|
+
// Error flagged with `timedOut: true` — the structured signal classifyProbeFailure
|
|
34
|
+
// keys on (never the message text). Issue #1841.
|
|
35
|
+
return Object.assign(new Error("qmd --version timed out after 8000ms"), {
|
|
36
|
+
timedOut: true as const,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Wire a fresh client whose probe runner + warning sink are fully scripted. */
|
|
41
|
+
function harness(opts: {
|
|
42
|
+
configuredBehavior: () => Promise<{ stdout: string; stderr: string }>;
|
|
43
|
+
}): {
|
|
44
|
+
internals: QmdPreflightInternals;
|
|
45
|
+
warnings: string[];
|
|
46
|
+
configuredProbeCalls: () => number;
|
|
47
|
+
} {
|
|
48
|
+
const client = new QmdClient("memories", 3, {
|
|
49
|
+
qmdPath: CONFIGURED_PATH,
|
|
50
|
+
qmdFallbackPaths: [],
|
|
51
|
+
});
|
|
52
|
+
const internals = client as unknown as QmdPreflightInternals;
|
|
53
|
+
const warnings: string[] = [];
|
|
54
|
+
internals.logCliProbeWarning = (message: string) => {
|
|
55
|
+
warnings.push(message);
|
|
56
|
+
};
|
|
57
|
+
let configuredCalls = 0;
|
|
58
|
+
internals.runVersionProbe = async (qmdPath: string) => {
|
|
59
|
+
if (qmdPath === CONFIGURED_PATH) {
|
|
60
|
+
configuredCalls += 1;
|
|
61
|
+
return opts.configuredBehavior();
|
|
62
|
+
}
|
|
63
|
+
// Any non-configured path (PATH/fallback probing) is a hard miss here so
|
|
64
|
+
// probeCli returns false without spawning a real qmd binary.
|
|
65
|
+
throw spawnENOENT(qmdPath);
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
internals,
|
|
69
|
+
warnings,
|
|
70
|
+
configuredProbeCalls: () => configuredCalls,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test("QmdClient preflight: transient timeout retries then succeeds with NO failure warning", async () => {
|
|
75
|
+
// Two timeouts, then success on the second retry.
|
|
76
|
+
const scripted: Array<"timeout" | "ok"> = ["timeout", "timeout", "ok"];
|
|
77
|
+
let i = 0;
|
|
78
|
+
const { internals, warnings, configuredProbeCalls } = harness({
|
|
79
|
+
configuredBehavior: async () => {
|
|
80
|
+
const step = scripted[i++];
|
|
81
|
+
if (step === "timeout") throw timeoutError();
|
|
82
|
+
return { stdout: "qmd 1.2.3\n", stderr: "" };
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const ok = await internals.probeCli({ allowAutoUpgrade: false });
|
|
87
|
+
|
|
88
|
+
assert.equal(ok, true, "configured probe should succeed after retry");
|
|
89
|
+
assert.equal(configuredProbeCalls(), 3, "initial attempt + 2 retries for transient timeout");
|
|
90
|
+
assert.deepEqual(warnings, [], "no failure warning emitted when retry succeeds");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("QmdClient preflight: persistent timeout emits the distinct under-load warning (not 'configured qmdPath failed')", async () => {
|
|
94
|
+
const { internals, warnings, configuredProbeCalls } = harness({
|
|
95
|
+
configuredBehavior: async () => {
|
|
96
|
+
throw timeoutError();
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const ok = await internals.probeCli({ allowAutoUpgrade: false });
|
|
101
|
+
|
|
102
|
+
assert.equal(ok, false, "exhausted retries fall through to unavailable");
|
|
103
|
+
assert.equal(configuredProbeCalls(), 3, "initial attempt + 2 retries before declaring transient failure");
|
|
104
|
+
assert.equal(warnings.length, 1, "exactly one failure warning");
|
|
105
|
+
const msg = warnings[0] ?? "";
|
|
106
|
+
assert.match(
|
|
107
|
+
msg,
|
|
108
|
+
/version check timed out \(host may be under load\); retried 2 times/,
|
|
109
|
+
"transient failure uses the under-load wording"
|
|
110
|
+
);
|
|
111
|
+
assert.doesNotMatch(msg, /configured qmdPath failed/, "must not read as a generic misconfiguration");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("QmdClient preflight: genuine ENOENT fails fast (no retry) with the not-found/not-executable warning", async () => {
|
|
115
|
+
const { internals, warnings, configuredProbeCalls } = harness({
|
|
116
|
+
configuredBehavior: async () => {
|
|
117
|
+
throw spawnENOENT(CONFIGURED_PATH);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const ok = await internals.probeCli({ allowAutoUpgrade: false });
|
|
122
|
+
|
|
123
|
+
assert.equal(ok, false, "missing binary is unavailable");
|
|
124
|
+
assert.equal(configuredProbeCalls(), 1, "ENOENT is a hard misconfiguration: never retried");
|
|
125
|
+
assert.equal(warnings.length, 1, "exactly one failure warning");
|
|
126
|
+
const msg = warnings[0] ?? "";
|
|
127
|
+
assert.match(msg, /configured qmdPath not found or not executable/, "missing binary uses the not-found wording");
|
|
128
|
+
assert.match(msg, /spawn .* ENOENT/, "underlying error detail preserved");
|
|
129
|
+
assert.doesNotMatch(msg, /timed out|under load/, "must not read as a transient timeout");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("QmdClient preflight: non-timeout exit-code failure keeps the generic configured-failed message and does not retry", async () => {
|
|
133
|
+
const { internals, warnings, configuredProbeCalls } = harness({
|
|
134
|
+
configuredBehavior: async () => {
|
|
135
|
+
throw new Error("qmd --version exited with code 2");
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const ok = await internals.probeCli({ allowAutoUpgrade: false });
|
|
140
|
+
|
|
141
|
+
assert.equal(ok, false, "exit-code failure is unavailable");
|
|
142
|
+
assert.equal(configuredProbeCalls(), 1, "non-transient, non-missing failures are not retried");
|
|
143
|
+
assert.equal(warnings.length, 1, "exactly one failure warning");
|
|
144
|
+
const msg = warnings[0] ?? "";
|
|
145
|
+
assert.match(msg, /configured qmdPath failed/, "ambiguous exit-code failures keep the historical generic wording");
|
|
146
|
+
assert.doesNotMatch(
|
|
147
|
+
msg,
|
|
148
|
+
/timed out|under load|not found or not executable/,
|
|
149
|
+
"must not be misclassified as timeout or missing"
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("QmdClient preflight: caller abort emits NO warning and does not retry", async () => {
|
|
154
|
+
// The caller cancelled (aborted signal + AbortError). Other QMD paths treat
|
|
155
|
+
// this as non-actionable noise via isCallerCancellation; the configured-path
|
|
156
|
+
// probe must do the same — no under-load warning, no transient retry. #1841.
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
controller.abort();
|
|
159
|
+
const { internals, warnings, configuredProbeCalls } = harness({
|
|
160
|
+
configuredBehavior: async () => {
|
|
161
|
+
// A real probe rejects with an AbortError when the caller's signal fires.
|
|
162
|
+
throw abortError("qmd --version aborted");
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const ok = await internals.probeCli({ allowAutoUpgrade: false, signal: controller.signal });
|
|
167
|
+
|
|
168
|
+
assert.equal(ok, false, "aborted configured probe falls through to unavailable");
|
|
169
|
+
assert.equal(configuredProbeCalls(), 1, "caller cancellation never retries");
|
|
170
|
+
assert.deepEqual(warnings, [], "caller cancellation is silent noise: no operator-facing warning");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("QmdClient preflight: non-zero exit whose stderr says 'timed out'/'not found' is classified 'other' (not transient/missing)", async () => {
|
|
174
|
+
// Mirrors runCommandWithTimeout's non-zero-exit rejection: the child's stderr
|
|
175
|
+
// (here containing "timed out" and "not found") is embedded in the message.
|
|
176
|
+
// A healthy binary can emit those words in error text; they must NOT upgrade
|
|
177
|
+
// the failure to transient/missing. classifyProbeFailure keys on structured
|
|
178
|
+
// signals, so this generic exit-code failure stays 'other'. Issue #1841.
|
|
179
|
+
const { internals, warnings, configuredProbeCalls } = harness({
|
|
180
|
+
configuredBehavior: async () => {
|
|
181
|
+
throw new Error("qmd --version failed (code 2): error: model timed out while loading; config not found");
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const ok = await internals.probeCli({ allowAutoUpgrade: false });
|
|
186
|
+
|
|
187
|
+
assert.equal(ok, false, "exit-code failure is unavailable");
|
|
188
|
+
assert.equal(configuredProbeCalls(), 1, "embedded-stderr failure is not transient — no retry");
|
|
189
|
+
assert.equal(warnings.length, 1, "exactly one failure warning");
|
|
190
|
+
const msg = warnings[0] ?? "";
|
|
191
|
+
assert.match(msg, /configured qmdPath failed/, "classified as generic 'other'");
|
|
192
|
+
assert.doesNotMatch(
|
|
193
|
+
msg,
|
|
194
|
+
/under load|not found or not executable/,
|
|
195
|
+
"must not misclassify embedded-stderr words as transient/missing"
|
|
196
|
+
);
|
|
197
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QMD version-check PREFLIGHT / probe-classification helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pure, self-contained functions extracted from qmd.ts (issue #1841) so the
|
|
5
|
+
* QmdClient class file stays under the structural-ratchet LOC ceiling. These
|
|
6
|
+
* cover two related concerns of the startup preflight:
|
|
7
|
+
*
|
|
8
|
+
* 1. VERSION parsing/comparison — turning `qmd --version` output into a tuple,
|
|
9
|
+
* comparing against the supported target, and resolving feature capability
|
|
10
|
+
* flags from the parsed version.
|
|
11
|
+
* 2. PROBE failure classification — distinguishing a slow/overloaded binary
|
|
12
|
+
* (transient → retry) from a genuine misconfiguration (ENOENT/EACCES → fail
|
|
13
|
+
* fast) using STRUCTURED signals, never the error message.
|
|
14
|
+
*
|
|
15
|
+
* No behavioral change from qmd.ts round 1 (issue #1841): structured-signal
|
|
16
|
+
* classification (code=ENOENT/EACCES=>missing, timedOut flag=>transient,
|
|
17
|
+
* caller-cancel silenced, non-zero-exit=>other); backoff [300,800].
|
|
18
|
+
*/
|
|
19
|
+
import { isAbortError } from "./abort-error.js";
|
|
20
|
+
import type { QmdCapabilities, QmdVersionTuple } from "./qmd.js";
|
|
21
|
+
|
|
22
|
+
// Deadline (ms) for a single `qmd --version` probe.
|
|
23
|
+
export const QMD_PROBE_TIMEOUT_MS = 8_000;
|
|
24
|
+
|
|
25
|
+
// Backoff schedule (ms) for retrying a TRANSIENT (timeout/abort) version-check
|
|
26
|
+
// probe of a CONFIGURED qmdPath before declaring it failed. A binary that
|
|
27
|
+
// resolved and ran before is more likely slow under load than gone, so we retry
|
|
28
|
+
// transient failures but NOT ENOENT/EACCES (hard misconfiguration, fail fast).
|
|
29
|
+
// Length == number of retries after the initial attempt. Issue #1841.
|
|
30
|
+
export const QMD_PROBE_RETRY_BACKOFF_MS = [300, 800];
|
|
31
|
+
|
|
32
|
+
export const QMD_SUPPORTED_VERSION = "2.5.3";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Classify a `qmd --version` probe failure so the preflight can distinguish a
|
|
36
|
+
* slow/overloaded binary (a genuine deadline timeout) from a real
|
|
37
|
+
* misconfiguration (ENOENT/EACCES — the configured path is missing or not
|
|
38
|
+
* executable). Only transient failures are retried; missing binaries fail fast.
|
|
39
|
+
*
|
|
40
|
+
* Classification keys on STRUCTURED signals, never the error *message*: a
|
|
41
|
+
* non-zero-exit failure embeds the child's stderr in its message, so scanning
|
|
42
|
+
* that text for "abort"/"timed out"/"not found" would misclassify a healthy
|
|
43
|
+
* binary. The structured signals are: `code` (ENOENT/EACCES) for missing; the
|
|
44
|
+
* `timedOut` flag set by `runCommandWithTimeout` for a genuine timeout; an
|
|
45
|
+
* AbortError name for a deadline/abort under load. Issue #1841.
|
|
46
|
+
*/
|
|
47
|
+
export type QmdProbeFailureKind = "transient" | "missing" | "other";
|
|
48
|
+
export function classifyProbeFailure(err: unknown): QmdProbeFailureKind {
|
|
49
|
+
if (err && typeof err === "object") {
|
|
50
|
+
// Node spawn failures carry a structured `code`: ENOENT (not found) /
|
|
51
|
+
// EACCES (not executable).
|
|
52
|
+
if ("code" in err) {
|
|
53
|
+
const code = err.code;
|
|
54
|
+
if (code === "ENOENT" || code === "EACCES") return "missing";
|
|
55
|
+
}
|
|
56
|
+
// runCommandWithTimeout flags a genuine deadline breach with `timedOut`.
|
|
57
|
+
if ("timedOut" in err && err.timedOut === true) return "transient";
|
|
58
|
+
}
|
|
59
|
+
// An AbortError reaching here is a deadline/abort under load → transient.
|
|
60
|
+
// (Caller cancellation is intercepted by the retry loop before this runs.)
|
|
61
|
+
if (isAbortError(err)) return "transient";
|
|
62
|
+
// Anything else — including a non-zero exit whose message embeds stderr — is
|
|
63
|
+
// a generic failure: never scan the message to upgrade/downgrade it.
|
|
64
|
+
return "other";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseQmdVersion(version: string | null): QmdVersionTuple | null {
|
|
68
|
+
if (!version) return null;
|
|
69
|
+
const match = version.match(/v?(\d{1,10})\.(\d{1,10})\.(\d{1,10})/i);
|
|
70
|
+
if (!match) return null;
|
|
71
|
+
return [
|
|
72
|
+
Number.parseInt(match[1] ?? "0", 10),
|
|
73
|
+
Number.parseInt(match[2] ?? "0", 10),
|
|
74
|
+
Number.parseInt(match[3] ?? "0", 10),
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseQmdVersionOutput(stdout: string, stderr: string): string | null {
|
|
79
|
+
const lines = `${stdout}\n${stderr}`
|
|
80
|
+
.split("\n")
|
|
81
|
+
.map((s) => s.trim())
|
|
82
|
+
.filter((s) => s.length > 0);
|
|
83
|
+
if (lines.length === 0) return null;
|
|
84
|
+
const semanticLines = lines.filter((line) => parseQmdVersion(line) !== null);
|
|
85
|
+
if (semanticLines.length === 0) return lines[0] ?? null;
|
|
86
|
+
return semanticLines.find((line) => /\bqmd\b/i.test(line)) ?? semanticLines[0] ?? null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function compareQmdVersions(left: QmdVersionTuple | null, right: QmdVersionTuple | null): number {
|
|
90
|
+
if (!left && !right) return 0;
|
|
91
|
+
if (!left) return -1;
|
|
92
|
+
if (!right) return 1;
|
|
93
|
+
for (let i = 0; i < 3; i += 1) {
|
|
94
|
+
if ((left[i] ?? 0) > (right[i] ?? 0)) return 1;
|
|
95
|
+
if ((left[i] ?? 0) < (right[i] ?? 0)) return -1;
|
|
96
|
+
}
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function versionAtLeast(current: QmdVersionTuple | null, target: QmdVersionTuple): boolean {
|
|
101
|
+
return compareQmdVersions(current, target) >= 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function resolveQmdCapabilities(version: string | null): QmdCapabilities {
|
|
105
|
+
const parsedVersion = parseQmdVersion(version);
|
|
106
|
+
const atLeast = (target: QmdVersionTuple): boolean => versionAtLeast(parsedVersion, target);
|
|
107
|
+
return {
|
|
108
|
+
version,
|
|
109
|
+
parsedVersion,
|
|
110
|
+
stableSdk: atLeast([2, 0, 0]),
|
|
111
|
+
unifiedSearch: atLeast([2, 0, 0]),
|
|
112
|
+
getDocumentBody: atLeast([2, 0, 0]),
|
|
113
|
+
maintenanceApi: atLeast([2, 0, 0]),
|
|
114
|
+
legacySkillInstall: atLeast([2, 0, 1]),
|
|
115
|
+
intentHints: atLeast([1, 1, 5]),
|
|
116
|
+
explainTraces: atLeast([1, 1, 2]),
|
|
117
|
+
candidateLimit: atLeast([1, 1, 2]),
|
|
118
|
+
v2McpQueryTool: atLeast([2, 0, 0]),
|
|
119
|
+
structuredSearches: atLeast([2, 0, 0]),
|
|
120
|
+
queryRerankToggle: atLeast([2, 1, 0]),
|
|
121
|
+
chunkStrategy: atLeast([2, 1, 0]),
|
|
122
|
+
qmdBench: atLeast([2, 1, 0]),
|
|
123
|
+
perCollectionModels: atLeast([2, 1, 0]),
|
|
124
|
+
jsonLineNumbers: atLeast([2, 1, 0]),
|
|
125
|
+
editorLinks: atLeast([2, 1, 0]),
|
|
126
|
+
doctor: atLeast([2, 5, 0]),
|
|
127
|
+
versionedSkills: atLeast([2, 5, 0]),
|
|
128
|
+
absoluteSnippetLines: atLeast([2, 5, 0]),
|
|
129
|
+
fullQueryOutput: atLeast([2, 5, 0]),
|
|
130
|
+
forceCpu: atLeast([2, 5, 0]),
|
|
131
|
+
gpuBackendOverride: atLeast([2, 5, 0]),
|
|
132
|
+
embedParallelism: atLeast([2, 5, 0]),
|
|
133
|
+
modelEnvConsistency: atLeast([2, 5, 0]),
|
|
134
|
+
scopedEmbed: atLeast([2, 5, 0]),
|
|
135
|
+
safeStatusDeviceProbe: atLeast([2, 5, 0]),
|
|
136
|
+
mcpIndexSelection: atLeast([2, 5, 0]),
|
|
137
|
+
outputFormatFlag: atLeast([2, 5, 3]),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function shouldAutoUpgradeQmd(
|
|
142
|
+
installedVersion: string | null,
|
|
143
|
+
supportedVersion: string = QMD_SUPPORTED_VERSION
|
|
144
|
+
): boolean {
|
|
145
|
+
const installed = parseQmdVersion(installedVersion);
|
|
146
|
+
const supported = parseQmdVersion(supportedVersion);
|
|
147
|
+
if (!installed || !supported) return false;
|
|
148
|
+
return compareQmdVersions(installed, supported) < 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getQmdPostInstallProbeTargets(
|
|
152
|
+
qmdPath: string,
|
|
153
|
+
qmdPathSource: "configured" | "auto-path" | "auto-fallback"
|
|
154
|
+
): Array<{ qmdPath: string; source: "auto-path" | "auto-fallback" }> {
|
|
155
|
+
const targets: Array<{ qmdPath: string; source: "auto-path" | "auto-fallback" }> = [
|
|
156
|
+
{ qmdPath: "qmd", source: "auto-path" },
|
|
157
|
+
];
|
|
158
|
+
const normalizedPath = qmdPath.trim();
|
|
159
|
+
if (qmdPathSource === "auto-fallback" && normalizedPath.length > 0 && normalizedPath !== "qmd") {
|
|
160
|
+
targets.push({ qmdPath: normalizedPath, source: "auto-fallback" });
|
|
161
|
+
}
|
|
162
|
+
return targets;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function qmdVersionToString(version: QmdVersionTuple): string {
|
|
166
|
+
return `${version[0]}.${version[1]}.${version[2]}`;
|
|
167
|
+
}
|
package/src/qmd.ts
CHANGED
|
@@ -18,6 +18,33 @@ import {
|
|
|
18
18
|
} from "./search/port.js";
|
|
19
19
|
import { launchProcess, type CommandChildProcess } from "./runtime/child-process.js";
|
|
20
20
|
import { mergeEnv } from "./runtime/env.js";
|
|
21
|
+
import {
|
|
22
|
+
classifyProbeFailure,
|
|
23
|
+
compareQmdVersions,
|
|
24
|
+
getQmdPostInstallProbeTargets,
|
|
25
|
+
parseQmdVersion,
|
|
26
|
+
parseQmdVersionOutput,
|
|
27
|
+
QMD_PROBE_RETRY_BACKOFF_MS,
|
|
28
|
+
QMD_PROBE_TIMEOUT_MS,
|
|
29
|
+
QMD_SUPPORTED_VERSION,
|
|
30
|
+
qmdVersionToString,
|
|
31
|
+
resolveQmdCapabilities,
|
|
32
|
+
shouldAutoUpgradeQmd,
|
|
33
|
+
versionAtLeast,
|
|
34
|
+
type QmdProbeFailureKind,
|
|
35
|
+
} from "./qmd-preflight.js";
|
|
36
|
+
// Re-export version-check / probe-preflight helpers (moved to qmd-preflight.ts,
|
|
37
|
+
// issue #1841) so existing imports from "./qmd.js" keep resolving.
|
|
38
|
+
export {
|
|
39
|
+
compareQmdVersions,
|
|
40
|
+
getQmdPostInstallProbeTargets,
|
|
41
|
+
parseQmdVersion,
|
|
42
|
+
parseQmdVersionOutput,
|
|
43
|
+
QMD_SUPPORTED_VERSION,
|
|
44
|
+
resolveQmdCapabilities,
|
|
45
|
+
shouldAutoUpgradeQmd,
|
|
46
|
+
versionAtLeast,
|
|
47
|
+
};
|
|
21
48
|
|
|
22
49
|
export interface QmdClientOptions {
|
|
23
50
|
slowLog?: { enabled: boolean; thresholdMs: number };
|
|
@@ -133,11 +160,9 @@ const QMD_TIMEOUT_MS = 30_000;
|
|
|
133
160
|
// `qmdDaemonTimeoutMs` config knob (issue #1335), e.g. to give CPU-only HyDE
|
|
134
161
|
// queries more headroom. The effective value lives in `this.daemonTimeoutMs`.
|
|
135
162
|
const QMD_DAEMON_TIMEOUT_MS = 8_000;
|
|
136
|
-
const QMD_PROBE_TIMEOUT_MS = 8_000;
|
|
137
163
|
const QMD_UPDATE_BACKOFF_MS = 15 * 60 * 1000; // 15m
|
|
138
164
|
const QMD_EMBED_BACKOFF_MS = 60 * 60 * 1000; // 60m
|
|
139
165
|
const QMD_CLI_WARN_THROTTLE_MS = 15 * 60 * 1000; // 15m
|
|
140
|
-
export const QMD_SUPPORTED_VERSION = "2.5.3";
|
|
141
166
|
const QMD_PACKAGE_NAME = "@tobilu/qmd";
|
|
142
167
|
const QMD_AUTO_UPGRADE_TIMEOUT_MS = 120_000;
|
|
143
168
|
const QMD_AUTO_UPGRADE_CHECK_INTERVAL_MS = 24 * 60 * 60_000;
|
|
@@ -269,118 +294,6 @@ function isVectorDimensionMismatchError(err: unknown): boolean {
|
|
|
269
294
|
);
|
|
270
295
|
}
|
|
271
296
|
|
|
272
|
-
export function parseQmdVersion(version: string | null): QmdVersionTuple | null {
|
|
273
|
-
if (!version) return null;
|
|
274
|
-
const match = version.match(/v?(\d{1,10})\.(\d{1,10})\.(\d{1,10})/i);
|
|
275
|
-
if (!match) return null;
|
|
276
|
-
return [
|
|
277
|
-
Number.parseInt(match[1] ?? "0", 10),
|
|
278
|
-
Number.parseInt(match[2] ?? "0", 10),
|
|
279
|
-
Number.parseInt(match[3] ?? "0", 10),
|
|
280
|
-
];
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function parseQmdVersionOutput(stdout: string, stderr: string): string | null {
|
|
284
|
-
const lines = `${stdout}\n${stderr}`
|
|
285
|
-
.split("\n")
|
|
286
|
-
.map((s) => s.trim())
|
|
287
|
-
.filter((s) => s.length > 0);
|
|
288
|
-
if (lines.length === 0) return null;
|
|
289
|
-
const semanticLines = lines.filter((line) => parseQmdVersion(line) !== null);
|
|
290
|
-
if (semanticLines.length === 0) return lines[0] ?? null;
|
|
291
|
-
return semanticLines.find((line) => /\bqmd\b/i.test(line)) ?? semanticLines[0] ?? null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export function compareQmdVersions(
|
|
295
|
-
left: QmdVersionTuple | null,
|
|
296
|
-
right: QmdVersionTuple | null,
|
|
297
|
-
): number {
|
|
298
|
-
if (!left && !right) return 0;
|
|
299
|
-
if (!left) return -1;
|
|
300
|
-
if (!right) return 1;
|
|
301
|
-
for (let i = 0; i < 3; i += 1) {
|
|
302
|
-
if ((left[i] ?? 0) > (right[i] ?? 0)) return 1;
|
|
303
|
-
if ((left[i] ?? 0) < (right[i] ?? 0)) return -1;
|
|
304
|
-
}
|
|
305
|
-
return 0;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
export function versionAtLeast(
|
|
309
|
-
current: QmdVersionTuple | null,
|
|
310
|
-
target: QmdVersionTuple,
|
|
311
|
-
): boolean {
|
|
312
|
-
return compareQmdVersions(current, target) >= 0;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export function resolveQmdCapabilities(version: string | null): QmdCapabilities {
|
|
316
|
-
const parsedVersion = parseQmdVersion(version);
|
|
317
|
-
const atLeast = (target: QmdVersionTuple): boolean => versionAtLeast(parsedVersion, target);
|
|
318
|
-
return {
|
|
319
|
-
version,
|
|
320
|
-
parsedVersion,
|
|
321
|
-
stableSdk: atLeast([2, 0, 0]),
|
|
322
|
-
unifiedSearch: atLeast([2, 0, 0]),
|
|
323
|
-
getDocumentBody: atLeast([2, 0, 0]),
|
|
324
|
-
maintenanceApi: atLeast([2, 0, 0]),
|
|
325
|
-
legacySkillInstall: atLeast([2, 0, 1]),
|
|
326
|
-
intentHints: atLeast([1, 1, 5]),
|
|
327
|
-
explainTraces: atLeast([1, 1, 2]),
|
|
328
|
-
candidateLimit: atLeast([1, 1, 2]),
|
|
329
|
-
v2McpQueryTool: atLeast([2, 0, 0]),
|
|
330
|
-
structuredSearches: atLeast([2, 0, 0]),
|
|
331
|
-
queryRerankToggle: atLeast([2, 1, 0]),
|
|
332
|
-
chunkStrategy: atLeast([2, 1, 0]),
|
|
333
|
-
qmdBench: atLeast([2, 1, 0]),
|
|
334
|
-
perCollectionModels: atLeast([2, 1, 0]),
|
|
335
|
-
jsonLineNumbers: atLeast([2, 1, 0]),
|
|
336
|
-
editorLinks: atLeast([2, 1, 0]),
|
|
337
|
-
doctor: atLeast([2, 5, 0]),
|
|
338
|
-
versionedSkills: atLeast([2, 5, 0]),
|
|
339
|
-
absoluteSnippetLines: atLeast([2, 5, 0]),
|
|
340
|
-
fullQueryOutput: atLeast([2, 5, 0]),
|
|
341
|
-
forceCpu: atLeast([2, 5, 0]),
|
|
342
|
-
gpuBackendOverride: atLeast([2, 5, 0]),
|
|
343
|
-
embedParallelism: atLeast([2, 5, 0]),
|
|
344
|
-
modelEnvConsistency: atLeast([2, 5, 0]),
|
|
345
|
-
scopedEmbed: atLeast([2, 5, 0]),
|
|
346
|
-
safeStatusDeviceProbe: atLeast([2, 5, 0]),
|
|
347
|
-
mcpIndexSelection: atLeast([2, 5, 0]),
|
|
348
|
-
outputFormatFlag: atLeast([2, 5, 3]),
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export function shouldAutoUpgradeQmd(
|
|
353
|
-
installedVersion: string | null,
|
|
354
|
-
supportedVersion: string = QMD_SUPPORTED_VERSION,
|
|
355
|
-
): boolean {
|
|
356
|
-
const installed = parseQmdVersion(installedVersion);
|
|
357
|
-
const supported = parseQmdVersion(supportedVersion);
|
|
358
|
-
if (!installed || !supported) return false;
|
|
359
|
-
return compareQmdVersions(installed, supported) < 0;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export function getQmdPostInstallProbeTargets(
|
|
363
|
-
qmdPath: string,
|
|
364
|
-
qmdPathSource: "configured" | "auto-path" | "auto-fallback",
|
|
365
|
-
): Array<{ qmdPath: string; source: "auto-path" | "auto-fallback" }> {
|
|
366
|
-
const targets: Array<{ qmdPath: string; source: "auto-path" | "auto-fallback" }> = [
|
|
367
|
-
{ qmdPath: "qmd", source: "auto-path" },
|
|
368
|
-
];
|
|
369
|
-
const normalizedPath = qmdPath.trim();
|
|
370
|
-
if (
|
|
371
|
-
qmdPathSource === "auto-fallback" &&
|
|
372
|
-
normalizedPath.length > 0 &&
|
|
373
|
-
normalizedPath !== "qmd"
|
|
374
|
-
) {
|
|
375
|
-
targets.push({ qmdPath: normalizedPath, source: "auto-fallback" });
|
|
376
|
-
}
|
|
377
|
-
return targets;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function qmdVersionToString(version: QmdVersionTuple): string {
|
|
381
|
-
return `${version[0]}.${version[1]}.${version[2]}`;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
297
|
function normalizeSearchOptions(options?: SearchQueryOptions): SearchQueryOptions | undefined {
|
|
385
298
|
if (!options) return undefined;
|
|
386
299
|
const intent = typeof options.intent === "string" ? options.intent.trim() : "";
|
|
@@ -671,7 +584,13 @@ function runCommandWithTimeout(
|
|
|
671
584
|
settled = true;
|
|
672
585
|
cleanup();
|
|
673
586
|
child.kill("SIGKILL");
|
|
674
|
-
|
|
587
|
+
// Flag the deadline breach so classifyProbeFailure can tell a genuine
|
|
588
|
+
// timeout from a non-zero-exit error whose message merely embeds stderr.
|
|
589
|
+
reject(
|
|
590
|
+
Object.assign(new Error(`${label} timed out after ${options.timeoutMs}ms`), {
|
|
591
|
+
timedOut: true as const,
|
|
592
|
+
}),
|
|
593
|
+
);
|
|
675
594
|
}, options.timeoutMs);
|
|
676
595
|
const onAbort = () => {
|
|
677
596
|
if (settled) return;
|
|
@@ -1437,6 +1356,18 @@ export class QmdClient implements SearchBackend {
|
|
|
1437
1356
|
}
|
|
1438
1357
|
}
|
|
1439
1358
|
|
|
1359
|
+
/**
|
|
1360
|
+
* Run a single `qmd --version` probe against `qmdPath`. Extracted as its own
|
|
1361
|
+
* method so tests can inject a fake probe runner that exercises the preflight
|
|
1362
|
+
* retry/classification paths without spawning a real qmd binary. Issue #1841.
|
|
1363
|
+
*/
|
|
1364
|
+
private runVersionProbe(
|
|
1365
|
+
qmdPath: string,
|
|
1366
|
+
signal?: AbortSignal,
|
|
1367
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
1368
|
+
return runQmd(["--version"], QMD_PROBE_TIMEOUT_MS, qmdPath, signal, this.qmdRuntimeEnv);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1440
1371
|
private async probeCli(
|
|
1441
1372
|
options: {
|
|
1442
1373
|
allowAutoUpgrade?: boolean;
|
|
@@ -1490,24 +1421,79 @@ export class QmdClient implements SearchBackend {
|
|
|
1490
1421
|
};
|
|
1491
1422
|
|
|
1492
1423
|
if (this.configuredQmdPath) {
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1424
|
+
const configuredPath = this.configuredQmdPath;
|
|
1425
|
+
// Retry only TRANSIENT (timeout/abort) probes of the configured path: a
|
|
1426
|
+
// binary that resolved and ran before is more likely slow under load than
|
|
1427
|
+
// gone. ENOENT/EACCES is a hard misconfiguration and must fail fast.
|
|
1428
|
+
// Issue #1841.
|
|
1429
|
+
let failureKind: QmdProbeFailureKind = "other";
|
|
1430
|
+
let retries = 0;
|
|
1431
|
+
let callerCancelled = false;
|
|
1432
|
+
for (let attempt = 0; attempt <= QMD_PROBE_RETRY_BACKOFF_MS.length; attempt += 1) {
|
|
1433
|
+
try {
|
|
1434
|
+
const result = await this.runVersionProbe(configuredPath, options.signal);
|
|
1435
|
+
await recordProbeSuccess(result, configuredPath, "configured");
|
|
1436
|
+
return true;
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
markProbeFailure(err);
|
|
1439
|
+
configuredProbeFailure = this.lastCliProbeError;
|
|
1440
|
+
// Detect CALLER cancellation FIRST: an aborted signal / AbortError is
|
|
1441
|
+
// non-actionable noise — the caller chose to stop. Other QMD paths
|
|
1442
|
+
// treat it via `isCallerCancellation`; do the same here so a cancelled
|
|
1443
|
+
// probe never reads as a transient host-load failure. Issue #1841.
|
|
1444
|
+
if (isCallerCancellation(err, options.signal)) {
|
|
1445
|
+
callerCancelled = true;
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
failureKind = classifyProbeFailure(err);
|
|
1449
|
+
// Never retry a hard misconfiguration (missing/not-executable) — only
|
|
1450
|
+
// transient slowness. (Caller cancellation is handled above.)
|
|
1451
|
+
if (
|
|
1452
|
+
failureKind === "transient" &&
|
|
1453
|
+
attempt < QMD_PROBE_RETRY_BACKOFF_MS.length
|
|
1454
|
+
) {
|
|
1455
|
+
try {
|
|
1456
|
+
await sleepWithSignal(
|
|
1457
|
+
QMD_PROBE_RETRY_BACKOFF_MS[attempt] ?? 500,
|
|
1458
|
+
options.signal,
|
|
1459
|
+
);
|
|
1460
|
+
} catch {
|
|
1461
|
+
// Aborted mid-backoff: caller cancelled — stop, emit no warning.
|
|
1462
|
+
callerCancelled = true;
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
retries += 1;
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
break;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
// Distinct operator-facing warnings so a slow binary is not mistaken for
|
|
1472
|
+
// a misconfiguration (and vice versa). Do not hard-fail here: fall through
|
|
1473
|
+
// to PATH/fallback probing so recall stays healthy when config is stale.
|
|
1474
|
+
// Caller cancellation is silent noise (issue #1841): suppress all warnings
|
|
1475
|
+
// for it exactly as the other QMD paths do.
|
|
1476
|
+
if (!callerCancelled) {
|
|
1477
|
+
if (failureKind === "transient") {
|
|
1478
|
+
this.logCliProbeWarning(
|
|
1479
|
+
`QMD: qmdPath ${configuredPath} version check timed out (host may be under load); retried ${retries} time${retries === 1 ? "" : "s"}`,
|
|
1480
|
+
);
|
|
1481
|
+
} else if (failureKind === "missing") {
|
|
1482
|
+
this.logCliProbeWarning(
|
|
1483
|
+
`QMD: configured qmdPath not found or not executable (${configuredPath}): ${this.lastCliProbeError}`,
|
|
1484
|
+
);
|
|
1485
|
+
} else {
|
|
1486
|
+
// Preserve the historical generic message for exit-code/other failures.
|
|
1487
|
+
this.logCliProbeWarning(
|
|
1488
|
+
`QMD: configured qmdPath failed (${configuredPath}): ${this.lastCliProbeError}`,
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1505
1491
|
}
|
|
1506
1492
|
}
|
|
1507
1493
|
|
|
1508
1494
|
// Try PATH first
|
|
1509
1495
|
try {
|
|
1510
|
-
const result = await
|
|
1496
|
+
const result = await this.runVersionProbe("qmd", options.signal);
|
|
1511
1497
|
await recordProbeSuccess(result, "qmd", "auto-path");
|
|
1512
1498
|
return true;
|
|
1513
1499
|
} catch (err) {
|
|
@@ -1515,7 +1501,7 @@ export class QmdClient implements SearchBackend {
|
|
|
1515
1501
|
// Try fallback paths
|
|
1516
1502
|
for (const fallbackPath of this.qmdFallbackPaths) {
|
|
1517
1503
|
try {
|
|
1518
|
-
const result = await
|
|
1504
|
+
const result = await this.runVersionProbe(fallbackPath, options.signal);
|
|
1519
1505
|
await recordProbeSuccess(result, fallbackPath, "auto-fallback");
|
|
1520
1506
|
log.info(`QMD: found at ${fallbackPath}`);
|
|
1521
1507
|
return true;
|