1use 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 = max_dev_continuations.saturating_add(1);
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 state = PipelineState::initial_with_continuation(
78 ctx.config.developer_iters,
79 ctx.config.reviewer_reviews,
80 &continuation,
81 );
82 let max_commit_residual_retries =
83 u8::try_from(ctx.config.max_commit_residual_retries.unwrap_or(10)).unwrap_or(u8::MAX);
84
85 let cloud = crate::config::CloudStateConfig::disabled();
89
90 PipelineState {
91 max_commit_residual_retries,
92 cloud,
93 ..state
94 }
95}
96
97pub fn overlay_checkpoint_progress_onto_base_state(
106 base_state: PipelineState,
107 migrated: PipelineState,
108 execution_history_limit: usize,
109) -> PipelineState {
110 let migrated_execution_history = migrated.execution_history().clone();
111
112 let cloud = base_state.cloud.clone();
113
114 let new_execution_history = base_state
115 .with_execution_history(migrated_execution_history, execution_history_limit)
116 .execution_history;
117
118 PipelineState {
119 phase: migrated.phase,
120 iteration: migrated.iteration,
121 total_iterations: migrated.total_iterations,
122 reviewer_pass: migrated.reviewer_pass,
123 total_reviewer_passes: migrated.total_reviewer_passes,
124 rebase: migrated.rebase,
125 execution_history: new_execution_history,
126 prompt_inputs: migrated.prompt_inputs,
127 prompt_permissions: migrated.prompt_permissions,
128 prompt_history: migrated.prompt_history,
129 metrics: migrated.metrics,
130 recovery_epoch: migrated.recovery_epoch,
131 recovery_escalation_level: migrated.recovery_escalation_level,
132 dev_fix_attempt_count: migrated.dev_fix_attempt_count,
133 failed_phase_for_recovery: migrated.failed_phase_for_recovery,
134 interrupted_by_user: migrated.interrupted_by_user,
135 pending_push_commit: migrated.pending_push_commit,
136 git_auth_configured: migrated.git_auth_configured,
137 pr_created: migrated.pr_created,
138 pr_url: migrated.pr_url,
139 pr_number: migrated.pr_number,
140 push_count: migrated.push_count,
141 push_retry_count: migrated.push_retry_count,
142 last_push_error: migrated.last_push_error,
143 unpushed_commits: migrated.unpushed_commits,
144 last_pushed_commit: migrated.last_pushed_commit,
145 cloud,
147 ..migrated
149 }
150}
151
152pub const MAX_EVENT_LOOP_ITERATIONS: usize = 1_000_000;
162
163#[cfg(test)]
164mod resume_overlay_tests {
165 use super::overlay_checkpoint_progress_onto_base_state;
166 use crate::config::{CloudStateConfig, GitAuthStateMethod, GitRemoteStateConfig};
167 use crate::reducer::event::PipelinePhase;
168 use crate::reducer::PipelineState;
169
170 #[test]
171 fn resume_overlay_restores_cloud_resume_fields_but_preserves_runtime_cloud() {
172 let base = PipelineState {
173 cloud: CloudStateConfig {
174 enabled: true,
175 api_url: None,
176 run_id: Some("run_from_env".to_string()),
177 heartbeat_interval_secs: 30,
178 graceful_degradation: true,
179 git_remote: GitRemoteStateConfig {
180 auth_method: GitAuthStateMethod::Token {
181 username: "x-access-token".to_string(),
182 },
183 push_branch: "env_branch".to_string(),
184 create_pr: true,
185 pr_title_template: None,
186 pr_body_template: None,
187 pr_base_branch: None,
188 force_push: false,
189 remote_name: "origin".to_string(),
190 },
191 },
192 ..PipelineState::initial(3, 2)
193 };
194
195 let migrated = PipelineState {
196 cloud: CloudStateConfig::disabled(),
197 pending_push_commit: Some("abc123".to_string()),
198 git_auth_configured: true,
199 pr_created: true,
200 pr_url: Some("https://example.com/pr/1".to_string()),
201 pr_number: Some(1),
202 push_count: 7,
203 push_retry_count: 2,
204 last_push_error: Some("push failed".to_string()),
205 unpushed_commits: vec!["deadbeef".to_string()],
206 last_pushed_commit: Some("beadfeed".to_string()),
207 ..PipelineState::initial(999, 999)
208 };
209
210 let base = overlay_checkpoint_progress_onto_base_state(base, migrated, 1000);
211
212 assert!(base.cloud.enabled);
214 assert_eq!(base.cloud.run_id.as_deref(), Some("run_from_env"));
215 assert_eq!(base.cloud.git_remote.push_branch.as_str(), "env_branch");
216
217 assert_eq!(base.pending_push_commit.as_deref(), Some("abc123"));
219 assert!(base.git_auth_configured);
220 assert!(base.pr_created);
221 assert_eq!(base.pr_url.as_deref(), Some("https://example.com/pr/1"));
222 assert_eq!(base.pr_number, Some(1));
223 assert_eq!(base.push_count, 7);
224 assert_eq!(base.push_retry_count, 2);
225 assert_eq!(base.last_push_error.as_deref(), Some("push failed"));
226 assert_eq!(base.unpushed_commits, vec!["deadbeef".to_string()]);
227 assert_eq!(base.last_pushed_commit.as_deref(), Some("beadfeed"));
228 }
229
230 #[test]
231 fn resume_overlay_restores_recovery_and_interrupt_fields() {
232 let base = PipelineState::initial(3, 2);
233
234 let migrated = PipelineState {
235 dev_fix_attempt_count: 42,
236 recovery_epoch: 7,
237 recovery_escalation_level: 3,
238 failed_phase_for_recovery: Some(PipelinePhase::Review),
239 interrupted_by_user: true,
240 ..PipelineState::initial(999, 999)
241 };
242
243 let base = overlay_checkpoint_progress_onto_base_state(base, migrated, 1000);
244
245 assert_eq!(base.dev_fix_attempt_count, 42);
246 assert_eq!(base.recovery_epoch, 7);
247 assert_eq!(base.recovery_escalation_level, 3);
248 assert_eq!(base.failed_phase_for_recovery, Some(PipelinePhase::Review));
249 assert!(
250 base.interrupted_by_user,
251 "interrupted_by_user must be restored from the migrated checkpoint state"
252 );
253 }
254}
255
256#[derive(Copy, Clone, Debug)]
258pub struct EventLoopConfig {
259 pub max_iterations: usize,
261}
262
263#[derive(Debug, Clone)]
265pub struct EventLoopResult {
266 pub completed: bool,
268 pub events_processed: usize,
270 pub final_phase: PipelinePhase,
272 pub final_state: PipelineState,
274}