agentmux_srv/backend/
memory_history_import.rs1use std::path::PathBuf;
23use std::time::Instant;
24
25use crate::backend::memory_record::{self as record, ImportedVersion};
26use crate::backend::storage::agent_native_memory_versions::NativeMemoryVersion;
27use crate::backend::storage::error::StoreError;
28use crate::backend::storage::filestore::FileStore;
29use crate::backend::storage::store::Store;
30
31const PROVEN_SOURCES: &[&str] = &["agent", "armory-ui", "human", "revert", "jekt"];
33
34fn history_store_paths() -> Vec<PathBuf> {
38 let Some(shared) = crate::registry::resolve_global_shared_root() else {
39 return Vec::new();
40 };
41 history_store_paths_under(&shared)
42}
43
44fn history_store_paths_under(shared: &std::path::Path) -> Vec<PathBuf> {
45 let mut paths = vec![shared.join("store.db")];
46 if let Some(home) = shared.parent() {
47 let children = |dir: PathBuf| -> Vec<PathBuf> {
48 std::fs::read_dir(dir).map(|d| d.flatten().map(|e| e.path()).collect()).unwrap_or_default()
49 };
50 for channel in children(home.join("channels")) {
51 paths.push(channel.join("identity-store.db"));
52 }
53 for branch in children(home.join("dev")) {
56 paths.push(branch.join("identity-store.db"));
57 for instance in children(branch) {
58 paths.push(instance.join("identity-store.db"));
59 }
60 }
61 }
62 paths.retain(|p| p.is_file());
63 paths
64}
65
66fn gather(uid: &str, stores: &[PathBuf], deadline: Instant) -> Option<Vec<NativeMemoryVersion>> {
70 let mut rows = Vec::new();
71 for path in stores {
72 if Instant::now() > deadline {
73 return None;
74 }
75 let Ok(store) = Store::open_read_only(path) else { continue };
76 match store.agent_native_memory_versions_for_agent(uid) {
77 Ok(r) => rows.extend(r),
78 Err(e) => tracing::debug!(store = %path.display(), error = %e, "memory import: history unreadable; skipped"),
79 }
80 }
81 Some(rows)
82}
83
84fn select(rows: Vec<NativeMemoryVersion>, disk_sha: impl Fn(&str) -> Option<String>) -> Vec<ImportedVersion> {
87 rows.into_iter()
88 .filter(|r| crate::server::native_memory_handlers::validate_filename(&r.filename).is_ok())
89 .filter(|r| {
90 PROVEN_SOURCES.contains(&r.source.as_str())
91 || disk_sha(&r.filename).as_deref() == Some(record::sha256_hex(r.content.as_bytes()).as_str())
92 })
93 .map(|r| ImportedVersion {
94 file: r.filename,
95 body: r.content.into_bytes(),
96 source_detail: if r.source_detail.is_empty() {
97 format!("imported {}", r.id)
98 } else {
99 format!("imported {}; {}", r.id, r.source_detail)
100 },
101 source: r.source,
102 created_at_ms: r.created_at,
103 })
104 .collect()
105}
106
107pub(crate) fn import_once(
113 fs: &FileStore,
114 uid: &str,
115 stores: Option<&[PathBuf]>,
116 disk_sha: impl Fn(&str) -> Option<String>,
117 deadline: Instant,
118) -> Result<Option<usize>, StoreError> {
119 if record::heads(fs, uid)?.imported_at_ms.is_some() {
120 return Ok(None);
121 }
122 let found;
123 let stores = match stores {
124 Some(s) => s,
125 None => {
126 found = history_store_paths();
127 &found
128 }
129 };
130 let Some(rows) = gather(uid, stores, deadline) else {
131 return Ok(None);
132 };
133 record::import_history(fs, uid, &select(rows, disk_sha))
134}
135
136#[cfg(test)]
137mod tests;