Skip to main content

mnemara_core/
query.rs

1use crate::config::{
2    EngineTuningInfo, RecallPlanningProfile, RecallPolicyProfile, RecallScorerKind,
3    RecallScoringProfile,
4};
5use crate::model::{
6    ConflictResolutionKind, ConflictReviewState, EpisodeContinuityState, MemoryHistoricalState,
7    MemoryQualityState, MemoryRecord, MemoryRecordKind, MemoryScope, MemoryTrustLevel,
8};
9use serde::{Deserialize, Serialize};
10use std::collections::{BTreeMap, BTreeSet};
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
13pub enum RecallTemporalOrder {
14    #[default]
15    Relevance,
16    ChronologicalAsc,
17    ChronologicalDesc,
18}
19
20#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
21pub enum RecallHistoricalMode {
22    #[default]
23    CurrentOnly,
24    IncludeHistorical,
25    HistoricalOnly,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
29#[serde(default)]
30pub struct RecallFilters {
31    pub kinds: Vec<MemoryRecordKind>,
32    pub required_labels: Vec<String>,
33    pub source: Option<String>,
34    pub from_unix_ms: Option<u64>,
35    pub to_unix_ms: Option<u64>,
36    pub min_importance_score: Option<f32>,
37    pub trust_levels: Vec<MemoryTrustLevel>,
38    pub states: Vec<MemoryQualityState>,
39    pub include_archived: bool,
40    pub episode_id: Option<String>,
41    pub continuity_states: Vec<EpisodeContinuityState>,
42    pub unresolved_only: bool,
43    pub temporal_order: RecallTemporalOrder,
44    pub historical_mode: RecallHistoricalMode,
45    pub lineage_record_id: Option<String>,
46    pub before_record_id: Option<String>,
47    pub after_record_id: Option<String>,
48    pub boundary_labels: Vec<String>,
49    pub recurrence_key: Option<String>,
50    pub conflict_states: Vec<ConflictReviewState>,
51    pub resolution_kinds: Vec<ConflictResolutionKind>,
52    pub unresolved_conflicts_only: bool,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
56pub struct RecallQuery {
57    pub scope: MemoryScope,
58    pub query_text: String,
59    pub max_items: usize,
60    pub token_budget: Option<usize>,
61    pub filters: RecallFilters,
62    pub include_explanation: bool,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
66pub struct TimeTravelRecallRequest {
67    pub query: RecallQuery,
68    pub as_of_unix_ms: u64,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72pub struct RecallScoreBreakdown {
73    pub lexical: f32,
74    pub semantic: f32,
75    pub graph: f32,
76    pub temporal: f32,
77    pub metadata: f32,
78    pub episodic: f32,
79    pub salience: f32,
80    pub curation: f32,
81    pub policy: f32,
82    pub total: f32,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
86pub struct RecallExplanation {
87    pub selected_channels: Vec<String>,
88    pub policy_notes: Vec<String>,
89    pub trace_id: Option<String>,
90    pub planning_trace: Option<RecallPlanningTrace>,
91    pub planning_profile: Option<RecallPlanningProfile>,
92    pub policy_profile: Option<RecallPolicyProfile>,
93    pub scorer_kind: Option<RecallScorerKind>,
94    pub scoring_profile: Option<RecallScoringProfile>,
95}
96
97#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
98pub enum RecallPlannerStage {
99    #[default]
100    CandidateGeneration,
101    GraphExpansion,
102    Selection,
103}
104
105#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
106pub enum RecallCandidateSource {
107    Lexical,
108    Semantic,
109    Metadata,
110    Episode,
111    Graph,
112    Temporal,
113    Provenance,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
117pub struct RecallPlanningTrace {
118    pub trace_id: String,
119    pub token_budget_applied: bool,
120    pub candidates: Vec<RecallTraceCandidate>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub struct RecallTraceCandidate {
125    pub record_id: String,
126    pub kind: MemoryRecordKind,
127    pub selected: bool,
128    pub planner_stage: RecallPlannerStage,
129    pub candidate_sources: Vec<RecallCandidateSource>,
130    pub relation_reasons: Vec<String>,
131    pub selection_rank: Option<u32>,
132    pub matched_terms: Vec<String>,
133    pub selected_channels: Vec<String>,
134    pub filter_reasons: Vec<String>,
135    pub decision_reason: String,
136    pub breakdown: RecallScoreBreakdown,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
140pub struct RecallHit {
141    pub record: MemoryRecord,
142    pub breakdown: RecallScoreBreakdown,
143    pub explanation: Option<RecallExplanation>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147pub struct RecallResult {
148    pub hits: Vec<RecallHit>,
149    pub total_candidates_examined: usize,
150    pub explanation: Option<RecallExplanation>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154pub struct CompactionRequest {
155    pub tenant_id: String,
156    pub namespace: Option<String>,
157    pub dry_run: bool,
158    pub reason: String,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
162pub struct CompactionReport {
163    pub deduplicated_records: u64,
164    pub archived_records: u64,
165    pub summarized_clusters: u64,
166    pub pruned_graph_edges: u64,
167    pub superseded_records: u64,
168    pub lineage_links_created: u64,
169    pub dry_run: bool,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
173#[serde(default)]
174pub struct SynthesisRequest {
175    pub tenant_id: String,
176    pub namespace: Option<String>,
177    pub actor_id: Option<String>,
178    pub conversation_id: Option<String>,
179    pub session_id: Option<String>,
180    pub from_unix_ms: Option<u64>,
181    pub to_unix_ms: Option<u64>,
182    pub min_source_records: usize,
183    pub max_source_records: usize,
184    pub max_proposals: usize,
185    pub dry_run: bool,
186    pub reason: String,
187}
188
189impl Default for SynthesisRequest {
190    fn default() -> Self {
191        Self {
192            tenant_id: String::new(),
193            namespace: None,
194            actor_id: None,
195            conversation_id: None,
196            session_id: None,
197            from_unix_ms: None,
198            to_unix_ms: None,
199            min_source_records: 2,
200            max_source_records: 12,
201            max_proposals: 16,
202            dry_run: true,
203            reason: "memory synthesis proposal".to_string(),
204        }
205    }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
209pub struct SynthesisProposal {
210    pub proposed_record: MemoryRecord,
211    pub source_record_ids: Vec<String>,
212    pub confidence: f32,
213    pub rationale: String,
214    pub metadata: BTreeMap<String, String>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
218pub struct SynthesisReport {
219    pub dry_run: bool,
220    pub scanned_records: u64,
221    pub eligible_records: u64,
222    pub proposed_records: u64,
223    pub persisted_records: u64,
224    pub lineage_links_created: u64,
225    pub proposals: Vec<SynthesisProposal>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
229pub struct SnapshotManifest {
230    pub snapshot_id: String,
231    pub created_at_unix_ms: u64,
232    pub namespaces: Vec<String>,
233    pub record_count: u64,
234    pub storage_bytes: u64,
235    pub engine: EngineTuningInfo,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
239pub struct StoreStatsRequest {
240    pub tenant_id: Option<String>,
241    pub namespace: Option<String>,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
245pub struct NamespaceStats {
246    pub tenant_id: String,
247    pub namespace: String,
248    pub active_records: u64,
249    pub archived_records: u64,
250    pub deleted_records: u64,
251    pub suppressed_records: u64,
252    pub pinned_records: u64,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
256pub struct MaintenanceStats {
257    pub duplicate_candidate_groups: u64,
258    pub duplicate_candidate_records: u64,
259    pub tombstoned_records: u64,
260    pub expired_records: u64,
261    pub stale_idempotency_keys: u64,
262    pub historical_records: u64,
263    pub superseded_records: u64,
264    pub lineage_links: u64,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
268pub struct StoreStatsReport {
269    pub generated_at_unix_ms: u64,
270    pub total_records: u64,
271    pub storage_bytes: u64,
272    pub namespaces: Vec<NamespaceStats>,
273    pub maintenance: MaintenanceStats,
274    pub engine: EngineTuningInfo,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
278#[serde(default)]
279pub struct GraphInspectionRequest {
280    pub tenant_id: Option<String>,
281    pub namespace: Option<String>,
282    pub actor_id: Option<String>,
283    pub conversation_id: Option<String>,
284    pub session_id: Option<String>,
285    pub include_archived: bool,
286    pub include_suppressed: bool,
287    pub include_deleted: bool,
288    pub max_nodes: Option<usize>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
292pub struct GraphInspectionNode {
293    pub record_id: String,
294    pub tenant_id: String,
295    pub namespace: String,
296    pub actor_id: String,
297    pub kind: MemoryRecordKind,
298    pub summary: Option<String>,
299    pub quality_state: MemoryQualityState,
300    pub historical_state: MemoryHistoricalState,
301    pub episode_id: Option<String>,
302    pub continuity_state: Option<EpisodeContinuityState>,
303    pub conflict_state: Option<ConflictReviewState>,
304    pub importance_per_mille: u16,
305    pub updated_at_unix_ms: u64,
306}
307
308#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
309pub enum GraphInspectionEdgeKind {
310    EpisodeMembership,
311    ChronologyPrevious,
312    ChronologyNext,
313    Causal,
314    Related,
315    Lineage,
316    Conflict,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320pub struct GraphInspectionEdge {
321    pub source_id: String,
322    pub target_id: String,
323    pub kind: GraphInspectionEdgeKind,
324    pub details: Vec<String>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
328pub struct GraphInspectionReport {
329    pub generated_at_unix_ms: u64,
330    pub total_records_scanned: u64,
331    pub nodes: Vec<GraphInspectionNode>,
332    pub edges: Vec<GraphInspectionEdge>,
333    pub truncated: bool,
334}
335
336pub fn build_graph_inspection_report(
337    records: &[MemoryRecord],
338    request: &GraphInspectionRequest,
339    generated_at_unix_ms: u64,
340) -> GraphInspectionReport {
341    let mut nodes = Vec::new();
342    let mut included_ids = BTreeSet::new();
343    let mut truncated = false;
344
345    for record in records {
346        if !graph_inspection_includes_record(record, request) {
347            continue;
348        }
349        if request.max_nodes.is_some_and(|limit| nodes.len() >= limit) {
350            truncated = true;
351            break;
352        }
353        included_ids.insert(record.id.clone());
354        nodes.push(graph_inspection_node(record));
355    }
356
357    let mut edge_keys = BTreeSet::new();
358    let mut edges = Vec::new();
359    for record in records {
360        if !included_ids.contains(&record.id) {
361            continue;
362        }
363        graph_inspection_edges(record, &included_ids, &mut edge_keys, &mut edges);
364    }
365
366    GraphInspectionReport {
367        generated_at_unix_ms,
368        total_records_scanned: records.len() as u64,
369        nodes,
370        edges,
371        truncated,
372    }
373}
374
375fn graph_inspection_includes_record(
376    record: &MemoryRecord,
377    request: &GraphInspectionRequest,
378) -> bool {
379    if request
380        .tenant_id
381        .as_ref()
382        .is_some_and(|expected| &record.scope.tenant_id != expected)
383        || request
384            .namespace
385            .as_ref()
386            .is_some_and(|expected| &record.scope.namespace != expected)
387        || request
388            .actor_id
389            .as_ref()
390            .is_some_and(|expected| &record.scope.actor_id != expected)
391        || request
392            .conversation_id
393            .as_ref()
394            .is_some_and(|expected| record.scope.conversation_id.as_ref() != Some(expected))
395        || request
396            .session_id
397            .as_ref()
398            .is_some_and(|expected| record.scope.session_id.as_ref() != Some(expected))
399    {
400        return false;
401    }
402
403    match record.quality_state {
404        MemoryQualityState::Archived => request.include_archived,
405        MemoryQualityState::Suppressed => request.include_suppressed,
406        MemoryQualityState::Deleted => request.include_deleted,
407        _ => true,
408    }
409}
410
411fn graph_inspection_node(record: &MemoryRecord) -> GraphInspectionNode {
412    GraphInspectionNode {
413        record_id: record.id.clone(),
414        tenant_id: record.scope.tenant_id.clone(),
415        namespace: record.scope.namespace.clone(),
416        actor_id: record.scope.actor_id.clone(),
417        kind: record.kind,
418        summary: record.summary.clone(),
419        quality_state: record.quality_state,
420        historical_state: record.historical_state,
421        episode_id: record
422            .episode
423            .as_ref()
424            .map(|episode| episode.episode_id.clone()),
425        continuity_state: record
426            .episode
427            .as_ref()
428            .map(|episode| episode.continuity_state),
429        conflict_state: record.conflict.as_ref().map(|conflict| conflict.state),
430        importance_per_mille: (record.importance_score.clamp(0.0, 1.0) * 1000.0).round() as u16,
431        updated_at_unix_ms: record.updated_at_unix_ms,
432    }
433}
434
435fn push_graph_inspection_edge(
436    edge_keys: &mut BTreeSet<(String, String, GraphInspectionEdgeKind)>,
437    edges: &mut Vec<GraphInspectionEdge>,
438    source_record_id: &str,
439    target_record_id: &str,
440    kind: GraphInspectionEdgeKind,
441    details: Vec<String>,
442) {
443    if source_record_id == target_record_id {
444        return;
445    }
446    let key = (
447        source_record_id.to_string(),
448        target_record_id.to_string(),
449        kind,
450    );
451    if edge_keys.insert(key) {
452        edges.push(GraphInspectionEdge {
453            source_id: source_record_id.to_string(),
454            target_id: target_record_id.to_string(),
455            kind,
456            details,
457        });
458    }
459}
460
461fn graph_inspection_edges(
462    record: &MemoryRecord,
463    included_ids: &BTreeSet<String>,
464    edge_keys: &mut BTreeSet<(String, String, GraphInspectionEdgeKind)>,
465    edges: &mut Vec<GraphInspectionEdge>,
466) {
467    if let Some(episode) = &record.episode {
468        if !episode.episode_id.is_empty() {
469            push_graph_inspection_edge(
470                edge_keys,
471                edges,
472                &record.id,
473                &format!("episode:{}", episode.episode_id),
474                GraphInspectionEdgeKind::EpisodeMembership,
475                vec![format!("continuity_state={:?}", episode.continuity_state)],
476            );
477        }
478        if let Some(previous) = &episode.previous_record_id
479            && included_ids.contains(previous)
480        {
481            push_graph_inspection_edge(
482                edge_keys,
483                edges,
484                &record.id,
485                previous,
486                GraphInspectionEdgeKind::ChronologyPrevious,
487                Vec::new(),
488            );
489        }
490        if let Some(next) = &episode.next_record_id
491            && included_ids.contains(next)
492        {
493            push_graph_inspection_edge(
494                edge_keys,
495                edges,
496                &record.id,
497                next,
498                GraphInspectionEdgeKind::ChronologyNext,
499                Vec::new(),
500            );
501        }
502        for causal_id in &episode.causal_record_ids {
503            if included_ids.contains(causal_id) {
504                push_graph_inspection_edge(
505                    edge_keys,
506                    edges,
507                    &record.id,
508                    causal_id,
509                    GraphInspectionEdgeKind::Causal,
510                    Vec::new(),
511                );
512            }
513        }
514        for related_id in &episode.related_record_ids {
515            if included_ids.contains(related_id) {
516                push_graph_inspection_edge(
517                    edge_keys,
518                    edges,
519                    &record.id,
520                    related_id,
521                    GraphInspectionEdgeKind::Related,
522                    Vec::new(),
523                );
524            }
525        }
526    }
527
528    for link in &record.lineage {
529        if included_ids.contains(&link.record_id) {
530            push_graph_inspection_edge(
531                edge_keys,
532                edges,
533                &record.id,
534                &link.record_id,
535                GraphInspectionEdgeKind::Lineage,
536                vec![
537                    format!("relation={:?}", link.relation),
538                    format!(
539                        "confidence_per_mille={}",
540                        (link.confidence.clamp(0.0, 1.0) * 1000.0).round() as u16
541                    ),
542                ],
543            );
544        }
545    }
546
547    if let Some(conflict) = &record.conflict {
548        for conflicting_id in &conflict.conflicting_record_ids {
549            if included_ids.contains(conflicting_id) {
550                push_graph_inspection_edge(
551                    edge_keys,
552                    edges,
553                    &record.id,
554                    conflicting_id,
555                    GraphInspectionEdgeKind::Conflict,
556                    vec![
557                        format!("state={:?}", conflict.state),
558                        format!("resolution={:?}", conflict.resolution),
559                    ],
560                );
561            }
562        }
563    }
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
567pub struct IntegrityCheckRequest {
568    pub tenant_id: Option<String>,
569    pub namespace: Option<String>,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
573pub struct IntegrityCheckReport {
574    pub generated_at_unix_ms: u64,
575    pub healthy: bool,
576    pub scanned_records: u64,
577    pub scanned_idempotency_keys: u64,
578    pub stale_idempotency_keys: u64,
579    pub missing_idempotency_keys: u64,
580    pub duplicate_active_records: u64,
581}
582
583#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
584pub struct RepairRequest {
585    pub tenant_id: Option<String>,
586    pub namespace: Option<String>,
587    pub dry_run: bool,
588    pub reason: String,
589    pub remove_stale_idempotency_keys: bool,
590    pub rebuild_missing_idempotency_keys: bool,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
594pub struct RepairReport {
595    pub dry_run: bool,
596    pub scanned_records: u64,
597    pub scanned_idempotency_keys: u64,
598    pub removed_stale_idempotency_keys: u64,
599    pub rebuilt_missing_idempotency_keys: u64,
600    pub healthy_after: bool,
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
604#[serde(default)]
605pub struct MaintenanceRunRequest {
606    pub tenant_id: Option<String>,
607    pub namespace: Option<String>,
608    pub dry_run: bool,
609    pub reason: String,
610    pub run_integrity_check: bool,
611    pub run_repair: bool,
612    pub run_compaction: bool,
613    pub run_synthesis: bool,
614    pub remove_stale_idempotency_keys: bool,
615    pub rebuild_missing_idempotency_keys: bool,
616}
617
618impl Default for MaintenanceRunRequest {
619    fn default() -> Self {
620        Self {
621            tenant_id: None,
622            namespace: None,
623            dry_run: true,
624            reason: "manual maintenance run".to_string(),
625            run_integrity_check: true,
626            run_repair: true,
627            run_compaction: true,
628            run_synthesis: false,
629            remove_stale_idempotency_keys: true,
630            rebuild_missing_idempotency_keys: true,
631        }
632    }
633}
634
635#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
636pub struct MaintenanceRunReport {
637    pub dry_run: bool,
638    pub integrity_before: Option<IntegrityCheckReport>,
639    pub repair: Option<RepairReport>,
640    pub compaction: Option<CompactionReport>,
641    pub synthesis: Option<SynthesisReport>,
642    pub integrity_after: Option<IntegrityCheckReport>,
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
646#[serde(default)]
647pub struct SnapshotShipRequest {
648    pub target_url: String,
649    pub bearer_token: Option<String>,
650    pub tenant_id: Option<String>,
651    pub namespace: Option<String>,
652    pub include_archived: bool,
653    pub mode: ImportMode,
654    pub dry_run: bool,
655}
656
657impl Default for SnapshotShipRequest {
658    fn default() -> Self {
659        Self {
660            target_url: String::new(),
661            bearer_token: None,
662            tenant_id: None,
663            namespace: None,
664            include_archived: false,
665            mode: ImportMode::Validate,
666            dry_run: true,
667        }
668    }
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
672pub struct SnapshotShipReport {
673    pub target_url: String,
674    pub exported_records: u64,
675    pub imported_records: u64,
676    pub skipped_records: u64,
677    pub dry_run: bool,
678    pub compatible_package: bool,
679    pub remote_status: u16,
680    pub remote_snapshot_id: Option<String>,
681}
682
683#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
684pub enum TraceOperationKind {
685    Upsert,
686    BatchUpsert,
687    Recall,
688    Snapshot,
689    Stats,
690    IntegrityCheck,
691    Repair,
692    Compact,
693    Delete,
694    Archive,
695    Suppress,
696    Recover,
697    Export,
698    Import,
699    MaintenanceRun,
700    SnapshotShip,
701    Changefeed,
702    Synthesis,
703}
704
705#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
706pub enum ChangefeedEventKind {
707    Upserted,
708    Deleted,
709    Archived,
710    Suppressed,
711    Recovered,
712    Compacted,
713    Imported,
714    RetentionArchived,
715    RetentionDeleted,
716}
717
718#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
719#[serde(default)]
720pub struct ChangefeedRequest {
721    pub tenant_id: Option<String>,
722    pub namespace: Option<String>,
723    pub after_sequence: Option<u64>,
724    pub limit: Option<usize>,
725}
726
727#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
728pub struct ChangefeedEvent {
729    pub sequence: u64,
730    pub event_id: String,
731    pub kind: ChangefeedEventKind,
732    pub tenant_id: String,
733    pub namespace: String,
734    pub record_id: Option<String>,
735    pub occurred_at_unix_ms: u64,
736    pub summary: Option<String>,
737    pub record: Option<MemoryRecord>,
738}
739
740#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
741pub struct ChangefeedReport {
742    pub events: Vec<ChangefeedEvent>,
743    pub last_sequence: Option<u64>,
744    pub truncated: bool,
745}
746
747#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
748pub enum TraceStatus {
749    Ok,
750    Rejected,
751    Error,
752}
753
754#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
755pub struct OperationTraceSummary {
756    pub record_id: Option<String>,
757    pub request_count: Option<u32>,
758    pub query_text: Option<String>,
759    pub max_items: Option<u32>,
760    pub token_budget: Option<u32>,
761    pub dry_run: Option<bool>,
762}
763
764#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
765pub struct OperationTrace {
766    pub trace_id: String,
767    pub correlation_id: String,
768    pub operation: TraceOperationKind,
769    pub transport: String,
770    pub backend: Option<String>,
771    pub admission_class: Option<String>,
772    pub tenant_id: Option<String>,
773    pub namespace: Option<String>,
774    pub principal: Option<String>,
775    pub store_span_id: Option<String>,
776    pub planning_trace_id: Option<String>,
777    pub started_at_unix_ms: u64,
778    pub completed_at_unix_ms: u64,
779    pub latency_ms: u64,
780    pub status: TraceStatus,
781    pub status_message: Option<String>,
782    pub summary: OperationTraceSummary,
783    pub recall_explanation: Option<RecallExplanation>,
784}
785
786#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
787pub struct TraceListRequest {
788    pub tenant_id: Option<String>,
789    pub namespace: Option<String>,
790    pub operation: Option<TraceOperationKind>,
791    pub status: Option<TraceStatus>,
792    pub before_started_at_unix_ms: Option<u64>,
793    pub limit: Option<usize>,
794}
795
796#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
797pub struct PortableRecord {
798    pub record: MemoryRecord,
799    pub idempotency_key: Option<String>,
800}
801
802#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
803pub struct ExportRequest {
804    pub tenant_id: Option<String>,
805    pub namespace: Option<String>,
806    pub include_archived: bool,
807}
808
809#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
810pub struct PortableStorePackage {
811    pub package_version: u32,
812    pub exported_at_unix_ms: u64,
813    pub manifest: SnapshotManifest,
814    pub records: Vec<PortableRecord>,
815}
816
817#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
818pub enum ImportMode {
819    Validate,
820    Merge,
821    Replace,
822}
823
824#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
825pub struct ImportRequest {
826    pub package: PortableStorePackage,
827    pub mode: ImportMode,
828    pub dry_run: bool,
829}
830
831#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
832pub struct ImportFailure {
833    pub record_id: Option<String>,
834    pub reason: String,
835}
836
837#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
838pub struct ImportReport {
839    pub mode: ImportMode,
840    pub dry_run: bool,
841    pub applied: bool,
842    pub compatible_package: bool,
843    pub package_version: u32,
844    pub validated_records: u64,
845    pub imported_records: u64,
846    pub skipped_records: u64,
847    pub replaced_existing: bool,
848    pub snapshot_id: String,
849    pub failed_records: Vec<ImportFailure>,
850}
851
852#[cfg(test)]
853mod tests {
854    use super::{GraphInspectionEdgeKind, GraphInspectionRequest, build_graph_inspection_report};
855    use crate::model::{
856        ConflictAnnotation, ConflictReviewState, EpisodeContext, EpisodeContinuityState,
857        LineageLink, LineageRelationKind, MemoryHistoricalState, MemoryQualityState, MemoryRecord,
858        MemoryRecordKind, MemoryScope, MemoryTrustLevel,
859    };
860    use std::collections::BTreeMap;
861
862    fn scope() -> MemoryScope {
863        MemoryScope {
864            tenant_id: "tenant-a".to_string(),
865            namespace: "ops".to_string(),
866            actor_id: "ava".to_string(),
867            conversation_id: Some("thread-1".to_string()),
868            session_id: Some("session-1".to_string()),
869            source: "test".to_string(),
870            labels: Vec::new(),
871            trust_level: MemoryTrustLevel::Verified,
872        }
873    }
874
875    fn record(id: &str) -> MemoryRecord {
876        MemoryRecord {
877            id: id.to_string(),
878            scope: scope(),
879            kind: MemoryRecordKind::Episodic,
880            content: format!("content {id}"),
881            summary: Some(format!("summary {id}")),
882            source_id: None,
883            metadata: BTreeMap::new(),
884            quality_state: MemoryQualityState::Active,
885            created_at_unix_ms: 1,
886            updated_at_unix_ms: 2,
887            expires_at_unix_ms: None,
888            importance_score: 0.5,
889            artifact: None,
890            episode: None,
891            historical_state: MemoryHistoricalState::Current,
892            lineage: Vec::new(),
893            conflict: None,
894        }
895    }
896
897    #[test]
898    fn graph_inspection_builds_typed_edges_and_filters_hidden_records() {
899        let mut seed = record("seed");
900        seed.episode = Some(EpisodeContext {
901            episode_id: "incident".to_string(),
902            continuity_state: EpisodeContinuityState::Open,
903            next_record_id: Some("next".to_string()),
904            related_record_ids: vec!["next".to_string()],
905            ..Default::default()
906        });
907
908        let mut next = record("next");
909        next.episode = Some(EpisodeContext {
910            episode_id: "incident".to_string(),
911            continuity_state: EpisodeContinuityState::Open,
912            previous_record_id: Some("seed".to_string()),
913            causal_record_ids: vec!["seed".to_string()],
914            ..Default::default()
915        });
916        next.lineage = vec![LineageLink {
917            record_id: "seed".to_string(),
918            relation: LineageRelationKind::DerivedFrom,
919            confidence: 0.8,
920        }];
921        next.conflict = Some(ConflictAnnotation {
922            state: ConflictReviewState::PotentialConflict,
923            conflicting_record_ids: vec!["seed".to_string()],
924            ..Default::default()
925        });
926
927        let mut hidden = record("hidden");
928        hidden.quality_state = MemoryQualityState::Suppressed;
929
930        let report = build_graph_inspection_report(
931            &[seed, next, hidden],
932            &GraphInspectionRequest {
933                tenant_id: Some("tenant-a".to_string()),
934                namespace: Some("ops".to_string()),
935                ..Default::default()
936            },
937            42,
938        );
939
940        assert_eq!(report.generated_at_unix_ms, 42);
941        assert_eq!(report.nodes.len(), 2);
942        assert!(!report.nodes.iter().any(|node| node.record_id == "hidden"));
943        assert!(report.edges.iter().any(|edge| matches!(
944            edge.kind,
945            GraphInspectionEdgeKind::ChronologyNext | GraphInspectionEdgeKind::ChronologyPrevious
946        )));
947        assert!(
948            report
949                .edges
950                .iter()
951                .any(|edge| edge.kind == GraphInspectionEdgeKind::Causal)
952        );
953        assert!(
954            report
955                .edges
956                .iter()
957                .any(|edge| edge.kind == GraphInspectionEdgeKind::Related)
958        );
959        assert!(
960            report
961                .edges
962                .iter()
963                .any(|edge| edge.kind == GraphInspectionEdgeKind::Lineage)
964        );
965        assert!(
966            report
967                .edges
968                .iter()
969                .any(|edge| edge.kind == GraphInspectionEdgeKind::Conflict)
970        );
971    }
972}