telos_agent/knowledge/memory/index/
maintenance.rs1use std::collections::HashSet;
2
3use serde::Serialize;
4
5use crate::knowledge::memory::format::{MemoryCategory, MemoryEntry, MemoryStatus};
6use crate::knowledge::memory::index::MemoryStore;
7use crate::knowledge::memory::index::query::status_rank;
8
9#[derive(Debug, Clone)]
11pub struct MemoryMaintenancePolicy {
12 pub max_auto_learned_commands: Option<usize>,
14 pub max_active_entries: Option<usize>,
16 pub archive_deprecated: bool,
18}
19
20impl Default for MemoryMaintenancePolicy {
21 fn default() -> Self {
22 Self {
23 max_auto_learned_commands: Some(20),
24 max_active_entries: None,
25 archive_deprecated: true,
26 }
27 }
28}
29
30#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
31#[serde(rename_all = "snake_case")]
32pub enum MemoryMaintenanceActionKind {
33 Archive,
34}
35
36#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
37pub struct MemoryMaintenanceAction {
38 pub name: String,
39 pub action: MemoryMaintenanceActionKind,
40 pub reason: String,
41}
42
43#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
44pub struct MemoryMaintenanceReport {
45 pub applied: bool,
46 pub active_before: usize,
47 pub archived_count: usize,
48 pub actions: Vec<MemoryMaintenanceAction>,
49}
50
51impl MemoryStore {
52 pub fn maintenance_report(&self, policy: &MemoryMaintenancePolicy) -> MemoryMaintenanceReport {
54 let mut entries: Vec<MemoryEntry> =
55 self.index.keys().filter_map(|name| self.read(name)).collect();
56 entries.sort_by(|a, b| a.name.cmp(&b.name));
57
58 let mut actions = Vec::new();
59 let mut selected = HashSet::new();
60
61 if policy.archive_deprecated {
62 for entry in entries.iter().filter(|entry| entry.status == MemoryStatus::Deprecated) {
63 push_archive_action(&mut actions, &mut selected, entry, "status is deprecated");
64 }
65 }
66
67 if let Some(max) = policy.max_auto_learned_commands {
68 let mut commands: Vec<&MemoryEntry> =
69 entries.iter().filter(|entry| is_auto_learned_command(entry)).collect();
70 if commands.len() > max {
71 commands.sort_by(|a, b| {
72 a.times_used
73 .cmp(&b.times_used)
74 .then_with(|| {
75 updated_sort_value(&a.updated).cmp(&updated_sort_value(&b.updated))
76 })
77 .then_with(|| a.updated.cmp(&b.updated))
78 .then_with(|| a.name.cmp(&b.name))
79 });
80 let overflow = commands.len().saturating_sub(max);
81 for entry in commands.into_iter().take(overflow) {
82 push_archive_action(
83 &mut actions,
84 &mut selected,
85 entry,
86 "exceeds auto-learned command retention limit",
87 );
88 }
89 }
90 }
91
92 if let Some(max) = policy.max_active_entries {
93 let remaining_active = entries.len().saturating_sub(selected.len());
94 if remaining_active > max {
95 let mut candidates: Vec<&MemoryEntry> =
96 entries.iter().filter(|entry| !selected.contains(&entry.name)).collect();
97 candidates.sort_by(|a, b| {
98 status_rank(&b.status)
99 .cmp(&status_rank(&a.status))
100 .then_with(|| a.times_used.cmp(&b.times_used))
101 .then_with(|| {
102 updated_sort_value(&a.updated).cmp(&updated_sort_value(&b.updated))
103 })
104 .then_with(|| a.updated.cmp(&b.updated))
105 .then_with(|| a.name.cmp(&b.name))
106 });
107 for entry in candidates.into_iter().take(remaining_active - max) {
108 push_archive_action(
109 &mut actions,
110 &mut selected,
111 entry,
112 "exceeds active memory retention limit",
113 );
114 }
115 }
116 }
117
118 MemoryMaintenanceReport {
119 applied: false,
120 active_before: entries.len(),
121 archived_count: 0,
122 actions,
123 }
124 }
125
126 pub fn apply_maintenance(
128 &mut self,
129 policy: &MemoryMaintenancePolicy,
130 ) -> std::io::Result<MemoryMaintenanceReport> {
131 let mut report = self.maintenance_report(policy);
132 let actions = report.actions.clone();
133 let mut archived = 0;
134 for action in actions {
135 match action.action {
136 MemoryMaintenanceActionKind::Archive => {
137 self.archive(&action.name)?;
138 archived += 1;
139 }
140 }
141 }
142 report.applied = true;
143 report.archived_count = archived;
144 Ok(report)
145 }
146}
147
148fn push_archive_action(
149 actions: &mut Vec<MemoryMaintenanceAction>,
150 selected: &mut HashSet<String>,
151 entry: &MemoryEntry,
152 reason: &str,
153) {
154 if selected.insert(entry.name.clone()) {
155 actions.push(MemoryMaintenanceAction {
156 name: entry.name.clone(),
157 action: MemoryMaintenanceActionKind::Archive,
158 reason: reason.to_string(),
159 });
160 }
161}
162
163fn is_auto_learned_command(entry: &MemoryEntry) -> bool {
164 entry.category == MemoryCategory::Command
165 && entry.status == MemoryStatus::Working
166 && entry.tags.iter().any(|tag| tag.eq_ignore_ascii_case("auto-learned"))
167}
168
169fn updated_sort_value(value: &str) -> u64 {
170 value.parse().unwrap_or(0)
171}