From 1d46559201a88040056fbfaa6f660083dd70a1ad Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 7 Apr 2026 12:01:19 -0700 Subject: [PATCH] feat: make ecc2 resume spawn real runner --- ecc2/src/main.rs | 2 +- ecc2/src/observability/mod.rs | 1 + ecc2/src/session/daemon.rs | 1 + ecc2/src/session/manager.rs | 62 ++++++++++++++++++++++++++++++++--- ecc2/src/session/mod.rs | 1 + ecc2/src/session/runtime.rs | 1 + ecc2/src/session/store.rs | 46 +++++++++++++++++--------- ecc2/src/tui/dashboard.rs | 14 +++++++- 8 files changed, 107 insertions(+), 21 deletions(-) diff --git a/ecc2/src/main.rs b/ecc2/src/main.rs index 86bce735..b4c4a4ca 100644 --- a/ecc2/src/main.rs +++ b/ecc2/src/main.rs @@ -105,7 +105,7 @@ async fn main() -> Result<()> { println!("Session stopped: {session_id}"); } Some(Commands::Resume { session_id }) => { - let resumed_id = session::manager::resume_session(&db, &session_id).await?; + let resumed_id = session::manager::resume_session(&db, &cfg, &session_id).await?; println!("Session resumed: {resumed_id}"); } Some(Commands::Daemon) => { diff --git a/ecc2/src/observability/mod.rs b/ecc2/src/observability/mod.rs index 80d0c8a2..13e43657 100644 --- a/ecc2/src/observability/mod.rs +++ b/ecc2/src/observability/mod.rs @@ -307,6 +307,7 @@ mod tests { id: id.to_string(), task: "test task".to_string(), agent_type: "claude".to_string(), + working_dir: PathBuf::from("/tmp"), state: SessionState::Pending, pid: None, worktree: None, diff --git a/ecc2/src/session/daemon.rs b/ecc2/src/session/daemon.rs index d9da8f0e..b2fb0372 100644 --- a/ecc2/src/session/daemon.rs +++ b/ecc2/src/session/daemon.rs @@ -122,6 +122,7 @@ mod tests { id: id.to_string(), task: "Recover crashed worker".to_string(), agent_type: "claude".to_string(), + working_dir: PathBuf::from("/tmp"), state, pid, worktree: None, diff --git a/ecc2/src/session/manager.rs b/ecc2/src/session/manager.rs index 2063ac95..b3ea2175 100644 --- a/ecc2/src/session/manager.rs +++ b/ecc2/src/session/manager.rs @@ -75,7 +75,15 @@ pub fn query_tool_calls( ToolLogger::new(db).query(&session.id, page, page_size) } -pub async fn resume_session(db: &StateStore, id: &str) -> Result { +pub async fn resume_session(db: &StateStore, _cfg: &Config, id: &str) -> Result { + resume_session_with_program(db, id, None).await +} + +async fn resume_session_with_program( + db: &StateStore, + id: &str, + runner_executable_override: Option<&Path>, +) -> Result { let session = resolve_session(db, id)?; if session.state == SessionState::Completed { @@ -87,6 +95,19 @@ pub async fn resume_session(db: &StateStore, id: &str) -> Result { } db.update_state_and_pid(&session.id, &SessionState::Pending, None)?; + let runner_executable = match runner_executable_override { + Some(program) => program.to_path_buf(), + None => std::env::current_exe().context("Failed to resolve ECC executable path")?, + }; + spawn_session_runner_for_program( + &session.task, + &session.id, + &session.agent_type, + &session.working_dir, + &runner_executable, + ) + .await + .with_context(|| format!("Failed to resume session {}", session.id))?; Ok(session.id) } @@ -223,11 +244,16 @@ fn build_session_record( } else { None }; + let working_dir = worktree + .as_ref() + .map(|worktree| worktree.path.clone()) + .unwrap_or_else(|| repo_root.to_path_buf()); Ok(Session { id, task: task.to_string(), agent_type: agent_type.to_string(), + working_dir, state: SessionState::Pending, pid: None, worktree, @@ -280,8 +306,24 @@ async fn spawn_session_runner( agent_type: &str, working_dir: &Path, ) -> Result<()> { - let current_exe = std::env::current_exe().context("Failed to resolve ECC executable path")?; - let child = Command::new(¤t_exe) + spawn_session_runner_for_program( + task, + session_id, + agent_type, + working_dir, + &std::env::current_exe().context("Failed to resolve ECC executable path")?, + ) + .await +} + +async fn spawn_session_runner_for_program( + task: &str, + session_id: &str, + agent_type: &str, + working_dir: &Path, + current_exe: &Path, +) -> Result<()> { + let child = Command::new(current_exe) .arg("run-session") .arg("--session-id") .arg(session_id) @@ -491,6 +533,7 @@ mod tests { id: id.to_string(), task: format!("task-{id}"), agent_type: "claude".to_string(), + working_dir: PathBuf::from("/tmp"), state, pid: None, worktree: None, @@ -681,6 +724,7 @@ mod tests { id: "deadbeef".to_string(), task: "resume previous task".to_string(), agent_type: "claude".to_string(), + working_dir: tempdir.path().join("resume-working-dir"), state: SessionState::Failed, pid: Some(31337), worktree: None, @@ -689,7 +733,10 @@ mod tests { metrics: SessionMetrics::default(), })?; - let resumed_id = resume_session(&db, "deadbeef").await?; + fs::create_dir_all(tempdir.path().join("resume-working-dir"))?; + let (fake_claude, log_path) = write_fake_claude(tempdir.path())?; + + let resumed_id = resume_session_with_program(&db, "deadbeef", Some(&fake_claude)).await?; let resumed = db .get_session(&resumed_id)? .context("resumed session should exist")?; @@ -697,6 +744,13 @@ mod tests { assert_eq!(resumed.state, SessionState::Pending); assert_eq!(resumed.pid, None); + let log = wait_for_file(&log_path)?; + assert!(log.contains("run-session")); + assert!(log.contains("--session-id")); + assert!(log.contains("deadbeef")); + assert!(log.contains("resume previous task")); + assert!(log.contains(tempdir.path().join("resume-working-dir").to_string_lossy().as_ref())); + Ok(()) } diff --git a/ecc2/src/session/mod.rs b/ecc2/src/session/mod.rs index 0e256e48..c12943b8 100644 --- a/ecc2/src/session/mod.rs +++ b/ecc2/src/session/mod.rs @@ -14,6 +14,7 @@ pub struct Session { pub id: String, pub task: String, pub agent_type: String, + pub working_dir: PathBuf, pub state: SessionState, pub pid: Option, pub worktree: Option, diff --git a/ecc2/src/session/runtime.rs b/ecc2/src/session/runtime.rs index 87da7b89..3fe605cf 100644 --- a/ecc2/src/session/runtime.rs +++ b/ecc2/src/session/runtime.rs @@ -240,6 +240,7 @@ mod tests { id: session_id.clone(), task: "stream output".to_string(), agent_type: "test".to_string(), + working_dir: env::temp_dir(), state: SessionState::Pending, pid: None, worktree: None, diff --git a/ecc2/src/session/store.rs b/ecc2/src/session/store.rs index 3a5f1dc2..194d665a 100644 --- a/ecc2/src/session/store.rs +++ b/ecc2/src/session/store.rs @@ -29,6 +29,7 @@ impl StateStore { id TEXT PRIMARY KEY, task TEXT NOT NULL, agent_type TEXT NOT NULL, + working_dir TEXT NOT NULL DEFAULT '.', state TEXT NOT NULL DEFAULT 'pending', pid INTEGER, worktree_path TEXT, @@ -84,6 +85,15 @@ impl StateStore { } fn ensure_session_columns(&self) -> Result<()> { + if !self.has_column("sessions", "working_dir")? { + self.conn + .execute( + "ALTER TABLE sessions ADD COLUMN working_dir TEXT NOT NULL DEFAULT '.'", + [], + ) + .context("Failed to add working_dir column to sessions table")?; + } + if !self.has_column("sessions", "pid")? { self.conn .execute("ALTER TABLE sessions ADD COLUMN pid INTEGER", []) @@ -105,12 +115,13 @@ impl StateStore { pub fn insert_session(&self, session: &Session) -> Result<()> { self.conn.execute( - "INSERT INTO sessions (id, task, agent_type, state, pid, worktree_path, worktree_branch, worktree_base, created_at, updated_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + "INSERT INTO sessions (id, task, agent_type, working_dir, state, pid, worktree_path, worktree_branch, worktree_base, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", rusqlite::params![ session.id, session.task, session.agent_type, + session.working_dir.to_string_lossy().to_string(), session.state.to_string(), session.pid.map(i64::from), session @@ -243,7 +254,7 @@ impl StateStore { pub fn list_sessions(&self) -> Result> { let mut stmt = self.conn.prepare( - "SELECT id, task, agent_type, state, pid, worktree_path, worktree_branch, worktree_base, + "SELECT id, task, agent_type, working_dir, state, pid, worktree_path, worktree_branch, worktree_base, tokens_used, tool_calls, files_changed, duration_secs, cost_usd, created_at, updated_at FROM sessions ORDER BY updated_at DESC", @@ -251,25 +262,26 @@ impl StateStore { let sessions = stmt .query_map([], |row| { - let state_str: String = row.get(3)?; + let state_str: String = row.get(4)?; let state = SessionState::from_db_value(&state_str); - let worktree_path: Option = row.get(5)?; + let worktree_path: Option = row.get(6)?; let worktree = worktree_path.map(|path| super::WorktreeInfo { path: PathBuf::from(path), - branch: row.get::<_, String>(6).unwrap_or_default(), - base_branch: row.get::<_, String>(7).unwrap_or_default(), + branch: row.get::<_, String>(7).unwrap_or_default(), + base_branch: row.get::<_, String>(8).unwrap_or_default(), }); - let created_str: String = row.get(13)?; - let updated_str: String = row.get(14)?; + let created_str: String = row.get(14)?; + let updated_str: String = row.get(15)?; Ok(Session { id: row.get(0)?, task: row.get(1)?, agent_type: row.get(2)?, + working_dir: PathBuf::from(row.get::<_, String>(3)?), state, - pid: row.get::<_, Option>(4)?, + pid: row.get::<_, Option>(5)?, worktree, created_at: chrono::DateTime::parse_from_rfc3339(&created_str) .unwrap_or_default() @@ -278,11 +290,11 @@ impl StateStore { .unwrap_or_default() .with_timezone(&chrono::Utc), metrics: SessionMetrics { - tokens_used: row.get(8)?, - tool_calls: row.get(9)?, - files_changed: row.get(10)?, - duration_secs: row.get(11)?, - cost_usd: row.get(12)?, + tokens_used: row.get(9)?, + tool_calls: row.get(10)?, + files_changed: row.get(11)?, + duration_secs: row.get(12)?, + cost_usd: row.get(13)?, }, }) })? @@ -518,6 +530,7 @@ mod tests { id: id.to_string(), task: "task".to_string(), agent_type: "claude".to_string(), + working_dir: PathBuf::from("/tmp"), state, pid: None, worktree: None, @@ -556,6 +569,7 @@ mod tests { id TEXT PRIMARY KEY, task TEXT NOT NULL, agent_type TEXT NOT NULL, + working_dir TEXT NOT NULL DEFAULT '.', state TEXT NOT NULL DEFAULT 'pending', worktree_path TEXT, worktree_branch TEXT, @@ -578,6 +592,7 @@ mod tests { .query_map([], |row| row.get::<_, String>(1))? .collect::, _>>()?; + assert!(column_names.iter().any(|column| column == "working_dir")); assert!(column_names.iter().any(|column| column == "pid")); Ok(()) } @@ -592,6 +607,7 @@ mod tests { id: "session-1".to_string(), task: "buffer output".to_string(), agent_type: "claude".to_string(), + working_dir: PathBuf::from("/tmp"), state: SessionState::Running, pid: None, worktree: None, diff --git a/ecc2/src/tui/dashboard.rs b/ecc2/src/tui/dashboard.rs index 246d9a93..312a13d6 100644 --- a/ecc2/src/tui/dashboard.rs +++ b/ecc2/src/tui/dashboard.rs @@ -551,7 +551,7 @@ impl Dashboard { return; }; - if let Err(error) = manager::resume_session(&self.db, &session.id).await { + if let Err(error) = manager::resume_session(&self.db, &self.cfg, &session.id).await { tracing::warn!("Failed to resume session {}: {error}", session.id); return; } @@ -1309,6 +1309,7 @@ mod tests { id: "older".to_string(), task: "older".to_string(), agent_type: "claude".to_string(), + working_dir: PathBuf::from("/tmp"), state: SessionState::Idle, pid: None, worktree: None, @@ -1321,6 +1322,7 @@ mod tests { id: "newer".to_string(), task: "newer".to_string(), agent_type: "claude".to_string(), + working_dir: PathBuf::from("/tmp"), state: SessionState::Running, pid: None, worktree: None, @@ -1349,6 +1351,7 @@ mod tests { id: "session-1".to_string(), task: "inspect output".to_string(), agent_type: "claude".to_string(), + working_dir: PathBuf::from("/tmp"), state: SessionState::Running, pid: None, worktree: None, @@ -1387,6 +1390,7 @@ mod tests { id: "session-1".to_string(), task: "tail output".to_string(), agent_type: "claude".to_string(), + working_dir: PathBuf::from("/tmp"), state: SessionState::Running, pid: None, worktree: None, @@ -1422,6 +1426,7 @@ mod tests { task: "stop me".to_string(), agent_type: "claude".to_string(), state: SessionState::Running, + working_dir: PathBuf::from("/tmp"), pid: Some(999_999), worktree: None, created_at: now, @@ -1454,6 +1459,7 @@ mod tests { task: "resume me".to_string(), agent_type: "claude".to_string(), state: SessionState::Failed, + working_dir: PathBuf::from("/tmp/ecc2-resume"), pid: None, worktree: Some(WorktreeInfo { path: PathBuf::from("/tmp/ecc2-resume"), @@ -1492,6 +1498,7 @@ mod tests { task: "cleanup me".to_string(), agent_type: "claude".to_string(), state: SessionState::Stopped, + working_dir: worktree_path.clone(), pid: None, worktree: Some(WorktreeInfo { path: worktree_path.clone(), @@ -1527,6 +1534,7 @@ mod tests { id: "done-1".to_string(), task: "delete me".to_string(), agent_type: "claude".to_string(), + working_dir: PathBuf::from("/tmp"), state: SessionState::Completed, pid: None, worktree: None, @@ -1638,6 +1646,9 @@ mod tests { task: "Render dashboard rows".to_string(), agent_type: agent_type.to_string(), state, + working_dir: branch + .map(|branch| PathBuf::from(format!("/tmp/{branch}"))) + .unwrap_or_else(|| PathBuf::from("/tmp")), pid: None, worktree: branch.map(|branch| WorktreeInfo { path: PathBuf::from(format!("/tmp/{branch}")), @@ -1663,6 +1674,7 @@ mod tests { task: "Budget tracking".to_string(), agent_type: "claude".to_string(), state: SessionState::Running, + working_dir: PathBuf::from("/tmp"), pid: None, worktree: None, created_at: now,