LLM Second Brain
by philadelfic
README.md
# LLM Second Brain



A self-hosted **long-term memory server** for LLMs running in harnesses
(primarily Open WebUI). Models get MCP access to a shared knowledge store —
notes, three knowledge areas (skills, terms, facts about the user) and the
assistant log: they can search it (hybrid vector + full-text), read, write,
update and delete records, and follow the version history of what changed.
## What it is
- **One Docker container**, self-hosted, non-root.
- **MCP Streamable HTTP** (`/mcp`, natively supported by Open WebUI) with a
Bearer token.
- **31 MCP tools**: 9 `memory_*` for notes, comments and namespaces; 5
`skills_*`, 5 `user_*` and 3 `terms_*` for the knowledge areas; 6 `logs_*`
for the assistant log; `history_list` / `history_get` for version history;
`warnings_list` for the "notes not in their place" job.
- **Storage**: one SQLite database + `sqlite-vec` (vector search) + FTS5
(full-text), merged via Reciprocal Rank Fusion; every knowledge area has
its own tables and indexes, isolated from notes and from each other.
- **Three knowledge areas next to notes** (since v3.0): **skills** — stored
procedures, read in full only when needed, with a version archive;
**terms** — terminology keyed by (term + context), every sense returned and
never overwritten; **user** — atomic facts about the user with dedup hints.
Each area has MCP tools and operator REST mirrors.
- **Assistant log next to notes** (since v3.2): the assistant's own record of
"what I did, when and why" — the changed object and the result, what was
checked; its own tables and indexes, its own `logs_*` tools, a compose flag,
isolated from note search.
- **Version history** (since v3.2): every content change to a note, an area
record or an assistant log is versioned with line-level deltas for long texts
and a full baseline for the first version; the operator reads it over REST
(`GET /history/<entity>/<id>`) and can roll back — a rollback writes a NEW
version and never rewrites history; the agent gets `history_list` and
`history_get`.
- **Slots that wait instead of failing** (since v3.2): every slot call has a
typed outcome (`unavailable` / `auth` / `timeout` / `empty` / `truncated` /
`off_contract`); a broken slot opens a break, does one cheap probe per pause
and resumes by itself, while its work stays visible as `waiting` instead of
burning calls. `decision` is a third slot type: the model answers structured
questions instead of generating text, and the product falls back to the
generative path on `unavailable` / `unsupported` / `low_confidence`.
- **Output budget "as much as fits"** (since v3.2): a slot spends the model's
context minus the prompt (a reserve plus a protective ceiling) instead of a
fixed small cap; `done_reason` / `finish_reason`, thinking tokens and the
applied budget land in the journal, and an empty answer is distinguished from
a truncated one.
- **Measured injected text** (since v3.2): one token counter measures and guards
everything the server injects into the agent — the instructions core, the tool
descriptions and the call weight (`app/services/injection_budget.py`), so the
surface cannot grow silently.
- **Vectorization & summarization** are external LLM calls. Each of the three
slots (embedding / summary / judge) is configured independently with its own
provider (`ollama` or an OpenAI-compatible API), base URL, model and optional
API key.
- **Hierarchical namespaces**: the store is split into large sections; the map
is exposed to models via MCP instructions and `memory_namespaces`.
- **Related notes** (since v3.1): reading a note also returns its `links` —
neighbours by meaning or by shared entities, from other namespaces; search,
list and read outputs carry `chars`, the size of the full note text.
- **Listings that page** (since v3.1): `memory_list` and `skills_list` report
`total` / `has_more` / `next_offset` / `next_cursor` and a `+N more` hint when
a page is capped; the ceiling is tighter on the MCP surface than on REST.
- **Background worker**: pending vectors, summaries, dedup, classification,
title generation and the new knowledge areas are processed asynchronously;
scheduled maintenance jobs (related-notes recompute, default-namespace
tidy-up) run on their own intervals; failures never break CRUD (pending
states + back-off retry).
- **Observability** (since v3.1, extended in v3.2): `/health` (no token) reports
the running application `version`, `queues` (how many jobs wait in each
background queue, how long the oldest has waited, and how many need a human),
`slots` (the last outcome and the pause of every slot, plus call diagnostics),
`decision`, `areas`, `warnings`, `update` and `backups`.
- **Backups**: periodic online SQLite snapshots with rotation.
## Why
1. **Distributed knowledge with fast access.** Knowledge lives in a separate
store, not in the system prompt or chat history; the model fetches only what
is relevant, on demand.
2. **Token economy.** Instead of a monolithic context — a measured, guarded
overhead for the tool spec (the instructions core 525 tokens, the 31 tool
descriptions 2170 tokens), a compact skills announce (budget 2000
characters, refreshed on every connect) and targeted retrieval of **short
summaries**, not full texts.
## Quick start
```bash
git clone <repo> llm-second-brain && cd llm-second-brain
mkdir -p data prompts
# Edit docker-compose.yml: set MCP_AUTH_TOKEN (openssl rand -hex 32) and the
# three LLM slot addresses/models. Full reference: docs/CONFIG.md.
docker compose up -d --build
curl -s http://localhost:8080/health | python -m json.tool
```
`/health` answers without a token. See [Installation](docs/INSTALL.md) for the
first-run walkthrough (compose, token, Open WebUI, `/health`).
## Documentation
- [Installation](docs/INSTALL.md) — setup, first run, Open WebUI, `/health`.
- [Configuration](docs/CONFIG.md) — environment variables, the prompt files,
and the `OLLAMA_KEEP_ALIVE` note.
- [Changelog](CHANGELOG.md) — release history (Keep a Changelog).
## Operational notes
- **`keep_alive` is not sent by the client** (since v2.1). Model residency is
managed by the server: set `OLLAMA_KEEP_ALIVE` on the Ollama side if you want
models to stay loaded. With the server default (5 min) models are unloaded
more often, and a cold start (~22.6 GB for the summarizer) returns to
latency.
- **Changing `EMBEDDING_PROVIDER` / `EMBEDDING_MODEL` / `EMBEDDING_DIM`**
triggers an automatic full reindex on startup: all notes go to `pending` and
the worker re-encodes them, and the knowledge-area indexes are rebuilt the
same way. Search/dedup thresholds are calibrated for `qwen3-embedding:8b` —
recalibrate after changing the model.
- **Knowledge areas (since v3.0)**: skills, terms and user facts live in the
same database but in their own tables and indexes; they are isolated from
notes in both directions. Their form limits and similarity thresholds are
environment-tunable and validated at startup.
- **Privacy**: an `openai` provider sends note texts to an external API (the
dedup judge and classifier see full texts). Choose providers per slot
deliberately.
## Restore from a snapshot
Snapshots are ordinary SQLite files in `BACKUP_DIR` (default `/data/backups`,
inside the `./data` volume); the name carries the UTC timestamp. To restore one:
1. Take the newest `notes-*.db`. `GET /backups` lists the snapshots with
`verified` (`ok` / `failed` / `unknown`) — prefer an `ok` one; a file marked
`failed` also has a `<file>.bad` sidecar next to it. `POST /backups/verify`
checks one on demand.
2. Stop the service: `docker compose down` (nothing must write to the file you
are about to replace).
3. Put the snapshot in place of the live database (`DB_PATH`, default
`data/notes.db`), e.g. `cp data/backups/notes-<stamp>.db data/notes.db`, and
delete the stale `notes.db-wal` / `notes.db-shm` beside it if any. To keep the
old file untouched, drop the copy next to it and point `DB_PATH` at the copy.
4. Start the service (`docker compose up -d`) and check `/health`: `status` is
`ok`, `notes_count` matches the snapshot, and `embedding_ok` turns `true`
after the first successful call to the embedding slot.
5. If the snapshot was taken with a different embedding model (the verification
note `embedding_mismatch`), a full reindex starts on its own — wait for
`pending_vector` to reach `0` before searching.
Shipping a verified copy off the box is optional and driven by `BACKUP_SHIP_CMD`
(empty by default); `scripts/backup_ship.sh` is a secret-free template. The
command and its output never reach the logs.
## License
Distributed under the [MIT](LICENSE) license.
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues