diff --git a/src/subconscious/hook.rs b/src/subconscious/hook.rs index 89aac4e..44da36b 100644 --- a/src/subconscious/hook.rs +++ b/src/subconscious/hook.rs @@ -143,10 +143,10 @@ pub struct AgentInfo { child: Option, } -/// Snapshot of agent state for sending to TUI (no Child handle). -#[derive(Clone, Debug)] +/// Snapshot of agent state — serializable, sendable to TUI. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct AgentSnapshot { - pub name: &'static str, + pub name: String, pub pid: Option, pub phase: Option, pub log_path: Option, @@ -155,7 +155,7 @@ pub struct AgentSnapshot { impl AgentInfo { fn snapshot(&self) -> AgentSnapshot { AgentSnapshot { - name: self.name, + name: self.name.to_string(), pid: self.pid, phase: self.phase.clone(), log_path: self.log_path.clone(), @@ -163,6 +163,47 @@ impl AgentInfo { } } +/// Serializable state for persisting across Claude Code hook invocations. +#[derive(serde::Serialize, serde::Deserialize)] +pub struct SavedAgentState { + pub agents: Vec, +} + +impl SavedAgentState { + fn state_path(session_id: &str) -> std::path::PathBuf { + let dir = dirs::home_dir().unwrap_or_default().join(".consciousness/sessions"); + fs::create_dir_all(&dir).ok(); + dir.join(format!("agent-state-{}.json", session_id)) + } + + pub fn load(session_id: &str) -> Self { + let path = Self::state_path(session_id); + let mut state: Self = fs::read_to_string(&path).ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or(SavedAgentState { agents: Vec::new() }); + + // Check if saved pids are still alive + for agent in &mut state.agents { + if let Some(pid) = agent.pid { + unsafe { + if libc::kill(pid as i32, 0) != 0 { + agent.pid = None; + agent.phase = None; + } + } + } + } + state + } + + pub fn save(&self, session_id: &str) { + let path = Self::state_path(session_id); + if let Ok(json) = serde_json::to_string(self) { + fs::write(path, json).ok(); + } + } +} + /// Persistent state for the agent orchestration cycle. /// Created once, `trigger()` called on each user message. /// TUI reads `agents` and `last_output` for display. @@ -207,7 +248,7 @@ impl AgentCycleState { } fn agent_running(&self, name: &str) -> bool { - self.agents.iter().any(|a| a.name == name && a.child.is_some()) + self.agents.iter().any(|a| a.name == name && a.pid.is_some()) } fn agent_spawned(&mut self, name: &str, phase: &str, @@ -220,18 +261,23 @@ impl AgentCycleState { } } - /// Check if any spawned agents have completed. Reap them. + /// Check if any agents have completed. Reap child handles, or + /// check pid liveness for restored-from-disk agents. fn poll_children(&mut self) { for agent in &mut self.agents { if let Some(ref mut child) = agent.child { - match child.try_wait() { - Ok(Some(_status)) => { + if let Ok(Some(_)) = child.try_wait() { + agent.pid = None; + agent.phase = None; + agent.child = None; + } + } else if let Some(pid) = agent.pid { + // No child handle (restored from saved state) — check pid + unsafe { + if libc::kill(pid as i32, 0) != 0 { agent.pid = None; agent.phase = None; - agent.child = None; - // log_path stays — TUI can still view the log } - _ => {} } } } @@ -241,6 +287,24 @@ impl AgentCycleState { self.agents.iter().map(|a| a.snapshot()).collect() } + /// Restore agent state from a saved snapshot (for Claude Code hook path). + pub fn restore(&mut self, saved: &SavedAgentState) { + for sa in &saved.agents { + if let Some(agent) = self.agents.iter_mut().find(|a| a.name == sa.name) { + agent.pid = sa.pid; + agent.phase = sa.phase.clone(); + agent.log_path = sa.log_path.clone(); + // No child handle — we just track the pid + } + } + } + + /// Save current state for the Claude Code hook path. + pub fn save(&self, session_id: &str) { + let state = SavedAgentState { agents: self.snapshots() }; + state.save(session_id); + } + /// Run all agent cycles. Call on each user message. pub fn trigger(&mut self, session: &HookSession) { let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S"); @@ -258,9 +322,12 @@ impl AgentCycleState { } /// Standalone entry point for the Claude Code hook path. +/// Loads saved state, runs cycles, saves state back. pub fn run_agent_cycles(session: &HookSession) -> AgentCycleOutput { let mut state = AgentCycleState::new(&session.session_id); + state.restore(&SavedAgentState::load(&session.session_id)); state.trigger(session); + state.save(&session.session_id); state.last_output }