{"record":{"id":"aebda9c923db8416","repo":"gitbutlerapp/gitbutler","slug":"failed-to-persist-action-e","errorCode":null,"errorMessage":"Failed to persist action: {e}","messagePattern":"Failed to persist action: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-action/src/action.rs","lineNumber":152,"sourceCode":"        Self {\n            id: Uuid::new_v4(),\n            created_at: chrono::Local::now().naive_local(),\n            handler,\n            external_prompt,\n            external_summary,\n            snapshot_before,\n            snapshot_after,\n            response: rsp.cloned(),\n            error,\n            source,\n        }\n    }\n}\n\nfn persist_action(db: &mut DbHandle, action: ButlerAction) -> anyhow::Result<()> {\n    db.butler_actions_mut()\n        .insert(action.try_into()?)\n        .map_err(|e| anyhow::anyhow!(\"Failed to persist action: {e}\"))?;\n    Ok(())\n}\n\n/// Persist a completed handle-changes action record and return its generated ID.\n///\n/// `db` is the already-open project database handle to write into. `change_summary`,\n/// `external_prompt`, `handler`, and `source` describe the request that produced the action.\n/// `snapshot_before` and `snapshot_after` link the action to the surrounding oplog snapshots.\n/// `response` is stored as either the successful action outcome or the error text.\n#[expect(clippy::too_many_arguments)]\npub(crate) fn record_handle_changes_action(\n    db: &mut DbHandle,\n    change_summary: &str,\n    external_prompt: Option<String>,\n    handler: crate::ActionHandler,\n    source: Source,\n    snapshot_before: gix::ObjectId,\n    snapshot_after: gix::ObjectId,","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/2497b8007aa4a1922dae9a805b32ffe5b5037785/crates/but-action/src/action.rs#L134-L170","documentation":"anyhow wrapper at crates/but-action/src/action.rs:152 around the SQLite insert of a ButlerAction row into the project's butler_actions table via db.butler_actions_mut().insert(...). Any insert failure — database locked, constraint or IO error, disk full — surfaces as \"Failed to persist action: {e}\" with the underlying error chained. The row conversion (action.try_into()?) runs before the map_err and keeps its own conversion error.","triggerScenarios":"Another process holds the project database write lock (desktop app plus CLI on the same project, SQLITE_BUSY); the butler_actions table is missing after opening a database created by an older binary; the DB file or directory is read-only; disk full during the write.","commonSituations":"Running but CLI or TUI commands while the desktop app has the project open; database on a network or synced drive; migrations not applied after a version jump; permission changes on the project storage directory.","solutions":["Read the chained cause {e} — \"database is locked\", \"no such table\", and \"disk I/O error\" each point to different fixes","Close other GitButler processes (desktop/CLI/TUI) touching the same project and retry","Ensure but-db migrations run on open and the schema version matches this binary","Check write permissions on the project database file and free disk space"],"exampleFix":"// before\n.insert(action.try_into()?).map_err(|e| anyhow::anyhow!(\"Failed to persist action: {e}\"))?;\n\n// after — keep the action id in the message so the failed record is identifiable\nlet row: ButlerActionRow = action.try_into()?;\nlet id = row.id.clone();\ndb.butler_actions_mut()\n\t.insert(row)\n\t.map_err(|e| anyhow::anyhow!(\"Failed to persist action {id}: {e}\"))?;","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"use anyhow::Context;\n\nlet mut attempts = 0u32;\nloop {\n    match persist_action(&mut db, action.clone()) {\n        Ok(()) => break,\n        Err(e) if attempts < 5 && e.to_string().contains(\"database is locked\") => {\n            std::thread::sleep(std::time::Duration::from_millis(50 << attempts));\n            attempts += 1; // transient SQLITE_BUSY: back off and retry\n        }\n        Err(e) => return Err(e.context(\"persist butler action\")),\n    }\n}","preventionTips":["Run only one GitButler process (desktop/CLI/TUI) per project database at a time","Let but-db migrations run on open; never reuse a database across schema versions","Keep the project database on local disk, not network or synced drives","Retry transient SQLITE_BUSY errors with backoff before surfacing them to users"],"tags":["database","sqlite","rust","persistence"],"backgroundTag":"database-write-failed","analyzedSha":"2497b8007aa4a1922dae9a805b32ffe5b5037785","analyzedAt":"2026-08-17T00:30:25.648Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}