#![allow(dead_code)]
use std::path::PathBuf;
use tokio::fs;
use super::agent_memory::{AgentMemoryScope, get_agent_memory_dir};
const SNAPSHOT_BASE: &str = "agent-memory-snapshots";
const SNAPSHOT_JSON: &str = "snapshot.json";
const SYNCED_JSON: &str = ".snapshot-synced.json";
#[derive(Debug, Clone, serde::Deserialize)]
struct SnapshotMeta {
#[serde(rename = "updatedAt")]
updated_at: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct SyncedMeta {
#[serde(rename = "syncedFrom")]
synced_from: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SnapshotAction {
None,
Initialize { snapshot_timestamp: String },
PromptUpdate { snapshot_timestamp: String },
}
fn get_snapshot_dir_for_agent(agent_type: &str) -> PathBuf {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(".claude")
.join(SNAPSHOT_BASE)
.join(agent_type)
}
fn get_snapshot_json_path(agent_type: &str) -> PathBuf {
get_snapshot_dir_for_agent(agent_type).join(SNAPSHOT_JSON)
}
fn get_synced_json_path(agent_type: &str, scope: AgentMemoryScope) -> PathBuf {
get_agent_memory_dir(agent_type, scope).join(SYNCED_JSON)
}
async fn read_json_file<T>(path: &PathBuf) -> Option<T>
where
T: serde::de::DeserializeOwned,
{
fs::read_to_string(path)
.await
.ok()
.and_then(|content| serde_json::from_str(&content).ok())
}
pub async fn check_agent_memory_snapshot(agent_type: &str, scope: AgentMemoryScope) -> SnapshotAction {
let snapshot_path = get_snapshot_json_path(agent_type);
let snapshot_meta: Option<SnapshotMeta> = read_json_file(&snapshot_path).await;
let Some(snapshot_meta) = snapshot_meta else {
return SnapshotAction::None;
};
let local_mem_dir = get_agent_memory_dir(agent_type, scope);
let has_local_memory = match fs::read_dir(&local_mem_dir).await {
Ok(mut entries) => {
let mut has_md = false;
while let Ok(Some(entry)) = entries.next_entry().await {
if entry.file_type().await.map(|ft| ft.is_file()).unwrap_or(false)
&& entry.file_name().to_string_lossy().ends_with(".md")
{
has_md = true;
break;
}
}
has_md
}
Err(_) => false,
};
if !has_local_memory {
return SnapshotAction::Initialize {
snapshot_timestamp: snapshot_meta.updated_at,
};
}
let synced_path = get_synced_json_path(agent_type, scope);
let synced_meta: Option<SyncedMeta> = read_json_file(&synced_path).await;
let snapshot_newer = synced_meta
.as_ref()
.map(|s| is_newer_timestamp(&snapshot_meta.updated_at, &s.synced_from))
.unwrap_or(true);
if snapshot_newer {
SnapshotAction::PromptUpdate {
snapshot_timestamp: snapshot_meta.updated_at,
}
} else {
SnapshotAction::None
}
}
pub async fn initialize_from_snapshot(
agent_type: &str,
scope: AgentMemoryScope,
snapshot_timestamp: &str,
) -> std::io::Result<()> {
log::debug!(
"Initializing agent memory for {} from project snapshot",
agent_type
);
copy_snapshot_to_local(agent_type, scope).await?;
save_synced_meta(agent_type, scope, snapshot_timestamp).await?;
Ok(())
}
pub async fn replace_from_snapshot(
agent_type: &str,
scope: AgentMemoryScope,
snapshot_timestamp: &str,
) -> std::io::Result<()> {
log::debug!(
"Replacing agent memory for {} with project snapshot",
agent_type
);
let local_mem_dir = get_agent_memory_dir(agent_type, scope);
if let Ok(mut entries) = fs::read_dir(&local_mem_dir).await {
while let Ok(Some(entry)) = entries.next_entry().await {
let path = entry.path();
if entry.file_type().await.map(|ft| ft.is_file()).unwrap_or(false)
&& entry.file_name().to_string_lossy().ends_with(".md")
{
let _ = fs::remove_file(&path).await;
}
}
}
copy_snapshot_to_local(agent_type, scope).await?;
save_synced_meta(agent_type, scope, snapshot_timestamp).await?;
Ok(())
}
pub async fn mark_snapshot_synced(
agent_type: &str,
scope: AgentMemoryScope,
snapshot_timestamp: &str,
) -> std::io::Result<()> {
save_synced_meta(agent_type, scope, snapshot_timestamp).await
}
async fn copy_snapshot_to_local(agent_type: &str, scope: AgentMemoryScope) -> std::io::Result<()> {
let snapshot_dir = get_snapshot_dir_for_agent(agent_type);
let local_dir = get_agent_memory_dir(agent_type, scope);
fs::create_dir_all(&local_dir).await?;
if let Ok(mut entries) = fs::read_dir(&snapshot_dir).await {
while let Ok(Some(entry)) = entries.next_entry().await {
let path = entry.path();
let name = entry.file_name();
if path.is_file() && name != SNAPSHOT_JSON {
let dest = local_dir.join(&name);
fs::copy(&path, &dest).await?;
}
}
}
Ok(())
}
async fn save_synced_meta(
agent_type: &str,
scope: AgentMemoryScope,
snapshot_timestamp: &str,
) -> std::io::Result<()> {
let synced_path = get_synced_json_path(agent_type, scope);
let local_dir = get_agent_memory_dir(agent_type, scope);
fs::create_dir_all(&local_dir).await?;
let meta = serde_json::json!({
"syncedFrom": snapshot_timestamp,
});
fs::write(&synced_path, serde_json::to_string_pretty(&meta)?).await
}
fn is_newer_timestamp(a: &str, b: &str) -> bool {
a > b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_newer_timestamp() {
assert!(is_newer_timestamp(
"2024-01-02T00:00:00Z",
"2024-01-01T00:00:00Z"
));
assert!(!is_newer_timestamp(
"2024-01-01T00:00:00Z",
"2024-01-02T00:00:00Z"
));
}
#[test]
fn test_snapshot_action_none_no_snapshot() {
let action = tokio::runtime::Runtime::new().unwrap().block_on(
check_agent_memory_snapshot("nonexistent", AgentMemoryScope::Local),
);
assert_eq!(action, SnapshotAction::None);
}
}