-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Two installable packages:
-
recall/— the library. No server, no framework, no global state. -
recall_mcp/— an MCP server wrapping it, so an agent can call the library as a tool.
recall_mcp depends on recall. Nothing in recall knows the MCP server exists.
flowchart TB
Q([query]) --> TS["trust.trusted_search()"]
TS --> R["retriever.HybridRetriever"]
R --> E["embeddings.Embedder"]
R --> SD["store.query_dense · pgvector"]
R --> SS["store.query_sparse · full-text"]
SD --> RRF["Reciprocal Rank Fusion"]
SS --> RRF
RRF --> RK["rerank.Reranker (optional)"]
RK --> G["guards: gap_warning · staleness"]
G --> EV["trust.evaluate()"]
SUP["store.supersession()"] --> EV
CAL["calibration.Calibration"] --> EV
EV --> EN["entailment.apply_entailment (opt-in)"]
EN --> O([TrustedResult])
The shape worth noticing: trust.evaluate() is pure. It reads no clock, touches no
database, and performs no I/O — now, the supersession map and the calibration are all passed
in. That is why the trust semantics can be tested exhaustively without a database, and why the
ordering rules are verifiable rather than merely observed.
| Module | Owns | Depends on |
|---|---|---|
types.py |
The data contract: Chunk, ScoredChunk, TrustedHit, TrustedResult, Verdict, Validity, Provenance, StalenessReport. All frozen dataclasses. |
nothing |
store.py |
PostgreSQL + pgvector. Schema, migration, RLS, dense/sparse queries, upsert, prune, the supersession scan and its cache, connection mode (single vs pool), retry, timeouts, DSN security checks. The largest module, and the only one that talks to a database. | types |
embeddings.py |
The Embedder protocol and its implementations (hashing, FastEmbed, sentence-transformers, Voyage), plus batching, retry-with-backoff and dimension checking. |
nothing |
retriever.py |
HybridRetriever — the two legs, RRF fusion, rerank hand-off, and annotation with gap/staleness. |
store, embeddings, rerank, guards, types
|
rerank.py |
The Reranker protocol, a no-op, and a cross-encoder implementation. |
types |
guards.py |
The two cheap honesty signals: gap_warning (are all candidates below threshold) and staleness (is the index too old). Holds the default gap threshold. |
types |
calibration.py |
Per-embedder abstention threshold and the cosine→confidence mapping; fitting from labelled samples; safe load/save. | observability |
frontmatter.py |
The dependency-free frontmatter parser, validity-date interpretation, and supersedes: reference normalisation. |
nothing |
trust.py |
Verdict assignment, successor resolution, successor promotion, valid-first ordering, the abstention decision and its reason. |
store, embeddings, retriever, rerank, calibration, frontmatter, guards, types
|
entailment.py |
Opt-in near-miss guard: demotes verdict-ok hits that do not entail an answer. Pure post-processing over a TrustedResult, mirroring trust. |
trust, types
|
index.py |
Indexer — file discovery, chunking (prose and code), NUL sanitisation, index-root confinement, content-hash skip, batched writes, pruning of vanished sources and the prune guard. |
store, embeddings, frontmatter, cache, lint, observability
|
cache.py |
Content-addressed embedding cache (SQLite), keyed by embedder name + dim + text. | embeddings |
lint.py |
Static supersession-graph lint: dangling edges, self-supersession, cycles, ambiguous targets, invalid dates. No database. | frontmatter |
fix.py |
Extracts prose closure markers and proposes supersedes: edges — under rules that refuse far more often than they act. |
frontmatter, lint
|
check.py |
Write-time gate for a single file being authored: surfaces candidate edges while the author is still present. |
frontmatter, lint
|
semantic_lint.py |
The missing-edge lint: queries the index with a new memo's text to surface closed decisions it should probably reference. |
trust, store, embeddings
|
observability.py |
get_logger (namespaced, never configures handlers), opt-in configure_logging with JSON output, and the METRICS counter/histogram registry. |
nothing |
timing.py |
Decorators that wrap an Embedder or Reranker to record per-call latency. Used by the eval harness. |
embeddings, rerank, types
|
cli.py |
Argument parsing and the command implementations. | most of the above |
_env.py |
Minimal .env loader for local development. Never overrides an already-set variable. |
nothing |
eval/ |
The evaluation harness: ablation matrix, trust eval, near-miss eval, calibration fitting, labelled-question runner, synthetic corpus generation, scale/latency runner, metrics. | most of the above |
| Module | Owns |
|---|---|
server.py |
Builds the FastMCP server, the four tools, the lifespan (store construction, calibration load, auth wiring), scope enforcement per tool, and transport selection. |
service.py |
The tool bodies, independent of MCP: search, index, forget, stats. Pydantic response models. This is where the logic lives, so it is testable without a server. |
auth.py |
Bearer-token registry, principal→tenant mapping, scope definitions, token-file parsing (plaintext or SHA-256 digests), expiry, and the fail-closed configuration check. |
limits.py |
Per-tenant token buckets: call rates per tool class and an aggregate indexing byte budget. |
stores.py |
Per-tenant store construction and lifetime. |
The trust layer is a pure function over a retrieval result. It could have been folded into the retriever's SQL — one query returning already-judged rows would be faster. It is separate because the judgment is the part most likely to be wrong, and a pure function is the only shape in which "a superseded hit with a top cosine loses to its lower-scoring successor" is a test rather than an observation.
Entailment mirrors trust rather than extending it. apply_entailment takes a TrustedResult
and returns a TrustedResult. It judges only verdict-ok hits — judging an already-demoted hit
would waste a model call and, worse, could resurrect a superseded memory by ranking it back up.
The two stages stack; neither replaces the other, and the measurements say so explicitly.
A store is bound to one tenant for its lifetime. Tenancy is not an argument you pass to a
query — it is a property of which store object you hold, enforced in the database by a row-level
security policy comparing against a per-connection setting. That is what makes a forgotten
WHERE clause return nothing rather than another tenant's memories, and it is why the MCP
server's authentication has to resolve a tenant before any tool body runs. → Tenancy-and-Auth
- Retrieval mechanics → Retrieval-Pipeline
- Verdicts, supersession, calibration, abstention → The-Trust-Layer
- Calling it → Python-API-and-MCP
- Every configuration knob → Configuration-Reference
This wiki explains design and intent. Measured figures, deployment defaults and CLI flag values
live in the repository, versioned with the code that produced them:
FINDINGS ·
RESULTS ·
MIGRATIONS ·
.env.example · --help.
If a page here disagrees with the repo, the repo is right.
Concepts
Using it
- Installation-and-Setup
- Embedders-and-Rerankers
- Configuration-Reference
- CLI-Reference
- Python-API-and-MCP
- Tenancy-and-Auth
Evidence
Contributing
In the repo