@@ -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(
21472148async 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
58135829fn 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
58495867async 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