1#![allow(clippy::pedantic)]
2
3pub mod admission;
4
5use std::sync::Arc;
6
7use chrono::Utc;
8
9use claw10_domain::{
10 AgentId, EvidenceId, Memory, MemoryId, MemorySource, MemoryStatus, MemoryType, TaskId,
11};
12use claw10_store::{Store, StoreError, StoreExt};
13
14pub use admission::{AdmissionConfig, AdmissionPipeline, AdmissionResult};
15
16#[derive(Debug, thiserror::Error)]
17pub enum MemoryError {
18 #[error("memory not found: {0}")]
19 NotFound(String),
20 #[error("invalid memory status transition: {from} → {to}")]
21 InvalidTransition {
22 from: MemoryStatus,
23 to: MemoryStatus,
24 },
25 #[error("{0}")]
26 Other(String),
27}
28
29impl From<StoreError> for MemoryError {
30 fn from(e: StoreError) -> Self {
31 Self::Other(e.to_string())
32 }
33}
34
35pub struct StoreMemoryInput {
37 pub tenant_id: String,
38 pub scope: String,
39 pub memory_type: MemoryType,
40 pub content: String,
41 pub source_agent: AgentId,
42 pub source_task: TaskId,
43 pub evidence_id: Option<EvidenceId>,
44 pub confidence: f64,
45 pub classification: String,
46}
47
48const KEY_PREFIX: &str = "memory:";
49
50pub struct MemoryService {
51 store: Arc<dyn Store>,
52 admission: AdmissionPipeline,
53}
54
55impl MemoryService {
56 #[must_use]
57 pub fn new(store: Arc<dyn Store>) -> Self {
58 Self::with_config(store, AdmissionConfig::default())
59 }
60
61 #[must_use]
62 pub fn with_config(store: Arc<dyn Store>, config: AdmissionConfig) -> Self {
63 Self {
64 store,
65 admission: AdmissionPipeline::new(config),
66 }
67 }
68
69 pub async fn store(&self, input: StoreMemoryInput) -> Memory {
75 let now = Utc::now();
76 let mut memory = Memory {
77 id: MemoryId(uuid::Uuid::now_v7()),
78 tenant_id: input.tenant_id,
79 scope: input.scope,
80 memory_type: input.memory_type,
81 content: input.content,
82 source: MemorySource {
83 agent_id: input.source_agent,
84 task_id: input.source_task,
85 evidence_id: input.evidence_id,
86 },
87 confidence: input.confidence,
88 classification: input.classification,
89 status: MemoryStatus::Candidate,
90 verified_by: vec![],
91 created_at: now,
92 updated_at: now,
93 };
94
95 let existing = self
97 .query(MemoryQuery {
98 status: Some(MemoryStatus::Active),
99 ..Default::default()
100 })
101 .await
102 .unwrap_or_default();
103
104 memory.status = match self.admission.evaluate(&memory, &existing) {
105 AdmissionResult::Activated => MemoryStatus::Active,
106 AdmissionResult::Rejected { reason } => {
107 tracing::debug!("memory {} rejected: {}", memory.id.0, reason);
108 MemoryStatus::Rejected
109 }
110 };
111 memory.updated_at = Utc::now();
112
113 let key = format!("{KEY_PREFIX}{}", memory.id.0);
114 self.store
115 .set(&key, &memory)
116 .await
117 .expect("MemoryService::store: store set failed");
118 memory
119 }
120
121 pub async fn get(&self, memory_id: &MemoryId) -> Result<Option<Memory>, MemoryError> {
123 let key = format!("{KEY_PREFIX}{}", memory_id.0);
124 Ok(self.store.get::<Memory>(&key).await?)
125 }
126
127 pub async fn update_content(
132 &self,
133 memory_id: &MemoryId,
134 new_content: String,
135 ) -> Result<(), MemoryError> {
136 let key = format!("{KEY_PREFIX}{}", memory_id.0);
137 let mut memory = self
138 .store
139 .get::<Memory>(&key)
140 .await?
141 .ok_or_else(|| MemoryError::NotFound(memory_id.0.to_string()))?;
142
143 memory.content = new_content;
144 memory.updated_at = Utc::now();
145
146 self.store.set(&key, &memory).await?;
147 Ok(())
148 }
149
150 pub async fn transition_status(
161 &self,
162 memory_id: &MemoryId,
163 new_status: MemoryStatus,
164 ) -> Result<(), MemoryError> {
165 let key = format!("{KEY_PREFIX}{}", memory_id.0);
166 let mut memory = self
167 .store
168 .get::<Memory>(&key)
169 .await?
170 .ok_or_else(|| MemoryError::NotFound(memory_id.0.to_string()))?;
171
172 if !Self::is_valid_transition(&memory.status, &new_status) {
173 return Err(MemoryError::InvalidTransition {
174 from: memory.status.clone(),
175 to: new_status,
176 });
177 }
178
179 memory.status = new_status;
180 memory.updated_at = Utc::now();
181
182 self.store.set(&key, &memory).await?;
183 Ok(())
184 }
185
186 #[must_use]
188 pub fn is_valid_transition(from: &MemoryStatus, to: &MemoryStatus) -> bool {
189 matches!(
190 (from, to),
191 (MemoryStatus::Candidate, MemoryStatus::Scanning)
192 | (MemoryStatus::Candidate, MemoryStatus::Rejected)
193 | (MemoryStatus::Scanning, MemoryStatus::Verified)
194 | (MemoryStatus::Scanning, MemoryStatus::Rejected)
195 | (MemoryStatus::Verified, MemoryStatus::Active)
196 | (MemoryStatus::Active, MemoryStatus::Expired)
197 | (MemoryStatus::Active, MemoryStatus::Rejected)
198 )
199 }
200
201 pub async fn verify(
206 &self,
207 memory_id: &MemoryId,
208 verifier: AgentId,
209 ) -> Result<(), MemoryError> {
210 let key = format!("{KEY_PREFIX}{}", memory_id.0);
211 let mut memory = self
212 .store
213 .get::<Memory>(&key)
214 .await?
215 .ok_or_else(|| MemoryError::NotFound(memory_id.0.to_string()))?;
216
217 if !memory.verified_by.contains(&verifier) {
218 memory.verified_by.push(verifier);
219 }
220 memory.updated_at = Utc::now();
221
222 self.store.set(&key, &memory).await?;
223 Ok(())
224 }
225
226 pub async fn query(&self, filter: MemoryQuery) -> Result<Vec<Memory>, MemoryError> {
228 let all: Vec<(String, Memory)> = self.store.scan_prefix(KEY_PREFIX).await?;
229 Ok(all
230 .into_iter()
231 .map(|(_, m)| m)
232 .filter(|m| {
233 if let Some(ref tenant) = filter.tenant_id && m.tenant_id != *tenant {
234 return false;
235 }
236 if let Some(ref scope) = filter.scope && m.scope != *scope {
237 return false;
238 }
239 if let Some(ref memory_type) = filter.memory_type && m.memory_type != *memory_type {
240 return false;
241 }
242 if let Some(ref status) = filter.status && m.status != *status {
243 return false;
244 }
245 if let Some(ref agent) = filter.source_agent && m.source.agent_id != *agent {
246 return false;
247 }
248 if let Some(min_confidence) = filter.min_confidence && m.confidence < min_confidence
249 {
250 return false;
251 }
252 true
253 })
254 .collect())
255 }
256
257 pub async fn delete(&self, memory_id: &MemoryId) -> Result<(), MemoryError> {
262 let key = format!("{KEY_PREFIX}{}", memory_id.0);
263 if !self.store.exists(&key).await? {
264 return Err(MemoryError::NotFound(memory_id.0.to_string()));
265 }
266 self.store.delete(&key).await?;
267 Ok(())
268 }
269
270 pub async fn count_by_status(&self) -> Result<std::collections::HashMap<String, usize>, MemoryError> {
272 let all: Vec<(String, Memory)> = self.store.scan_prefix(KEY_PREFIX).await?;
273 let mut counts = std::collections::HashMap::new();
274 for (_, memory) in all {
275 *counts.entry(format!("{:?}", memory.status)).or_insert(0) += 1;
276 }
277 Ok(counts)
278 }
279}
280
281#[derive(Debug, Default)]
283pub struct MemoryQuery {
284 pub tenant_id: Option<String>,
285 pub scope: Option<String>,
286 pub memory_type: Option<MemoryType>,
287 pub status: Option<MemoryStatus>,
288 pub source_agent: Option<AgentId>,
289 pub min_confidence: Option<f64>,
290}