Skip to main content

The Storage Stack

Three embedded pieces, each owning what it is best at. Markdown is the source of truth; the other two are derived and rebuildable.
LayerBacked byHoldsRebuildable?
Markdown + YAML frontmatterplain .md filesthe memory content itself — the only portable, human-editable asset✗ (it is the truth)
SQLite (aiosqlite).index/sqlite/*.dbsystem state, audit log, the cascade queue, the boundary buffer, OME engine state✓ from markdown
LanceDB (Arrow).index/lancedb/*.lancevector + BM25 + scalar columns for retrieval✓ from markdown
Delete the entire .index/ directory and no memory is lost — it rebuilds from the .md tree. There is no separate “export”; the markdown is the export. See Operating It to trigger a rebuild.

Storage Paths

The default memory root is ~/.everos/ (override with EVEROS_MEMORY__ROOT or [memory] root in TOML). Configuration (the .env file) is separate from data (the memory root). Memory is partitioned by <app_id>/<project_id> before the user-visible directories, so different (app, project) spaces never share a directory or cross in search. The reserved id "default" materialises as default_app / default_project on disk.
~/.everos/                                  ← memory root (EVEROS_MEMORY__ROOT)
├── default_app/                            ← <app_id>   ("default" → default_app)
│   └── default_project/                    ← <project_id> ("default" → default_project)
│       ├── users/
│       │   └── <user_id>/
│       │       ├── user.md                 single-file   (profile)
│       │       ├── episodes/
│       │       │   └── episode-<YYYY-MM-DD>.md      daily-log append
│       │       ├── .atomic_facts/                   daily-log (hidden)
│       │       │   └── atomic_fact-<YYYY-MM-DD>.md
│       │       └── .foresights/                     daily-log (hidden)
│       │           └── foresight-<YYYY-MM-DD>.md
│       ├── agents/
│       │   └── <agent_id>/
│       │       ├── .cases/                          daily-log (hidden)
│       │       │   └── agent_case-<YYYY-MM-DD>.md
│       │       └── skills/                          skill-named dir
│       │           └── skill_<name>/SKILL.md        (+ references/ scripts/)
│       └── knowledge/                      ← shared / global (reserved)

├── .index/                                 ← system-managed, rebuildable (gitignore)
│   ├── sqlite/
│   │   ├── system.db                       state / audit / cascade queue / buffer / LSN
│   │   ├── ome.db                          Offline Memory Engine state
│   │   ├── ome.aps.db                      APScheduler jobstore
│   │   └── ome.db.lock                     OME single-engine guard
│   └── lancedb/
│       └── <kind>.lance/                   one Arrow table per kind

├── ome.toml                                ← user-editable OME strategy overrides (hot-reloaded)
└── .tmp/                                   atomic-write staging
The index dir is .index/ (dot-prefixed), not _index/. The cascade queue and LSN/audit state live in SQLite (system.db, table md_change_state) — there is no .cascade.log or .manifest.json file. There is no everos reindex command — see Operating It.

How a Memory Is Born

A message does not become memory immediately — it accumulates, a boundary is detected, an LLM extracts a cell, writers persist markdown, and the index catches up asynchronously.
 POST /add  ───▶  unprocessed_buffer (SQLite)        ← messages accumulate per (session, app, project)

                      ├─ boundary detector trips  ──┐
 POST /flush ─────────┤  (or you force it)          │  one LLM call
                      │                             ▼
                      │                       extract MemCell  ───▶  memcell row (SQLite)
                      │                             │
                      │              ───────────────┴───────────────
                      │              ▼                              ▼
                      │   UserMemoryPipeline (sync)      AgentMemoryPipeline (fire-and-forget)
                      │   writes episode .md NOW          emits AgentPipelineStarted
                      ▼              │                              │
   (response returns once md is on disk)                           │
                                     ▼                              ▼
                          ──────────────────  Offline Memory Engine (OME)  ──────────────────
                          │  async strategies write derived .md:                             │
                          │  atomic_facts · foresight · user profile · agent cases · skills  │
                          └───────────────────────────┬─────────────────────────────────────┘

                                         cascade daemon watches the .md tree

                                      md_change_state queue (SQLite, durable)

                                           rebuild LanceDB rows  ───▶  searchable
  • /add appends messages to a per-(session_id, app_id, project_id) buffer and returns accumulated (or extracted if the boundary tripped on this call).
  • /flush forces the boundary now (one extraction LLM call), used at the end of a chat/agent run.
  • Episode markdown is written synchronously — when /flush returns extracted, the episode file is already on disk.
  • Everything else (atomic facts, foresight, profile, agent cases/skills) is produced asynchronously by the OME.
  • The cascade daemon turns every .md write into LanceDB rows so the content becomes searchable.

Memory Types & Storage Strategies

Six memory kinds today, each picking one of three on-disk patterns:
KindOwnerDir / fileStrategyProduced by
episodeuserepisodes/episode-<date>.mddaily-logextraction (sync)
atomic_factuser.atomic_facts/atomic_fact-<date>.md (hidden)daily-logOME
foresightuser.foresights/foresight-<date>.md (hidden)daily-logOME
profileuseruser.mdsingle-file rewriteOME
agent_caseagent.cases/agent_case-<date>.md (hidden)daily-logOME
agent_skillagentskills/skill_<name>/SKILL.mdskill-named dirOME (clustering)
StrategyShapeWhy
Daily-log append<prefix>-<YYYY-MM-DD>.md, one entry appended per memorycollapses thousands of per-entry files into one file per day
Single-file rewritea fixed filename overwritten in placefor a single evolving document (a user/agent profile)
Skill-named dirone directory per skilla skill is a richer unit (body + optional references/ scripts/)

The Cascade Daemon

The cascade subsystem keeps LanceDB in sync with the markdown tree. It runs in-process with the server (a coroutine started by the app lifespan), not as a separate OS daemon.
  1. A native filesystem watcher (watchdog: FSEvents on macOS, inotify on Linux) sees a .md create/modify.
  2. The change is enqueued in the md_change_state table (SQLite) — durable, so a crash mid-sync replays on restart.
  3. A worker drains the queue at entry-level granularity: it diffs the file, re-embeds only changed entries (keyed by content_sha256), and upserts the LanceDB rows.
Because markdown is the source of truth, editing a file directly is fully supported — open an episode in VSCode, Obsidian, or Vim, change an entry, save, and the daemon re-indexes just that entry.

The Offline Memory Engine (OME)

Most memory kinds are not extracted on the request path — they are derived later by the OME, an in-process async strategy engine. When extraction carves a MemCell, it emits an event; OME strategies pick it up and write their markdown when ready:
  • extract_atomic_facts — single-sentence facts from an episode
  • extract_foresight — anticipatory notes
  • extract_user_profile — the aggregated user.md
  • extract_agent_case — a reusable agent trajectory
  • extract_agent_skill — clusters related cases into a named skill
Strategies are configurable without a code change via ome.toml at the memory root (hot-reloaded within ~2s). Example — disable two strategies:
[strategies.extract_foresight]
enabled = false

[strategies.extract_user_profile]
enabled = false
After /flush returns extracted, the episode is queryable soon (once cascade indexes it), but atomic facts / profile / agent cases appear only after their OME strategy runs — typically seconds later. Poll with backoff if you need them immediately.

Consistency Model

PathGuaranteeDetail
Write (/add, /flush)strongthe episode .md is on disk before the call returns extracted; never blocks on LanceDB
Read (/search, /get)eventualreads LanceDB, which lags md by the cascade processing time — sub-second typically, up to ~10–15s under load
A /search immediately after the /flush that produced a record may miss it. The markdown is durable regardless; index lag never loses data. If you need read-your-write, retry with backoff, or force the queue with everos cascade sync.

Zero External Services

No database server, message broker, or vector service to run. Vector ANN, full-text BM25, and scalar filtering all execute inside the embedded LanceDB engine; SQLite is a local file. The whole stack is a single directory you can copy, back up, or check into git (user-visible parts only).
There is no automatic “grep over markdown” search fallback — if the LanceDB index is unavailable, rebuild it from markdown rather than relying on a degraded search path.

Operating It

CommandWhat it does
everos initwrite a starter .env
everos server startrun the HTTP API (cascade + OME start with it)
everos cascade statusqueue / LSN summary
everos cascade syncdrain the cascade queue now (force md → LanceDB)
everos cascade fixlist failed rows / re-enqueue retryable ones
There is no everos reindex or everos flush CLI command.
  • Reindex — the index is rebuildable: stop the server, rm -rf <memory-root>/.index/lancedb, restart — the cascade rebuilds from markdown. For an incremental catch-up, use everos cascade sync.
  • Flush is an HTTP endpoint (POST /api/v1/memory/flush), not a CLI command — it forces extraction of the session buffer, which is different from forcing index sync (cascade sync).