From dfd65965513fbb801ffffaf983f58a0385401f4e Mon Sep 17 00:00:00 2001 From: rUv Date: Mon, 14 Sep 2026 18:41:07 +0000 Subject: [PATCH] fix(agentdb): surface degraded:true on the pattern-search/store fallback path (#3323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 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 Claude-Session: https://claude.ai/code/session_01RYP825phHMjPRmmjkcAWpq --- .../__tests__/bug-cluster-2219-2226.test.ts | 55 +++++++++++++++++++ .../cli/src/mcp-tools/agentdb-tools.ts | 41 +++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/v3/@claude-flow/cli/__tests__/bug-cluster-2219-2226.test.ts b/v3/@claude-flow/cli/__tests__/bug-cluster-2219-2226.test.ts index 4981879c7a..2513d8e0ee 100644 --- a/v3/@claude-flow/cli/__tests__/bug-cluster-2219-2226.test.ts +++ b/v3/@claude-flow/cli/__tests__/bug-cluster-2219-2226.test.ts @@ -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 diff --git a/v3/@claude-flow/cli/src/mcp-tools/agentdb-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/agentdb-tools.ts index 1ab11a42f6..151fd37139 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/agentdb-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/agentdb-tools.ts @@ -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