Skip to content

Storage Architecture

Default Locations

User-level memory:

~/.nuzo/memory/memories.sqlite

Project-level memory:

<project>/.nuzo/memory/memories.sqlite

Temporary exports:

~/.nuzo/memory/exports/

Operational logs:

~/.nuzo/memory/logs/

Scope Resolution

Memory lookup should search scopes in this order:

  1. Active project scope.
  2. User global scope.
  3. Optional organization/team scope.

The response must include which scope produced each result.

SQLite Tables

The current schema version is 7. Nuzo stores it in SQLite user_version and rejects databases created by newer unsupported Nuzo versions with the structured MEMORY_SCHEMA_UNSUPPORTED error.

Initial schema:

CREATE TABLE memories (
  id TEXT PRIMARY KEY,
  revision INTEGER NOT NULL DEFAULT 1,
  scope TEXT NOT NULL,
  kind TEXT NOT NULL,
  content TEXT NOT NULL,
  capture_key TEXT,
  tags TEXT NOT NULL DEFAULT '[]',
  source TEXT NOT NULL,
  confidence REAL NOT NULL DEFAULT 1.0,
  confidence_state TEXT,
  provenance TEXT,
  review_after TEXT,
  expires_at TEXT,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  last_used_at TEXT,
  archived_at TEXT
);

CREATE TABLE memory_events (
  id TEXT PRIMARY KEY,
  memory_id TEXT,
  event_type TEXT NOT NULL,
  actor TEXT NOT NULL,
  payload TEXT NOT NULL,
  created_at TEXT NOT NULL
);

CREATE TABLE memory_relations (
  id TEXT PRIMARY KEY,
  source_memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
  target_memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
  relation TEXT NOT NULL,
  reason TEXT,
  created_at TEXT NOT NULL,
  UNIQUE(source_memory_id, target_memory_id, relation)
);

CREATE VIRTUAL TABLE memories_fts USING fts5(
  id UNINDEXED,
  scope UNINDEXED,
  content,
  tags
);

memories, memory_events, and memory_relations are canonical. memories_fts is a derived search index containing one row for each active memory. Integrity diagnostics compare its ID, scope, content, and flattened tag text with canonical rows and separately report missing, orphaned or archived, duplicate, and mismatched entries.

The local memory integrity repair-fts workflow is deliberately outside the MCP surface. Preview is read-only at the logical-store level. Apply requires explicit confirmation and refuses unsupported versions, altered canonical or FTS schemas, invalid canonical tag JSON, foreign-key violations, symlinked source or backup files, overlapping SQLite filesets, and existing backup destinations. Stable intermediate directory aliases are permitted for common platform paths, while fileset comparisons resolve their real parent paths.

Repair holds BEGIN IMMEDIATE on the original source identity, takes a WAL-consistent snapshot through a read-only sibling connection, and converts the isolated snapshot to a self-contained DELETE-journal backup. Only the backup's derived FTS rows are normalized; its canonical rows remain the exact pre-repair snapshot. The owner-only backup is validated and published without replacing an existing path before the source FTS table is rebuilt by INSERT ... SELECT. Source validation occurs inside the same transaction, so a failed rebuild rolls back without changing canonical data. The validated backup remains available when a later source step fails.

Schema version 7 backfills capture_key with the exact-capture normalization of each memory's content and maintains it on canonical creates and updates. A partial index covers active-memory scope counts through its scope prefix and deterministic same-scope duplicate lookup through the complete key:

CREATE INDEX idx_memories_active_capture_key
  ON memories(scope, capture_key, id) WHERE archived_at IS NULL;

Capture relationship lookup scans all active records only while the scope has at most 100 records. Larger scopes use SQLite FTS to return at most 20 prefiltered records and explicitly report a non-exhaustive search. Exact duplicates never depend on the FTS candidate cap. This bounds application-row allocation while preserving the fail-closed rule that non-exhaustive evidence cannot establish independence.

Optional Semantic Sidecar

Optional semantic vectors are not part of the canonical schema above. They live in memories.semantic.sqlite, beside the configured canonical store. The sidecar contains provider fingerprint, build metadata, memory ID, canonical revision, scope, and derived vector data. It contains no audit log and does not own memory lifecycle.

The sidecar is derived, disposable, and excluded from export. Deleting it is a safe way to disable or reset semantic retrieval. Rebuilding reads active canonical memory into a temporary sidecar and replaces the previous completed index only after validation. Canonical writes do not invoke an embedding provider and therefore cannot be rolled back by semantic failure.

See Optional Semantic Retrieval for provider, staleness, fallback, and scope contracts.

Audit Log

Every write operation creates an event:

  • memory.created
  • memory.updated
  • memory.archived
  • memory.deleted
  • memory.imported
  • memory.exported
  • memory.recalled
  • memory.scope.rehomed
  • memory.challenged
  • memory.relation.created
  • memory.relation.deleted

Recall events are opt-in because queries may contain sensitive task context and can grow quickly. Normal CLI and MCP recall do not record query text or update last_used_at by default. A caller must explicitly request usage recording through the core API. When enabled, new memory.recalled events retain a SHA-256 query hash, hash-algorithm marker, score, and scope instead of the full query. Existing events are not rewritten during migration and may retain the legacy query field.

Transaction Guarantees

SQLite-backed logical mutations commit memory rows, FTS changes, and audit events atomically.

  • remember, update, challenge, relate, unrelate, forget, and usage-recording recall use one transaction per command;
  • import uses one transaction for the complete planned document and rolls back every item if any persistence step fails;
  • bulk forget uses one transaction per matched memory, so an unexpected failure may leave earlier memories committed while the failing memory is rolled back;
  • dry runs do not open write transactions.
  • FTS repair changes only the derived memories_fts table, after publishing a validated recovery backup; canonical rows and audit history are unchanged.

Policy validation happens before write transactions. Import duplicate planning happens inside the write transaction so equivalent imports from multiple local processes serialize deterministically.

SQLite uses WAL mode and a five-second busy timeout so short concurrent writes from multiple local agent processes wait instead of failing immediately.

Memory rows include a monotonically increasing revision. Stateful writes use compare-and-swap semantics and return MEMORY_REVISION_CONFLICT when another process commits a newer row before the operation can commit.

Local Permissions

Nuzo-created SQLite databases, WAL/SHM sidecars, config files, and exports use owner-only 0600 permissions. Nuzo-owned memory, export, and log directories use 0700 when created.

Project config accepts only the portable .nuzo/memory/memories.sqlite storage path. Absolute paths, traversal, and symlinked .nuzo paths are rejected so repository-controlled config cannot redirect writes outside the project.

Secrets And Sensitive Data

Nuzo rejects obvious secret-like values:

  • API keys;
  • private keys;
  • passwords;
  • auth tokens;
  • cookie/session blobs.

The CLI includes nuzo memory doctor, a diagnostic command that reports whether any memory database or export file is tracked by Git.

Scope Boundary

Scopes organize recall and lifecycle operations, but selectors alone are not authorization boundaries. Nuzo can run a restricted core or MCP session with an explicit scope allowlist; cross-scope reads, writes, exports, and destructive operations are then rejected.

An unrestricted local CLI or core session remains an administrator workflow over the store and can access every scope. Use restricted sessions for repository-controlled hosts, and use separate stores when process-level or machine-level isolation is required.