//! Stable errors used by Lisa's foundation components.
use thiserror::Error;
/// Reports a configuration problem before Lisa connects to Matrix.
#[derive(Debug, Error)]
pub enum ConfigurationError {
/// Reports an invalid configuration value without exposing secrets.
#[error("invalid configuration for {key}: {reason}")]
InvalidValue {
/// Names the invalid configuration key.
key: &'static str,
/// Describes why the public, non-secret value is invalid.
reason: String,
},
/// Reports a failed configuration file read or TOML parse.
#[error("could not load configuration: {0}")]
Load(#[from] std::io::Error),
/// Reports an invalid TOML document.
#[error("could not parse configuration: {0}")]
Parse(#[from] toml::de::Error),
}
/// Reports a durable-state failure.
#[derive(Debug, Error)]
pub enum StateStoreError {
/// Reports an SQLite operation failure.
#[error("state database operation failed: {0}")]
Database(#[from] rusqlite::Error),
/// Reports that the state worker stopped before answering a request.
#[error("state database worker stopped")]
WorkerStopped,
/// Reports a malformed persisted value.
#[error("invalid persisted state: {0}")]
InvalidData(String),
}
/// Reports Matrix client or protocol failures.
#[derive(Debug, Error)]
pub enum MatrixError {
/// Wraps a Matrix SDK failure.
#[error("Matrix SDK request failed: {0}")]
Sdk(#[from] matrix_sdk::Error),
/// Reports a response that cannot be safely ingested.
#[error("invalid Matrix response: {0}")]
InvalidResponse(String),
/// Reports a Matrix session identity mismatch.
#[error("Matrix identity does not match the configured account")]
IdentityMismatch,
/// Reports a rejected Matrix password without exposing it.
#[error("Matrix credentials were rejected")]
InvalidCredentials,
}
impl MatrixError {
/// Returns whether rebuilding the Matrix session can recover this error.
#[must_use]
pub fn needsReauthentication(&self) -> bool {
let Self::Sdk(error) = self else {
return false;
};
let matrix_sdk::Error::Http(error) = error else {
return false;
};
matches!(
error.client_api_error_kind(),
Some(matrix_sdk::ruma::api::error::ErrorKind::UnknownToken(_))
) || matches!(error.as_ref(), matrix_sdk::HttpError::RefreshToken(_))
}
}
/// Reports a bounded LLM completion failure without exposing request content.
#[derive(Debug, Error)]
pub enum LlmError {
/// Reports an HTTP transport or timeout failure.
#[error("LLM request failed")]
Request(#[source] reqwest::Error),
/// Reports an unsuccessful LLM HTTP status.
#[error("LLM service returned HTTP status {0}")]
Status(reqwest::StatusCode),
/// Reports malformed or disallowed model output.
#[error("LLM response did not match Lisa's final-answer contract")]
Protocol,
}
/// Reports an application-level failure.
#[derive(Debug, Error)]
pub enum AppError {
/// Wraps configuration errors.
#[error(transparent)]
Configuration(#[from] ConfigurationError),
/// Wraps state-store errors.
#[error(transparent)]
State(#[from] StateStoreError),
/// Wraps Matrix errors.
#[error(transparent)]
Matrix(#[from] MatrixError),
/// Wraps LLM failures.
#[error(transparent)]
Llm(#[from] LlmError),
}
impl AppError {
/// Returns a safe, stable code that may be shown in a room reply.
#[must_use]
pub fn userVisibleCode(&self) -> &'static str {
match self {
Self::Configuration(_) => "E_CONFIGURATION",
Self::State(_) => "E_STATE",
Self::Matrix(MatrixError::InvalidCredentials) => "E_MATRIX_CREDENTIALS",
Self::Matrix(MatrixError::IdentityMismatch) => "E_MATRIX_IDENTITY",
Self::Matrix(_) => "E_MATRIX_REQUEST",
Self::Llm(LlmError::Request(error)) if error.is_timeout() => "E_LLM_TIMEOUT",
Self::Llm(LlmError::Request(_)) => "E_LLM_REQUEST",
Self::Llm(LlmError::Status(status)) if status.as_u16() == 429 => "E_LLM_HTTP_429",
Self::Llm(LlmError::Status(status)) if status.is_server_error() => "E_LLM_HTTP_5XX",
Self::Llm(LlmError::Status(_)) => "E_LLM_HTTP",
Self::Llm(LlmError::Protocol) => "E_LLM_PROTOCOL",
}
}
/// Returns whether the Matrix session must be recreated before continuing.
#[must_use]
pub fn needsMatrixReauthentication(&self) -> bool {
matches!(self, Self::Matrix(error) if error.needsReauthentication())
}
/// Returns whether systemd should stop restarting for this error.
#[must_use]
pub fn isPermanentConfigurationFailure(&self) -> bool {
matches!(
self,
Self::Configuration(_)
| Self::Matrix(MatrixError::IdentityMismatch | MatrixError::InvalidCredentials)
)
}
}
#[cfg(test)]
mod tests {
use super::{AppError, LlmError};
#[test]
fn exposes_only_stable_llm_error_codes() {
assert_eq!(
AppError::from(LlmError::Protocol).userVisibleCode(),
"E_LLM_PROTOCOL"
);
}
}