Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,49 @@ jobs:
- name: Integration tests
run: make integration

milvus-integration:
name: Milvus Server integration
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v8.2.0
with:
enable-cache: true
cache-dependency-glob: uv.lock

- name: Set up Python
run: uv python install 3.12

- name: Install dependencies (frozen)
run: make install-deps

- name: Start Milvus Server
run: |
curl -fsSL \
https://github.com/milvus-io/milvus/releases/download/v2.6.22/milvus-standalone-docker-compose.yml \
-o /tmp/milvus-compose.yml
docker compose -f /tmp/milvus-compose.yml up -d
for attempt in $(seq 1 90); do
if curl -fsS http://127.0.0.1:9091/healthz; then
exit 0
fi
sleep 2
done
docker compose -f /tmp/milvus-compose.yml logs
exit 1

- name: Milvus derived-index contract
env:
EVEROS_TEST_MILVUS_URI: http://127.0.0.1:19530
EVEROS_TEST_MILVUS_FULL_STARTUP: "1"
run: uv run pytest tests/integration/test_milvus_remote.py -v

- name: Stop Milvus Server
if: always()
run: docker compose -f /tmp/milvus-compose.yml down -v

package:
name: package build
runs-on: ubuntu-latest
Expand Down
16 changes: 16 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,19 @@ max_concurrent = 5
#
# [lancedb]
# read_consistency_seconds = 5.0

# ── Optional Milvus derived index ─────────────────────
# Markdown remains the source of truth. Set the index backend to Milvus
# when you want the rebuildable vector/BM25 index to live in an external
# Milvus Server or Zilliz Cloud deployment. Embedded Milvus Lite and local
# database paths are not supported.
#
# [index]
# backend = "milvus"
#
# [milvus]
# uri = "http://localhost:19530" # required remote endpoint
# token = "" # required for Zilliz Cloud/auth-enabled Milvus
# db_name = "" # use a dedicated database when available
# consistency_level = "Session"
# collection_prefix = "everos" # make unique on a shared database
15 changes: 8 additions & 7 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ bare FastAPI `detail`); see [Errors](#errors).

`/add` and `/flush` write the markdown file (the source of truth)
**synchronously** — when the call returns with `status: "extracted"`,
the new entry exists on disk. The LanceDB vector / BM25 / scalar index
is rebuilt by the in-process **cascade coroutine asynchronously**.
the new entry exists on disk. The configured vector / BM25 / scalar
index backend is rebuilt by the in-process **cascade coroutine
asynchronously**.

That means `/search` and `/get` may not see a record immediately after
the `/flush` that produced it. Typical sync latency is sub-second, but
Expand Down Expand Up @@ -371,7 +372,7 @@ A recursive boolean tree of predicates. Used by `/search.filters` and
`/get.filters`. The Pydantic envelope only checks the recursive
combinator shape; field-level validity (which scalar fields are
filterable, which operators apply, value coercion) runs when the
node is compiled to a LanceDB `where` clause server-side. Compile
node is compiled to a backend-specific filter clause server-side. Compile
errors surface as `422` with the offending field / operator in
`error.message`.

Expand Down Expand Up @@ -464,12 +465,12 @@ Examples:
|---|---|
| `"keyword"` | BM25 only — pure lexical match, no embedding cost |
| `"vector"` | Dense vector ANN only — semantic recall, no lexical |
| `"hybrid"` *(default)* | Reciprocal-rank fuse of BM25 + vector + optional scalar filter in a single LanceDB query |
| `"hybrid"` *(default)* | Reciprocal-rank fuse of BM25 + vector + optional scalar filter against the configured derived index backend |
| `"agentic"` | Iterative cluster-path retrieval driven by a cross-encoder rerank loop; higher quality at higher latency / cost |

`"hybrid"` is the default because it balances recall and precision
with one LanceDB roundtrip. `"agentic"` calls the LLM in a loop and
should be reserved for offline or background workflows.
without requiring the agentic loop. `"agentic"` calls the LLM in a loop
and should be reserved for offline or background workflows.

### GetMemoryType

Expand Down Expand Up @@ -613,7 +614,7 @@ scope.
this `(session_id, app_id, project_id)`, or it was already flushed).

`/flush` is synchronous with respect to markdown persistence: by the
time the response returns, the new entry is on disk. LanceDB index
time the response returns, the new entry is on disk. Derived index
sync is still asynchronous — see
[Eventual consistency](#eventual-consistency).

Expand Down
26 changes: 15 additions & 11 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
│ + reflection + strategies + get + events │
├──────────────────────────────────────────────────────┤
│ infra/persistence (Storage adapters; infra/ may host other adapter types) │
│ markdown + sqlite + lancedb
│ markdown + sqlite + derived index
└──────────────────────────────────────────────────────┘

Cross-cutting (used by all layers, depends on none):
Expand Down Expand Up @@ -68,8 +68,8 @@ layers = [
└────────────────────────────────────────────────────────────────┘

┌──────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Markdown │ │ SQLite │ │ LanceDB
│ (truth) │ │ (state) │ │ (index)
│ Markdown │ │ SQLite │ │ Derived index
│ (truth) │ │ (state) │ │ LanceDB/Milvus
├──────────────┤ ├──────────────┤ ├─────────────────┤
│ entries + │ │ change queue │ │ vector ANN │
│ frontmatter │ │ + state/LSN │ │ BM25 (Tantivy) │
Expand All @@ -78,7 +78,7 @@ layers = [
└──────────────┘ └──────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
memory-root/ .index/sqlite/ .index/lancedb/
memory-root/ .index/sqlite/ .index/<backend>/
(truth source) (system data) (rebuildable)
```

Expand All @@ -101,10 +101,13 @@ External message
│ │
▼ ▼
4a. SQLite 4b. memory.cascade (async daemon)
audit watches md → diff entries → LanceDB sync
audit watches md → diff entries → index sync
```

**Key guarantee**: md write is strongly consistent (fsync). LanceDB is eventually consistent. LanceDB unavailability does not block response — changes buffer in the SQLite `md_change_state` queue, replayed on recovery.
**Key guarantee**: md write is strongly consistent (fsync). The derived
index is eventually consistent. Index backend unavailability does not block
response — changes buffer in the SQLite `md_change_state` queue, replayed on
recovery.

## Read path

Expand All @@ -115,8 +118,8 @@ User query
1. service.search
2. memory.search (hybrid) single LanceDB query =
BM25 + vector ANN + scalar filter
2. memory.search (hybrid) BM25 + vector ANN + scalar filter
through the configured index backend
3. (optional) read md original markdown for context
Expand All @@ -139,12 +142,13 @@ extract/

### `memory/cascade/`

Daemon that watches markdown changes and syncs to LanceDB:
Daemon that watches markdown changes and syncs to the configured derived
index backend:

- inotify / FSEvents file watcher (cross-platform via `watchdog`)
- 500ms debounce
- Entry-level diff (added / changed / removed)
- LanceDB single-transaction update (text + vector columns atomic)
- Per-entry index upsert / delete (text + vector columns update together)
- LSN-based crash recovery via the SQLite `md_change_state` queue
- Handlers for all eight business kinds: episode, atomic_fact, foresight,
user_profile, agent_case, agent_skill, knowledge_document, knowledge_topic
Expand Down Expand Up @@ -225,7 +229,7 @@ holding **only memory extraction algorithms**:
everalgo is:

- **Stateless** — pure functions, no class hierarchy
- **No I/O** — does not touch md files / LanceDB / SQLite
- **No I/O** — does not touch md files, derived indexes, or SQLite
- **No prompts inline** — extractors that accept a prompt-override parameter use the project-supplied value; others use their algo-bundled defaults

This boundary lets everalgo be reused across product forms (this open-source build, EverOS Cloud, OpenClaw plugins, etc.).
Expand Down
29 changes: 19 additions & 10 deletions docs/cascade_runbook.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# Cascade Runbook

The cascade daemon keeps LanceDB in sync with the markdown files under
the memory root. Service / entry points only ever write markdown; the
daemon is the **sole** writer of the LanceDB index. This runbook covers
the recurring operational questions.
The cascade daemon keeps the configured derived index in sync with the
markdown files under the memory root. Service / entry points only ever write
markdown; the daemon is the **sole** writer of the derived index. This runbook
covers the recurring operational questions.

Sections that mention LanceDB-specific schemas, index cache, file descriptors,
or `lance error` messages apply to the default LanceDB backend. Milvus uses the
same cascade queue with backend-specific collection management.

## What runs where

Expand All @@ -13,7 +17,7 @@ providers in order:
1. **Metrics** — Prometheus collector.
2. **LLM** — LLM client initialisation.
3. **SQLite** — system DB + schema (`SQLModel.metadata.create_all`).
4. **LanceDB** — async connection + schema verification + FTS indexes.
4. **Derived index** — async connection + schema verification + search indexes.
5. **Cascade** — watcher + scanner + worker, all in-process tasks.
6. **OME** — offline memory engine.

Expand All @@ -23,7 +27,7 @@ The cascade subsystem itself is three independent loops:
|---|---|---|
| Watcher | `watchdog` filesystem events (sync thread) | `md_change_state.upsert` per registered kind |
| Scanner | Periodic walk (`scan_interval_seconds`, default 30 s) | Same — catches changes the watcher missed |
| Worker | `claim_pending_batch` polling (default 1 s when idle) | Handler dispatch → LanceDB upsert / delete |
| Worker | `claim_pending_batch` polling (default 1 s when idle) | Handler dispatch → index upsert / delete |

Every loop talks to the same `md_change_state` sqlite table. The
worker's claim mode (`pending → processing → done/failed`) keeps
Expand Down Expand Up @@ -123,7 +127,7 @@ parallel with a live `everos server`.

## Rebuild the index: `everos cascade rebuild`

The safe recovery from a drifted or corrupt LanceDB index. It rebuilds
The safe recovery from a drifted or corrupt derived index. It rebuilds
the whole index from markdown (the source of truth) in one shot:

```bash
Expand All @@ -132,15 +136,16 @@ everos cascade rebuild --yes # non-interactive
```

> **Stop the `everos server` first.** Unlike `cascade sync`, rebuild
> **drops and recreates** the LanceDB tables. A running daemon holds
> **drops and recreates** the active backend's tables or collections. A running
> daemon holds
> cached table handles that would keep pointing at (and writing to) the
> dropped dataset, corrupting the rebuild. This is the one cascade
> command that is **not** safe to run alongside a live server.

What it does, in order:

1. **Drops** every business LanceDB table (`drop_business_tables`) and
evicts them from the connection cache.
1. **Drops** every business table or collection (`drop_business_tables`) and
evicts it from the process cache.
2. **Recreates** them empty from the current schema + FTS indexes
(`ensure_business_indexes`).
3. **Clears** the cascade queue (`md_change_state.reset_all`) so every
Expand All @@ -160,6 +165,10 @@ Why not a bare `rm`:
| `rm -rf .index` | ✅ | ❌ deletes un-extracted messages |
| `everos cascade rebuild` | ✅ | ✅ |

For a remote Milvus backend, rebuild acts only on collections whose names use
the configured `collection_prefix`. Use a unique prefix or dedicated database
before running it on a shared Milvus Server or Zilliz Cloud deployment.

## Recovery paths

### LanceDB schema drift on startup
Expand Down
62 changes: 62 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,63 @@ everos init --root /data/everos
| `read_consistency_seconds` | float \| null | `null` | Read consistency interval. `null` = no check, `0` = strict, `>0` = eventual. |
| `index_cache_size_bytes` | int | `16777216` | Upper bound on LanceDB index cache (16 MB default). |

### `[index]`

| Field | Type | Default | Description |
|---|---|---|---|
| `backend` | `"lancedb"` \| `"milvus"` | `"lancedb"` | Rebuildable vector/BM25 index backend used by cascade, search, and get. Markdown remains the source of truth; SQLite remains the system state store. |

### `[milvus]`

Milvus is optional. Install with `everos[milvus]`, then set
`[index].backend = "milvus"`. The backend connects only to an external
Milvus Server or Zilliz Cloud endpoint; embedded Milvus Lite and local `.db`
paths are not supported.

| Field | Type | Default | Description |
|---|---|---|---|
| `uri` | string | `""` | Required when the Milvus backend is selected. Must be a remote Milvus Server or Zilliz Cloud endpoint. |
| `token` | string | `""` | Token for Zilliz Cloud or auth-enabled Milvus Server. |
| `db_name` | string | `""` | Optional Milvus database name. Prefer a dedicated database on shared clusters. |
| `consistency_level` | `"Strong"` \| `"Session"` \| `"Bounded"` \| `"Eventually"` | `"Session"` | Milvus consistency level for created collections. |
| `collection_prefix` | string | `"everos"` | Prefix for EverOS collections. Must start with a letter or underscore and contain only letters, digits, and underscores. Make it unique when deployments share a database. |

For a self-hosted Milvus Server, a minimal configuration is:

```toml
[index]
backend = "milvus"

[milvus]
uri = "http://localhost:19530"
collection_prefix = "everos"
```

Zilliz Cloud uses the same backend. Credentials can be supplied through
environment variables:

```bash
export EVEROS_INDEX__BACKEND=milvus
export EVEROS_MILVUS__URI="https://your-cluster-endpoint"
export EVEROS_MILVUS__TOKEN="<api-key>"
export EVEROS_MILVUS__DB_NAME="default"
export EVEROS_MILVUS__COLLECTION_PREFIX="everos_prod"
```

Use a deployment secret rather than committing the token to a configuration
file. If the memory root already contains Markdown records, stop the EverOS
server and run `everos cascade rebuild --yes` to populate the derived index.

EverOS creates seven collections and supports every dense field in the shared
index schema, including `episode.subject_vector`. The vector dimension is owned
by that schema (currently 1024), rather than duplicated in Milvus config.

Milvus imposes limits that LanceDB does not: strings are capped at 65,535
UTF-8 bytes, string arrays at 256 items, and each array item at 512 UTF-8
bytes. EverOS validates these before a write and reports the exact table and
field instead of relying on a server-side insert error. Ensure the selected
Zilliz Cloud plan permits at least seven collections.

### `[llm]`

| Field | Type | Default | Required | Description |
Expand Down Expand Up @@ -228,3 +285,8 @@ Examples:
| `[llm] api_key = "sk-..."` | `EVEROS_LLM__API_KEY=sk-...` |
| `[sqlite] busy_timeout_ms = 10000` | `EVEROS_SQLITE__BUSY_TIMEOUT_MS=10000` |
| `[memory] timezone = "Asia/Tokyo"` | `EVEROS_MEMORY__TIMEZONE=Asia/Tokyo` |
| `[index] backend = "milvus"` | `EVEROS_INDEX__BACKEND=milvus` |
| `[milvus] uri = "http://localhost:19530"` | `EVEROS_MILVUS__URI=http://localhost:19530` |
| `[milvus] token = "..."` | `EVEROS_MILVUS__TOKEN=...` |
| `[milvus] db_name = "default"` | `EVEROS_MILVUS__DB_NAME=default` |
| `[milvus] collection_prefix = "everos_prod"` | `EVEROS_MILVUS__COLLECTION_PREFIX=everos_prod` |
Loading