How to Restore an ai-memory SQLite Database from a Backup: Complete CLI Guide

Restoring an ai-memory database requires stopping the server first, then using the ai-memory restore --from <tarball> --force command to extract a gzipped backup over the data directory.

The ai-memory project by akitaonrails/ai-memory stores its knowledge graph in a SQLite file located at db/memory.sqlite within the data directory. Because a background writer actor continuously accesses this database, restoration is treated as a disk-level lifecycle operation that must run exclusively when no server instances are active. The CLI enforces this constraint through system process detection before extracting the tarball.

Prerequisites and Safety Constraints

Before running any restore command, you must ensure complete process isolation. According to the lifecycle documentation in docs/lifecycle-ops.md, the restore operation is only safe when the server is fully stopped.

The CLI implements this via the sibling_processes() function in crates/ai-memory-cli/src/commands/restore.rs. This routine scans for any running ai-memory processes and aborts immediately if siblings are detected, preventing data corruption from concurrent access:

// From restore.rs lines 34-37
if !sibling_processes.is_empty() {
    eprintln!("Error: Cannot restore while ai-memory is running.");
    process::exit(1);
}

Attempting to restore while the server is active would corrupt the SQLite file because the backup overwrites the entire data directory while the writer expects exclusive file locks.

Understanding the Backup Format

The backup itself is produced by the admin endpoint POST /admin/backup implemented in crates/ai-memory-mcp/src/admin.rs. The handler streams a gzipped tarball containing:

This format allows hot backups (taken while the server runs) to be safely restored onto cold storage (when the server is stopped).

Step-by-Step Restore Process

The restoration logic in crates/ai-memory-cli/src/commands/restore.rs follows a strict validation pipeline:

1. Process-Safety Check

The command first invokes sibling_processes() to scan the system process table. If any ai-memory instance is found alive, the CLI exits with a clear error message before touching any files.

2. Validate the Source Tarball

The --from argument must point to an existing file on disk. The CLI verifies path existence and readability before attempting extraction.

3. Handle Existing Data with --force

If the target wiki/ directory or db/memory.sqlite already exist, the command requires the --force flag. Without it, the CLI aborts with an explanatory error. When --force is provided, the existing directories are removed while preserving config.toml, logs, and model files:

// From restore.rs lines 52-59
if force {
    if wiki_dir.exists() { fs::remove_dir_all(wiki_dir)?; }
    if db_dir.exists() { fs::remove_dir_all(db_dir)?; }
}

4. Extract and Validate Entries

The tarball is opened using GzDecoder and tar::Archive. Each entry passes through validate_restore_entry, which guarantees that only safe paths, regular files, and expected top-level directories (wiki, db, config.toml) are unpacked. This function prevents directory traversal attacks by validating paths before extraction.

5. Reopen and Migrate

After extraction, the code calls Store::open(&config.data_dir). This triggers any pending Refine migrations and validates the restored SQLite file, ensuring the database is usable before the CLI reports success:

// From restore.rs lines 71-74
let store = Store::open(&config.data_dir)?;

CLI Command Examples

Fetch a backup from a running server, stop the service, then restore:


# Create backup from live server

ai-memory backup --to /tmp/ai-memory-backup.tar.gz

# Stop any running instance

systemctl stop ai-memory

# Restore with forced overwrite

ai-memory restore --from /tmp/ai-memory-backup.tar.gz --force

Programmatic Restoration (Rust API)

For custom tooling, you can replicate the CLI logic using the ai-memory-store crate:

use ai_memory_store::Store;
use anyhow::Result;
use std::path::Path;
use flate2::read::GzDecoder;
use tar::Archive;

fn restore_backup(data_dir: &Path, tarball: &Path) -> Result<Store> {
    // Open the gzipped tarball
    let file = std::fs::File::open(tarball)?;
    let decoder = GzDecoder::new(file);
    let mut archive = Archive::new(decoder);

    // Extract entries (validation logic omitted for brevity)
    for entry in archive.entries()? {
        let mut entry = entry?;
        entry.unpack_in(data_dir)?;
    }

    // Open store and apply pending migrations
    Store::open(data_dir)
}

Summary

  • Stop the server before restoring; the CLI enforces this via sibling_processes() to prevent corruption.
  • Use --force to overwrite existing wiki/ and db/ directories when restoring to an initialized data directory.
  • Backups are gzipped tarballs containing SQLite snapshots and wiki content, created via the online backup API in reader.rs.
  • The validate_restore_entry function blocks directory traversal attacks during extraction.
  • Calling Store::open() after extraction automatically runs pending Refine migrations on the restored database.

Frequently Asked Questions

Can I restore while the ai-memory server is running?

No. The restore command aborts if sibling_processes() detects any active ai-memory instances. Restoration overwrites files that the writer actor expects to control exclusively, which would corrupt the database. Always stop the server first.

What files are overwritten when using --force?

The --force flag removes only the wiki/ and db/ directories before extraction. Your config.toml, log files, and downloaded model files remain untouched. This design preserves configuration while replacing data content.

How does the restore command prevent directory traversal attacks?

The validate_restore_entry function inspects each tar entry before unpacking, ensuring paths stay within the data directory and only expected file types (regular files and safe directories) are extracted. Malformed entries containing .. sequences or absolute paths are rejected.

Does restoring trigger database migrations?

Yes. After extraction, the CLI calls Store::open(&config.data_dir), which automatically applies any pending Refine schema migrations to the restored SQLite file. This ensures the database schema matches the current codebase version before the operation completes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →