Skip to main content

semantic_memory/
error.rs

1/// Error types for the semantic-memory crate.
2///
3/// All errors flow through [`MemoryError`], using `#[from]` for automatic
4/// conversion from rusqlite and reqwest errors.
5#[derive(Debug, thiserror::Error)]
6pub enum MemoryError {
7    /// SQLite / rusqlite error.
8    #[error("Database error: {0}")]
9    Database(#[from] rusqlite::Error),
10
11    /// HTTP error from the embedding provider.
12    #[error("Embedding request failed: {0}")]
13    EmbeddingRequest(#[from] reqwest::Error),
14
15    /// Embedding vector has wrong number of dimensions.
16    #[error("Embedding provider returned {actual} dimensions, expected {expected}")]
17    DimensionMismatch { expected: usize, actual: usize },
18
19    /// Raw BLOB data is not a valid embedding.
20    #[error("Invalid embedding data: expected {expected_bytes} bytes, got {actual_bytes}")]
21    InvalidEmbedding {
22        expected_bytes: usize,
23        actual_bytes: usize,
24    },
25
26    /// Database was created with a different embedding model.
27    #[error("Embedding model mismatch: database has '{stored}', config specifies '{configured}'")]
28    ModelMismatch { stored: String, configured: String },
29
30    /// Session with the given ID does not exist.
31    #[error("Session not found: {0}")]
32    SessionNotFound(String),
33
34    /// Fact with the given ID does not exist.
35    #[error("Fact not found: {0}")]
36    FactNotFound(String),
37
38    /// Document with the given ID does not exist.
39    #[error("Document not found: {0}")]
40    DocumentNotFound(String),
41
42    /// Embedding provider is unreachable or misconfigured.
43    #[error("Embedding provider unavailable: {0}")]
44    EmbedderUnavailable(String),
45
46    /// Database migration failed.
47    #[error("Migration failed at version {version}: {reason}")]
48    MigrationFailed { version: u32, reason: String },
49
50    /// HNSW index error.
51    #[error("HNSW index error: {0}")]
52    HnswError(String),
53
54    /// Invalid HNSW key format.
55    #[error("Invalid HNSW key format: {0}")]
56    InvalidKey(String),
57
58    /// Quantization error.
59    #[error("Quantization error: {0}")]
60    QuantizationError(String),
61
62    /// Storage path error.
63    #[error("Storage path error: {0}")]
64    StorageError(String),
65
66    /// Index integrity check failed.
67    #[error("Index integrity check failed: {in_sqlite_not_hnsw} items in SQLite but not HNSW, {in_hnsw_not_sqlite} items in HNSW but not SQLite")]
68    IntegrityError {
69        in_sqlite_not_hnsw: usize,
70        in_hnsw_not_sqlite: usize,
71    },
72
73    /// Database schema is newer than this library version can handle.
74    #[error(
75        "Schema version {found} is ahead of max supported {supported} — upgrade semantic-memory"
76    )]
77    SchemaAhead {
78        /// Schema version found in the database.
79        found: u32,
80        /// Maximum version supported by this build.
81        supported: u32,
82    },
83
84    /// Content exceeds configured size limit.
85    #[error("Content too large: {size} bytes exceeds limit of {limit} bytes")]
86    ContentTooLarge {
87        /// Actual content size in bytes.
88        size: usize,
89        /// Configured limit in bytes.
90        limit: usize,
91    },
92
93    /// Namespace fact count would exceed the configured limit.
94    #[error("Namespace '{namespace}' has {count} facts, limit is {limit}")]
95    NamespaceFull {
96        /// Namespace that is full.
97        namespace: String,
98        /// Current fact count.
99        count: usize,
100        /// Configured limit.
101        limit: usize,
102    },
103
104    /// The configured database size ceiling would be exceeded by a new write.
105    #[error("Database size limit exceeded: current footprint is {current} bytes, limit is {limit} bytes")]
106    DatabaseSizeLimitExceeded {
107        /// Current observed database footprint in bytes.
108        current: u64,
109        /// Configured limit in bytes.
110        limit: u64,
111    },
112
113    /// Episode with the given ID does not exist.
114    #[error("Episode not found: {0}")]
115    EpisodeNotFound(String),
116
117    /// Connection pool reader acquisition timed out.
118    #[error("Pool reader acquisition timed out after {elapsed_ms}ms (pool size: {pool_size})")]
119    PoolTimeout {
120        /// How long the caller waited before giving up.
121        elapsed_ms: u64,
122        /// Number of reader slots in the pool.
123        pool_size: usize,
124    },
125
126    /// Configuration could not be normalized into a valid runtime state.
127    #[error("Invalid configuration for '{field}': {reason}")]
128    InvalidConfig {
129        /// The config field or section that failed validation.
130        field: &'static str,
131        /// Human-readable explanation of the invalid value.
132        reason: String,
133    },
134
135    /// Stored data is malformed or internally inconsistent.
136    #[error("Corrupt data in {table} ({row_id}): {detail}")]
137    CorruptData {
138        /// Table or logical collection containing the bad row.
139        table: &'static str,
140        /// Primary key / row identifier for the corrupt record.
141        row_id: String,
142        /// Human-readable description of the corruption.
143        detail: String,
144    },
145
146    /// Import envelope is structurally invalid.
147    #[error("Invalid import envelope: {reason}")]
148    ImportInvalid {
149        /// What is wrong with the envelope.
150        reason: String,
151    },
152
153    /// Import envelope has already been ingested (idempotent duplicate).
154    #[error("Import envelope already ingested: {envelope_id}")]
155    ImportDuplicate {
156        /// The duplicate envelope ID.
157        envelope_id: String,
158    },
159
160    /// Import hit a historical digest/receipt drift seam and needs operator repair.
161    #[error(
162        "Import requires digest migration or receipt repair for {source_envelope_id}: {detail}"
163    )]
164    ImportMigrationRequired {
165        /// The source envelope whose historical import receipts no longer line up.
166        source_envelope_id: String,
167        /// Human-readable conflict details and operator guidance.
168        detail: String,
169    },
170
171    /// Catch-all for other errors.
172    #[error("{0}")]
173    Other(String),
174}
175
176impl MemoryError {
177    /// Returns a stable string discriminant for programmatic matching.
178    pub fn kind(&self) -> &'static str {
179        match self {
180            Self::Database(_) => "database",
181            Self::EmbeddingRequest(_) => "embedding_request",
182            Self::DimensionMismatch { .. } => "dimension_mismatch",
183            Self::InvalidEmbedding { .. } => "invalid_embedding",
184            Self::ModelMismatch { .. } => "model_mismatch",
185            Self::SessionNotFound(_) => "session_not_found",
186            Self::FactNotFound(_) => "fact_not_found",
187            Self::DocumentNotFound(_) => "document_not_found",
188            Self::EpisodeNotFound(_) => "episode_not_found",
189            Self::PoolTimeout { .. } => "pool_timeout",
190            Self::EmbedderUnavailable(_) => "embedder_unavailable",
191            Self::MigrationFailed { .. } => "migration_failed",
192            Self::HnswError(_) => "hnsw_error",
193            Self::InvalidKey(_) => "invalid_key",
194            Self::QuantizationError(_) => "quantization_error",
195            Self::StorageError(_) => "storage_error",
196            Self::IntegrityError { .. } => "integrity_error",
197            Self::SchemaAhead { .. } => "schema_ahead",
198            Self::ContentTooLarge { .. } => "content_too_large",
199            Self::NamespaceFull { .. } => "namespace_full",
200            Self::DatabaseSizeLimitExceeded { .. } => "database_size_limit_exceeded",
201            Self::InvalidConfig { .. } => "invalid_config",
202            Self::CorruptData { .. } => "corrupt_data",
203            Self::ImportInvalid { .. } => "import_invalid",
204            Self::ImportDuplicate { .. } => "import_duplicate",
205            Self::ImportMigrationRequired { .. } => "import_migration_required",
206            Self::Other(_) => "other",
207        }
208    }
209}