How it works¶
Indexing¶
The engine keeps its own copy of what it needs to search, in a SQLite file on its volume. Trilium stays untouched.
flowchart LR
trilium[("Trilium Notes")] -- "ETAPI, read-only" --> sync["Sync"]
sync --> split["Split notes<br/>into sections"]
split --> fts["Keyword index<br/>(SQLite FTS5)"]
split --> vec["Meaning index<br/>(e5-small embeddings,<br/>sqlite-vec)"]
sync --> links["Links and relations"]
- First start: a full build, about half a minute for a few hundred notes.
/healthsaysstartinguntil it's done. - Every minute: notes changed since the last run are picked up.
- Every 30 minutes: a full comparison catches deletions, moves and label changes. Trilium doesn't mark a note as modified when only its attributes change, so those wait for this run.
Indexed: all text and code notes. Left out: archived notes, Trilium's hidden notes and, with an LLM wiki, its plumbing (Locks, Templates, Views). Notes are sorted into areas: wiki, sources, journal (found by #calendarRoot) and everything else.
Answering a prompt¶
sequenceDiagram
participant C as Claude Code
participant H as Prompt hook
participant E as trilium-recall
C->>H: your prompt
H->>E: POST /recall (1.5 s limit)
E->>E: skip? (slash commands, "ok", bare follow-ups)
E->>E: keyword search + meaning search, fused
E->>E: relevance gate: good enough, or nothing
E->>E: drop notes already sent in this session
E-->>H: up to 3 passages, or empty
H-->>C: passages added to the context
- Two searches, one list. Keyword matches and meaning matches are merged (reciprocal rank fusion), then weighted by area: wiki pages first, then sources, then journal and other notes.
- Silence is a feature. A gate drops results whose meaning similarity is below a fixed threshold, so an unrelated prompt gets nothing rather than three weak guesses. Strong keyword matches on several terms get through anyway.
- Summaries stand in for sources. With an LLM wiki, a source that has a summary page shows up as the summary, unless the source itself holds the matching detail.
- No repeats. Within one Claude Code session, a note is sent once.
- Never in the way. No config, a slow or broken engine, any error: the hook adds nothing and the prompt goes through.
The recall MCP tool runs the same search, without the skip and session steps. When a client connects, the server sends instructions (RECALL_INSTRUCTIONS) saying when to call it; clients like Claude Code and claude.ai put them into the model's system prompt.
Security¶
One secret, RECALL_LOGIN_PASSWORD, guards both /recall and /mcp: as a Bearer header, or as the password on the engine's OAuth login page (with RECALL_BASE_URL set). The login comes from the shared mcp-login-oauth library. Whoever holds the password can search every configured instance.
The engine's ETAPI token could write (Trilium has no read-only tokens), but the engine only sends GET requests. Keep that token in the engine's environment and nowhere else.