feat(backup): SQLite Online Backup API, auto-backup, icm export/import - #431
Open
kzzalews wants to merge 1 commit into
Open
feat(backup): SQLite Online Backup API, auto-backup, icm export/import#431kzzalews wants to merge 1 commit into
kzzalews wants to merge 1 commit into
Conversation
kzzalews
force-pushed
the
feature/backup-api-and-export
branch
from
August 18, 2026 09:44
335cc72 to
98c791c
Compare
Fixes rtk-ai#313 — three durability gaps for multi-agent setups: ## A. SQLite Online Backup API (replaces std::fs::copy) backup_db() previously copied the database file with std::fs::copy plus separate copies of the -wal/-shm sidecars — two syscalls that could capture an inconsistent snapshot under active WAL writes. Replaced with rusqlite::Connection::backup() (sqlite3_backup_init/step/finish), which locks pages one at a time, retries pages dirtied by concurrent writers, and produces a fully-checkpointed destination without needing sidecars. New command: icm backup [--output PATH] New Unix permission: backup files created with mode 0o600 (non-fatal warn on FAT32 / NFS where chmod is unavailable). ## B. Preventive auto-backup + BackupConfig Auto-backup (default: enabled, interval 7 days, keep 5 files) runs on every writable store open via open_store_with_backup(). Race condition in multi-agent thundering-herd scenarios (N agents starting at once) eliminated by claim_backup_slot(): a single atomic SQL upsert (INSERT … ON CONFLICT DO UPDATE … WHERE julianday(now)-julianday(value) >= N) guarantees exactly one process performs the backup; others see changed==0 and skip. Modelled on the existing maybe_auto_decay() pattern. After a failed backup the slot is reset to the Unix epoch (1970-01-01T00:00:00+00:00) instead of chrono::DateTime::MIN_UTC (year -262143, outside SQLite julianday()'s 0000–9999 range, which would return NULL and permanently block retries). backup.keep_backups = 0 disables rotation (backups accumulate indefinitely); a once-per-process OnceLock warning is emitted when this is set with backup enabled. Configuration ([store.backup] in config.toml): enabled = true # opt-out with enabled = false interval_days = 7 keep_backups = 5 # 0 = accumulate indefinitely (warn) ## C. Portable JSONL snapshot: icm export / icm import --from-export icm export [--output PATH] [--format jsonl|json] Dumps memories, facts, and feedback to a SQLite-independent JSONL snapshot. Each line is a self-contained JSON object with a "type" discriminator (header / memory / fact / feedback). Sessions and transcripts are intentionally excluded (transient, high-volume). icm import --from-export <PATH|-> Restores a snapshot produced by icm export. Idempotent: records whose ID already exists are silently skipped. Supports stdin (-). Mutually exclusive with <PATH>, --format, and --project (snapshot topics are restored verbatim from the file). Idempotency fixes applied during review: KRIT-1: fact import compares value (not ID — set_fact always generates a new ULID, so ID comparison never matched). KRIT-2: memory import checks stored_id == expected_id after store.store() to detect summary_hash dedup. KRIT-3: feedback import uses a pre-built HashSet<id> (O(n) instead of O(n²) list_feedback per record). The previous icm import-from-export command is retained as a hidden, deprecated alias (eprintln! migration hint emitted at runtime). Integrated into icm import via --from-export flag; --project conflicts with --from-export because snapshot topics are restored verbatim. ## Additional fixes - list_all() LIMIT 10000 removed (silent data loss on export of large DBs) - rotate_backups uses exact prefix {stem}.backup- (KRIT-4: prevents accidental deletion of unrelated files) - rotate_backups logs tracing::warn! on removal failures (D5) - cmd_export uses .expect(msg) instead of .unwrap() (6 occurrences) - dry-run output clarified: "file totals — existing records not checked" - keep_backups = 0 documented in default.toml with disk-space warning - default.toml [store.backup] updated: backup is ENABLED by default ## Tests added - claim_backup_slot_first_call_wins - claim_backup_slot_second_call_within_interval_skips - claim_backup_slot_after_interval_wins_again - claim_backup_slot_epoch_reset_allows_immediate_retry - list_all_facts_returns_only_active - rotate_backups_removes_oldest_files - rotate_backups_does_not_remove_unrelated_files - rotate_backups_keep_zero_does_nothing - rotate_backups_noop_when_within_limit - export_import_roundtrip_is_idempotent - cmd_import_from_export_is_idempotent
kzzalews
force-pushed
the
feature/backup-api-and-export
branch
from
August 18, 2026 09:48
98c791c to
dfd185e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🛡️ Fixes #313 — Database durability for multi-agent setups
Closes #313 — SQLite store corruption under concurrent writes.
ICM is designed to be the shared brain of multiple AI agents writing concurrently to the same
memories.db. The existingstd::fs::copybackup, the lack of preventive backups, and the absence of a SQLite-independent recovery format left that brain unprotected. This PR closes all three gaps.🔧 What's changed
A · SQLite Online Backup API — safe backups under concurrent writers
Solution: replaced with
rusqlite::Connection::backup()(sqlite3_backup_init/step/finish), which locks pages one at a time, retries pages dirtied by concurrent writers, and produces a fully-checkpointed copy without needing sidecars.New command:
Backup files are created with mode
0o600on Unix (non-fatal warn on FAT32/NFS).B · Preventive auto-backup +
BackupConfigSolution:
open_store_with_backup()runs a background backup on every writable store open, subject to a configurable interval.Race condition eliminated. In thundering-herd scenarios (50 agents starting at once), the previous read→decide→write sequence let all of them trigger a backup simultaneously. A single atomic SQL upsert now acts as a distributed mutex — exactly one process wins:
After a failed backup the slot resets to the Unix epoch (
1970-01-01T00:00:00+00:00) instead ofchrono::DateTime::MIN_UTC(year −262143, outside SQLitejulianday()'s 0000–9999 range, which returnsNULLand permanently blocks retries).Configuration (
config.tomlorICM_CONFIG):C ·
icm export/icm import --from-export— portable JSONL snapshotSolution: a streaming JSONL snapshot that any tool can read.
Each line is a self-contained JSON object:
{"type":"header","icm_export_version":1,"exported_at":"2026-08-18T...","counts":{"memories":42,...}} {"type":"memory","id":"01JX...","topic":"arch","summary":"We chose SQLite..."} {"type":"fact","id":"01JX...","entity":"db","key":"host","value":"prod-1"} {"type":"feedback","id":"01JX...","topic":"api",...}Restore (idempotent — existing records are silently skipped):
🧪 Tests added
claim_backup_slotrotate_backupsexport/importcmd_import_from_exportidempotencylist_all_facts📋 Files changed
Cargo.tomlbackupfeature torusqliteconfig/default.toml[store.backup]section with documented defaultscrates/icm-cli/src/config.rsBackupConfig, extendedStoreConfigcrates/icm-cli/src/main.rsbackup_db,open_store_with_backup,rotate_backups,cmd_backup,cmd_export,cmd_import_from_export,icm import --from-exportcrates/icm-store/src/store.rsbackup_to,claim_backup_slot,list_all(no LIMIT),list_all_facts,get/set_metadata_strcrates/icm-store/src/backend.rs✅ Checklist
cargo fmt --all -- --checkcleancargo clippy --workspace --all-targets -- -D warningscleancargo test --workspace— 225 passed, 0 new failuresicm import,icm repair,icm doctorunchangedchmod 0o600are platform-guarded)🤖 How this PR was built
This PR was developed using a mixture-of-experts agentic workflow inside Kiro:
The implementation went through 8 rounds of multi-model review (up to 10 independent agents per round), each producing a consolidated report before fixes were applied. Known bugs caught this way include the
MIN_UTC/julianday()incompatibility, the thundering-herd race in auto-backup, O(n²) feedback dedup, and silent--projectignore.