use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::types::*;
pub const STORAGE_ERROR_MESSAGE: &str = "An internal storage error occurred";
pub const SERIALIZATION_ERROR_MESSAGE: &str = "An internal data processing error occurred";
pub const EXPORT_ERROR_MESSAGE: &str = "An internal error occurred during export";
pub const INTERNAL_ERROR_MESSAGE: &str = "An internal error occurred";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CMPHeader {
pub protocol: String,
pub version: String,
pub request_id: Uuid,
pub timestamp: DateTime<Utc>,
}
impl CMPHeader {
pub fn new() -> Self {
Self {
protocol: "cmp".to_string(),
version: "1.0".to_string(),
request_id: Uuid::now_v7(),
timestamp: Utc::now(),
}
}
pub fn is_compatible(&self) -> bool {
self.protocol == "cmp" && self.version.starts_with("1.")
}
}
impl Default for CMPHeader {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct EncodeContext {
pub source: Option<String>,
pub session_id: Option<String>,
pub spatial: Option<serde_json::Value>,
pub temporal: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManualAssociation {
pub target_id: Uuid,
pub association_type: AssociationType,
pub weight: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodeStoreRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
pub content: MemoryContent,
#[serde(default)]
pub store: Option<StoreType>,
#[serde(default)]
pub emotion: Option<EmotionVector>,
#[serde(default)]
pub context: Option<EncodeContext>,
#[serde(default)]
pub metadata: Option<serde_json::Value>,
#[serde(default)]
pub associations: Option<Vec<ManualAssociation>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodeStoreResponse {
pub record_id: Uuid,
pub store: StoreType,
pub initial_fidelity: f64,
pub associations_created: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodeBatchRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
pub records: Vec<EncodeStoreRequest>,
#[serde(default)]
pub infer_associations: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodeBatchResponse {
pub results: Vec<EncodeStoreResponse>,
pub associations_inferred: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodeUpdateRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
pub record_id: Uuid,
#[serde(default)]
pub content: Option<MemoryContent>,
#[serde(default)]
pub emotion: Option<EmotionVector>,
#[serde(default)]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodeStoreRawRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
pub session_id: String,
#[serde(default)]
pub turn_id: Option<String>,
#[serde(default)]
pub topic_id: Option<String>,
pub source: RawSource,
pub speaker: RawSpeaker,
pub visibility: RawVisibility,
pub secrecy_level: SecrecyLevel,
pub content: MemoryContent,
#[serde(default)]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodeStoreRawResponse {
pub record_id: Uuid,
pub session_id: String,
pub visibility: RawVisibility,
pub secrecy_level: SecrecyLevel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodeBatchStoreRawRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
pub records: Vec<EncodeStoreRawRequest>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodeBatchStoreRawResponse {
pub results: Vec<EncodeStoreRawResponse>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct RecallCue {
pub text: Option<String>,
pub image: Option<Vec<u8>>,
pub audio: Option<Vec<u8>>,
pub emotion: Option<EmotionVector>,
pub temporal: Option<TemporalRange>,
pub spatial: Option<serde_json::Value>,
pub semantic: Option<serde_json::Value>,
pub embedding: Option<Vec<f32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemporalRange {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecallQueryRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
pub cue: RecallCue,
#[serde(default)]
pub stores: Option<Vec<StoreType>>,
#[serde(default = "default_recall_limit")]
pub limit: u32,
#[serde(default)]
pub min_fidelity: Option<f64>,
#[serde(default)]
pub include_decayed: bool,
#[serde(default = "default_true")]
pub reconsolidate: bool,
#[serde(default = "default_activation_depth")]
pub activation_depth: u32,
#[serde(default = "default_recall_mode")]
pub recall_mode: RecallMode,
}
pub const DEFAULT_RECALL_LIMIT: u32 = 10;
pub const DEFAULT_RECONSOLIDATE: bool = true;
pub const DEFAULT_ACTIVATION_DEPTH: u32 = 2;
fn default_recall_limit() -> u32 {
DEFAULT_RECALL_LIMIT
}
fn default_true() -> bool {
DEFAULT_RECONSOLIDATE
}
fn default_activation_depth() -> u32 {
DEFAULT_ACTIVATION_DEPTH
}
fn default_recall_mode() -> RecallMode {
RecallMode::Human
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecalledMemory {
pub record: MemoryRecord,
pub relevance_score: f64,
#[serde(default)]
pub activation_path: Option<Vec<Uuid>>,
pub rendered_content: MemoryContent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActivationTrace {
pub source_id: Uuid,
pub activations: Vec<ActivationNode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActivationNode {
pub record_id: Uuid,
pub activation_level: f64,
pub hop: u32,
pub edge_type: AssociationType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryMetadata {
pub total_records_scanned: u32,
pub stores_searched: Vec<StoreType>,
pub fidelity_filtered: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecallQueryResponse {
pub memories: Vec<RecalledMemory>,
#[serde(default)]
pub activation_trace: Option<ActivationTrace>,
pub total_candidates: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub query_metadata: Option<QueryMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecallAssociateRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
pub record_id: Uuid,
#[serde(default)]
pub association_types: Option<Vec<AssociationType>>,
#[serde(default = "default_activation_depth")]
pub depth: u32,
#[serde(default = "default_min_weight")]
pub min_weight: f64,
#[serde(default = "default_recall_limit")]
pub limit: u32,
}
fn default_min_weight() -> f64 {
0.1
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecallAssociateResponse {
pub memories: Vec<RecalledMemory>,
pub total_candidates: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TimeGranularity {
Minute,
Hour,
Day,
Week,
Month,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecallTimelineRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
pub range: TemporalRange,
#[serde(default = "default_granularity")]
pub granularity: TimeGranularity,
#[serde(default)]
pub min_fidelity: Option<f64>,
#[serde(default)]
pub emotion_filter: Option<EmotionVector>,
}
fn default_granularity() -> TimeGranularity {
TimeGranularity::Hour
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecallGraphRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
#[serde(default)]
pub center_id: Option<Uuid>,
#[serde(default = "default_activation_depth")]
pub depth: u32,
#[serde(default)]
pub edge_types: Option<Vec<String>>,
#[serde(default = "default_recall_limit")]
pub limit_nodes: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecallRawQueryRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub query: Option<String>,
#[serde(default)]
pub temporal: Option<TemporalRange>,
#[serde(default = "default_recall_limit")]
pub limit: u32,
#[serde(default)]
pub include_private_scratch: bool,
#[serde(default)]
pub include_sealed: bool,
#[serde(default)]
pub secrecy_levels: Option<Vec<SecrecyLevel>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecallRawQueryResponse {
pub records: Vec<RawJournalRecord>,
pub total_candidates: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecallTimelineResponse {
pub buckets: Vec<TimelineBucket>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimelineBucket {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
pub memories: Vec<RecalledMemory>,
pub count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecallGraphResponse {
pub nodes: Vec<GraphNode>,
pub edges: Vec<GraphEdge>,
pub total_nodes: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphNode {
pub id: Uuid,
pub store: StoreType,
pub summary: Option<String>,
pub fidelity: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphEdge {
pub source: Uuid,
pub target: Uuid,
pub edge_type: AssociationType,
pub weight: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ConsolidationStrategy {
Full,
Incremental,
Selective,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsolidateRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
#[serde(default = "default_consolidation_strategy")]
pub strategy: ConsolidationStrategy,
#[serde(default)]
pub min_age_hours: u32,
#[serde(default)]
pub min_access_count: u32,
#[serde(default)]
pub dry_run: bool,
}
fn default_consolidation_strategy() -> ConsolidationStrategy {
ConsolidationStrategy::Incremental
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsolidateResponse {
pub records_processed: u32,
pub records_migrated: u32,
pub records_compressed: u32,
pub records_pruned: u32,
pub semantic_nodes_created: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DreamTickRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub dry_run: bool,
#[serde(default = "default_recall_limit")]
pub max_groups: u32,
#[serde(default)]
pub include_private_scratch: bool,
#[serde(default)]
pub include_sealed: bool,
#[serde(default = "default_true")]
pub promote_semantic: bool,
#[serde(default)]
pub secrecy_levels: Option<Vec<SecrecyLevel>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DreamTickResponse {
pub groups_processed: u32,
pub raw_records_processed: u32,
pub episodic_summaries_created: u32,
pub semantic_nodes_created: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecayTickRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
#[serde(default)]
pub tick_duration_seconds: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecayTickResponse {
pub records_updated: u32,
pub records_below_threshold: u32,
pub records_pruned: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetModeRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
pub mode: RecallMode,
#[serde(default)]
pub scope: Option<Vec<StoreType>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForgetRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
#[serde(default)]
pub record_ids: Option<Vec<Uuid>>,
#[serde(default)]
pub store: Option<StoreType>,
#[serde(default)]
pub temporal_range: Option<TemporalRange>,
#[serde(default)]
pub cascade: bool,
pub confirm: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForgetResponse {
pub records_deleted: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
#[serde(default = "default_format")]
pub format: String,
#[serde(default)]
pub stores: Option<Vec<StoreType>>,
#[serde(default)]
pub include_raw_journal: bool,
#[serde(default)]
pub encrypt: bool,
#[serde(default)]
pub encryption_key: Option<String>,
}
fn default_format() -> String {
"cma".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportResponse {
pub archive_id: String,
pub size_bytes: u64,
pub record_count: u32,
pub checksum: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportArchiveResponse {
#[serde(flatten)]
pub metadata: ExportResponse,
pub archive_data: Vec<u8>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConflictResolution {
KeepExisting,
KeepImported,
#[default]
KeepNewer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ImportStrategy {
Merge,
Replace,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
pub archive_id: String,
#[serde(default = "default_import_strategy")]
pub strategy: ImportStrategy,
#[serde(default = "default_conflict_resolution")]
pub conflict_resolution: ConflictResolution,
#[serde(default)]
pub decryption_key: Option<String>,
#[serde(default)]
pub archive_data: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportResponse {
pub records_imported: u32,
}
fn default_import_strategy() -> ImportStrategy {
ImportStrategy::Merge
}
fn default_conflict_resolution() -> ConflictResolution {
ConflictResolution::KeepNewer
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatsResponse {
pub total_records: u32,
pub records_by_store: std::collections::HashMap<StoreType, u32>,
pub total_associations: u32,
pub avg_fidelity: f64,
pub avg_fidelity_by_store: std::collections::HashMap<StoreType, f64>,
#[serde(default)]
pub oldest_record: Option<DateTime<Utc>>,
#[serde(default)]
pub newest_record: Option<DateTime<Utc>>,
pub total_recall_count: u64,
#[serde(default)]
pub raw_journal_records: u32,
#[serde(default)]
pub raw_journal_pending_dream: u32,
#[serde(default)]
pub dream_episodic_summaries: u32,
#[serde(default)]
pub dream_semantic_nodes: u32,
#[serde(default)]
pub last_dream_tick_at: Option<DateTime<Utc>>,
#[serde(default)]
pub evolution_metrics: Option<EvolutionMetrics>,
#[serde(default)]
pub background_decay_enabled: bool,
#[serde(default)]
pub background_dream_enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterAdjustment {
pub store: StoreType,
pub parameter: String,
pub original_value: f64,
pub current_value: f64,
pub reason: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct EvolutionMetrics {
pub parameter_adjustments: Vec<ParameterAdjustment>,
pub detected_patterns: Vec<String>,
pub schema_adaptations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordIntrospectRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
pub record_id: Uuid,
#[serde(default)]
pub include_history: bool,
#[serde(default)]
pub include_associations: bool,
#[serde(default)]
pub include_versions: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecayForecastRequest {
#[serde(default)]
pub header: Option<CMPHeader>,
pub record_ids: Vec<Uuid>,
pub forecast_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecayForecast {
pub record_id: Uuid,
pub current_fidelity: f64,
pub forecasted_fidelity: f64,
#[serde(default)]
pub estimated_threshold_date: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecayForecastResponse {
pub forecasts: Vec<DecayForecast>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CMPErrorCode {
RecordNotFound,
StoreInvalid,
ContentTooLarge,
ValidationError,
ModalityUnsupported,
WorkingMemoryFull,
DecayEngineBusy,
ConsolidationInProgress,
ExportFailed,
ImportConflict,
ForgetUnconfirmed,
VersionMismatch,
Unauthorized,
RateLimited,
InternalError,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CMPError {
pub code: CMPErrorCode,
pub message: String,
#[serde(default)]
pub details: Option<serde_json::Value>,
#[serde(default)]
pub retry_after: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<Uuid>,
}
impl CMPError {
pub fn new(code: CMPErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
details: None,
retry_after: None,
request_id: None,
}
}
}
impl From<&crate::error::CerememoryError> for CMPError {
fn from(err: &crate::error::CerememoryError) -> Self {
use crate::error::CerememoryError;
match err {
CerememoryError::RecordNotFound(id) => CMPError::new(
CMPErrorCode::RecordNotFound,
format!("Record not found: {id}"),
),
CerememoryError::StoreInvalid(s) => {
CMPError::new(CMPErrorCode::StoreInvalid, format!("Invalid store: {s}"))
}
CerememoryError::ContentTooLarge { size, limit } => CMPError::new(
CMPErrorCode::ContentTooLarge,
format!("{size} bytes exceeds {limit}"),
),
CerememoryError::ModalityUnsupported(m) => CMPError::new(
CMPErrorCode::ModalityUnsupported,
format!("Unsupported: {m}"),
),
CerememoryError::WorkingMemoryFull => CMPError::new(
CMPErrorCode::WorkingMemoryFull,
"Working memory at capacity",
),
CerememoryError::DecayEngineBusy { retry_after_secs } => {
let mut e = CMPError::new(CMPErrorCode::DecayEngineBusy, "Decay engine busy");
e.retry_after = Some(*retry_after_secs);
e
}
CerememoryError::ConsolidationInProgress => CMPError::new(
CMPErrorCode::ConsolidationInProgress,
"Consolidation in progress",
),
CerememoryError::ExportFailed(_msg) => {
CMPError::new(CMPErrorCode::ExportFailed, EXPORT_ERROR_MESSAGE)
}
CerememoryError::ImportConflict(msg) => {
CMPError::new(CMPErrorCode::ImportConflict, msg.clone())
}
CerememoryError::ForgetUnconfirmed => CMPError::new(
CMPErrorCode::ForgetUnconfirmed,
"Forget requires explicit confirmation. Set 'confirm: true' to proceed.",
),
CerememoryError::VersionMismatch { expected, got } => CMPError::new(
CMPErrorCode::VersionMismatch,
format!("Expected {expected}, got {got}"),
),
CerememoryError::Validation(msg) => {
CMPError::new(CMPErrorCode::ValidationError, msg.clone())
}
CerememoryError::Storage(_msg) => {
CMPError::new(CMPErrorCode::InternalError, STORAGE_ERROR_MESSAGE)
}
CerememoryError::Serialization(_msg) => {
CMPError::new(CMPErrorCode::InternalError, SERIALIZATION_ERROR_MESSAGE)
}
CerememoryError::Internal(_msg) => {
CMPError::new(CMPErrorCode::InternalError, INTERNAL_ERROR_MESSAGE)
}
CerememoryError::Unauthorized(msg) => {
CMPError::new(CMPErrorCode::Unauthorized, msg.clone())
}
CerememoryError::RateLimited { retry_after_secs } => {
let mut e = CMPError::new(CMPErrorCode::RateLimited, "Rate limit exceeded");
e.retry_after = Some(*retry_after_secs);
e
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cmp_header_defaults_are_valid() {
let header = CMPHeader::new();
assert_eq!(header.protocol, "cmp");
assert_eq!(header.version, "1.0");
assert!(header.is_compatible());
}
#[test]
fn cmp_header_version_check() {
let mut header = CMPHeader::new();
header.version = "1.1".to_string();
assert!(header.is_compatible());
header.version = "2.0".to_string();
assert!(!header.is_compatible());
}
#[test]
fn encode_store_request_json_roundtrip() {
let req = EncodeStoreRequest {
header: Some(CMPHeader::new()),
content: MemoryContent {
blocks: vec![ContentBlock {
modality: Modality::Text,
format: "text/plain".to_string(),
data: b"Hello world".to_vec(),
embedding: None,
}],
summary: Some("Test".to_string()),
},
store: Some(StoreType::Episodic),
emotion: None,
context: None,
metadata: None,
associations: None,
};
let json = serde_json::to_string(&req).unwrap();
let decoded: EncodeStoreRequest = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.store, Some(StoreType::Episodic));
assert_eq!(decoded.content.blocks.len(), 1);
}
#[test]
fn encode_store_request_msgpack_roundtrip() {
let req = EncodeStoreRequest {
header: None,
content: MemoryContent {
blocks: vec![ContentBlock {
modality: Modality::Text,
format: "text/plain".to_string(),
data: b"Test data".to_vec(),
embedding: None,
}],
summary: None,
},
store: None,
emotion: None,
context: None,
metadata: None,
associations: None,
};
let packed = rmp_serde::to_vec(&req).unwrap();
let decoded: EncodeStoreRequest = rmp_serde::from_slice(&packed).unwrap();
assert_eq!(decoded.content.blocks[0].data, b"Test data");
}
#[test]
fn recall_query_request_defaults() {
let json = r#"{"cue":{"text":"hello"}}"#;
let req: RecallQueryRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.limit, 10);
assert!(req.reconsolidate);
assert_eq!(req.activation_depth, 2);
assert_eq!(req.recall_mode, RecallMode::Human);
}
#[test]
fn cmp_error_from_cerememory_error() {
let err = crate::error::CerememoryError::RecordNotFound("abc".to_string());
let cmp_err = CMPError::from(&err);
assert_eq!(cmp_err.code, CMPErrorCode::RecordNotFound);
}
#[test]
fn decay_tick_request_defaults() {
let json = r#"{}"#;
let req: DecayTickRequest = serde_json::from_str(json).unwrap();
assert!(req.tick_duration_seconds.is_none());
assert!(req.header.is_none());
}
#[test]
fn forget_request_requires_confirm() {
let json = r#"{"confirm": true, "record_ids": ["01916e3a-1234-7000-8000-000000000001"]}"#;
let req: ForgetRequest = serde_json::from_str(json).unwrap();
assert!(req.confirm);
assert_eq!(req.record_ids.unwrap().len(), 1);
}
#[test]
fn stats_response_roundtrip() {
let mut by_store = std::collections::HashMap::new();
by_store.insert(StoreType::Episodic, 42u32);
let stats = StatsResponse {
total_records: 42,
records_by_store: by_store,
total_associations: 10,
avg_fidelity: 0.85,
avg_fidelity_by_store: std::collections::HashMap::new(),
oldest_record: None,
newest_record: None,
total_recall_count: 100,
raw_journal_records: 7,
raw_journal_pending_dream: 3,
dream_episodic_summaries: 2,
dream_semantic_nodes: 1,
last_dream_tick_at: None,
evolution_metrics: None,
background_decay_enabled: false,
background_dream_enabled: false,
};
let json = serde_json::to_string(&stats).unwrap();
let decoded: StatsResponse = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.total_records, 42);
}
}