-
Notifications
You must be signed in to change notification settings - Fork 0
The Trust Layer
Semantic similarity answers "which memory looks most like the query". It cannot answer "should this memory still be believed". The trust layer is that second judgment.
It runs as pure post-processing over a retrieval result — no database access, no clock
reads, no I/O. Everything it needs (the supersession map, the calibration, the current time) is
passed in. Source: recall/trust.py.
Every hit is annotated with four things, not one:
| verdict | Whether the memory can be relied on, and if not, why |
| confidence | Calibrated, on a scale where the midpoint sits exactly on the abstention boundary |
| provenance | Source, file, chunk ordinal, and when it entered the index |
| validity | The window it is valid in, and its successor if it has been superseded |
And the result as a whole carries an abstained flag with a human-readable reason, a
calibrated flag, calibration and generation identity where available, a corpus gap_warning,
and a staleness report on the index itself.
Only ok hits should be acted on. That is the entire contract, and it is why the return
type is a judgment rather than a ranked list.
| Verdict | Meaning |
|---|---|
ok |
Current, in-window, above the confidence boundary. Trust it. |
superseded |
A later memory declared that it replaces this one. validity.superseded_by names the successor. |
expired |
Past its authored valid_until. |
not_yet_valid |
Before its authored valid_from. |
low_confidence |
Nothing wrong with it — it simply did not clear the calibrated threshold. |
invalid_metadata |
Its validity dates could not be parsed. Fails closed. |
ambiguous_supersession |
A supersession edge points at this memory's basename, but more than one document in the corpus carries that basename. Fails closed. |
not_entailed |
(Opt-in entailment stage) Semantically close, but does not actually answer the query. |
Precedence when several apply: invalid_metadata > superseded > expired / not_yet_valid
low_confidence>ok.
Two of these are worth dwelling on, because they are where the design's principle shows.
invalid_metadata fails closed per hit. A memory whose validity window cannot be parsed is
not served as healthy — but nor does it crash the search that retrieved it. One bad row must not
take down every query that touches it, and it must not read as trustworthy either. (It is
reachable only by writing directly to the store; the indexer refuses malformed dates up front.)
ambiguous_supersession refuses rather than guesses. If an edge names notes.md and two
directories each contain a notes.md, which document the author meant is genuinely
unanswerable. Serving the hit as ok — quietly dropping the unresolvable edge — would be exactly
the silent wrong answer this layer exists to prevent. So it fails closed and the abstention
reason tells you to disambiguate the corpus.
Supersession is a relation between two memories. Both look valid in isolation, so it cannot be recovered from the documents at read time. RE-call requires the author of the replacement to declare it, in frontmatter:
---
supersedes: rate_limits_v1.md
valid_until: 2026-12-31
---That is the whole mechanism. A claim honoured as written is safe; a claim guessed at is not.
Because it was tested, steelmanned, and it does not work. Recency and authority are different properties: a memo can be newer than a memory it contradicts without being the one that replaced it, and "most recent relevant document" keeps selecting the stale memory at a rate that makes it useless as a guard. The numbers are in FINDINGS; the design argument is in The-Problem.
Edges form a graph, and retrieval follows it to the terminal successor — if a is
superseded by b and b by c, a hit on a reports c. Two degenerate cases are handled
explicitly rather than left to crash:
-
A cycle (
a → b → a) stops on the first revisit rather than looping forever. - A self-claim (a document declaring it supersedes itself — an authoring slip) is ignored, because a document cannot supersede itself.
A real corpus taught this one. On the reference corpus, every declared supersession edge failed
to resolve — not because the targets were missing, but because of how the references were
written: some as wikilinks ([[name]]), some without the .md extension, while the matcher
compared full basenames. The linter dutifully reported "does not exist in the corpus", which was
actively misleading.
The conclusion drawn was that a convention the corpus's own author cannot follow is a defect in
the convention, not in the author. So name, name.md, [name] and [[name]] now all resolve
to the same document — brackets stripped, extension stripped, compared on the stem. Directory
prefixes are dropped too.
Ambiguity handling did not get the same treatment: two files sharing a stem are still refused rather than guessed at. Tolerance about formatting is safe; tolerance about identity is not.
A subtle ordering rule that matters more than it looks. When a superseded hit scored above
the threshold — meaning it would have been the confident answer — its retrieved successor is
promoted from low_confidence to ok, even if the successor's own wording scores lower.
The reasoning: the explicit supersession edge transfers the topical relevance the stale memory just proved. The stale memo demonstrated the query is about this subject; its replacement is by declaration the current statement on that subject. Without this rule, a query phrased in the old memo's vocabulary would demote the stale hit correctly and then abstain — technically honest, but it would be withholding an answer the corpus actually contains.
Hits are then reordered valid-first, so the successor outranks the memory it replaced. This is the ordering decision the whole project turns on.
An author can forget to declare the edge. That is a much better place to fail than at read time,
because the omission is lintable — which is what recall lint, recall fix and recall check
address (CLI-Reference).
But the honest finding is that it could not be automated. On the reference corpus, dozens of memos described a closure in prose while declaring nothing in frontmatter. The extractor built to recover those edges proposed almost nothing, and on review every survivor was wrong for a different reason: one was reported speech, some superseded a claim or a scope inside their target rather than the target itself, and one was hedged — its author, asked directly, said the memo augmented rather than replaced.
Narrating versus declaring, part versus whole, augmenting versus replacing: invisible to a
pattern, obvious to the person writing. So the tool ships as a reviewing aid, and
recall check moves the question to write time, where the one who knows is still in the room.
Note the deliberate inversion between the two: fix refuses everything it cannot prove, because
it writes unattended. check surfaces every candidate it can find, because a human is right
there to pick one. Same extraction, opposite disposition — and the reason is who is in the room.
valid_from and valid_until are ISO dates in frontmatter, interpreted in UTC. valid_from
starts at the beginning of its day; valid_until runs to the end of its day, inclusive — so
a memo valid until a given date is valid for all of that date, which is what an author writing
the date means.
Source: recall/frontmatter.py.
Two robustness details, both from real files:
- A UTF-8 BOM before the opening fence is tolerated. Windows editors add one, and a BOM that silently disabled frontmatter would mean validity metadata lost without any signal.
-
Quoted values are unwrapped. Writing
supersedes: "v1.md"out of YAML habit must match the unquoted filename, not silently never apply.
Both are the same class of bug: a guard that fails silently is worse than one that fails loudly, because nothing tells you the protection is gone.
A fixed cosine threshold does not transfer across embedding models. Each model places its similarity scores in a different regime, so a threshold tuned on one is arbitrary on another. This was measured rather than assumed → FINDINGS §2.
recall calibrate fits a threshold against a labelled set of answerable and unanswerable
queries and writes it to a file. Every search then maps raw cosine to a calibrated confidence
through a monotone logistic curve centred on that boundary — so 0.5 is exactly the decision
point, by construction.
That confidence is a calibrated ranking confidence, not a posterior probability. The calibration sets are small and the docstring says so; treating it as a probability would be reading more into it than the data supports.
In the middle of the observed gap between the two score distributions — bisecting the answerable floor and the unanswerable ceiling, each taken as a quantile rather than an extreme so that a single outlier cannot define the boundary.
The previous rule minimised misclassification on the samples it was given. That sounds principled and is not: the cheapest way to keep every answerable sample above the boundary is to put the boundary exactly on the lowest one, which has three consequences that were all measured —
- no margin on the answerable side, so any real answer weaker than the weakest calibration sample abstains;
- one sample decides everything, because the answerable distribution has a long lower tail and the boundary sat at the bottom of it;
- it inherited ANN noise — HNSW index builds are nondeterministic, so the identity of the worst sample changed on every rebuild and the operating point moved with it.
The replacement was validated by fitting on half the queries and scoring on the other half, across multiple index rebuilds. It trades a small increase in wrongly-abstained answerable queries for a large cut in confidently-answered unanswerable ones. Going further was tested and rejected as a bad trade. Figures → FINDINGS.
A legacy calibration file is ignored — falling back to the uncalibrated default, with
calibrated=False flagged in every result — when it is absent, unreadable, malformed, fitted for
a different embedder, or carries out-of-range values.
Each of those is a real failure mode: a threshold fitted in another model's cosine regime must never be applied, and a corrupt file must never be able to silently disable abstention (a NaN threshold compares false against everything) or crash every search (a zero or negative scale). The flag matters as much as the fallback — a caller can tell that the guard is running on defaults.
In v1 generation serving, calibration is resolved from PostgreSQL by tenant and active generation. Legacy local files are import-only evidence and are not automatically selected by the MCP server. Production strict trust refuses when the active generation, lineage or certified calibration cannot be established.
Calibration now records whether the answerable and unanswerable classes were actually separable.
Calibration.certified is false when the classes overlap or either class is too small, and None
for older artifacts where the question cannot be answered. Certification changes no runtime
threshold by itself; it is a gate and a warning surface, not a retune. That matters because moving
the boundary during certification would mix diagnosis with behavior.
abstained is true when no hit earned verdict ok. The reason is specific, not generic:
whether the best candidate was superseded (and by what), outside its validity window, carrying
malformed metadata, ambiguously superseded, or simply below the confidence threshold — the last
being the probable-corpus-gap case.
A separate, cheaper signal runs alongside: gap_warning fires when every dense candidate
scores below the threshold. It is computed from dense cosines only, never the fused ranks, so a
purely lexical match still reports a gap. That is deliberate — a keyword hit is not evidence of
semantic relevance, and letting the sparse leg suppress the gap warning would quietly reintroduce
confident answers over an empty corpus.
The threshold catches far gaps: queries whose best match is semantically distant. It cannot catch the near-miss — a memory adjacent to the query that does not answer it. Its similarity clears any threshold by construction, which is a stronger statement than "the threshold is badly tuned". No threshold works here.
The abstention signal for that class cannot come from the retriever's own score. It needs a separate judgment that the memory actually entails an answer. Proximity is a candidate; entailment is the evidence.
So apply_entailment is an opt-in stage that judges only verdict-ok hits — judging an
already-demoted hit would waste a model call and could resurrect a superseded memory — demoting
those that do not entail an answer to not_entailed, and abstaining when none remain.
It is off by default, and the honest reason is in the measurements: the judge cuts near-miss
false-confidence substantially, but degrades far-gap detection. The two stages stack; neither
replaces the other. Enabling it costs one judge pass per ok hit. Full study →
docs/ENTAILMENT_SUPERSESSION_STUDY.md.
Independent of any individual hit: the result reports how old the newest indexed content is, and flags the whole result stale past a maximum age. An index that stopped updating returns confident, well-formed, quietly obsolete answers — and nothing in the hits themselves would show it.
Strict mode is the default. When the trust gate cannot certify an answer because the index is not ready, lineage mismatches, calibration is missing or uncertified, calibration is stale, or a needed dependency is unavailable, the production surface raises a refusal rather than returning an empty result. Empty means "the gate ran and found nothing trustworthy"; refusal means "the gate could not run." Collapsing those two would let an agent treat an outage as no prior memory.
RECALL_TRUST_MODE=development exists for local ad hoc corpora. It can return degraded results, but
those hits are unverified context and must not be read as a trust-layer claim.
Next: Retrieval-Pipeline for what happens before the trust layer ·
Python-API-and-MCP for how to branch on these fields · CLI-Reference for calibrate,
lint and check.
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