Understanding Cross-Cutting Invariants in ai-memory's Architecture

The ai-memory system enforces fifteen cross-cutting invariants—from single-writer SQLite channels to typed sanitization boundaries—that guarantee data integrity, thread safety, and predictable behavior across every Rust crate.

The akitaonrails/ai-memory repository implements these architectural rules as non-negotiable constraints documented in docs/ARCHITECTURE.md. Each invariant addresses a specific failure mode discovered in prior prototyping, ensuring that configuration, storage, and concurrency behave identically across all runtime paths.

What Are Cross-Cutting Invariants?

Cross-cutting invariants are architectural constraints that span multiple modules or layers of an application. Unlike local business logic, these rules apply universally—regardless of which crate, function, or thread is executing—to prevent systemic failures.

The 15 Cross-Cutting Invariants in ai-memory

Configuration and Initialization

  • One config-read path: Config::load() executes exactly once at startup. No module reads environment variables directly, ensuring a single source of truth and preventing hidden-state bugs.
  • Absolute canonical data directory: The data directory resolves to an absolute path once at startup and is logged prominently. All components reference this same physical location to avoid accidental duplication.
  • Zero-LLM default: The server operates fully without any LLM provider configured, enabling headless deployment and simplifying operations for users who do not need AI features.
  • Provider auth resolves before construction: Each LLM provider receives a typed ProviderAuth object during initialization; implementations never read environment variables directly. This centralizes secret handling and makes credential rotation safe.
  • No global singletons: The codebase explicitly rejects lazy_static or hidden globals. All dependencies are injected, making the system testable and preventing state leakage across threads.

Storage and Data Integrity

  • Single-writer SQLite actor: All mutations route through a single mpsc channel to one dedicated OS thread. This prevents race conditions while preserving SQLite's ACID guarantees.
  • Indexes commit in the same transaction as data: Schema changes and associated FTS5 or entity indexes write atomically with payload rows. This prevents stale search indexes after a crash.
  • Typed three-tuple identity: Every domain row stores (workspace_id, project_id, path) from day zero. This enables proper multi-workspace isolation and safe cross-project linking.
  • Embedding metadata denormalised: Each page_embeddings row stores the provider, model, and dimension alongside the vector. Stale vectors are automatically ignored when embedding configurations change.
  • Atomic file writes: Wiki writes use a temporary file, rename, then fsync. The file-watcher ignores its own writes by filename prefix, guaranteeing durability of the markdown wiki.

Security and Validation

  • Privacy strip is a typed boundary: Only the sanitize() function can create a Sanitized<NewObservation> value. Untrusted text must pass through this sanitizer before entering the store.
  • JSON-schema-only structured outputs: LLM providers must emit JSON matching a predefined schema. The system rejects XML or free-form text to keep downstream parsers stable and prevent injection attacks.

Concurrency and Runtime Safety

  • Hooks are fire-and-forget: Hook scripts enforce a timeout of ≤200 ms, and the server replies with HTTP 202 (or 429 when saturated). This guarantees user-visible CLI commands never block on external network latency.
  • Live-process check before direct-disk lifecycle ops: Destructive commands like reset, restore, reindex, and uninstall --purge-data first query the running process via sysinfo. This prevents file corruption while the server actively uses the data directory.
  • Tracing subscribers filter their own module: No tracing subscriber creates a feedback loop that would cause unbounded logging. This keeps observability overhead bounded and prevents recursive tracing crashes.

How Invariants Are Enforced in Code

Centralized Configuration Loading

The system guarantees a single configuration load at startup through Config::load() in crates/ai-memory-core/src/config.rs. This function is called exactly once in the CLI entry point:

let cfg = ai_memory_core::config::Config::load()?;   // called at program start only

Single-Writer Channel Pattern

All write operations serialize through the writer actor defined in crates/ai-memory-store/src/lib.rs. Mutations become messages rather than direct database calls:

let write = ai_memory_store::WriteCmd::WritePage { 
    path: "notes/todo.md".into(), 
    body: "…".into() 
};
ai_memory_store::writer::WRITER.send(write)?;   // all mutations go through this channel

Sanitization Boundary

The privacy invariant enforces that only sanitize() can produce sanitized observations. This type-level guarantee prevents accidental storage of untrusted data:

let raw = HookPayload { … };
let sanitized = ai_memory_hooks::sanitizer::sanitize(raw)?;   // only way to get a Sanitized<…>

Atomic Wiki Operations

The markdown wiki—the system's source of truth—uses atomic file operations implemented in crates/ai-memory-wiki/src/wiki.rs:

let mut wiki = ai_memory_wiki::Wiki::open(&cfg.data_dir)?;
wiki.write_page("ideas/feature.md", "# New Feature\nDetails…")?;

Process-Aware Lifecycle Commands

Destructive CLI commands check for running server processes before executing. The reset command in crates/ai-memory-cli/src/commands/reset.rs implements this safety check:

ai_memory_cli::commands::reset::run()?;   // internally looks at sysinfo before touching files

Identity-Based Queries

The three-tuple identity invariant enables precise data isolation. Queries in crates/ai-memory-store/src/queries.rs require all three identifiers:

let rows = ai_memory_store::queries::pages_by_path(
    &reader_pool,
    workspace_id,
    project_id,
    "docs/architecture.md"
)?;

Key Files Implementing Cross-Cutting Invariants

File Role
docs/ARCHITECTURE.md The single-source design document that lists and explains all fifteen invariants.
crates/ai-memory-core/src/config.rs Implements the immutable configuration struct and single-load policy.
crates/ai-memory-store/src/lib.rs Contains the writer actor, WriteCmd enum, and read-only connection pool.
crates/ai-memory-wiki/src/wiki.rs Provides atomic markdown writes and the Git-backed file watcher.
crates/ai-memory-hooks/src/sanitizer.rs Central sanitization boundary for all hook payloads.
crates/ai-memory-cli/src/main.rs CLI entry point that initializes config and spawns the writer actor.

Summary

  • Cross-cutting invariants in akitaonrails/ai-memory are architectural rules enforced across all crates to prevent data corruption and race conditions.
  • Configuration safety is guaranteed by loading Config exactly once and resolving all credentials before provider construction.
  • Storage integrity relies on a single-writer SQLite actor, atomic file operations, and transaction-bound index updates.
  • Security boundaries use type-system enforcement via Sanitized<NewObservation> and mandatory JSON schema validation for LLM outputs.
  • Operational safety includes live-process checks before destructive commands and fire-and-forget hook timeouts to prevent CLI blocking.

Frequently Asked Questions

What is a cross-cutting invariant in software architecture?

A cross-cutting invariant is a constraint that applies universally across multiple modules or layers of an application, rather than being localized to specific business logic. In the ai-memory codebase, these invariants ensure that critical behaviors—such as configuration loading and database access—remain consistent regardless of which crate or function is executing.

Why does ai-memory use a single-writer pattern for SQLite?

The single-writer pattern funnels all mutations through one dedicated OS thread via an mpsc channel. This design eliminates race conditions without requiring complex locking schemes, while preserving SQLite's native ACID guarantees. As implemented in crates/ai-memory-store/src/lib.rs, this pattern allows concurrent reads while serializing writes safely.

How does the system prevent configuration drift and hidden state bugs?

The one config-read path invariant mandates that Config::load() executes exactly once at startup, and the no global singletons rule prohibits lazy_static or hidden globals. Combined with provider auth resolving before construction, these constraints ensure that all configuration state is explicitly injected and immutable after initialization, preventing scattered environment variable access.

Why is the zero-LLM default important for deployment?

The zero-LLM default invariant ensures the server operates fully without any LLM provider configured. This enables headless deployment scenarios and simplifies operations for users who only need the memory store's document and search capabilities without artificial intelligence features, reducing both attack surface and operational complexity.

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 →