# How ai-memory Prevents Read-After-Write Inconsistency: A Deep Dive into its SQLite Architecture

> Learn how ai-memory prevents read-after-write inconsistency. Discover its single-writer SQLite architecture and mpsc channel for guaranteed data integrity.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: deep-dive
- Published: 2026-08-26

---

**Ai-memory prevents read-after-write inconsistency by funneling all database mutations through a single-writer SQLite actor that processes commands via an `mpsc` channel, ensuring every write is fully committed before any read-only connection can observe the new state.**

The `ai-memory` crate (from the `akitaonrails/ai-memory` repository) eliminates classic race conditions in local-first AI knowledge bases by implementing a strict **write-commit-then-read pipeline**. By serializing mutations through a dedicated writer thread and isolating reads to a separate connection pool, the system guarantees that readers never observe partially committed or stale data. This architecture effectively prevents read-after-write inconsistency without requiring complex distributed consensus mechanisms.

## Single-Writer Actor: Serializing Mutations in writer.rs

At the core of ai-memory’s consistency model is the **single-writer SQLite actor** implemented in [`crates/ai-memory-store/src/writer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/writer.rs). All mutations are funneled through one dedicated thread that receives commands over an `mpsc` channel. This design guarantees that every write is serialized, committed in a single transaction, and only then becomes visible to readers.

Because the writer thread is the sole path that touches the writable database handle, concurrent operations must wait for the previous commit to finish. This eliminates the classic race where a read could slip in between a multi-step write operation.

## Isolating Reads with a Read-Only Connection Pool

Reads are served by a pool of SQLite connections defined in [`crates/ai-memory-store/src/reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs). These connections are strictly **read-only**, meaning they cannot see partially committed data. They always query the latest committed state after the writer thread finishes its transaction.

Since the read-only pool never shares transaction state with the writer, readers are physically incapable of observing uncommitted changes. When a write commits, the next query from any reader connection will see the new state, but never before.

## Atomic File System Guarantees for Wiki Storage

When updating markdown files, ai-memory employs an atomic write protocol to prevent half-written pages. In [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs), the system writes to a temporary file, performs an atomic rename operation, and calls `fsync` on the file descriptor. This **tmp + rename + fsync** pattern ensures that consumers never read truncated or corrupted content, even if the process crashes mid-write.

## Stable Identity with Scope-Resolved IDs

To prevent mismatched lookups that could surface stale data, ai-memory uses **scope-resolved IDs** defined in [`crates/ai-memory-core/src/id.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/id.rs). Every row is identified by the stable triple `(workspace_id, project_id, path)`. These scoped IDs are created once at write time and never recomputed on read, ensuring that identity remains constant throughout the write-commit-then-read cycle.

## Transaction-Boundary Indexing

Index updates—including FTS5 indices and foreign-key tables—are performed inside the same SQLite transaction that writes the data. As implemented in [`crates/ai-memory-store/src/schema.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/schema.rs), there is no “index-after-return” background job that could expose an interim state. This **transaction-boundary indexing** guarantees that indexes and primary data remain synchronized at every commit point.

## The Write-Commit-Then-Read API in Practice

The public API surface enforces these guarantees automatically. Client code never needs to manually coordinate between writers and readers because the library blocks appropriately until consistency is achieved.

```rust
use ai_memory_store::{Store, WriteCommand};

// 1️⃣ Acquire the store (creates the writer thread + read pool)
let store = Store::open_default()?;

// 2️⃣ Write a new page (the command is queued to the writer)
store.write_page("my/project", "notes.md", b"Hello world!")?;

// 3️⃣ Immediately read the page – the read will block until the previous
//    write transaction has been committed, guaranteeing consistency.
let content = store.read_page("my/project", "notes.md")?;
assert_eq!(content, b"Hello world!");

```

*The `write_page` call does **not** return until the writer thread has successfully committed the transaction. The subsequent `read_page` therefore always sees the freshly written data.*

This pattern—**write request → writer actor → transaction commit → read notification**—forms the complete pipeline that prevents read-after-write anomalies. For a high-level overview of these invariants, see [`docs/ARCHITECTURE.md`](https://github.com/akitaonrails/ai-memory/blob/main/docs/ARCHITECTURE.md) in the repository.

## Summary

- **Single-writer serialization**: All mutations pass through a dedicated actor in [`writer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/writer.rs) that processes commands via an `mpsc` channel, ensuring writes never overlap.
- **Read-only isolation**: The connection pool in [`reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/reader.rs) serves only committed data and cannot view uncommitted transactions.
- **Atomic durability**: File updates in [`wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/wiki.rs) use tmp + rename + fsync to prevent partial reads on the filesystem.
- **Stable identity**: The `(workspace_id, project_id, path)` triple in [`id.rs`](https://github.com/akitaonrails/ai-memory/blob/main/id.rs) eliminates stale lookup issues by never recomputing IDs.
- **Synchronous indexing**: Schema updates in [`schema.rs`](https://github.com/akitaonrails/ai-memory/blob/main/schema.rs) occur within the same transaction as data writes, preventing index lag.
- **Blocking API guarantees**: Public methods like `write_page` block until commit, making immediate subsequent reads always consistent.

## Frequently Asked Questions

### Does ai-memory use database locks to prevent read-after-write inconsistency?

No, ai-memory does not rely on traditional database locking for its primary consistency guarantee. Instead, it uses the **single-writer actor pattern** in [`crates/ai-memory-store/src/writer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/writer.rs) to serialize all mutations through one thread. Since only this thread can modify the database, and reads use separate read-only connections that only see committed data, there is no opportunity for a reader to observe a write in progress.

### How does ai-memory handle concurrent writes from multiple threads?

All write requests from any thread are converted into `WriteCommand` objects and sent to the single writer actor via an `mpsc` channel. The writer processes these commands sequentially in a single thread. This means writes are inherently serialized; the second write cannot begin until the first write’s transaction has fully committed, preventing any interleaving of partial states.

### Can ai-memory readers ever see uncommitted or partially written data?

No, readers are physically incapable of seeing uncommitted data. The **read-only connection pool** in [`crates/ai-memory-store/src/reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs) uses SQLite connections opened with read-only flags. These connections query the database file at specific snapshot points and cannot access the write-ahead log or uncommitted pages that the writer thread holds open during an active transaction.

### What ensures that full-text search indexes are consistent with the main data?

Ai-memory implements **transaction-boundary indexing** where all index updates—including FTS5 tables—are executed within the same SQLite transaction as the data modification. As defined in [`crates/ai-memory-store/src/schema.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/schema.rs), there are no background jobs or eventual consistency windows; the transaction commits only when both the primary data and all associated indexes are fully written, ensuring atomic visibility.