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    /// Error from the Candle ML framework (in-process embedder).
16    #[cfg(feature = "candle-embedder")]
17    #[error("Candle error: {0}")]
18    CandleError(#[from] candle_core::Error),
19
20    /// Embedding vector has wrong number of dimensions.
21    #[error("Embedding provider returned {actual} dimensions, expected {expected}")]
22    DimensionMismatch { expected: usize, actual: usize },
23
24    /// Embedding provider returned a different number of vectors than requested.
25    #[error("Embedding provider returned {returned} vectors, expected {requested}")]
26    EmbeddingBatchCountMismatch { requested: usize, returned: usize },
27
28    /// Embedding vector has wrong number of dimensions.
29    #[error("Embedding vector has {actual} dimensions, expected {expected}")]
30    EmbeddingDimensionMismatch { expected: usize, actual: usize },
31
32    /// Embedding vector contains NaN or infinity.
33    #[error("Embedding vector contains non-finite value at index {index}")]
34    NonFiniteEmbeddingValue { index: usize },
35
36    /// Raw vector BLOB length does not match the expected f32 dimensions.
37    #[error("Vector blob length mismatch: expected {expected_bytes} bytes, got {actual_bytes}")]
38    VectorBlobLengthMismatch {
39        expected_bytes: usize,
40        actual_bytes: usize,
41    },
42
43    /// Encoded vector artifact was produced with a different codec profile.
44    #[error("Vector codec profile mismatch: expected {expected_digest}, got {actual_digest}")]
45    VectorCodecProfileMismatch {
46        /// Digest required by the decoding codec.
47        expected_digest: String,
48        /// Digest carried by the encoded artifact.
49        actual_digest: String,
50    },
51
52    /// A durable search receipt ID already exists with different payload bytes.
53    #[error("Search receipt ID conflict for {receipt_id}")]
54    SearchReceiptConflict {
55        /// Conflicting receipt/request ID.
56        receipt_id: String,
57    },
58
59    /// Canonical content digest computation failed.
60    #[error("Digest error: {0}")]
61    DigestError(String),
62
63    /// A requested durable search receipt was not found.
64    #[error("Search receipt not found: {receipt_id}")]
65    SearchReceiptNotFound {
66        /// Requested receipt/request ID.
67        receipt_id: String,
68    },
69
70    /// Raw BLOB data is not a valid embedding.
71    #[error("Invalid embedding data: expected {expected_bytes} bytes, got {actual_bytes}")]
72    InvalidEmbedding {
73        expected_bytes: usize,
74        actual_bytes: usize,
75    },
76
77    /// Database was created with a different embedding model.
78    #[error("Embedding model mismatch: database has '{stored}', config specifies '{configured}'")]
79    ModelMismatch { stored: String, configured: String },
80
81    /// Session with the given ID does not exist.
82    #[error("Session not found: {0}")]
83    SessionNotFound(String),
84
85    /// Fact with the given ID does not exist.
86    #[error("Fact not found: {0}")]
87    FactNotFound(String),
88
89    /// Document with the given ID does not exist.
90    #[error("Document not found: {0}")]
91    DocumentNotFound(String),
92
93    /// Embedding provider is unreachable or misconfigured.
94    #[error("Embedding provider unavailable: {0}")]
95    EmbedderUnavailable(String),
96
97    /// Database migration failed.
98    #[error("Migration failed at version {version}: {reason}")]
99    MigrationFailed { version: u32, reason: String },
100
101    /// HNSW index error.
102    #[error("HNSW index error: {0}")]
103    HnswError(String),
104
105    /// Vector backend not yet implemented (e.g. usearch stub during migration).
106    #[error("Not implemented: {0}")]
107    NotImplemented(String),
108
109    /// Invalid HNSW key format.
110    #[error("Invalid HNSW key format: {0}")]
111    InvalidKey(String),
112
113    /// Quantization error.
114    #[error("Quantization error: {0}")]
115    QuantizationError(String),
116
117    /// Storage path error.
118    #[error("Storage path error: {0}")]
119    StorageError(String),
120
121    /// Index integrity check failed.
122    #[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")]
123    IntegrityError {
124        in_sqlite_not_hnsw: usize,
125        in_hnsw_not_sqlite: usize,
126    },
127
128    /// Database schema is newer than this library version can handle.
129    #[error(
130        "Schema version {found} is ahead of max supported {supported} — upgrade semantic-memory"
131    )]
132    SchemaAhead {
133        /// Schema version found in the database.
134        found: u32,
135        /// Maximum version supported by this build.
136        supported: u32,
137    },
138
139    /// Content exceeds configured size limit.
140    #[error("Content too large: {size} bytes exceeds limit of {limit} bytes")]
141    ContentTooLarge {
142        /// Actual content size in bytes.
143        size: usize,
144        /// Configured limit in bytes.
145        limit: usize,
146    },
147
148    /// Namespace fact count would exceed the configured limit.
149    #[error("Namespace '{namespace}' has {count} facts, limit is {limit}")]
150    NamespaceFull {
151        /// Namespace that is full.
152        namespace: String,
153        /// Current fact count.
154        count: usize,
155        /// Configured limit.
156        limit: usize,
157    },
158
159    /// The configured database size ceiling would be exceeded by a new write.
160    #[error("Database size limit exceeded: current footprint is {current} bytes, limit is {limit} bytes")]
161    DatabaseSizeLimitExceeded {
162        /// Current observed database footprint in bytes.
163        current: u64,
164        /// Configured limit in bytes.
165        limit: u64,
166    },
167
168    /// Episode with the given ID does not exist.
169    #[error("Episode not found: {0}")]
170    EpisodeNotFound(String),
171
172    /// Connection pool reader acquisition timed out.
173    #[error("Pool reader acquisition timed out after {elapsed_ms}ms (pool size: {pool_size})")]
174    PoolTimeout {
175        /// How long the caller waited before giving up.
176        elapsed_ms: u64,
177        /// Number of reader slots in the pool.
178        pool_size: usize,
179    },
180
181    /// Brute-force vector search would scan more rows than the configured hard limit.
182    #[error(
183        "Vector scan hard limit exceeded for {table}: scanned {scanned} rows, limit is {limit}"
184    )]
185    VectorScanLimitExceeded {
186        /// Logical table/collection being scanned.
187        table: String,
188        /// Rows scanned before the circuit breaker tripped.
189        scanned: usize,
190        /// Configured hard limit.
191        limit: usize,
192    },
193
194    /// Configuration could not be normalized into a valid runtime state.
195    #[error("Invalid configuration for '{field}': {reason}")]
196    InvalidConfig {
197        /// The config field or section that failed validation.
198        field: &'static str,
199        /// Human-readable explanation of the invalid value.
200        reason: String,
201    },
202
203    /// Stored data is malformed or internally inconsistent.
204    #[error("Corrupt data in {table} ({row_id}): {detail}")]
205    CorruptData {
206        /// Table or logical collection containing the bad row.
207        table: &'static str,
208        /// Primary key / row identifier for the corrupt record.
209        row_id: String,
210        /// Human-readable description of the corruption.
211        detail: String,
212    },
213
214    /// Import envelope is structurally invalid.
215    #[error("Invalid import envelope: {reason}")]
216    ImportInvalid {
217        /// What is wrong with the envelope.
218        reason: String,
219    },
220
221    /// Import envelope has already been ingested (idempotent duplicate).
222    #[error("Import envelope already ingested: {envelope_id}")]
223    ImportDuplicate {
224        /// The duplicate envelope ID.
225        envelope_id: String,
226    },
227
228    /// Import hit a historical digest/receipt drift seam and needs operator repair.
229    #[error(
230        "Import requires digest migration or receipt repair for {source_envelope_id}: {detail}"
231    )]
232    ImportMigrationRequired {
233        /// The source envelope whose historical import receipts no longer line up.
234        source_envelope_id: String,
235        /// Human-readable conflict details and operator guidance.
236        detail: String,
237    },
238
239    /// Catch-all for other errors.
240    #[error("{0}")]
241    Other(String),
242}
243
244impl MemoryError {
245    /// Returns a stable string discriminant for programmatic matching.
246    pub fn kind(&self) -> &'static str {
247        match self {
248            Self::Database(_) => "database",
249            Self::EmbeddingRequest(_) => "embedding_request",
250            #[cfg(feature = "candle-embedder")]
251            Self::CandleError(_) => "candle_error",
252            Self::DimensionMismatch { .. } => "dimension_mismatch",
253            Self::EmbeddingBatchCountMismatch { .. } => "embedding_batch_count_mismatch",
254            Self::EmbeddingDimensionMismatch { .. } => "embedding_dimension_mismatch",
255            Self::NonFiniteEmbeddingValue { .. } => "non_finite_embedding_value",
256            Self::VectorBlobLengthMismatch { .. } => "vector_blob_length_mismatch",
257            Self::VectorCodecProfileMismatch { .. } => "vector_codec_profile_mismatch",
258            Self::SearchReceiptConflict { .. } => "search_receipt_conflict",
259            Self::DigestError(_) => "digest_error",
260            Self::SearchReceiptNotFound { .. } => "search_receipt_not_found",
261            Self::InvalidEmbedding { .. } => "invalid_embedding",
262            Self::ModelMismatch { .. } => "model_mismatch",
263            Self::SessionNotFound(_) => "session_not_found",
264            Self::FactNotFound(_) => "fact_not_found",
265            Self::DocumentNotFound(_) => "document_not_found",
266            Self::EpisodeNotFound(_) => "episode_not_found",
267            Self::PoolTimeout { .. } => "pool_timeout",
268            Self::VectorScanLimitExceeded { .. } => "vector_scan_limit_exceeded",
269            Self::EmbedderUnavailable(_) => "embedder_unavailable",
270            Self::MigrationFailed { .. } => "migration_failed",
271            Self::HnswError(_) => "hnsw_error",
272            Self::NotImplemented(_) => "not_implemented",
273            Self::InvalidKey(_) => "invalid_key",
274            Self::QuantizationError(_) => "quantization_error",
275            Self::StorageError(_) => "storage_error",
276            Self::IntegrityError { .. } => "integrity_error",
277            Self::SchemaAhead { .. } => "schema_ahead",
278            Self::ContentTooLarge { .. } => "content_too_large",
279            Self::NamespaceFull { .. } => "namespace_full",
280            Self::DatabaseSizeLimitExceeded { .. } => "database_size_limit_exceeded",
281            Self::InvalidConfig { .. } => "invalid_config",
282            Self::CorruptData { .. } => "corrupt_data",
283            Self::ImportInvalid { .. } => "import_invalid",
284            Self::ImportDuplicate { .. } => "import_duplicate",
285            Self::ImportMigrationRequired { .. } => "import_migration_required",
286            Self::Other(_) => "other",
287        }
288    }
289}