ai-memory v0.7.0 ships three interfaces (MCP stdio, HTTP REST, native CLI) over one SQLite-backed core with optional Postgres+AGE adapter. This page is the canonical architecture snapshot at release/v0.7.0 @ 1cb95bbe7 — post-#1174 pm-v3.1 refactor train, post-#1182 vendor-neutrality sweep, post-#1192/#1196 cross-surface RuntimeContext.
Advertised entries on --profile full (73 callable tools + the always-on memory_capabilities bootstrap). Default --profile core ships 7 tools. Plus 2 prompts. Canonical assertion: Profile::full().expected_tool_count() in src/profile.rs.
Production .route(...) registrations in src/lib.rs (75 unique URL paths × multiple HTTP methods per path) plus the bare /metrics Prometheus surface. Authoritative source: codegraph codegraph_search kind=route limit=100.
Top-level Command enum variants in src/daemon_runtime.rs (default build ships 80; --features sal/sal-postgres unlocks 82 by adding migrate + schema-init, both #[cfg(feature = "sal")] — not postgres-only).
Each count is asserted by a regression test. The architectural North Star (per CLAUDE.md prime directive): long-term codebase manageability. Growth narrative: 70 MCP / 73 HTTP / 40 CLI at v0.6.4 → 74 / 89 / 80 at v0.7.0 (FX-12/ARCH-3 + FX-C3 batch2 added 16 CLI subcommands closing MCP/CLI parity; v0.7.0 attested-cortex feature surface + #1146 sectioned-config + #1156 per-namespace K8 quota + #1255 federation-nonce persistence (federation_nonce_cache) drove the HTTP + schema bumps).
The pm-v3.1 refactor mandate (canonical memory f5334545-c1f5-4f5c-9efb-a0ec3a0c1fcd) landed across 10 PRs (#1184-#1200), supplemented by the #1192+#1196 follow-up (#1204) and the #1183 split. The combined output:
| PR | Scope | Outcome |
|---|---|---|
| #1187 | PR1 — extract 74 MCP tool names | New crate::mcp::registry::tool_names::* const module. Every tool-name literal at production-code sites reads from here instead of scattered "memory_*" string literals. |
| #1188 | PR2 — HTTP MIME constants | HEADER_CONTENT_TYPE / MIME_JSON extracted; eliminates the "application/json" literal-by-grep refactor risk. |
| #1185 | PR3 — SECS_PER_* constants | SECS_PER_HOUR (3_600), SECS_PER_DAY (86_400), SECS_PER_WEEK (604_800). Lint-gated against Duration::from_secs(3600 | 86400 | 604800 | ...). |
| #1184 | PR4 — Ollama backend literal sweep | Every production-code "ollama" literal reads crate::llm::BACKEND_OLLAMA. Substrate is vendor-neutral by code, not just by docs. |
| #1190 | PR5 — DEFAULT_NAMESPACE extraction | Disambiguated from quotas::GLOBAL_NAMESPACE. The user-facing default + the quota sentinel are now two semantically distinct named constants. |
| #1186 | PR6 — raw tier strings → Tier::<X>.as_str() | Every "short" / "mid" / "long" literal in production code routes through the typed enum. |
| #1191 | PR7 — collapse permissions-mode dual-source | Eliminated the ACTIVE / OVERRIDE_PERMISSIONS_MODE static drift surface. Single RwLock source of truth. |
| #1195 | PR8 — Class A SHOULD statics → AppState | Wave-2 boundary: scattered OnceLock<T> statics promoted to AppState / metrics registry fields. Tests can now construct an isolated AppState without bleeding global state. |
| #1189 | PR9 — vendor-specific test-fixture deflake | Test fixtures stop hardcoding "claude" as a stand-in source-of-truth; routed through DEFAULT_NHI_SOURCE. |
| #1200 | PR10 — lint-gate CI enforcement | scripts/check-vendor-literals.sh in CI. HARD-BLOCK on vendor identifiers + SECS_PER_* magic numbers outside the 7-file substrate carve-out. Self-test verifies the gate is load-bearing. |
| #1204 | #1192+#1196 follow-up — RuntimeContext | Cross-surface RuntimeContext threading: HTTP / MCP / CLI / federation paths thread one struct carrying the resolved agent + config + caller-context instead of N positional args. |
| #1199 | #1183 — WrapStrategy split | Per-CLI-binary WrapStrategy table moved from src/cli/wrap.rs to sibling module src/llm_cli_wrap.rs. Vendor-specific knowledge stays in the substrate carve-out. |
Why the lint-gate is load-bearing. A scripts/check-vendor-literals.sh --self-test step in CI injects a contrived "anthropic" literal at a production site, runs the gate, verifies the gate exits non-zero with the expected violation message, then cleans up. A regression in the gate's detection logic (e.g. an over-broad allowlist, a broken test-boundary heuristic) trips immediately rather than silently letting drift back in. Per pm-v3.2 NO FAIL MISSION closure discipline (memory 2cb15d34-...): the gate itself must be load-bearing, not decorative.
The post-#1174 public-const surface gives every production-code path a single source of truth for substrate identifiers:
| Module path | What it carries |
|---|---|
crate::mcp::registry::tool_names::* | All 74 MCP tool names (e.g. RECALL, STORE, CAPABILITIES). Production code reads these instead of bare string literals. |
crate::llm::BACKEND_OLLAMAcrate::llm::BACKEND_OPENAIcrate::llm::BACKEND_XAI … | 15 vendor-backend identifiers (ollama, openai, xai, anthropic, gemini, deepseek, kimi, qwen, mistral, groq, together, cerebras, openrouter, fireworks, lmstudio) + the generic openai-compatible escape hatch. |
crate::config::KNOWN_EMBEDDING_DIMS | Canonical embedding-model → vector-dim lookup table (closes #1169 drift surface). canonical_embedding_dim(model) helper threads through ResolvedEmbeddings. |
crate::DEFAULT_NHI_SOURCE | Vendor-neutral default source identifier ("nhi") for the reflection / curator / synthesis surfaces. Replaces the pre-#1175 hardcoded "claude" (the hardcode was removed by commit 84ec159c1; current source-stamp site uses DEFAULT_NHI_SOURCE from src/validate.rs:85). |
crate::DEFAULT_NAMESPACEcrate::quotas::GLOBAL_NAMESPACE | User-facing default vs quota-sentinel namespaces (disambiguated per PR5 #1190). |
crate::SECS_PER_HOURcrate::SECS_PER_DAYcrate::SECS_PER_WEEK | Time-unit constants. Lint-gated against magic-number Duration::from_secs(3600 | 86400 | 604800). |
crate::HEADER_CONTENT_TYPEcrate::MIME_JSON | HTTP header + MIME constants. Eliminates "application/json" string-literal drift surface. |
crate::Tier::Short.as_str()crate::Tier::Mid.as_str()crate::Tier::Long.as_str() | Typed-enum tier identifiers. Pre-#1186 raw "short" / "mid" / "long" literals are gone from production code. |
Per #1192/#1196 follow-up landed in PR #1204: the HTTP / MCP / CLI / federation entry surfaces now thread a single RuntimeContext carrying the resolved agent, config slice, caller-context, and admin-allowlist resolution rather than N positional args. The pattern eliminates the pre-#1174 drift surface where (a) adding a new resolution input meant patching N callers per surface and (b) wire-side handlers and MCP-side handlers could diverge on which inputs they consumed.
Why this matters at the substrate level:
RuntimeContext shape. Vendor-specific call shapes do not leak into the resolution path.OnceLock<T> statics with backdoor setters.Pre-#1067 the LLM client (then OllamaClient) was Ollama-only and gated by feature tier. Post-#1067 (v0.7.0): tier no longer dictates LLM vendor. Every tier (keyword / semantic / smart / autonomous) can speak to any provider via AI_MEMORY_LLM_BACKEND:
/api/chat + /api/embed, no auth), OpenAI-compatible (/v1/chat/completions + /v1/embeddings, Bearer auth).ollama, openai, xai, anthropic, gemini, deepseek, kimi, qwen, mistral, groq, together, cerebras, openrouter, fireworks, lmstudio, plus the generic openai-compatible escape hatch.AI_MEMORY_LLM_API_KEY takes precedence; vendor-specific fallbacks (XAI_API_KEY, OPENAI_API_KEY, …) honored in src/llm.rs::alias_api_key_env_vars.crate::llm::BACKEND_* constants, not bare string literals. CI enforces.The substrate-side struct name OllamaClient is preserved post-#1066 for call-site backward compat; rename to LlmClient is non-breaking and tracked separately.
Both adapters carry CURRENT_SCHEMA_VERSION = 57. SQLite ladder ends at the v57 version stamp in src/storage/migrations.rs (the v56 composite-index arm is version-pinned); postgres ladder ends at migrate_v57() in src/store/postgres.rs. The two adapters share a single logical schema number even though the on-disk file-name counters differ because the sqlite split numbers per-bump while the postgres ladder is a single greenfield+upgrade pair.
federation_push_dlq table.archived_memories so archive→restore is lossless for the full v0.7.0 Memory shape on both backends.agent_quotas PRIMARY KEY extended from (agent_id) to (agent_id, namespace). Per-namespace K8 quota allotments hold even when a single agent operates across many namespaces. Pre-v50 rows backfill to the _global sentinel namespace.federation_nonce_cache table so peer-replay-prevention nonces persist across daemon restarts.transcript_line_dedup idempotency table ((host_pubkey_b64, line_sha256) composite key) so a SIGKILL between turns never double-ingests the same transcript line on rehydration.memories_au FTS5 sync trigger to AFTER UPDATE OF title, content, tags so non-FTS column updates no longer pay spurious FTS5 row ops. Pure DDL, idempotent.created_at + tier.default_ttl_secs()) onto legacy NULL-expiry mid/short rows to close the TTL-leak immortal-rows class. In-code backfill arm, no new .sql file.list_memories_updated_since federation-catchup query sargable and added idx_memories_updated_at on SQLite. Postgres adds no new index (its bootstrap memories_updated_at_idx (updated_at DESC) already serves the range via Index Scan Backward), so migrate_v55() is a postgres version-stamp no-op.idx_memories_list_order, idx_memories_ns_list_order, idx_archived_ns_archived_at) paired with the sargable storage::list rewrite (P1-measured 141 ms → 0.06 ms at 100k rows). Postgres twin is a version-stamp no-op.tsv tsvector column + memories_tsv_gin GIN index; recall/search/contradiction shapes match AND rank on the column (kills the per-matched-row tsvector recompute, ~305 of 306 ms at 8k rows), legacy expression index dropped. SQLite twin is a version-stamp no-op (FTS5 already materialises the text). Note: the ADD COLUMN takes an ACCESS EXCLUSIVE table rewrite — sub-second at ~8k rows.Migrations run on first open of the new binary. The migration set is dry-run-tested against the operator's own DB by scripts/dogfood-rebuild.sh before a release ships.