Skip to content

Commit c48e2d6

Browse files
committed
fix(brain): add text fallback + resilient parsing for Google Chat
- Add 'text' field to all Chat card responses (required for HTTP endpoint mode) - Parse Chat events from raw bytes for resilience against unknown fields - Log raw payload on parse failure for debugging - Return helpful fallback text on malformed events Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent 0ce919a commit c48e2d6

File tree

1 file changed

+34
-4
lines changed

1 file changed

+34
-4
lines changed

crates/mcp-brain-server/src/routes.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ pub async fn create_router() -> (Router, AppState) {
274274
crawl_adapter,
275275
cached_partition: Arc::new(parking_lot::RwLock::new(None)),
276276
notifier: crate::notify::ResendNotifier::from_env(),
277+
cached_status: Arc::new(parking_lot::RwLock::new(None)),
277278
};
278279

279280
let router = Router::new()
@@ -2147,6 +2148,16 @@ async fn partition(
21472148
async fn status(
21482149
State(state): State<AppState>,
21492150
) -> Json<StatusResponse> {
2151+
// Return cached response if fresh (< 5 seconds old)
2152+
{
2153+
let cache = state.cached_status.read();
2154+
if let Some((ts, ref resp)) = *cache {
2155+
if ts.elapsed() < std::time::Duration::from_secs(5) {
2156+
return Json(resp.clone());
2157+
}
2158+
}
2159+
}
2160+
21502161
let graph = state.graph.read();
21512162
// Use node_count as a cheap proxy for cluster count instead of running
21522163
// full MinCut partitioning on every status call (expensive O(V*E) op)
@@ -2215,7 +2226,7 @@ async fn status(
22152226
0.0
22162227
};
22172228

2218-
Json(StatusResponse {
2229+
let resp = StatusResponse {
22192230
total_memories: state.store.memory_count(),
22202231
total_contributors: state.store.contributor_count(),
22212232
graph_nodes: graph.node_count(),
@@ -2266,7 +2277,12 @@ async fn status(
22662277
midstream_strange_loop_version: strange_loop::VERSION.to_string(),
22672278
sparsifier_compression: graph.sparsifier_stats().map(|s| s.compression_ratio).unwrap_or(0.0),
22682279
sparsifier_edges: graph.sparsifier_stats().map(|s| s.sparsified_edges).unwrap_or(0),
2269-
})
2280+
};
2281+
2282+
// Cache the computed response for 5 seconds
2283+
*state.cached_status.write() = Some((std::time::Instant::now(), resp.clone()));
2284+
2285+
Json(resp)
22702286
}
22712287

22722288
/// GET /v1/sona/stats — SONA learning engine statistics (auth required)
@@ -5809,9 +5825,10 @@ struct GoogleChatUser {
58095825
email: Option<String>,
58105826
}
58115827

5812-
/// Google Chat card response
5828+
/// Google Chat card response — always includes `text` fallback for HTTP endpoint mode
58135829
fn chat_card(title: &str, subtitle: &str, sections: Vec<serde_json::Value>) -> serde_json::Value {
58145830
serde_json::json!({
5831+
"text": format!("{} — {}", title, subtitle),
58155832
"cardsV2": [{
58165833
"cardId": "brain-response",
58175834
"card": {
@@ -5846,10 +5863,23 @@ fn chat_kv_section(items: &[(&str, &str)]) -> serde_json::Value {
58465863
}
58475864

58485865
/// POST /v1/chat/google — Google Chat bot webhook
5866+
/// Accepts any JSON (serde_json::Value) to handle all Google Chat payload variants
58495867
async fn google_chat_handler(
58505868
State(state): State<AppState>,
5851-
Json(event): Json<GoogleChatEvent>,
5869+
body: axum::body::Bytes,
58525870
) -> Json<serde_json::Value> {
5871+
// Parse body manually for resilience — log raw payload on failure
5872+
let event: GoogleChatEvent = match serde_json::from_slice(&body) {
5873+
Ok(e) => e,
5874+
Err(err) => {
5875+
let raw = String::from_utf8_lossy(&body);
5876+
tracing::warn!("Failed to parse Chat event: {}. Raw: {}", err, &raw[..raw.len().min(500)]);
5877+
return Json(serde_json::json!({
5878+
"text": "Pi Brain received your message but couldn't parse it. Try: help"
5879+
}));
5880+
}
5881+
};
5882+
58535883
let event_type = event.event_type.as_deref().unwrap_or("MESSAGE");
58545884
let user_name = event.user.as_ref()
58555885
.and_then(|u| u.display_name.as_deref())

0 commit comments

Comments
 (0)