@remnic/core 9.3.660 → 9.3.662
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 +10 -10
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +10 -10
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +9 -9
- 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 +8 -8
- 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/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/{chunk-7H7J3ZWN.js → chunk-2KDQI363.js} +2 -2
- package/dist/{chunk-R2EBP6CM.js → chunk-35HP3TGR.js} +5 -5
- package/dist/{chunk-FWIROLS6.js → chunk-44VFF3BB.js} +18 -16
- package/dist/chunk-44VFF3BB.js.map +1 -0
- package/dist/{chunk-OYXVENIS.js → chunk-4KDLCMLK.js} +3 -3
- package/dist/{chunk-MO77TWPS.js → chunk-5AYAZN45.js} +2 -2
- package/dist/{chunk-7PCZGNG2.js → chunk-6RHNCKHG.js} +113 -24
- package/dist/chunk-6RHNCKHG.js.map +1 -0
- package/dist/{chunk-RP2U54GG.js → chunk-DFAXGZKI.js} +2 -2
- package/dist/{chunk-6G5JEN55.js → chunk-FZC2WSDB.js} +2 -2
- package/dist/{chunk-2EVZ5EN6.js → chunk-HSCJYHYV.js} +6 -6
- package/dist/{chunk-B57QYSWN.js → chunk-TGOOJCGA.js} +109 -16
- package/dist/chunk-TGOOJCGA.js.map +1 -0
- package/dist/{chunk-UNLHHTKN.js → chunk-WOQIHC67.js} +10 -2
- package/dist/chunk-WOQIHC67.js.map +1 -0
- 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-256W7AXC.js → chunk-YYQRVNSV.js} +2 -2
- 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 +13 -13
- package/dist/compounding/engine.d.ts +1 -1
- 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.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- 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-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 +15 -15
- 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/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +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 +8 -8
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +9 -9
- package/dist/{orchestrator-D1wcmPNj.d.ts → orchestrator-Cg1UkvmO.d.ts} +2 -2
- package/dist/orchestrator.d.ts +3 -3
- package/dist/orchestrator.js +9 -9
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +50 -2
- package/dist/qmd.js +8 -2
- 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/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +6 -6
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +6 -6
- 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-rule-verifier.d.ts +1 -1
- 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/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/package.json +3 -2
- package/scripts/build-with-heap.mjs +25 -0
- package/src/namespaces/search.ts +16 -0
- package/src/orchestrator.ts +144 -3
- package/src/qmd.ts +137 -18
- package/src/recall-state.ts +47 -21
- package/src/search/port.ts +25 -0
- package/dist/chunk-7PCZGNG2.js.map +0 -1
- package/dist/chunk-B57QYSWN.js.map +0 -1
- package/dist/chunk-FWIROLS6.js.map +0 -1
- package/dist/chunk-M3VYPE2H.js.map +0 -1
- package/dist/chunk-UNLHHTKN.js.map +0 -1
- /package/dist/{chunk-7H7J3ZWN.js.map → chunk-2KDQI363.js.map} +0 -0
- /package/dist/{chunk-R2EBP6CM.js.map → chunk-35HP3TGR.js.map} +0 -0
- /package/dist/{chunk-OYXVENIS.js.map → chunk-4KDLCMLK.js.map} +0 -0
- /package/dist/{chunk-MO77TWPS.js.map → chunk-5AYAZN45.js.map} +0 -0
- /package/dist/{chunk-RP2U54GG.js.map → chunk-DFAXGZKI.js.map} +0 -0
- /package/dist/{chunk-6G5JEN55.js.map → chunk-FZC2WSDB.js.map} +0 -0
- /package/dist/{chunk-2EVZ5EN6.js.map → chunk-HSCJYHYV.js.map} +0 -0
- /package/dist/{chunk-5PLUC5OB.js.map → chunk-WSQG37DV.js.map} +0 -0
- /package/dist/{chunk-256W7AXC.js.map → chunk-YYQRVNSV.js.map} +0 -0
- /package/dist/{chunk-GRYAECRV.js.map → chunk-ZJH723NM.js.map} +0 -0
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";
|
|
@@ -1211,6 +1212,12 @@ async function releaseSharedDaemonSession(session: QmdDaemonSession | null): Pro
|
|
|
1211
1212
|
}
|
|
1212
1213
|
}
|
|
1213
1214
|
|
|
1215
|
+
// Test-only seams (#1537): lifecycle tests assert the pool reaches zero after
|
|
1216
|
+
// every holder disposes — the property the stop()-teardown fix restores.
|
|
1217
|
+
export const sharedDaemonSessionCountForTest = (): number => SHARED_DAEMON_SESSIONS.size;
|
|
1218
|
+
export { retainSharedDaemonSession as retainSharedDaemonSessionForTest };
|
|
1219
|
+
export { releaseSharedDaemonSession as releaseSharedDaemonSessionForTest };
|
|
1220
|
+
|
|
1214
1221
|
// ---------------------------------------------------------------------------
|
|
1215
1222
|
// QmdClient
|
|
1216
1223
|
// ---------------------------------------------------------------------------
|
|
@@ -1941,7 +1948,10 @@ export class QmdClient implements SearchBackend {
|
|
|
1941
1948
|
const trimmed = query.trim();
|
|
1942
1949
|
if (!trimmed) return [];
|
|
1943
1950
|
await this.maybeProbeDaemon();
|
|
1944
|
-
if (!this.isAvailable())
|
|
1951
|
+
if (!this.isAvailable()) {
|
|
1952
|
+
this.notifyDegradation(execution?.onDegradation, "backend_unavailable");
|
|
1953
|
+
return [];
|
|
1954
|
+
}
|
|
1945
1955
|
|
|
1946
1956
|
const col = collection ?? this.collection;
|
|
1947
1957
|
const n = maxResults ?? this.maxResults;
|
|
@@ -1962,6 +1972,12 @@ export class QmdClient implements SearchBackend {
|
|
|
1962
1972
|
.digest("hex");
|
|
1963
1973
|
const cached = getCachedQmdSearch(cacheKey);
|
|
1964
1974
|
if (cached) {
|
|
1975
|
+
// No degradation fires on a cache hit BY DESIGN (#1536): only
|
|
1976
|
+
// trustworthy results are ever cached (degraded empties are excluded
|
|
1977
|
+
// at the write below), so a TTL hit is a healthy serve of a recently
|
|
1978
|
+
// valid answer — the backend's live state is irrelevant because no
|
|
1979
|
+
// live call is attempted. Reporting unavailability here would fabricate
|
|
1980
|
+
// a degradation for a recall that was not degraded.
|
|
1965
1981
|
log.debug(`QMD search cache hit (${cached.length} results)`);
|
|
1966
1982
|
return cached as QmdSearchResult[];
|
|
1967
1983
|
}
|
|
@@ -1993,7 +2009,8 @@ export class QmdClient implements SearchBackend {
|
|
|
1993
2009
|
}
|
|
1994
2010
|
// Daemon timed out or had a transient error — skip subprocess for large
|
|
1995
2011
|
// collections. Return empty rather than hanging the caller.
|
|
1996
|
-
log.
|
|
2012
|
+
log.warn("QMD daemon search timed out/failed; skipping subprocess (daemon-only mode)");
|
|
2013
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_timeout");
|
|
1997
2014
|
return [];
|
|
1998
2015
|
}
|
|
1999
2016
|
|
|
@@ -2002,12 +2019,29 @@ export class QmdClient implements SearchBackend {
|
|
|
2002
2019
|
// Return empty and let the next recheck cycle pick up the daemon once ready.
|
|
2003
2020
|
if (this.daemonSession?.isLoading()) {
|
|
2004
2021
|
log.debug("QMD search: daemon loading, skipping subprocess");
|
|
2022
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_loading");
|
|
2005
2023
|
return [];
|
|
2006
2024
|
}
|
|
2007
2025
|
|
|
2008
2026
|
// Subprocess fallback (only reached when daemon is unavailable and not loading)
|
|
2009
|
-
|
|
2010
|
-
|
|
2027
|
+
let subprocessDegraded = false;
|
|
2028
|
+
const subprocessResults = await this.searchViaSubprocess(
|
|
2029
|
+
trimmed,
|
|
2030
|
+
col,
|
|
2031
|
+
n,
|
|
2032
|
+
searchOptions,
|
|
2033
|
+
execution?.signal,
|
|
2034
|
+
(degradation) => {
|
|
2035
|
+
subprocessDegraded = true;
|
|
2036
|
+
this.notifyDegradation(execution?.onDegradation, degradation.code, degradation.detail);
|
|
2037
|
+
},
|
|
2038
|
+
);
|
|
2039
|
+
// Never cache a degraded empty result: a 60s TTL hit would serve the
|
|
2040
|
+
// failure as a genuine no-matches WITHOUT re-reporting the degradation
|
|
2041
|
+
// (codex review on #1544). Only trustworthy results are cacheable.
|
|
2042
|
+
if (!subprocessDegraded) {
|
|
2043
|
+
setCachedQmdSearch(cacheKey, subprocessResults);
|
|
2044
|
+
}
|
|
2011
2045
|
return subprocessResults;
|
|
2012
2046
|
}
|
|
2013
2047
|
|
|
@@ -2019,7 +2053,10 @@ export class QmdClient implements SearchBackend {
|
|
|
2019
2053
|
const trimmed = query.trim();
|
|
2020
2054
|
if (!trimmed) return [];
|
|
2021
2055
|
await this.maybeProbeDaemon();
|
|
2022
|
-
if (!this.isAvailable())
|
|
2056
|
+
if (!this.isAvailable()) {
|
|
2057
|
+
this.notifyDegradation(execution?.onDegradation, "backend_unavailable");
|
|
2058
|
+
return [];
|
|
2059
|
+
}
|
|
2023
2060
|
|
|
2024
2061
|
const n = maxResults ?? 6;
|
|
2025
2062
|
const searchOptions = this.resolveSearchOptions();
|
|
@@ -2043,18 +2080,26 @@ export class QmdClient implements SearchBackend {
|
|
|
2043
2080
|
}
|
|
2044
2081
|
return results;
|
|
2045
2082
|
}
|
|
2046
|
-
log.
|
|
2083
|
+
log.warn("QMD daemon global search timed out/failed; skipping subprocess (daemon-only mode)");
|
|
2084
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_timeout");
|
|
2047
2085
|
return [];
|
|
2048
2086
|
}
|
|
2049
2087
|
|
|
2050
2088
|
// If the daemon is spawned but still loading, skip subprocess — same as search().
|
|
2051
2089
|
if (this.daemonSession?.isLoading()) {
|
|
2052
2090
|
log.debug("QMD searchGlobal: daemon loading, skipping subprocess");
|
|
2091
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_loading");
|
|
2053
2092
|
return [];
|
|
2054
2093
|
}
|
|
2055
2094
|
|
|
2056
2095
|
// Subprocess fallback (only reached when daemon is unavailable and not loading)
|
|
2057
|
-
return this.searchGlobalViaSubprocess(
|
|
2096
|
+
return this.searchGlobalViaSubprocess(
|
|
2097
|
+
trimmed,
|
|
2098
|
+
n,
|
|
2099
|
+
searchOptions,
|
|
2100
|
+
execution?.signal,
|
|
2101
|
+
execution?.onDegradation,
|
|
2102
|
+
);
|
|
2058
2103
|
}
|
|
2059
2104
|
|
|
2060
2105
|
/**
|
|
@@ -2069,7 +2114,10 @@ export class QmdClient implements SearchBackend {
|
|
|
2069
2114
|
const trimmed = query.trim();
|
|
2070
2115
|
if (!trimmed) return [];
|
|
2071
2116
|
await this.maybeProbeDaemon();
|
|
2072
|
-
if (!this.isAvailable())
|
|
2117
|
+
if (!this.isAvailable()) {
|
|
2118
|
+
this.notifyDegradation(execution?.onDegradation, "backend_unavailable");
|
|
2119
|
+
return [];
|
|
2120
|
+
}
|
|
2073
2121
|
const col = collection ?? this.collection;
|
|
2074
2122
|
const n = maxResults ?? this.maxResults;
|
|
2075
2123
|
|
|
@@ -2092,14 +2140,16 @@ export class QmdClient implements SearchBackend {
|
|
|
2092
2140
|
}
|
|
2093
2141
|
return results;
|
|
2094
2142
|
}
|
|
2095
|
-
log.
|
|
2143
|
+
log.warn("QMD daemon bm25 timed out/failed; skipping subprocess (daemon-only mode)");
|
|
2144
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_timeout");
|
|
2096
2145
|
return [];
|
|
2097
2146
|
}
|
|
2098
2147
|
if (this.daemonSession?.isLoading()) {
|
|
2099
2148
|
log.debug("QMD bm25: daemon loading, skipping subprocess");
|
|
2149
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_loading");
|
|
2100
2150
|
return [];
|
|
2101
2151
|
}
|
|
2102
|
-
return this.bm25SearchViaSubprocess(trimmed, col, n, execution?.signal);
|
|
2152
|
+
return this.bm25SearchViaSubprocess(trimmed, col, n, execution?.signal, execution?.onDegradation);
|
|
2103
2153
|
}
|
|
2104
2154
|
|
|
2105
2155
|
/**
|
|
@@ -2114,7 +2164,10 @@ export class QmdClient implements SearchBackend {
|
|
|
2114
2164
|
const trimmed = query.trim();
|
|
2115
2165
|
if (!trimmed) return [];
|
|
2116
2166
|
await this.maybeProbeDaemon();
|
|
2117
|
-
if (!this.isAvailable())
|
|
2167
|
+
if (!this.isAvailable()) {
|
|
2168
|
+
this.notifyDegradation(execution?.onDegradation, "backend_unavailable");
|
|
2169
|
+
return [];
|
|
2170
|
+
}
|
|
2118
2171
|
const col = collection ?? this.collection;
|
|
2119
2172
|
const n = maxResults ?? this.maxResults;
|
|
2120
2173
|
|
|
@@ -2137,14 +2190,16 @@ export class QmdClient implements SearchBackend {
|
|
|
2137
2190
|
}
|
|
2138
2191
|
return results;
|
|
2139
2192
|
}
|
|
2140
|
-
log.
|
|
2193
|
+
log.warn("QMD daemon vsearch timed out/failed; skipping subprocess (daemon-only mode)");
|
|
2194
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_timeout");
|
|
2141
2195
|
return [];
|
|
2142
2196
|
}
|
|
2143
2197
|
if (this.daemonSession?.isLoading()) {
|
|
2144
2198
|
log.debug("QMD vsearch: daemon loading, skipping subprocess");
|
|
2199
|
+
this.notifyDegradation(execution?.onDegradation, "daemon_loading");
|
|
2145
2200
|
return [];
|
|
2146
2201
|
}
|
|
2147
|
-
return this.vsearchViaSubprocess(trimmed, col, n, execution?.signal);
|
|
2202
|
+
return this.vsearchViaSubprocess(trimmed, col, n, execution?.signal, execution?.onDegradation);
|
|
2148
2203
|
}
|
|
2149
2204
|
|
|
2150
2205
|
/**
|
|
@@ -2356,12 +2411,63 @@ export class QmdClient implements SearchBackend {
|
|
|
2356
2411
|
}
|
|
2357
2412
|
}
|
|
2358
2413
|
|
|
2414
|
+
/**
|
|
2415
|
+
* Report a backend degradation to the caller's observer (#1536): an empty
|
|
2416
|
+
* result caused by unavailability/loading/timeout is otherwise
|
|
2417
|
+
* indistinguishable from "no matches" (CLAUDE.md rule 34). Observer
|
|
2418
|
+
* failures are swallowed — observability must never break search.
|
|
2419
|
+
*/
|
|
2420
|
+
private notifyDegradation(
|
|
2421
|
+
onDegradation: SearchExecutionOptions["onDegradation"],
|
|
2422
|
+
code: SearchDegradation["code"],
|
|
2423
|
+
detail?: string,
|
|
2424
|
+
): void {
|
|
2425
|
+
if (!onDegradation) return;
|
|
2426
|
+
try {
|
|
2427
|
+
onDegradation({ backend: "qmd", code, ...(detail !== undefined ? { detail } : {}) });
|
|
2428
|
+
} catch (err) {
|
|
2429
|
+
log.debug(`QMD degradation observer threw: ${err}`);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* Condense an error into a degradation `detail` string that is safe to
|
|
2435
|
+
* serialize on LastRecallSnapshot and expose via last-recall MCP/HTTP
|
|
2436
|
+
* surfaces (cursor review on #1544): first line only, path-like tokens
|
|
2437
|
+
* redacted (absolute, home-rooted, and Windows drive paths can leak
|
|
2438
|
+
* usernames and filesystem layout), capped at 160 chars. The unredacted
|
|
2439
|
+
* error still reaches operators via the warn log at the failure site.
|
|
2440
|
+
*/
|
|
2441
|
+
private degradationDetail(err: unknown, sensitive?: string[]): string {
|
|
2442
|
+
let message = String(err instanceof Error ? err.message : err);
|
|
2443
|
+
// Strip known-sensitive strings BEFORE any line-splitting: runQmdCommand
|
|
2444
|
+
// error labels embed the full command line including the raw recall
|
|
2445
|
+
// query, and a multi-line query would otherwise leave its first-line
|
|
2446
|
+
// prefix behind after the first-line cut (codex round-6 P1 on #1544).
|
|
2447
|
+
// Whole values first, then their individual line fragments so truncated
|
|
2448
|
+
// embeddings cannot leak partial prompt text either.
|
|
2449
|
+
for (const value of sensitive ?? []) {
|
|
2450
|
+
if (typeof value !== "string" || value.length === 0) continue;
|
|
2451
|
+
message = message.split(value).join("<query>");
|
|
2452
|
+
for (const fragment of value.split("\n")) {
|
|
2453
|
+
const trimmed = fragment.trim();
|
|
2454
|
+
if (trimmed.length >= 4) {
|
|
2455
|
+
message = message.split(trimmed).join("<query>");
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
const firstLine = message.split("\n")[0] ?? "";
|
|
2460
|
+
const redacted = firstLine.replace(/(?:~|\/|[A-Za-z]:\\)[^\s'"`]+/g, "<path>");
|
|
2461
|
+
return redacted.length > 160 ? `${redacted.slice(0, 159)}…` : redacted;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2359
2464
|
private async searchViaSubprocess(
|
|
2360
2465
|
query: string,
|
|
2361
2466
|
collection: string,
|
|
2362
2467
|
maxResults: number,
|
|
2363
2468
|
options?: SearchQueryOptions,
|
|
2364
2469
|
signal?: AbortSignal,
|
|
2470
|
+
onDegradation?: SearchExecutionOptions["onDegradation"],
|
|
2365
2471
|
): Promise<QmdSearchResult[]> {
|
|
2366
2472
|
if (this.available === false) return [];
|
|
2367
2473
|
|
|
@@ -2373,7 +2479,7 @@ export class QmdClient implements SearchBackend {
|
|
|
2373
2479
|
// `qmdSubprocessStrategy: "search"` — but that trades away expansion + rerank,
|
|
2374
2480
|
// so it stays opt-in and the default remains `query`.
|
|
2375
2481
|
if (this.qmdSubprocessStrategy === "search") {
|
|
2376
|
-
return this.bm25SearchViaSubprocess(query, collection, maxResults, signal);
|
|
2482
|
+
return this.bm25SearchViaSubprocess(query, collection, maxResults, signal, onDegradation);
|
|
2377
2483
|
}
|
|
2378
2484
|
|
|
2379
2485
|
const startedAtMs = Date.now();
|
|
@@ -2395,7 +2501,11 @@ export class QmdClient implements SearchBackend {
|
|
|
2395
2501
|
if (isCallerCancellation(err, signal)) {
|
|
2396
2502
|
throw isAbortError(err) ? err : abortError("QMD subprocess search aborted");
|
|
2397
2503
|
}
|
|
2398
|
-
|
|
2504
|
+
// Sanitized in the LOG too: the raw error label embeds the argv,
|
|
2505
|
+
// which includes the user's recall query (codex round-5 P1 on #1544).
|
|
2506
|
+
const detail = this.degradationDetail(err, [query]);
|
|
2507
|
+
log.warn(`QMD subprocess search failed (returning empty): ${detail}`);
|
|
2508
|
+
this.notifyDegradation(onDegradation, "subprocess_error", detail);
|
|
2399
2509
|
return [];
|
|
2400
2510
|
}
|
|
2401
2511
|
}
|
|
@@ -2405,6 +2515,7 @@ export class QmdClient implements SearchBackend {
|
|
|
2405
2515
|
collection: string,
|
|
2406
2516
|
maxResults: number,
|
|
2407
2517
|
signal?: AbortSignal,
|
|
2518
|
+
onDegradation?: SearchExecutionOptions["onDegradation"],
|
|
2408
2519
|
): Promise<QmdSearchResult[]> {
|
|
2409
2520
|
if (this.available === false) return [];
|
|
2410
2521
|
const startedAtMs = Date.now();
|
|
@@ -2419,7 +2530,9 @@ export class QmdClient implements SearchBackend {
|
|
|
2419
2530
|
if (isCallerCancellation(err, signal)) {
|
|
2420
2531
|
throw isAbortError(err) ? err : abortError("QMD subprocess bm25 aborted");
|
|
2421
2532
|
}
|
|
2422
|
-
|
|
2533
|
+
const detail = this.degradationDetail(err, [query]);
|
|
2534
|
+
log.warn(`QMD bm25 subprocess search failed (returning empty): ${detail}`);
|
|
2535
|
+
this.notifyDegradation(onDegradation, "subprocess_error", detail);
|
|
2423
2536
|
return [];
|
|
2424
2537
|
}
|
|
2425
2538
|
}
|
|
@@ -2429,6 +2542,7 @@ export class QmdClient implements SearchBackend {
|
|
|
2429
2542
|
collection: string,
|
|
2430
2543
|
maxResults: number,
|
|
2431
2544
|
signal?: AbortSignal,
|
|
2545
|
+
onDegradation?: SearchExecutionOptions["onDegradation"],
|
|
2432
2546
|
): Promise<QmdSearchResult[]> {
|
|
2433
2547
|
if (this.available === false) return [];
|
|
2434
2548
|
const startedAtMs = Date.now();
|
|
@@ -2444,7 +2558,9 @@ export class QmdClient implements SearchBackend {
|
|
|
2444
2558
|
if (isCallerCancellation(err, signal)) {
|
|
2445
2559
|
throw isAbortError(err) ? err : abortError("QMD subprocess vsearch aborted");
|
|
2446
2560
|
}
|
|
2447
|
-
|
|
2561
|
+
const detail = this.degradationDetail(err, [query]);
|
|
2562
|
+
log.warn(`QMD vsearch subprocess failed (returning empty): ${detail}`);
|
|
2563
|
+
this.notifyDegradation(onDegradation, "subprocess_error", detail);
|
|
2448
2564
|
return [];
|
|
2449
2565
|
}
|
|
2450
2566
|
}
|
|
@@ -2454,6 +2570,7 @@ export class QmdClient implements SearchBackend {
|
|
|
2454
2570
|
maxResults: number,
|
|
2455
2571
|
options?: SearchQueryOptions,
|
|
2456
2572
|
signal?: AbortSignal,
|
|
2573
|
+
onDegradation?: SearchExecutionOptions["onDegradation"],
|
|
2457
2574
|
): Promise<QmdSearchResult[]> {
|
|
2458
2575
|
if (this.available === false) return [];
|
|
2459
2576
|
|
|
@@ -2485,7 +2602,9 @@ export class QmdClient implements SearchBackend {
|
|
|
2485
2602
|
if (isCallerCancellation(err, signal)) {
|
|
2486
2603
|
throw isAbortError(err) ? err : abortError("QMD subprocess global search aborted");
|
|
2487
2604
|
}
|
|
2488
|
-
|
|
2605
|
+
const detail = this.degradationDetail(err, [query]);
|
|
2606
|
+
log.warn(`QMD global subprocess search failed (returning empty): ${detail}`);
|
|
2607
|
+
this.notifyDegradation(onDegradation, "subprocess_error", detail);
|
|
2489
2608
|
return [];
|
|
2490
2609
|
}
|
|
2491
2610
|
}
|
package/src/recall-state.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { log } from "./logger.js";
|
|
5
5
|
import { writeFileAtomically } from "./maintenance/atomic-file.js";
|
|
6
|
+
import type { SearchDegradation } from "./search/port.js";
|
|
6
7
|
import type {
|
|
7
8
|
IdentityInjectionMode,
|
|
8
9
|
RecallPlanMode,
|
|
@@ -59,6 +60,14 @@ export interface LastRecallSnapshot {
|
|
|
59
60
|
* graph-path `recallExplain` operation.
|
|
60
61
|
*/
|
|
61
62
|
tierExplain?: RecallTierExplain;
|
|
63
|
+
/**
|
|
64
|
+
* Backend degradations observed while serving this recall (issue #1536).
|
|
65
|
+
* Present only when a search backend reported unavailable/loading/timeout
|
|
66
|
+
* during the recall — distinguishing "no matches" from "backend could not
|
|
67
|
+
* answer" (CLAUDE.md rule 34). Deliberately independent of `tierExplain`,
|
|
68
|
+
* which is gated behind `recallDirectAnswerEnabled`.
|
|
69
|
+
*/
|
|
70
|
+
backendDegradations?: SearchDegradation[];
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
export interface GraphRecallExpandedEntry {
|
|
@@ -136,6 +145,31 @@ type StateFileWriter = (filePath: string, content: string) => Promise<void>;
|
|
|
136
145
|
* qmd-recall-cache.ts). The payload is pure JSON-shaped data, so
|
|
137
146
|
* structuredClone is both safe and complete here.
|
|
138
147
|
*/
|
|
148
|
+
/**
|
|
149
|
+
* Stale-snapshot identity guard shared by the annotate methods (issue #518):
|
|
150
|
+
* a writeNonce match wins outright; otherwise traceId (when expected) and
|
|
151
|
+
* recordedAt are compared. Extracted so every annotation surface applies
|
|
152
|
+
* identical guards (CLAUDE.md rule 22).
|
|
153
|
+
*/
|
|
154
|
+
function snapshotMatchesExpectedIdentity(
|
|
155
|
+
current: LastRecallSnapshot,
|
|
156
|
+
expected?: { writeNonce?: string; traceId?: string; recordedAt?: string },
|
|
157
|
+
): boolean {
|
|
158
|
+
if (!expected) return true;
|
|
159
|
+
if (typeof expected.writeNonce === "string" && expected.writeNonce.length > 0) {
|
|
160
|
+
return current.writeNonce === expected.writeNonce;
|
|
161
|
+
}
|
|
162
|
+
const hasExpectedTraceId =
|
|
163
|
+
typeof expected.traceId === "string" && expected.traceId.length > 0;
|
|
164
|
+
if (hasExpectedTraceId) {
|
|
165
|
+
return current.traceId === expected.traceId;
|
|
166
|
+
}
|
|
167
|
+
if (expected.recordedAt !== undefined) {
|
|
168
|
+
return current.recordedAt === expected.recordedAt;
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
139
173
|
function cloneTierExplain(
|
|
140
174
|
tierExplain: RecallTierExplain | undefined,
|
|
141
175
|
): RecallTierExplain | undefined {
|
|
@@ -271,6 +305,13 @@ export class LastRecallStore {
|
|
|
271
305
|
* can render which retrieval tier served the query.
|
|
272
306
|
*/
|
|
273
307
|
tierExplain?: RecallTierExplain;
|
|
308
|
+
/**
|
|
309
|
+
* Backend degradations observed while serving this recall (issue #1536).
|
|
310
|
+
* Passed at record time so the published snapshot is born annotated —
|
|
311
|
+
* a post-record annotation would leave a window where readers see the
|
|
312
|
+
* snapshot without them (codex review on #1544).
|
|
313
|
+
*/
|
|
314
|
+
backendDegradations?: SearchDegradation[];
|
|
274
315
|
}): Promise<void> {
|
|
275
316
|
const now = new Date().toISOString();
|
|
276
317
|
const queryHash = createHash("sha256").update(opts.query).digest("hex");
|
|
@@ -302,6 +343,10 @@ export class LastRecallStore {
|
|
|
302
343
|
identityInjectedChars: opts.identityInjection?.injectedChars,
|
|
303
344
|
identityInjectionTruncated: opts.identityInjection?.truncated,
|
|
304
345
|
tierExplain: opts.tierExplain,
|
|
346
|
+
backendDegradations:
|
|
347
|
+
opts.backendDegradations && opts.backendDegradations.length > 0
|
|
348
|
+
? opts.backendDegradations
|
|
349
|
+
: undefined,
|
|
305
350
|
};
|
|
306
351
|
// `cloneLastRecallSnapshot` handles `null` but that never applies
|
|
307
352
|
// at this call site — the non-null assertion keeps the type
|
|
@@ -355,27 +400,7 @@ export class LastRecallStore {
|
|
|
355
400
|
): Promise<void> {
|
|
356
401
|
const current = this.state[sessionKey];
|
|
357
402
|
if (!current) return;
|
|
358
|
-
if (expected)
|
|
359
|
-
if (
|
|
360
|
-
typeof expected.writeNonce === "string" &&
|
|
361
|
-
expected.writeNonce.length > 0
|
|
362
|
-
) {
|
|
363
|
-
if (current.writeNonce !== expected.writeNonce) return;
|
|
364
|
-
} else {
|
|
365
|
-
const hasExpectedTraceId =
|
|
366
|
-
typeof expected.traceId === "string" && expected.traceId.length > 0;
|
|
367
|
-
const traceIdMatches =
|
|
368
|
-
hasExpectedTraceId && current.traceId === expected.traceId;
|
|
369
|
-
const recordedAtMatches =
|
|
370
|
-
expected.recordedAt !== undefined &&
|
|
371
|
-
current.recordedAt === expected.recordedAt;
|
|
372
|
-
if (hasExpectedTraceId) {
|
|
373
|
-
if (!traceIdMatches) return;
|
|
374
|
-
} else if (expected.recordedAt !== undefined && !recordedAtMatches) {
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
403
|
+
if (!snapshotMatchesExpectedIdentity(current, expected)) return;
|
|
379
404
|
this.state[sessionKey] = {
|
|
380
405
|
...current,
|
|
381
406
|
tierExplain: cloneTierExplain(tierExplain),
|
|
@@ -387,6 +412,7 @@ export class LastRecallStore {
|
|
|
387
412
|
}
|
|
388
413
|
}
|
|
389
414
|
|
|
415
|
+
|
|
390
416
|
private flushState(): Promise<void> {
|
|
391
417
|
const run = this.stateWriteChain.catch(() => undefined).then(async () => {
|
|
392
418
|
await mkdir(path.dirname(this.statePath), { recursive: true });
|
package/src/search/port.ts
CHANGED
|
@@ -12,8 +12,33 @@ export interface SearchQueryOptions {
|
|
|
12
12
|
structuredSearches?: Array<{ type: "lex" | "vec" | "hyde"; query: string }>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A search that returned empty (or partial) results because the backend was
|
|
17
|
+
* unavailable, still loading, or timed out — cases that are otherwise
|
|
18
|
+
* indistinguishable from a genuine "no matches" (issue #1536, CLAUDE.md
|
|
19
|
+
* rule 34).
|
|
20
|
+
*/
|
|
21
|
+
export interface SearchDegradation {
|
|
22
|
+
backend: "qmd";
|
|
23
|
+
code:
|
|
24
|
+
| "backend_unavailable"
|
|
25
|
+
| "daemon_timeout"
|
|
26
|
+
| "daemon_loading"
|
|
27
|
+
| "subprocess_error"
|
|
28
|
+
| "deadline_exceeded";
|
|
29
|
+
detail?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
export interface SearchExecutionOptions {
|
|
16
33
|
signal?: AbortSignal;
|
|
34
|
+
/**
|
|
35
|
+
* Observer invoked when the backend degrades during this call (#1536).
|
|
36
|
+
* Callers that need to distinguish empty-because-degraded from
|
|
37
|
+
* empty-because-no-matches (recall x-ray, fallback decisions) pass a
|
|
38
|
+
* collector here. Observer failures are swallowed by the notifier —
|
|
39
|
+
* observability must never break search.
|
|
40
|
+
*/
|
|
41
|
+
onDegradation?: (degradation: SearchDegradation) => void;
|
|
17
42
|
}
|
|
18
43
|
|
|
19
44
|
export function resolveEnsureCollectionArgs(
|