1use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct SearchResult {
9 pub id: String,
10 pub content: String,
11 pub source: String,
12 pub source_id: String,
13 pub title: String,
14 pub url: Option<String>,
15 pub chunk_index: i32,
16 pub last_modified: i64,
17 pub score: f32,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub chunk_type: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub language: Option<String>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub semantic_unit: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub memory_type: Option<String>,
26 #[serde(default, alias = "domain", skip_serializing_if = "Option::is_none")]
27 pub space: Option<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub source_agent: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub confidence: Option<f32>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub confirmed: Option<bool>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub stability: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub supersedes: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub summary: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub entity_id: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub entity_name: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub quality: Option<String>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub importance: Option<u8>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub event_date: Option<i64>,
55 #[serde(default)]
56 pub is_archived: bool,
57 #[serde(default)]
58 pub is_recap: bool,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub structured_fields: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub retrieval_cue: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub source_text: Option<String>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub content_hash: Option<String>,
69 #[serde(default)]
71 pub raw_score: f32,
72 #[serde(default)]
73 pub version: i64,
74 #[serde(default)]
75 pub pending_revision: bool,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub merged_from: Option<Vec<String>>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub last_delta_summary: Option<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct MemoryItem {
85 pub source_id: String,
86 pub title: String,
87 pub content: String,
88 pub summary: Option<String>,
89 pub memory_type: Option<String>,
90 #[serde(default, alias = "domain")]
91 pub space: Option<String>,
92 pub source_agent: Option<String>,
93 pub confidence: Option<f32>,
94 pub confirmed: bool,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub stability: Option<String>,
97 pub pinned: bool,
98 pub supersedes: Option<String>,
99 pub last_modified: i64,
100 pub chunk_count: u64,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub entity_id: Option<String>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub quality: Option<String>,
105 #[serde(default)]
111 pub is_archived: bool,
112 #[serde(default)]
113 pub is_recap: bool,
114 pub enrichment_status: String,
115 pub supersede_mode: String,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub structured_fields: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub retrieval_cue: Option<String>,
120 pub access_count: u64,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub source_text: Option<String>,
123 #[serde(default = "default_version")]
124 pub version: i64,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub changelog: Option<String>,
127 #[serde(default)]
128 pub pending_revision: bool,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub merged_from: Option<Vec<String>>,
131}
132
133fn default_version() -> i64 {
134 1
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct EnrichmentStepStatus {
140 pub step: String,
141 pub status: String,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub error: Option<String>,
144 pub attempts: u32,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub input_version: Option<i64>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct EnrichmentStatusResponse {
152 pub source_id: String,
153 pub summary: String,
154 pub steps: Vec<EnrichmentStepStatus>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct MemoryVersionItem {
160 pub source_id: String,
161 pub title: String,
162 pub content: String,
163 pub memory_type: Option<String>,
164 pub confirmed: bool,
165 pub supersedes: Option<String>,
166 pub last_modified: i64,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct MemoryStats {
172 pub total: u64,
173 pub new_today: u64,
174 pub confirmed: u64,
175 pub domains: Vec<DomainInfo>,
176 #[serde(default)]
177 pub by_type: Vec<TypeBreakdown>,
178 #[serde(default)]
179 pub entity_linked: u64,
180 #[serde(default)]
181 pub enrichment_pending: u64,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct TypeBreakdown {
187 pub memory_type: String,
188 pub count: u64,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct DomainInfo {
194 pub name: String,
195 pub count: u64,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct IndexedFileInfo {
201 pub source_id: String,
202 pub title: String,
203 pub source: String,
204 pub url: Option<String>,
205 pub chunk_count: u64,
206 pub last_modified: i64,
207 pub summary: Option<String>,
208 #[serde(default)]
209 pub processing: bool,
210 pub memory_type: Option<String>,
211 #[serde(default, alias = "domain")]
212 pub space: Option<String>,
213 pub source_agent: Option<String>,
214 pub confidence: Option<f32>,
215 pub confirmed: Option<bool>,
216 pub stability: Option<String>,
217 pub pinned: bool,
218 #[serde(default)]
222 pub created_at: i64,
223 #[serde(default)]
227 pub content: String,
228 #[serde(default)]
233 pub is_archived: bool,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct HomeStats {
239 pub total: u64,
240 pub new_today: u64,
241 pub confirmed: u64,
242 pub total_ingested: u64,
243 pub active_insights: u64,
244 pub distilled_today: u64,
245 pub distilled_all: u64,
246 pub sources_archived: u64,
247 pub times_served_today: u64,
248 pub words_saved_today: u64,
249 pub times_served_week: u64,
250 pub words_saved_week: u64,
251 pub times_served_all: u64,
252 pub words_saved_all: u64,
253 pub corrections_active: u64,
254 pub top_memories: Vec<TopMemory>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct TopMemory {
260 pub source_id: String,
261 pub content: String,
262 pub memory_type: Option<String>,
263 #[serde(default, alias = "domain")]
264 pub space: Option<String>,
265 pub times_retrieved: u64,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct SessionSnapshot {
276 pub id: String,
277 pub activity_id: String,
278 pub started_at: i64,
279 pub ended_at: i64,
280 pub primary_apps: Vec<String>,
281 pub summary: String,
282 pub tags: Vec<String>,
283 pub capture_count: u64,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct SnapshotCapture {
291 pub source_id: String,
292 pub app_name: String,
293 pub window_title: String,
294 pub timestamp: i64,
295 pub source: String,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct SnapshotCaptureWithContent {
304 pub source_id: String,
305 pub app_name: String,
306 pub window_title: String,
307 pub timestamp: i64,
308 pub source: String,
309 pub content: String,
310 pub summary: Option<String>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct Profile {
316 pub id: String,
317 pub name: String,
318 pub display_name: Option<String>,
319 pub email: Option<String>,
320 pub bio: Option<String>,
321 pub avatar_path: Option<String>,
322 pub created_at: i64,
323 pub updated_at: i64,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct AgentConnection {
329 pub id: String,
330 pub name: String,
334 #[serde(default)]
337 pub display_name: Option<String>,
338 pub agent_type: String,
339 pub description: Option<String>,
340 pub enabled: bool,
341 pub trust_level: String,
342 pub last_seen_at: Option<i64>,
343 pub memory_count: i64,
344 pub created_at: i64,
345 pub updated_at: i64,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct AgentActivityRow {
351 pub id: i64,
352 pub timestamp: i64,
353 pub agent_name: String,
354 pub action: String,
355 pub memory_ids: Option<String>,
356 pub query: Option<String>,
357 pub detail: Option<String>,
358 pub memory_titles: Vec<String>,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct Space {
364 pub id: String,
365 pub name: String,
366 pub description: Option<String>,
367 pub suggested: bool,
368 pub starred: bool,
369 #[serde(default)]
370 pub is_default: bool,
371 pub sort_order: i64,
372 pub memory_count: u64,
373 pub entity_count: u64,
374 pub created_at: f64,
375 pub updated_at: f64,
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct RejectionRecord {
381 pub id: String,
382 pub content: String,
383 pub source_agent: Option<String>,
384 pub rejection_reason: String,
385 pub rejection_detail: Option<String>,
386 pub similarity_score: Option<f64>,
387 pub similar_to_source_id: Option<String>,
388 pub created_at: i64,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
397pub struct RetrievalEvent {
398 pub timestamp_ms: i64,
399 pub agent_name: String,
400 #[serde(default, skip_serializing_if = "Option::is_none")]
401 pub query: Option<String>,
402 #[serde(default)]
403 pub page_titles: Vec<String>,
404 #[serde(default)]
409 pub page_ids: Vec<String>,
410 #[serde(default)]
411 pub memory_snippets: Vec<String>,
412}
413
414#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
416#[serde(rename_all = "snake_case")]
417pub enum PageChangeKind {
418 Created,
419 Revised,
420 Merged,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
428pub struct PageChange {
429 pub page_id: String,
430 pub title: String,
431 pub change_kind: PageChangeKind,
432 pub changed_at_ms: i64,
433}
434
435#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
437#[serde(rename_all = "snake_case")]
438pub enum ActivityKind {
439 Page,
440 Memory,
441}
442
443#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
445#[serde(tag = "kind", rename_all = "snake_case")]
446pub enum ActivityBadge {
447 New,
448 Revised,
449 Refined,
450 Growing { added: u32 },
451 NeedsReview,
452 None,
453}
454
455#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
457pub struct RecentActivityItem {
458 pub kind: ActivityKind,
459 pub id: String,
460 pub title: String,
461 pub snippet: Option<String>,
462 pub timestamp_ms: u64,
463 pub badge: ActivityBadge,
464}
465
466#[cfg(test)]
467mod indexed_file_info_created_at_test {
468 use super::*;
469
470 fn make_info(created_at: i64) -> IndexedFileInfo {
471 IndexedFileInfo {
472 source_id: "mem_abc".into(),
473 title: "Title".into(),
474 source: "memory".into(),
475 url: None,
476 chunk_count: 1,
477 last_modified: 1000,
478 summary: None,
479 processing: false,
480 memory_type: None,
481 space: None,
482 source_agent: None,
483 confidence: None,
484 confirmed: None,
485 stability: None,
486 pinned: false,
487 created_at,
488 content: String::new(),
489 is_archived: false,
490 }
491 }
492
493 #[test]
494 fn created_at_serializes_in_json() {
495 let info = make_info(1234);
496 let s = serde_json::to_string(&info).unwrap();
497 assert!(s.contains("\"created_at\":1234"), "got: {s}");
498 }
499
500 #[test]
501 fn created_at_defaults_to_zero_when_missing() {
502 let json = r#"{"source_id":"x","title":"T","source":"memory","url":null,
503 "chunk_count":1,"last_modified":1000,"processing":false,
504 "memory_type":null,"space":null,"source_agent":null,
505 "confidence":null,"confirmed":null,"stability":null,"pinned":false}"#;
506 let info: IndexedFileInfo = serde_json::from_str(json).unwrap();
507 assert_eq!(info.created_at, 0);
508 }
509
510 #[test]
511 fn content_round_trips() {
512 let mut info = make_info(9999);
513 info.content = "memory body".to_string();
514 let json = serde_json::to_string(&info).unwrap();
515 let back: IndexedFileInfo = serde_json::from_str(&json).unwrap();
516 assert_eq!(back.content, "memory body");
517 }
518
519 #[test]
520 fn content_defaults_to_empty_when_missing() {
521 let json = r#"{"source_id":"x","title":"T","source":"memory","url":null,
522 "chunk_count":1,"last_modified":1000,"processing":false,
523 "memory_type":null,"space":null,"source_agent":null,
524 "confidence":null,"confirmed":null,"stability":null,"pinned":false}"#;
525 let info: IndexedFileInfo = serde_json::from_str(json).unwrap();
526 assert_eq!(info.content, "");
527 }
528
529 #[test]
530 fn legacy_domain_alias_deserializes_to_space() {
531 let json = r#"{"source_id":"x","title":"T","source":"memory","url":null,
532 "chunk_count":1,"last_modified":1000,"processing":false,
533 "memory_type":null,"domain":"work","source_agent":null,
534 "confidence":null,"confirmed":null,"stability":null,"pinned":false}"#;
535 let info: IndexedFileInfo =
536 serde_json::from_str(json).expect("legacy domain key should deserialize");
537 assert_eq!(
538 info.space.as_deref(),
539 Some("work"),
540 "alias should map domain -> space"
541 );
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 #[test]
548 fn activity_badge_serializes_as_tagged_enum() {
549 use super::ActivityBadge;
550 assert_eq!(
551 serde_json::to_string(&ActivityBadge::New).unwrap(),
552 r#"{"kind":"new"}"#
553 );
554 assert_eq!(
555 serde_json::to_string(&ActivityBadge::Refined).unwrap(),
556 r#"{"kind":"refined"}"#
557 );
558 assert_eq!(
559 serde_json::to_string(&ActivityBadge::Revised).unwrap(),
560 r#"{"kind":"revised"}"#
561 );
562 assert_eq!(
563 serde_json::to_string(&ActivityBadge::NeedsReview).unwrap(),
564 r#"{"kind":"needs_review"}"#
565 );
566 assert_eq!(
567 serde_json::to_string(&ActivityBadge::None).unwrap(),
568 r#"{"kind":"none"}"#
569 );
570 assert_eq!(
571 serde_json::to_string(&ActivityBadge::Growing { added: 3 }).unwrap(),
572 r#"{"kind":"growing","added":3}"#
573 );
574 }
575
576 #[test]
577 fn recent_activity_item_round_trips() {
578 use super::{ActivityBadge, ActivityKind, RecentActivityItem};
579 let item = RecentActivityItem {
580 kind: ActivityKind::Memory,
581 id: "mem_abc".into(),
582 title: "A memory".into(),
583 snippet: Some("Snippet text".into()),
584 timestamp_ms: 1_776_000_000_000,
585 badge: ActivityBadge::New,
586 };
587 let s = serde_json::to_string(&item).unwrap();
588 let back: RecentActivityItem = serde_json::from_str(&s).unwrap();
589 assert_eq!(back.id, "mem_abc");
590 assert!(matches!(back.kind, ActivityKind::Memory));
591 assert!(matches!(back.badge, ActivityBadge::New));
592 }
593
594 #[test]
595 fn retrieval_event_includes_memory_snippets() {
596 use super::RetrievalEvent;
597 let evt = RetrievalEvent {
598 timestamp_ms: 1,
599 agent_name: "claude-code".into(),
600 query: None,
601 page_titles: vec![],
602 page_ids: vec![],
603 memory_snippets: vec!["The first line of the memory".into()],
604 };
605 let s = serde_json::to_string(&evt).unwrap();
606 assert!(s.contains("memory_snippets"));
607 }
608}