How ai-memory Handles Concurrent Writes and Supersessions: Architecture Deep Dive
ai-memory guarantees race-free concurrent writes by serializing all mutating SQL operations through a single writer thread that maintains version history via a supersession chain, using SHA-256 hash checks to avoid duplicate versions.
The akitaonrails/ai-memory project implements a robust SQLite-backed storage layer for AI context management. To solve the classic "database is locked" problem while maintaining full edit history, the codebase employs a single-writer actor pattern combined with an immutable supersession chain. This architecture ensures that concurrent writes and supersessions are handled safely without explicit locks or complex conflict resolution.
Single-Threaded Writer Architecture
In crates/ai-memory-store/src/writer.rs, the WriterHandle::spawn method creates the sole rusqlite::Connection and confines it to a dedicated writer thread. All mutating operations—including page upserts, handoff inserts, and session moves—are funneled through a Tokio mpsc channel (WriteCmd) with a buffer of 1024 slots by default.
Each async caller dispatches a command paired with a oneshot::Sender for the reply. Because the writer processes commands sequentially, there is exactly one writer at any time, eliminating race conditions and the "database is locked" error mode entirely.
The Supersession Chain Mechanism
When content changes, ai-memory does not overwrite existing rows. Instead, ops::upsert_page in crates/ai-memory-store/src/ops.rs implements a supersession chain:
- New versions are inserted with
superseded_at IS NULL - Previous heads are updated with a timestamp, linking the rows chronologically
Hash-Based Short-Circuiting
Before creating a new version, upsert_page computes a SHA-256 hash of the page body. If the hash matches the current latest version, the function returns the existing PageId immediately, avoiding unnecessary supersessions and database writes.
Atomic Transaction Boundaries
The entire upsert logic—hash comparison, conditional insert, and update of the previous row's superseded_at—executes inside a single SQLite transaction within the writer thread. This atomicity prevents concurrent upserts from interleaving and creating duplicate or orphaned versions.
Scope-Aware Operations and Deletion Safety
The supersession chain persists across workspace and project boundaries. When executing MoveSession, the writer preserves the full version history even as pages migrate between scopes.
For deletions, delete_page_if_latest accepts an expected_latest_id parameter. The operation succeeds only if the supplied ID matches the current head of the supersession chain, preventing accidental removal of historical versions when concurrent edits occur.
Practical Implementation Examples
This pattern ensures that even when many asynchronous tasks issue writes simultaneously, the writer's channel buffers them and processes them one-by-one, preserving data integrity.
// Upserting a page creates a new version only when content changes
let page = NewPage {
workspace_id,
project_id,
path: page_path,
title: "My Page".into(),
body: "Updated content".into(),
};
let page_id = writer_handle.upsert_page(page).await?;
// Deleting only works on the latest version, protecting the chain
let deleted = writer_handle
.delete_page_if_latest(workspace_id, project_id, page_path, latest_id)
.await?;
assert!(deleted, "Only the head of the supersession chain may be removed");
// Moving sessions preserves supersession history across projects
writer_handle
.move_session(
session_id,
target_workspace,
target_project,
PagesMode::Include,
None,
false,
)
.await?;
Summary
- Single-writer serialization: All mutations pass through
WriterHandle::spawnand a Tokiompscchannel, ensuring linearizable operations without database locks. - Immutable versioning: The supersession chain in
ops::upsert_pagelinks versions viasuperseded_attimestamps, creating a complete audit trail. - Content-aware deduplication: SHA-256 hashing prevents redundant supersessions when page content remains unchanged.
- Atomic consistency: SQLite transactions within the writer thread guarantee that hash checks, inserts, and updates succeed or fail together.
- Safe deletion guards:
delete_page_if_latestrequires the caller to prove they hold the current head of the chain, preventing history corruption.
Frequently Asked Questions
How does ai-memory prevent "database is locked" errors during high concurrency?
By confining the rusqlite::Connection to a single writer thread and routing all writes through an mpsc channel, ai-memory ensures only one SQL statement executes at a time. This eliminates the busy-wait conditions that cause SQLite locking errors in multi-threaded scenarios.
What happens when two clients try to update the same page simultaneously?
Both requests queue in the writer's channel and process sequentially. The first completes a full upsert (hash check, insert, supersession update). The second performs its own hash check against the new head; if the content differs, it creates another version in the chain. The linear execution guarantees a valid chronological history without merge conflicts.
Can old versions of a page be deleted without breaking the supersession chain?
No. The delete_page_if_latest function requires the expected_latest_id parameter to match the current head. Attempting to delete an older version in the middle of the chain fails, ensuring the linked history remains intact and traversable via crates/ai-memory-store/src/reader.rs.
Does moving a page between projects preserve its edit history?
Yes. The MoveSession command preserves the superseded_at linkage. The page's entire version history remains accessible after migration because the supersession chain stores workspace and project associations per version rather than globally.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →