Skip to content

RFC/draft: opt-in libSQL/Turso storage backend for concurrent multi-writer memory#262

Draft
Donach wants to merge 8 commits into
rtk-ai:mainfrom
Donach:turso-backend-minimal
Draft

RFC/draft: opt-in libSQL/Turso storage backend for concurrent multi-writer memory#262
Donach wants to merge 8 commits into
rtk-ai:mainfrom
Donach:turso-backend-minimal

Conversation

@Donach

@Donach Donach commented Jun 4, 2026

Copy link
Copy Markdown

Stacked on #320 (Nix flake packaging). This branch includes #320's commit, so the diff here shows flake.nix/flake.lock/README too until #320 merges — after that this PR shrinks to just the backend (+ a small icm-turso flake package output added here). Review #320 first if you're taking both; this PR works without the flake if you'd rather drop #320.

Summary — opt-in libSQL/Turso storage backend (4th additive backend)

ICM keeps memory in a single local SQLite file, so it can't be shared or written
concurrently from more than one machine/process. This adds an opt-in Turso/libSQL
backend that runs the existing icm-store SQL over the async libsql client, so
the store can live in a libSQL/Turso database — local file, a remote sqld/Turso
server, or an embedded replica. A remote server is the multi-writer path (serialises
writes from every ICM process).

The default build is completely unchangedrusqlite, exactly as today.

Update: now using shared-source (#[path]) — upstream-mergeable

The original draft duplicated ~7,000 lines (turso_store.rs + turso_schema.rs).
This revision eliminates the copies entirely via Rust's #[path] attribute:

// sqlite_backend.rs — compiles store.rs/schema.rs with real rusqlite
pub(crate) mod sql { pub use rusqlite::{params, Connection, ...}; }
#[path = "schema.rs"] pub(crate) mod schema;
#[path = "store.rs"]  pub(crate) mod store;

// turso_backend.rs — same files with the libsql sync facade
pub(crate) mod sql { pub use crate::dbcompat::{params, Connection, ...}; }
#[path = "schema.rs"] pub(crate) mod schema;
#[path = "store.rs"]  pub(crate) mod store;

Net change vs the original draft: −8,022 lines (302 inserted, 8,324 deleted).
store.rs and schema.rs are only touched by two mechanical changes:

  • use rusqlite::use super::sql:: (provider alias)
  • collect_rows<T> signature: MappedRows<'_, F>impl Iterator<Item = sql::Result<T>>

This makes the backend a genuine minimal diff against upstream and easy to rebase going forward.

What's new since the original draft

  • Fully rebased on v0.10.57 (76 upstream commits, including the additive-backend sprint feat(store): support pluggable storage backends (beyond SQLite) #301)
  • Turso is the 4th additive backend alongside sqlite/postgres/opensearch — same dispatch! macro
  • Auto-detect: if ICM_DB_BACKEND is unset but TURSO_DATABASE_URL or LIBSQL_URL is non-empty, turso is selected automatically (gated behind #[cfg(feature = "turso")])
  • dbcompat.rs: 540-line synchronous rusqlite-shaped facade over the async libsql client
  • Full-coverage: unlike postgres/opensearch, the Turso backend supports all of memoirs, concepts, transcripts, feedback, and facts (same SQL dialect as sqlite)

Feature flag / env vars

# Cargo.toml
[features]
turso = ["dep:libsql", "dep:libsql-ffi", "dep:once_cell", "dep:tokio", "dep:sqlite-vec", "dep:zerocopy"]
# Auto-detect (no ICM_DB_BACKEND needed):
export TURSO_DATABASE_URL=http://localhost:8080       # local sqld
export TURSO_DATABASE_URL=libsql://xyz.turso.io      # Turso cloud

# Explicit:
export ICM_DB_BACKEND=turso

Test results

cargo test -p icm-store --lib
  test result: ok. 190 passed; 0 failed; 2 ignored  (sqlite)

cargo test -p icm-store --no-default-features --features turso --lib
  test result: ok. 194 passed; 0 failed; 2 ignored  (turso)

All upstream tests pass on both backends. Smoke-tested against sqld 0.24.33:
memoir, memory, facts, feedback, transcript — all ✅.

Head branch

This PR's head has been updated to Donach:rebase/turso-on-upstream-v0.10.57
(the turso-backend-minimal branch on the Donach/icm fork is our production
branch and cannot be force-pushed — it is tracked by NixOS infrastructure).


RFC — happy to reshape per your feedback before marking ready.

@Donach Donach changed the title RFC/draft: optional libSQL/Turso storage backend for concurrent multi-writer memory RFC/draft: opt-in libSQL/Turso storage backend for concurrent multi-writer memory Jun 4, 2026
@Donach
Donach force-pushed the turso-backend-minimal branch 2 times, most recently from 6616d9b to a779688 Compare July 9, 2026 20:26
@Donach
Donach force-pushed the turso-backend-minimal branch from a779688 to edef56a Compare July 10, 2026 07:51
dhavli and others added 8 commits July 10, 2026 14:01
Adds a flake.nix so Nix/NixOS users can build, run, and install icm
directly from the repo:

  nix run github:rtk-ai/icm
  nix profile install github:rtk-ai/icm

Design notes:
- cargoLock.lockFile vendors deps straight from the committed Cargo.lock,
  so there is no cargoHash to bump on dependency updates — the flake is
  zero-maintenance for day-to-day development.
- OPENSSL_NO_VENDOR + ORT_STRATEGY=system link the nixpkgs openssl and
  onnxruntime (fastembed/ort cannot download prebuilts in the Nix sandbox).
- Default features (rusqlite backend, embeddings, tui) — the binary is
  identical in behaviour to a stock cargo build.
- devShells.default provides the Rust toolchain + native deps for hacking.
- Linux systems only for now (x86_64/aarch64); darwin likely works but is
  unverified, trivially addable.

No impact on cargo users; .gitignore gains the nix 'result' symlink.
Upstream (rtk-ai/icm) has been on an active backend-pluggability sprint
since our fork point (v0.10.50 / commit 804dac2). 76 commits landed,
the biggest being:
 - feat(store/postgres): opt-in PostgreSQL backend (rtk-ai#302)
 - feat(store/opensearch): opt-in OpenSearch backend (rtk-ai#304)
 - refactor(store): additive backends + runtime selection in one binary (rtk-ai#301)
   (ICM_DB_BACKEND=sqlite|postgres|opensearch; all backends in one binary)
 - feat(init): icm forget + icm list in icm_block (rtk-ai#252)
 - fix(init): Codex PostToolUse opt-in (rtk-ai#293)
 - feat(serve): persistent local HTTP API (rtk-ai#291)
 - feat(store/facts): structured facts table (rtk-ai#273)
 - feat(store/code_areas): code_areas table (rtk-ai#196)

The fork's changes (3 commits, 4d2853f..9a67d54) were:
1. Minimal-diff libSQL overlay (dbcompat.rs — rusqlite-shaped sync facade)
2. Make turso an opt-in Cargo feature (backend-rusqlite vs turso, mutually exclusive)
3. flake.nix + onnxruntime LD_LIBRARY_PATH wrapProgram fix

Integration strategy (forward-porting, not rebasing commits):
- Upstream restructured to additive backends + runtime dispatch via Store enum
  in backend.rs (BackendKind::Sqlite/Postgres/OpenSearch + dispatch! macro).
- We add Turso as a 4th additive backend: `--features turso` compiles in
  TursoStore; ICM_DB_BACKEND=turso (or TURSO_DATABASE_URL set) selects it.
- turso_store.rs = upstream store.rs adapted to use `dbcompat as rusqlite`
  + turso-specific open_connection/apply_pragmas (URL env var logic).
- turso_schema.rs = upstream schema.rs adapted similarly.
- dbcompat.rs: updated libsql dep to include `remote` feature (needed for
  Builder::new_remote in libsql 0.9; fork had `core,replication` only).
- MappedRows<'_, F> → MappedRows<T> (dbcompat's type has no lifetime param).
- flake.nix: bumped version string to 0.10.57-turso; builds with
  --no-default-features --features turso (additive, not conflicting).

Both builds verified:
- cargo check -p icm-cli (default, backend-sqlite): OK
- cargo check --no-default-features --features "turso,embeddings,tui" -p icm-cli: OK

Conflicts resolved:
- crates/icm-store/src/lib.rs: keep upstream's full module structure, add
  turso/dbcompat/turso_schema/turso_store under #[cfg(feature = "turso")]
- crates/icm-store/Cargo.toml: keep upstream's feature/dep structure, add
  turso feature + libsql/once_cell optional deps
- Cargo.lock: take upstream's, regenerate (cargo update) for new libsql+remote
- Cargo.toml: add libsql/libsql-ffi/once_cell workspace deps with remote feature

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the 7393-line verbatim copy (turso_store.rs) and 847-line
schema copy (turso_schema.rs) with a shared-source approach:

- Add sqlite_backend.rs: wraps store.rs/schema.rs with real rusqlite
- Add turso_backend.rs: wraps the same files with the dbcompat shim
- Both use `#[path]` to load store.rs and schema.rs from src/ directly
- lib.rs restructured to declare file-based backend modules
- backend.rs updated to import from the new module paths
- store.rs: collect_rows() now uses impl Iterator instead of MappedRows<'_, F>
  so it compiles under both rusqlite (MappedRows<'stmt, F>) and dbcompat
  (IntoIter<Result<T>>); add `use super::sql;` for qualified sql:: paths
- schema.rs: import params from super::sql directly; use params! not sql::params!
- Delete turso_store.rs (7398 lines) and turso_schema.rs (847 lines)

All three cargo check variants pass clean:
  cargo check -p icm-cli                                    (default/sqlite)
  cargo check --no-default-features --features turso,tui   (turso-only)
  cargo check --features backend-sqlite,turso,tui           (both compiled in)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VhDidsi8NWn6udUMLr6Sv
Three correctness fixes surfaced by the shared-source compile:

1. schema.rs test: `super::store` resolves to the schema module's own
   parent (sqlite_backend::schema), not to the backend wrapper.  Need
   `super::super::store` to reach the sibling store module.

2. store.rs perf test: `perf_fts_search_100` hard-coded a 1 s ceiling
   that only holds for native rusqlite.  The dbcompat/libsql path wraps
   an async runtime in a sync facade, which is 2–3× slower in-process.
   Raise the ceiling to 5 s so the test remains a sanity-check rather
   than a rusqlite-only benchmark that always fails on the turso path.

3. Cargo.toml: libsql `tls` feature is required even for plain http://
   URLs — the libsql crate gates rustls on it, and Builder::new_remote
   panics without it.  Add `"tls"` to the feature list.

Also regenerate Cargo.lock for the libsql tls dependency.

Smoke-tested all turso paths against a local sqld:
  memoir create/list, memory store/recall, facts set/get,
  feedback record/list, transcript start-session/record/show/search

All 194 turso-backend unit tests pass (0 failures after the ceiling fix).
All 190 sqlite-backend tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove flake.nix and flake.lock (NixOS packaging, fork-only)
- Update docs/turso-backend.md:
  - Remove flake.nix mention
  - Correct "mutually exclusive" → additive feature (with link caveat)
  - Replace "byte-identical via alias" with the #[path] shared-source description
  - Update test counts (190/194, all passing)
  - Update Known limitations (perf ceiling adjusted, not a failure)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends the base flake (see the flake-packaging PR this stacks on) with a
second package output built with --no-default-features --features
turso,embeddings,tui. A separate output rather than one combined binary
because libsql-ffi and libsqlite3-sys both bundle the sqlite3 amalgamation
and their symbols collide at link time (documented in docs/turso-backend.md).

Also wraps the binary with the libstdc++/onnxruntime LD_LIBRARY_PATH fix:
system-ORT onnxruntime needs a newer GLIBCXX than the Rust toolchain's
libstdc++ that otherwise wins the RPATH ordering.
…estructure

Adversarial review of the shared-source restructure found the turso backend
never actually went remote: dbcompat had open_remote()/open_replica(), but
the shared store.rs called Connection::open(path) — always a local libsql
file. TURSO_DATABASE_URL was silently ignored; a dead server "worked" by
writing to a local file (split-brain waiting to happen).

- provider modules (sqlite_backend.rs / turso_backend.rs) now own
  open_backend() and apply_pragmas(); the turso variant restores the
  original env dispatch (URL → remote, +ICM_TURSO_REPLICA → embedded
  replica, none → local file) and remote-safe PRAGMAs. Shared store.rs
  diff: two lines (open + pragma call).
- BackendKind::from_env(): with nothing set, default to the first
  *compiled* local-capable backend (sqlite, else turso) instead of a
  hard not-compiled error in turso-only builds. Test updated to match.
- flake.nix: parameterised mkIcm builder — the previous overrideAttrs
  approach silently produced a default-features (rusqlite!) binary named
  icm-turso, because buildRustPackage consumes buildFeatures at call time.

Verified: dead URL now fails loudly (connection refused); store/recall +
memoir against a throwaway local sqld with the row confirmed SERVER-side
via raw /v2/pipeline HTTP; icm-store tests 190/190 (sqlite) + 194/194
(turso); ICM_DB_BACKEND=sqlite on a turso-only build errors as intended.
…10s)

The libsql HTTP client has no default request timeout, so a stalled remote
connection (packet loss, wedged server) hangs a synchronous icm CLI call —
and any editor/agent hook that shells out to it — forever. Route every DB
round-trip in the dbcompat facade through a deadline: ICM_DB_TIMEOUT
seconds, default 10, 0 disables. Local-file operations are far under the
limit and unaffected. Timeouts surface as a distinct error naming the
variable instead of an indefinite hang.
@Donach
Donach force-pushed the turso-backend-minimal branch from 7c076e3 to 6f74f1f Compare July 10, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants