Skip to main content

cerememory_core/
protocol.rs

1//! CMP (Cerememory Protocol) request and response types.
2//!
3//! Implements CMP Spec v1.0 — all operation categories:
4//! Encode, Recall, Lifecycle, Introspect, plus error handling and versioning.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use crate::types::*;
11
12// ─── Client-facing error messages (sanitized, no internal details) ───
13
14/// Opaque client-facing message for storage errors.
15pub const STORAGE_ERROR_MESSAGE: &str = "An internal storage error occurred";
16/// Opaque client-facing message for serialization errors.
17pub const SERIALIZATION_ERROR_MESSAGE: &str = "An internal data processing error occurred";
18/// Opaque client-facing message for export errors.
19pub const EXPORT_ERROR_MESSAGE: &str = "An internal error occurred during export";
20/// Opaque client-facing message for generic internal errors.
21pub const INTERNAL_ERROR_MESSAGE: &str = "An internal error occurred";
22
23// ─── Protocol Versioning (CMP Spec §8) ───────────────────────────────
24
25/// Protocol version header included in all CMP messages.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CMPHeader {
28    pub protocol: String,
29    pub version: String,
30    pub request_id: Uuid,
31    pub timestamp: DateTime<Utc>,
32}
33
34impl CMPHeader {
35    pub fn new() -> Self {
36        Self {
37            protocol: "cmp".to_string(),
38            version: "1.0".to_string(),
39            request_id: Uuid::now_v7(),
40            timestamp: Utc::now(),
41        }
42    }
43
44    pub fn is_compatible(&self) -> bool {
45        self.protocol == "cmp" && self.version.starts_with("1.")
46    }
47}
48
49impl Default for CMPHeader {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55// ─── Encode Operations (CMP Spec §3) ─────────────────────────────────
56
57/// Context for encoding operations.
58#[derive(Debug, Clone, Default, Serialize, Deserialize)]
59#[serde(default)]
60pub struct EncodeContext {
61    pub source: Option<String>,
62    pub session_id: Option<String>,
63    pub spatial: Option<serde_json::Value>,
64    pub temporal: Option<serde_json::Value>,
65}
66
67/// Manual association hint provided during encoding.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ManualAssociation {
70    pub target_id: Uuid,
71    pub association_type: AssociationType,
72    pub weight: f64,
73}
74
75/// encode.store request (CMP Spec §3.1) — Store a new memory record.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct EncodeStoreRequest {
78    #[serde(default)]
79    pub header: Option<CMPHeader>,
80    pub content: MemoryContent,
81    #[serde(default)]
82    pub store: Option<StoreType>,
83    #[serde(default)]
84    pub emotion: Option<EmotionVector>,
85    #[serde(default)]
86    pub context: Option<EncodeContext>,
87    #[serde(default)]
88    pub metadata: Option<serde_json::Value>,
89    #[serde(default)]
90    pub associations: Option<Vec<ManualAssociation>>,
91}
92
93/// encode.store response (CMP Spec §3.1).
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct EncodeStoreResponse {
96    pub record_id: Uuid,
97    pub store: StoreType,
98    pub initial_fidelity: f64,
99    pub associations_created: u32,
100}
101
102/// encode.batch request (CMP Spec §3.2) — Store multiple records.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct EncodeBatchRequest {
105    #[serde(default)]
106    pub header: Option<CMPHeader>,
107    pub records: Vec<EncodeStoreRequest>,
108    #[serde(default)]
109    pub infer_associations: bool,
110}
111
112/// encode.batch response (CMP Spec §3.2).
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct EncodeBatchResponse {
115    pub results: Vec<EncodeStoreResponse>,
116    pub associations_inferred: u32,
117}
118
119/// encode.update request (CMP Spec §3.3) — Update an existing record.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct EncodeUpdateRequest {
122    #[serde(default)]
123    pub header: Option<CMPHeader>,
124    pub record_id: Uuid,
125    #[serde(default)]
126    pub content: Option<MemoryContent>,
127    #[serde(default)]
128    pub emotion: Option<EmotionVector>,
129    #[serde(default)]
130    pub metadata: Option<serde_json::Value>,
131}
132
133/// encode.store_raw request — append a raw journal record.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct EncodeStoreRawRequest {
136    #[serde(default)]
137    pub header: Option<CMPHeader>,
138    pub session_id: String,
139    #[serde(default)]
140    pub turn_id: Option<String>,
141    #[serde(default)]
142    pub topic_id: Option<String>,
143    pub source: RawSource,
144    pub speaker: RawSpeaker,
145    pub visibility: RawVisibility,
146    pub secrecy_level: SecrecyLevel,
147    pub content: MemoryContent,
148    #[serde(default)]
149    pub metadata: Option<serde_json::Value>,
150}
151
152/// encode.store_raw response.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct EncodeStoreRawResponse {
155    pub record_id: Uuid,
156    pub session_id: String,
157    pub visibility: RawVisibility,
158    pub secrecy_level: SecrecyLevel,
159}
160
161/// encode.batch_raw request — append multiple raw journal records.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct EncodeBatchStoreRawRequest {
164    #[serde(default)]
165    pub header: Option<CMPHeader>,
166    pub records: Vec<EncodeStoreRawRequest>,
167}
168
169/// encode.batch_raw response.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct EncodeBatchStoreRawResponse {
172    pub results: Vec<EncodeStoreRawResponse>,
173}
174
175// ─── Recall Operations (CMP Spec §4) ─────────────────────────────────
176
177/// Multimodal recall cue (CMP Spec §4.1).
178#[derive(Debug, Clone, Default, Serialize, Deserialize)]
179#[serde(default)]
180pub struct RecallCue {
181    pub text: Option<String>,
182    /// Raw image bytes. The engine auto-detects common formats (PNG, JPEG, GIF, WebP).
183    pub image: Option<Vec<u8>>,
184    /// Raw audio bytes. The engine auto-detects common formats (WAV, MP3, FLAC, OGG, MP4/WebM).
185    pub audio: Option<Vec<u8>>,
186    pub emotion: Option<EmotionVector>,
187    pub temporal: Option<TemporalRange>,
188    pub spatial: Option<serde_json::Value>,
189    pub semantic: Option<serde_json::Value>,
190    /// Optional embedding vector for semantic similarity search.
191    pub embedding: Option<Vec<f32>>,
192}
193
194/// Temporal range filter for recall.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct TemporalRange {
197    pub start: DateTime<Utc>,
198    pub end: DateTime<Utc>,
199}
200
201/// recall.query request (CMP Spec §4.1).
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct RecallQueryRequest {
204    #[serde(default)]
205    pub header: Option<CMPHeader>,
206    pub cue: RecallCue,
207    #[serde(default)]
208    pub stores: Option<Vec<StoreType>>,
209    #[serde(default = "default_recall_limit")]
210    pub limit: u32,
211    #[serde(default)]
212    pub min_fidelity: Option<f64>,
213    #[serde(default)]
214    pub include_decayed: bool,
215    #[serde(default = "default_true")]
216    pub reconsolidate: bool,
217    #[serde(default = "default_activation_depth")]
218    pub activation_depth: u32,
219    #[serde(default = "default_recall_mode")]
220    pub recall_mode: RecallMode,
221}
222
223/// Default recall query limit.
224pub const DEFAULT_RECALL_LIMIT: u32 = 10;
225/// Default reconsolidate flag for recall queries.
226pub const DEFAULT_RECONSOLIDATE: bool = true;
227/// Default activation depth for spreading activation.
228pub const DEFAULT_ACTIVATION_DEPTH: u32 = 2;
229
230fn default_recall_limit() -> u32 {
231    DEFAULT_RECALL_LIMIT
232}
233fn default_true() -> bool {
234    DEFAULT_RECONSOLIDATE
235}
236fn default_activation_depth() -> u32 {
237    DEFAULT_ACTIVATION_DEPTH
238}
239fn default_recall_mode() -> RecallMode {
240    RecallMode::Human
241}
242
243/// A single recalled memory with relevance scoring (CMP Spec §4.1).
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct RecalledMemory {
246    pub record: MemoryRecord,
247    pub relevance_score: f64,
248    #[serde(default)]
249    pub activation_path: Option<Vec<Uuid>>,
250    pub rendered_content: MemoryContent,
251}
252
253/// Activation trace for debugging recall paths.
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct ActivationTrace {
256    pub source_id: Uuid,
257    pub activations: Vec<ActivationNode>,
258}
259
260/// A single node in an activation trace.
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct ActivationNode {
263    pub record_id: Uuid,
264    pub activation_level: f64,
265    pub hop: u32,
266    pub edge_type: AssociationType,
267}
268
269/// Metadata about a recall query execution.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct QueryMetadata {
272    pub total_records_scanned: u32,
273    pub stores_searched: Vec<StoreType>,
274    pub fidelity_filtered: u32,
275}
276
277/// recall.query response (CMP Spec §4.1).
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct RecallQueryResponse {
280    pub memories: Vec<RecalledMemory>,
281    #[serde(default)]
282    pub activation_trace: Option<ActivationTrace>,
283    pub total_candidates: u32,
284    #[serde(default, skip_serializing_if = "Option::is_none")]
285    pub query_metadata: Option<QueryMetadata>,
286}
287
288/// recall.associate request (CMP Spec §4.2).
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct RecallAssociateRequest {
291    #[serde(default)]
292    pub header: Option<CMPHeader>,
293    pub record_id: Uuid,
294    #[serde(default)]
295    pub association_types: Option<Vec<AssociationType>>,
296    #[serde(default = "default_activation_depth")]
297    pub depth: u32,
298    #[serde(default = "default_min_weight")]
299    pub min_weight: f64,
300    #[serde(default = "default_recall_limit")]
301    pub limit: u32,
302}
303
304fn default_min_weight() -> f64 {
305    0.1
306}
307
308/// recall.associate response.
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct RecallAssociateResponse {
311    pub memories: Vec<RecalledMemory>,
312    pub total_candidates: u32,
313}
314
315/// Time granularity for timeline queries.
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
317#[serde(rename_all = "lowercase")]
318pub enum TimeGranularity {
319    Minute,
320    Hour,
321    Day,
322    Week,
323    Month,
324}
325
326/// recall.timeline request (CMP Spec §4.3, OPTIONAL).
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct RecallTimelineRequest {
329    #[serde(default)]
330    pub header: Option<CMPHeader>,
331    pub range: TemporalRange,
332    #[serde(default = "default_granularity")]
333    pub granularity: TimeGranularity,
334    #[serde(default)]
335    pub min_fidelity: Option<f64>,
336    #[serde(default)]
337    pub emotion_filter: Option<EmotionVector>,
338}
339
340fn default_granularity() -> TimeGranularity {
341    TimeGranularity::Hour
342}
343
344/// recall.graph request (CMP Spec §4.4, OPTIONAL).
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct RecallGraphRequest {
347    #[serde(default)]
348    pub header: Option<CMPHeader>,
349    #[serde(default)]
350    pub center_id: Option<Uuid>,
351    #[serde(default = "default_activation_depth")]
352    pub depth: u32,
353    #[serde(default)]
354    pub edge_types: Option<Vec<String>>,
355    #[serde(default = "default_recall_limit")]
356    pub limit_nodes: u32,
357}
358
359/// recall.raw_query request — explicit retrieval from the raw journal.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct RecallRawQueryRequest {
362    #[serde(default)]
363    pub header: Option<CMPHeader>,
364    #[serde(default)]
365    pub session_id: Option<String>,
366    #[serde(default)]
367    pub query: Option<String>,
368    #[serde(default)]
369    pub temporal: Option<TemporalRange>,
370    #[serde(default = "default_recall_limit")]
371    pub limit: u32,
372    #[serde(default)]
373    pub include_private_scratch: bool,
374    #[serde(default)]
375    pub include_sealed: bool,
376    #[serde(default)]
377    pub secrecy_levels: Option<Vec<SecrecyLevel>>,
378}
379
380/// recall.raw_query response.
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct RecallRawQueryResponse {
383    pub records: Vec<RawJournalRecord>,
384    pub total_candidates: u32,
385}
386
387/// recall.timeline response (CMP Spec §4.3).
388#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct RecallTimelineResponse {
390    pub buckets: Vec<TimelineBucket>,
391}
392
393/// A single time bucket in a timeline query.
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct TimelineBucket {
396    pub start: DateTime<Utc>,
397    pub end: DateTime<Utc>,
398    pub memories: Vec<RecalledMemory>,
399    pub count: u32,
400}
401
402/// recall.graph response (CMP Spec §4.4).
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct RecallGraphResponse {
405    pub nodes: Vec<GraphNode>,
406    pub edges: Vec<GraphEdge>,
407    pub total_nodes: u32,
408}
409
410/// A node in the memory graph.
411#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct GraphNode {
413    pub id: Uuid,
414    pub store: StoreType,
415    pub summary: Option<String>,
416    pub fidelity: f64,
417}
418
419/// An edge in the memory graph.
420#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct GraphEdge {
422    pub source: Uuid,
423    pub target: Uuid,
424    pub edge_type: AssociationType,
425    pub weight: f64,
426}
427
428// ─── Lifecycle Operations (CMP Spec §5) ──────────────────────────────
429
430/// Consolidation strategy.
431#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
432#[serde(rename_all = "lowercase")]
433pub enum ConsolidationStrategy {
434    Full,
435    Incremental,
436    Selective,
437}
438
439/// lifecycle.consolidate request (CMP Spec §5.1).
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct ConsolidateRequest {
442    #[serde(default)]
443    pub header: Option<CMPHeader>,
444    #[serde(default = "default_consolidation_strategy")]
445    pub strategy: ConsolidationStrategy,
446    #[serde(default)]
447    pub min_age_hours: u32,
448    #[serde(default)]
449    pub min_access_count: u32,
450    #[serde(default)]
451    pub dry_run: bool,
452}
453
454fn default_consolidation_strategy() -> ConsolidationStrategy {
455    ConsolidationStrategy::Incremental
456}
457
458/// lifecycle.consolidate response (CMP Spec §5.1).
459#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct ConsolidateResponse {
461    pub records_processed: u32,
462    pub records_migrated: u32,
463    pub records_compressed: u32,
464    pub records_pruned: u32,
465    pub semantic_nodes_created: u32,
466}
467
468/// lifecycle.dream_tick request — process uncurated raw journal entries into episodic summaries.
469#[derive(Debug, Clone, Serialize, Deserialize)]
470pub struct DreamTickRequest {
471    #[serde(default)]
472    pub header: Option<CMPHeader>,
473    #[serde(default)]
474    pub session_id: Option<String>,
475    #[serde(default)]
476    pub dry_run: bool,
477    #[serde(default = "default_recall_limit")]
478    pub max_groups: u32,
479    #[serde(default)]
480    pub include_private_scratch: bool,
481    #[serde(default)]
482    pub include_sealed: bool,
483    #[serde(default = "default_true")]
484    pub promote_semantic: bool,
485    #[serde(default)]
486    pub secrecy_levels: Option<Vec<SecrecyLevel>>,
487}
488
489/// lifecycle.dream_tick response.
490#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct DreamTickResponse {
492    pub groups_processed: u32,
493    pub raw_records_processed: u32,
494    pub episodic_summaries_created: u32,
495    pub semantic_nodes_created: u32,
496}
497
498/// lifecycle.decay_tick request (CMP Spec §5.2).
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct DecayTickRequest {
501    #[serde(default)]
502    pub header: Option<CMPHeader>,
503    #[serde(default)]
504    pub tick_duration_seconds: Option<u32>,
505}
506
507/// lifecycle.decay_tick response (CMP Spec §5.2).
508#[derive(Debug, Clone, Serialize, Deserialize)]
509pub struct DecayTickResponse {
510    pub records_updated: u32,
511    pub records_below_threshold: u32,
512    pub records_pruned: u32,
513}
514
515/// lifecycle.set_mode request (CMP Spec §5.3).
516#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct SetModeRequest {
518    #[serde(default)]
519    pub header: Option<CMPHeader>,
520    pub mode: RecallMode,
521    #[serde(default)]
522    pub scope: Option<Vec<StoreType>>,
523}
524
525/// lifecycle.forget request (CMP Spec §5.4).
526#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct ForgetRequest {
528    #[serde(default)]
529    pub header: Option<CMPHeader>,
530    #[serde(default)]
531    pub record_ids: Option<Vec<Uuid>>,
532    #[serde(default)]
533    pub store: Option<StoreType>,
534    #[serde(default)]
535    pub temporal_range: Option<TemporalRange>,
536    #[serde(default)]
537    pub cascade: bool,
538    pub confirm: bool,
539}
540
541/// lifecycle.forget response (CMP Spec §5.4).
542#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct ForgetResponse {
544    pub records_deleted: u32,
545}
546
547/// lifecycle.export request (CMP Spec §5.5).
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct ExportRequest {
550    #[serde(default)]
551    pub header: Option<CMPHeader>,
552    #[serde(default = "default_format")]
553    pub format: String,
554    #[serde(default)]
555    pub stores: Option<Vec<StoreType>>,
556    #[serde(default)]
557    pub include_raw_journal: bool,
558    #[serde(default)]
559    pub encrypt: bool,
560    #[serde(default)]
561    pub encryption_key: Option<String>,
562}
563
564fn default_format() -> String {
565    "cma".to_string()
566}
567
568/// lifecycle.export response (CMP Spec §5.5).
569#[derive(Debug, Clone, Serialize, Deserialize)]
570pub struct ExportResponse {
571    pub archive_id: String,
572    pub size_bytes: u64,
573    pub record_count: u32,
574    pub checksum: String,
575}
576
577/// HTTP-friendly export response that includes the archive payload.
578#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct ExportArchiveResponse {
580    #[serde(flatten)]
581    pub metadata: ExportResponse,
582    pub archive_data: Vec<u8>,
583}
584
585/// Import conflict resolution strategy.
586#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
587#[serde(rename_all = "snake_case")]
588pub enum ConflictResolution {
589    KeepExisting,
590    KeepImported,
591    #[default]
592    KeepNewer,
593}
594
595/// Import strategy.
596#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
597#[serde(rename_all = "lowercase")]
598pub enum ImportStrategy {
599    Merge,
600    Replace,
601}
602
603/// lifecycle.import request (CMP Spec §5.6).
604#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct ImportRequest {
606    #[serde(default)]
607    pub header: Option<CMPHeader>,
608    pub archive_id: String,
609    #[serde(default = "default_import_strategy")]
610    pub strategy: ImportStrategy,
611    #[serde(default = "default_conflict_resolution")]
612    pub conflict_resolution: ConflictResolution,
613    #[serde(default)]
614    pub decryption_key: Option<String>,
615    /// Raw CMA archive bytes (optionally encrypted).
616    #[serde(default)]
617    pub archive_data: Option<Vec<u8>>,
618}
619
620/// lifecycle.import response (HTTP/SDK friendly).
621#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct ImportResponse {
623    pub records_imported: u32,
624}
625
626fn default_import_strategy() -> ImportStrategy {
627    ImportStrategy::Merge
628}
629
630fn default_conflict_resolution() -> ConflictResolution {
631    ConflictResolution::KeepNewer
632}
633
634// ─── Introspect Operations (CMP Spec §6) ─────────────────────────────
635
636/// introspect.stats response (CMP Spec §6.1).
637#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct StatsResponse {
639    pub total_records: u32,
640    pub records_by_store: std::collections::HashMap<StoreType, u32>,
641    pub total_associations: u32,
642    pub avg_fidelity: f64,
643    pub avg_fidelity_by_store: std::collections::HashMap<StoreType, f64>,
644    #[serde(default)]
645    pub oldest_record: Option<DateTime<Utc>>,
646    #[serde(default)]
647    pub newest_record: Option<DateTime<Utc>>,
648    pub total_recall_count: u64,
649    #[serde(default)]
650    pub raw_journal_records: u32,
651    #[serde(default)]
652    pub raw_journal_pending_dream: u32,
653    #[serde(default)]
654    pub dream_episodic_summaries: u32,
655    #[serde(default)]
656    pub dream_semantic_nodes: u32,
657    #[serde(default)]
658    pub last_dream_tick_at: Option<DateTime<Utc>>,
659    #[serde(default)]
660    pub evolution_metrics: Option<EvolutionMetrics>,
661    /// Whether background decay is enabled and running.
662    #[serde(default)]
663    pub background_decay_enabled: bool,
664    /// Whether background dream processing is enabled and running.
665    #[serde(default)]
666    pub background_dream_enabled: bool,
667}
668
669/// Parameter adjustment record.
670#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct ParameterAdjustment {
672    pub store: StoreType,
673    pub parameter: String,
674    pub original_value: f64,
675    pub current_value: f64,
676    pub reason: String,
677}
678
679/// Evolution metrics (CMP Spec §6.4, OPTIONAL).
680#[derive(Debug, Clone, Default, Serialize, Deserialize)]
681#[serde(default)]
682pub struct EvolutionMetrics {
683    pub parameter_adjustments: Vec<ParameterAdjustment>,
684    pub detected_patterns: Vec<String>,
685    pub schema_adaptations: Vec<String>,
686}
687
688/// introspect.record request (CMP Spec §6.2).
689#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct RecordIntrospectRequest {
691    #[serde(default)]
692    pub header: Option<CMPHeader>,
693    pub record_id: Uuid,
694    #[serde(default)]
695    pub include_history: bool,
696    #[serde(default)]
697    pub include_associations: bool,
698    #[serde(default)]
699    pub include_versions: bool,
700}
701
702/// introspect.decay_forecast request (CMP Spec §6.3, OPTIONAL).
703#[derive(Debug, Clone, Serialize, Deserialize)]
704pub struct DecayForecastRequest {
705    #[serde(default)]
706    pub header: Option<CMPHeader>,
707    pub record_ids: Vec<Uuid>,
708    pub forecast_at: DateTime<Utc>,
709}
710
711/// A single record's decay forecast.
712#[derive(Debug, Clone, Serialize, Deserialize)]
713pub struct DecayForecast {
714    pub record_id: Uuid,
715    pub current_fidelity: f64,
716    pub forecasted_fidelity: f64,
717    #[serde(default)]
718    pub estimated_threshold_date: Option<DateTime<Utc>>,
719}
720
721/// introspect.decay_forecast response (CMP Spec §6.3).
722#[derive(Debug, Clone, Serialize, Deserialize)]
723pub struct DecayForecastResponse {
724    pub forecasts: Vec<DecayForecast>,
725}
726
727// ─── Error Handling (CMP Spec §7) ────────────────────────────────────
728
729/// CMP error codes (CMP Spec §7).
730#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
731#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
732pub enum CMPErrorCode {
733    RecordNotFound,
734    StoreInvalid,
735    ContentTooLarge,
736    ValidationError,
737    ModalityUnsupported,
738    WorkingMemoryFull,
739    DecayEngineBusy,
740    ConsolidationInProgress,
741    ExportFailed,
742    ImportConflict,
743    ForgetUnconfirmed,
744    VersionMismatch,
745    Unauthorized,
746    RateLimited,
747    InternalError,
748}
749
750/// Standardized CMP error envelope (CMP Spec §7).
751#[derive(Debug, Clone, Serialize, Deserialize)]
752pub struct CMPError {
753    pub code: CMPErrorCode,
754    pub message: String,
755    #[serde(default)]
756    pub details: Option<serde_json::Value>,
757    #[serde(default)]
758    pub retry_after: Option<u32>,
759    #[serde(default, skip_serializing_if = "Option::is_none")]
760    pub request_id: Option<Uuid>,
761}
762
763impl CMPError {
764    pub fn new(code: CMPErrorCode, message: impl Into<String>) -> Self {
765        Self {
766            code,
767            message: message.into(),
768            details: None,
769            retry_after: None,
770            request_id: None,
771        }
772    }
773}
774
775impl From<&crate::error::CerememoryError> for CMPError {
776    fn from(err: &crate::error::CerememoryError) -> Self {
777        use crate::error::CerememoryError;
778        match err {
779            CerememoryError::RecordNotFound(id) => CMPError::new(
780                CMPErrorCode::RecordNotFound,
781                format!("Record not found: {id}"),
782            ),
783            CerememoryError::StoreInvalid(s) => {
784                CMPError::new(CMPErrorCode::StoreInvalid, format!("Invalid store: {s}"))
785            }
786            CerememoryError::ContentTooLarge { size, limit } => CMPError::new(
787                CMPErrorCode::ContentTooLarge,
788                format!("{size} bytes exceeds {limit}"),
789            ),
790            CerememoryError::ModalityUnsupported(m) => CMPError::new(
791                CMPErrorCode::ModalityUnsupported,
792                format!("Unsupported: {m}"),
793            ),
794            CerememoryError::WorkingMemoryFull => CMPError::new(
795                CMPErrorCode::WorkingMemoryFull,
796                "Working memory at capacity",
797            ),
798            CerememoryError::DecayEngineBusy { retry_after_secs } => {
799                let mut e = CMPError::new(CMPErrorCode::DecayEngineBusy, "Decay engine busy");
800                e.retry_after = Some(*retry_after_secs);
801                e
802            }
803            CerememoryError::ConsolidationInProgress => CMPError::new(
804                CMPErrorCode::ConsolidationInProgress,
805                "Consolidation in progress",
806            ),
807            CerememoryError::ExportFailed(_msg) => {
808                CMPError::new(CMPErrorCode::ExportFailed, EXPORT_ERROR_MESSAGE)
809            }
810            CerememoryError::ImportConflict(msg) => {
811                CMPError::new(CMPErrorCode::ImportConflict, msg.clone())
812            }
813            CerememoryError::ForgetUnconfirmed => CMPError::new(
814                CMPErrorCode::ForgetUnconfirmed,
815                "Forget requires explicit confirmation. Set 'confirm: true' to proceed.",
816            ),
817            CerememoryError::VersionMismatch { expected, got } => CMPError::new(
818                CMPErrorCode::VersionMismatch,
819                format!("Expected {expected}, got {got}"),
820            ),
821            CerememoryError::Validation(msg) => {
822                CMPError::new(CMPErrorCode::ValidationError, msg.clone())
823            }
824            CerememoryError::Storage(_msg) => {
825                CMPError::new(CMPErrorCode::InternalError, STORAGE_ERROR_MESSAGE)
826            }
827            CerememoryError::Serialization(_msg) => {
828                CMPError::new(CMPErrorCode::InternalError, SERIALIZATION_ERROR_MESSAGE)
829            }
830            CerememoryError::Internal(_msg) => {
831                CMPError::new(CMPErrorCode::InternalError, INTERNAL_ERROR_MESSAGE)
832            }
833            CerememoryError::Unauthorized(msg) => {
834                CMPError::new(CMPErrorCode::Unauthorized, msg.clone())
835            }
836            CerememoryError::RateLimited { retry_after_secs } => {
837                let mut e = CMPError::new(CMPErrorCode::RateLimited, "Rate limit exceeded");
838                e.retry_after = Some(*retry_after_secs);
839                e
840            }
841        }
842    }
843}
844
845#[cfg(test)]
846mod tests {
847    use super::*;
848
849    #[test]
850    fn cmp_header_defaults_are_valid() {
851        let header = CMPHeader::new();
852        assert_eq!(header.protocol, "cmp");
853        assert_eq!(header.version, "1.0");
854        assert!(header.is_compatible());
855    }
856
857    #[test]
858    fn cmp_header_version_check() {
859        let mut header = CMPHeader::new();
860        header.version = "1.1".to_string();
861        assert!(header.is_compatible());
862        header.version = "2.0".to_string();
863        assert!(!header.is_compatible());
864    }
865
866    #[test]
867    fn encode_store_request_json_roundtrip() {
868        let req = EncodeStoreRequest {
869            header: Some(CMPHeader::new()),
870            content: MemoryContent {
871                blocks: vec![ContentBlock {
872                    modality: Modality::Text,
873                    format: "text/plain".to_string(),
874                    data: b"Hello world".to_vec(),
875                    embedding: None,
876                }],
877                summary: Some("Test".to_string()),
878            },
879            store: Some(StoreType::Episodic),
880            emotion: None,
881            context: None,
882            metadata: None,
883            associations: None,
884        };
885
886        let json = serde_json::to_string(&req).unwrap();
887        let decoded: EncodeStoreRequest = serde_json::from_str(&json).unwrap();
888        assert_eq!(decoded.store, Some(StoreType::Episodic));
889        assert_eq!(decoded.content.blocks.len(), 1);
890    }
891
892    #[test]
893    fn encode_store_request_msgpack_roundtrip() {
894        let req = EncodeStoreRequest {
895            header: None,
896            content: MemoryContent {
897                blocks: vec![ContentBlock {
898                    modality: Modality::Text,
899                    format: "text/plain".to_string(),
900                    data: b"Test data".to_vec(),
901                    embedding: None,
902                }],
903                summary: None,
904            },
905            store: None,
906            emotion: None,
907            context: None,
908            metadata: None,
909            associations: None,
910        };
911
912        let packed = rmp_serde::to_vec(&req).unwrap();
913        let decoded: EncodeStoreRequest = rmp_serde::from_slice(&packed).unwrap();
914        assert_eq!(decoded.content.blocks[0].data, b"Test data");
915    }
916
917    #[test]
918    fn recall_query_request_defaults() {
919        let json = r#"{"cue":{"text":"hello"}}"#;
920        let req: RecallQueryRequest = serde_json::from_str(json).unwrap();
921        assert_eq!(req.limit, 10);
922        assert!(req.reconsolidate);
923        assert_eq!(req.activation_depth, 2);
924        assert_eq!(req.recall_mode, RecallMode::Human);
925    }
926
927    #[test]
928    fn cmp_error_from_cerememory_error() {
929        let err = crate::error::CerememoryError::RecordNotFound("abc".to_string());
930        let cmp_err = CMPError::from(&err);
931        assert_eq!(cmp_err.code, CMPErrorCode::RecordNotFound);
932    }
933
934    #[test]
935    fn decay_tick_request_defaults() {
936        let json = r#"{}"#;
937        let req: DecayTickRequest = serde_json::from_str(json).unwrap();
938        assert!(req.tick_duration_seconds.is_none());
939        assert!(req.header.is_none());
940    }
941
942    #[test]
943    fn forget_request_requires_confirm() {
944        let json = r#"{"confirm": true, "record_ids": ["01916e3a-1234-7000-8000-000000000001"]}"#;
945        let req: ForgetRequest = serde_json::from_str(json).unwrap();
946        assert!(req.confirm);
947        assert_eq!(req.record_ids.unwrap().len(), 1);
948    }
949
950    #[test]
951    fn stats_response_roundtrip() {
952        let mut by_store = std::collections::HashMap::new();
953        by_store.insert(StoreType::Episodic, 42u32);
954
955        let stats = StatsResponse {
956            total_records: 42,
957            records_by_store: by_store,
958            total_associations: 10,
959            avg_fidelity: 0.85,
960            avg_fidelity_by_store: std::collections::HashMap::new(),
961            oldest_record: None,
962            newest_record: None,
963            total_recall_count: 100,
964            raw_journal_records: 7,
965            raw_journal_pending_dream: 3,
966            dream_episodic_summaries: 2,
967            dream_semantic_nodes: 1,
968            last_dream_tick_at: None,
969            evolution_metrics: None,
970            background_decay_enabled: false,
971            background_dream_enabled: false,
972        };
973
974        let json = serde_json::to_string(&stats).unwrap();
975        let decoded: StatsResponse = serde_json::from_str(&json).unwrap();
976        assert_eq!(decoded.total_records, 42);
977    }
978}