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
ProviderAuthobject 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_staticor 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
mpscchannel 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_embeddingsrow 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, thenfsync. 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 aSanitized<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, anduninstall --purge-datafirst query the running process viasysinfo. 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-memoryare architectural rules enforced across all crates to prevent data corruption and race conditions. - Configuration safety is guaranteed by loading
Configexactly 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →