use crate::core::hook::{HookCategory, HookEvent};
use crate::core::memory::MemoryPressure;
#[derive(Debug, Clone, Default)]
pub struct AlertConfig {
pub categories: Vec<HookCategory>,
pub memory_alerts: bool,
}
impl AlertConfig {
pub fn recommended() -> Self {
Self {
categories: vec![HookCategory::Permission, HookCategory::Agent],
memory_alerts: true,
}
}
}
pub fn should_alert(config: &AlertConfig, event: HookEvent) -> bool {
config.categories.contains(&event.category())
}
pub fn should_memory_alert(config: &AlertConfig, pressure: MemoryPressure) -> bool {
config.memory_alerts && pressure >= MemoryPressure::Alert
}
pub fn format_memory_alert(session_id: &str, pressure: MemoryPressure, fraction: f32) -> String {
let pct = (fraction * 100.0).round() as u32;
format!(
"⚠️ trusty-mpm: session {session_id} memory pressure {pressure:?} ({pct}% of context window)"
)
}
pub fn format_event_alert(session_id: &str, event: HookEvent) -> String {
format!(
"🔔 trusty-mpm: {} in session {session_id}",
event.wire_name()
)
}
pub fn format_overseer_block_alert(session_id: &str) -> String {
format!("🛑 trusty-mpm: overseer blocked session {session_id}")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingAlert {
pub message: String,
}
pub type LastSeen = std::collections::HashMap<String, String>;
pub fn check_and_alert(
sessions: &[serde_json::Value],
events_by_session: &std::collections::HashMap<String, Vec<serde_json::Value>>,
last_seen: &mut LastSeen,
config: &AlertConfig,
) -> Vec<PendingAlert> {
let mut alerts = Vec::new();
for session in sessions {
let Some(id) = session["id"].as_str() else {
continue;
};
let Some(events) = events_by_session.get(id) else {
continue;
};
let prev = last_seen.get(id).cloned().unwrap_or_default();
let mut newest = prev.clone();
for record in events {
let at = record["at"].as_str().unwrap_or_default();
if at <= prev.as_str() {
continue;
}
if at > newest.as_str() {
newest = at.to_string();
}
let Some(event_name) = record["event"].as_str() else {
continue;
};
let Some(event) = HookEvent::from_wire(event_name) else {
continue;
};
if should_alert(config, event) {
alerts.push(PendingAlert {
message: format_event_alert(id, event),
});
}
}
if !newest.is_empty() {
last_seen.insert(id.to_string(), newest);
}
}
alerts
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_alerts_on_permission() {
let cfg = AlertConfig::recommended();
assert!(should_alert(&cfg, HookEvent::PermissionDenied));
assert!(cfg.memory_alerts);
}
#[test]
fn subscription_filter_respects_categories() {
let cfg = AlertConfig {
categories: vec![HookCategory::Permission],
memory_alerts: false,
};
assert!(should_alert(&cfg, HookEvent::PermissionGranted));
assert!(!should_alert(&cfg, HookEvent::PreToolUse));
}
#[test]
fn memory_alert_threshold() {
let cfg = AlertConfig {
categories: vec![],
memory_alerts: true,
};
assert!(!should_memory_alert(&cfg, MemoryPressure::Warn));
assert!(should_memory_alert(&cfg, MemoryPressure::Alert));
assert!(should_memory_alert(&cfg, MemoryPressure::Compact));
let off = AlertConfig {
categories: vec![],
memory_alerts: false,
};
assert!(!should_memory_alert(&off, MemoryPressure::Compact));
}
#[test]
fn memory_alert_message_names_session() {
let msg = format_memory_alert("sess-1", MemoryPressure::Alert, 0.86);
assert!(msg.contains("sess-1"));
assert!(msg.contains("86%"));
assert!(msg.contains("Alert"));
}
#[test]
fn event_alert_message_names_event() {
let msg = format_event_alert("sess-2", HookEvent::SubagentStopFailure);
assert!(msg.contains("sess-2"));
assert!(msg.contains("SubagentStopFailure"));
}
#[test]
fn overseer_block_alert_names_session() {
let msg = format_overseer_block_alert("sess-7");
assert!(msg.contains("sess-7"));
assert!(msg.contains("overseer"));
}
#[test]
fn alert_loop_does_not_panic_on_empty_sessions() {
let cfg = AlertConfig::recommended();
let mut last_seen = LastSeen::new();
let alerts = check_and_alert(&[], &std::collections::HashMap::new(), &mut last_seen, &cfg);
assert!(alerts.is_empty());
assert!(last_seen.is_empty());
}
#[test]
fn check_and_alert_emits_for_new_subscribed_event() {
let cfg = AlertConfig::recommended();
let sessions = vec![serde_json::json!({ "id": "sess-1" })];
let mut events = std::collections::HashMap::new();
events.insert(
"sess-1".to_string(),
vec![serde_json::json!({
"event": "PermissionDenied",
"at": "2026-05-17T10:00:00Z",
})],
);
let mut last_seen = LastSeen::new();
let alerts = check_and_alert(&sessions, &events, &mut last_seen, &cfg);
assert_eq!(alerts.len(), 1);
assert!(alerts[0].message.contains("PermissionDenied"));
assert_eq!(
last_seen.get("sess-1").map(String::as_str),
Some("2026-05-17T10:00:00Z")
);
let again = check_and_alert(&sessions, &events, &mut last_seen, &cfg);
assert!(again.is_empty());
}
#[test]
fn check_and_alert_skips_unsubscribed_category() {
let cfg = AlertConfig::recommended();
let sessions = vec![serde_json::json!({ "id": "sess-1" })];
let mut events = std::collections::HashMap::new();
events.insert(
"sess-1".to_string(),
vec![serde_json::json!({
"event": "PreToolUse",
"at": "2026-05-17T10:00:00Z",
})],
);
let mut last_seen = LastSeen::new();
let alerts = check_and_alert(&sessions, &events, &mut last_seen, &cfg);
assert!(alerts.is_empty());
}
}