@remnic/core 9.69.63 → 9.69.65
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 +24 -24
- package/dist/access-authorization-probe.js +25 -25
- package/dist/access-boundary.js +24 -24
- package/dist/access-cli.js +65 -65
- package/dist/access-extraction-force-flush.js +24 -24
- package/dist/access-http-briefing.js +25 -25
- package/dist/access-http-query.js +26 -26
- package/dist/access-http.js +41 -41
- package/dist/access-identity-continuity-surface.js +24 -24
- package/dist/access-lcm-surface.js +24 -24
- package/dist/access-mcp-output-schemas.d.ts.map +1 -1
- package/dist/access-mcp-output-schemas.js +1 -1
- package/dist/access-mcp.js +34 -34
- package/dist/access-namespace-preflight.js +24 -24
- package/dist/access-observe-write-surface.js +24 -24
- package/dist/access-operations-batch.js +25 -25
- package/dist/access-operations.js +32 -32
- package/dist/access-recall-concurrency.js +24 -24
- package/dist/access-recall-response.d.ts.map +1 -1
- package/dist/access-recall-response.js +24 -24
- package/dist/access-recall-surface.js +24 -24
- package/dist/access-service.d.ts +4 -0
- package/dist/access-service.d.ts.map +1 -1
- package/dist/access-service.js +24 -24
- package/dist/access-surface-catalog.js +26 -26
- package/dist/active-recall.js +9 -9
- package/dist/briefing.js +10 -10
- package/dist/buffer.js +5 -5
- package/dist/causal-consolidation.js +11 -11
- package/dist/{chunk-52VL6LCY.js → chunk-2VH2JWYN.js} +2 -2
- package/dist/{chunk-VXGB766B.js → chunk-2ZVSQPAA.js} +30 -14
- package/dist/chunk-2ZVSQPAA.js.map +1 -0
- package/dist/{chunk-G4WZDQ32.js → chunk-3NG5OOCX.js} +2 -2
- package/dist/{chunk-KG2OOAFF.js → chunk-3NTVCMDD.js} +2 -2
- package/dist/{chunk-7OC3Q5X4.js → chunk-3S4R2HA5.js} +2 -2
- package/dist/{chunk-RA3C46VX.js → chunk-4RWPQZYD.js} +2 -2
- package/dist/{chunk-B5GMOOY2.js → chunk-5AABWM72.js} +3 -3
- package/dist/{chunk-KRPMISWO.js → chunk-5C765XYU.js} +2 -2
- package/dist/{chunk-24TJ77TT.js → chunk-6YTL3JYX.js} +2 -2
- package/dist/{chunk-F4PEETM4.js → chunk-755VRFBS.js} +2 -2
- package/dist/{chunk-HNOKNHFO.js → chunk-7L747CFR.js} +2 -2
- package/dist/{chunk-WPGR3RW4.js → chunk-7VO6E5UD.js} +2 -2
- package/dist/{chunk-LMLPLIT4.js → chunk-AQ6IBVFQ.js} +2 -2
- package/dist/{chunk-LVGVXG22.js → chunk-B2JE56NB.js} +2 -2
- package/dist/{chunk-L4WBADKG.js → chunk-B5EZWPSM.js} +2 -2
- package/dist/{chunk-EIU642TN.js → chunk-BE4GMH45.js} +2 -2
- package/dist/{chunk-JTTLH646.js → chunk-C2W6JKQQ.js} +2 -2
- package/dist/{chunk-FG5QU7A2.js → chunk-CFCAFAJC.js} +2 -2
- package/dist/{chunk-WAR75CRY.js → chunk-CZM4MHZF.js} +34 -29
- package/dist/chunk-CZM4MHZF.js.map +1 -0
- package/dist/{chunk-LAEW4S43.js → chunk-DDY7LJMP.js} +2 -2
- package/dist/{chunk-4EEDRKRT.js → chunk-DT33WNG2.js} +4 -4
- package/dist/{chunk-SZAWZPY6.js → chunk-E7SKAUEW.js} +7 -7
- package/dist/{chunk-NRVGODSE.js → chunk-EK4MOTHM.js} +6 -6
- package/dist/{chunk-QEGKMZNQ.js → chunk-FO5NDQZH.js} +3 -3
- package/dist/chunk-G26YNEOC.js +37 -0
- package/dist/chunk-G26YNEOC.js.map +1 -0
- package/dist/{chunk-GMHWCPLP.js → chunk-GZLSIPYU.js} +2 -2
- package/dist/{chunk-ZFUPRODV.js → chunk-HPWJ5B7D.js} +39 -3
- package/dist/chunk-HPWJ5B7D.js.map +1 -0
- package/dist/{chunk-SEHKTE4F.js → chunk-I4IKTNZP.js} +3 -3
- package/dist/{chunk-W65KK3X6.js → chunk-II4TIQCU.js} +2 -2
- package/dist/{chunk-FWVDTLRK.js → chunk-IJQEVNEF.js} +1 -1
- package/dist/{chunk-KXJ7XYUT.js → chunk-IOUQUXHO.js} +2 -2
- package/dist/{chunk-V3JM4XEI.js → chunk-ITLFUILH.js} +2 -2
- package/dist/{chunk-W5UAB6EH.js → chunk-IULYVB5X.js} +5 -5
- package/dist/{chunk-Z3YQOV45.js → chunk-JLU7WK22.js} +2 -2
- package/dist/{chunk-NRTVZSPO.js → chunk-L4HT5QZ5.js} +2 -2
- package/dist/{chunk-5QZHDS4X.js → chunk-LI4IWBLN.js} +2 -2
- package/dist/{chunk-3MSMBVA3.js → chunk-LQ4OSM5V.js} +3 -3
- package/dist/{chunk-2OUSWDY6.js → chunk-M3TMVHQ2.js} +2 -2
- package/dist/{chunk-IDRSTHVR.js → chunk-MB3HMRXM.js} +2 -2
- package/dist/{chunk-PZEXA6CM.js → chunk-MUGLJAZ3.js} +2 -2
- package/dist/{chunk-ZOASYXBS.js → chunk-MY2PLXSM.js} +6 -2
- package/dist/chunk-MY2PLXSM.js.map +1 -0
- package/dist/{chunk-FEHVG4C4.js → chunk-NYN4ZMPL.js} +2 -2
- package/dist/{chunk-ZDZJ2PG4.js → chunk-O7QF3TCV.js} +17 -17
- package/dist/{chunk-KV44HTMJ.js → chunk-OBD77TYC.js} +2 -2
- package/dist/{chunk-GYNCJSQ7.js → chunk-OCEIHKH5.js} +3 -3
- package/dist/{chunk-FTNV5476.js → chunk-PSGRZFBH.js} +10 -10
- package/dist/{chunk-FQGY7XB2.js → chunk-S52VKUUW.js} +2 -2
- package/dist/{chunk-D7MAYFQP.js → chunk-S7LWX2PQ.js} +2 -2
- package/dist/{chunk-TYP7GYRI.js → chunk-SB4ZWVKI.js} +2 -2
- package/dist/{chunk-KVO2BTA4.js → chunk-T2YX56QI.js} +2 -2
- package/dist/{chunk-G6NQ3L4T.js → chunk-T3FMRLWQ.js} +2 -2
- package/dist/{chunk-CDQO5DCD.js → chunk-TO3G56GM.js} +2 -2
- package/dist/{chunk-WEQZ5OYK.js → chunk-U6GKPSZH.js} +2 -2
- package/dist/{chunk-FM5HN7LL.js → chunk-WQC54W25.js} +9 -17
- package/dist/chunk-WQC54W25.js.map +1 -0
- package/dist/{chunk-XDH5MO6L.js → chunk-WZ33AIOP.js} +2 -2
- package/dist/{chunk-U6HXB2OG.js → chunk-XDIFADVN.js} +2 -2
- package/dist/{chunk-SVWYZXIL.js → chunk-XDOXR3U6.js} +2 -2
- package/dist/{chunk-VKDWYDFQ.js → chunk-XGLBVYEA.js} +5 -5
- package/dist/{chunk-5Y7VCE3A.js → chunk-XJNSOI23.js} +2 -2
- package/dist/{chunk-LDP6LBDS.js → chunk-Y3AFTYK4.js} +2 -2
- package/dist/{chunk-TPRBJF46.js → chunk-YGJMMMPN.js} +2 -2
- package/dist/{chunk-WKNWC46C.js → chunk-YIDBKTZJ.js} +2 -2
- package/dist/{chunk-F5ZCNJ3V.js → chunk-ZPPWW576.js} +2 -2
- package/dist/{chunk-TQC6ELJO.js → chunk-ZSXSMVW6.js} +2 -2
- package/dist/{chunk-6RD7MSIO.js → chunk-ZUT4FK5W.js} +2 -2
- package/dist/cli.js +51 -51
- package/dist/coding/export-okf-codegraph.js +9 -9
- package/dist/compounding/engine.js +10 -10
- package/dist/config.js +9 -9
- package/dist/connectors/codex-materialize-runner.js +10 -10
- package/dist/connectors/index.js +10 -10
- package/dist/convergence-refresh.js +12 -12
- package/dist/corpus-watermark.js +12 -12
- package/dist/entity-retrieval.js +10 -10
- package/dist/external-wiki-access.js +25 -25
- package/dist/extraction.js +14 -14
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/index.js +65 -65
- package/dist/location/index.js +12 -12
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/memory-browse-operations.js +25 -25
- package/dist/memory-browse.js +5 -5
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/search.js +8 -8
- package/dist/namespaces/storage.js +10 -10
- package/dist/operator-doctor-corpus.js +13 -13
- package/dist/operator-toolkit.js +18 -18
- package/dist/orchestration/maintenance.js +18 -18
- package/dist/orchestration/recall-internal.d.ts.map +1 -1
- package/dist/orchestrator.js +65 -65
- package/dist/qmd.js +2 -2
- package/dist/recall-composition-decision.d.ts +9 -0
- package/dist/recall-composition-decision.d.ts.map +1 -1
- package/dist/recall-composition-decision.js +2 -1
- package/dist/recall-navigation-operations.js +25 -25
- package/dist/recall-navigation.js +24 -24
- package/dist/recall-state-view-admission.js +5 -5
- package/dist/recall-why-service.d.ts.map +1 -1
- package/dist/recall-why-service.js +5 -5
- package/dist/resume-bundles.js +9 -9
- package/dist/search/factory.js +7 -7
- package/dist/search/index.js +7 -7
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +9 -0
- package/dist/search/port.d.ts.map +1 -1
- package/dist/search/port.js +7 -1
- package/dist/search/remote-backend.js +2 -2
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +10 -10
- package/dist/semantic-rule-verifier.js +10 -10
- package/dist/source-agent-qualifier.js +13 -13
- package/dist/standup/build.js +10 -10
- package/dist/storage.js +9 -9
- package/dist/support-passport/index.js +27 -27
- package/dist/transfer/export-okf.js +10 -10
- package/dist/verified-recall.js +10 -10
- package/dist/who-knows-cli.js +11 -11
- package/dist/who-knows.js +10 -10
- package/package.json +2 -2
- package/src/access-mcp-output-schemas.ts +4 -0
- package/src/access-recall-response.ts +26 -1
- package/src/access-service.ts +1 -0
- package/src/orchestration/recall-internal.ts +8 -5
- package/src/recall-composition-decision.test.ts +75 -0
- package/src/recall-composition-decision.ts +52 -3
- package/src/recall-retrieval-failure.test.ts +245 -0
- package/src/recall-why-service.ts +8 -24
- package/src/search/port.ts +32 -0
- package/dist/chunk-FM5HN7LL.js.map +0 -1
- package/dist/chunk-U2VDCFXW.js +0 -19
- package/dist/chunk-U2VDCFXW.js.map +0 -1
- package/dist/chunk-VXGB766B.js.map +0 -1
- package/dist/chunk-WAR75CRY.js.map +0 -1
- package/dist/chunk-ZFUPRODV.js.map +0 -1
- package/dist/chunk-ZOASYXBS.js.map +0 -1
- /package/dist/{chunk-52VL6LCY.js.map → chunk-2VH2JWYN.js.map} +0 -0
- /package/dist/{chunk-G4WZDQ32.js.map → chunk-3NG5OOCX.js.map} +0 -0
- /package/dist/{chunk-KG2OOAFF.js.map → chunk-3NTVCMDD.js.map} +0 -0
- /package/dist/{chunk-7OC3Q5X4.js.map → chunk-3S4R2HA5.js.map} +0 -0
- /package/dist/{chunk-RA3C46VX.js.map → chunk-4RWPQZYD.js.map} +0 -0
- /package/dist/{chunk-B5GMOOY2.js.map → chunk-5AABWM72.js.map} +0 -0
- /package/dist/{chunk-KRPMISWO.js.map → chunk-5C765XYU.js.map} +0 -0
- /package/dist/{chunk-24TJ77TT.js.map → chunk-6YTL3JYX.js.map} +0 -0
- /package/dist/{chunk-F4PEETM4.js.map → chunk-755VRFBS.js.map} +0 -0
- /package/dist/{chunk-HNOKNHFO.js.map → chunk-7L747CFR.js.map} +0 -0
- /package/dist/{chunk-WPGR3RW4.js.map → chunk-7VO6E5UD.js.map} +0 -0
- /package/dist/{chunk-LMLPLIT4.js.map → chunk-AQ6IBVFQ.js.map} +0 -0
- /package/dist/{chunk-LVGVXG22.js.map → chunk-B2JE56NB.js.map} +0 -0
- /package/dist/{chunk-L4WBADKG.js.map → chunk-B5EZWPSM.js.map} +0 -0
- /package/dist/{chunk-EIU642TN.js.map → chunk-BE4GMH45.js.map} +0 -0
- /package/dist/{chunk-JTTLH646.js.map → chunk-C2W6JKQQ.js.map} +0 -0
- /package/dist/{chunk-FG5QU7A2.js.map → chunk-CFCAFAJC.js.map} +0 -0
- /package/dist/{chunk-LAEW4S43.js.map → chunk-DDY7LJMP.js.map} +0 -0
- /package/dist/{chunk-4EEDRKRT.js.map → chunk-DT33WNG2.js.map} +0 -0
- /package/dist/{chunk-SZAWZPY6.js.map → chunk-E7SKAUEW.js.map} +0 -0
- /package/dist/{chunk-NRVGODSE.js.map → chunk-EK4MOTHM.js.map} +0 -0
- /package/dist/{chunk-QEGKMZNQ.js.map → chunk-FO5NDQZH.js.map} +0 -0
- /package/dist/{chunk-GMHWCPLP.js.map → chunk-GZLSIPYU.js.map} +0 -0
- /package/dist/{chunk-SEHKTE4F.js.map → chunk-I4IKTNZP.js.map} +0 -0
- /package/dist/{chunk-W65KK3X6.js.map → chunk-II4TIQCU.js.map} +0 -0
- /package/dist/{chunk-FWVDTLRK.js.map → chunk-IJQEVNEF.js.map} +0 -0
- /package/dist/{chunk-KXJ7XYUT.js.map → chunk-IOUQUXHO.js.map} +0 -0
- /package/dist/{chunk-V3JM4XEI.js.map → chunk-ITLFUILH.js.map} +0 -0
- /package/dist/{chunk-W5UAB6EH.js.map → chunk-IULYVB5X.js.map} +0 -0
- /package/dist/{chunk-Z3YQOV45.js.map → chunk-JLU7WK22.js.map} +0 -0
- /package/dist/{chunk-NRTVZSPO.js.map → chunk-L4HT5QZ5.js.map} +0 -0
- /package/dist/{chunk-5QZHDS4X.js.map → chunk-LI4IWBLN.js.map} +0 -0
- /package/dist/{chunk-3MSMBVA3.js.map → chunk-LQ4OSM5V.js.map} +0 -0
- /package/dist/{chunk-2OUSWDY6.js.map → chunk-M3TMVHQ2.js.map} +0 -0
- /package/dist/{chunk-IDRSTHVR.js.map → chunk-MB3HMRXM.js.map} +0 -0
- /package/dist/{chunk-PZEXA6CM.js.map → chunk-MUGLJAZ3.js.map} +0 -0
- /package/dist/{chunk-FEHVG4C4.js.map → chunk-NYN4ZMPL.js.map} +0 -0
- /package/dist/{chunk-ZDZJ2PG4.js.map → chunk-O7QF3TCV.js.map} +0 -0
- /package/dist/{chunk-KV44HTMJ.js.map → chunk-OBD77TYC.js.map} +0 -0
- /package/dist/{chunk-GYNCJSQ7.js.map → chunk-OCEIHKH5.js.map} +0 -0
- /package/dist/{chunk-FTNV5476.js.map → chunk-PSGRZFBH.js.map} +0 -0
- /package/dist/{chunk-FQGY7XB2.js.map → chunk-S52VKUUW.js.map} +0 -0
- /package/dist/{chunk-D7MAYFQP.js.map → chunk-S7LWX2PQ.js.map} +0 -0
- /package/dist/{chunk-TYP7GYRI.js.map → chunk-SB4ZWVKI.js.map} +0 -0
- /package/dist/{chunk-KVO2BTA4.js.map → chunk-T2YX56QI.js.map} +0 -0
- /package/dist/{chunk-G6NQ3L4T.js.map → chunk-T3FMRLWQ.js.map} +0 -0
- /package/dist/{chunk-CDQO5DCD.js.map → chunk-TO3G56GM.js.map} +0 -0
- /package/dist/{chunk-WEQZ5OYK.js.map → chunk-U6GKPSZH.js.map} +0 -0
- /package/dist/{chunk-XDH5MO6L.js.map → chunk-WZ33AIOP.js.map} +0 -0
- /package/dist/{chunk-U6HXB2OG.js.map → chunk-XDIFADVN.js.map} +0 -0
- /package/dist/{chunk-SVWYZXIL.js.map → chunk-XDOXR3U6.js.map} +0 -0
- /package/dist/{chunk-VKDWYDFQ.js.map → chunk-XGLBVYEA.js.map} +0 -0
- /package/dist/{chunk-5Y7VCE3A.js.map → chunk-XJNSOI23.js.map} +0 -0
- /package/dist/{chunk-LDP6LBDS.js.map → chunk-Y3AFTYK4.js.map} +0 -0
- /package/dist/{chunk-TPRBJF46.js.map → chunk-YGJMMMPN.js.map} +0 -0
- /package/dist/{chunk-WKNWC46C.js.map → chunk-YIDBKTZJ.js.map} +0 -0
- /package/dist/{chunk-F5ZCNJ3V.js.map → chunk-ZPPWW576.js.map} +0 -0
- /package/dist/{chunk-TQC6ELJO.js.map → chunk-ZSXSMVW6.js.map} +0 -0
- /package/dist/{chunk-6RD7MSIO.js.map → chunk-ZUT4FK5W.js.map} +0 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3082: recall must distinguish a QMD daemon timeout from a genuine
|
|
3
|
+
* empty retrieval. Internally the pipeline already collected the
|
|
4
|
+
* degradation; this file asserts it is no longer flattened away by
|
|
5
|
+
* the time it reaches orchestrator.recall / EngramAccessService.recall
|
|
6
|
+
* / the MCP tool payload.
|
|
7
|
+
*
|
|
8
|
+
* Fixture data is synthetic and the store is an empty temp directory.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
|
|
17
|
+
import { EngramMcpServer } from "./access-mcp.js";
|
|
18
|
+
import { EngramAccessService } from "./access-service.js";
|
|
19
|
+
import type { EngramAccessRecallRequest, EngramAccessRecallResponse } from "./access-service.js";
|
|
20
|
+
import { parseConfig } from "./config.js";
|
|
21
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
22
|
+
import {
|
|
23
|
+
MEMORY_CONTEXT_UNAVAILABLE_NOTE,
|
|
24
|
+
type RecallContextComposition,
|
|
25
|
+
} from "./recall-context-composition.js";
|
|
26
|
+
import type { SearchBackend, SearchDegradation, SearchExecutionOptions } from "./search/port.js";
|
|
27
|
+
import { DEFAULT_RECALL_DISCLOSURE } from "./types.js";
|
|
28
|
+
|
|
29
|
+
const TIMEOUT_DEGRADATION: SearchDegradation = {
|
|
30
|
+
backend: "qmd",
|
|
31
|
+
code: "daemon_timeout",
|
|
32
|
+
detail: "no response within the deadline",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const QUERY = "what writing rules apply here?";
|
|
36
|
+
|
|
37
|
+
function searchBackend(observed: { calls: number }, degrade: boolean): SearchBackend {
|
|
38
|
+
const search = (execution?: SearchExecutionOptions) => {
|
|
39
|
+
observed.calls += 1;
|
|
40
|
+
if (degrade) execution?.onDegradation?.(TIMEOUT_DEGRADATION);
|
|
41
|
+
return [];
|
|
42
|
+
};
|
|
43
|
+
const backend: Partial<SearchBackend> = {
|
|
44
|
+
async probe() {
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
isAvailable() {
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
debugStatus() {
|
|
51
|
+
return degrade ? "backend=timeout-stub" : "backend=empty-stub";
|
|
52
|
+
},
|
|
53
|
+
async search(_query, _collection, _maxResults, _options, execution) {
|
|
54
|
+
return search(execution);
|
|
55
|
+
},
|
|
56
|
+
async searchGlobal(_query, _maxResults, execution) {
|
|
57
|
+
return search(execution);
|
|
58
|
+
},
|
|
59
|
+
async bm25Search(_query, _collection, _maxResults, execution) {
|
|
60
|
+
return search(execution);
|
|
61
|
+
},
|
|
62
|
+
async vectorSearch(_query, _collection, _maxResults, execution) {
|
|
63
|
+
return search(execution);
|
|
64
|
+
},
|
|
65
|
+
async hybridSearch(_query, _collection, _maxResults, execution) {
|
|
66
|
+
return search(execution);
|
|
67
|
+
},
|
|
68
|
+
async update() {},
|
|
69
|
+
async updateCollection() {},
|
|
70
|
+
async embed() {},
|
|
71
|
+
async embedCollection() {},
|
|
72
|
+
async ensureCollection() {
|
|
73
|
+
return "skipped";
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
return backend as SearchBackend;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function withOrchestrator(
|
|
80
|
+
prefix: string,
|
|
81
|
+
degrade: boolean,
|
|
82
|
+
run: (orchestrator: Orchestrator, observed: { calls: number }) => Promise<void>,
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), prefix));
|
|
85
|
+
const orchestrator = new Orchestrator(
|
|
86
|
+
parseConfig({
|
|
87
|
+
memoryDir,
|
|
88
|
+
workspaceDir: memoryDir,
|
|
89
|
+
qmdEnabled: true,
|
|
90
|
+
embeddingFallbackEnabled: false,
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
const observed = { calls: 0 };
|
|
94
|
+
// Unchecked cast with reason: `qmd` is assigned post-construction because the
|
|
95
|
+
// backend is built by the search factory, which would reach for a real daemon.
|
|
96
|
+
const withBackend = orchestrator as unknown as { qmd: SearchBackend };
|
|
97
|
+
withBackend.qmd = searchBackend(observed, degrade);
|
|
98
|
+
try {
|
|
99
|
+
await orchestrator.initialize();
|
|
100
|
+
await orchestrator.deferredReady;
|
|
101
|
+
await run(orchestrator, observed);
|
|
102
|
+
} finally {
|
|
103
|
+
await orchestrator.destroy();
|
|
104
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
test("QMD daemon_timeout mid-recall is distinguishable from genuine empty at the orchestrator", async () => {
|
|
109
|
+
await withOrchestrator("remnic-recall-timeout-", true, async (orchestrator, observed) => {
|
|
110
|
+
let composition: RecallContextComposition | undefined;
|
|
111
|
+
const context = await orchestrator.recall(QUERY, "timeout-session", {
|
|
112
|
+
onContextComposition: (value) => {
|
|
113
|
+
composition = value;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
assert.ok(observed.calls > 0, "recall must consult the backend");
|
|
118
|
+
assert.equal(composition?.degradation?.state, "missing");
|
|
119
|
+
assert.equal(composition?.degradation?.reason, "backend-unavailable");
|
|
120
|
+
assert.match(composition?.degradation?.detail ?? "", /qmd:daemon_timeout/);
|
|
121
|
+
assert.equal(context, MEMORY_CONTEXT_UNAVAILABLE_NOTE);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("genuine empty retrieval stays marker-free at the orchestrator", async () => {
|
|
126
|
+
await withOrchestrator("remnic-recall-empty-", false, async (orchestrator, observed) => {
|
|
127
|
+
let composition: RecallContextComposition | undefined;
|
|
128
|
+
const context = await orchestrator.recall(QUERY, "empty-session", {
|
|
129
|
+
onContextComposition: (value) => {
|
|
130
|
+
composition = value;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
assert.ok(observed.calls > 0, "recall must consult the backend");
|
|
135
|
+
assert.equal(composition?.degradation, undefined);
|
|
136
|
+
assert.equal(context.includes("Memory context unavailable"), false);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("access recall surfaces retrievalFailure on daemon timeout and omits it on genuine empty", async () => {
|
|
141
|
+
await withOrchestrator("remnic-recall-access-timeout-", true, async (timedOut, observed) => {
|
|
142
|
+
const failed = await new EngramAccessService(timedOut).recall({
|
|
143
|
+
query: QUERY,
|
|
144
|
+
sessionKey: "access-timeout",
|
|
145
|
+
});
|
|
146
|
+
assert.ok(observed.calls > 0, "timeout recall must consult the backend");
|
|
147
|
+
assert.equal(failed.retrievalFailure?.reason, "backend_unavailable");
|
|
148
|
+
assert.match(failed.retrievalFailure?.detail ?? "", /qmd:daemon_timeout/);
|
|
149
|
+
assert.equal(failed.contextComposition?.degradation?.state, "missing");
|
|
150
|
+
assert.match(failed.context, /memory context unavailable/i);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await withOrchestrator("remnic-recall-access-empty-", false, async (empty, observed) => {
|
|
154
|
+
const okEmpty = await new EngramAccessService(empty).recall({
|
|
155
|
+
query: QUERY,
|
|
156
|
+
sessionKey: "access-empty",
|
|
157
|
+
});
|
|
158
|
+
assert.ok(observed.calls > 0, "empty recall must consult the backend");
|
|
159
|
+
assert.equal(okEmpty.retrievalFailure, undefined);
|
|
160
|
+
assert.equal(okEmpty.contextComposition?.degradation, undefined);
|
|
161
|
+
assert.equal(okEmpty.context.includes("Memory context unavailable"), false);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("a reused degradationSink cannot mark a later healthy empty recall as failed", async () => {
|
|
166
|
+
await withOrchestrator("remnic-recall-reused-sink-", true, async (orchestrator, observed) => {
|
|
167
|
+
const sink: SearchDegradation[] = [];
|
|
168
|
+
await orchestrator.recall(QUERY, "first-timeout", { degradationSink: sink });
|
|
169
|
+
assert.ok(sink.length > 0, "the first recall must record the timeout");
|
|
170
|
+
|
|
171
|
+
const withBackend = orchestrator as unknown as { qmd: SearchBackend };
|
|
172
|
+
withBackend.qmd = searchBackend(observed, false);
|
|
173
|
+
let composition: RecallContextComposition | undefined;
|
|
174
|
+
await orchestrator.recall(QUERY, "second-empty", {
|
|
175
|
+
degradationSink: sink,
|
|
176
|
+
onContextComposition: (value) => {
|
|
177
|
+
composition = value;
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
assert.equal(composition?.degradation, undefined);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("MCP recall payload keeps retrievalFailure so a tool caller can branch on it", async () => {
|
|
185
|
+
const timeoutResponse: EngramAccessRecallResponse = {
|
|
186
|
+
query: QUERY,
|
|
187
|
+
namespace: "default",
|
|
188
|
+
context: MEMORY_CONTEXT_UNAVAILABLE_NOTE,
|
|
189
|
+
count: 0,
|
|
190
|
+
memoryIds: [],
|
|
191
|
+
results: [],
|
|
192
|
+
fallbackUsed: false,
|
|
193
|
+
sourcesUsed: [],
|
|
194
|
+
disclosure: DEFAULT_RECALL_DISCLOSURE,
|
|
195
|
+
retrievalFailure: {
|
|
196
|
+
reason: "backend_unavailable",
|
|
197
|
+
detail: "qmd:daemon_timeout (no response within the deadline)",
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
const emptyResponse: EngramAccessRecallResponse = {
|
|
201
|
+
query: QUERY,
|
|
202
|
+
namespace: "default",
|
|
203
|
+
context: "",
|
|
204
|
+
count: 0,
|
|
205
|
+
memoryIds: [],
|
|
206
|
+
results: [],
|
|
207
|
+
fallbackUsed: false,
|
|
208
|
+
sourcesUsed: [],
|
|
209
|
+
disclosure: DEFAULT_RECALL_DISCLOSURE,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
async function callRecall(response: EngramAccessRecallResponse): Promise<EngramAccessRecallResponse> {
|
|
213
|
+
const service = {
|
|
214
|
+
briefingEnabled: false,
|
|
215
|
+
recall: (_req: EngramAccessRecallRequest) => Promise.resolve(response),
|
|
216
|
+
} as unknown as EngramAccessService;
|
|
217
|
+
const server = new EngramMcpServer(service);
|
|
218
|
+
// Unchecked cast with reason: handleRequest is a JSON-RPC envelope; this
|
|
219
|
+
// test owns both the spy recall result and the tool name, so the payload
|
|
220
|
+
// shape is the MCP recall structuredContent, not untrusted input.
|
|
221
|
+
const rpc = (await server.handleRequest({
|
|
222
|
+
jsonrpc: "2.0",
|
|
223
|
+
id: 1,
|
|
224
|
+
method: "tools/call",
|
|
225
|
+
params: { name: "engram.recall", arguments: { query: QUERY } },
|
|
226
|
+
})) as {
|
|
227
|
+
result?: { structuredContent?: EngramAccessRecallResponse; content?: Array<{ text?: string }> };
|
|
228
|
+
};
|
|
229
|
+
const structured = rpc.result?.structuredContent;
|
|
230
|
+
assert.ok(structured, "MCP recall must return structuredContent");
|
|
231
|
+
const text = rpc.result?.content?.[0]?.text ?? "{}";
|
|
232
|
+
const parsed: unknown = JSON.parse(text);
|
|
233
|
+
assert.ok(parsed && typeof parsed === "object");
|
|
234
|
+
const parsedFailure = "retrievalFailure" in parsed ? parsed.retrievalFailure : undefined;
|
|
235
|
+
assert.deepEqual(parsedFailure, structured.retrievalFailure);
|
|
236
|
+
return structured;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const timedOut = await callRecall(timeoutResponse);
|
|
240
|
+
const genuineEmpty = await callRecall(emptyResponse);
|
|
241
|
+
assert.equal(timedOut.count, genuineEmpty.count);
|
|
242
|
+
assert.deepEqual(timedOut.results, genuineEmpty.results);
|
|
243
|
+
assert.equal(timedOut.retrievalFailure?.reason, "backend_unavailable");
|
|
244
|
+
assert.equal(genuineEmpty.retrievalFailure, undefined);
|
|
245
|
+
});
|
|
@@ -27,29 +27,13 @@ import {
|
|
|
27
27
|
type RecallWhyReport,
|
|
28
28
|
} from "./recall-why.js";
|
|
29
29
|
import { summarizeRecallWhy } from "./recall-why-renderer.js";
|
|
30
|
-
import
|
|
30
|
+
import {
|
|
31
|
+
describeSearchDegradations,
|
|
32
|
+
fatalSearchDegradations,
|
|
33
|
+
type SearchDegradation,
|
|
34
|
+
} from "./search/port.js";
|
|
31
35
|
import type { MemoryFile, MemoryStatus } from "./types.js";
|
|
32
36
|
|
|
33
|
-
/**
|
|
34
|
-
* Degradation codes that mean the search backend could NOT answer, so an
|
|
35
|
-
* empty pipeline carries no information. `vector_tier_unavailable` is
|
|
36
|
-
* excluded on purpose: the lexical tier still answered, so the result set
|
|
37
|
-
* is degraded but real.
|
|
38
|
-
*/
|
|
39
|
-
const FATAL_DEGRADATION_CODES: Partial<Record<SearchDegradation["code"], true>> = {
|
|
40
|
-
backend_unavailable: true,
|
|
41
|
-
backend_error: true,
|
|
42
|
-
daemon_timeout: true,
|
|
43
|
-
daemon_loading: true,
|
|
44
|
-
subprocess_error: true,
|
|
45
|
-
deadline_exceeded: true,
|
|
46
|
-
remote_error: true,
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
function describeDegradations(degradations: readonly SearchDegradation[]): string {
|
|
50
|
-
return degradations.map((d) => `${d.backend}:${d.code}${d.detail !== undefined ? ` (${d.detail})` : ""}`).join("; ");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
37
|
export interface RecallWhyRequest {
|
|
54
38
|
query: string;
|
|
55
39
|
/** Memory id or substring to trace through every stage. */
|
|
@@ -139,15 +123,15 @@ export async function runRecallWhy(deps: RecallWhyServiceDeps, request: RecallWh
|
|
|
139
123
|
...(options.abortSignal !== undefined ? { abortSignal: options.abortSignal } : {}),
|
|
140
124
|
degradationSink: degradations,
|
|
141
125
|
});
|
|
142
|
-
const fatal = degradations
|
|
126
|
+
const fatal = fatalSearchDegradations(degradations);
|
|
143
127
|
if (fatal.length > 0) {
|
|
144
|
-
return { ok: false, reason: "backend_unavailable", detail:
|
|
128
|
+
return { ok: false, reason: "backend_unavailable", detail: describeSearchDegradations(fatal) };
|
|
145
129
|
}
|
|
146
130
|
return { ok: true, snapshot: captured.snapshot };
|
|
147
131
|
} catch (err) {
|
|
148
132
|
// Cancellation is the caller's decision, not a backend outage.
|
|
149
133
|
if (err instanceof Error && err.name === "AbortError") throw err;
|
|
150
|
-
const observed = degradations.length > 0 ? `${
|
|
134
|
+
const observed = degradations.length > 0 ? `${describeSearchDegradations(degradations)}; ` : "";
|
|
151
135
|
return {
|
|
152
136
|
ok: false,
|
|
153
137
|
reason: "backend_unavailable",
|
package/src/search/port.ts
CHANGED
|
@@ -55,6 +55,38 @@ export function reportSearchDegradation(
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Codes that mean the search backend could NOT answer, so an empty
|
|
60
|
+
* result set carries no information (issue #3082 / recall_why #3033).
|
|
61
|
+
* `vector_tier_unavailable` is excluded on purpose: the lexical tier
|
|
62
|
+
* still answered, so the result set is degraded but real.
|
|
63
|
+
*/
|
|
64
|
+
export const FATAL_SEARCH_DEGRADATION_CODES: Readonly<
|
|
65
|
+
Partial<Record<SearchDegradation["code"], true>>
|
|
66
|
+
> = Object.freeze({
|
|
67
|
+
backend_unavailable: true,
|
|
68
|
+
backend_error: true,
|
|
69
|
+
daemon_timeout: true,
|
|
70
|
+
daemon_loading: true,
|
|
71
|
+
subprocess_error: true,
|
|
72
|
+
deadline_exceeded: true,
|
|
73
|
+
remote_error: true,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export function fatalSearchDegradations(
|
|
77
|
+
degradations: readonly SearchDegradation[],
|
|
78
|
+
): SearchDegradation[] {
|
|
79
|
+
return degradations.filter((d) => FATAL_SEARCH_DEGRADATION_CODES[d.code] === true);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function describeSearchDegradations(
|
|
83
|
+
degradations: readonly SearchDegradation[],
|
|
84
|
+
): string {
|
|
85
|
+
return degradations
|
|
86
|
+
.map((d) => `${d.backend}:${d.code}${d.detail !== undefined ? ` (${d.detail})` : ""}`)
|
|
87
|
+
.join("; ");
|
|
88
|
+
}
|
|
89
|
+
|
|
58
90
|
export function resolveEnsureCollectionArgs(
|
|
59
91
|
collectionOrExecution?: string | SearchExecutionOptions,
|
|
60
92
|
execution?: SearchExecutionOptions
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/recall-why-service.ts"],"sourcesContent":["/**\n * Access-surface binding for the recall-miss diagnosis (issue #3033).\n *\n * `recall-why.ts` holds the stage logic and a port; this module binds that\n * port to the live orchestrator. It lives beside `access-recall-surface.ts`\n * rather than inside it because that file is at its file-size ratchet\n * ceiling (AGENTS.md: extract the addition to a sibling module).\n *\n * Namespace scope is enforced BEFORE the recall fires and the expected-memory\n * lookup is confined to namespaces the caller may read, so the diagnosis can\n * never confirm the existence of a memory in a namespace the caller has no\n * read access to. Read path and write path resolve through the same namespace\n * layer (Review Prevention Checklist #30): the lookup consumes exactly the\n * `recallNamespacesForPrincipal` set the recall itself is scoped by.\n */\n\nimport { resolveNamespaceCapabilities, resolveQmdCapabilities } from \"./capabilities.js\";\nimport { planRecallMode } from \"./intent.js\";\nimport { canReadNamespace, recallNamespacesForPrincipal, resolvePrincipal } from \"./namespaces/principal.js\";\nimport { isGenericRecallExcludedPath } from \"./orchestration/generic-recall-paths.js\";\nimport type { Orchestrator } from \"./orchestrator.js\";\nimport {\n explainRecallMiss,\n RecallWhyInputError,\n type RecallWhyDeps,\n type RecallWhyMemoryRef,\n type RecallWhyReport,\n} from \"./recall-why.js\";\nimport { summarizeRecallWhy } from \"./recall-why-renderer.js\";\nimport type { SearchDegradation } from \"./search/port.js\";\nimport type { MemoryFile, MemoryStatus } from \"./types.js\";\n\n/**\n * Degradation codes that mean the search backend could NOT answer, so an\n * empty pipeline carries no information. `vector_tier_unavailable` is\n * excluded on purpose: the lexical tier still answered, so the result set\n * is degraded but real.\n */\nconst FATAL_DEGRADATION_CODES: Partial<Record<SearchDegradation[\"code\"], true>> = {\n backend_unavailable: true,\n backend_error: true,\n daemon_timeout: true,\n daemon_loading: true,\n subprocess_error: true,\n deadline_exceeded: true,\n remote_error: true,\n};\n\nfunction describeDegradations(degradations: readonly SearchDegradation[]): string {\n return degradations.map((d) => `${d.backend}:${d.code}${d.detail !== undefined ? ` (${d.detail})` : \"\"}`).join(\"; \");\n}\n\nexport interface RecallWhyRequest {\n query: string;\n /** Memory id or substring to trace through every stage. */\n expect?: string;\n sessionKey?: string;\n namespace?: string;\n authenticatedPrincipal?: string;\n sourceConnector?: string;\n abortSignal?: AbortSignal;\n}\n\nexport interface RecallWhyResponse {\n /** False when the namespace ACL denied the diagnosis. */\n reportFound: boolean;\n report?: RecallWhyReport;\n /** One-line verdict, for agent callers that do not parse the full report. */\n summary?: string;\n}\n\nexport interface RecallWhyServiceDeps {\n readonly orchestrator: Orchestrator;\n /** The surface's shared namespace resolver. */\n resolveNamespace(namespace?: string): string;\n}\n\n/**\n * Run the recall-miss diagnosis for one request.\n *\n * Throws {@link RecallWhyInputError} for caller-input faults so surfaces can\n * map them to a 400; backend faults are reported inside the report as\n * `backend_unavailable` rather than thrown, because an outage is a diagnosis\n * result, not a diagnosis failure.\n */\nexport async function runRecallWhy(deps: RecallWhyServiceDeps, request: RecallWhyRequest): Promise<RecallWhyResponse> {\n const query = typeof request.query === \"string\" ? request.query : \"\";\n if (query.trim().length === 0) {\n throw new RecallWhyInputError(\"recallWhy: query is required and must be non-empty\");\n }\n const orchestrator = deps.orchestrator;\n const config = orchestrator.config;\n const namespacesEnabled = resolveNamespaceCapabilities(config).namespaces;\n const requestedNamespace = request.namespace?.trim() ? deps.resolveNamespace(request.namespace) : undefined;\n const authenticatedPrincipal = request.authenticatedPrincipal?.trim();\n const principal = authenticatedPrincipal || resolvePrincipal(request.sessionKey, config);\n\n if (requestedNamespace !== undefined) {\n if (!canReadNamespace(principal, requestedNamespace, config)) {\n return { reportFound: false };\n }\n } else if (namespacesEnabled && !authenticatedPrincipal && !request.sessionKey?.trim()) {\n // Namespaces on but no identity supplied: refuse rather than diagnose\n // against the global namespace (least-privileged default).\n return { reportFound: false };\n }\n\n const readableNamespaces =\n requestedNamespace !== undefined ? [requestedNamespace] : [...recallNamespacesForPrincipal(principal, config)];\n const recallSessionKey = request.sessionKey?.trim() || undefined;\n\n const whyDeps: RecallWhyDeps = {\n plannerMode: (prompt) => planRecallMode(prompt),\n scopedNamespaces: readableNamespaces,\n namespacesEnabled,\n isExcludedPath: (memoryPath) => isGenericRecallExcludedPath(memoryPath, config),\n runRecall: async (recallQuery, options) => {\n // A configured-but-unreachable search backend is an OUTAGE, and must\n // never render as \"0 candidates\" (Review Prevention Checklist #22).\n // This is the same pair of expressions the pipeline emits as\n // `qmdEnabled` / `qmdAvailable` on its recall_summary trace.\n if (resolveQmdCapabilities(config).qmd && !orchestrator.qmd.isAvailable()) {\n return {\n ok: false,\n reason: \"backend_unavailable\",\n detail: \"the search backend is enabled but reported itself unavailable\",\n };\n }\n // A backend that answers the availability probe can still fail\n // mid-recall (daemon timeout, deadline exceeded, subprocess error).\n // The pipeline already collects those; `degradationSink` hands us the\n // same array it fills, so the outage is observed rather than inferred.\n const degradations: SearchDegradation[] = [];\n try {\n const captured = await orchestrator.recallWithXrayCapture(recallQuery, options.sessionKey, {\n ...(options.namespace !== undefined ? { namespace: options.namespace } : {}),\n ...(authenticatedPrincipal ? { principalOverride: authenticatedPrincipal } : {}),\n ...(request.sourceConnector ? { sourceConnector: request.sourceConnector } : {}),\n ...(options.abortSignal !== undefined ? { abortSignal: options.abortSignal } : {}),\n degradationSink: degradations,\n });\n const fatal = degradations.filter((d) => FATAL_DEGRADATION_CODES[d.code] === true);\n if (fatal.length > 0) {\n return { ok: false, reason: \"backend_unavailable\", detail: describeDegradations(fatal) };\n }\n return { ok: true, snapshot: captured.snapshot };\n } catch (err) {\n // Cancellation is the caller's decision, not a backend outage.\n if (err instanceof Error && err.name === \"AbortError\") throw err;\n const observed = degradations.length > 0 ? `${describeDegradations(degradations)}; ` : \"\";\n return {\n ok: false,\n reason: \"backend_unavailable\",\n detail: `${observed}${err instanceof Error ? err.message : \"recall failed\"}`,\n };\n }\n },\n findExpected: (expect) => findExpectedMemory(orchestrator, readableNamespaces, expect),\n };\n\n const report = await explainRecallMiss(query, {\n deps: whyDeps,\n ...(request.expect !== undefined ? { expect: request.expect } : {}),\n ...(recallSessionKey !== undefined ? { sessionKey: recallSessionKey } : {}),\n ...(requestedNamespace !== undefined ? { namespace: requestedNamespace } : {}),\n ...(request.abortSignal !== undefined ? { abortSignal: request.abortSignal } : {}),\n });\n\n return { reportFound: true, report, summary: summarizeRecallWhy(report) };\n}\n\n/**\n * Resolve `expect` to one stored memory, searching ONLY namespaces the\n * caller may read. An exact id match wins over a substring match; among\n * substring matches the lowest (memoryId, path) pair wins, so repeated runs\n * report the same memory.\n */\nasync function findExpectedMemory(\n orchestrator: Orchestrator,\n namespaces: readonly string[],\n expect: string\n): Promise<RecallWhyMemoryRef | null> {\n const substringMatches: RecallWhyMemoryRef[] = [];\n const seenNamespaces = new Set<string>();\n for (const namespace of namespaces) {\n if (seenNamespaces.has(namespace)) continue;\n seenNamespaces.add(namespace);\n let memories: MemoryFile[];\n try {\n const storage = await orchestrator.getStorageForNamespace(namespace);\n memories = await storage.readAllMemories();\n } catch {\n // A namespace whose storage cannot be read contributes no candidates;\n // the diagnosis still reports on the ones that could be read.\n continue;\n }\n for (const memory of memories) {\n const memoryId = typeof memory.frontmatter.id === \"string\" ? memory.frontmatter.id : \"\";\n if (memoryId.length === 0) continue;\n const ref: RecallWhyMemoryRef = {\n memoryId,\n path: memory.path,\n status: (memory.frontmatter.status as MemoryStatus | undefined) ?? \"active\",\n namespace,\n };\n if (memoryId === expect) return ref;\n if (memoryId.includes(expect) || memory.path.includes(expect)) substringMatches.push(ref);\n }\n }\n if (substringMatches.length === 0) return null;\n // Total comparator: -1 / 0 / 1, and 0 for genuinely equal rows (#12).\n substringMatches.sort((a, b) => {\n if (a.memoryId !== b.memoryId) return a.memoryId < b.memoryId ? -1 : 1;\n if (a.path !== b.path) return a.path < b.path ? -1 : 1;\n return 0;\n });\n return substringMatches[0] ?? null;\n}\n"],"mappings":"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","names":[]}
|
package/dist/chunk-U2VDCFXW.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// src/search/port.ts
|
|
2
|
-
function reportSearchDegradation(execution, degradation) {
|
|
3
|
-
try {
|
|
4
|
-
execution?.onDegradation?.(degradation);
|
|
5
|
-
} catch {
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
function resolveEnsureCollectionArgs(collectionOrExecution, execution) {
|
|
9
|
-
if (typeof collectionOrExecution === "string") {
|
|
10
|
-
return { collection: collectionOrExecution, execution };
|
|
11
|
-
}
|
|
12
|
-
return { collection: void 0, execution: collectionOrExecution ?? execution };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export {
|
|
16
|
-
reportSearchDegradation,
|
|
17
|
-
resolveEnsureCollectionArgs
|
|
18
|
-
};
|
|
19
|
-
//# sourceMappingURL=chunk-U2VDCFXW.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/search/port.ts"],"sourcesContent":["import type { QmdSearchResult } from \"../types.js\";\n\n/** Alias so consumers don't need to reference \"Qmd\" in a backend-agnostic context. */\nexport type SearchResult = QmdSearchResult;\n\nexport interface SearchQueryOptions {\n intent?: string;\n explain?: boolean;\n candidateLimit?: number;\n rerank?: boolean;\n chunkStrategy?: \"auto\" | \"regex\";\n structuredSearches?: Array<{ type: \"lex\" | \"vec\" | \"hyde\"; query: string }>;\n}\n\n/**\n * A search that returned empty (or partial) results because the backend was\n * unavailable, still loading, or timed out — cases that are otherwise\n * indistinguishable from a genuine \"no matches\" (issue #1536, CLAUDE.md\n * rule 34).\n */\nexport interface SearchDegradation {\n backend: \"qmd\" | \"remote\" | \"meilisearch\" | \"orama\" | \"lancedb\";\n code:\n | \"backend_unavailable\"\n | \"backend_error\"\n | \"daemon_timeout\"\n | \"daemon_loading\"\n | \"subprocess_error\"\n | \"deadline_exceeded\"\n | \"remote_error\"\n | \"vector_tier_unavailable\";\n detail?: string;\n}\n\nexport interface SearchExecutionOptions {\n signal?: AbortSignal;\n /**\n * Observer invoked when the backend degrades during this call (#1536).\n * Callers that need to distinguish empty-because-degraded from\n * empty-because-no-matches (recall x-ray, fallback decisions) pass a\n * collector here. Observer failures are swallowed by the notifier —\n * observability must never break search.\n */\n onDegradation?: (degradation: SearchDegradation) => void;\n}\n\nexport function reportSearchDegradation(\n execution: SearchExecutionOptions | undefined,\n degradation: SearchDegradation\n): void {\n try {\n execution?.onDegradation?.(degradation);\n } catch {\n // Observability must never break search.\n }\n}\n\nexport function resolveEnsureCollectionArgs(\n collectionOrExecution?: string | SearchExecutionOptions,\n execution?: SearchExecutionOptions\n): { collection?: string; execution?: SearchExecutionOptions } {\n if (typeof collectionOrExecution === \"string\") {\n return { collection: collectionOrExecution, execution };\n }\n return { collection: undefined, execution: collectionOrExecution ?? execution };\n}\n\n/**\n * Optional status report for search backends that track embedding backlog.\n * Backends without embedding pipelines return null from status().\n */\nexport interface SearchBackendStatus {\n pendingEmbeddings: number | null;\n oldestPendingAgeMs: number | null;\n totalFiles: number | null;\n embeddedFiles: number | null;\n}\n\n/**\n * Abstract search backend interface.\n *\n * Implementations:\n * - QmdClient (default, local hybrid BM25+vector+reranking)\n * - OramaBackend (embedded, pure JS, hybrid FTS+vector)\n * - LanceDbBackend (embedded, native Arrow bindings, RRF reranking)\n * - MeilisearchBackend (server-based SDK, hybrid search)\n * - RemoteSearchBackend (HTTP REST adapter)\n * - NoopSearchBackend (graceful degradation)\n *\n * See docs/writing-a-search-backend.md for the implementation guide.\n */\nexport interface SearchBackend {\n // ── Lifecycle ──\n probe(execution?: SearchExecutionOptions): Promise<boolean>;\n /**\n * Optional non-mutating availability probe for health/readiness checks.\n * Implementations must avoid auto-upgrades, collection creation, daemon\n * startup, or any other runtime-modifying side effects.\n */\n checkAvailability?(execution?: SearchExecutionOptions): Promise<boolean>;\n isAvailable(): boolean;\n debugStatus(): string;\n /** Optional embedding backlog status for health surfaces. */\n status?(): Promise<SearchBackendStatus>;\n\n // ── Search ──\n search(\n query: string,\n collection?: string,\n maxResults?: number,\n options?: SearchQueryOptions,\n execution?: SearchExecutionOptions\n ): Promise<SearchResult[]>;\n searchGlobal(query: string, maxResults?: number, execution?: SearchExecutionOptions): Promise<SearchResult[]>;\n bm25Search(\n query: string,\n collection?: string,\n maxResults?: number,\n execution?: SearchExecutionOptions\n ): Promise<SearchResult[]>;\n vectorSearch(\n query: string,\n collection?: string,\n maxResults?: number,\n execution?: SearchExecutionOptions\n ): Promise<SearchResult[]>;\n hybridSearch(\n query: string,\n collection?: string,\n maxResults?: number,\n execution?: SearchExecutionOptions\n ): Promise<SearchResult[]>;\n\n // ── Maintenance ──\n update(execution?: SearchExecutionOptions): Promise<void>;\n /**\n * Wall-clock ms of the most recent SUCCESSFUL index refresh, or null when the\n * backend has never run one. Ordinary update() is fail-open: it returns\n * silently (without throwing or advancing this timestamp) when the backend is\n * unavailable or the call is suppressed by a min-interval / failure backoff.\n * Callers that must know whether update() actually indexed — so they can\n * invalidate dependent caches only on a real refresh — snapshot this before\n * the call and treat the backend as refreshed iff the value advanced. Optional\n * because not every backend tracks it; absent means \"assume refreshed\".\n */\n readonly lastUpdateRanAtMs?: number | null;\n /**\n * Optional strict refresh used by callers that must know whether the backend\n * was actually refreshed before writing success markers. Ordinary update\n * calls remain fail-open for migration/maintenance resilience.\n */\n updateStrict?(execution?: SearchExecutionOptions): Promise<void>;\n updateCollection(collection: string, execution?: SearchExecutionOptions): Promise<void>;\n updateCollectionFromDir?(collection: string, memoryDir: string, execution?: SearchExecutionOptions): Promise<void>;\n /**\n * True when update() refreshes every indexed collection, not just this\n * backend's configured collection. Namespace routers use this to avoid\n * repeating the same expensive global update once per namespace.\n */\n updatesAllCollections?(): boolean;\n /**\n * Optional strict refresh used by callers that must know whether a collection\n * was actually refreshed before writing success markers. Ordinary update\n * calls remain fail-open for migration/maintenance resilience.\n */\n updateCollectionStrict?(collection: string, execution?: SearchExecutionOptions): Promise<void>;\n embed(): Promise<void>;\n /**\n * Optional strict embed used by callers that must know vectors were actually\n * refreshed before writing success markers.\n */\n embedStrict?(): Promise<void>;\n embedCollection(collection: string): Promise<void>;\n /**\n * Optional strict collection embed used by callers that must know vectors were\n * actually refreshed before writing success markers.\n */\n embedCollectionStrict?(collection: string): Promise<void>;\n\n // ── Collection management ──\n /**\n * True only when the backend can bind, isolate, search, and delete collections\n * outside Remnic's primary memory roots.\n */\n supportsAdditionalCollections?(): boolean;\n /**\n * Prevent a collection from participating in unscoped/global searches.\n * Dedicated corpora must call this immediately after collection creation.\n */\n excludeCollectionFromGlobalSearch?(collection: string, execution?: SearchExecutionOptions): Promise<void>;\n /** Remove a non-primary collection and its backend-owned index state. */\n deleteCollection?(collection: string, execution?: SearchExecutionOptions): Promise<boolean>;\n /** Canonical source root currently bound to a named collection. */\n collectionRoot?(collection: string, execution?: SearchExecutionOptions): Promise<string | null>;\n /** Collection-scoped status for observability surfaces. */\n collectionStatus?(collection: string): Promise<Pick<SearchBackendStatus, \"totalFiles\">>;\n /**\n * Optional non-mutating collection probe. Backends that can distinguish a\n * missing collection from a transient probe failure should implement this so\n * callers can avoid auto-creating collections in unsafe layouts.\n */\n checkCollection?(\n collectionOrExecution?: string | SearchExecutionOptions,\n execution?: SearchExecutionOptions\n ): Promise<\"present\" | \"missing\" | \"unknown\" | \"skipped\">;\n ensureCollection(\n memoryDir: string,\n execution?: SearchExecutionOptions\n ): Promise<\"present\" | \"missing\" | \"unknown\" | \"skipped\">;\n ensureCollection(\n memoryDir: string,\n collection?: string,\n execution?: SearchExecutionOptions\n ): Promise<\"present\" | \"missing\" | \"unknown\" | \"skipped\">;\n}\n"],"mappings":";AA8CO,SAAS,wBACd,WACA,aACM;AACN,MAAI;AACF,eAAW,gBAAgB,WAAW;AAAA,EACxC,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,4BACd,uBACA,WAC6D;AAC7D,MAAI,OAAO,0BAA0B,UAAU;AAC7C,WAAO,EAAE,YAAY,uBAAuB,UAAU;AAAA,EACxD;AACA,SAAO,EAAE,YAAY,QAAW,WAAW,yBAAyB,UAAU;AAChF;","names":[]}
|