diff --git a/src/agent/tui.rs b/src/agent/tui.rs index bb3a1a8..1256864 100644 --- a/src/agent/tui.rs +++ b/src/agent/tui.rs @@ -1089,9 +1089,9 @@ impl App { frame.render_widget(para, size); } - fn draw_agent_log(&self, frame: &mut Frame, size: Rect, output_dir: &std::path::Path) { + fn draw_agent_log(&self, frame: &mut Frame, size: Rect, _output_dir: &std::path::Path) { let name = AGENT_NAMES.get(self.agent_selected).unwrap_or(&"?"); - let agent_dir = output_dir.join(name); + let agent = self.agent_state.iter().find(|a| a.name == *name); let mut lines: Vec = Vec::new(); let section = Style::default().fg(Color::Yellow); let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC); @@ -1101,73 +1101,42 @@ impl App { lines.push(Line::styled(" (Esc/← back, PgUp/PgDn scroll)", hint)); lines.push(Line::raw("")); - // Show pid status - let live = crate::subconscious::knowledge::scan_pid_files(&agent_dir, 0); - if live.is_empty() { - lines.push(Line::styled(" Status: idle", Style::default().fg(Color::DarkGray))); - } else { - for (phase, pid) in &live { + // Show pid status from state + match agent.and_then(|a| a.pid) { + Some(pid) => { + let phase = agent.and_then(|a| a.phase.as_deref()).unwrap_or("?"); lines.push(Line::from(vec![ Span::styled(" Status: ", Style::default()), Span::styled(format!("● running pid {} phase: {}", pid, phase), Style::default().fg(Color::Green)), ])); } - } - lines.push(Line::raw("")); - - // Show output files - lines.push(Line::styled("── Output Files ──", section)); - let mut files: Vec<_> = std::fs::read_dir(&agent_dir) - .into_iter().flatten().flatten() - .filter(|e| { - let n = e.file_name().to_string_lossy().to_string(); - !n.starts_with("pid-") && !n.starts_with("transcript-offset") - && !n.starts_with("chunks-") && !n.starts_with("seen") - }) - .collect(); - files.sort_by_key(|e| std::cmp::Reverse( - e.metadata().ok().and_then(|m| m.modified().ok()) - .unwrap_or(std::time::SystemTime::UNIX_EPOCH) - )); - - for entry in files.iter().take(10) { - let name = entry.file_name().to_string_lossy().to_string(); - let ago = entry.metadata().ok() - .and_then(|m| m.modified().ok()) - .and_then(|t| t.elapsed().ok()) - .map(|d| Self::format_duration(d)) - .unwrap_or_else(|| "?".into()); - let size = entry.metadata().ok().map(|m| m.len()).unwrap_or(0); - lines.push(Line::raw(format!(" {:<30} {:>6}B {}", name, size, ago))); - } - - // Show hook log tail - lines.push(Line::raw("")); - lines.push(Line::styled("── Hook Log ──", section)); - - let log_dir = dirs::home_dir().unwrap_or_default().join(".consciousness/logs"); - // Find latest hook log - if let Ok(mut entries) = std::fs::read_dir(&log_dir) { - let mut logs: Vec<_> = entries.by_ref().flatten() - .filter(|e| e.file_name().to_string_lossy().starts_with("hook-")) - .collect(); - logs.sort_by_key(|e| std::cmp::Reverse( - e.metadata().ok().and_then(|m| m.modified().ok()) - .unwrap_or(std::time::SystemTime::UNIX_EPOCH) - )); - if let Some(log_entry) = logs.first() { - if let Ok(content) = std::fs::read_to_string(log_entry.path()) { - // Show last ~30 lines - let log_lines: Vec<&str> = content.lines().collect(); - let start = log_lines.len().saturating_sub(30); - for line in &log_lines[start..] { - lines.push(Line::raw(format!(" {}", line))); - } - } + None => { + lines.push(Line::styled(" Status: idle", Style::default().fg(Color::DarkGray))); } } + // Show log path + if let Some(log_path) = agent.and_then(|a| a.log_path.as_ref()) { + lines.push(Line::raw(format!(" Log: {}", log_path.display()))); + } + lines.push(Line::raw("")); + + // Show agent log tail + lines.push(Line::styled("── Agent Log ──", section)); + if let Some(content) = agent + .and_then(|a| a.log_path.as_ref()) + .and_then(|p| std::fs::read_to_string(p).ok()) + { + let log_lines: Vec<&str> = content.lines().collect(); + let start = log_lines.len().saturating_sub(40); + for line in &log_lines[start..] { + lines.push(Line::raw(format!(" {}", line))); + } + } else { + lines.push(Line::styled(" (no log available)", hint)); + } + let block = Block::default() .title_top(Line::from(SCREEN_LEGEND).left_aligned()) .title_top(Line::from(format!(" {} ", name)).right_aligned()) diff --git a/src/subconscious/hook.rs b/src/subconscious/hook.rs index e15a9a0..6d4b4bd 100644 --- a/src/subconscious/hook.rs +++ b/src/subconscious/hook.rs @@ -140,6 +140,8 @@ pub struct AgentInfo { pub name: &'static str, pub pid: Option, pub phase: Option, + /// Path to the most recent agent log file. + pub log_path: Option, } /// Persistent state for the agent orchestration cycle. @@ -164,7 +166,7 @@ impl AgentCycleState { .create(true).append(true).open(log_path).ok(); let agents = AGENT_CYCLE_NAMES.iter() - .map(|&name| AgentInfo { name, pid: None, phase: None }) + .map(|&name| AgentInfo { name, pid: None, phase: None, log_path: None }) .collect(); AgentCycleState { @@ -185,10 +187,14 @@ impl AgentCycleState { } } - fn update_agent(&mut self, name: &str, pid: Option, phase: Option) { + fn update_agent(&mut self, name: &str, pid: Option, phase: Option, + log_path: Option) { if let Some(agent) = self.agents.iter_mut().find(|a| a.name == name) { agent.pid = pid; agent.phase = phase; + if log_path.is_some() { + agent.log_path = log_path; + } } } @@ -266,10 +272,10 @@ impl AgentCycleState { let live = crate::agents::knowledge::scan_pid_files(&state_dir, timeout); if let Some((phase, pid)) = live.first() { - self.update_agent("surface-observe", Some(*pid), Some(phase.clone())); + self.update_agent("surface-observe", Some(*pid), Some(phase.clone()), None); self.log(format_args!("alive pid-{}: phase={}\n", pid, phase)); } else { - self.update_agent("surface-observe", None, None); + self.update_agent("surface-observe", None, None, None); } // Read surfaced keys @@ -304,11 +310,12 @@ impl AgentCycleState { if transcript.size > 0 { fs::write(&offset_path, transcript.size.to_string()).ok(); } - let pid = crate::agents::knowledge::spawn_agent( + let spawned = crate::agents::knowledge::spawn_agent( "surface-observe", &state_dir, &session.session_id); self.update_agent("surface-observe", - pid, Some("surface".into())); - self.log(format_args!("spawned agent {:?}\n", pid)); + spawned.as_ref().map(|s| s.pid), Some("surface".into()), + spawned.as_ref().map(|s| s.log_path.clone())); + self.log(format_args!("spawned agent {:?}\n", spawned.as_ref().map(|s| s.pid))); } // Wait if agent is significantly behind @@ -353,7 +360,7 @@ impl AgentCycleState { let live = crate::agents::knowledge::scan_pid_files(&state_dir, 300); if let Some((phase, pid)) = live.first() { - self.update_agent("reflect", Some(*pid), Some(phase.clone())); + self.update_agent("reflect", Some(*pid), Some(phase.clone()), None); self.log(format_args!("reflect: already running pid {}\n", pid)); return None; } @@ -373,10 +380,12 @@ impl AgentCycleState { } fs::write(&offset_path, transcript.size.to_string()).ok(); - let pid = crate::agents::knowledge::spawn_agent( + let spawned = crate::agents::knowledge::spawn_agent( "reflect", &state_dir, &session.session_id); - self.update_agent("reflect", pid, Some("step-0".into())); - self.log(format_args!("reflect: spawned {:?}\n", pid)); + self.update_agent("reflect", + spawned.as_ref().map(|s| s.pid), Some("step-0".into()), + spawned.as_ref().map(|s| s.log_path.clone())); + self.log(format_args!("reflect: spawned {:?}\n", spawned.as_ref().map(|s| s.pid))); reflection } @@ -397,16 +406,18 @@ impl AgentCycleState { let live = crate::agents::knowledge::scan_pid_files(&state_dir, 300); if let Some((phase, pid)) = live.first() { - self.update_agent("journal", Some(*pid), Some(phase.clone())); + self.update_agent("journal", Some(*pid), Some(phase.clone()), None); self.log(format_args!("journal: already running pid {}\n", pid)); return; } fs::write(&offset_path, transcript.size.to_string()).ok(); - let pid = crate::agents::knowledge::spawn_agent( + let spawned = crate::agents::knowledge::spawn_agent( "journal", &state_dir, &session.session_id); - self.update_agent("journal", pid, Some("step-0".into())); - self.log(format_args!("journal: spawned {:?}\n", pid)); + self.update_agent("journal", + spawned.as_ref().map(|s| s.pid), Some("step-0".into()), + spawned.as_ref().map(|s| s.log_path.clone())); + self.log(format_args!("journal: spawned {:?}\n", spawned.as_ref().map(|s| s.pid))); } } // end impl AgentCycleState (cycle methods) diff --git a/src/subconscious/knowledge.rs b/src/subconscious/knowledge.rs index c97482e..72841bf 100644 --- a/src/subconscious/knowledge.rs +++ b/src/subconscious/knowledge.rs @@ -252,11 +252,17 @@ pub fn scan_pid_files(state_dir: &std::path::Path, timeout_secs: u64) -> Vec<(St /// Spawn an agent asynchronously. Writes the pid file before returning /// so the caller immediately sees the agent as running. +/// Spawn result: pid and path to the agent's log file. +pub struct SpawnResult { + pub pid: u32, + pub log_path: PathBuf, +} + pub fn spawn_agent( agent_name: &str, state_dir: &std::path::Path, session_id: &str, -) -> Option { +) -> Option { let def = super::defs::get_def(agent_name)?; let first_phase = def.steps.first() .map(|s| s.phase.as_str()) @@ -265,8 +271,8 @@ pub fn spawn_agent( let log_dir = dirs::home_dir().unwrap_or_default() .join(format!(".consciousness/logs/{}", agent_name)); fs::create_dir_all(&log_dir).ok(); - let agent_log = fs::File::create( - log_dir.join(format!("{}.log", store::compact_timestamp()))) + let log_path = log_dir.join(format!("{}.log", store::compact_timestamp())); + let agent_log = fs::File::create(&log_path) .unwrap_or_else(|_| fs::File::create("/dev/null").unwrap()); let child = std::process::Command::new("poc-memory") @@ -281,7 +287,7 @@ pub fn spawn_agent( let pid = child.id(); let pid_path = state_dir.join(format!("pid-{}", pid)); fs::write(&pid_path, first_phase).ok(); - Some(pid) + Some(SpawnResult { pid, log_path }) } fn run_one_agent_inner(