#[derive(Debug, PartialEq)]
pub(super) enum CheckpointLoadError {
InvalidJson(String),
MissingVersion,
UnsupportedVersionTooNew(u32),
LegacyVersion(u32),
}
impl std::fmt::Display for CheckpointLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidJson(msg) => write!(f, "checkpoint JSON parse error: {msg}"),
Self::MissingVersion => write!(
f,
"Invalid checkpoint format: missing or invalid version field. Supported versions: 2 (migrated) and 3 (current)."
),
Self::UnsupportedVersionTooNew(v) => write!(
f,
"Invalid checkpoint format: version {v} is newer than this binary supports. Supported versions: 2 (migrated) and 3 (current). Please upgrade Ralph Workflow to resume this checkpoint."
),
Self::LegacyVersion(v) => write!(
f,
"Invalid checkpoint format: version {v} is no longer supported (v1 and earlier). Supported versions: 2 (best-effort migration) and 3 (current). Legacy checkpoint formats are no longer supported. To start fresh without data loss: cp .agent/checkpoint.json .agent/checkpoint.backup.json && rm .agent/checkpoint.json"
),
}
}
}
fn load_checkpoint_with_fallback(
content: &str,
) -> Result<PipelineCheckpoint, CheckpointLoadError> {
let parsed_value: serde_json::Value = serde_json::from_str(content)
.map_err(|e| CheckpointLoadError::InvalidJson(e.to_string()))?;
let version = parsed_value
.get("version")
.and_then(|v| v.as_u64())
.ok_or(CheckpointLoadError::MissingVersion)? as u32;
if version == 2 {
let checkpoint: PipelineCheckpoint = serde_json::from_str(content)
.map_err(|e| CheckpointLoadError::InvalidJson(e.to_string()))?;
return Ok(PipelineCheckpoint { version: 3, ..checkpoint });
} else if version == 3 {
let checkpoint: PipelineCheckpoint = serde_json::from_str(content)
.map_err(|e| CheckpointLoadError::InvalidJson(e.to_string()))?;
return Ok(checkpoint);
} else if version > 3 {
return Err(CheckpointLoadError::UnsupportedVersionTooNew(version));
}
Err(CheckpointLoadError::LegacyVersion(version))
}
pub fn calculate_file_checksum_with_workspace(
workspace: &dyn Workspace,
path: &Path,
) -> Option<String> {
let content = workspace.read_bytes(path).ok()?;
Some(calculate_checksum_from_bytes(&content))
}
pub fn save_checkpoint_with_workspace(
workspace: &dyn Workspace,
checkpoint: &PipelineCheckpoint,
) -> io::Result<()> {
let json = serde_json::to_string(checkpoint).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to serialize checkpoint: {e}"),
)
})?;
workspace.create_dir_all(Path::new(AGENT_DIR))?;
workspace.write_atomic(Path::new(&checkpoint_path()), &json)
}
pub fn load_checkpoint_with_workspace(
workspace: &dyn Workspace,
) -> io::Result<Option<PipelineCheckpoint>> {
let checkpoint_path_str = checkpoint_path();
let checkpoint_file = Path::new(&checkpoint_path_str);
if !workspace.exists(checkpoint_file) {
return Ok(None);
}
let content = workspace.read(checkpoint_file)?;
let loaded_checkpoint = load_checkpoint_with_fallback(&content).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to parse checkpoint: {e}"),
)
})?;
Ok(Some(loaded_checkpoint))
}
pub fn clear_checkpoint_with_workspace(workspace: &dyn Workspace) -> io::Result<()> {
let checkpoint_path_str = checkpoint_path();
let checkpoint_file = Path::new(&checkpoint_path_str);
if workspace.exists(checkpoint_file) {
workspace.remove(checkpoint_file)?;
}
Ok(())
}
pub fn checkpoint_exists_with_workspace(workspace: &dyn Workspace) -> bool {
workspace.exists(Path::new(&checkpoint_path()))
}