fix(agentdb): surface degraded:true on the pattern-search/store fallback path (#3323)

* fix(agentdb): surface degraded:true on the pattern-search/store fallback path

Fixes #3288. agentdb_pattern-search and agentdb_pattern-store both return
{success: true} (or a results array with no error field) when they've
silently fallen back to the memory-store-fallback / substring-match path
because the ReasoningBank controller is unavailable — the only signal was
a free-text `note` string, which a caller has no structural reason to
check on an apparently-successful response.

Add degraded:true to both fallback return paths, matching the existing
{degraded: true, reason} convention already used by agentbbs-tools.ts.
The healthy ReasoningBank path is unaffected (no degraded field).

Regression tests added to bug-cluster-2219-2226.test.ts, alongside the
existing #2226 test for the same two handlers: assert degraded:true when
controller === 'memory-store-fallback', and degraded === undefined
otherwise. Both skip gracefully when the backend can't initialize in
isolation, matching the file's existing convention.

Does not address the underlying reason vectorBackend/reasoningBank are
disabled in a given deployment (tracked separately in #1228/#2296) — this
fix is scoped to making that state observable from the tool response
shape, per the report's suggested fix direction.

Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01RYP825phHMjPRmmjkcAWpq

* fix(agentdb): flag degraded at the controller boundary, not per fallback block

Review caught a real gap in the first commit: bridgeStorePattern and
bridgeSearchPatterns (memory-bridge.ts) return non-null with
controller: 'bridge-fallback' when the registry exists but reasoningBank
itself is unusable — this is the degraded state #3288 actually reported.
That result flows through `if (result) return result;` / the
results.length>0 early-return in agentdb-tools.ts, both of which sit
BEFORE the memory-store-fallback blocks the previous commit patched, so
agentdb_pattern-store never got flagged at all, and agentdb_pattern-search
only got flagged when bridge-fallback happened to return zero results.

Fix: check `result.controller === 'reasoningBank'` at both return
boundaries instead of patching specific fallback-label blocks. Any other
label — bridge-fallback, memory-store-fallback, or one added later — is
degraded by definition, so this can't silently reopen the gap the way
per-block matching just did. Also adds a `reason` field (stable,
machine-checkable) alongside the existing `note` (human-readable),
matching agentbbs-tools.ts's {degraded, reason} shape exactly rather than
degraded-only.

Tests inverted to match: assert degraded:true for every controller value
EXCEPT 'reasoningBank', instead of asserting undefined for everything
except the one label the previous commit patched — the old assertion
would have made bridge-fallback's un-flagged state an enforced invariant,
failing CI on a later correct fix.

Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01RYP825phHMjPRmmjkcAWpq
This commit is contained in:
rUv authored and GitHub committed 2026-09-14 18:41:07 +00:00
1 parent 8008bc3562
commit dfd6596551
2 files changed
+94 -2

No files matched your search

@@ -168,6 +168,61 @@ describe('#2226 — pattern store and search share a backend', () => {
}, 60_000);
});
describe('#3288 — pattern store/search surface degraded:true on every non-reasoningBank path', () => {
// `controller: 'reasoningBank'` is the ONLY label that means the healthy
// path ran. Every other label (`bridge-fallback`, `memory-store-fallback`,
// and any future addition) is a degraded response — assert the invariant
// by controller identity, not by enumerating specific fallback labels, so
// a new label can't silently reopen this gap the way `bridge-fallback` did
// (bridgeStorePattern/bridgeSearchPatterns return it non-null, which used
// to skip the fallback blocks entirely and return undegraded).
const HEALTHY_CONTROLLER = 'reasoningBank';
it('agentdb_pattern-store sets degraded:true on every path except controller=reasoningBank', async () => {
const marker = `degraded-flag-store-${process.pid}-${process.hrtime.bigint()}`;
const stored = await agentdbPatternStore.handler({
pattern: `Use ${marker} for secure session renewal`,
type: 'auth-pattern',
confidence: 0.9,
});
if (!stored || stored.success !== true) return; // both bridge and fallback unavailable — skip
if (stored.controller === HEALTHY_CONTROLLER) {
expect(stored.degraded).toBeUndefined();
} else {
// The whole point of #3288: a caller must be able to detect degradation
// from response SHAPE, not by parsing a free-text `note` string —
// covers bridge-fallback (registry present, reasoningBank unusable)
// and memory-store-fallback (registry entirely absent) alike.
expect(stored.degraded).toBe(true);
expect(typeof stored.reason).toBe('string');
}
}, 60_000);
it('agentdb_pattern-search sets degraded:true on every path except controller=reasoningBank', async () => {
const marker = `degraded-flag-search-${process.pid}-${process.hrtime.bigint()}`;
await agentdbPatternStore.handler({
pattern: `Use ${marker} for secure session renewal`,
type: 'auth-pattern',
confidence: 0.9,
});
const found = await agentdbPatternSearch.handler({
query: marker,
topK: 5,
minConfidence: 0.1,
});
if (!found || !Array.isArray(found.results)) return; // backend unavailable in isolation — skip
if (found.controller === HEALTHY_CONTROLLER) {
expect(found.degraded).toBeUndefined();
} else {
expect(found.degraded).toBe(true);
expect(typeof found.reason).toBe('string');
}
}, 60_000);
});
/**
* 3.10.8 routing-learning fixes (follow-ups to the intelligence audit):
* Bug B — Q-router cached a stale route decision and only invalidated the
@@ -166,7 +166,21 @@ export const agentdbPatternStore: MCPTool = {
const bridge = await getBridge();
const result = await bridge.bridgeStorePattern({ pattern, type, confidence });
if (result) return result;
if (result) {
// #3288: `controller: 'reasoningBank'` is the ONLY label that means
// the healthy path ran. Every other label bridgeStorePattern can
// return (`bridge-fallback` today; more may be added later) is a
// degraded write that still reports {success: true} — flag it at
// this one boundary instead of chasing each fallback label
// individually, so a new label can't silently reopen this gap.
if (result.controller === 'reasoningBank') return result;
return {
...result,
degraded: true,
reason: `reasoningBank-unavailable:${result.controller}`,
note: `ReasoningBank controller unavailable (controller=${result.controller}). Pattern persisted via the fallback path. Run \`agentdb_health\` to inspect controller registration.`,
};
}
// ADR-093 F4: when the ReasoningBank controller registry returns
// null (the cause of audit-reported "AgentDB bridge not available"
@@ -186,6 +200,12 @@ export const agentdbPatternStore: MCPTool = {
});
return {
success: true,
// #3288: a caller must not have to already know to distrust a
// "successful" response — degraded:true is the structural signal
// (matching agentbbs-tools.ts's degradedResult() convention),
// `note` stays for the human-readable detail.
degraded: true,
reason: 'reasoningBank-unavailable:registry-null',
patternId,
controller: 'memory-store-fallback',
note: 'ReasoningBank controller registry unavailable. Pattern persisted via memory_store. Run `agentdb_health` to inspect controller registration.',
@@ -230,7 +250,19 @@ export const agentdbPatternSearch: MCPTool = {
const bridge = await getBridge();
const result = await bridge.bridgeSearchPatterns({ query, topK, minConfidence });
if (result && Array.isArray(result.results) && result.results.length > 0) {
return result;
// #3288: `controller: 'reasoningBank'` is the only label meaning the
// healthy path ran and actually found results. Any other label
// (`bridge-fallback` today; more may be added later) is a degraded
// response even though it has results — flag it here, since this
// early return bypasses the tier1/tier2 fallback block below
// entirely (and its degraded:true) whenever there ARE results.
if (result.controller === 'reasoningBank') return result;
return {
...result,
degraded: true,
reason: `reasoningBank-unavailable:${result.controller}`,
note: `ReasoningBank controller unavailable (controller=${result.controller}); results returned via the fallback path. Run \`agentdb_health\` to inspect controller registration.`,
};
}
// #1889 — symmetric fallback. pattern-store writes to the `pattern`
@@ -310,8 +342,13 @@ export const agentdbPatternSearch: MCPTool = {
// round-trip sees both ends agree. The store reports
// `memory-store-fallback`; we use the same name + a `tier` field
// to expose which sub-strategy fired.
// #3288: degraded:true is the structural signal a caller can check
// without already knowing to distrust an apparently-successful
// response — matches agentbbs-tools.ts's degradedResult() convention.
return {
results,
degraded: true,
reason: result ? `reasoningBank-empty:${result.controller ?? 'unknown'}` : 'reasoningBank-unavailable:registry-null',
controller: 'memory-store-fallback',
tier,
note: result