use crate::phases::PhaseContext;
use crate::reducer::event::PipelinePhase;
use crate::reducer::state::ContinuationState;
use crate::reducer::PipelineState;
pub fn create_initial_state_with_config(ctx: &PhaseContext<'_>) -> PipelineState {
debug_assert!(
ctx.config.max_dev_continuations.is_some(),
"BUG: max_dev_continuations is None when it should always have a value from config loading. \
This indicates config_from_unified() did not properly set the field, or Config was \
constructed directly without defaults."
);
debug_assert!(
ctx.config.max_xsd_retries.is_some(),
"BUG: max_xsd_retries is None when it should always have a value from config loading."
);
debug_assert!(
ctx.config.max_same_agent_retries.is_some(),
"BUG: max_same_agent_retries is None when it should always have a value from config loading."
);
debug_assert!(
ctx.config.max_commit_residual_retries.is_some(),
"BUG: max_commit_residual_retries is None when it should always have a value from config loading."
);
let max_dev_continuations = ctx.config.max_dev_continuations.unwrap_or(2);
let max_continue_count = max_dev_continuations.saturating_add(1);
if ctx.config.max_dev_continuations.is_none() {
debug_assert_eq!(
max_continue_count, 3,
"BUG: missing max_dev_continuations must default to 3 total attempts. Got: {max_continue_count}"
);
}
let continuation = ContinuationState::with_limits(
ctx.config.max_xsd_retries.unwrap_or(10),
max_continue_count,
ctx.config.max_same_agent_retries.unwrap_or(2),
);
let state = PipelineState::initial_with_continuation(
ctx.config.developer_iters,
ctx.config.reviewer_reviews,
&continuation,
);
let max_commit_residual_retries =
u8::try_from(ctx.config.max_commit_residual_retries.unwrap_or(10)).unwrap_or(u8::MAX);
let cloud = crate::config::CloudStateConfig::disabled();
PipelineState {
max_commit_residual_retries,
cloud,
..state
}
}
pub fn overlay_checkpoint_progress_onto_base_state(
base_state: PipelineState,
migrated: PipelineState,
execution_history_limit: usize,
) -> PipelineState {
let migrated_execution_history = migrated.execution_history().clone();
let cloud = base_state.cloud.clone();
let new_execution_history = base_state
.with_execution_history(migrated_execution_history, execution_history_limit)
.execution_history;
PipelineState {
phase: migrated.phase,
iteration: migrated.iteration,
total_iterations: migrated.total_iterations,
reviewer_pass: migrated.reviewer_pass,
total_reviewer_passes: migrated.total_reviewer_passes,
rebase: migrated.rebase,
execution_history: new_execution_history,
prompt_inputs: migrated.prompt_inputs,
prompt_permissions: migrated.prompt_permissions,
prompt_history: migrated.prompt_history,
metrics: migrated.metrics,
recovery_epoch: migrated.recovery_epoch,
recovery_escalation_level: migrated.recovery_escalation_level,
dev_fix_attempt_count: migrated.dev_fix_attempt_count,
failed_phase_for_recovery: migrated.failed_phase_for_recovery,
interrupted_by_user: migrated.interrupted_by_user,
pending_push_commit: migrated.pending_push_commit,
git_auth_configured: migrated.git_auth_configured,
pr_created: migrated.pr_created,
pr_url: migrated.pr_url,
pr_number: migrated.pr_number,
push_count: migrated.push_count,
push_retry_count: migrated.push_retry_count,
last_push_error: migrated.last_push_error,
unpushed_commits: migrated.unpushed_commits,
last_pushed_commit: migrated.last_pushed_commit,
cloud,
..migrated
}
}
pub const MAX_EVENT_LOOP_ITERATIONS: usize = 1_000_000;
#[cfg(test)]
mod resume_overlay_tests {
use super::overlay_checkpoint_progress_onto_base_state;
use crate::config::{CloudStateConfig, GitAuthStateMethod, GitRemoteStateConfig};
use crate::reducer::event::PipelinePhase;
use crate::reducer::PipelineState;
#[test]
fn resume_overlay_restores_cloud_resume_fields_but_preserves_runtime_cloud() {
let base = PipelineState {
cloud: CloudStateConfig {
enabled: true,
api_url: None,
run_id: Some("run_from_env".to_string()),
heartbeat_interval_secs: 30,
graceful_degradation: true,
git_remote: GitRemoteStateConfig {
auth_method: GitAuthStateMethod::Token {
username: "x-access-token".to_string(),
},
push_branch: "env_branch".to_string(),
create_pr: true,
pr_title_template: None,
pr_body_template: None,
pr_base_branch: None,
force_push: false,
remote_name: "origin".to_string(),
},
},
..PipelineState::initial(3, 2)
};
let migrated = PipelineState {
cloud: CloudStateConfig::disabled(),
pending_push_commit: Some("abc123".to_string()),
git_auth_configured: true,
pr_created: true,
pr_url: Some("https://example.com/pr/1".to_string()),
pr_number: Some(1),
push_count: 7,
push_retry_count: 2,
last_push_error: Some("push failed".to_string()),
unpushed_commits: vec!["deadbeef".to_string()],
last_pushed_commit: Some("beadfeed".to_string()),
..PipelineState::initial(999, 999)
};
let base = overlay_checkpoint_progress_onto_base_state(base, migrated, 1000);
assert!(base.cloud.enabled);
assert_eq!(base.cloud.run_id.as_deref(), Some("run_from_env"));
assert_eq!(base.cloud.git_remote.push_branch.as_str(), "env_branch");
assert_eq!(base.pending_push_commit.as_deref(), Some("abc123"));
assert!(base.git_auth_configured);
assert!(base.pr_created);
assert_eq!(base.pr_url.as_deref(), Some("https://example.com/pr/1"));
assert_eq!(base.pr_number, Some(1));
assert_eq!(base.push_count, 7);
assert_eq!(base.push_retry_count, 2);
assert_eq!(base.last_push_error.as_deref(), Some("push failed"));
assert_eq!(base.unpushed_commits, vec!["deadbeef".to_string()]);
assert_eq!(base.last_pushed_commit.as_deref(), Some("beadfeed"));
}
#[test]
fn resume_overlay_restores_recovery_and_interrupt_fields() {
let base = PipelineState::initial(3, 2);
let migrated = PipelineState {
dev_fix_attempt_count: 42,
recovery_epoch: 7,
recovery_escalation_level: 3,
failed_phase_for_recovery: Some(PipelinePhase::Review),
interrupted_by_user: true,
..PipelineState::initial(999, 999)
};
let base = overlay_checkpoint_progress_onto_base_state(base, migrated, 1000);
assert_eq!(base.dev_fix_attempt_count, 42);
assert_eq!(base.recovery_epoch, 7);
assert_eq!(base.recovery_escalation_level, 3);
assert_eq!(base.failed_phase_for_recovery, Some(PipelinePhase::Review));
assert!(
base.interrupted_by_user,
"interrupted_by_user must be restored from the migrated checkpoint state"
);
}
}
#[derive(Copy, Clone, Debug)]
pub struct EventLoopConfig {
pub max_iterations: usize,
}
#[derive(Debug, Clone)]
pub struct EventLoopResult {
pub completed: bool,
pub events_processed: usize,
pub final_phase: PipelinePhase,
pub final_state: PipelineState,
}