agentmux_srv/migrations/
m0023_native_memory_versions_backfill.rs1use std::sync::Arc;
37
38use crate::backend::storage::store::Store;
39
40use super::{Migration, MigrationContext, MigrationError, MigrationScope};
41
42pub struct M0023NativeMemoryVersionsBackfill;
43
44impl Migration for M0023NativeMemoryVersionsBackfill {
45 fn id(&self) -> &'static str { "0023_native_memory_versions_backfill" }
46 fn scope(&self) -> MigrationScope { MigrationScope::Global }
47 fn description(&self) -> &'static str {
48 "Backfill one agent_inferred version per existing native memory file"
49 }
50
51 fn up(&self, ctx: &MigrationContext) -> Result<(), MigrationError> {
52 if !ctx.shared_store_path.exists() {
53 return Ok(());
54 }
55 let store = Arc::new(
56 Store::open_shared(&ctx.shared_store_path)
57 .map_err(|e| MigrationError(format!("native_memory_versions_backfill: open shared store: {e}")))?,
58 );
59
60 let rows = store
61 .agent_native_memory_list_all_rows()
62 .map_err(|e| MigrationError(format!("native_memory_versions_backfill: list rows: {e}")))?;
63
64 let mut backfilled = 0usize;
65 let mut skipped_already_versioned = 0usize;
66 for row in rows {
67 let existing = store
68 .agent_native_memory_version_latest(&row.agent_id, &row.filename)
69 .map_err(|e| {
70 MigrationError(format!(
71 "native_memory_versions_backfill: check existing for {}/{}: {e}",
72 row.agent_id, row.filename
73 ))
74 })?;
75 if existing.is_some() {
76 skipped_already_versioned += 1;
77 continue;
78 }
79 store
80 .agent_native_memory_version_insert(
81 &row.agent_id,
82 &row.filename,
83 &row.content,
84 "agent_inferred",
85 "{}",
86 "",
87 )
88 .map_err(|e| {
89 MigrationError(format!(
90 "native_memory_versions_backfill: insert for {}/{}: {e}",
91 row.agent_id, row.filename
92 ))
93 })?;
94 backfilled += 1;
95 }
96
97 tracing::info!(
98 backfilled,
99 skipped_already_versioned,
100 "native_memory_versions_backfill: complete"
101 );
102 Ok(())
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn ctx_for(shared_store_path: std::path::PathBuf) -> MigrationContext {
111 MigrationContext {
112 home: std::env::temp_dir(),
113 data_dir: std::env::temp_dir(),
114 shared_store_path,
115 channel_store_path: std::env::temp_dir().join("unused-objects.db"),
116 }
117 }
118
119 #[test]
120 fn backfills_a_version_for_an_existing_mirror_row() {
121 let tmp = tempfile::NamedTempFile::new().unwrap();
122 let store = Store::open_shared(tmp.path()).unwrap();
123 store
124 .agent_native_memory_upsert("agent-1", "MEMORY.md", "pre-upgrade content", None, "/some/path", 20, 1000)
125 .unwrap();
126
127 M0023NativeMemoryVersionsBackfill.up(&ctx_for(tmp.path().to_path_buf())).unwrap();
128
129 let versions = store.agent_native_memory_version_list("agent-1", "MEMORY.md").unwrap();
130 assert_eq!(versions.len(), 1);
131 assert_eq!(versions[0].source, "agent_inferred");
132
133 let latest = store.agent_native_memory_version_latest("agent-1", "MEMORY.md").unwrap().unwrap();
134 assert_eq!(latest.content, "pre-upgrade content");
135 }
136
137 #[test]
138 fn skips_a_pair_that_already_has_a_version() {
139 let tmp = tempfile::NamedTempFile::new().unwrap();
140 let store = Store::open_shared(tmp.path()).unwrap();
141 store
142 .agent_native_memory_upsert("agent-1", "MEMORY.md", "mirror content", None, "/some/path", 14, 1000)
143 .unwrap();
144 store
148 .agent_native_memory_version_insert("agent-1", "MEMORY.md", "already-recorded content", "human", "{}", "")
149 .unwrap();
150
151 M0023NativeMemoryVersionsBackfill.up(&ctx_for(tmp.path().to_path_buf())).unwrap();
152
153 let versions = store.agent_native_memory_version_list("agent-1", "MEMORY.md").unwrap();
154 assert_eq!(versions.len(), 1, "must not append a backfilled version onto an already-versioned pair");
155 }
156
157 #[test]
158 fn is_idempotent_on_a_second_run() {
159 let tmp = tempfile::NamedTempFile::new().unwrap();
160 let store = Store::open_shared(tmp.path()).unwrap();
161 store
162 .agent_native_memory_upsert("agent-1", "MEMORY.md", "content", None, "/some/path", 7, 1000)
163 .unwrap();
164
165 M0023NativeMemoryVersionsBackfill.up(&ctx_for(tmp.path().to_path_buf())).unwrap();
166 M0023NativeMemoryVersionsBackfill.up(&ctx_for(tmp.path().to_path_buf())).unwrap();
167
168 let versions = store.agent_native_memory_version_list("agent-1", "MEMORY.md").unwrap();
169 assert_eq!(versions.len(), 1, "second run must not duplicate the backfilled version");
170 }
171
172 #[test]
173 fn backfills_every_agent_and_file_independently() {
174 let tmp = tempfile::NamedTempFile::new().unwrap();
175 let store = Store::open_shared(tmp.path()).unwrap();
176 store.agent_native_memory_upsert("agent-1", "MEMORY.md", "a1", None, "/p", 2, 1000).unwrap();
177 store.agent_native_memory_upsert("agent-1", "topic.md", "a2", None, "/p", 2, 1000).unwrap();
178 store.agent_native_memory_upsert("agent-2", "MEMORY.md", "b1", None, "/p", 2, 1000).unwrap();
179
180 M0023NativeMemoryVersionsBackfill.up(&ctx_for(tmp.path().to_path_buf())).unwrap();
181
182 assert_eq!(store.agent_native_memory_version_list("agent-1", "MEMORY.md").unwrap().len(), 1);
183 assert_eq!(store.agent_native_memory_version_list("agent-1", "topic.md").unwrap().len(), 1);
184 assert_eq!(store.agent_native_memory_version_list("agent-2", "MEMORY.md").unwrap().len(), 1);
185 }
186
187 #[test]
188 fn missing_shared_store_is_a_noop() {
189 let ctx = ctx_for(std::path::Path::new("Z:/does/not/exist/store.db").to_path_buf());
190 M0023NativeMemoryVersionsBackfill.up(&ctx).unwrap();
191 }
192}