codetether_agent/session/
checkpoint_store.rs1use 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}