Skip to main content

ralph_workflow/checkpoint/
restore.rs

1//! State restoration from checkpoints.
2//!
3//! This module provides functionality to restore pipeline state from a checkpoint,
4//! including CLI arguments and configuration overrides.
5
6use crate::checkpoint::execution_history::ExecutionHistory;
7use crate::checkpoint::state::{PipelineCheckpoint, PipelinePhase, RebaseState};
8use crate::config::Config;
9
10/// Rich context about a resumed session for use in agent prompts.
11///
12/// This struct contains information that helps AI agents understand where
13/// they are in the pipeline when resuming from a checkpoint, enabling them
14/// to provide more contextual and appropriate responses.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ResumeContext {
17    /// The phase being resumed from
18    pub phase: PipelinePhase,
19    /// Current iteration number (for development)
20    pub iteration: u32,
21    /// Total iterations
22    pub total_iterations: u32,
23    /// Current reviewer pass
24    pub reviewer_pass: u32,
25    /// Total reviewer passes
26    pub total_reviewer_passes: u32,
27    /// Number of times this session has been resumed
28    pub resume_count: u32,
29    /// Rebase state if applicable
30    pub rebase_state: RebaseState,
31    /// Run ID for tracing
32    pub run_id: String,
33    /// Captured prompts from the original run for deterministic replay
34    pub prompt_history:
35        Option<std::collections::HashMap<String, crate::prompts::PromptHistoryEntry>>,
36    /// Execution history from the checkpoint (if available)
37    pub execution_history: Option<ExecutionHistory>,
38}
39
40impl ResumeContext {
41    /// Display name for the current phase.
42    #[must_use]
43    pub fn phase_name(&self) -> String {
44        match self.phase {
45            PipelinePhase::Rebase => "Rebase".to_string(),
46            PipelinePhase::Planning => "Planning".to_string(),
47            PipelinePhase::Development => format!(
48                "Development iteration {}/{}",
49                self.iteration, self.total_iterations
50            ),
51            PipelinePhase::Review => format!(
52                "Review (pass {}/{})",
53                self.reviewer_pass, self.total_reviewer_passes
54            ),
55            PipelinePhase::CommitMessage => "Commit Message Generation".to_string(),
56            PipelinePhase::FinalValidation => "Final Validation".to_string(),
57            PipelinePhase::Complete => "Complete".to_string(),
58            PipelinePhase::PreRebase => "Pre-Rebase".to_string(),
59            PipelinePhase::PreRebaseConflict => "Pre-Rebase Conflict".to_string(),
60            PipelinePhase::PostRebase => "Post-Rebase".to_string(),
61            PipelinePhase::PostRebaseConflict => "Post-Rebase Conflict".to_string(),
62            PipelinePhase::AwaitingDevFix => "Awaiting Dev Fix".to_string(),
63            PipelinePhase::Interrupted => "Interrupted".to_string(),
64        }
65    }
66}
67
68impl PipelineCheckpoint {
69    /// Extract rich resume context from this checkpoint.
70    ///
71    /// This method creates a `ResumeContext` containing all the information
72    /// needed to generate informative prompts for agents when resuming.
73    #[must_use]
74    pub fn resume_context(&self) -> ResumeContext {
75        ResumeContext {
76            phase: self.phase,
77            iteration: self.iteration,
78            total_iterations: self.total_iterations,
79            reviewer_pass: self.reviewer_pass,
80            total_reviewer_passes: self.total_reviewer_passes,
81            resume_count: self.resume_count,
82            rebase_state: self.rebase_state.clone(),
83            run_id: self.run_id.clone(),
84            prompt_history: self.prompt_history.clone(),
85            execution_history: self.execution_history.clone(),
86        }
87    }
88}
89
90/// Apply checkpoint values to the current config for deterministic resume.
91pub fn apply_checkpoint_to_config(config: &mut Config, checkpoint: &PipelineCheckpoint) {
92    let cli_args = &checkpoint.cli_args;
93
94    // Always restore developer_iters and reviewer_reviews from checkpoint
95    // to ensure exact state restoration, even if zero
96    config.developer_iters = cli_args.developer_iters;
97    config.reviewer_reviews = cli_args.reviewer_reviews;
98
99    // Note: review_depth is stored as a string in the checkpoint
100    // but as an enum in Config. For now, we don't override it.
101    // This could be enhanced to parse the string back to an enum.
102
103    // Apply model overrides if they exist in the checkpoint
104    if let Some(ref model) = checkpoint.developer_agent_config.model_override {
105        config.developer_model = Some(model.clone());
106    }
107    if let Some(ref model) = checkpoint.reviewer_agent_config.model_override {
108        config.reviewer_model = Some(model.clone());
109    }
110
111    // Apply provider overrides if they exist in the checkpoint
112    if let Some(ref provider) = checkpoint.developer_agent_config.provider_override {
113        config.developer_provider = Some(provider.clone());
114    }
115    if let Some(ref provider) = checkpoint.reviewer_agent_config.provider_override {
116        config.reviewer_provider = Some(provider.clone());
117    }
118
119    // Apply context levels if they exist in the checkpoint
120    config.developer_context = checkpoint.developer_agent_config.context_level;
121    config.reviewer_context = checkpoint.reviewer_agent_config.context_level;
122
123    // Apply git identity if it exists in the checkpoint
124    if let Some(ref name) = checkpoint.git_user_name {
125        config.git_user_name = Some(name.clone());
126    }
127    if let Some(ref email) = checkpoint.git_user_email {
128        config.git_user_email = Some(email.clone());
129    }
130
131    // Always restore isolation_mode from checkpoint for exact state restoration
132    config.isolation_mode = cli_args.isolation_mode;
133
134    // Apply verbosity level from checkpoint
135    config.verbosity = crate::config::types::Verbosity::from(cli_args.verbosity);
136
137    // Apply show_streaming_metrics from checkpoint
138    config.show_streaming_metrics = cli_args.show_streaming_metrics;
139
140    // Apply reviewer_json_parser from checkpoint if it exists
141    if let Some(ref parser) = cli_args.reviewer_json_parser {
142        config.reviewer_json_parser = Some(parser.clone());
143    }
144}
145
146/// Restore environment variables from a checkpoint.
147///
148/// Restore safe environment variables from the checkpoint snapshot.
149#[must_use]
150pub fn restore_environment_from_checkpoint(checkpoint: &PipelineCheckpoint) -> usize {
151    restore_environment_impl(checkpoint, |key, value| {
152        std::env::set_var(key, value);
153    })
154}
155
156/// Inner implementation for restoring environment variables from a checkpoint.
157///
158/// Accepts an injectable `set_var` callback so tests can verify which variables
159/// would be set without mutating the real process environment (eliminating the
160/// need for `#[serial]`).
161pub(crate) fn restore_environment_impl(
162    checkpoint: &PipelineCheckpoint,
163    mut set_var: impl FnMut(&str, &str),
164) -> usize {
165    let Some(ref env_snap) = checkpoint.env_snapshot else {
166        return 0;
167    };
168
169    let mut restored = 0;
170
171    // Restore RALPH_* variables (safe only)
172    for (key, value) in &env_snap.ralph_vars {
173        if crate::checkpoint::state::is_sensitive_env_key(key) {
174            continue;
175        }
176        set_var(key, value);
177        restored += 1;
178    }
179
180    restored
181}
182
183/// Calculate the starting iteration for development phase resume.
184#[must_use]
185pub fn calculate_start_iteration(checkpoint: &PipelineCheckpoint, max_iterations: u32) -> u32 {
186    match checkpoint.phase {
187        PipelinePhase::Planning | PipelinePhase::Development => {
188            checkpoint.iteration.clamp(1, max_iterations)
189        }
190        // For later phases, development is already complete
191        _ => max_iterations,
192    }
193}
194
195/// Calculate the starting reviewer pass for review phase resume.
196///
197/// When resuming from a checkpoint in the review phase, this determines
198/// which pass to start from based on the checkpoint state.
199///
200/// # Arguments
201///
202/// * `checkpoint` - The checkpoint to calculate from
203/// * `max_passes` - Maximum review passes configured
204///
205/// # Returns
206///
207/// The pass number to start from (1-indexed).
208#[must_use]
209pub fn calculate_start_reviewer_pass(checkpoint: &PipelineCheckpoint, max_passes: u32) -> u32 {
210    match checkpoint.phase {
211        PipelinePhase::Review => checkpoint.reviewer_pass.clamp(1, max_passes.max(1)),
212        // For earlier phases, start from the beginning
213        PipelinePhase::Planning
214        | PipelinePhase::Development
215        | PipelinePhase::PreRebase
216        | PipelinePhase::PreRebaseConflict => 1,
217        // For later phases, review is already complete
218        _ => max_passes,
219    }
220}
221
222/// Determine if a phase should be skipped based on checkpoint.
223///
224/// Returns true if the checkpoint indicates this phase has already been completed.
225#[must_use]
226pub const fn should_skip_phase(phase: PipelinePhase, checkpoint: &PipelineCheckpoint) -> bool {
227    phase_rank(phase) < phase_rank(checkpoint.phase)
228}
229
230/// Get the rank (position) of a phase in the pipeline.
231///
232/// Lower values indicate earlier phases in the pipeline.
233const fn phase_rank(phase: PipelinePhase) -> u32 {
234    match phase {
235        PipelinePhase::Planning => 0,
236        PipelinePhase::Development => 1,
237        PipelinePhase::CommitMessage => 3,
238        PipelinePhase::FinalValidation => 4,
239        PipelinePhase::Complete => 5,
240        PipelinePhase::AwaitingDevFix => 6,
241        PipelinePhase::Interrupted => 7,
242        // Review and rebase phases all map to rank 2
243        PipelinePhase::Review
244        | PipelinePhase::PreRebase
245        | PipelinePhase::PreRebaseConflict
246        | PipelinePhase::Rebase
247        | PipelinePhase::PostRebase
248        | PipelinePhase::PostRebaseConflict => 2,
249    }
250}
251
252#[cfg(test)]
253#[derive(Debug, Clone)]
254pub struct RestoredContext {
255    pub phase: PipelinePhase,
256    pub resume_iteration: u32,
257    pub total_iterations: u32,
258    pub resume_reviewer_pass: u32,
259    pub total_reviewer_passes: u32,
260    pub developer_agent: String,
261    pub reviewer_agent: String,
262    pub cli_args: Option<crate::checkpoint::state::CliArgsSnapshot>,
263}
264
265#[cfg(test)]
266impl RestoredContext {
267    /// Create a restored context from a checkpoint.
268    #[must_use]
269    pub fn from_checkpoint(checkpoint: &PipelineCheckpoint) -> Self {
270        // Determine if CLI args are meaningful (non-default values)
271        let cli_args = if checkpoint.cli_args.developer_iters > 0
272            || checkpoint.cli_args.reviewer_reviews > 0
273        {
274            Some(checkpoint.cli_args.clone())
275        } else {
276            None
277        };
278
279        Self {
280            phase: checkpoint.phase,
281            resume_iteration: checkpoint.iteration,
282            total_iterations: checkpoint.total_iterations,
283            resume_reviewer_pass: checkpoint.reviewer_pass,
284            total_reviewer_passes: checkpoint.total_reviewer_passes,
285            developer_agent: checkpoint.developer_agent.clone(),
286            reviewer_agent: checkpoint.reviewer_agent.clone(),
287            cli_args,
288        }
289    }
290
291    /// Check if we should use checkpoint values for iteration counts.
292    ///
293    /// Returns true if the checkpoint has meaningful CLI args that should
294    /// override the current configuration.
295    #[must_use]
296    pub fn should_use_checkpoint_iterations(&self) -> bool {
297        self.cli_args
298            .as_ref()
299            .is_some_and(|args| args.developer_iters > 0)
300    }
301
302    /// Check if we should use checkpoint values for reviewer counts.
303    #[must_use]
304    pub fn should_use_checkpoint_reviewer_passes(&self) -> bool {
305        self.cli_args
306            .as_ref()
307            .is_some_and(|args| args.reviewer_reviews > 0)
308    }
309}
310
311#[cfg(test)]
312mod tests;