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