ralph_workflow/
interrupt.rs1use std::sync::Mutex;
12
13use crate::workspace::Workspace;
14
15static INTERRUPT_CONTEXT: Mutex<Option<InterruptContext>> = Mutex::new(None);
20
21pub struct InterruptContext {
26 pub phase: crate::checkpoint::PipelinePhase,
28 pub iteration: u32,
30 pub total_iterations: u32,
32 pub reviewer_pass: u32,
34 pub total_reviewer_passes: u32,
36 pub run_context: crate::checkpoint::RunContext,
38 pub execution_history: crate::checkpoint::ExecutionHistory,
40 pub prompt_history: std::collections::HashMap<String, String>,
42 pub workspace: std::sync::Arc<dyn Workspace>,
44}
45
46pub fn set_interrupt_context(context: InterruptContext) {
61 let mut ctx = INTERRUPT_CONTEXT.lock().unwrap_or_else(|poison| {
62 poison.into_inner()
64 });
65 *ctx = Some(context);
66}
67
68pub fn clear_interrupt_context() {
73 let mut ctx = INTERRUPT_CONTEXT.lock().unwrap_or_else(|poison| {
74 poison.into_inner()
76 });
77 *ctx = None;
78}
79
80pub fn setup_interrupt_handler() {
89 ctrlc::set_handler(|| {
90 eprintln!("\nā Interrupt received! Saving checkpoint...");
91
92 let ctx = INTERRUPT_CONTEXT.lock().unwrap_or_else(|poison| {
94 poison.into_inner()
96 });
97 if let Some(ref context) = *ctx {
98 if let Err(e) = save_interrupt_checkpoint(context) {
99 eprintln!("Warning: Failed to save checkpoint: {}", e);
100 } else {
101 eprintln!("ā Checkpoint saved. Resume with: ralph --resume");
102 }
103 }
104 drop(ctx); eprintln!("Cleaning up...");
107 crate::git_helpers::cleanup_agent_phase_silent();
108 std::process::exit(130); })
110 .ok(); }
112
113fn save_interrupt_checkpoint(context: &InterruptContext) -> anyhow::Result<()> {
125 use crate::checkpoint::PipelinePhase;
126 use crate::checkpoint::{load_checkpoint_with_workspace, save_checkpoint_with_workspace};
127
128 if let Ok(Some(mut checkpoint)) = load_checkpoint_with_workspace(&*context.workspace) {
130 let _original_phase = context.phase;
132
133 checkpoint.phase = PipelinePhase::Interrupted;
135 checkpoint.iteration = context.iteration;
136 checkpoint.total_iterations = context.total_iterations;
137 checkpoint.reviewer_pass = context.reviewer_pass;
138 checkpoint.total_reviewer_passes = context.total_reviewer_passes;
139 checkpoint.actual_developer_runs = context.run_context.actual_developer_runs;
140 checkpoint.actual_reviewer_runs = context.run_context.actual_reviewer_runs;
141 checkpoint.execution_history = Some(context.execution_history.clone());
142 checkpoint.prompt_history = Some(context.prompt_history.clone());
143 save_checkpoint_with_workspace(&*context.workspace, &checkpoint)?;
144 } else {
145 eprintln!("Note: Interrupted before first checkpoint. Minimal state saved.");
149 }
150
151 Ok(())
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::workspace::MemoryWorkspace;
158 use std::sync::Arc;
159
160 #[test]
161 fn test_interrupt_context_creation() {
162 let workspace = Arc::new(MemoryWorkspace::new_test());
163 let context = InterruptContext {
164 phase: crate::checkpoint::PipelinePhase::Development,
165 iteration: 2,
166 total_iterations: 5,
167 reviewer_pass: 0,
168 total_reviewer_passes: 2,
169 run_context: crate::checkpoint::RunContext::new(),
170 execution_history: crate::checkpoint::ExecutionHistory::new(),
171 prompt_history: std::collections::HashMap::new(),
172 workspace,
173 };
174
175 assert_eq!(context.phase, crate::checkpoint::PipelinePhase::Development);
176 assert_eq!(context.iteration, 2);
177 assert_eq!(context.total_iterations, 5);
178 }
179
180 #[test]
181 fn test_set_and_clear_interrupt_context() {
182 let workspace = Arc::new(MemoryWorkspace::new_test());
183 let context = InterruptContext {
184 phase: crate::checkpoint::PipelinePhase::Planning,
185 iteration: 1,
186 total_iterations: 3,
187 reviewer_pass: 0,
188 total_reviewer_passes: 1,
189 run_context: crate::checkpoint::RunContext::new(),
190 execution_history: crate::checkpoint::ExecutionHistory::new(),
191 prompt_history: std::collections::HashMap::new(),
192 workspace,
193 };
194
195 set_interrupt_context(context);
196 {
197 let ctx = INTERRUPT_CONTEXT.lock().unwrap();
198 assert!(ctx.is_some());
199 assert_eq!(
200 ctx.as_ref().unwrap().phase,
201 crate::checkpoint::PipelinePhase::Planning
202 );
203 }
204
205 clear_interrupt_context();
206 let ctx = INTERRUPT_CONTEXT.lock().unwrap();
207 assert!(ctx.is_none());
208 }
209}