#[non_exhaustive]pub enum AppError {
Show 32 variants
Validation(String),
Usage {
message: String,
discarded_flags: Vec<String>,
},
BinaryNotFound {
name: String,
},
RateLimited {
detail: String,
},
Timeout {
operation: String,
duration_secs: u64,
},
Duplicate(String),
Conflict(String),
NotFound(String),
MemoryNotFound {
name: String,
namespace: String,
},
MemoryNotFoundById {
id: i64,
},
EntityNotYetMaterialized {
name: String,
namespace: String,
},
NamespaceError(String),
LimitExceeded(String),
BodyTooLarge {
bytes: u64,
limit: u64,
},
TooManyChunks {
chunks: usize,
limit: usize,
},
TooManyTokens {
tokens: u64,
limit: u64,
},
Database(Error),
Embedding(String),
EmbeddingClassified {
message: String,
retry_class: AttemptOutcome,
},
VecExtension(String),
DbBusy(String),
BatchPartialFailure {
total: usize,
failed: usize,
},
Io(Error),
Internal(Error),
Json(Error),
LockBusy(String),
AllSlotsFull {
max: usize,
waited_secs: u64,
},
JobSingletonLocked {
job_type: String,
namespace: String,
},
EmbeddingSingletonLocked {
namespace: String,
},
LowMemory {
available_mb: u64,
required_mb: u64,
},
Shutdown {
signal: String,
},
ProviderError {
code: String,
message: String,
},
}Expand description
Unified error type for all CLI and library operations.
Each variant corresponds to a distinct failure category. The
AppError::exit_code method converts a variant into a stable numeric
code so that shell callers and LLM agents can route on it.
§SemVer Policy
This enum is #[non_exhaustive]. New variants may be added in minor
releases without breaking downstream match arms (use a wildcard _).
Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
Validation(String)
Input failed schema, length or format validation. Maps to exit code 1.
This variant groups multiple validation failure causes. Callers that need
programmatic retry decisions should use AppError::is_retryable instead
of parsing the message string.
Usage
The argv parsed but asks for something impossible. Maps to exit code 2.
Distinct from AppError::Validation on purpose. Validation means the
DATA the caller supplied is wrong and exits 1; this means the REQUEST
itself is incoherent and exits crate::constants::USAGE_EXIT_CODE, the
same code clap returns for a rejected flag. An agent therefore branches on
one number for “fix the command line” whether the parser or the
agent-native surface caught it.
Introduced in v1.2.6 for the refusals the surface can only make once it has the envelope: a projection or predicate key that exists in no result element, a predicate aimed at a collection the caller never named, and a filter evaluated over a page the query had already truncated.
Carries the flags the request declared and the binary could not honour.
rules-rust-cli-stdin-stdout-silent-discard requires the error JSON to
name them in a field: leaving them inside the prose would force the
caller to parse a sentence — in one of two languages — to learn which of
its own arguments were dropped. Empty for refusals that discard nothing,
such as a predicate judged over a truncated page.
Fields
BinaryNotFound
External binary required for operation was not found in PATH. Maps to exit code 1.
RateLimited
Remote service signaled rate limiting; caller should retry with backoff. Maps to exit code 1.
Timeout
Operation exceeded its time budget. Maps to exit code 1.
Duplicate(String)
A memory or entity with the same (namespace, name) already exists. Maps to exit code 9.
Conflict(String)
Optimistic update lost the race because updated_at changed. Maps to exit code 3.
NotFound(String)
The requested record does not exist or was soft-deleted. Maps to exit code 4.
MemoryNotFound
Memory lookup by (namespace, name) returned no row. Maps to exit code 4.
G55 S2 (v1.0.80): structural variant that carries the requested identifier
and namespace, eliminating the “not found: unknown in namespace ‘X’” class
of bugs that masked which lookup target failed. The display format matches
the legacy string-based NotFound so the i18n replace-chain and external
scripts that pattern-match on memory not found: name='N' in namespace 'NS'
keep working.
MemoryNotFoundById
Memory lookup by integer id returned no row. Maps to exit code 4.
EntityNotYetMaterialized
GAP-SG-78: an entity referenced by a queued enrich item does not yet
exist in entities. Maps to exit code 4.
§Cause
Distinct from the terminal Self::NotFound / Self::MemoryNotFound
cases (a memory that was deleted or renamed, permanently gone). An
entity can be referenced by a queue row BEFORE it is materialized: a
later enrich pass creates the entity, so its absence now is TRANSITORY,
not terminal. Collapsing both into a single NotFound string sent every
such item to the dead-letter on the first failure.
§When it occurs
Raised by the entity call-sites of enrich — entity-descriptions
(call_entity_description) and entity-type-validate
(call_entity_type_validate) — when the (namespace, name) lookup
returns no row. Classified as Self::is_retryable so the item is
rescheduled until --max-attempts is exhausted.
NamespaceError(String)
Namespace could not be resolved from flag, environment or markers. Maps to exit code 5.
LimitExceeded(String)
Payload exceeded one of the configured body, name or batch limits. Maps to exit code 6.
v1.1.1 (P11): kept for caps other than the body-bytes and chunk-count
ceilings, which now have the typed Self::BodyTooLarge and
Self::TooManyChunks variants so the operator can tell WHICH cap
fired without parsing the message.
BodyTooLarge
Body payload exceeded crate::constants::MAX_MEMORY_BODY_LEN bytes.
Maps to exit code 6 (same contract as Self::LimitExceeded).
v1.1.1 (P11): the two independent write ceilings — body bytes and chunk
count — used to collapse into the generic LimitExceeded string, so an
operator hitting exit 6 could not tell WHICH cap fired. This variant
carries the measured size and the cap, and the message names the
constant, so both the stderr line and the JSON envelope identify the
ceiling deterministically (never by substring matching).
Body too large.
TooManyChunks
Chunking produced more chunks than
crate::constants::REMEMBER_MAX_SAFE_MULTI_CHUNKS. Maps to exit
code 6 (same contract as Self::LimitExceeded).
v1.1.1 (P11): counterpart of Self::BodyTooLarge for the chunk-count
ceiling. Carries the measured chunk count and the cap so the operator
can distinguish a chunk overflow from a byte overflow on exit 6.
Too many chunks.
TooManyTokens
Body exceeded crate::constants::EMBEDDING_REQUEST_MAX_TOKENS tokens
(conservative cl100k proxy for the qwen/qwen3-embedding-8b window).
Maps to exit code 6 (same contract as Self::LimitExceeded).
v1.1.2 (Gap 2): third typed payload ceiling alongside
Self::BodyTooLarge (bytes) and Self::TooManyChunks (chunks).
The token cap used to surface as a generic Validation (exit 1) deep
inside the REST embedding client; it now fires at the write-command
boundary with the estimated token count and the cap, so the operator
can tell WHICH ceiling fired without substring matching (GAP-SG-73).
Too many tokens.
Database(Error)
Low-level SQLite error propagated from rusqlite. Maps to exit code 10.
Embedding(String)
Embedding generation via fastembed failed or produced the wrong shape. Maps to exit code 11.
EmbeddingClassified
GAP-SG-270: an embedding failure that still CARRIES the retry verdict
EmbedError computed at its origin (exact HTTP status / provider code).
Exit code 11 and Display text are identical to Self::Embedding,
so no operator-facing contract changes; only the enrich queue reads the
extra field, via queue_ops::classify_enrich_outcome. Without it every
embedding failure landed in the untyped Self::Embedding bucket and a
PERMANENT one burned every --max-attempts retry. Deliberately absent
from Self::is_retryable, which never covered Self::Embedding.
Fields
message: StringFailure detail, identical to the payload of Self::Embedding.
retry_class: AttemptOutcomeRetry verdict computed where the failure originated (HTTP status /
provider code), never inferred from message.
VecExtension(String)
The sqlite-vec extension could not load or register its virtual table. Maps to exit code 12.
DbBusy(String)
SQLite returned SQLITE_BUSY after exhausting retries. Maps to exit code 15 (was 13 before v2.0.0; relocated to free 13 for BatchPartialFailure per PRD).
BatchPartialFailure
Batch operation failed partially — N of M items failed. Maps to exit code 13 (PRD 1822).
Reserved for use in import, reindex and batch stdin (BLOCK 3/4). Variant present
since v2.0.0 even if call-sites do not yet exist — stable exit code mapping.
Io(Error)
Filesystem I/O error while reading or writing the database or cache. Maps to exit code 14.
Internal(Error)
Unexpected internal error surfaced through anyhow. Maps to exit code 20.
Json(Error)
JSON serialization or deserialization failure. Maps to exit code 20.
LockBusy(String)
Another instance is already running and holds the advisory lock. Maps to exit code 75.
Use --wait-lock <SECONDS> to poll until the lock drops.
AllSlotsFull
All concurrency slots are occupied after the wait timeout. Maps to exit code 75.
Occurs when crate::constants::MAX_CONCURRENT_CLI_INSTANCES instances are already
active and the wait limit crate::constants::CLI_LOCK_DEFAULT_WAIT_SECS is exhausted.
All slots full.
JobSingletonLocked
A heavy long-running job is already running for this job_type/namespace
pair. Maps to exit code 75 (the same EX_TEMPFAIL code used by the
CLI semaphore).
G28-B (v1.0.68): ensures at most one enrich
or ingest runs at a time per namespace. The guard predates v1.2.0 and
once also covered ingest --mode claude-code / --mode codex.
Use --wait-job-singleton <SECONDS> (per-command) to poll until the
other invocation finishes.
Job singleton locked.
EmbeddingSingletonLocked
G45: an LLM embedding operation is already running against the
same (namespace, db) pair in another process. Exit code 75
(retryable). The caller can pass --wait-lock <SECONDS> to poll until
the lock drops. Until v1.2.8 this message named the wait flag removed in
v1.2.0, so obeying the refusal returned exit 2 (GAP-SG-303).
Embedding singleton locked.
LowMemory
Available memory is below the minimum required to load the model. Maps to exit code 77.
Returned when sysinfo reports available memory below
crate::constants::MIN_AVAILABLE_MEMORY_MB MiB before starting the ONNX model load.
Low memory.
Fields
Shutdown
v1.0.82 (GAP-002 final): shutdown was requested via SIGINT, SIGTERM or
SIGHUP before the current command completed. Maps to exit code
crate::constants::SHUTDOWN_EXIT_CODE (19).
The signal name is preserved in the signal field so the JSON
envelope emitted before exit can route the operator to a
deterministic branch. Distinct from the legacy 128 + signal
Unix convention (130/143/129) so LLM agents can match on a
single code for “cancelled by user”.
ProviderError
v1.0.97 (GAP-SG-01/03): the OpenRouter provider returned a structured
error object (an error field carrying code and message), often
inside an HTTP 200 body (e.g. token/context-length overflow). Maps to
exit code 1.
Modelling the provider rejection as a typed variant — instead of the
generic Embedding/Validation string — stops the optimistic success
parse from masking the cause with a misleading missing-field error. The
code and message carry the REAL provider diagnostics.
This variant is permanent: a structured provider error in a success
body is a content or configuration rejection that retrying the identical
request will not fix. Genuine rate limiting surfaces as HTTP 429 and is
retried inside the HTTP client (then exposed via RateLimited when
attempts are exhausted), so it never reaches callers as ProviderError.
Implementations§
Source§impl AppError
impl AppError
Sourcepub fn exit_code(&self) -> i32
pub fn exit_code(&self) -> i32
Returns the deterministic process exit code for this error variant.
The codes follow the contract documented in the README: 1 for
validation, 9 for duplicates (moved from 2 in v1.0.52), 3 for conflicts, 4 for missing
records, 5 for namespace errors, 6 for limit violations, 10–14
for infrastructure failures, 13 for BatchPartialFailure (PRD 1822),
15 for DbBusy (migrated from 13 in v2.0.0), 20 for internal errors,
75 (EX_TEMPFAIL) when the advisory CLI lock is held or all concurrency
slots are exhausted, and 77 when available memory is insufficient to
load the embedding model.
§Examples
use sqlite_graphrag::errors::AppError;
assert_eq!(AppError::Validation("invalid field".into()).exit_code(), 1);
assert_eq!(AppError::Duplicate("ns/mem".into()).exit_code(), 9);
assert_eq!(AppError::Conflict("ts changed".into()).exit_code(), 3);
assert_eq!(AppError::NotFound("id 42".into()).exit_code(), 4);
assert_eq!(AppError::NamespaceError("no marker".into()).exit_code(), 5);
assert_eq!(AppError::LimitExceeded("body too large".into()).exit_code(), 6);
assert_eq!(AppError::Embedding("wrong dim".into()).exit_code(), 11);
assert_eq!(AppError::DbBusy("retries exhausted".into()).exit_code(), 15);
assert_eq!(AppError::LockBusy("another instance".into()).exit_code(), 75);Sourcepub fn discarded_flags(&self) -> &[String]
pub fn discarded_flags(&self) -> &[String]
Flags the caller passed that this invocation could not honour.
Empty for every variant that discards nothing, so a consumer reads one field instead of branching on the variant it happened to receive.
Sourcepub fn is_retryable(&self) -> bool
pub fn is_retryable(&self) -> bool
Returns true when the error is transient and the operation may
succeed on retry with backoff.
§Examples
use sqlite_graphrag::errors::AppError;
assert!(AppError::DbBusy("busy".into()).is_retryable());
assert!(AppError::LockBusy("held".into()).is_retryable());
assert!(!AppError::NotFound("x".into()).is_retryable());
assert!(!AppError::Validation("bad".into()).is_retryable());Sourcepub fn is_shutdown(&self) -> bool
pub fn is_shutdown(&self) -> bool
Returns true when shutdown was requested by the user via signal.
Distinct from is_permanent because shutdown is a USER intent, not
a state to retry against. The operation should be retried with
--resume (GAP-001) when the persisted staging row still exists.
§Examples
use sqlite_graphrag::errors::AppError;
assert!(AppError::Shutdown { signal: "SIGINT".into() }.is_shutdown());
assert!(!AppError::Validation("x".into()).is_shutdown());Sourcepub fn is_permanent(&self) -> bool
pub fn is_permanent(&self) -> bool
Returns true when the error is permanent and must NOT be retried.
Complement to Self::is_retryable. Errors not classified by either
method (e.g. Database, Io, Internal) are ambiguous — the caller
decides based on context.
§Examples
use sqlite_graphrag::errors::AppError;
assert!(AppError::Validation("bad".into()).is_permanent());
assert!(!AppError::DbBusy("busy".into()).is_permanent());Sourcepub fn error_class(&self) -> &'static str
pub fn error_class(&self) -> &'static str
Stable retry classification carried by the stdout error envelope as
error_class.
An agent that reads only code cannot tell a busy lock from a malformed
name, so it either retries what will never succeed or gives up on what
would. This field answers that question without the agent knowing a
single exit code by heart.
The vocabulary matches the one the enrich queue already persists in
queue.error_class, plus ambiguous for the third state
Self::is_permanent documents: errors classified by neither predicate,
where the caller decides from context.
§Examples
use sqlite_graphrag::errors::AppError;
assert_eq!(AppError::DbBusy("busy".into()).error_class(), "transient");
assert_eq!(AppError::Validation("bad".into()).error_class(), "permanent");
assert_eq!(
AppError::Internal(anyhow::anyhow!("x")).error_class(),
"ambiguous"
);Sourcepub fn suggestion(&self) -> Option<&'static str>
pub fn suggestion(&self) -> Option<&'static str>
GAP-SG-39: returns an actionable remediation hint for the error, surfaced
in the stdout error envelope as the suggestion field. The hint tells the
operator HOW to recover instead of leaving an exit code without guidance —
this is what makes a write rejection (e.g. a malformed name) observable and
fixable. Returns None for variants whose own message is already
self-remediating.
Localized through Self::suggestion_for, so a pt-BR operator never
receives a Portuguese message beside an English suggestion in the same
envelope.
Sourcepub fn suggestion_for(&self, lang: Language) -> Option<&'static str>
pub fn suggestion_for(&self, lang: Language) -> Option<&'static str>
Returns the remediation hint in the explicitly provided language.
Mirrors Self::localized_message_for so tests can assert both languages
without depending on the global OnceLock.
§Examples
use sqlite_graphrag::errors::AppError;
use sqlite_graphrag::i18n::Language;
let err = AppError::Duplicate("mem-xyz".into());
assert!(err.suggestion_for(Language::English).unwrap().contains("pass"));
assert!(err.suggestion_for(Language::Portuguese).unwrap().contains("passe"));Sourcepub fn localized_message(&self) -> String
pub fn localized_message(&self) -> String
Returns the localized error message in the active language (--lang / XDG i18n.lang).
In English the text is identical to the Display generated by thiserror.
In Portuguese the prefixes and messages are translated to PT-BR.
Sourcepub fn localized_message_for(&self, lang: Language) -> String
pub fn localized_message_for(&self, lang: Language) -> String
Returns the localized message for the explicitly provided language.
Useful in tests that cannot depend on the global OnceLock.
§Examples
use sqlite_graphrag::errors::AppError;
use sqlite_graphrag::i18n::Language;
let err = AppError::NotFound("mem-xyz".into());
let en = err.localized_message_for(Language::English);
assert!(en.contains("not found"));
let pt = err.localized_message_for(Language::Portuguese);
assert!(pt.contains("n\u{e3}o encontrado"));Trait Implementations§
Source§impl Error for AppError
impl Error for AppError
Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()
Source§impl From<AppError> for EmbedError
Converts a bare AppError into an EmbedError with retry_class: HardFailure. Used by the ? operator on call sites that predate the
origin-typed classification (the GAP-SG-02 oversized-input guard, the
dimension-mismatch guard in OpenRouterClient::truncate_embedding, and
the batch-size-mismatch check) — all of those are genuine permanent
client/config errors, never transient. Every EmbedError constructed
inside execute_with_retry uses EmbedError::new explicitly with a
retry verdict computed at the exact HTTP status / provider code instead.
impl From<AppError> for EmbedError
Converts a bare AppError into an EmbedError with retry_class: HardFailure. Used by the ? operator on call sites that predate the
origin-typed classification (the GAP-SG-02 oversized-input guard, the
dimension-mismatch guard in OpenRouterClient::truncate_embedding, and
the batch-size-mismatch check) — all of those are genuine permanent
client/config errors, never transient. Every EmbedError constructed
inside execute_with_retry uses EmbedError::new explicitly with a
retry verdict computed at the exact HTTP status / provider code instead.
Source§impl From<EmbedError> for AppError
Unwraps EmbedError back down to its source, discarding retry_class.
Lets the many pre-existing ?-based callers of crate::embedding_api::OpenRouterClient::embed_single
/ crate::embedding_api::OpenRouterClient::embed_batch (in crate::embedder) keep compiling
unchanged; callers that need the typed retry verdict (the enrich
re-embed path) should match on EmbedError directly instead of relying
on this conversion.
impl From<EmbedError> for AppError
Unwraps EmbedError back down to its source, discarding retry_class.
Lets the many pre-existing ?-based callers of crate::embedding_api::OpenRouterClient::embed_single
/ crate::embedding_api::OpenRouterClient::embed_batch (in crate::embedder) keep compiling
unchanged; callers that need the typed retry verdict (the enrich
re-embed path) should match on EmbedError directly instead of relying
on this conversion.
Source§fn from(err: EmbedError) -> Self
fn from(err: EmbedError) -> Self
Auto Trait Implementations§
impl !RefUnwindSafe for AppError
impl !UnwindSafe for AppError
impl Freeze for AppError
impl Send for AppError
impl Sync for AppError
impl Unpin for AppError
impl UnsafeUnpin for AppError
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<T> ErasedDestructor for Twhere
T: 'static,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<T> Pointable for T
impl<T> Pointable for T
Source§impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> PolicyExt for Twhere
T: ?Sized,
Source§impl<T> ToStringFallible for Twhere
T: Display,
impl<T> ToStringFallible for Twhere
T: Display,
Source§fn try_to_string(&self) -> Result<String, TryReserveError>
fn try_to_string(&self) -> Result<String, TryReserveError>
ToString::to_string, but without panic on OOM.