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)]
106 pub is_recap: bool,
107 pub enrichment_status: String,
108 pub supersede_mode: String,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub structured_fields: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub retrieval_cue: Option<String>,
113 pub access_count: u64,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub source_text: Option<String>,
116 #[serde(default = "default_version")]
117 pub version: i64,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub changelog: Option<String>,
120 #[serde(default)]
121 pub pending_revision: bool,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub merged_from: Option<Vec<String>>,
124}
125
126fn default_version() -> i64 {
127 1
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct EnrichmentStepStatus {
133 pub step: String,
134 pub status: String,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub error: Option<String>,
137 pub attempts: u32,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct EnrichmentStatusResponse {
143 pub source_id: String,
144 pub summary: String,
145 pub steps: Vec<EnrichmentStepStatus>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct MemoryVersionItem {
151 pub source_id: String,
152 pub title: String,
153 pub content: String,
154 pub memory_type: Option<String>,
155 pub confirmed: bool,
156 pub supersedes: Option<String>,
157 pub last_modified: i64,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct MemoryStats {
163 pub total: u64,
164 pub new_today: u64,
165 pub confirmed: u64,
166 pub domains: Vec<DomainInfo>,
167 #[serde(default)]
168 pub by_type: Vec<TypeBreakdown>,
169 #[serde(default)]
170 pub entity_linked: u64,
171 #[serde(default)]
172 pub enrichment_pending: u64,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct TypeBreakdown {
178 pub memory_type: String,
179 pub count: u64,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct DomainInfo {
185 pub name: String,
186 pub count: u64,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct IndexedFileInfo {
192 pub source_id: String,
193 pub title: String,
194 pub source: String,
195 pub url: Option<String>,
196 pub chunk_count: u64,
197 pub last_modified: i64,
198 pub summary: Option<String>,
199 #[serde(default)]
200 pub processing: bool,
201 pub memory_type: Option<String>,
202 #[serde(default, alias = "domain")]
203 pub space: Option<String>,
204 pub source_agent: Option<String>,
205 pub confidence: Option<f32>,
206 pub confirmed: Option<bool>,
207 pub stability: Option<String>,
208 pub pinned: bool,
209 #[serde(default)]
213 pub created_at: i64,
214 #[serde(default)]
218 pub content: String,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct HomeStats {
224 pub total: u64,
225 pub new_today: u64,
226 pub confirmed: u64,
227 pub total_ingested: u64,
228 pub active_insights: u64,
229 pub distilled_today: u64,
230 pub distilled_all: u64,
231 pub sources_archived: u64,
232 pub times_served_today: u64,
233 pub words_saved_today: u64,
234 pub times_served_week: u64,
235 pub words_saved_week: u64,
236 pub times_served_all: u64,
237 pub words_saved_all: u64,
238 pub corrections_active: u64,
239 pub top_memories: Vec<TopMemory>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct TopMemory {
245 pub source_id: String,
246 pub content: String,
247 pub memory_type: Option<String>,
248 #[serde(default, alias = "domain")]
249 pub space: Option<String>,
250 pub times_retrieved: u64,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct SessionSnapshot {
261 pub id: String,
262 pub activity_id: String,
263 pub started_at: i64,
264 pub ended_at: i64,
265 pub primary_apps: Vec<String>,
266 pub summary: String,
267 pub tags: Vec<String>,
268 pub capture_count: u64,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct SnapshotCapture {
276 pub source_id: String,
277 pub app_name: String,
278 pub window_title: String,
279 pub timestamp: i64,
280 pub source: String,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct SnapshotCaptureWithContent {
289 pub source_id: String,
290 pub app_name: String,
291 pub window_title: String,
292 pub timestamp: i64,
293 pub source: String,
294 pub content: String,
295 pub summary: Option<String>,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct Profile {
301 pub id: String,
302 pub name: String,
303 pub display_name: Option<String>,
304 pub email: Option<String>,
305 pub bio: Option<String>,
306 pub avatar_path: Option<String>,
307 pub created_at: i64,
308 pub updated_at: i64,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct AgentConnection {
314 pub id: String,
315 pub name: String,
319 #[serde(default)]
322 pub display_name: Option<String>,
323 pub agent_type: String,
324 pub description: Option<String>,
325 pub enabled: bool,
326 pub trust_level: String,
327 pub last_seen_at: Option<i64>,
328 pub memory_count: i64,
329 pub created_at: i64,
330 pub updated_at: i64,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct AgentActivityRow {
336 pub id: i64,
337 pub timestamp: i64,
338 pub agent_name: String,
339 pub action: String,
340 pub memory_ids: Option<String>,
341 pub query: Option<String>,
342 pub detail: Option<String>,
343 pub memory_titles: Vec<String>,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct Space {
349 pub id: String,
350 pub name: String,
351 pub description: Option<String>,
352 pub suggested: bool,
353 pub starred: bool,
354 pub sort_order: i64,
355 pub memory_count: u64,
356 pub entity_count: u64,
357 pub created_at: f64,
358 pub updated_at: f64,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct RejectionRecord {
364 pub id: String,
365 pub content: String,
366 pub source_agent: Option<String>,
367 pub rejection_reason: String,
368 pub rejection_detail: Option<String>,
369 pub similarity_score: Option<f64>,
370 pub similar_to_source_id: Option<String>,
371 pub created_at: i64,
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
380pub struct RetrievalEvent {
381 pub timestamp_ms: i64,
382 pub agent_name: String,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub query: Option<String>,
385 #[serde(default)]
386 pub page_titles: Vec<String>,
387 #[serde(default)]
392 pub page_ids: Vec<String>,
393 #[serde(default)]
394 pub memory_snippets: Vec<String>,
395}
396
397#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
399#[serde(rename_all = "snake_case")]
400pub enum PageChangeKind {
401 Created,
402 Revised,
403 Merged,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
411pub struct PageChange {
412 pub page_id: String,
413 pub title: String,
414 pub change_kind: PageChangeKind,
415 pub changed_at_ms: i64,
416}
417
418#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
420#[serde(rename_all = "snake_case")]
421pub enum ActivityKind {
422 Page,
423 Memory,
424}
425
426#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
428#[serde(tag = "kind", rename_all = "snake_case")]
429pub enum ActivityBadge {
430 New,
431 Revised,
432 Refined,
433 Growing { added: u32 },
434 NeedsReview,
435 None,
436}
437
438#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
440pub struct RecentActivityItem {
441 pub kind: ActivityKind,
442 pub id: String,
443 pub title: String,
444 pub snippet: Option<String>,
445 pub timestamp_ms: u64,
446 pub badge: ActivityBadge,
447}
448
449#[cfg(test)]
450mod indexed_file_info_created_at_test {
451 use super::*;
452
453 fn make_info(created_at: i64) -> IndexedFileInfo {
454 IndexedFileInfo {
455 source_id: "mem_abc".into(),
456 title: "Title".into(),
457 source: "memory".into(),
458 url: None,
459 chunk_count: 1,
460 last_modified: 1000,
461 summary: None,
462 processing: false,
463 memory_type: None,
464 space: None,
465 source_agent: None,
466 confidence: None,
467 confirmed: None,
468 stability: None,
469 pinned: false,
470 created_at,
471 content: String::new(),
472 }
473 }
474
475 #[test]
476 fn created_at_serializes_in_json() {
477 let info = make_info(1234);
478 let s = serde_json::to_string(&info).unwrap();
479 assert!(s.contains("\"created_at\":1234"), "got: {s}");
480 }
481
482 #[test]
483 fn created_at_defaults_to_zero_when_missing() {
484 let json = r#"{"source_id":"x","title":"T","source":"memory","url":null,
485 "chunk_count":1,"last_modified":1000,"processing":false,
486 "memory_type":null,"space":null,"source_agent":null,
487 "confidence":null,"confirmed":null,"stability":null,"pinned":false}"#;
488 let info: IndexedFileInfo = serde_json::from_str(json).unwrap();
489 assert_eq!(info.created_at, 0);
490 }
491
492 #[test]
493 fn content_round_trips() {
494 let mut info = make_info(9999);
495 info.content = "memory body".to_string();
496 let json = serde_json::to_string(&info).unwrap();
497 let back: IndexedFileInfo = serde_json::from_str(&json).unwrap();
498 assert_eq!(back.content, "memory body");
499 }
500
501 #[test]
502 fn content_defaults_to_empty_when_missing() {
503 let json = r#"{"source_id":"x","title":"T","source":"memory","url":null,
504 "chunk_count":1,"last_modified":1000,"processing":false,
505 "memory_type":null,"space":null,"source_agent":null,
506 "confidence":null,"confirmed":null,"stability":null,"pinned":false}"#;
507 let info: IndexedFileInfo = serde_json::from_str(json).unwrap();
508 assert_eq!(info.content, "");
509 }
510
511 #[test]
512 fn legacy_domain_alias_deserializes_to_space() {
513 let json = r#"{"source_id":"x","title":"T","source":"memory","url":null,
514 "chunk_count":1,"last_modified":1000,"processing":false,
515 "memory_type":null,"domain":"work","source_agent":null,
516 "confidence":null,"confirmed":null,"stability":null,"pinned":false}"#;
517 let info: IndexedFileInfo =
518 serde_json::from_str(json).expect("legacy domain key should deserialize");
519 assert_eq!(
520 info.space.as_deref(),
521 Some("work"),
522 "alias should map domain -> space"
523 );
524 }
525}
526
527#[cfg(test)]
528mod tests {
529 #[test]
530 fn activity_badge_serializes_as_tagged_enum() {
531 use super::ActivityBadge;
532 assert_eq!(
533 serde_json::to_string(&ActivityBadge::New).unwrap(),
534 r#"{"kind":"new"}"#
535 );
536 assert_eq!(
537 serde_json::to_string(&ActivityBadge::Refined).unwrap(),
538 r#"{"kind":"refined"}"#
539 );
540 assert_eq!(
541 serde_json::to_string(&ActivityBadge::Revised).unwrap(),
542 r#"{"kind":"revised"}"#
543 );
544 assert_eq!(
545 serde_json::to_string(&ActivityBadge::NeedsReview).unwrap(),
546 r#"{"kind":"needs_review"}"#
547 );
548 assert_eq!(
549 serde_json::to_string(&ActivityBadge::None).unwrap(),
550 r#"{"kind":"none"}"#
551 );
552 assert_eq!(
553 serde_json::to_string(&ActivityBadge::Growing { added: 3 }).unwrap(),
554 r#"{"kind":"growing","added":3}"#
555 );
556 }
557
558 #[test]
559 fn recent_activity_item_round_trips() {
560 use super::{ActivityBadge, ActivityKind, RecentActivityItem};
561 let item = RecentActivityItem {
562 kind: ActivityKind::Memory,
563 id: "mem_abc".into(),
564 title: "A memory".into(),
565 snippet: Some("Snippet text".into()),
566 timestamp_ms: 1_776_000_000_000,
567 badge: ActivityBadge::New,
568 };
569 let s = serde_json::to_string(&item).unwrap();
570 let back: RecentActivityItem = serde_json::from_str(&s).unwrap();
571 assert_eq!(back.id, "mem_abc");
572 assert!(matches!(back.kind, ActivityKind::Memory));
573 assert!(matches!(back.badge, ActivityBadge::New));
574 }
575
576 #[test]
577 fn retrieval_event_includes_memory_snippets() {
578 use super::RetrievalEvent;
579 let evt = RetrievalEvent {
580 timestamp_ms: 1,
581 agent_name: "claude-code".into(),
582 query: None,
583 page_titles: vec![],
584 page_ids: vec![],
585 memory_snippets: vec!["The first line of the memory".into()],
586 };
587 let s = serde_json::to_string(&evt).unwrap();
588 assert!(s.contains("memory_snippets"));
589 }
590}