v0.10.0 — Release 0.10.0
Security
- Right-to-erasure now reaches the learned-sparse sidecar.
delete_sources,
delete_sources_across,replace_sourcesandgenerations.forgetscrub
recall_sparse_v1in the same transaction as the chunk delete (DELETE ... RETURNING id
feeds the scrub, so the ids come from the delete itself). Before, a forgotten chunk's
SPLADE term weights — partially reconstructable content over a 30,522-term vocabulary —
survived every erasure path exceptdrop_table. TheRECALL_ENV=productionrefusal on
the splade backend stays until an orphan sweep exists for corpora encoded before this
fix, and that gate now also matchesProductionandproduction(the bare compare
meant a capital letter silently disabled it). - Forgetting a source now also unlinks its staged upload file.
recall_forgeterased
the DB rows and left the original text underRECALL_INDEX_ROOT/uploads/, where the next
index run would re-ingest it. Cleanup is best-effort after the committed delete, reported
in the result (staged_files_removed, -1 on failure with a warning in the message), and
hard-confined to the uploads tree: a source indexed from the user's own directory is
never deleted. - BREAKING:
recall_calibration_publishnow requires therecall:adminscope. Publication
changes the serve/abstain decision for every query a tenant runs — the blast radius the admin
scope was defined for, and until now no tool enforced it: any write token could publish. An
HTTP deployment whose write token publishes calibrations must add"recall:admin"to that
principal'sscopes(static token file) or grant it in the IdP role (OIDC). Stdio and the
desktop-local runtime are unaffected.recall_calibration_runstays on write: it produces a
draft and changes nothing served. The requirement is advertised to clients in the tool's
_meta(recall/requiredScope), and admin calls draw on their ownadmincall budget
(RECALL_RATE_ADMIN_PER_MIN, default 10/min). recall_ingestnow debits the same per-tenantindex_bytesquota asrecall_index.
Before, only the 50 MiB per-request cap and the write call budget bounded uploads, so a loop
under the per-request cap could ingest roughly 300× the intended hourly embedding spend
unmetered. The debit lands after staging and before any embedding, so a refusal costs nothing;
a refused or failed ingest also removes its staged files instead of leaving them inside the
index root for a later index run to pick up.recall_tenantsno longer hands the full tenant inventory to every read token. In a
multi-tenant deployment tenant ids are often customer names. An authenticated principal now
sees its own tenant; the full provisioned list requiresrecall:admin.recall_job_statusis tenant-scoped and the job ledger is bounded. A foreign tenant
probing a job id gets the sameunknownshape as a nonexistent one, and completed jobs are
evicted by count and age instead of accumulating for the life of the process.- Failed bearer-token authentication is throttled before any hashing or JWKS work.
A process-global failure budget (RECALL_RATE_AUTH_FAILURES_PER_MIN, default 60/min,off
supported) closes the gate against a brute-force or forgery storm. Valid tokens never touch
it, and an identity-provider outage deliberately does not debit it. Token-file entries
provisioned bytoken_sha256digest are now named in the boot log, since their length can
never be verified against the 32-character floor. host.docker.internalno longer counts as a local host for the default-credentials guard.
From inside a container it reaches the container HOST, which can be a shared machine. The
compose quickstart keeps working: the guard warns instead of refusing for exactly this host.- Duplicate file names in one
recall_ingestupload are refused instead of last-writer-wins,
and an oversized entry is refused from its encoded length before being decoded into memory.
Added
-
recall.errors.RecallErroris the common base of every deliberate exception. Sixty-five
exception classes existed with no shared root, so a consumer could not write
except RecallErrorand had to enumerate families or catch built-ins. Every family keeps its
historicalRuntimeError/ValueErrorbase first, so existing handlers keep working, and a
structural test walks both packages so a new family cannot silently opt out. -
The ATM-Bench harness that produced the published run is now in this repository, byte for
byte.benchmarks/atm_full_run.pyandbenchmarks/atm_bench.pyare copied from the run's own
commit without a character changed, hashed inresults/atm/atm_harness_20260823.json, and pinned
bytests/test_atm_runner_published.py, which fails on a single changed byte. The leaderboard
submission had pointed at a commit that was on no public branch, so the code behind a published
number could not be read, let alone checked.Both files are exempt from
ruffandmypywith the reasons stated beside the exemptions: a
style fix would make the published harness a different program from the one that ran.⚠️ This publishes the harness, not the run.recall/has moved since, the answer model is a
moving alias, and the dataset stays outside this tree, so a re-execution reproduces the method
rather than the last decimal.docs/ATM_BENCH.mdsection 6 states all three.
Changed
-
The wheel no longer ships the one-off research drivers.
recall/evalkeeps its
load-bearing slice (the wizard's calibration engine, the documentedlabelledCLI, query
generation, and the sample corpus and labels the README points the wizard at) and drops the
session scripts, benchmark drivers and result fixtures that never worked from a wheel;
recall/wizard/llm.py(a preregistered experiment arm imported by nothing but its test)
stays repo-only until that experiment resolves. Working from a clone is unaffected. -
Benchmark-harness tests carry a
benchharnessmarker (1,011 of 6,591), so a
product-only run ispytest -m 'not benchharness'. CI behavior is unchanged. -
recall quickstartgoes from a freshpip installto three answered queries in one command,
database included. It starts a throwaway pgvector container on a free port, applies the schema,
indexes a corpus that ships inside the wheel, and runs three queries chosen to show the retrieval
contract rather than to flatter it: one answerable, one whose nearest match is a claim that was
later retracted, and one it refuses.--removestops the stack and destroys its volume,
--existing-dsnskips Docker for anyone already running PostgreSQL.It deliberately does NOT calibrate, register an MCP server, or build the
recall-wizardimage
the full installer builds. Those are the slow steps, and each is printed as a named next command
instead. Every result carriesDEGRADED:INDEX_NOT_READY, which is explained in the output rather
than suppressed: the corpus genuinely has no calibration, and hiding that in the demo would
misrepresent the one property the project is about.⚠️ recall demois not a substitute and cannot be. It indexes the relative pathcorpus,
which exists only in a git clone, so from a PyPI install it indexes nothing.quickstart
resolves its corpus from the installed package.
Fixed
-
RetrievalDiagnosticsreportsmax_dense_score. The published ATM harness records
the best dense cosine per question, and that field had only ever existed on the run's own private
branch: publishing the harness without it would have published a program that raises
AttributeErroron its first retrieval record. Additive, defaulted toNone, and populated at
both of the retriever's construction sites. -
The unanswerable query in the demo corpus did not abstain.
recall demo's "llamas on mars"
scores a top cosine of 0.505 against the 0.50 development threshold onrecall/eval/corpus,
so it answers, out ofsecrets_handling.md. It was the obvious query to reuse forquickstart
and reusing it would have captioned an answer as a refusal. Replaced by measurement with one that
clears the threshold by 0.054, andtests/test_quickstart.pynow asserts the margin rather than
the boolean, so drift toward the edge is visible before it flips. A query's absence from the
corpus does not imply abstention; only the cosine does.