use std::path::PathBuf;
use zag_agent::builder::AgentBuilder;
use crate::error::ZigError;
use crate::paths;
use crate::session::{
SessionEventKind, SessionLogIndexEntry, load_project_index, read_session_events,
};
pub struct ContinueOptions {
pub workflow: Option<String>,
pub session: Option<String>,
}
#[derive(Debug)]
pub struct ResumeTarget {
pub zig_session_id: String,
pub workflow_name: String,
pub zag_session_id: String,
pub log_path: PathBuf,
}
pub fn resolve(opts: &ContinueOptions) -> Result<ResumeTarget, ZigError> {
let entry = if let Some(id) = &opts.session {
find_by_prefix(id)?
} else if let Some(name) = &opts.workflow {
find_latest_for_workflow(name)?
} else {
find_latest()?
};
let log_path = PathBuf::from(&entry.log_path);
resolve_from_log(&log_path, entry)
}
pub fn resolve_from_log(
log_path: &std::path::Path,
entry: SessionLogIndexEntry,
) -> Result<ResumeTarget, ZigError> {
let events = read_session_events(log_path)?;
let zag_session_id = events
.iter()
.rev()
.find_map(|e| match &e.kind {
SessionEventKind::StepStarted { zag_session_id, .. } => Some(zag_session_id.clone()),
_ => None,
})
.ok_or_else(|| {
ZigError::Io(format!(
"session '{}' has no recorded step to resume",
entry.zig_session_id
))
})?;
Ok(ResumeTarget {
zig_session_id: entry.zig_session_id,
workflow_name: entry.workflow_name,
zag_session_id,
log_path: log_path.to_path_buf(),
})
}
fn project_sessions() -> Result<Vec<SessionLogIndexEntry>, ZigError> {
let idx_path = paths::project_index_path(None)
.ok_or_else(|| ZigError::Io("HOME environment variable not set".into()))?;
if !idx_path.exists() {
return Err(ZigError::Io(
"no zig sessions yet — run `zig run` first.".into(),
));
}
Ok(load_project_index(&idx_path).sessions)
}
fn find_by_prefix(id: &str) -> Result<SessionLogIndexEntry, ZigError> {
let sessions = project_sessions()?;
let matches: Vec<_> = sessions
.into_iter()
.filter(|e| e.zig_session_id.starts_with(id))
.collect();
match matches.len() {
0 => Err(ZigError::Io(format!("no session matches '{id}'"))),
1 => Ok(matches.into_iter().next().unwrap()),
n => Err(ZigError::Io(format!(
"ambiguous session prefix '{id}' matches {n} sessions"
))),
}
}
fn find_latest() -> Result<SessionLogIndexEntry, ZigError> {
project_sessions()?
.into_iter()
.max_by(|a, b| a.started_at.cmp(&b.started_at))
.ok_or_else(|| ZigError::Io("no zig sessions yet — run `zig run` first.".into()))
}
fn find_latest_for_workflow(name: &str) -> Result<SessionLogIndexEntry, ZigError> {
project_sessions()?
.into_iter()
.filter(|e| e.workflow_name == name)
.max_by(|a, b| a.started_at.cmp(&b.started_at))
.ok_or_else(|| ZigError::Io(format!("no zig sessions found for workflow '{name}'.")))
}
pub async fn continue_run(opts: ContinueOptions) -> Result<(), ZigError> {
let target = resolve(&opts)?;
let short_id = &target.zig_session_id[..target.zig_session_id.len().min(8)];
eprintln!(
"resuming workflow '{}' (zig session {}, zag session {})",
target.workflow_name, short_id, target.zag_session_id,
);
AgentBuilder::new()
.resume(&target.zag_session_id)
.await
.map_err(|e| ZigError::Zag(format!("resume failed: {e}")))
}
#[cfg(test)]
#[path = "resume_tests.rs"]
mod tests;