gemini_memory_rs/core/
error.rs

1//! Error taxonomy for the memory engine.
2//!
3//! Memory failure is never fatal to a voice session: every error type here is
4//! something a caller can degrade past (empty context, deferred commit) rather
5//! than something that terminates the conversation.
6
7use thiserror::Error;
8
9use super::ids::MemoryId;
10
11/// Anything that can go wrong inside the memory engine.
12#[derive(Debug, Error)]
13pub enum MemoryError {
14    /// A malformed OKF document.
15    #[error("malformed OKF record at {path}: {message}")]
16    MalformedRecord {
17        /// Where the record came from.
18        path: String,
19        /// What was wrong with it.
20        message: String,
21    },
22
23    /// A record was expected but not found.
24    #[error("memory {0} not found")]
25    NotFound(MemoryId),
26
27    /// The caller's revision no longer matches the repository's.
28    #[error("write conflict: expected revision {expected}, found {actual}")]
29    RevisionConflict {
30        /// The revision the caller read.
31        expected: u64,
32        /// The revision the repository is actually at.
33        actual: u64,
34    },
35
36    /// A structured-output extraction failed or returned an unusable shape.
37    #[error("extraction failed: {0}")]
38    Extraction(String),
39
40    /// A retrieval backend failed or timed out.
41    #[error("retrieval failed: {0}")]
42    Retrieval(String),
43
44    /// Consolidation or reconciliation failed and should be retried.
45    #[error("reconciliation failed: {0}")]
46    Reconciliation(String),
47
48    /// A durable event could not be appended.
49    #[error("event log unavailable: {0}")]
50    EventLog(String),
51
52    /// The operation was refused for policy reasons.
53    #[error("refused by policy: {0}")]
54    PolicyRefused(String),
55
56    /// An underlying storage or filesystem failure.
57    #[error("storage error: {0}")]
58    Storage(String),
59
60    /// A deadline elapsed before the operation completed.
61    #[error("{operation} exceeded its {budget_ms}ms budget")]
62    DeadlineExceeded {
63        /// What was being attempted.
64        operation: &'static str,
65        /// The budget that elapsed.
66        budget_ms: u64,
67    },
68}
69
70impl MemoryError {
71    /// Whether retrying the same operation could plausibly succeed.
72    pub fn is_retryable(&self) -> bool {
73        matches!(
74            self,
75            Self::Extraction(_)
76                | Self::Retrieval(_)
77                | Self::Reconciliation(_)
78                | Self::EventLog(_)
79                | Self::Storage(_)
80                | Self::DeadlineExceeded { .. }
81                | Self::RevisionConflict { .. }
82        )
83    }
84
85    /// Whether the voice path should silently degrade rather than surface this.
86    ///
87    /// Everything except an explicit policy refusal degrades: the user asked a
88    /// question, and an internal memory failure is not their problem.
89    pub fn should_degrade_silently(&self) -> bool {
90        !matches!(self, Self::PolicyRefused(_))
91    }
92}
93
94impl From<std::io::Error> for MemoryError {
95    fn from(value: std::io::Error) -> Self {
96        Self::Storage(value.to_string())
97    }
98}
99
100impl From<serde_json::Error> for MemoryError {
101    fn from(value: serde_json::Error) -> Self {
102        Self::Storage(value.to_string())
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn transient_failures_are_retryable_but_policy_refusals_are_not() {
112        assert!(MemoryError::Retrieval("timeout".into()).is_retryable());
113        assert!(!MemoryError::PolicyRefused("sensitive".into()).is_retryable());
114    }
115
116    #[test]
117    fn only_policy_refusals_surface_to_the_caller() {
118        assert!(MemoryError::EventLog("down".into()).should_degrade_silently());
119        assert!(!MemoryError::PolicyRefused("restricted".into()).should_degrade_silently());
120    }
121}