@remnic/core 9.3.659 → 9.3.661
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-cli.js +22 -22
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +15 -14
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +14 -13
- package/dist/{access-service-D_nbpexW.d.ts → access-service-D0SLB4MH.d.ts} +2 -2
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +13 -12
- 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/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +3 -3
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +4 -3
- 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/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +5 -4
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-QRSKPI62.js → chunk-34NSUPWS.js} +110 -25
- package/dist/chunk-34NSUPWS.js.map +1 -0
- package/dist/{chunk-SCPFRKIT.js → chunk-42JKGUFJ.js} +6 -2
- package/dist/{chunk-SCPFRKIT.js.map → chunk-42JKGUFJ.js.map} +1 -1
- package/dist/{chunk-FWIROLS6.js → chunk-44VFF3BB.js} +18 -16
- package/dist/chunk-44VFF3BB.js.map +1 -0
- package/dist/{chunk-5PFIMBJJ.js → chunk-4SYURHI6.js} +12 -12
- package/dist/{chunk-2VCTTEJM.js → chunk-5G2DNO54.js} +3 -3
- package/dist/{chunk-3R6OP33G.js → chunk-7F7LC6HW.js} +3 -3
- package/dist/{chunk-ZCMO46YY.js → chunk-A7EF2XRO.js} +2 -2
- package/dist/{chunk-BNUAOLDK.js → chunk-ANJOULTP.js} +2 -2
- package/dist/{chunk-CPPS65WS.js → chunk-AWJ2FHCF.js} +84 -17
- package/dist/chunk-AWJ2FHCF.js.map +1 -0
- package/dist/{chunk-4PLOQDBB.js → chunk-AX5O25EF.js} +7 -5
- package/dist/chunk-AX5O25EF.js.map +1 -0
- package/dist/{chunk-QFKRE7AU.js → chunk-BP5O3GYD.js} +4 -4
- package/dist/{chunk-BKRIAXTU.js → chunk-D2EFNQMY.js} +2 -2
- package/dist/{chunk-6M4LYWA2.js → chunk-D44FQVCU.js} +5 -5
- package/dist/{chunk-SSSXWIBP.js → chunk-EMSC4P66.js} +5 -5
- package/dist/{chunk-MBZAESQ3.js → chunk-F6O7IOS3.js} +2 -2
- package/dist/{chunk-6G5JEN55.js → chunk-FZC2WSDB.js} +2 -2
- package/dist/{chunk-KI6QM5AV.js → chunk-GY3V3SUI.js} +2 -2
- package/dist/{chunk-EKQMQQ3U.js → chunk-LFZUFZQR.js} +10 -2
- package/dist/chunk-LFZUFZQR.js.map +1 -0
- package/dist/{chunk-7VWDC7AD.js → chunk-MHYRRV43.js} +122 -29
- package/dist/chunk-MHYRRV43.js.map +1 -0
- package/dist/{chunk-VJYFXDCZ.js → chunk-NMPEJV5M.js} +3 -3
- package/dist/{chunk-7KSPKZIQ.js → chunk-PXVFMQLD.js} +3 -3
- package/dist/{chunk-FIS5RT6K.js → chunk-QXHBWFR3.js} +2 -2
- package/dist/{chunk-G2VVBWFU.js → chunk-RQGR3ETH.js} +2 -2
- package/dist/{chunk-GGL7R2L2.js → chunk-RQRKQJYM.js} +4 -4
- package/dist/{chunk-JI3LQFJH.js → chunk-TBLGI2LT.js} +2 -2
- package/dist/{chunk-RVT6U6PV.js → chunk-TWAJICBN.js} +2 -2
- package/dist/{chunk-46RXRASB.js → chunk-TYIXG4VR.js} +3 -3
- package/dist/{chunk-5PLUC5OB.js → chunk-WSQG37DV.js} +2 -2
- package/dist/{chunk-M3VYPE2H.js → chunk-YNQ6DFSV.js} +1 -1
- package/dist/chunk-YNQ6DFSV.js.map +1 -0
- package/dist/{chunk-GRYAECRV.js → chunk-ZJH723NM.js} +2 -2
- package/dist/{cli-aYxSuPvP.d.ts → cli-C6twwe84.d.ts} +3 -3
- package/dist/cli.d.ts +5 -5
- package/dist/cli.js +24 -23
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +4 -3
- 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/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +4 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +4 -3
- 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/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 +4 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +3 -3
- 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/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +30 -30
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- 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/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +4 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -3
- package/dist/maintenance/rebuild-memory-projection.js +5 -4
- 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 +44 -2
- package/dist/memory-cache.js +6 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- 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 +1 -1
- package/dist/namespaces/migrate.js +12 -11
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +9 -8
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +4 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +14 -13
- package/dist/{orchestrator-D1wcmPNj.d.ts → orchestrator-Cg1UkvmO.d.ts} +2 -2
- package/dist/orchestrator.d.ts +3 -3
- package/dist/orchestrator.js +20 -20
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +5 -2
- package/dist/qmd-recall-cache.js +3 -1
- package/dist/qmd.d.ts +17 -1
- package/dist/qmd.js +4 -3
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +2 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +17 -1
- package/dist/recall-state.js +1 -1
- package/dist/recall-tag-filter.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/retrieval-agents.d.ts +1 -1
- 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/schemas.d.ts +24 -24
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +8 -7
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +8 -7
- 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 +21 -2
- package/dist/search/port.js +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-MWOdNtSE.d.ts → semantic-consolidation-BICZvQ3C.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +5 -4
- package/dist/semantic-rule-promotion.js +4 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +4 -3
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +3 -2
- 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/{types-CgcCpUrf.d.ts → types-D96bCB3C.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +4 -3
- package/package.json +1 -1
- package/src/memory-cache.test.ts +30 -0
- package/src/memory-cache.ts +129 -16
- package/src/namespaces/search.ts +16 -0
- package/src/orchestrator.ts +144 -3
- package/src/qmd-recall-cache.ts +6 -0
- package/src/qmd.ts +131 -18
- package/src/recall-state.ts +47 -21
- package/src/search/port.ts +25 -0
- package/src/storage.ts +27 -3
- package/dist/chunk-4PLOQDBB.js.map +0 -1
- package/dist/chunk-7VWDC7AD.js.map +0 -1
- package/dist/chunk-CPPS65WS.js.map +0 -1
- package/dist/chunk-EKQMQQ3U.js.map +0 -1
- package/dist/chunk-FWIROLS6.js.map +0 -1
- package/dist/chunk-M3VYPE2H.js.map +0 -1
- package/dist/chunk-QRSKPI62.js.map +0 -1
- /package/dist/{chunk-5PFIMBJJ.js.map → chunk-4SYURHI6.js.map} +0 -0
- /package/dist/{chunk-2VCTTEJM.js.map → chunk-5G2DNO54.js.map} +0 -0
- /package/dist/{chunk-3R6OP33G.js.map → chunk-7F7LC6HW.js.map} +0 -0
- /package/dist/{chunk-ZCMO46YY.js.map → chunk-A7EF2XRO.js.map} +0 -0
- /package/dist/{chunk-BNUAOLDK.js.map → chunk-ANJOULTP.js.map} +0 -0
- /package/dist/{chunk-QFKRE7AU.js.map → chunk-BP5O3GYD.js.map} +0 -0
- /package/dist/{chunk-BKRIAXTU.js.map → chunk-D2EFNQMY.js.map} +0 -0
- /package/dist/{chunk-6M4LYWA2.js.map → chunk-D44FQVCU.js.map} +0 -0
- /package/dist/{chunk-SSSXWIBP.js.map → chunk-EMSC4P66.js.map} +0 -0
- /package/dist/{chunk-MBZAESQ3.js.map → chunk-F6O7IOS3.js.map} +0 -0
- /package/dist/{chunk-6G5JEN55.js.map → chunk-FZC2WSDB.js.map} +0 -0
- /package/dist/{chunk-KI6QM5AV.js.map → chunk-GY3V3SUI.js.map} +0 -0
- /package/dist/{chunk-VJYFXDCZ.js.map → chunk-NMPEJV5M.js.map} +0 -0
- /package/dist/{chunk-7KSPKZIQ.js.map → chunk-PXVFMQLD.js.map} +0 -0
- /package/dist/{chunk-FIS5RT6K.js.map → chunk-QXHBWFR3.js.map} +0 -0
- /package/dist/{chunk-G2VVBWFU.js.map → chunk-RQGR3ETH.js.map} +0 -0
- /package/dist/{chunk-GGL7R2L2.js.map → chunk-RQRKQJYM.js.map} +0 -0
- /package/dist/{chunk-JI3LQFJH.js.map → chunk-TBLGI2LT.js.map} +0 -0
- /package/dist/{chunk-RVT6U6PV.js.map → chunk-TWAJICBN.js.map} +0 -0
- /package/dist/{chunk-46RXRASB.js.map → chunk-TYIXG4VR.js.map} +0 -0
- /package/dist/{chunk-5PLUC5OB.js.map → chunk-WSQG37DV.js.map} +0 -0
- /package/dist/{chunk-GRYAECRV.js.map → chunk-ZJH723NM.js.map} +0 -0
package/src/orchestrator.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
import { findUnresolvedEntityRefs } from "./reconstruct.js";
|
|
53
53
|
import type {
|
|
54
54
|
SearchBackend,
|
|
55
|
+
SearchDegradation,
|
|
55
56
|
SearchExecutionOptions,
|
|
56
57
|
SearchQueryOptions,
|
|
57
58
|
} from "./search/port.js";
|
|
@@ -6392,6 +6393,8 @@ export class Orchestrator {
|
|
|
6392
6393
|
queryAwarePrefilter?: QueryAwarePrefilter;
|
|
6393
6394
|
searchOptions?: SearchQueryOptions;
|
|
6394
6395
|
onDebugSnapshot?: (snapshot: QmdRecallSnapshot) => Promise<void>;
|
|
6396
|
+
/** Backend degradation observer, threaded into every QMD call (#1536). */
|
|
6397
|
+
onDegradation?: (degradation: SearchDegradation) => void;
|
|
6395
6398
|
abortSignal?: AbortSignal;
|
|
6396
6399
|
},
|
|
6397
6400
|
): Promise<QmdSearchResult[]> {
|
|
@@ -6497,6 +6500,7 @@ export class Orchestrator {
|
|
|
6497
6500
|
primarySearchOptions,
|
|
6498
6501
|
{
|
|
6499
6502
|
signal: options.abortSignal,
|
|
6503
|
+
onDegradation: options.onDegradation,
|
|
6500
6504
|
},
|
|
6501
6505
|
)
|
|
6502
6506
|
: await this.qmd.search(
|
|
@@ -6504,6 +6508,7 @@ export class Orchestrator {
|
|
|
6504
6508
|
options.collection,
|
|
6505
6509
|
fetchLimit,
|
|
6506
6510
|
primarySearchOptions,
|
|
6511
|
+
{ onDegradation: options.onDegradation },
|
|
6507
6512
|
)
|
|
6508
6513
|
: await this.searchAcrossNamespaces({
|
|
6509
6514
|
query: prompt,
|
|
@@ -6513,7 +6518,10 @@ export class Orchestrator {
|
|
|
6513
6518
|
maxResults: fetchLimit,
|
|
6514
6519
|
mode: "search",
|
|
6515
6520
|
searchOptions: primarySearchOptions,
|
|
6516
|
-
execution: {
|
|
6521
|
+
execution: {
|
|
6522
|
+
signal: options.abortSignal,
|
|
6523
|
+
onDegradation: options.onDegradation,
|
|
6524
|
+
},
|
|
6517
6525
|
});
|
|
6518
6526
|
lastPrimaryResultCount = primaryResults.length;
|
|
6519
6527
|
lastHybridResultCount = 0;
|
|
@@ -6543,6 +6551,7 @@ export class Orchestrator {
|
|
|
6543
6551
|
fetchLimit,
|
|
6544
6552
|
{
|
|
6545
6553
|
signal: options.abortSignal,
|
|
6554
|
+
onDegradation: options.onDegradation,
|
|
6546
6555
|
},
|
|
6547
6556
|
)
|
|
6548
6557
|
: await this.searchAcrossNamespaces({
|
|
@@ -6552,7 +6561,10 @@ export class Orchestrator {
|
|
|
6552
6561
|
: undefined,
|
|
6553
6562
|
maxResults: fetchLimit,
|
|
6554
6563
|
mode: "hybrid",
|
|
6555
|
-
execution: {
|
|
6564
|
+
execution: {
|
|
6565
|
+
signal: options.abortSignal,
|
|
6566
|
+
onDegradation: options.onDegradation,
|
|
6567
|
+
},
|
|
6556
6568
|
});
|
|
6557
6569
|
lastHybridResultCount = hybridResults.length;
|
|
6558
6570
|
lastHybridTopUpUsed = hybridResults.length > 0;
|
|
@@ -7241,6 +7253,11 @@ export class Orchestrator {
|
|
|
7241
7253
|
options: RecallInvocationOptions = {},
|
|
7242
7254
|
): Promise<string> {
|
|
7243
7255
|
const recallStart = Date.now();
|
|
7256
|
+
// Backend degradations observed by this recall's QMD searches (#1536):
|
|
7257
|
+
// collected via the execution-options observer and attached to the
|
|
7258
|
+
// LastRecallSnapshot after it is recorded, so surfaces can distinguish
|
|
7259
|
+
// "no matches" from "backend could not answer" (CLAUDE.md rule 34).
|
|
7260
|
+
const backendDegradations: SearchDegradation[] = [];
|
|
7244
7261
|
// Issue #680 — historical recall. Parse `options.asOf` once at the
|
|
7245
7262
|
// top of the recall so each boost-pass uses identical filter logic.
|
|
7246
7263
|
// Invalid values are rejected at input boundaries (CLI / HTTP / MCP)
|
|
@@ -9230,12 +9247,39 @@ export class Orchestrator {
|
|
|
9230
9247
|
* an exact entity-name match) are not discarded just because the QMD
|
|
9231
9248
|
* contextual pass returned a weak result. */
|
|
9232
9249
|
maxSpecializedScore: number;
|
|
9250
|
+
/**
|
|
9251
|
+
* Degradations observed while producing this phase result (#1536).
|
|
9252
|
+
* Cached WITH the result so cache hits replay them — a served-from-
|
|
9253
|
+
* cache partial result must still explain why it is partial (codex
|
|
9254
|
+
* round-4 review on #1544).
|
|
9255
|
+
*/
|
|
9256
|
+
degradations?: SearchDegradation[];
|
|
9233
9257
|
} | null;
|
|
9234
9258
|
|
|
9235
9259
|
const qmdEnrichmentAbort = createEnrichmentAbortHandle(options.abortSignal);
|
|
9236
9260
|
const qmdPromise = observeEnrichmentPromise(
|
|
9237
9261
|
(async (): Promise<QmdPhaseResult> => {
|
|
9238
9262
|
const t0 = Date.now();
|
|
9263
|
+
// Degradation accounting for this phase (#1536): everything pushed
|
|
9264
|
+
// after this mark belongs to this phase and is cached with its
|
|
9265
|
+
// result; cache hits and stale fallbacks replay stored degradations
|
|
9266
|
+
// so served-from-cache results still explain their gaps, and every
|
|
9267
|
+
// path that skips QMD entirely reports backend_unavailable.
|
|
9268
|
+
const phaseDegradationsStart = backendDegradations.length;
|
|
9269
|
+
const replayCachedDegradations = (value: {
|
|
9270
|
+
degradations?: SearchDegradation[];
|
|
9271
|
+
}) => {
|
|
9272
|
+
for (const degradation of value.degradations ?? []) {
|
|
9273
|
+
backendDegradations.push(degradation);
|
|
9274
|
+
}
|
|
9275
|
+
};
|
|
9276
|
+
const reportRecallQmdUnavailable = (detail: string) => {
|
|
9277
|
+
backendDegradations.push({
|
|
9278
|
+
backend: "qmd",
|
|
9279
|
+
code: "backend_unavailable",
|
|
9280
|
+
detail,
|
|
9281
|
+
});
|
|
9282
|
+
};
|
|
9239
9283
|
if (recallResultLimit <= 0) {
|
|
9240
9284
|
recordRecallSectionMetric({
|
|
9241
9285
|
section: "qmd",
|
|
@@ -9287,6 +9331,7 @@ export class Orchestrator {
|
|
|
9287
9331
|
success: true,
|
|
9288
9332
|
timing: `${Math.max(0, Math.round(cachedQmd.ageMs))}ms-cache`,
|
|
9289
9333
|
});
|
|
9334
|
+
replayCachedDegradations(cachedQmd.value);
|
|
9290
9335
|
if (queryAwarePrefilterIsEmpty) {
|
|
9291
9336
|
return emptyQueryAwareQmdResult;
|
|
9292
9337
|
}
|
|
@@ -9310,6 +9355,8 @@ export class Orchestrator {
|
|
|
9310
9355
|
success: true,
|
|
9311
9356
|
timing: `stale-cache(reprobe-cooldown:${Math.max(0, Math.round(staleQmdFallback.ageMs))}ms)`,
|
|
9312
9357
|
});
|
|
9358
|
+
reportRecallQmdUnavailable("served stale recall cache (reprobe cooldown)");
|
|
9359
|
+
replayCachedDegradations(staleQmdFallback.value);
|
|
9313
9360
|
if (queryAwarePrefilterIsEmpty) {
|
|
9314
9361
|
return emptyQueryAwareQmdResult;
|
|
9315
9362
|
}
|
|
@@ -9324,6 +9371,7 @@ export class Orchestrator {
|
|
|
9324
9371
|
success: true,
|
|
9325
9372
|
timing: "skip(reprobe-cooldown)",
|
|
9326
9373
|
});
|
|
9374
|
+
reportRecallQmdUnavailable("recall skipped QMD (reprobe cooldown)");
|
|
9327
9375
|
return null;
|
|
9328
9376
|
}
|
|
9329
9377
|
this.lastQmdReprobeAtMs = now;
|
|
@@ -9339,6 +9387,8 @@ export class Orchestrator {
|
|
|
9339
9387
|
success: true,
|
|
9340
9388
|
timing: `stale-cache(reprobe-failed:${Math.max(0, Math.round(staleQmdFallback.ageMs))}ms)`,
|
|
9341
9389
|
});
|
|
9390
|
+
reportRecallQmdUnavailable("served stale recall cache (reprobe failed)");
|
|
9391
|
+
replayCachedDegradations(staleQmdFallback.value);
|
|
9342
9392
|
if (queryAwarePrefilterIsEmpty) {
|
|
9343
9393
|
return emptyQueryAwareQmdResult;
|
|
9344
9394
|
}
|
|
@@ -9356,6 +9406,7 @@ export class Orchestrator {
|
|
|
9356
9406
|
log.debug(
|
|
9357
9407
|
`Search skip (re-probe failed): ${this.qmd.debugStatus()}`,
|
|
9358
9408
|
);
|
|
9409
|
+
reportRecallQmdUnavailable("recall skipped QMD (reprobe failed)");
|
|
9359
9410
|
return null;
|
|
9360
9411
|
}
|
|
9361
9412
|
log.info(`QMD re-probe succeeded: ${this.qmd.debugStatus()}`);
|
|
@@ -9437,6 +9488,9 @@ export class Orchestrator {
|
|
|
9437
9488
|
queryAwarePrefilter,
|
|
9438
9489
|
searchOptions: qmdSearchOptions,
|
|
9439
9490
|
abortSignal: qmdEnrichmentAbort.signal,
|
|
9491
|
+
onDegradation: (degradation) => {
|
|
9492
|
+
backendDegradations.push(degradation);
|
|
9493
|
+
},
|
|
9440
9494
|
onDebugSnapshot: async (snapshot) => {
|
|
9441
9495
|
await this.recordLastQmdRecallSnapshot({
|
|
9442
9496
|
storage: profileStorage,
|
|
@@ -9487,11 +9541,17 @@ export class Orchestrator {
|
|
|
9487
9541
|
}
|
|
9488
9542
|
}
|
|
9489
9543
|
|
|
9544
|
+
const phaseDegradations = backendDegradations.slice(
|
|
9545
|
+
phaseDegradationsStart,
|
|
9546
|
+
);
|
|
9490
9547
|
const result = {
|
|
9491
9548
|
memoryResultsLists: [augmentedResults],
|
|
9492
9549
|
globalResults: [],
|
|
9493
9550
|
preAugmentTopScore,
|
|
9494
9551
|
maxSpecializedScore,
|
|
9552
|
+
...(phaseDegradations.length > 0
|
|
9553
|
+
? { degradations: phaseDegradations }
|
|
9554
|
+
: {}),
|
|
9495
9555
|
};
|
|
9496
9556
|
if (
|
|
9497
9557
|
augmentedResults.length > 0 ||
|
|
@@ -9521,6 +9581,8 @@ export class Orchestrator {
|
|
|
9521
9581
|
success: true,
|
|
9522
9582
|
timing: `stale-cache(${err instanceof Error ? err.message : String(err)})`,
|
|
9523
9583
|
});
|
|
9584
|
+
reportRecallQmdUnavailable("served stale recall cache (qmd phase error)");
|
|
9585
|
+
replayCachedDegradations(staleQmdFallback.value);
|
|
9524
9586
|
if (queryAwarePrefilterIsEmpty) {
|
|
9525
9587
|
return emptyQueryAwareQmdResult;
|
|
9526
9588
|
}
|
|
@@ -9542,7 +9604,23 @@ export class Orchestrator {
|
|
|
9542
9604
|
return null;
|
|
9543
9605
|
})
|
|
9544
9606
|
.finally(() => qmdEnrichmentAbort.dispose()),
|
|
9545
|
-
() =>
|
|
9607
|
+
() => {
|
|
9608
|
+
// The enrichment budget abandoned the hot QMD phase mid-flight
|
|
9609
|
+
// (#1536, codex round-7 on #1544): QmdClient treats this abort as
|
|
9610
|
+
// caller cancellation and never reports, so report the abandonment
|
|
9611
|
+
// deterministically here — the exact mirror of the cold-tier
|
|
9612
|
+
// deadline gate. Guarded: a CALLER abort also routes through this
|
|
9613
|
+
// cancel callback, and an aborted recall is not a backend
|
|
9614
|
+
// degradation (no snapshot is recorded for it anyway).
|
|
9615
|
+
if (!options.abortSignal?.aborted) {
|
|
9616
|
+
backendDegradations.push({
|
|
9617
|
+
backend: "qmd",
|
|
9618
|
+
code: "deadline_exceeded",
|
|
9619
|
+
detail: "hot qmd enrichment abandoned (enrichment deadline)",
|
|
9620
|
+
});
|
|
9621
|
+
}
|
|
9622
|
+
qmdEnrichmentAbort.cancel();
|
|
9623
|
+
},
|
|
9546
9624
|
);
|
|
9547
9625
|
|
|
9548
9626
|
const transcriptPromise = (async (): Promise<string | null> => {
|
|
@@ -11329,6 +11407,9 @@ export class Orchestrator {
|
|
|
11329
11407
|
recallMode,
|
|
11330
11408
|
queryAwarePrefilter,
|
|
11331
11409
|
abortSignal: options.abortSignal,
|
|
11410
|
+
onDegradation: (degradation) => {
|
|
11411
|
+
backendDegradations.push(degradation);
|
|
11412
|
+
},
|
|
11332
11413
|
xrayPoolSizeSink: xrayColdPoolSink,
|
|
11333
11414
|
deadlineAtMs: enrichmentAssemblyDeadlineAtMs,
|
|
11334
11415
|
asOfMs,
|
|
@@ -11551,6 +11632,9 @@ export class Orchestrator {
|
|
|
11551
11632
|
recallMode,
|
|
11552
11633
|
queryAwarePrefilter,
|
|
11553
11634
|
abortSignal: options.abortSignal,
|
|
11635
|
+
onDegradation: (degradation) => {
|
|
11636
|
+
backendDegradations.push(degradation);
|
|
11637
|
+
},
|
|
11554
11638
|
xrayPoolSizeSink: xrayColdPoolSink,
|
|
11555
11639
|
deadlineAtMs: enrichmentAssemblyDeadlineAtMs,
|
|
11556
11640
|
asOfMs,
|
|
@@ -11660,6 +11744,9 @@ export class Orchestrator {
|
|
|
11660
11744
|
recallMode,
|
|
11661
11745
|
queryAwarePrefilter,
|
|
11662
11746
|
abortSignal: options.abortSignal,
|
|
11747
|
+
onDegradation: (degradation) => {
|
|
11748
|
+
backendDegradations.push(degradation);
|
|
11749
|
+
},
|
|
11663
11750
|
xrayPoolSizeSink: xrayColdPoolSink,
|
|
11664
11751
|
deadlineAtMs: enrichmentAssemblyDeadlineAtMs,
|
|
11665
11752
|
asOfMs,
|
|
@@ -11704,6 +11791,9 @@ export class Orchestrator {
|
|
|
11704
11791
|
recallMode,
|
|
11705
11792
|
queryAwarePrefilter,
|
|
11706
11793
|
abortSignal: options.abortSignal,
|
|
11794
|
+
onDegradation: (degradation) => {
|
|
11795
|
+
backendDegradations.push(degradation);
|
|
11796
|
+
},
|
|
11707
11797
|
xrayPoolSizeSink: xrayColdPoolSink,
|
|
11708
11798
|
deadlineAtMs: enrichmentAssemblyDeadlineAtMs,
|
|
11709
11799
|
asOfMs,
|
|
@@ -12105,6 +12195,13 @@ export class Orchestrator {
|
|
|
12105
12195
|
injectedChars: identityInjectedChars,
|
|
12106
12196
|
truncated: identityInjectionTruncated,
|
|
12107
12197
|
},
|
|
12198
|
+
// Included at record time so the published snapshot is born
|
|
12199
|
+
// annotated — a post-record annotation leaves a window where
|
|
12200
|
+
// readers see the snapshot without degradations, and a concurrent
|
|
12201
|
+
// same-session recall could drop them entirely (codex + cursor
|
|
12202
|
+
// reviews on #1544).
|
|
12203
|
+
backendDegradations:
|
|
12204
|
+
backendDegradations.length > 0 ? backendDegradations : undefined,
|
|
12108
12205
|
})
|
|
12109
12206
|
.catch((err) => log.debug(`last recall record failed: ${err}`));
|
|
12110
12207
|
}
|
|
@@ -18577,6 +18674,8 @@ export class Orchestrator {
|
|
|
18577
18674
|
recallMode: RecallPlanMode;
|
|
18578
18675
|
queryAwarePrefilter?: QueryAwarePrefilter;
|
|
18579
18676
|
abortSignal?: AbortSignal;
|
|
18677
|
+
/** Backend degradation observer — cold-tier QMD must report like hot (#1536). */
|
|
18678
|
+
onDegradation?: (degradation: SearchDegradation) => void;
|
|
18580
18679
|
/** Issue #680 — historical recall point in ms-since-epoch. */
|
|
18581
18680
|
asOfMs?: number;
|
|
18582
18681
|
/**
|
|
@@ -18604,10 +18703,19 @@ export class Orchestrator {
|
|
|
18604
18703
|
label: string,
|
|
18605
18704
|
fallback: T,
|
|
18606
18705
|
task: () => Promise<T>,
|
|
18706
|
+
// Invoked when the deadline abandons this step (before it started or
|
|
18707
|
+
// while it runs), so callers can report the abandonment and gate off
|
|
18708
|
+
// late observer callbacks (#1536, cursor round-6 on #1544).
|
|
18709
|
+
onDeadline?: () => void,
|
|
18607
18710
|
): Promise<T> => {
|
|
18608
18711
|
throwIfRecallAborted(options.abortSignal);
|
|
18609
18712
|
const remainingMs = deadlineRemainingMs();
|
|
18610
18713
|
if (remainingMs === 0) {
|
|
18714
|
+
try {
|
|
18715
|
+
onDeadline?.();
|
|
18716
|
+
} catch {
|
|
18717
|
+
// Observers must never break recall.
|
|
18718
|
+
}
|
|
18611
18719
|
log.debug(`cold-tier recall ${label} skipped: shared assembly deadline expired`);
|
|
18612
18720
|
return fallback;
|
|
18613
18721
|
}
|
|
@@ -18629,6 +18737,11 @@ export class Orchestrator {
|
|
|
18629
18737
|
new Promise<T>((resolve) => {
|
|
18630
18738
|
timeoutHandle = setTimeout(() => {
|
|
18631
18739
|
timedOut = true;
|
|
18740
|
+
try {
|
|
18741
|
+
onDeadline?.();
|
|
18742
|
+
} catch {
|
|
18743
|
+
// Observers must never break recall.
|
|
18744
|
+
}
|
|
18632
18745
|
log.debug(
|
|
18633
18746
|
`cold-tier recall ${label} skipped: shared assembly deadline expired`,
|
|
18634
18747
|
);
|
|
@@ -18659,6 +18772,24 @@ export class Orchestrator {
|
|
|
18659
18772
|
false,
|
|
18660
18773
|
0,
|
|
18661
18774
|
);
|
|
18775
|
+
// Deadline-gated observer (#1536, cursor round-6 on #1544): when the
|
|
18776
|
+
// shared assembly deadline abandons this lookup, the still-running
|
|
18777
|
+
// fetch's LATE reports must not land after the recall snapshot has
|
|
18778
|
+
// been recorded — gate them off and report the abandonment itself
|
|
18779
|
+
// deterministically at resolution time instead.
|
|
18780
|
+
let coldQmdObserverActive = true;
|
|
18781
|
+
const reportColdQmdDeadline = () => {
|
|
18782
|
+
coldQmdObserverActive = false;
|
|
18783
|
+
try {
|
|
18784
|
+
options.onDegradation?.({
|
|
18785
|
+
backend: "qmd",
|
|
18786
|
+
code: "deadline_exceeded",
|
|
18787
|
+
detail: "cold-tier qmd lookup abandoned (assembly deadline)",
|
|
18788
|
+
});
|
|
18789
|
+
} catch {
|
|
18790
|
+
// Observers must never break recall.
|
|
18791
|
+
}
|
|
18792
|
+
};
|
|
18662
18793
|
longTerm = await runColdStepWithinDeadline(
|
|
18663
18794
|
"qmd lookup",
|
|
18664
18795
|
[],
|
|
@@ -18675,9 +18806,19 @@ export class Orchestrator {
|
|
|
18675
18806
|
queryAwarePrefilter: options.queryAwarePrefilter,
|
|
18676
18807
|
searchOptions: this.buildConfiguredQmdSearchOptions(options.prompt),
|
|
18677
18808
|
abortSignal: options.abortSignal,
|
|
18809
|
+
onDegradation: (degradation) => {
|
|
18810
|
+
if (coldQmdObserverActive) {
|
|
18811
|
+
options.onDegradation?.(degradation);
|
|
18812
|
+
}
|
|
18813
|
+
},
|
|
18678
18814
|
},
|
|
18679
18815
|
),
|
|
18816
|
+
reportColdQmdDeadline,
|
|
18680
18817
|
);
|
|
18818
|
+
// Normal completion also closes the gate: a deadline that fires
|
|
18819
|
+
// after this await has nothing left to suppress, and a fetch that
|
|
18820
|
+
// limps home later cannot mutate a recorded recall's collector.
|
|
18821
|
+
coldQmdObserverActive = false;
|
|
18681
18822
|
if (longTerm.length > 0) {
|
|
18682
18823
|
log.debug(
|
|
18683
18824
|
`cold-tier recall source=cold-qmd collection=${coldCollection} hits=${longTerm.length}`,
|
package/src/qmd-recall-cache.ts
CHANGED
|
@@ -116,3 +116,9 @@ export function setCachedQmdRecall<T>(
|
|
|
116
116
|
export function clearQmdRecallCache(): void {
|
|
117
117
|
qmdRecallCache.clear();
|
|
118
118
|
}
|
|
119
|
+
|
|
120
|
+
/** Number of cached recall entries. Used by the cache-layer registry in
|
|
121
|
+
* memory-cache.ts (`ALL_CACHE_LAYERS`) and its fitness tests (issue #1535). */
|
|
122
|
+
export function qmdRecallCacheSize(): number {
|
|
123
|
+
return qmdRecallCache.size;
|
|
124
|
+
}
|
package/src/qmd.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { QmdSearchExplain, QmdSearchResult } from "./types.js";
|
|
|
12
12
|
import {
|
|
13
13
|
resolveEnsureCollectionArgs,
|
|
14
14
|
type SearchBackend,
|
|
15
|
+
type SearchDegradation,
|
|
15
16
|
type SearchExecutionOptions,
|
|
16
17
|
type SearchQueryOptions,
|
|
17
18
|
} from "./search/port.js";
|
|
@@ -1941,7 +1942,10 @@ export class QmdClient implements SearchBackend {
|
|
|
1941
1942
|
const trimmed = query.trim();
|
|
1942
1943
|
if (!trimmed) return [];
|
|
1943
1944
|
await this.maybeProbeDaemon();
|
|
1944
|
-
if (!this.isAvailable())
|
|
1945
|
+
if (!this.isAvailable()) {
|
|
1946
|
+
this.notifyDegradation(execution?.onDegradation, "backend_unavailable");
|
|
1947
|
+
return [];
|
|
1948
|
+
}
|
|
1945
1949
|
|
|
1946
1950
|
const col = collection ?? this.collection;
|
|
1947
1951
|
const n = maxResults ?? this.maxResults;
|
|
@@ -1962,6 +1966,12 @@ export class QmdClient implements SearchBackend {
|
|
|
1962
1966
|
.digest("hex");
|
|
1963
1967
|
const cached = getCachedQmdSearch(cacheKey);
|
|
1964
1968
|
if (cached) {
|
|
1969
|
+
// No degradation fires on a cache hit BY DESIGN (#1536): only
|
|
1970
|
+
// trustworthy results are ever cached (degraded empties are excluded
|
|
1971
|
+
// at the write below), so a TTL hit is a healthy serve of a recently
|
|
1972
|
+
// valid answer — the backend's live state is irrelevant because no
|
|
1973
|
+
// live call is attempted. Reporting unavailability here would fabricate
|
|
1974
|
+
// a degradation for a recall that was not degraded.
|
|
1965
1975
|
log.debug(`QMD search cache hit (${cached.length} results)`);
|
|
1966
1976
|
return cached as QmdSearchResult[];
|
|
1967
1977
|
}
|
|
@@ -1993,7 +2003,8 @@ export class QmdClient implements SearchBackend {
|
|
|
1993
2003
|
}
|
|
1994
2004
|
// Daemon timed out or had a transient error — skip subprocess for large
|
|
1995
2005
|
// collections. Return empty rather than hanging the caller.
|
|
1996
|
-
log.
|
|
2006
|
+
log.warn("QMD daemon search timed out/failed; skipping subprocess (daemon-only mode)");
|
|
2007
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_timeout");
|
|
1997
2008
|
return [];
|
|
1998
2009
|
}
|
|
1999
2010
|
|
|
@@ -2002,12 +2013,29 @@ export class QmdClient implements SearchBackend {
|
|
|
2002
2013
|
// Return empty and let the next recheck cycle pick up the daemon once ready.
|
|
2003
2014
|
if (this.daemonSession?.isLoading()) {
|
|
2004
2015
|
log.debug("QMD search: daemon loading, skipping subprocess");
|
|
2016
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_loading");
|
|
2005
2017
|
return [];
|
|
2006
2018
|
}
|
|
2007
2019
|
|
|
2008
2020
|
// Subprocess fallback (only reached when daemon is unavailable and not loading)
|
|
2009
|
-
|
|
2010
|
-
|
|
2021
|
+
let subprocessDegraded = false;
|
|
2022
|
+
const subprocessResults = await this.searchViaSubprocess(
|
|
2023
|
+
trimmed,
|
|
2024
|
+
col,
|
|
2025
|
+
n,
|
|
2026
|
+
searchOptions,
|
|
2027
|
+
execution?.signal,
|
|
2028
|
+
(degradation) => {
|
|
2029
|
+
subprocessDegraded = true;
|
|
2030
|
+
this.notifyDegradation(execution?.onDegradation, degradation.code, degradation.detail);
|
|
2031
|
+
},
|
|
2032
|
+
);
|
|
2033
|
+
// Never cache a degraded empty result: a 60s TTL hit would serve the
|
|
2034
|
+
// failure as a genuine no-matches WITHOUT re-reporting the degradation
|
|
2035
|
+
// (codex review on #1544). Only trustworthy results are cacheable.
|
|
2036
|
+
if (!subprocessDegraded) {
|
|
2037
|
+
setCachedQmdSearch(cacheKey, subprocessResults);
|
|
2038
|
+
}
|
|
2011
2039
|
return subprocessResults;
|
|
2012
2040
|
}
|
|
2013
2041
|
|
|
@@ -2019,7 +2047,10 @@ export class QmdClient implements SearchBackend {
|
|
|
2019
2047
|
const trimmed = query.trim();
|
|
2020
2048
|
if (!trimmed) return [];
|
|
2021
2049
|
await this.maybeProbeDaemon();
|
|
2022
|
-
if (!this.isAvailable())
|
|
2050
|
+
if (!this.isAvailable()) {
|
|
2051
|
+
this.notifyDegradation(execution?.onDegradation, "backend_unavailable");
|
|
2052
|
+
return [];
|
|
2053
|
+
}
|
|
2023
2054
|
|
|
2024
2055
|
const n = maxResults ?? 6;
|
|
2025
2056
|
const searchOptions = this.resolveSearchOptions();
|
|
@@ -2043,18 +2074,26 @@ export class QmdClient implements SearchBackend {
|
|
|
2043
2074
|
}
|
|
2044
2075
|
return results;
|
|
2045
2076
|
}
|
|
2046
|
-
log.
|
|
2077
|
+
log.warn("QMD daemon global search timed out/failed; skipping subprocess (daemon-only mode)");
|
|
2078
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_timeout");
|
|
2047
2079
|
return [];
|
|
2048
2080
|
}
|
|
2049
2081
|
|
|
2050
2082
|
// If the daemon is spawned but still loading, skip subprocess — same as search().
|
|
2051
2083
|
if (this.daemonSession?.isLoading()) {
|
|
2052
2084
|
log.debug("QMD searchGlobal: daemon loading, skipping subprocess");
|
|
2085
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_loading");
|
|
2053
2086
|
return [];
|
|
2054
2087
|
}
|
|
2055
2088
|
|
|
2056
2089
|
// Subprocess fallback (only reached when daemon is unavailable and not loading)
|
|
2057
|
-
return this.searchGlobalViaSubprocess(
|
|
2090
|
+
return this.searchGlobalViaSubprocess(
|
|
2091
|
+
trimmed,
|
|
2092
|
+
n,
|
|
2093
|
+
searchOptions,
|
|
2094
|
+
execution?.signal,
|
|
2095
|
+
execution?.onDegradation,
|
|
2096
|
+
);
|
|
2058
2097
|
}
|
|
2059
2098
|
|
|
2060
2099
|
/**
|
|
@@ -2069,7 +2108,10 @@ export class QmdClient implements SearchBackend {
|
|
|
2069
2108
|
const trimmed = query.trim();
|
|
2070
2109
|
if (!trimmed) return [];
|
|
2071
2110
|
await this.maybeProbeDaemon();
|
|
2072
|
-
if (!this.isAvailable())
|
|
2111
|
+
if (!this.isAvailable()) {
|
|
2112
|
+
this.notifyDegradation(execution?.onDegradation, "backend_unavailable");
|
|
2113
|
+
return [];
|
|
2114
|
+
}
|
|
2073
2115
|
const col = collection ?? this.collection;
|
|
2074
2116
|
const n = maxResults ?? this.maxResults;
|
|
2075
2117
|
|
|
@@ -2092,14 +2134,16 @@ export class QmdClient implements SearchBackend {
|
|
|
2092
2134
|
}
|
|
2093
2135
|
return results;
|
|
2094
2136
|
}
|
|
2095
|
-
log.
|
|
2137
|
+
log.warn("QMD daemon bm25 timed out/failed; skipping subprocess (daemon-only mode)");
|
|
2138
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_timeout");
|
|
2096
2139
|
return [];
|
|
2097
2140
|
}
|
|
2098
2141
|
if (this.daemonSession?.isLoading()) {
|
|
2099
2142
|
log.debug("QMD bm25: daemon loading, skipping subprocess");
|
|
2143
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_loading");
|
|
2100
2144
|
return [];
|
|
2101
2145
|
}
|
|
2102
|
-
return this.bm25SearchViaSubprocess(trimmed, col, n, execution?.signal);
|
|
2146
|
+
return this.bm25SearchViaSubprocess(trimmed, col, n, execution?.signal, execution?.onDegradation);
|
|
2103
2147
|
}
|
|
2104
2148
|
|
|
2105
2149
|
/**
|
|
@@ -2114,7 +2158,10 @@ export class QmdClient implements SearchBackend {
|
|
|
2114
2158
|
const trimmed = query.trim();
|
|
2115
2159
|
if (!trimmed) return [];
|
|
2116
2160
|
await this.maybeProbeDaemon();
|
|
2117
|
-
if (!this.isAvailable())
|
|
2161
|
+
if (!this.isAvailable()) {
|
|
2162
|
+
this.notifyDegradation(execution?.onDegradation, "backend_unavailable");
|
|
2163
|
+
return [];
|
|
2164
|
+
}
|
|
2118
2165
|
const col = collection ?? this.collection;
|
|
2119
2166
|
const n = maxResults ?? this.maxResults;
|
|
2120
2167
|
|
|
@@ -2137,14 +2184,16 @@ export class QmdClient implements SearchBackend {
|
|
|
2137
2184
|
}
|
|
2138
2185
|
return results;
|
|
2139
2186
|
}
|
|
2140
|
-
log.
|
|
2187
|
+
log.warn("QMD daemon vsearch timed out/failed; skipping subprocess (daemon-only mode)");
|
|
2188
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_timeout");
|
|
2141
2189
|
return [];
|
|
2142
2190
|
}
|
|
2143
2191
|
if (this.daemonSession?.isLoading()) {
|
|
2144
2192
|
log.debug("QMD vsearch: daemon loading, skipping subprocess");
|
|
2193
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_loading");
|
|
2145
2194
|
return [];
|
|
2146
2195
|
}
|
|
2147
|
-
return this.vsearchViaSubprocess(trimmed, col, n, execution?.signal);
|
|
2196
|
+
return this.vsearchViaSubprocess(trimmed, col, n, execution?.signal, execution?.onDegradation);
|
|
2148
2197
|
}
|
|
2149
2198
|
|
|
2150
2199
|
/**
|
|
@@ -2356,12 +2405,63 @@ export class QmdClient implements SearchBackend {
|
|
|
2356
2405
|
}
|
|
2357
2406
|
}
|
|
2358
2407
|
|
|
2408
|
+
/**
|
|
2409
|
+
* Report a backend degradation to the caller's observer (#1536): an empty
|
|
2410
|
+
* result caused by unavailability/loading/timeout is otherwise
|
|
2411
|
+
* indistinguishable from "no matches" (CLAUDE.md rule 34). Observer
|
|
2412
|
+
* failures are swallowed — observability must never break search.
|
|
2413
|
+
*/
|
|
2414
|
+
private notifyDegradation(
|
|
2415
|
+
onDegradation: SearchExecutionOptions["onDegradation"],
|
|
2416
|
+
code: SearchDegradation["code"],
|
|
2417
|
+
detail?: string,
|
|
2418
|
+
): void {
|
|
2419
|
+
if (!onDegradation) return;
|
|
2420
|
+
try {
|
|
2421
|
+
onDegradation({ backend: "qmd", code, ...(detail !== undefined ? { detail } : {}) });
|
|
2422
|
+
} catch (err) {
|
|
2423
|
+
log.debug(`QMD degradation observer threw: ${err}`);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
/**
|
|
2428
|
+
* Condense an error into a degradation `detail` string that is safe to
|
|
2429
|
+
* serialize on LastRecallSnapshot and expose via last-recall MCP/HTTP
|
|
2430
|
+
* surfaces (cursor review on #1544): first line only, path-like tokens
|
|
2431
|
+
* redacted (absolute, home-rooted, and Windows drive paths can leak
|
|
2432
|
+
* usernames and filesystem layout), capped at 160 chars. The unredacted
|
|
2433
|
+
* error still reaches operators via the warn log at the failure site.
|
|
2434
|
+
*/
|
|
2435
|
+
private degradationDetail(err: unknown, sensitive?: string[]): string {
|
|
2436
|
+
let message = String(err instanceof Error ? err.message : err);
|
|
2437
|
+
// Strip known-sensitive strings BEFORE any line-splitting: runQmdCommand
|
|
2438
|
+
// error labels embed the full command line including the raw recall
|
|
2439
|
+
// query, and a multi-line query would otherwise leave its first-line
|
|
2440
|
+
// prefix behind after the first-line cut (codex round-6 P1 on #1544).
|
|
2441
|
+
// Whole values first, then their individual line fragments so truncated
|
|
2442
|
+
// embeddings cannot leak partial prompt text either.
|
|
2443
|
+
for (const value of sensitive ?? []) {
|
|
2444
|
+
if (typeof value !== "string" || value.length === 0) continue;
|
|
2445
|
+
message = message.split(value).join("<query>");
|
|
2446
|
+
for (const fragment of value.split("\n")) {
|
|
2447
|
+
const trimmed = fragment.trim();
|
|
2448
|
+
if (trimmed.length >= 4) {
|
|
2449
|
+
message = message.split(trimmed).join("<query>");
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
const firstLine = message.split("\n")[0] ?? "";
|
|
2454
|
+
const redacted = firstLine.replace(/(?:~|\/|[A-Za-z]:\\)[^\s'"`]+/g, "<path>");
|
|
2455
|
+
return redacted.length > 160 ? `${redacted.slice(0, 159)}…` : redacted;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2359
2458
|
private async searchViaSubprocess(
|
|
2360
2459
|
query: string,
|
|
2361
2460
|
collection: string,
|
|
2362
2461
|
maxResults: number,
|
|
2363
2462
|
options?: SearchQueryOptions,
|
|
2364
2463
|
signal?: AbortSignal,
|
|
2464
|
+
onDegradation?: SearchExecutionOptions["onDegradation"],
|
|
2365
2465
|
): Promise<QmdSearchResult[]> {
|
|
2366
2466
|
if (this.available === false) return [];
|
|
2367
2467
|
|
|
@@ -2373,7 +2473,7 @@ export class QmdClient implements SearchBackend {
|
|
|
2373
2473
|
// `qmdSubprocessStrategy: "search"` — but that trades away expansion + rerank,
|
|
2374
2474
|
// so it stays opt-in and the default remains `query`.
|
|
2375
2475
|
if (this.qmdSubprocessStrategy === "search") {
|
|
2376
|
-
return this.bm25SearchViaSubprocess(query, collection, maxResults, signal);
|
|
2476
|
+
return this.bm25SearchViaSubprocess(query, collection, maxResults, signal, onDegradation);
|
|
2377
2477
|
}
|
|
2378
2478
|
|
|
2379
2479
|
const startedAtMs = Date.now();
|
|
@@ -2395,7 +2495,11 @@ export class QmdClient implements SearchBackend {
|
|
|
2395
2495
|
if (isCallerCancellation(err, signal)) {
|
|
2396
2496
|
throw isAbortError(err) ? err : abortError("QMD subprocess search aborted");
|
|
2397
2497
|
}
|
|
2398
|
-
|
|
2498
|
+
// Sanitized in the LOG too: the raw error label embeds the argv,
|
|
2499
|
+
// which includes the user's recall query (codex round-5 P1 on #1544).
|
|
2500
|
+
const detail = this.degradationDetail(err, [query]);
|
|
2501
|
+
log.warn(`QMD subprocess search failed (returning empty): ${detail}`);
|
|
2502
|
+
this.notifyDegradation(onDegradation, "subprocess_error", detail);
|
|
2399
2503
|
return [];
|
|
2400
2504
|
}
|
|
2401
2505
|
}
|
|
@@ -2405,6 +2509,7 @@ export class QmdClient implements SearchBackend {
|
|
|
2405
2509
|
collection: string,
|
|
2406
2510
|
maxResults: number,
|
|
2407
2511
|
signal?: AbortSignal,
|
|
2512
|
+
onDegradation?: SearchExecutionOptions["onDegradation"],
|
|
2408
2513
|
): Promise<QmdSearchResult[]> {
|
|
2409
2514
|
if (this.available === false) return [];
|
|
2410
2515
|
const startedAtMs = Date.now();
|
|
@@ -2419,7 +2524,9 @@ export class QmdClient implements SearchBackend {
|
|
|
2419
2524
|
if (isCallerCancellation(err, signal)) {
|
|
2420
2525
|
throw isAbortError(err) ? err : abortError("QMD subprocess bm25 aborted");
|
|
2421
2526
|
}
|
|
2422
|
-
|
|
2527
|
+
const detail = this.degradationDetail(err, [query]);
|
|
2528
|
+
log.warn(`QMD bm25 subprocess search failed (returning empty): ${detail}`);
|
|
2529
|
+
this.notifyDegradation(onDegradation, "subprocess_error", detail);
|
|
2423
2530
|
return [];
|
|
2424
2531
|
}
|
|
2425
2532
|
}
|
|
@@ -2429,6 +2536,7 @@ export class QmdClient implements SearchBackend {
|
|
|
2429
2536
|
collection: string,
|
|
2430
2537
|
maxResults: number,
|
|
2431
2538
|
signal?: AbortSignal,
|
|
2539
|
+
onDegradation?: SearchExecutionOptions["onDegradation"],
|
|
2432
2540
|
): Promise<QmdSearchResult[]> {
|
|
2433
2541
|
if (this.available === false) return [];
|
|
2434
2542
|
const startedAtMs = Date.now();
|
|
@@ -2444,7 +2552,9 @@ export class QmdClient implements SearchBackend {
|
|
|
2444
2552
|
if (isCallerCancellation(err, signal)) {
|
|
2445
2553
|
throw isAbortError(err) ? err : abortError("QMD subprocess vsearch aborted");
|
|
2446
2554
|
}
|
|
2447
|
-
|
|
2555
|
+
const detail = this.degradationDetail(err, [query]);
|
|
2556
|
+
log.warn(`QMD vsearch subprocess failed (returning empty): ${detail}`);
|
|
2557
|
+
this.notifyDegradation(onDegradation, "subprocess_error", detail);
|
|
2448
2558
|
return [];
|
|
2449
2559
|
}
|
|
2450
2560
|
}
|
|
@@ -2454,6 +2564,7 @@ export class QmdClient implements SearchBackend {
|
|
|
2454
2564
|
maxResults: number,
|
|
2455
2565
|
options?: SearchQueryOptions,
|
|
2456
2566
|
signal?: AbortSignal,
|
|
2567
|
+
onDegradation?: SearchExecutionOptions["onDegradation"],
|
|
2457
2568
|
): Promise<QmdSearchResult[]> {
|
|
2458
2569
|
if (this.available === false) return [];
|
|
2459
2570
|
|
|
@@ -2485,7 +2596,9 @@ export class QmdClient implements SearchBackend {
|
|
|
2485
2596
|
if (isCallerCancellation(err, signal)) {
|
|
2486
2597
|
throw isAbortError(err) ? err : abortError("QMD subprocess global search aborted");
|
|
2487
2598
|
}
|
|
2488
|
-
|
|
2599
|
+
const detail = this.degradationDetail(err, [query]);
|
|
2600
|
+
log.warn(`QMD global subprocess search failed (returning empty): ${detail}`);
|
|
2601
|
+
this.notifyDegradation(onDegradation, "subprocess_error", detail);
|
|
2489
2602
|
return [];
|
|
2490
2603
|
}
|
|
2491
2604
|
}
|