From d9591f73eba6a1914e96a94f8891469db7edf681 Mon Sep 17 00:00:00 2001 From: CodeGhost21 <164498022+CodeGhost21@users.noreply.github.com> Date: Tue, 16 Jun 2026 05:02:09 +0530 Subject: [PATCH] fix(observability): skip SQLITE_FULL "database or disk is full" (Sentry TAURI-RUST-B6N) (#3672) --- src/core/observability.rs | 101 +++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/src/core/observability.rs b/src/core/observability.rs index 6537459cf..936c6276a 100644 --- a/src/core/observability.rs +++ b/src/core/observability.rs @@ -506,14 +506,55 @@ pub fn expected_error_kind(message: &str) -> Option { /// That string carries no "no space left on device" text, so anchor /// additionally on the cross-platform `StorageFull` ErrorKind token (std maps /// ENOSPC / `ERROR_DISK_FULL` / `ERROR_HANDLE_DISK_FULL` all to -/// `ErrorKind::StorageFull`). This is defense-in-depth for the genuinely +/// `ErrorKind::StorageFull`). +/// +/// A fifth shape comes from SQLite itself. When the engine detects the +/// disk-full condition during its own page bookkeeping (journal/WAL extension) +/// before the next syscall surfaces an errno, rusqlite renders the `SQLITE_FULL` +/// result code as `"database or disk is full"` (Sentry TAURI-RUST-B6N, hit at +/// `memory_store::unified::documents::tx.commit()` during +/// `openhuman.memory_doc_ingest`). `SQLITE_FULL` has only two causes: +/// genuine ENOSPC/ERROR_DISK_FULL (always the case in practice — the same +/// burst always produces an os-error-28/112 sibling event) or a +/// `max_page_count` PRAGMA cap (we set none). +/// +/// The rusqlite `Display` for `SQLITE_FULL` is exactly the five words +/// `"database or disk is full"` — no preamble, no trailing context. Our local +/// memory-store write call-sites wrap it with `format!(": {e}")` +/// (e.g. `"commit tx: ..."` / `"clear_namespace commit tx: ..."` in +/// `memory_store::unified::documents`), so the phrase always lands as the +/// **suffix** of the local emit. Anchor on suffix, not `contains`, so the +/// silencer does not match a non-2xx backend response body whose payload +/// happens to mention the same phrase (e.g. an `api.tinyhumans.ai` 5xx whose +/// server-side SQLite is full). Non-2xx backend bodies are framed by +/// `integrations::client::post` / `composio::client` as `"Backend returned +/// for : "` — an operator-actionable +/// server/storage failure that must still surface to Sentry. As +/// defense-in-depth for the edge case where the backend body itself ends with +/// the phrase, reject any message that also carries the `"backend returned "` +/// envelope prefix (codex CR on #3672, mirrors the precedent set by +/// [`is_backend_user_error_message`]). +/// +/// This is defense-in-depth for the genuinely /// unpreventable **write** paths (a write can't succeed on a full disk); the /// read path no longer emits this error at all (it degrades to a lock-free /// read — see `AuthProfilesStore::load`). fn is_disk_full_message(lower: &str) -> bool { - lower.contains("no space left on device") + if lower.contains("no space left on device") || lower.contains("not enough space on the disk") || lower.contains("storagefull") + { + return true; + } + // SQLITE_FULL — see the fifth-shape section above for the scoping + // rationale. Suffix anchor (after trimming trailing whitespace and + // punctuation that closures / JSON wrappers commonly append) pins to the + // local-emit shape; the negative `"backend returned "` guard rejects the + // remote envelope as a second line of defense. + let trimmed = lower.trim_end_matches(|c: char| { + c.is_ascii_whitespace() || matches!(c, '.' | ',' | ';' | ':' | '"' | '\'') + }); + trimmed.ends_with("database or disk is full") && !lower.contains("backend returned ") } /// Detect the literal `"Config loading timed out"` string produced by @@ -2818,6 +2859,13 @@ mod tests { // only the `ErrorKind` debug + os_code survive — no "no space left // on device" text. Must still classify via the StorageFull anchor. "Failed to create auth profile lock (kind=Some(StorageFull), os_code=Some(28))", + // SQLITE_FULL rendering from rusqlite — engine-level disk-full + // detection during page-bookkeeping (journal/WAL extension) that + // beats the next syscall to the errno. Production hit at + // `memory_store::unified::documents::tx.commit()` during + // `openhuman.memory_doc_ingest`, in the same burst that emits + // os-error-112 siblings (Sentry TAURI-RUST-B6N). + "commit tx: database or disk is full", ] { assert_eq!( expected_error_kind(raw), @@ -2840,6 +2888,55 @@ mod tests { expected_error_kind("not enough memory to allocate buffer"), None ); + // The SQLite anchor pins to the exact `"database or disk is full"` + // phrase. Generic prose that mentions a full database for unrelated + // reasons (e.g. duplicate-row complaints, application-level capacity + // talk) must not be silenced. + assert_eq!( + expected_error_kind("upsert failed: database is full of duplicates"), + None + ); + assert_eq!( + expected_error_kind("user quota: database is full for this tier"), + None + ); + // A non-2xx backend body whose payload contains the SQLITE_FULL phrase + // (e.g. `api.tinyhumans.ai` server-side SQLite is full) is an + // operator-actionable storage failure, not the user's local disk — + // must still surface to Sentry. `integrations::client::post` frames + // these as `"Backend returned for POST : + // "` (codex CR on #3672). The suffix anchor excludes the + // embedded-in-JSON case; the negative `"backend returned "` guard + // covers the rare case where the body itself ends with the phrase. + assert_eq!( + expected_error_kind( + "Backend returned 500 Internal Server Error for POST \ + https://api.tinyhumans.ai/agent-integrations/composio/list: \ + {\"error\":\"database or disk is full\"}" + ), + None, + "remote-backend body must surface" + ); + assert_eq!( + expected_error_kind( + "Backend returned 500 Internal Server Error for POST \ + https://api.tinyhumans.ai/agent-integrations/composio/list: \ + database or disk is full" + ), + None, + "remote-backend body must surface even when the body itself ends with the phrase" + ); + // Non-suffix occurrences in other body framings (no `"Backend + // returned"` prefix) are also excluded by the suffix anchor — locks + // in the primary defense layer. + assert_eq!( + expected_error_kind( + "Embedding API error (500 Internal Server Error): \ + {\"error\":\"database or disk is full\",\"retry\":true}" + ), + None, + "embedded-in-JSON body must surface" + ); } #[test]