agentmux_srv/backend/
registry_reconcile.rs1use crate::registry::{DefinitionStore, RecordScope, Registry};
33
34pub fn prune_tombstoned_instance_records(reg: &Registry, defs: &DefinitionStore) -> usize {
38 let records = match reg.list_active() {
39 Ok(v) => v,
40 Err(e) => {
41 tracing::warn!(error = %e, "registry reconcile: list_active failed — skipping prune");
42 return 0;
43 }
44 };
45 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
49 let mut removed = 0usize;
50 for rec in records {
51 let def_id = rec.data.definition_id;
52 if def_id.is_empty() || !seen.insert(def_id.clone()) {
53 continue;
54 }
55 if defs.exists(&def_id) || !defs.exists_anywhere(&def_id) {
58 continue;
59 }
60 match reg.hard_delete_for_agent(&def_id, RecordScope::Agent) {
61 Ok(n) => removed += n,
62 Err(e) => tracing::warn!(
63 definition_id = %def_id,
64 error = %e,
65 "registry reconcile: failed to drop records for a tombstoned definition"
66 ),
67 }
68 }
69 removed
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::registry::{
76 DefinitionRecord, DefinitionRecordV1, NamedAgentRecord, NamedAgentRecordV1,
77 DEF_MAX_SUPPORTED_SCHEMA, MAX_SUPPORTED_SCHEMA,
78 };
79
80 fn instance(instance_id: &str, definition_id: &str) -> NamedAgentRecord {
81 NamedAgentRecord {
82 schema_version: MAX_SUPPORTED_SCHEMA,
83 data: NamedAgentRecordV1 {
84 instance_id: instance_id.to_string(),
85 instance_name: "Maks".to_string(),
86 definition_id: definition_id.to_string(),
87 identity_id: None,
88 memory_id: None,
89 session_id: None,
90 working_dir: "maks".to_string(),
91 source_agents_base: None,
92 created_at_ms: 1,
93 last_launched_at_ms: 1,
94 created_by_version: "0.56.1".to_string(),
95 last_launched_by_version: "0.56.1".to_string(),
96 },
97 }
98 }
99
100 fn definition(id: &str) -> DefinitionRecord {
101 DefinitionRecord {
102 schema_version: DEF_MAX_SUPPORTED_SCHEMA,
103 data: DefinitionRecordV1 {
104 id: id.to_string(),
105 name: "Maks".to_string(),
106 ..Default::default()
107 },
108 }
109 }
110
111 fn trees() -> (tempfile::TempDir, Registry, DefinitionStore) {
112 let dir = tempfile::tempdir().unwrap();
113 let reg = Registry::open(dir.path().join("registry")).unwrap();
114 let defs = DefinitionStore::open(dir.path().join("definitions")).unwrap();
115 (dir, reg, defs)
116 }
117
118 #[test]
121 fn a_record_whose_definition_is_tombstoned_is_dropped() {
122 let (_dir, reg, defs) = trees();
123 defs.upsert(&definition("agent-1")).unwrap();
124 reg.upsert(&instance("legacy-launch-id", "agent-1")).unwrap();
125 defs.retire("agent-1").unwrap();
126
127 assert_eq!(prune_tombstoned_instance_records(®, &defs), 1);
128 assert!(reg.list_active().unwrap().is_empty());
129 }
130
131 #[test]
132 fn a_record_with_a_live_definition_is_kept() {
133 let (_dir, reg, defs) = trees();
134 defs.upsert(&definition("agent-1")).unwrap();
135 reg.upsert(&instance("legacy-launch-id", "agent-1")).unwrap();
136
137 assert_eq!(prune_tombstoned_instance_records(®, &defs), 0);
138 assert_eq!(reg.list_active().unwrap().len(), 1);
139 }
140
141 #[test]
145 fn a_record_whose_definition_is_merely_unknown_is_kept() {
146 let (_dir, reg, defs) = trees();
147 reg.upsert(&instance("some-id", "channel-local-only")).unwrap();
148
149 assert_eq!(prune_tombstoned_instance_records(®, &defs), 0);
150 assert_eq!(reg.list_active().unwrap().len(), 1);
151 }
152
153 #[test]
154 fn an_unretired_definition_keeps_its_records() {
155 let (_dir, reg, defs) = trees();
156 defs.upsert(&definition("agent-1")).unwrap();
157 reg.upsert(&instance("some-id", "agent-1")).unwrap();
158 defs.retire("agent-1").unwrap();
159 defs.unretire("agent-1").unwrap();
160
161 assert_eq!(prune_tombstoned_instance_records(®, &defs), 0);
162 assert_eq!(reg.list_active().unwrap().len(), 1);
163 }
164
165 #[test]
168 fn every_record_for_one_tombstoned_definition_goes_at_once() {
169 let (_dir, reg, defs) = trees();
170 defs.upsert(&definition("agent-1")).unwrap();
171 reg.upsert(&instance("agent-1", "agent-1")).unwrap();
172 reg.upsert(&instance("legacy-launch-id", "agent-1")).unwrap();
173 defs.retire("agent-1").unwrap();
174
175 assert_eq!(prune_tombstoned_instance_records(®, &defs), 2);
176 assert!(reg.list_active().unwrap().is_empty());
177 }
178
179 #[test]
180 fn running_twice_changes_nothing_the_second_time() {
181 let (_dir, reg, defs) = trees();
182 defs.upsert(&definition("agent-1")).unwrap();
183 reg.upsert(&instance("legacy-launch-id", "agent-1")).unwrap();
184 defs.retire("agent-1").unwrap();
185
186 assert_eq!(prune_tombstoned_instance_records(®, &defs), 1);
187 assert_eq!(prune_tombstoned_instance_records(®, &defs), 0);
188 }
189}