zeroclaw-labs/zeroclaw · critical · anyhow::Error

rename_agent not supported by this memory backend

Error message

rename_agent not supported by this memory backend

What it means

Final step of the DeviceRegistry::new warm-up: query_map executes the SELECT and builds the cache rows. The expect fires on runtime errors while running the query — most commonly SQLITE_BUSY ('database is locked') when another process or connection holds devices.db, and otherwise I/O errors such as disk full or a truncated file. Like the other constructor expects, it panics registry construction, taking pairing and startup with it.

Source

Thrown at crates/zeroclaw-api/src/memory_traits.rs:372

    /// Export every memory row attributed to `agent_alias`, for the agent-
    /// deletion archive (export-then-delete,). Pairs with
    /// [`Self::purge_agent`]: the surface exports these rows to the archive,
    /// then purges. Default: empty (backends without per-agent export).
    async fn export_agent(&self, _agent_alias: &str) -> anyhow::Result<Vec<MemoryEntry>> {
        Ok(Vec::new())
    }

    /// Re-point every memory row from the `from` alias to the `to` alias,
    /// returning the number of rows re-pointed. Called when an alias is renamed.
    /// For the SQL backends (sqlite/postgres) memory rows ride the
    /// agent's UUID, so this is a single `UPDATE agents SET alias` and the count
    /// is the agents-row count (0 or 1); payload-keyed backends (qdrant) rewrite
    /// the alias on every matching memory point and return that count.
    /// Default: unsupported error; backends with per-agent storage override.
    /// Markdown/none keep the default and the caller logs a warning.
    async fn rename_agent(&self, _from: &str, _to: &str) -> anyhow::Result<usize> {
        anyhow::bail!("rename_agent not supported by this memory backend")
    }

    /// Read-only residue probe for the agent-rename cascade: the count
    /// of state [`Self::rename_agent`] WOULD re-point for `agent_alias`, without
    /// mutating anything. Used by the gateway to tell a genuine post-persist
    /// partial failure (state still lagging at the old alias) apart from an
    /// unrelated request, so a resume only fires on real residue.
    ///
    /// MUST mirror exactly what `rename_agent` moves: for the SQL backends that
    /// is the `agents` row (alias presence), NOT the memory-row count - an agent
    /// with an `agents` row but zero memory rows still gets re-pointed, so a
    /// memory-row probe would be a false negative. Default 0 (markdown/none have
    /// no DB rows and their `rename_agent` is a no-op).
    async fn count_agent(&self, _agent_alias: &str) -> anyhow::Result<usize> {
        Ok(0)
    }

    /// Count total memories

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Ensure exactly one gateway instance runs per workspace; stop overlapping processes or supervisors mid-restart.
  2. Close external sqlite3 sessions on devices.db before starting the gateway.
  3. Check free space on the workspace volume and integrity (`PRAGMA integrity_check;`).
  4. Retry startup once the competing holder exits — WAL permits concurrent readers, so a persistent failure means a writer is holding the database exclusively.
  5. As a maintainer, return Result and retry briefly on SQLITE_BUSY during warm-up.

Example fix

// before
let rows = stmt
    .query_map([], |row| { ... })
    .expect("Failed to query devices");

// after (maintainer fix)
let rows = stmt
    .query_map([], |row| { ... })
    .context("failed to warm device registry cache (devices.db locked or I/O error)")?;
Defensive patterns

Strategy: validation

Validate before calling

// Advisory lock so two gateways never share devices.db:
fn workspace_exclusive(dir: &std::path::Path) -> bool {
    use std::os::unix::io::AsRawFd;
    let f = std::fs::OpenOptions::new()
        .write(true)
        .create(true)
        .open(dir.join(".gateway.lock"))
        .ok();
    match f {
        Some(f) => unsafe { libc::flock(f.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) == 0 },
        None => false,
    }
}

Try / catch

let reg = std::panic::catch_unwind(|| DeviceRegistry::new(&workspace_dir));
if reg.is_err() {
    // check for a second gateway or sqlite3 session holding devices.db, then retry once cleared
}

Prevention

When it happens

Trigger: A second gateway instance (or an external sqlite3 session) holds devices.db while this process warms its cache; the filesystem runs out of space mid-query; the file is truncated after a hard crash.

Common situations: Two gateway processes pointed at the same workspace during a migration or restart overlap; an operator inspecting devices.db with the sqlite3 CLI while the gateway starts; disk-full events on the workspace volume.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/935dadd8250e3407. Report an issue: GitHub.