ralph_workflow/app/event_loop/
config.rs1use crate::phases::PhaseContext;
7use crate::reducer::event::PipelinePhase;
8use crate::reducer::state::ContinuationState;
9use crate::reducer::PipelineState;
10
11pub fn create_initial_state_with_config(ctx: &PhaseContext<'_>) -> PipelineState {
17 debug_assert!(
29 ctx.config.max_dev_continuations.is_some(),
30 "BUG: max_dev_continuations is None when it should always have a value from config loading. \
31 This indicates config_from_unified() did not properly set the field, or Config was \
32 constructed directly without defaults."
33 );
34 debug_assert!(
35 ctx.config.max_xsd_retries.is_some(),
36 "BUG: max_xsd_retries is None when it should always have a value from config loading."
37 );
38 debug_assert!(
39 ctx.config.max_same_agent_retries.is_some(),
40 "BUG: max_same_agent_retries is None when it should always have a value from config loading."
41 );
42 debug_assert!(
43 ctx.config.max_commit_residual_retries.is_some(),
44 "BUG: max_commit_residual_retries is None when it should always have a value from config loading."
45 );
46
47 let max_dev_continuations = ctx.config.max_dev_continuations.unwrap_or(2);
61 let max_continue_count = 1 + max_dev_continuations;
62
63 if ctx.config.max_dev_continuations.is_none() {
66 debug_assert_eq!(
67 max_continue_count, 3,
68 "BUG: missing max_dev_continuations must default to 3 total attempts. Got: {max_continue_count}"
69 );
70 }
71
72 let continuation = ContinuationState::with_limits(
73 ctx.config.max_xsd_retries.unwrap_or(10),
74 max_continue_count,
75 ctx.config.max_same_agent_retries.unwrap_or(2),
76 );
77 let mut state = PipelineState::initial_with_continuation(
78 ctx.config.developer_iters,
79 ctx.config.reviewer_reviews,
80 &continuation,
81 );
82 state.max_commit_residual_retries =
83 u8::try_from(ctx.config.max_commit_residual_retries.unwrap_or(10)).unwrap_or(u8::MAX);
84
85 state.cloud = crate::config::CloudStateConfig::from(ctx.cloud);
89
90 state
91}
92
93pub fn overlay_checkpoint_progress_onto_base_state(
102 base_state: &mut PipelineState,
103 migrated: PipelineState,
104 execution_history_limit: usize,
105) {
106 let migrated_execution_history = migrated.execution_history().clone();
107
108 base_state.phase = migrated.phase;
109 base_state.iteration = migrated.iteration;
110 base_state.total_iterations = migrated.total_iterations;
111 base_state.reviewer_pass = migrated.reviewer_pass;
112 base_state.total_reviewer_passes = migrated.total_reviewer_passes;
113 base_state.rebase = migrated.rebase;
114 base_state
115 .replace_execution_history_bounded(migrated_execution_history, execution_history_limit);
116 base_state.prompt_inputs = migrated.prompt_inputs;
117 base_state.prompt_permissions = migrated.prompt_permissions;
118 base_state.prompt_history = migrated.prompt_history;
119 base_state.metrics = migrated.metrics;
120
121 base_state.recovery_epoch = migrated.recovery_epoch;
125 base_state.recovery_escalation_level = migrated.recovery_escalation_level;
126 base_state.dev_fix_attempt_count = migrated.dev_fix_attempt_count;
127 base_state.failed_phase_for_recovery = migrated.failed_phase_for_recovery;
128 base_state.interrupted_by_user = migrated.interrupted_by_user;
129
130 base_state.pending_push_commit = migrated.pending_push_commit;
133 base_state.git_auth_configured = migrated.git_auth_configured;
134 base_state.pr_created = migrated.pr_created;
135 base_state.pr_url = migrated.pr_url;
136 base_state.pr_number = migrated.pr_number;
137 base_state.push_count = migrated.push_count;
138 base_state.push_retry_count = migrated.push_retry_count;
139 base_state.last_push_error = migrated.last_push_error;
140 base_state.unpushed_commits = migrated.unpushed_commits;
141 base_state.last_pushed_commit = migrated.last_pushed_commit;
142}
143
144pub const MAX_EVENT_LOOP_ITERATIONS: usize = 1_000_000;
154
155#[cfg(test)]
156mod resume_overlay_tests {
157 use super::overlay_checkpoint_progress_onto_base_state;
158 use crate::config::{CloudStateConfig, GitAuthStateMethod, GitRemoteStateConfig};
159 use crate::reducer::event::PipelinePhase;
160 use crate::reducer::PipelineState;
161
162 #[test]
163 fn resume_overlay_restores_cloud_resume_fields_but_preserves_runtime_cloud() {
164 let mut base = PipelineState::initial(3, 2);
165 base.cloud = CloudStateConfig {
166 enabled: true,
167 api_url: None,
168 run_id: Some("run_from_env".to_string()),
169 heartbeat_interval_secs: 30,
170 graceful_degradation: true,
171 git_remote: GitRemoteStateConfig {
172 auth_method: GitAuthStateMethod::Token {
173 username: "x-access-token".to_string(),
174 },
175 push_branch: "env_branch".to_string(),
176 create_pr: true,
177 pr_title_template: None,
178 pr_body_template: None,
179 pr_base_branch: None,
180 force_push: false,
181 remote_name: "origin".to_string(),
182 },
183 };
184
185 let mut migrated = PipelineState::initial(999, 999);
186 migrated.cloud = CloudStateConfig::disabled();
187 migrated.pending_push_commit = Some("abc123".to_string());
188 migrated.git_auth_configured = true;
189 migrated.pr_created = true;
190 migrated.pr_url = Some("https://example.com/pr/1".to_string());
191 migrated.pr_number = Some(1);
192 migrated.push_count = 7;
193 migrated.push_retry_count = 2;
194 migrated.last_push_error = Some("push failed".to_string());
195 migrated.unpushed_commits = vec!["deadbeef".to_string()];
196 migrated.last_pushed_commit = Some("beadfeed".to_string());
197
198 overlay_checkpoint_progress_onto_base_state(&mut base, migrated, 1000);
199
200 assert!(base.cloud.enabled);
202 assert_eq!(base.cloud.run_id.as_deref(), Some("run_from_env"));
203 assert_eq!(base.cloud.git_remote.push_branch.as_str(), "env_branch");
204
205 assert_eq!(base.pending_push_commit.as_deref(), Some("abc123"));
207 assert!(base.git_auth_configured);
208 assert!(base.pr_created);
209 assert_eq!(base.pr_url.as_deref(), Some("https://example.com/pr/1"));
210 assert_eq!(base.pr_number, Some(1));
211 assert_eq!(base.push_count, 7);
212 assert_eq!(base.push_retry_count, 2);
213 assert_eq!(base.last_push_error.as_deref(), Some("push failed"));
214 assert_eq!(base.unpushed_commits, vec!["deadbeef".to_string()]);
215 assert_eq!(base.last_pushed_commit.as_deref(), Some("beadfeed"));
216 }
217
218 #[test]
219 fn resume_overlay_restores_recovery_and_interrupt_fields() {
220 let mut base = PipelineState::initial(3, 2);
221
222 let mut migrated = PipelineState::initial(999, 999);
223 migrated.dev_fix_attempt_count = 42;
224 migrated.recovery_epoch = 7;
225 migrated.recovery_escalation_level = 3;
226 migrated.failed_phase_for_recovery = Some(PipelinePhase::Review);
227 migrated.interrupted_by_user = true;
228
229 overlay_checkpoint_progress_onto_base_state(&mut base, migrated, 1000);
230
231 assert_eq!(base.dev_fix_attempt_count, 42);
232 assert_eq!(base.recovery_epoch, 7);
233 assert_eq!(base.recovery_escalation_level, 3);
234 assert_eq!(base.failed_phase_for_recovery, Some(PipelinePhase::Review));
235 assert!(
236 base.interrupted_by_user,
237 "interrupted_by_user must be restored from the migrated checkpoint state"
238 );
239 }
240}
241
242#[derive(Copy, Clone, Debug)]
244pub struct EventLoopConfig {
245 pub max_iterations: usize,
247}
248
249#[derive(Debug, Clone)]
251pub struct EventLoopResult {
252 pub completed: bool,
254 pub events_processed: usize,
256 pub final_phase: PipelinePhase,
258 pub final_state: PipelineState,
260}