1use std::path::PathBuf;
16
17use zag_agent::builder::AgentBuilder;
18
19use crate::error::ZigError;
20use crate::paths;
21use crate::session::{
22 SessionEventKind, SessionLogIndexEntry, load_project_index, read_session_events,
23};
24
25pub struct ContinueOptions {
27 pub workflow: Option<String>,
29 pub session: Option<String>,
31}
32
33#[derive(Debug)]
35pub struct ResumeTarget {
36 pub zig_session_id: String,
37 pub workflow_name: String,
38 pub zag_session_id: String,
39 pub log_path: PathBuf,
40}
41
42pub fn resolve(opts: &ContinueOptions) -> Result<ResumeTarget, ZigError> {
52 let entry = if let Some(id) = &opts.session {
53 find_by_prefix(id)?
54 } else if let Some(name) = &opts.workflow {
55 find_latest_for_workflow(name)?
56 } else {
57 find_latest()?
58 };
59
60 let log_path = PathBuf::from(&entry.log_path);
61 resolve_from_log(&log_path, entry)
62}
63
64pub fn resolve_from_log(
67 log_path: &std::path::Path,
68 entry: SessionLogIndexEntry,
69) -> Result<ResumeTarget, ZigError> {
70 let events = read_session_events(log_path)?;
71
72 let zag_session_id = events
73 .iter()
74 .rev()
75 .find_map(|e| match &e.kind {
76 SessionEventKind::StepStarted { zag_session_id, .. } => Some(zag_session_id.clone()),
77 _ => None,
78 })
79 .ok_or_else(|| {
80 ZigError::Io(format!(
81 "session '{}' has no recorded step to resume",
82 entry.zig_session_id
83 ))
84 })?;
85
86 Ok(ResumeTarget {
87 zig_session_id: entry.zig_session_id,
88 workflow_name: entry.workflow_name,
89 zag_session_id,
90 log_path: log_path.to_path_buf(),
91 })
92}
93
94fn project_sessions() -> Result<Vec<SessionLogIndexEntry>, ZigError> {
95 let idx_path = paths::project_index_path(None)
96 .ok_or_else(|| ZigError::Io("HOME environment variable not set".into()))?;
97 if !idx_path.exists() {
98 return Err(ZigError::Io(
99 "no zig sessions yet — run `zig run` first.".into(),
100 ));
101 }
102 Ok(load_project_index(&idx_path).sessions)
103}
104
105fn find_by_prefix(id: &str) -> Result<SessionLogIndexEntry, ZigError> {
106 let sessions = project_sessions()?;
107 let matches: Vec<_> = sessions
108 .into_iter()
109 .filter(|e| e.zig_session_id.starts_with(id))
110 .collect();
111 match matches.len() {
112 0 => Err(ZigError::Io(format!("no session matches '{id}'"))),
113 1 => Ok(matches.into_iter().next().unwrap()),
114 n => Err(ZigError::Io(format!(
115 "ambiguous session prefix '{id}' matches {n} sessions"
116 ))),
117 }
118}
119
120fn find_latest() -> Result<SessionLogIndexEntry, ZigError> {
121 project_sessions()?
122 .into_iter()
123 .max_by(|a, b| a.started_at.cmp(&b.started_at))
124 .ok_or_else(|| ZigError::Io("no zig sessions yet — run `zig run` first.".into()))
125}
126
127fn find_latest_for_workflow(name: &str) -> Result<SessionLogIndexEntry, ZigError> {
128 project_sessions()?
129 .into_iter()
130 .filter(|e| e.workflow_name == name)
131 .max_by(|a, b| a.started_at.cmp(&b.started_at))
132 .ok_or_else(|| ZigError::Io(format!("no zig sessions found for workflow '{name}'.")))
133}
134
135pub async fn continue_run(opts: ContinueOptions) -> Result<(), ZigError> {
138 let target = resolve(&opts)?;
139 let short_id = &target.zig_session_id[..target.zig_session_id.len().min(8)];
140 eprintln!(
141 "resuming workflow '{}' (zig session {}, zag session {})",
142 target.workflow_name, short_id, target.zag_session_id,
143 );
144 AgentBuilder::new()
145 .resume(&target.zag_session_id)
146 .await
147 .map_err(|e| ZigError::Zag(format!("resume failed: {e}")))
148}
149
150#[cfg(test)]
151#[path = "resume_tests.rs"]
152mod tests;