From 5d93bf44a0a16e33e9319db838152efa72025e2a Mon Sep 17 00:00:00 2001 From: Reuven Date: Mon, 13 Apr 2026 15:52:04 -0400 Subject: [PATCH] =?UTF-8?q?fix(brain):=20improve=20daily=20digest=20email?= =?UTF-8?q?=20=E2=80=94=20filter=20noise,=20better=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daily digest was showing 10 identical "Self-reflection: training cycle" debug entries. Now: 1. Filters out debug category memories entirely 2. Filters known noise patterns (training cycles, IEEE events, DailyMed) 3. Skips content < 50 chars (scraping artifacts) 4. Category emojis for visual scanning 5. Cleaner layout with sentence-boundary truncation 6. Better subject line: "[pi brain] 5 new discoveries today" 7. Updated header: "What the Brain Learned Today" 8. Filters auto-generated tags from display Co-Authored-By: claude-flow --- crates/mcp-brain-server/src/routes.rs | 111 +++++++++++++++++++------- 1 file changed, 83 insertions(+), 28 deletions(-) diff --git a/crates/mcp-brain-server/src/routes.rs b/crates/mcp-brain-server/src/routes.rs index 7b1a06ab..95313f11 100644 --- a/crates/mcp-brain-server/src/routes.rs +++ b/crates/mcp-brain-server/src/routes.rs @@ -5980,20 +5980,45 @@ async fn notify_digest( let topic = body["topic"].as_str(); let hours = body["hours"].as_u64().unwrap_or(24); - // Gather recent discoveries from the store + // Gather recent discoveries from the store — excluding debug/training noise let cutoff = chrono::Utc::now() - chrono::Duration::hours(hours as i64); let mut all = state.store.all_memories(); all.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - // Filter by recency and optionally by topic + // Filter out noise: training cycles, self-reflections, debug entries, + // and low-signal web scraping results + let noise_patterns: &[&str] = &[ + "Self-reflection: training cycle", + "Fact Check: Self-reflection", + "vTools Events", + "Executive Committee Meeting", + "DailyMed", + "AccessGUDID", + "Site en construction", + ]; + let filtered: Vec<_> = all.iter() .filter(|m| { if m.created_at < cutoff { return false; } + // Skip debug/auto-generated training noise + if matches!(m.category, crate::types::BrainCategory::Debug) { + return false; + } + // Skip known noise patterns in titles + let title_lower = m.title.to_lowercase(); + if noise_patterns.iter().any(|p| title_lower.contains(&p.to_lowercase())) { + return false; + } + // Skip very short content (likely scraping artifacts) + if m.content.len() < 50 { + return false; + } + // Apply optional topic filter topic.map_or(true, |t| { let t_lower = t.to_lowercase(); - m.title.to_lowercase().contains(&t_lower) + title_lower.contains(&t_lower) || m.content.to_lowercase().contains(&t_lower) || m.tags.iter().any(|tag| tag.to_lowercase().contains(&t_lower)) }) @@ -6009,27 +6034,52 @@ async fn notify_digest( }))); } - // Build HTML rows + // Build HTML rows — human-readable format let mut rows = String::new(); + let category_emoji = |cat: &crate::types::BrainCategory| -> &str { + use crate::types::BrainCategory::*; + match cat { + Architecture => "🏗️", + Pattern => "🔄", + Solution => "💡", + Security => "🔒", + Convention => "📐", + Performance => "⚡", + Tooling => "🔧", + Debug => "🐛", + _ => "📝", + } + }; + for (i, m) in filtered.iter().enumerate() { - let title = if m.title.len() > 100 { &m.title[..100] } else { &m.title }; - let content = if m.content.len() > 200 { &m.content[..200] } else { &m.content }; - let quality = m.quality_score.mean(); - let tags_html: Vec<_> = m.tags.iter().take(4).map(|t| { - format!("{}", t) - }).collect(); + let title = if m.title.len() > 120 { &m.title[..120] } else { &m.title }; + // Take first ~250 chars but break at sentence boundary + let content_raw = if m.content.len() > 250 { &m.content[..250] } else { &m.content }; + let content = match content_raw.rfind(". ") { + Some(pos) if pos > 80 => &content_raw[..pos + 1], + _ => content_raw, + }; + let emoji = category_emoji(&m.category); + let tags_html: Vec<_> = m.tags.iter() + .filter(|t| !t.contains("auto-generated") && !t.contains("training-cycle")) + .take(3) + .map(|t| { + format!("{}", t) + }).collect(); rows.push_str(&format!( - r#" - -{num}. {title}
-{cat} | quality: {quality:.2} {tags}
-{content}... + r#" + +
{emoji} {title}
+
{tags}
+
{content}
"#, - num = i + 1, + emoji = emoji, title = title, - cat = m.category, - quality = quality, - tags = tags_html.join(""), + tags = if tags_html.is_empty() { + format!("{:?}", m.category) + } else { + tags_html.join("") + }, content = content, )); } @@ -6042,16 +6092,21 @@ async fn notify_digest( let edges = state.graph.read().edge_count(); let html = format!( - r#"
-

Daily Discovery Digest

-

Last {hours}h | {count} discoveries | {total} total memories | {edges} edges

+ r#"
+

What the Brain Learned Today

+

+{count} new discoveries in the last {hours} hours. +The brain now holds {total} memories connected by {edges} relationships. +

{topic_line} -{rows}
-
-

Reply with search <query> to explore | help for commands

+{rows}
+
+

Explore the brain

+

Reply search seizure prediction to find related knowledge, +or help for all commands.

-pi.ruv.io | Powered by Resend +pi.ruv.io — the shared brain for collective intelligence
"#, hours = hours, count = filtered.len(), @@ -6062,8 +6117,8 @@ async fn notify_digest( ); let subject = match topic { - Some(t) => format!("[pi.ruv.io/discovery] Daily Digest: {}", t), - None => "[pi.ruv.io/discovery] Daily Discovery Digest".into(), + Some(t) => format!("[pi brain] {} — {} new discoveries", t, filtered.len()), + None => format!("[pi brain] {} new discoveries today", filtered.len()), }; match notifier.send("discovery", &subject, &html).await {