mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-11-09 22:13:39 +09:00
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:
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
|
||||
|
||||
Reference in new issue
Block a user