Opinion

Five-Layer Immutable Memory for an AI Agent: Why One Vector Database Is Never Enough

Trying to store an AI agent's entire long-term memory in a single vector index fails, because exact word search, semantic similarity, multi-hop relationship traversal, and decision-change history all require physically different data structures. An incremental SQLite FTS5 index with BM25 ranking over 66,995 sessions and 538,956 messages (1.35 GB) eliminated dev environment crashes and sped up session startup from 15 seconds to tenths of a second. To protect memory from degradation, we implemented deterministic key sanitization via UUID5, bitemporal writes without deleting old facts, and a nightly consolidation cycle with Weibull-distribution decay.

Five-Layer Immutable Memory for an AI Agent: Why One Vector Database Is Never Enough
Hamidun News.
Listen to article
Article Fact Sheet & Key Figures
ParameterDetails
Editor-in-Chief
Episodic index volume66,995 sessions and 538,956 messages in a 1,353 MB SQLite FTS5 database
Second-brain and embeddings database10,019 structured knowledge cards and a 2.37 GB `brain.db`
Startup speed after dropping heavy hooksinitialization latency drops from 5–15 s to < 0.2 s
Nightly consolidation thresholdsduplicate merging at cosine similarity ≥ 0.70 and a contradiction detector with a 0.40 threshold, no LLM call required
  • Episodic index volume: 66,995 sessions and 538,956 messages in a 1,353 MB SQLite FTS5 database
  • Second-brain and embeddings database: 10,019 structured knowledge cards and a 2.37 GB `brain.db`
  • Startup speed after dropping heavy hooks: initialization latency drops from 5–15 s to < 0.2 s
  • Nightly consolidation thresholds: duplicate merging at cosine similarity ≥ 0.70 and a contradiction detector with a 0.40 threshold, no LLM call required

The starting point for my memory architecture was a hard crash in my code editor. When I tried to open the AI agent's conversation history, the Extension Host process blew past its memory limit and took the terminal down with it. Over six months of work, I had accumulated 2.45 GB of raw JSONL session logs, and the extension tried to read all their headers off disk at startup. On top of that, the default `cleanupPeriodDays=30` setting was silently deleting sessions older than a month — and by the time I swapped it for `99999`, part of my winter archive had already vanished.

An audit revealed that two of my four memory databases were effectively dead, since they required manually launching background daemons. The real discovery, though, was that "agent memory" is actually five distinct classes of questions, and each one needs its own engine.

Why

Did SQLite FTS5 Beat Qdrant and grep on Half a Million Messages?

For episodic memory ("where did we discuss the webhook setup three months ago?"), `grep` across gigabytes of text was too slow, and spinning up a local `Qdrant` instance with `fastembed` at session start added 5–15 seconds to launch time and ate hundreds of megabytes of RAM.

The winner was Python's built-in SQLite FTS5 engine with BM25 ranking and `WAL` mode. The `search_chats.py` indexer checks `file_mtime` against the `sessions` table: if the gap is under a second, the file is skipped.

Raw logs older than 14 days and dumps over 20 MB get moved to cold storage — the working directory shrank from 2.45 GB to 527 MB, the crashes stopped, and the index grew to 66,995 sessions and 538,956 messages (290,544 from the human, 248,412 from the assistant) at 1,353 MB. For critical reference material (like a 20-chapter manual built from 2,240 messages and 177 voice notes transcribed via Deepgram), a 4-tier backup scheme applies: a working-memory digest, a pointer in `MEMORY.

md` (strictly capped at 200 lines and ~22 KB), a routing rule, and a vector snapshot.

Why

Do You Need Deterministic UUID5 in a Vector Database, and How Does Bitemporality Work?

When the query doesn't contain the exact words (someone asks "how do I wash fruit without chemicals" while the database holds a note about an ionizer's alkaline `pH 11.5` mode), a semantic layer built on embeddings kicks in (384 dimensions locally, 1536 for books). In practice, document identifiers look like strings such as `knowledge:42` or `session/2026-04-10/msg-17` with Cyrillic characters, but the Qdrant vector engine requires `point_id` to be either an integer or a strict UUID. A random `uuid4()` produces duplicates on re-indexing, so we introduced a deterministic `uuid5` over the DNS namespace:

```python import uuid

def to_deterministic_point_id(raw_doc_id: str) -> str: # Turns any string with Cyrillic and special characters into a stable ASCII UUID5 return str(uuid.uuid5(uuid.NAMESPACE_DNS, raw_doc_id))

point_id = to_deterministic_point_id(doc_id) payload = {**metadata, "doc_id": doc_id, "content": text_chunk} ```

The same string always produces an identical UUID, turning re-writes into idempotent `upsert` operations, while the original key is preserved in the `payload`.

So the agent can remember the history of how decisions changed ("why did we switch engines in the spring?"), we borrowed an idea from Datomic and banned `UPDATE` operations that overwrite the past. Every relationship has `valid_from`, `valid_to`, and `ingested_at` fields, and the second-brain database also tracks usage counters `use_count` and `last_used`. When a decision becomes outdated, the old edge gets a `valid_to` timestamp, and a new record is inserted alongside it with a `supersedes` link.

How

Does Algorithmic Sleep with a Weibull Curve Clean Contradictions Out of Memory?

To keep the database from turning into a junk pile, a background consolidation pipeline (`dream` and the `sleeptime` modules) periodically runs a three-stage cleanup:

1. Duplicate clustering (SHMR): a cosine-similarity matrix on 1024-dimensional embeddings (`Qwen3-Embedding-0.6B` in LanceDB) collapses records with similarity `>= 0.70`. 2. Selective decay via a Weibull distribution: fleeting observations (`observation`) decay over 336 hours (~2 weeks), working facts decay over 2,160 hours, while personal preferences (`preference`), procedures, and error patterns (`error_pattern`) are protected by an `EXEMPT_TYPES` list. 3. Contradiction search without an LLM: a deterministic scanner looks for conflicts over a 7-day window using weighted signals (`negation: 0.35`, `antonym: 0.25`, `numeric: 0.25`, `temporal: 0.15`) at a `0.40` threshold.

This five-layer memory scheme (SQLite FTS5, LanceDB, and a temporal graph) is built into the core of [Nomad AI](https://nomadnet.ai) and the cloud platform [agent.nomadnet.ai](https://agent.nomadnet.ai). A course on memory engineering is available at [Hamidun Academy](https://academy.hamidun.com) (72-hour trial), and the indexing scripts live in the [Vibe Club](https://hamidun.com/club) library.

Story

Key Questions Answered

Why Did SQLite FTS5 Beat Qdrant and grep on Half a Million Messages?

For episodic memory ("where did we discuss the webhook setup three months ago?"), `grep` across gigabytes of text was too slow, and spinning up a local `Qdrant` instance with `fastembed` at session start added 5–15 seconds to launch time and ate hundreds of megabytes of RAM. The winner was Python's built-in SQLite FTS5 engine with BM25 ranking and `WAL` mode. The `search_chats.py` indexer checks `file_mtime` against the `sessions` table: if the gap is under a second, the file is skipped. Raw logs older than 14 days and dumps over 20 MB get moved to cold storage — the workin

Why Do You Need Deterministic UUID5 in a Vector Database, and How Does Bitemporality Work?

When the query doesn't contain the exact words (someone asks "how do I wash fruit without chemicals" while the database holds a note about an ionizer's alkaline `pH 11.5` mode), a semantic layer built on embeddings kicks in (384 dimensions locally, 1536 for books). In practice, document identifiers look like strings such as `knowledge:42` or `session/2026-04-10/msg-17` with Cyrillic characters, but the Qdrant vector engine requires `point_id` to be either an integer or a strict UUID. A random `uuid4()` produces duplicates on re-indexing, so we introduced a deterministic `uu

How Does Algorithmic Sleep with a Weibull Curve Clean Contradictions Out of Memory?

To keep the database from turning into a junk pile, a background consolidation pipeline (`dream` and the `sleeptime` modules) periodically runs a three-stage cleanup: 1. Duplicate clustering (SHMR): a cosine-similarity matrix on 1024-dimensional embeddings (`Qwen3-Embedding-0.6B` in LanceDB) collapses records with similarity `>= 0.70`. 2. Selective decay via a Weibull distribution: fleeting observations (`observation`) decay over 336 hours (~2 weeks), working facts decay over 2,160 hours, while personal preferences (`preference`), procedures, and error patterns (`error_patt

ZK
Zhemal Khamidun — CPO at Alpina Digital, product manager of AlpinaGPT. Building AI products since 2019. Hamidun News is a pet project that grew into a production aggregator with 6,000+ readers.
✦ Hands-On AI Engineering · Hamidun Academy

Build & Ship Production AI Agents Without Boilerplate Code

Start your free 72-hour trial at Hamidun Academy (academy.hamidun.com) — interactive lessons on Claude Code & Cursor, live Tech Radar, and production agent templates.

Start 72h Free Trial →Vibe-Coding Intensive
What do you think?
Loading comments…