From 7aba17e5f01a792db020d7a9a204377525f9f5e1 Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Thu, 9 Apr 2026 00:45:26 -0400 Subject: [PATCH] Compute graph health in consciousness, rename F4 to hippocampus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Graph health stats (alpha, gini, cc, episodic ratio, consolidation plan) now computed directly by the unconscious module on startup and every 10 minutes, instead of fetching from the poc-memory daemon. F4 screen renamed to hippocampus, stripped down to just the health gauges — daemon task list removed (agents now shown on F3). Co-Authored-By: Proof of Concept --- src/mind/mod.rs | 2 +- src/mind/unconscious.rs | 27 ++++++++++- src/subconscious/daemon.rs | 2 +- src/user/mod.rs | 8 +++- src/user/unconscious.rs | 98 +++++--------------------------------- 5 files changed, 46 insertions(+), 91 deletions(-) diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 138623d..765190f 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -259,7 +259,7 @@ pub struct Mind { pub shared: Arc, pub config: SessionConfig, subconscious: Arc>, - unconscious: Arc>, + pub unconscious: Arc>, turn_tx: mpsc::Sender<(Result, StreamTarget)>, turn_watch: tokio::sync::watch::Sender, bg_tx: mpsc::UnboundedSender, diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index 68f3501..adfe72b 100644 --- a/src/mind/unconscious.rs +++ b/src/mind/unconscious.rs @@ -67,6 +67,8 @@ pub struct UnconsciousSnapshot { pub struct Unconscious { agents: Vec, max_concurrent: usize, + pub graph_health: Option, + last_health_check: Option, } impl Unconscious { @@ -75,7 +77,13 @@ impl Unconscious { .filter(|name| defs::get_def(name).is_some()) .map(|name| UnconsciousAgent::new(name)) .collect(); - Self { agents, max_concurrent: 2 } + let mut s = Self { + agents, max_concurrent: 2, + graph_health: None, + last_health_check: None, + }; + s.refresh_health(); + s } /// Toggle an agent on/off by name. Returns new enabled state. @@ -95,8 +103,25 @@ impl Unconscious { }).collect() } + fn refresh_health(&mut self) { + let store = match crate::store::Store::load() { + Ok(s) => s, + Err(_) => return, + }; + self.graph_health = Some(crate::subconscious::daemon::compute_graph_health(&store)); + self.last_health_check = Some(Instant::now()); + } + /// Reap finished agents and spawn new ones. pub fn trigger(&mut self) { + // Periodic graph health refresh + if self.last_health_check + .map(|t| t.elapsed() > Duration::from_secs(600)) + .unwrap_or(false) + { + self.refresh_health(); + } + for agent in &mut self.agents { if agent.handle.as_ref().is_some_and(|h| h.is_finished()) { agent.last_run = Some(Instant::now()); diff --git a/src/subconscious/daemon.rs b/src/subconscious/daemon.rs index 9ce1138..50b47b5 100644 --- a/src/subconscious/daemon.rs +++ b/src/subconscious/daemon.rs @@ -367,7 +367,7 @@ fn job_daily_check( }) } -fn compute_graph_health(store: &crate::store::Store) -> GraphHealth { +pub fn compute_graph_health(store: &crate::store::Store) -> GraphHealth { let graph = store.build_graph(); let snap = crate::graph::current_metrics(&graph); diff --git a/src/user/mod.rs b/src/user/mod.rs index dcb9a18..1261bd2 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -108,6 +108,7 @@ struct App { context_info: Option, agent_state: Vec, unconscious_state: Vec, + graph_health: Option, walked_count: usize, channel_status: Vec, idle_info: Option, @@ -132,6 +133,7 @@ impl App { context_info: None, agent_state: Vec::new(), unconscious_state: Vec::new(), + graph_health: None, walked_count: 0, channel_status: Vec::new(), idle_info: None, } @@ -372,7 +374,11 @@ async fn run( idle_state.decay_ewma(); app.update_idle(&idle_state); app.agent_state = mind.subconscious_snapshots().await; - app.unconscious_state = mind.unconscious_snapshots().await; + { + let unc = mind.unconscious.lock().await; + app.unconscious_state = unc.snapshots(); + app.graph_health = unc.graph_health.clone(); + } app.walked_count = mind.subconscious_walked().await.len(); if !startup_done { if let Ok(mut ag) = agent.state.try_lock() { diff --git a/src/user/unconscious.rs b/src/user/unconscious.rs index 99876f8..9f5c98b 100644 --- a/src/user/unconscious.rs +++ b/src/user/unconscious.rs @@ -1,10 +1,10 @@ -// unconscious_screen.rs — F4: memory daemon status +// unconscious_screen.rs — F4: graph health use ratatui::{ layout::{Constraint, Layout, Rect}, - style::{Color, Modifier, Style}, + style::{Color, Style}, text::{Line, Span}, - widgets::{Block, Borders, Gauge, Paragraph, Wrap}, + widgets::{Block, Borders, Gauge, Paragraph}, Frame, crossterm::event::KeyCode, }; @@ -12,20 +12,6 @@ use ratatui::{ use super::{App, ScreenView, screen_legend}; use crate::subconscious::daemon::GraphHealth; -#[derive(serde::Deserialize, Default)] -struct DaemonStatus { - #[allow(dead_code)] - pid: u32, - tasks: Vec, - #[serde(default)] - graph_health: Option, -} - -fn fetch_status() -> Option { - let json = jobkit::daemon::socket::send_rpc(&crate::config::get().data_dir, "")?; - serde_json::from_str(&json).ok() -} - pub(crate) struct UnconsciousScreen { scroll: u16, } @@ -35,10 +21,10 @@ impl UnconsciousScreen { } impl ScreenView for UnconsciousScreen { - fn label(&self) -> &'static str { "unconscious" } + fn label(&self) -> &'static str { "hippocampus" } fn tick(&mut self, frame: &mut Frame, area: Rect, - events: &[ratatui::crossterm::event::Event], _app: &mut App) { + events: &[ratatui::crossterm::event::Event], app: &mut App) { for event in events { if let ratatui::crossterm::event::Event::Key(key) = event { if key.kind != ratatui::crossterm::event::KeyEventKind::Press { continue; } @@ -52,33 +38,22 @@ impl ScreenView for UnconsciousScreen { let block = Block::default() .title_top(Line::from(screen_legend()).left_aligned()) - .title_top(Line::from(" unconscious ").right_aligned()) + .title_top(Line::from(" hippocampus ").right_aligned()) .borders(Borders::ALL) .border_style(Style::default().fg(Color::Cyan)); let inner = block.inner(area); frame.render_widget(block, area); - let status = fetch_status(); - - match &status { + match &app.graph_health { None => { frame.render_widget( - Paragraph::new(Line::styled(" daemon not running", + Paragraph::new(Line::styled(" computing graph health...", Style::default().fg(Color::DarkGray))), inner, ); } - Some(st) => { - let has_health = st.graph_health.is_some(); - let [health_area, tasks_area] = Layout::vertical([ - Constraint::Length(if has_health { 9 } else { 0 }), - Constraint::Min(1), - ]).areas(inner); - - if let Some(ref gh) = st.graph_health { - render_health(frame, gh, health_area); - } - render_tasks(frame, &st.tasks, tasks_area); + Some(gh) => { + render_health(frame, gh, inner); } } } @@ -132,58 +107,7 @@ fn render_health(frame: &mut Frame, gh: &GraphHealth, area: Rect) { let plan_summary: Vec = plan_items.iter().map(|(a, c)| format!("{}{}", &a[..1], c)).collect(); frame.render_widget(Paragraph::new(Line::from(vec![ Span::raw(" plan: "), - Span::styled(format!("{}", plan_total), Style::default().add_modifier(Modifier::BOLD)), + Span::styled(format!("{}", plan_total), Style::default().fg(Color::White)), Span::raw(format!(" agents ({} +health)", plan_summary.join(" "))), ])), plan_area); } - -fn render_tasks(frame: &mut Frame, tasks: &[jobkit::TaskInfo], area: Rect) { - let mut lines: Vec = Vec::new(); - let section = Style::default().fg(Color::Yellow); - let dim = Style::default().fg(Color::DarkGray); - - let running: Vec<_> = tasks.iter().filter(|t| matches!(t.status, jobkit::TaskStatus::Running)).collect(); - let completed: Vec<_> = tasks.iter().filter(|t| matches!(t.status, jobkit::TaskStatus::Completed)).collect(); - let failed: Vec<_> = tasks.iter().filter(|t| matches!(t.status, jobkit::TaskStatus::Failed)).collect(); - - lines.push(Line::styled("── Tasks ──", section)); - lines.push(Line::raw(format!(" Running: {} Completed: {} Failed: {}", running.len(), completed.len(), failed.len()))); - lines.push(Line::raw("")); - - if !running.is_empty() { - for task in &running { - let elapsed = task.started_at.map(|s| { - let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs_f64(); - format!("{}s", (now - s) as u64) - }).unwrap_or_default(); - lines.push(Line::from(vec![ - Span::raw(" "), Span::styled("●", Style::default().fg(Color::Green)), - Span::raw(format!(" {} ({})", task.name, elapsed)), - ])); - } - lines.push(Line::raw("")); - } - - if !completed.is_empty() { - lines.push(Line::styled(" Recent:", dim)); - for task in completed.iter().rev().take(10) { - lines.push(Line::from(vec![ - Span::raw(" "), Span::styled("✓", Style::default().fg(Color::Green)), - Span::raw(format!(" {}", task.name)), - ])); - } - } - - if !failed.is_empty() { - lines.push(Line::raw("")); - lines.push(Line::styled(" Failed:", Style::default().fg(Color::Red))); - for task in failed.iter().rev().take(5) { - lines.push(Line::from(vec![ - Span::raw(" "), Span::styled("✗", Style::default().fg(Color::Red)), - Span::raw(format!(" {}", task.name)), - ])); - } - } - - frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), area); -}