Skip to main content

ralph_workflow/interrupt/
checkpoint.rs

1// Interrupt checkpoint: InterruptContext and save_interrupt_checkpoint.
2//
3// This file contains the data structure holding pipeline state needed to
4// persist a checkpoint when the user interrupts with Ctrl+C, and the
5// function that writes that checkpoint.
6
7use crate::workspace::Workspace;
8
9/// Context needed to save a checkpoint when interrupted.
10///
11/// This structure holds references to all the state needed to create
12/// a checkpoint when the user interrupts the pipeline with Ctrl+C.
13#[derive(Clone)]
14pub struct InterruptContext {
15    /// Current pipeline phase
16    pub phase: crate::checkpoint::PipelinePhase,
17    /// Current iteration number
18    pub iteration: u32,
19    /// Total iterations configured
20    pub total_iterations: u32,
21    /// Current reviewer pass number
22    pub reviewer_pass: u32,
23    /// Total reviewer passes configured
24    pub total_reviewer_passes: u32,
25    /// Run context for tracking execution lineage
26    pub run_context: crate::checkpoint::RunContext,
27    /// Execution history tracking
28    pub execution_history: crate::checkpoint::ExecutionHistory,
29    /// Prompt history for deterministic resume
30    pub prompt_history: std::collections::HashMap<String, crate::prompts::PromptHistoryEntry>,
31    /// Workspace for checkpoint persistence
32    pub workspace: std::sync::Arc<dyn Workspace>,
33}
34
35/// Save a checkpoint when the pipeline is interrupted.
36///
37/// This function persists a checkpoint that records the *current operational phase*
38/// and sets `interrupted_by_user=true`.
39///
40/// We intentionally do NOT overwrite the phase to `Interrupted` because that makes
41/// `--resume` terminate immediately in `PipelinePhase::Interrupted`.
42///
43/// # Arguments
44///
45/// * `context` - The interrupt context containing the current pipeline state
46pub(super) fn save_interrupt_checkpoint(context: &InterruptContext) -> anyhow::Result<()> {
47    use crate::checkpoint::state::{
48        calculate_file_checksum_with_workspace, AgentConfigSnapshot, CheckpointParams,
49        CliArgsSnapshotBuilder, PipelineCheckpoint, RebaseState,
50    };
51    use crate::checkpoint::{load_checkpoint_with_workspace, save_checkpoint_with_workspace};
52    use std::path::Path;
53
54    // Read checkpoint from file if exists, update it with current operational phase
55    if let Ok(Some(checkpoint)) = load_checkpoint_with_workspace(&*context.workspace) {
56        // Update existing checkpoint with current operational phase and progress.
57        let checkpoint = PipelineCheckpoint {
58            phase: context.phase,
59            iteration: context.iteration,
60            total_iterations: context.total_iterations,
61            reviewer_pass: context.reviewer_pass,
62            total_reviewer_passes: context.total_reviewer_passes,
63            actual_developer_runs: context.run_context.actual_developer_runs,
64            actual_reviewer_runs: context.run_context.actual_reviewer_runs,
65            execution_history: Some(context.execution_history.clone()),
66            prompt_history: Some(context.prompt_history.clone()),
67            interrupted_by_user: true,
68            ..checkpoint
69        };
70
71        save_checkpoint_with_workspace(&*context.workspace, &checkpoint)?;
72    } else {
73        // No checkpoint exists yet - this is early interruption.
74        //
75        // We still MUST persist a checkpoint (not just print) so that resume can reliably
76        // honor the Ctrl+C exemption via `interrupted_by_user=true`.
77        //
78        // This checkpoint uses conservative placeholder agent snapshots because we don't
79        // have access to Config/AgentRegistry in the signal handler.
80        let prompt_md_checksum =
81            calculate_file_checksum_with_workspace(&*context.workspace, Path::new("PROMPT.md"))
82                .or_else(|| Some("unknown".to_string()));
83
84        let cli_args = CliArgsSnapshotBuilder::new(
85            context.total_iterations,
86            context.total_reviewer_passes,
87            /* review_depth */ None,
88            /* isolation_mode */ true,
89        )
90        .build();
91
92        let developer_agent = "unknown";
93        let reviewer_agent = "unknown";
94        let developer_agent_config = AgentConfigSnapshot::new(
95            developer_agent.to_string(),
96            "unknown".to_string(),
97            "-o".to_string(),
98            None,
99            /* can_commit */ true,
100        );
101        let reviewer_agent_config = AgentConfigSnapshot::new(
102            reviewer_agent.to_string(),
103            "unknown".to_string(),
104            "-o".to_string(),
105            None,
106            /* can_commit */ true,
107        );
108
109        let working_dir = context.workspace.root().to_string_lossy().to_string();
110        let base_checkpoint = PipelineCheckpoint::from_params(CheckpointParams {
111            phase: context.phase,
112            iteration: context.iteration,
113            total_iterations: context.total_iterations,
114            reviewer_pass: context.reviewer_pass,
115            total_reviewer_passes: context.total_reviewer_passes,
116            developer_agent,
117            reviewer_agent,
118            cli_args,
119            developer_agent_config,
120            reviewer_agent_config,
121            rebase_state: RebaseState::default(),
122            git_user_name: None,
123            git_user_email: None,
124            run_id: &context.run_context.run_id,
125            parent_run_id: context.run_context.parent_run_id.as_deref(),
126            resume_count: context.run_context.resume_count,
127            actual_developer_runs: context.run_context.actual_developer_runs,
128            actual_reviewer_runs: context.run_context.actual_reviewer_runs,
129            working_dir,
130            prompt_md_checksum,
131            config_path: None,
132            config_checksum: None,
133        });
134
135        let checkpoint = PipelineCheckpoint {
136            execution_history: Some(context.execution_history.clone()),
137            prompt_history: Some(context.prompt_history.clone()),
138            interrupted_by_user: true,
139            ..base_checkpoint
140        };
141
142        save_checkpoint_with_workspace(&*context.workspace, &checkpoint)?;
143    }
144
145    Ok(())
146}