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