1use std::fmt;
2use std::str::FromStr;
3
4use anyhow::Result;
5use serde::{Deserialize, Serialize};
6
7pub const TTL_EPHEMERAL: i64 = 3600;
8pub const TTL_SHORT_TERM: i64 = 86_400;
9pub const TTL_LONG_TERM: i64 = 1_209_600;
10
11pub const REL_PRECEDED_BY: &str = "PRECEDED_BY";
15pub const REL_RELATES_TO: &str = "RELATES_TO";
17pub const REL_RELATED: &str = "related";
19pub const REL_SIMILAR_TO: &str = "SIMILAR_TO";
21pub const REL_SHARES_THEME: &str = "SHARES_THEME";
22pub const REL_PARALLEL_CONTEXT: &str = "PARALLEL_CONTEXT";
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum MemoryKind {
26 Episodic,
27 Semantic,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub enum EventType {
38 SessionSummary,
39 TaskCompletion,
40 ErrorPattern,
41 LessonLearned,
42 Decision,
43 BlockedContext,
44 UserPreference,
45 UserFact,
46 AdvisorInsight,
47 GitCommit,
48 GitMerge,
49 GitConflict,
50 SessionStart,
51 SessionEnd,
52 ContextWarning,
53 BudgetAlert,
54 CoordinationSnapshot,
55 Checkpoint,
56 Reminder,
57 Memory,
58 CodeChunk,
59 FileSummary,
60 Unknown(String),
62}
63
64impl EventType {
65 pub fn is_valid(&self) -> bool {
67 !matches!(self, EventType::Unknown(_))
68 }
69
70 pub fn memory_kind(&self) -> MemoryKind {
72 match self {
73 EventType::ErrorPattern
74 | EventType::LessonLearned
75 | EventType::UserPreference
76 | EventType::GitConflict
77 | EventType::Reminder
78 | EventType::Decision => MemoryKind::Semantic,
79 EventType::Unknown(_) => MemoryKind::Episodic,
80 _ => MemoryKind::Episodic,
81 }
82 }
83
84 pub fn default_priority(&self) -> i32 {
86 match self {
87 EventType::ErrorPattern
88 | EventType::LessonLearned
89 | EventType::UserPreference
90 | EventType::GitConflict => 4,
91 EventType::Decision | EventType::TaskCompletion | EventType::AdvisorInsight => 3,
92 EventType::GitCommit
93 | EventType::GitMerge
94 | EventType::SessionEnd
95 | EventType::BudgetAlert => 2,
96 EventType::SessionSummary
97 | EventType::SessionStart
98 | EventType::ContextWarning
99 | EventType::CoordinationSnapshot => 1,
100 EventType::BlockedContext
101 | EventType::Checkpoint
102 | EventType::Reminder
103 | EventType::Memory
104 | EventType::FileSummary => 1,
105 EventType::CodeChunk => 0,
106 EventType::UserFact => 4,
107 EventType::Unknown(_) => 0,
108 }
109 }
110
111 pub fn default_ttl(&self) -> Option<i64> {
113 match self {
114 EventType::SessionSummary => Some(TTL_EPHEMERAL),
115 EventType::TaskCompletion => Some(TTL_LONG_TERM),
116 EventType::ErrorPattern => None,
117 EventType::LessonLearned => None,
118 EventType::Decision => Some(TTL_LONG_TERM),
119 EventType::BlockedContext => Some(TTL_SHORT_TERM),
120 EventType::UserPreference => None,
121 EventType::UserFact => None,
122 EventType::AdvisorInsight => Some(TTL_LONG_TERM),
123 EventType::GitCommit => Some(TTL_LONG_TERM),
124 EventType::GitMerge => Some(TTL_LONG_TERM),
125 EventType::GitConflict => None,
126 EventType::SessionStart => Some(TTL_SHORT_TERM),
127 EventType::SessionEnd => Some(TTL_LONG_TERM),
128 EventType::ContextWarning => Some(TTL_SHORT_TERM),
129 EventType::BudgetAlert => Some(TTL_LONG_TERM),
130 EventType::CoordinationSnapshot => Some(TTL_SHORT_TERM),
131 EventType::Checkpoint => Some(604_800),
132 EventType::Reminder => None,
133 EventType::Memory => Some(TTL_SHORT_TERM),
134 EventType::CodeChunk => Some(TTL_EPHEMERAL),
135 EventType::FileSummary => Some(TTL_SHORT_TERM),
136 EventType::Unknown(_) => Some(TTL_LONG_TERM),
137 }
138 }
139
140 pub fn type_weight(&self) -> f64 {
142 match self {
143 EventType::Checkpoint => 2.5,
144 EventType::Reminder => 3.0,
145 EventType::Decision => 2.0,
146 EventType::LessonLearned => 2.0,
147 EventType::ErrorPattern => 2.0,
148 EventType::UserPreference => 2.0,
149 EventType::TaskCompletion => 1.4,
150 EventType::SessionSummary => 1.2,
151 EventType::BlockedContext => 1.0,
152 EventType::GitCommit => 1.0,
153 EventType::GitMerge => 1.0,
154 EventType::GitConflict => 1.0,
155 EventType::CoordinationSnapshot => 0.2,
156 EventType::Memory => 1.0,
157 _ => 1.0,
158 }
159 }
160
161 pub fn dedup_threshold(&self) -> Option<f64> {
163 match self {
164 EventType::ErrorPattern => Some(0.70),
165 EventType::SessionSummary => Some(0.75),
166 EventType::TaskCompletion => Some(0.85),
167 EventType::Decision => Some(0.80),
168 EventType::LessonLearned => Some(0.85),
169 EventType::UserFact => Some(0.85),
170 _ => None,
171 }
172 }
173
174 pub fn is_supersession_type(&self) -> bool {
176 matches!(
177 self,
178 EventType::Decision
179 | EventType::LessonLearned
180 | EventType::UserPreference
181 | EventType::UserFact
182 | EventType::Reminder
183 )
184 }
185
186 pub fn from_optional(s: Option<&str>) -> Option<EventType> {
188 s.map(|v| EventType::from_str(v).unwrap_or_else(|e| match e {}))
189 }
190
191 pub fn types_with_dedup_threshold() -> Vec<EventType> {
193 vec![
195 EventType::ErrorPattern,
196 EventType::SessionSummary,
197 EventType::TaskCompletion,
198 EventType::Decision,
199 EventType::LessonLearned,
200 EventType::UserFact,
201 ]
202 }
203}
204
205impl fmt::Display for EventType {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 let s = match self {
208 EventType::SessionSummary => "session_summary",
209 EventType::TaskCompletion => "task_completion",
210 EventType::ErrorPattern => "error_pattern",
211 EventType::LessonLearned => "lesson_learned",
212 EventType::Decision => "decision",
213 EventType::BlockedContext => "blocked_context",
214 EventType::UserPreference => "user_preference",
215 EventType::UserFact => "user_fact",
216 EventType::AdvisorInsight => "advisor_insight",
217 EventType::GitCommit => "git_commit",
218 EventType::GitMerge => "git_merge",
219 EventType::GitConflict => "git_conflict",
220 EventType::SessionStart => "session_start",
221 EventType::SessionEnd => "session_end",
222 EventType::ContextWarning => "context_warning",
223 EventType::BudgetAlert => "budget_alert",
224 EventType::CoordinationSnapshot => "coordination_snapshot",
225 EventType::Checkpoint => "checkpoint",
226 EventType::Reminder => "reminder",
227 EventType::Memory => "memory",
228 EventType::CodeChunk => "code_chunk",
229 EventType::FileSummary => "file_summary",
230 EventType::Unknown(s) => s.as_str(),
231 };
232 write!(f, "{s}")
233 }
234}
235
236impl FromStr for EventType {
237 type Err = std::convert::Infallible;
238
239 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
240 Ok(match s {
241 "session_summary" => EventType::SessionSummary,
242 "task_completion" => EventType::TaskCompletion,
243 "error_pattern" => EventType::ErrorPattern,
244 "lesson_learned" => EventType::LessonLearned,
245 "decision" => EventType::Decision,
246 "blocked_context" => EventType::BlockedContext,
247 "user_preference" => EventType::UserPreference,
248 "user_fact" => EventType::UserFact,
249 "advisor_insight" => EventType::AdvisorInsight,
250 "git_commit" => EventType::GitCommit,
251 "git_merge" => EventType::GitMerge,
252 "git_conflict" => EventType::GitConflict,
253 "session_start" => EventType::SessionStart,
254 "session_end" => EventType::SessionEnd,
255 "context_warning" => EventType::ContextWarning,
256 "budget_alert" => EventType::BudgetAlert,
257 "coordination_snapshot" => EventType::CoordinationSnapshot,
258 "checkpoint" => EventType::Checkpoint,
259 "reminder" => EventType::Reminder,
260 "memory" => EventType::Memory,
261 "code_chunk" => EventType::CodeChunk,
262 "file_summary" => EventType::FileSummary,
263 other => EventType::Unknown(other.to_string()),
264 })
265 }
266}
267
268impl Serialize for EventType {
269 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
270 where
271 S: serde::Serializer,
272 {
273 serializer.serialize_str(&self.to_string())
274 }
275}
276
277impl<'de> Deserialize<'de> for EventType {
278 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
279 where
280 D: serde::Deserializer<'de>,
281 {
282 let s = String::deserialize(deserializer)?;
283 Ok(EventType::from_str(&s).unwrap_or_else(|e| match e {}))
285 }
286}
287
288impl schemars::JsonSchema for EventType {
289 fn schema_name() -> std::borrow::Cow<'static, str> {
290 std::borrow::Cow::Borrowed("EventType")
291 }
292
293 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
294 schemars::json_schema!({
295 "type": "string",
296 "description": "Memory event type",
297 "enum": [
298 "session_summary", "task_completion", "error_pattern", "lesson_learned",
299 "decision", "blocked_context", "user_preference", "user_fact",
300 "advisor_insight", "git_commit", "git_merge", "git_conflict",
301 "session_start", "session_end", "context_warning", "budget_alert",
302 "coordination_snapshot", "checkpoint", "reminder", "memory",
303 "code_chunk", "file_summary"
304 ]
305 })
306 }
307}
308
309#[derive(Debug, Clone)]
310pub struct MemoryInput {
311 pub content: String,
312 pub id: Option<String>,
313 pub tags: Vec<String>,
314 pub importance: f64,
315 pub metadata: serde_json::Value,
316 pub event_type: Option<EventType>,
317 pub session_id: Option<String>,
318 pub project: Option<String>,
319 pub priority: Option<i32>,
320 pub entity_id: Option<String>,
321 pub agent_type: Option<String>,
322 pub ttl_seconds: Option<i64>,
323 pub referenced_date: Option<String>,
326 pub source_type: Option<String>,
328}
329
330impl Default for MemoryInput {
331 fn default() -> Self {
332 Self {
333 content: String::new(),
334 id: None,
335 tags: Vec::new(),
336 importance: 0.5,
337 metadata: serde_json::json!({}),
338 event_type: None,
339 session_id: None,
340 project: None,
341 priority: None,
342 entity_id: None,
343 agent_type: None,
344 ttl_seconds: None,
345 referenced_date: None,
346 source_type: None,
347 }
348 }
349}
350
351impl MemoryInput {
352 pub fn apply_event_type_defaults(&mut self, event_type_str: Option<&str>) {
356 let event_type =
357 EventType::from_optional(event_type_str).or_else(|| self.event_type.clone());
358
359 if self.ttl_seconds.is_none() {
360 self.ttl_seconds = event_type
361 .as_ref()
362 .map(EventType::default_ttl)
363 .unwrap_or(Some(TTL_LONG_TERM));
364 }
365 if self.priority.is_none() {
366 self.priority = event_type.as_ref().map(EventType::default_priority);
367 }
368 self.event_type = event_type;
369 }
370}
371
372#[derive(Debug, Clone, Default)]
373pub struct MemoryUpdate {
374 pub content: Option<String>,
375 pub tags: Option<Vec<String>>,
376 pub importance: Option<f64>,
377 pub metadata: Option<serde_json::Value>,
378 pub event_type: Option<EventType>,
379 pub priority: Option<i32>,
380}
381
382#[derive(Debug, Clone, Default)]
383pub struct SearchOptions {
384 pub event_type: Option<EventType>,
385 pub project: Option<String>,
386 pub session_id: Option<String>,
387 pub include_superseded: Option<bool>,
388 pub importance_min: Option<f64>,
389 pub created_after: Option<String>,
390 pub created_before: Option<String>,
391 pub context_tags: Option<Vec<String>>,
392 pub entity_id: Option<String>,
393 pub agent_type: Option<String>,
394 pub event_after: Option<String>,
396 pub event_before: Option<String>,
398 pub explain: Option<bool>,
400}
401
402#[derive(Debug, Clone)]
403pub struct CheckpointInput {
404 pub task_title: String,
405 pub progress: String,
406 pub plan: Option<String>,
407 pub files_touched: Option<serde_json::Value>,
408 pub decisions: Option<Vec<String>>,
409 pub key_context: Option<String>,
410 pub next_steps: Option<String>,
411 pub session_id: Option<String>,
412 pub project: Option<String>,
413}
414
415pub fn is_valid_event_type(event_type: &str) -> bool {
418 EventType::from_str(event_type)
419 .map(|et| et.is_valid())
420 .unwrap_or(false)
421}
422
423pub fn parse_duration(text: &str) -> Result<chrono::Duration> {
424 if text.is_empty() {
425 return Err(anyhow::anyhow!("duration cannot be empty"));
426 }
427
428 let mut weeks: i64 = 0;
429 let mut days: i64 = 0;
430 let mut hours: i64 = 0;
431 let mut minutes: i64 = 0;
432 let mut last_rank: i32 = -1;
433 let mut idx: usize = 0;
434 let bytes = text.as_bytes();
435
436 while idx < bytes.len() {
437 if !bytes[idx].is_ascii_digit() {
438 return Err(anyhow::anyhow!("invalid duration format: {text}"));
439 }
440
441 let start = idx;
442 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
443 idx += 1;
444 }
445
446 if idx >= bytes.len() {
447 return Err(anyhow::anyhow!("invalid duration format: {text}"));
448 }
449
450 let value = text[start..idx]
451 .parse::<i64>()
452 .map_err(|_| anyhow::anyhow!("invalid duration value: {text}"))?;
453 let unit = bytes[idx] as char;
454 idx += 1;
455
456 let rank = match unit {
457 'w' => 0,
458 'd' => 1,
459 'h' => 2,
460 'm' => 3,
461 _ => return Err(anyhow::anyhow!("invalid duration unit in: {text}")),
462 };
463
464 if rank <= last_rank {
465 return Err(anyhow::anyhow!("invalid duration order in: {text}"));
466 }
467 last_rank = rank;
468
469 match unit {
470 'w' => weeks = value,
471 'd' => days = value,
472 'h' => hours = value,
473 'm' => minutes = value,
474 _ => return Err(anyhow::anyhow!("invalid duration unit in: {text}")),
475 }
476 }
477
478 let total = chrono::Duration::weeks(weeks)
479 + chrono::Duration::days(days)
480 + chrono::Duration::hours(hours)
481 + chrono::Duration::minutes(minutes);
482
483 if total.num_seconds() <= 0 {
484 return Err(anyhow::anyhow!("duration must be greater than zero"));
485 }
486
487 Ok(total)
488}
489
490#[derive(Debug, Clone, PartialEq, Serialize)]
492pub struct SearchResult {
493 pub id: String,
495 pub content: String,
497 pub tags: Vec<String>,
499 pub importance: f64,
501 pub metadata: serde_json::Value,
503 pub event_type: Option<EventType>,
504 pub session_id: Option<String>,
505 pub project: Option<String>,
506 pub entity_id: Option<String>,
507 pub agent_type: Option<String>,
508}
509
510#[derive(Debug, Clone, PartialEq, Serialize)]
512pub struct SemanticResult {
513 pub id: String,
515 pub content: String,
517 pub tags: Vec<String>,
519 pub importance: f64,
521 pub metadata: serde_json::Value,
523 pub event_type: Option<EventType>,
524 pub session_id: Option<String>,
525 pub project: Option<String>,
526 pub entity_id: Option<String>,
527 pub agent_type: Option<String>,
528 pub score: f32,
530}
531
532#[derive(Debug, Clone)]
533pub struct GraphNode {
534 pub id: String,
535 pub content: String,
536 pub event_type: Option<EventType>,
537 pub metadata: serde_json::Value,
538 pub hop: usize,
539 pub weight: f64,
540 pub edge_type: String,
541 pub created_at: String,
542}
543
544#[derive(Debug, Clone, PartialEq)]
546pub struct Relationship {
547 pub id: String,
549 pub source_id: String,
551 pub target_id: String,
553 pub rel_type: String,
555 pub weight: f64,
556 pub metadata: serde_json::Value,
558 pub created_at: String,
559}
560
561#[derive(Debug, Clone, PartialEq)]
563pub struct ListResult {
564 pub memories: Vec<SearchResult>,
566 pub total: usize,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct BackupInfo {
573 pub path: std::path::PathBuf,
574 pub size_bytes: u64,
575 pub created_at: String,
576}