Skip to main content

codetether_agent/session/
checkpoint_store.rs

1//! Sidecar persistence for resumable run checkpoints.
2
3use super::checkpoint::RunCheckpoint;
4use super::types::Session;
5use anyhow::Result;
6use std::path::PathBuf;
7use tokio::fs;
8
9impl Session {
10    pub async fn save_run_checkpoint(&mut self, checkpoint: RunCheckpoint) -> Result<PathBuf> {
11        let path = Self::checkpoint_path(&self.id)?;
12        if let Some(parent) = path.parent() {
13            fs::create_dir_all(parent).await?;
14        }
15        self.metadata.run_checkpoint = Some(checkpoint.clone());
16        fs::write(&path, serde_json::to_vec_pretty(&checkpoint)?).await?;
17        self.save().await?;
18        Ok(path)
19    }
20
21    pub async fn load_run_checkpoint(&self) -> Result<Option<RunCheckpoint>> {
22        if let Some(checkpoint) = &self.metadata.run_checkpoint {
23            return Ok(Some(checkpoint.clone()));
24        }
25        let path = Self::checkpoint_path(&self.id)?;
26        match fs::read_to_string(path).await {
27            Ok(raw) => Ok(Some(serde_json::from_str(&raw)?)),
28            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
29            Err(err) => Err(err.into()),
30        }
31    }
32
33    pub async fn clear_run_checkpoint(&mut self) -> Result<()> {
34        self.metadata.run_checkpoint = None;
35        let path = Self::checkpoint_path(&self.id)?;
36        match fs::remove_file(path).await {
37            Ok(()) => {}
38            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
39            Err(err) => return Err(err.into()),
40        }
41        self.save().await
42    }
43
44    fn checkpoint_path(id: &str) -> Result<PathBuf> {
45        if id.is_empty()
46            || id.len() > 128
47            || id.contains(|c: char| !c.is_alphanumeric() && c != '-' && c != '_')
48        {
49            anyhow::bail!("Invalid session ID: rejecting checkpoint path traversal risk");
50        }
51        Ok(Self::sessions_dir()?.join(format!("{id}.checkpoint.json")))
52    }
53}