Compute graph health in consciousness, rename F4 to hippocampus
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 <poc@bcachefs.org>
This commit is contained in:
parent
1df49482fd
commit
7aba17e5f0
5 changed files with 46 additions and 91 deletions
|
|
@ -259,7 +259,7 @@ pub struct Mind {
|
|||
pub shared: Arc<SharedMindState>,
|
||||
pub config: SessionConfig,
|
||||
subconscious: Arc<tokio::sync::Mutex<Subconscious>>,
|
||||
unconscious: Arc<tokio::sync::Mutex<Unconscious>>,
|
||||
pub unconscious: Arc<tokio::sync::Mutex<Unconscious>>,
|
||||
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
|
||||
turn_watch: tokio::sync::watch::Sender<bool>,
|
||||
bg_tx: mpsc::UnboundedSender<BgEvent>,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ pub struct UnconsciousSnapshot {
|
|||
pub struct Unconscious {
|
||||
agents: Vec<UnconsciousAgent>,
|
||||
max_concurrent: usize,
|
||||
pub graph_health: Option<crate::subconscious::daemon::GraphHealth>,
|
||||
last_health_check: Option<Instant>,
|
||||
}
|
||||
|
||||
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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ struct App {
|
|||
context_info: Option<ContextInfo>,
|
||||
agent_state: Vec<crate::mind::SubconsciousSnapshot>,
|
||||
unconscious_state: Vec<crate::mind::UnconsciousSnapshot>,
|
||||
graph_health: Option<crate::subconscious::daemon::GraphHealth>,
|
||||
walked_count: usize,
|
||||
channel_status: Vec<ChannelStatus>,
|
||||
idle_info: Option<IdleInfo>,
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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<jobkit::TaskInfo>,
|
||||
#[serde(default)]
|
||||
graph_health: Option<GraphHealth>,
|
||||
}
|
||||
|
||||
fn fetch_status() -> Option<DaemonStatus> {
|
||||
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<String> = 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<Line> = 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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue