1use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5use std::time::Duration;
6
7use jiff::{SignedDuration, Timestamp};
8use okbot_core::{
9 AgentRunRecord, ChannelId, Clock, ConversationKey, ConversationStore, Fire, FireState,
10 NewMessage, PAIRING_MAX_FAILURES, StoreError, StoredMessage, UserId, async_trait,
11};
12
13#[derive(Default)]
14struct State {
15 seen: HashMap<(ChannelId, String), Timestamp>,
16 messages: Vec<(u64, NewMessage)>,
17 reset_after: HashMap<ConversationKey, u64>,
18 sessions: HashMap<ConversationKey, (String, String)>,
19 runs: Vec<AgentRunRecord>,
20 next: u64,
21 owners: Vec<(ChannelId, UserId)>,
22 codes: HashMap<String, (Timestamp, u32)>,
24 fires: HashMap<(String, Timestamp), Fire>,
25 direct: HashMap<(ChannelId, UserId), ConversationKey>,
26}
27
28pub struct MemoryStore {
30 state: Mutex<State>,
31 clock: Arc<dyn Clock>,
32}
33
34impl std::fmt::Debug for MemoryStore {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("MemoryStore").finish_non_exhaustive()
37 }
38}
39
40impl MemoryStore {
41 pub fn new(clock: Arc<dyn Clock>) -> Self {
43 Self {
44 state: Mutex::new(State::default()),
45 clock,
46 }
47 }
48
49 fn lock(&self) -> std::sync::MutexGuard<'_, State> {
50 self.state
51 .lock()
52 .unwrap_or_else(std::sync::PoisonError::into_inner)
53 }
54
55 pub fn runs(&self) -> Vec<AgentRunRecord> {
57 self.lock().runs.clone()
58 }
59}
60
61fn signed(d: Duration) -> SignedDuration {
62 SignedDuration::try_from(d).unwrap_or(SignedDuration::MAX)
63}
64
65#[async_trait]
66impl ConversationStore for MemoryStore {
67 async fn mark_seen(
68 &self,
69 channel: &ChannelId,
70 event_id: &str,
71 ttl: Duration,
72 ) -> Result<bool, StoreError> {
73 let now = self.clock.now();
74 let mut state = self.lock();
75 let key = (channel.clone(), event_id.to_string());
76 if state.seen.get(&key).is_some_and(|expires| *expires > now) {
77 return Ok(false);
78 }
79 state
80 .seen
81 .insert(key, now.saturating_add(signed(ttl)).unwrap_or(now));
82 Ok(true)
83 }
84
85 async fn append(&self, message: NewMessage) -> Result<(), StoreError> {
86 let mut state = self.lock();
87 state.next += 1;
88 let id = state.next;
89 state.messages.push((id, message));
90 Ok(())
91 }
92
93 async fn recent(
94 &self,
95 conversation: &ConversationKey,
96 limit: usize,
97 ) -> Result<Vec<StoredMessage>, StoreError> {
98 let state = self.lock();
99 let after = state.reset_after.get(conversation).copied().unwrap_or(0);
100 let mut out: Vec<StoredMessage> = state
101 .messages
102 .iter()
103 .rev()
104 .filter(|(id, m)| *id > after && m.conversation == *conversation)
105 .take(limit)
106 .map(|(_, m)| StoredMessage {
107 role: m.role,
108 author_name: m.author_name.clone(),
109 text: m.text.clone(),
110 at: m.at,
111 })
112 .collect();
113 out.reverse();
114 Ok(out)
115 }
116
117 async fn reset(&self, conversation: &ConversationKey) -> Result<(), StoreError> {
118 let mut state = self.lock();
119 let last = state.next;
120 state.reset_after.insert(conversation.clone(), last);
121 state.sessions.remove(conversation);
122 Ok(())
123 }
124
125 async fn session(
126 &self,
127 conversation: &ConversationKey,
128 ) -> Result<Option<(String, String)>, StoreError> {
129 Ok(self.lock().sessions.get(conversation).cloned())
130 }
131
132 async fn set_session(
133 &self,
134 conversation: &ConversationKey,
135 backend: &str,
136 session: &str,
137 ) -> Result<(), StoreError> {
138 self.lock().sessions.insert(
139 conversation.clone(),
140 (backend.to_string(), session.to_string()),
141 );
142 Ok(())
143 }
144
145 async fn owners(&self) -> Result<Vec<(ChannelId, UserId)>, StoreError> {
146 let mut owners = self.lock().owners.clone();
147 owners.sort();
148 Ok(owners)
149 }
150
151 async fn remove_owner(&self, channel: &ChannelId, user: &UserId) -> Result<bool, StoreError> {
152 let mut state = self.lock();
153 let before = state.owners.len();
154 state.owners.retain(|(c, u)| c != channel || u != user);
155 Ok(state.owners.len() != before)
156 }
157
158 async fn create_pairing_code(&self, code: &str, ttl: Duration) -> Result<(), StoreError> {
159 let expires = self.clock.now() + signed(ttl);
160 self.lock().codes.insert(code.to_string(), (expires, 0));
161 Ok(())
162 }
163
164 async fn redeem_pairing_code(
165 &self,
166 code: &str,
167 channel: &ChannelId,
168 user: &UserId,
169 ) -> Result<bool, StoreError> {
170 let now = self.clock.now();
171 let mut state = self.lock();
172 state.codes.retain(|_, (expires, _)| *expires > now);
173 if state.codes.remove(code).is_some() {
174 let account = (channel.clone(), user.clone());
175 if !state.owners.contains(&account) {
176 state.owners.push(account);
177 }
178 return Ok(true);
179 }
180 for (_, failures) in state.codes.values_mut() {
181 *failures += 1;
182 }
183 state
184 .codes
185 .retain(|_, (_, failures)| *failures < PAIRING_MAX_FAILURES);
186 Ok(false)
187 }
188
189 async fn record_run(&self, run: AgentRunRecord) -> Result<(), StoreError> {
190 self.lock().runs.push(run);
191 Ok(())
192 }
193
194 async fn claim_fire(&self, task_id: &str, fire_at: Timestamp) -> Result<Fire, StoreError> {
195 let now = self.clock.now();
196 Ok(self
197 .lock()
198 .fires
199 .entry((task_id.to_string(), fire_at))
200 .or_insert_with(|| Fire {
201 task_id: task_id.to_string(),
202 fire_at,
203 state: FireState::Sending,
204 attempts: 0,
205 updated_at: now,
206 })
207 .clone())
208 }
209
210 async fn set_fire(
211 &self,
212 task_id: &str,
213 fire_at: Timestamp,
214 state: FireState,
215 attempts: u32,
216 ) -> Result<(), StoreError> {
217 let fire = Fire {
218 task_id: task_id.to_string(),
219 fire_at,
220 state,
221 attempts,
222 updated_at: self.clock.now(),
223 };
224 self.lock()
225 .fires
226 .insert((task_id.to_string(), fire_at), fire);
227 Ok(())
228 }
229
230 async fn recent_fires(&self, limit: usize) -> Result<Vec<Fire>, StoreError> {
231 let mut fires: Vec<Fire> = self.lock().fires.values().cloned().collect();
232 fires.sort_by_key(|f| std::cmp::Reverse((f.updated_at, f.fire_at)));
233 fires.truncate(limit);
234 Ok(fires)
235 }
236
237 async fn set_direct_chat(
238 &self,
239 channel: &ChannelId,
240 user: &UserId,
241 conversation: &ConversationKey,
242 ) -> Result<(), StoreError> {
243 self.lock()
244 .direct
245 .insert((channel.clone(), user.clone()), conversation.clone());
246 Ok(())
247 }
248
249 async fn direct_chat(
250 &self,
251 channel: &ChannelId,
252 user: &UserId,
253 ) -> Result<Option<ConversationKey>, StoreError> {
254 Ok(self
255 .lock()
256 .direct
257 .get(&(channel.clone(), user.clone()))
258 .cloned())
259 }
260
261 async fn purge_before(&self, before: Timestamp) -> Result<u64, StoreError> {
262 let now = self.clock.now();
263 let mut state = self.lock();
264 let count = |s: &State| s.messages.len() + s.runs.len() + s.seen.len() + s.fires.len();
265 let start = count(&state);
266 state.messages.retain(|(_, m)| m.at >= before);
267 state.runs.retain(|r| r.started_at >= before);
268 state.seen.retain(|_, expires| *expires > now);
269 state
270 .fires
271 .retain(|_, f| f.updated_at >= before || f.state == FireState::Sending);
272 let end = count(&state);
273 Ok((start - end) as u64)
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::ManualClock;
281
282 #[tokio::test]
283 async fn conformance() {
284 let clock = Arc::new(ManualClock::default());
285 let store = MemoryStore::new(clock.clone());
286 crate::conformance::conversation_store(&store, &clock).await;
287 }
288}