1use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5use std::time::Duration;
6
7use jiff::{SignedDuration, Timestamp};
8use okbot_core::{
9 AgentRunRecord, AutomationRun, ChannelId, Clock, ConversationKey, ConversationStore, Fire,
10 FireState, NewMessage, PAIRING_MAX_FAILURES, PeerTaskRecord, PeerTaskState, RoleGrant,
11 SealedEntry, StoreError, StoredMessage, ToolCallRecord, Usage, UserId, async_trait,
12};
13
14#[derive(Default)]
15struct State {
16 seen: HashMap<(ChannelId, String), Timestamp>,
17 messages: Vec<(u64, NewMessage)>,
18 reset_after: HashMap<ConversationKey, u64>,
19 sessions: HashMap<ConversationKey, (String, String)>,
20 runs: Vec<AgentRunRecord>,
21 next: u64,
22 owners: Vec<(ChannelId, UserId)>,
23 codes: HashMap<String, (Timestamp, u32)>,
25 fires: HashMap<(String, Timestamp), Fire>,
26 direct: HashMap<(ChannelId, UserId), ConversationKey>,
27 tool_calls: Vec<ToolCallRecord>,
28 grants: HashMap<(String, String), RoleGrant>,
29 sealed: HashMap<(String, String, String), (Vec<u8>, Timestamp)>,
31 items: HashMap<(String, String), Timestamp>,
33 automation_runs: Vec<AutomationRun>,
34 peer_tasks: HashMap<(bool, String), PeerTaskRecord>,
36}
37
38pub struct MemoryStore {
40 state: Mutex<State>,
41 clock: Arc<dyn Clock>,
42}
43
44impl std::fmt::Debug for MemoryStore {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("MemoryStore").finish_non_exhaustive()
47 }
48}
49
50impl MemoryStore {
51 pub fn new(clock: Arc<dyn Clock>) -> Self {
53 Self {
54 state: Mutex::new(State::default()),
55 clock,
56 }
57 }
58
59 fn lock(&self) -> std::sync::MutexGuard<'_, State> {
60 self.state
61 .lock()
62 .unwrap_or_else(std::sync::PoisonError::into_inner)
63 }
64
65 pub fn runs(&self) -> Vec<AgentRunRecord> {
67 self.lock().runs.clone()
68 }
69
70 pub fn recorded_tool_calls(&self) -> Vec<ToolCallRecord> {
72 self.lock().tool_calls.clone()
73 }
74}
75
76fn signed(d: Duration) -> SignedDuration {
77 SignedDuration::try_from(d).unwrap_or(SignedDuration::MAX)
78}
79
80#[async_trait]
81impl ConversationStore for MemoryStore {
82 async fn mark_seen(
83 &self,
84 channel: &ChannelId,
85 event_id: &str,
86 ttl: Duration,
87 ) -> Result<bool, StoreError> {
88 let now = self.clock.now();
89 let mut state = self.lock();
90 let key = (channel.clone(), event_id.to_string());
91 if state.seen.get(&key).is_some_and(|expires| *expires > now) {
92 return Ok(false);
93 }
94 state
95 .seen
96 .insert(key, now.saturating_add(signed(ttl)).unwrap_or(now));
97 Ok(true)
98 }
99
100 async fn append(&self, message: NewMessage) -> Result<(), StoreError> {
101 let mut state = self.lock();
102 state.next += 1;
103 let id = state.next;
104 state.messages.push((id, message));
105 Ok(())
106 }
107
108 async fn recent(
109 &self,
110 conversation: &ConversationKey,
111 limit: usize,
112 ) -> Result<Vec<StoredMessage>, StoreError> {
113 let state = self.lock();
114 let after = state.reset_after.get(conversation).copied().unwrap_or(0);
115 let mut out: Vec<StoredMessage> = state
116 .messages
117 .iter()
118 .rev()
119 .filter(|(id, m)| *id > after && m.conversation == *conversation)
120 .take(limit)
121 .map(|(_, m)| StoredMessage {
122 role: m.role,
123 author_name: m.author_name.clone(),
124 text: m.text.clone(),
125 at: m.at,
126 })
127 .collect();
128 out.reverse();
129 Ok(out)
130 }
131
132 async fn reset(&self, conversation: &ConversationKey) -> Result<(), StoreError> {
133 let mut state = self.lock();
134 let last = state.next;
135 state.reset_after.insert(conversation.clone(), last);
136 state.sessions.remove(conversation);
137 Ok(())
138 }
139
140 async fn session(
141 &self,
142 conversation: &ConversationKey,
143 ) -> Result<Option<(String, String)>, StoreError> {
144 Ok(self.lock().sessions.get(conversation).cloned())
145 }
146
147 async fn set_session(
148 &self,
149 conversation: &ConversationKey,
150 backend: &str,
151 session: &str,
152 ) -> Result<(), StoreError> {
153 self.lock().sessions.insert(
154 conversation.clone(),
155 (backend.to_string(), session.to_string()),
156 );
157 Ok(())
158 }
159
160 async fn owners(&self) -> Result<Vec<(ChannelId, UserId)>, StoreError> {
161 let mut owners = self.lock().owners.clone();
162 owners.sort();
163 Ok(owners)
164 }
165
166 async fn remove_owner(&self, channel: &ChannelId, user: &UserId) -> Result<bool, StoreError> {
167 let mut state = self.lock();
168 let before = state.owners.len();
169 state.owners.retain(|(c, u)| c != channel || u != user);
170 Ok(state.owners.len() != before)
171 }
172
173 async fn create_pairing_code(&self, code: &str, ttl: Duration) -> Result<(), StoreError> {
174 let expires = self.clock.now() + signed(ttl);
175 self.lock().codes.insert(code.to_string(), (expires, 0));
176 Ok(())
177 }
178
179 async fn redeem_pairing_code(
180 &self,
181 code: &str,
182 channel: &ChannelId,
183 user: &UserId,
184 ) -> Result<bool, StoreError> {
185 let now = self.clock.now();
186 let mut state = self.lock();
187 state.codes.retain(|_, (expires, _)| *expires > now);
188 if state.codes.remove(code).is_some() {
189 let account = (channel.clone(), user.clone());
190 if !state.owners.contains(&account) {
191 state.owners.push(account);
192 }
193 return Ok(true);
194 }
195 for (_, failures) in state.codes.values_mut() {
196 *failures += 1;
197 }
198 state
199 .codes
200 .retain(|_, (_, failures)| *failures < PAIRING_MAX_FAILURES);
201 Ok(false)
202 }
203
204 async fn record_run(&self, run: AgentRunRecord) -> Result<(), StoreError> {
205 self.lock().runs.push(run);
206 Ok(())
207 }
208
209 async fn claim_fire(&self, task_id: &str, fire_at: Timestamp) -> Result<Fire, StoreError> {
210 let now = self.clock.now();
211 Ok(self
212 .lock()
213 .fires
214 .entry((task_id.to_string(), fire_at))
215 .or_insert_with(|| Fire {
216 task_id: task_id.to_string(),
217 fire_at,
218 state: FireState::Sending,
219 attempts: 0,
220 updated_at: now,
221 })
222 .clone())
223 }
224
225 async fn set_fire(
226 &self,
227 task_id: &str,
228 fire_at: Timestamp,
229 state: FireState,
230 attempts: u32,
231 ) -> Result<(), StoreError> {
232 let fire = Fire {
233 task_id: task_id.to_string(),
234 fire_at,
235 state,
236 attempts,
237 updated_at: self.clock.now(),
238 };
239 self.lock()
240 .fires
241 .insert((task_id.to_string(), fire_at), fire);
242 Ok(())
243 }
244
245 async fn recent_fires(&self, limit: usize) -> Result<Vec<Fire>, StoreError> {
246 let mut fires: Vec<Fire> = self.lock().fires.values().cloned().collect();
247 fires.sort_by_key(|f| std::cmp::Reverse((f.updated_at, f.fire_at)));
248 fires.truncate(limit);
249 Ok(fires)
250 }
251
252 async fn set_direct_chat(
253 &self,
254 channel: &ChannelId,
255 user: &UserId,
256 conversation: &ConversationKey,
257 ) -> Result<(), StoreError> {
258 self.lock()
259 .direct
260 .insert((channel.clone(), user.clone()), conversation.clone());
261 Ok(())
262 }
263
264 async fn direct_chat(
265 &self,
266 channel: &ChannelId,
267 user: &UserId,
268 ) -> Result<Option<ConversationKey>, StoreError> {
269 Ok(self
270 .lock()
271 .direct
272 .get(&(channel.clone(), user.clone()))
273 .cloned())
274 }
275
276 async fn usage_since(&self, person: &str, since: Timestamp) -> Result<Usage, StoreError> {
277 let state = self.lock();
278 let mut usage = Usage::default();
279 for run in state
280 .runs
281 .iter()
282 .filter(|r| r.person.as_deref() == Some(person) && r.started_at >= since)
283 {
284 usage.turns += 1;
285 usage.tokens += run.usage.map_or(0, |u| u.input + u.output);
286 }
287 Ok(usage)
288 }
289
290 async fn record_tool_call(&self, call: ToolCallRecord) -> Result<(), StoreError> {
291 self.lock().tool_calls.push(call);
292 Ok(())
293 }
294
295 async fn tool_calls(
296 &self,
297 since: Timestamp,
298 limit: usize,
299 ) -> Result<Vec<ToolCallRecord>, StoreError> {
300 Ok(self
301 .lock()
302 .tool_calls
303 .iter()
304 .rev()
305 .filter(|c| c.at >= since)
306 .take(limit)
307 .cloned()
308 .collect())
309 }
310
311 async fn grant_role(&self, grant: RoleGrant) -> Result<(), StoreError> {
312 self.lock()
313 .grants
314 .insert((grant.role.clone(), grant.account.clone()), grant);
315 Ok(())
316 }
317
318 async fn revoke_role(&self, role: &str, account: &str) -> Result<bool, StoreError> {
319 let now = self.clock.now();
320 Ok(self
321 .lock()
322 .grants
323 .remove(&(role.to_string(), account.to_string()))
324 .is_some_and(|g| g.expires_at > now))
325 }
326
327 async fn role_grants(&self, now: Timestamp) -> Result<Vec<RoleGrant>, StoreError> {
328 let mut grants: Vec<RoleGrant> = self
329 .lock()
330 .grants
331 .values()
332 .filter(|g| g.expires_at > now)
333 .cloned()
334 .collect();
335 grants.sort_by(|a, b| (&a.role, &a.account).cmp(&(&b.role, &b.account)));
336 Ok(grants)
337 }
338
339 async fn purge_before(&self, before: Timestamp) -> Result<u64, StoreError> {
340 let now = self.clock.now();
341 let mut state = self.lock();
342 let count = |s: &State| {
343 s.messages.len()
344 + s.runs.len()
345 + s.seen.len()
346 + s.fires.len()
347 + s.tool_calls.len()
348 + s.grants.len()
349 + s.items.len()
350 + s.automation_runs.len()
351 + s.peer_tasks.len()
352 };
353 let start = count(&state);
354 state.messages.retain(|(_, m)| m.at >= before);
355 state.runs.retain(|r| r.started_at >= before);
356 state.tool_calls.retain(|c| c.at >= before);
357 state.grants.retain(|_, g| g.expires_at > now);
358 state.items.retain(|_, seen| *seen >= before);
359 state.automation_runs.retain(|r| r.at >= before);
360 state.peer_tasks.retain(|_, t| t.updated_at >= before);
361 state.seen.retain(|_, expires| *expires > now);
362 state
363 .fires
364 .retain(|_, f| f.updated_at >= before || f.state == FireState::Sending);
365 let end = count(&state);
366 Ok((start - end) as u64)
367 }
368
369 async fn sealed(
370 &self,
371 kind: &str,
372 server: &str,
373 subject: &str,
374 ) -> Result<Option<Vec<u8>>, StoreError> {
375 let key = (kind.to_string(), server.to_string(), subject.to_string());
376 Ok(self.lock().sealed.get(&key).map(|(v, _)| v.clone()))
377 }
378
379 async fn set_sealed(
380 &self,
381 kind: &str,
382 server: &str,
383 subject: &str,
384 sealed: Vec<u8>,
385 ) -> Result<(), StoreError> {
386 let key = (kind.to_string(), server.to_string(), subject.to_string());
387 let now = self.clock.now();
388 self.lock().sealed.insert(key, (sealed, now));
389 Ok(())
390 }
391
392 async fn delete_sealed(
393 &self,
394 kind: &str,
395 server: &str,
396 subject: &str,
397 ) -> Result<bool, StoreError> {
398 let key = (kind.to_string(), server.to_string(), subject.to_string());
399 Ok(self.lock().sealed.remove(&key).is_some())
400 }
401
402 async fn sealed_entries(&self, kind: &str) -> Result<Vec<SealedEntry>, StoreError> {
403 let mut out: Vec<SealedEntry> = self
404 .lock()
405 .sealed
406 .iter()
407 .filter(|((k, _, _), _)| k == kind)
408 .map(|((k, server, subject), (_, at))| SealedEntry {
409 kind: k.clone(),
410 server: server.clone(),
411 subject: subject.clone(),
412 updated_at: *at,
413 })
414 .collect();
415 out.sort_by(|a, b| (&a.server, &a.subject).cmp(&(&b.server, &b.subject)));
416 Ok(out)
417 }
418
419 async fn claim_items(
420 &self,
421 automation: &str,
422 items: Vec<String>,
423 ) -> Result<Vec<String>, StoreError> {
424 let now = self.clock.now();
425 let mut state = self.lock();
426 let mut new = Vec::new();
427 for item in items {
428 let fresh = state
429 .items
430 .insert((automation.to_string(), item.clone()), now)
431 .is_none();
432 if fresh {
433 new.push(item);
434 }
435 }
436 Ok(new)
437 }
438
439 async fn release_items(&self, automation: &str, items: Vec<String>) -> Result<(), StoreError> {
440 let mut state = self.lock();
441 for item in items {
442 state.items.remove(&(automation.to_string(), item));
443 }
444 Ok(())
445 }
446
447 async fn record_automation_run(&self, run: AutomationRun) -> Result<(), StoreError> {
448 self.lock().automation_runs.push(run);
449 Ok(())
450 }
451
452 async fn automation_runs(
453 &self,
454 automation: Option<&str>,
455 since: Timestamp,
456 limit: usize,
457 ) -> Result<Vec<AutomationRun>, StoreError> {
458 Ok(self
459 .lock()
460 .automation_runs
461 .iter()
462 .rev()
463 .filter(|r| r.at >= since && automation.is_none_or(|a| r.automation == a))
464 .take(limit)
465 .cloned()
466 .collect())
467 }
468
469 async fn put_peer_task(&self, task: PeerTaskRecord) -> Result<(), StoreError> {
470 let mut state = self.lock();
471 let key = (task.inbound, task.id.clone());
472 let duplicate = state.peer_tasks.values().any(|t| {
473 t.inbound == task.inbound
474 && t.team == task.team
475 && t.peer == task.peer
476 && t.message_id == task.message_id
477 && t.id != task.id
478 });
479 if duplicate {
480 return Err(StoreError::Backend("duplicate peer message".into()));
481 }
482 match state.peer_tasks.get_mut(&key) {
483 Some(old) => {
484 old.state = task.state;
485 old.conversation = task.conversation;
486 old.text = task.text;
487 old.tokens = task.tokens;
488 old.updated_at = task.updated_at;
489 }
490 None => {
491 state.peer_tasks.insert(key, task);
492 }
493 }
494 Ok(())
495 }
496
497 async fn peer_task(
498 &self,
499 inbound: bool,
500 id: &str,
501 ) -> Result<Option<PeerTaskRecord>, StoreError> {
502 Ok(self
503 .lock()
504 .peer_tasks
505 .get(&(inbound, id.to_string()))
506 .cloned())
507 }
508
509 async fn peer_task_by_message(
510 &self,
511 inbound: bool,
512 team: &str,
513 peer: &str,
514 message_id: &str,
515 ) -> Result<Option<PeerTaskRecord>, StoreError> {
516 Ok(self
517 .lock()
518 .peer_tasks
519 .values()
520 .find(|t| {
521 t.inbound == inbound
522 && t.team == team
523 && t.peer == peer
524 && t.message_id == message_id
525 })
526 .cloned())
527 }
528
529 async fn peer_tasks(&self, limit: usize) -> Result<Vec<PeerTaskRecord>, StoreError> {
530 let mut tasks: Vec<PeerTaskRecord> = self.lock().peer_tasks.values().cloned().collect();
531 tasks.sort_by_key(|t| std::cmp::Reverse((t.updated_at, t.created_at)));
532 tasks.truncate(limit);
533 Ok(tasks)
534 }
535
536 async fn interrupt_peer_tasks(&self, now: Timestamp) -> Result<u64, StoreError> {
537 let mut changed = 0;
538 for task in self.lock().peer_tasks.values_mut() {
539 if task.inbound && !task.state.is_final() {
540 task.state = PeerTaskState::Failed;
541 task.text = Some("interrupted: the bot stopped".into());
542 task.updated_at = now;
543 changed += 1;
544 }
545 }
546 Ok(changed)
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use crate::ManualClock;
554
555 #[tokio::test]
556 async fn conformance() {
557 let clock = Arc::new(ManualClock::default());
558 let store = MemoryStore::new(clock.clone());
559 crate::conformance::conversation_store(&store, &clock).await;
560 }
561}