# Cross‑Cutting Invariants in ai‑memory's Architecture: A Complete Technical Guide

> Explore ai-memory's cross-cutting invariants for robust data integrity and predictable behavior. Understand configuration loading, DB access, file ops, and LLM integrations in this technical guide.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: architecture
- Published: 2026-09-05

---

**The ai-memory system enforces 15 documented cross-cutting invariants that govern configuration loading, database access, file operations, and LLM integrations to ensure data integrity and predictable behavior across all crates.**

This guide examines the architectural invariants defined in [`docs/ARCHITECTURE.md`](https://github.com/akitaonrails/ai-memory/blob/main/docs/ARCHITECTURE.md) and shows how they are implemented in the Rust codebase. These rules were introduced after concrete bugs in prior prototypes and are now enforced at the CI level—any new feature that violates them is rejected.

## What Are Cross‑Cutting Invariants?

Cross-cutting invariants are system-wide rules that apply across multiple modules, crates, and execution paths. Unlike local constraints, they cannot be enforced by a single type or function. Instead, they require architectural discipline: specific patterns in code review, explicit APIs, and sometimes runtime checks.

In ai-memory, these invariants prevent hidden state, race conditions, and security vulnerabilities that would otherwise emerge from the interaction of independent components.

## The 15 Core Invariants in ai‑memory

The following table summarizes each invariant, its purpose, and its enforcement location in [`docs/ARCHITECTURE.md`](https://github.com/akitaonrails/ai-memory/blob/main/docs/ARCHITECTURE.md):

| # | Invariant | Purpose |

|---|-----------|---------|
| 1 | **One config-read path** | Single source of truth for configuration |
| 2 | **Single-writer SQLite actor** | ACID preservation and race condition avoidance |
| 3 | **Indexes commit in the same transaction as data** | Consistent search indexes after crashes |
| 4 | **Typed three-tuple identity** | Multi-workspace, multi-project isolation |
| 5 | **Hooks are fire-and-forget** | Non-blocking CLI commands |
| 6 | **Privacy strip is a typed boundary** | Mandatory sanitization of untrusted input |
| 7 | **JSON-schema-only structured outputs** | Parser stability and injection prevention |
| 8 | **Embedding metadata denormalized** | Automatic invalidation of stale vectors |
| 9 | **Live-process check before direct-disk lifecycle ops** | Prevention of destructive actions on active data |
| 10 | **Atomic file writes** | Guaranteed durability of markdown wiki |
| 11 | **Absolute canonical data directory** | Elimination of accidental data duplication |
| 12 | **No global singletons** | Testability and prevention of hidden state leakage |
| 13 | **Zero-LLM default** | Headless operation without provider configuration |
| 14 | **Provider auth resolves before construction** | Centralized secret handling and safe rotation |
| 15 | **Tracing subscribers filter their own module** | Bounded observability overhead |

## Configuration and Initialization Invariants

### One Config-Read Path

The entire process must call `Config::load()` exactly once at startup. No module reads environment variables directly.

This invariant guarantees a single source of truth and prevents hidden-state bugs where different components interpret the same environment variable differently. The configuration is loaded in [`crates/ai-memory-cli/src/main.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/main.rs) and passed explicitly to all downstream components.

```rust
// Called once at program start; never again
let cfg = ai_memory_core::config::Config::load()?;

```

The `Config` struct in [`crates/ai-memory-core/src/config.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/config.rs) implements this policy. Its fields are immutable after construction, and the type does not implement `Default` to prevent accidental re-creation.

### Absolute Canonical Data Directory

The data directory is resolved once at startup using `std::fs::canonicalize` and logged at the `INFO` level. All components receive the same `PathBuf`, eliminating path confusion between relative and absolute references.

```rust
// Resolution happens in Config::load()
let data_dir = std::fs::canonicalize(&raw_config.data_dir)?;
eprintln!("[ai-memory] Using data directory: {}", data_dir.display());

```

### No Global Singletons

The codebase avoids `lazy_static`, `thread_local!`, and other hidden globals. All dependencies are injected explicitly through constructor functions. This makes the system testable and prevents state leakage across threads.

## Database and Storage Invariants

### Single-Writer SQLite Actor

All database mutations are sent through a single `mpsc` channel to one dedicated OS thread. This serializes writes and preserves SQLite's ACID guarantees without requiring `WAL` mode or external locking.

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

```

The `WRITER` handle is the only public interface for mutations. Direct `rusqlite::Connection` access is restricted to the internal writer module in [`crates/ai-memory-store/src/lib.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/lib.rs).

### Indexes Commit in the Same Transaction as Data

Schema changes and their associated FTS5 or entity indexes are written atomically with payload rows. This prevents stale indexes after a crash—if the transaction commits, both data and indexes are consistent; if it rolls back, neither is present.

### Typed Three-Tuple Identity

Every domain row stores `(workspace_id, project_id, path)` from initial creation. This enables proper multi-workspace, multi-project isolation and safe cross-project linking through foreign-key relationships.

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

```

Query functions in [`crates/ai-memory-store/src/queries.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/queries.rs) require all three components, making accidental cross-workspace data leakage a type error.

## Security and Privacy Invariants

### Privacy Strip Is a Typed Boundary

Only the `sanitize()` function can create a `Sanitized<NewObservation>` value. This forces all untrusted text through validation before it enters the store.

```rust
let raw = HookPayload { /* … */ };
let sanitized = ai_memory_hooks::sanitizer::sanitize(raw)?;  // Type-system enforced
// sanitized is Sanitized<NewObservation>; no other constructor exists

```

The `Sanitized<T>` type in [`crates/ai-memory-hooks/src/sanitizer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/sanitizer.rs) has a private constructor, making the sanitization boundary unbypassable.

### Provider Auth Resolves Before Construction

Each LLM provider receives a typed `ProviderAuth` object. Implementations never read environment variables directly. This centralizes secret handling and makes credential rotation atomic—change one configuration value, not scattered `std::env::var` calls.

## LLM and Integration Invariants

### Zero-LLM Default

The server operates fully without any LLM provider configured. This allows headless deployment for users who need only the memory store, not AI features. The absence of a provider is represented by an empty vector in the configuration, not an `Option` that might be mishandled.

### JSON-Schema-Only Structured Outputs

LLM providers must emit JSON matching a declared schema. No XML, no free-form text, no "helpful" natural language wrapping. This keeps downstream parsers stable and prevents prompt injection attacks that rely on output format confusion.

### Embedding Metadata Denormalized

Each row in `page_embeddings` stores `provider`, `model`, and `dimension` alongside the vector itself. When the embedding configuration changes, stale vectors are ignored automatically rather than returning incorrect similarity scores.

## File System and Lifecycle Invariants

### Atomic File Writes

Writes to the markdown wiki use a temporary file, `fs::rename` for atomic replacement, and `fsync` for durability. The file watcher ignores its own writes by recognizing a special filename prefix.

```rust
// In ai_memory_wiki::Wiki::write_page
let temp_path = format!(".{}.tmp", uuid::Uuid::new_v4());
std::fs::write(&temp_path, content)?;
std::fs::rename(&temp_path, final_path)?;
let file = std::fs::File::open(final_path)?;
file.sync_all()?;

```

This pattern in [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs) guarantees that the wiki—the single source of truth—is never in a partially-written state.

### Live-Process Check Before Direct-Disk Lifecycle Ops

Commands such as `reset`, `restore`, `reindex`, and `uninstall --purge-data` first query running processes via `sysinfo` to detect an active ai-memory server. Destructive actions are blocked if the data directory is in use.

```rust
// ai_memory_cli::commands::reset::run
if ai_memory_cli::lifecycle::is_process_running(&cfg.data_dir)? {
    return Err(anyhow!("Server is running; stop it first with `ai-memory stop`"));
}

```

## Observability Invariants

### Tracing Subscribers Filter Their Own Module

No tracing subscriber creates a feedback loop that would cause unbounded logging. Each subscriber explicitly excludes its own crate from the events it processes, preventing recursive tracing crashes when internal operations generate log output.

### Hooks Are Fire-and-Forget

Hook scripts have a hard timeout of 200ms. The server responds with HTTP 202 Accepted (or 429 when saturated), guaranteeing that user-visible CLI commands never block on network latency. This is enforced by `tokio::time::timeout` in the hook dispatch code.

## Implementing New Features Within the Invariants

When extending ai-memory, verify your change against this checklist:

- Does it read environment variables directly? Route through `Config::load()`.
- Does it write to SQLite? Use `ai_memory_store::writer::WRITER.send()`.
- Does it handle untrusted input? Produce a `Sanitized<T>` via the sanitization boundary.
- Does it write files? Use the atomic pattern with temp file, rename, fsync.
- Does it spawn tasks? Ensure no `lazy_static` or hidden state; inject dependencies explicitly.

Violations are caught in code review and rejected by CI. The invariants are not suggestions—they are enforced architecture.

## Summary

- **Configuration invariants** ensure one load, canonical paths, and explicit dependency injection.
- **Storage invariants** serialize writes, preserve ACID properties, and enforce multi-tenancy through typed identities.
- **Security invariants** create unbypassable boundaries for sanitization and secret handling.
- **LLM invariants** guarantee headless operation, stable parsing, and automatic invalidation of stale embeddings.
- **File system invariants** provide atomic durability and prevent destructive operations on live data.
- **Observability invariants** bound overhead and eliminate feedback loops.

These 15 cross-cutting invariants are documented in [`docs/ARCHITECTURE.md`](https://github.com/akitaonrails/ai-memory/blob/main/docs/ARCHITECTURE.md) and implemented across [`crates/ai-memory-core/src/config.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/config.rs), [`crates/ai-memory-store/src/lib.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/lib.rs), [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs), and related modules.

## Frequently Asked Questions

### What happens if a new feature violates a cross-cutting invariant?

The CI pipeline rejects the change. Each invariant has a corresponding architectural test or lint rule. Additionally, code reviewers check all pull requests against the invariant list in [`docs/ARCHITECTURE.md`](https://github.com/akitaonrails/ai-memory/blob/main/docs/ARCHITECTURE.md). Historical bugs from prior prototypes inform why each rule exists, making violations easy to identify.

### Why use a single-writer SQLite actor instead of WAL mode or connection pooling?

The `mpsc` channel provides serialization without requiring SQLite's `WAL` mode, which has different consistency guarantees on some filesystems. The actor pattern also enables backpressure—when the channel is full, senders receive an error immediately rather than blocking indefinitely. This aligns with the fire-and-forget semantics of other system components.

### How does the zero-LLM default affect the embedding system?

The embedding pipeline is entirely optional. Without configured providers, the server skips vector generation and operates as a pure document store with full-text search. The `page_embeddings` table remains empty, and similarity-based queries return empty results gracefully. No code paths panic or unwrap on missing providers.