From 72e353ae0240a256c160715d178880ab2adcff6b Mon Sep 17 00:00:00 2001 From: Sam Valladares Date: Sun, 19 Apr 2026 16:46:31 -0500 Subject: [PATCH] fix(predict): surface degraded state instead of silent empty responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four PredictiveMemory calls (predict_needed_memories, get_proactive_suggestions, get_top_interests, prediction_accuracy) return Result, and all four were being swallowed by `.unwrap_or_default()` / `.unwrap_or(0.0)` — every lock-poisoning or internal error produced a response indistinguishable from a genuine cold-start "I have no predictions yet." Callers (dashboard, Claude Code, Cursor) had no way to tell "the system is broken" from "there genuinely isn't anything to predict." Now each call uses `unwrap_or_else` to (a) `tracing::warn!` the error with its source channel for observability, (b) flip a local `degraded` flag. The JSON response gains a new `predict_degraded: bool` field. Empty + degraded=false = cold start (expected). Empty + degraded=true = something went wrong, check logs. 6 existing predict tests pass (return shape unchanged on success path). --- crates/vestige-mcp/src/tools/predict.rs | 59 ++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/crates/vestige-mcp/src/tools/predict.rs b/crates/vestige-mcp/src/tools/predict.rs index 020f54a..1499979 100644 --- a/crates/vestige-mcp/src/tools/predict.rs +++ b/crates/vestige-mcp/src/tools/predict.rs @@ -67,20 +67,62 @@ pub async fn execute( ), }; - // Get predictions from predictive memory + // Get predictions from predictive memory. + // + // Each of these four calls can fail (lock poisoning, internal PredictiveMemory + // errors). Before v2.0.7 the failures were silently swallowed by + // `unwrap_or_default()`, producing an empty response that was indistinguishable + // from a genuine cold-start "I don't have any predictions yet" state. That + // ambiguity is a user-visible bug: callers can't tell "model is degraded" + // from "nothing to predict." Now we track whether ANY call errored and expose + // it as `predict_degraded: true` in the response, with per-channel errors + // logged via `tracing::warn!` for observability. + let mut degraded = false; let predictions = cog .predictive_memory .predict_needed_memories(&session_ctx) - .unwrap_or_default(); + .unwrap_or_else(|e| { + tracing::warn!( + target: "vestige::predict", + error = %e, + "predict_needed_memories failed; returning empty predictions" + ); + degraded = true; + Vec::new() + }); let suggestions = cog .predictive_memory .get_proactive_suggestions(0.3) - .unwrap_or_default(); + .unwrap_or_else(|e| { + tracing::warn!( + target: "vestige::predict", + error = %e, + "get_proactive_suggestions failed; returning empty suggestions" + ); + degraded = true; + Vec::new() + }); let top_interests = cog .predictive_memory .get_top_interests(10) - .unwrap_or_default(); - let accuracy = cog.predictive_memory.prediction_accuracy().unwrap_or(0.0); + .unwrap_or_else(|e| { + tracing::warn!( + target: "vestige::predict", + error = %e, + "get_top_interests failed; returning empty interests" + ); + degraded = true; + Vec::new() + }); + let accuracy = cog.predictive_memory.prediction_accuracy().unwrap_or_else(|e| { + tracing::warn!( + target: "vestige::predict", + error = %e, + "prediction_accuracy failed; returning 0.0" + ); + degraded = true; + 0.0 + }); // Build speculative context let speculative_context = vestige_core::PredictionContext { @@ -123,6 +165,13 @@ pub async fn execute( })).collect::>(), "top_interests": top_interests, "prediction_accuracy": accuracy, + // predict_degraded == true means at least one of the four underlying + // PredictiveMemory calls errored (lock poisoning, internal failure) + // and the corresponding field above was substituted with an empty + // default. Callers should treat an empty response as "genuinely nothing + // to predict" only when predict_degraded is false. See tracing logs + // under target "vestige::predict" for per-channel error details. + "predict_degraded": degraded, })) }