gitbutlerapp/gitbutler · error

Failed to persist action: {e}

Error message

Failed to persist action: {e}

What it means

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.

Source

Thrown at crates/but-action/src/action.rs:152

        Self {
            id: Uuid::new_v4(),
            created_at: chrono::Local::now().naive_local(),
            handler,
            external_prompt,
            external_summary,
            snapshot_before,
            snapshot_after,
            response: rsp.cloned(),
            error,
            source,
        }
    }
}

fn persist_action(db: &mut DbHandle, action: ButlerAction) -> anyhow::Result<()> {
    db.butler_actions_mut()
        .insert(action.try_into()?)
        .map_err(|e| anyhow::anyhow!("Failed to persist action: {e}"))?;
    Ok(())
}

/// Persist a completed handle-changes action record and return its generated ID.
///
/// `db` is the already-open project database handle to write into. `change_summary`,
/// `external_prompt`, `handler`, and `source` describe the request that produced the action.
/// `snapshot_before` and `snapshot_after` link the action to the surrounding oplog snapshots.
/// `response` is stored as either the successful action outcome or the error text.
#[expect(clippy::too_many_arguments)]
pub(crate) fn record_handle_changes_action(
    db: &mut DbHandle,
    change_summary: &str,
    external_prompt: Option<String>,
    handler: crate::ActionHandler,
    source: Source,
    snapshot_before: gix::ObjectId,
    snapshot_after: gix::ObjectId,

View on GitHub (pinned to 2497b8007a)

Solutions

  1. Read the chained cause {e} — "database is locked", "no such table", and "disk I/O error" each point to different fixes
  2. Close other GitButler processes (desktop/CLI/TUI) touching the same project and retry
  3. Ensure but-db migrations run on open and the schema version matches this binary
  4. Check write permissions on the project database file and free disk space

Example fix

// before
.insert(action.try_into()?).map_err(|e| anyhow::anyhow!("Failed to persist action: {e}"))?;

// after — keep the action id in the message so the failed record is identifiable
let row: ButlerActionRow = action.try_into()?;
let id = row.id.clone();
db.butler_actions_mut()
	.insert(row)
	.map_err(|e| anyhow::anyhow!("Failed to persist action {id}: {e}"))?;
Defensive patterns

Strategy: retry

Try / catch

use anyhow::Context;

let mut attempts = 0u32;
loop {
    match persist_action(&mut db, action.clone()) {
        Ok(()) => break,
        Err(e) if attempts < 5 && e.to_string().contains("database is locked") => {
            std::thread::sleep(std::time::Duration::from_millis(50 << attempts));
            attempts += 1; // transient SQLITE_BUSY: back off and retry
        }
        Err(e) => return Err(e.context("persist butler action")),
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17). Data as JSON: /api/errors/aebda9c923db8416. Report an issue: GitHub.