1. Executive Summary
XERJ is a local search engine written in Rust — Apache-2.0, about
380,000 lines across sixteen crates, shipping as a single static binary
with an Elasticsearch-compatible HTTP surface. Agent memory is one API
on that engine: /_memory/{namespace}, five endpoints,
implemented in a 2,306-line adapter that does not re-implement search. A
namespace is a reserved index (.xerj-memory-{ns}), so store
and recall reuse the same BM25, dense_vector and
metadata-filter paths the rest of the engine serves.
That framing is the design's best idea and the source of its sharpest defect, and both are worth reading.
The best idea is that memory built on a real search engine inherits
things memory libraries usually reimplement badly. Recall has four modes
— an explicit caller vector, server-side semantic
embedding, plain BM25, and hybrid: true fusing the last two
by reciprocal rank — and all four run offline, because the built-in
embedder is lexical feature hashing rather than a model. A write is
visible immediately: IndexDocParams.refresh is documented
as "accepted without error; memtable is always visible", so there is no
refresh lag between storing a memory and recalling it. And the tenant
boundary is enforced where a search engine actually leaks, which is not
at the memory API at all.
The defect is in the graph layer above it. Edges between memories
carry a genuine bitemporal record — valid_at,
invalid_at and created_at as three separate
fields, with the first two independently settable by the caller. Edge
identity is xxh3_128(src, type, dst, valid_at), and
valid_at for a detector-taught edge is the source file's
mtime. Invalidation, though, is keyed on the edge id:
DELETE /_graph/{brain}/link/{edge_id}. Save the file again
and the mtime changes, the hash changes, and the same
(src, type, dst) claim is re-taught under an id the
invalidation never covered. The record of the rejection survives; what
it rejected comes back beside it.
Four marks. bitemporal is earned on two clocks filled
from different sources rather than from one now, which is
the distinction the mark turns on. scope_enforced and
negative_eval rest on the same test file, which is unusual
enough to describe precisely in section 10. audit_log is
earned on a hash-chained log that states its own coverage boundaries in
its module doc. tombstone, trust_state and
human_review are withheld, each for a reason given in
section 9.
2. Mental Model
A memory is a document in an index named after its namespace. There
is no extraction step, no consolidation pass, and no model in the write
path: what the agent sends is what is stored, plus a
stored_at timestamp. The only write-time judgement
available is dedup: true, which probes for the single
nearest existing memory and skips the write when its cosine score meets
a threshold defaulting to 0.95 — returning
{created: false, deduplicated: true} with the existing
id.
The epistemic machinery is not on the memories. It is on the edges between them, which is what makes this system worth reading beside simpler stores. An edge asserts that one node relates to another, from some instant onward, as judged by some detector, with evidence attached. It can later be judged to have stopped being true — soft-invalidated, never deleted, so a reader can still ask what the graph believed last Tuesday.
Two clocks carry that. valid_at is when the fact became
true; created_at is when the system recorded it. For an
edge a detector taught, those come from different places:
valid_at is the source file's mtime and
created_at is the indexing run's wall clock. The autoindex
module says so where it builds the document — created_at_ms
is "the one non-deterministic field and never participates in
edge_id". Invalidation adds the mirror pair:
invalid_at is when the fact stopped being true,
caller-settable; expired_at is always server now.
The gap is that identity is derived from one of those clocks and
invalidation is keyed on identity. An edge is
xxh3_128(src, type, dst, valid_at). Unlinking stamps
invalid_at on that id. Re-detecting the same relationship
from a file whose mtime has moved computes a different id, and the new
edge is written with invalid_at omitted — which, by the
module's own stated discipline, is the signal for "still valid".
Diagram source
%% caption: an edge's identity hashes the source file's mtime into itself, so a person's invalidation — keyed on that identity — stops applying the moment the file is saved again, and the same claim returns live beside the rejection that still describes the old id
flowchart TD
F["a.md saved at mtime T1"] --> D["detector reads the file"]
D --> E1["edge id = hash of src, type, dst, T1<br/>valid_at = T1 (file mtime)<br/>created_at = run clock<br/>invalid_at omitted"]
E1 --> L["live: matched by as_of >= T1"]
L --> U["person judges the claim wrong<br/>DELETE /_graph/b/link/{id}"]
U --> I["same document re-indexed<br/>invalid_at set, expired_at = now<br/>nothing is deleted"]
I --> H["excluded from ego and overview<br/>returned again by include_expired"]
H --> S["a.md saved again, mtime becomes T2"]
S --> D2["detector reads the file again"]
D2 --> E2["edge id = hash of src, type, dst, T2<br/>a DIFFERENT id<br/>invalid_at omitted again"]
E2 --> L2["live: the same claim, rejected once,<br/>asserted again under a new identity"]
H -.->|"the rejection is still on disk,<br/>keyed on an id nothing now produces"| L23. Architecture
One static binary, no JVM, no second datastore.
xerj --data-dir ./data starts a node; the memory API needs
nothing else running. The sixteen crates split the engine
(xerj-engine, xerj-storage,
xerj-fts, xerj-vector,
xerj-query) from the surfaces over it
(xerj-api, xerj-mcp,
xerj-console-api, xerj-server), and agent
memory lives entirely in xerj-api:
memory_api.rs (2,306 lines), graph_api.rs,
authz.rs and audit_mw.rs.
What an operator stands up is therefore a search node, with a search node's costs and a search node's operational surface. A namespace costs one index. The audit log is one bounded file in the data directory. Embedding is in-process and offline by default — the built-in embedder is lexical feature hashing, and the neural one is opt-in and downloads roughly 90 MB — so recall has no external dependency and no per-call cost to anything outside the box.
Authorization is two layers, and the second one is the point.
auth_middleware resolves an API key to a
Principal; authz_middleware classifies the
request path into a target and checks the privilege. A
Principal::Scoped carries role descriptors naming index
patterns. Because a namespace is an ordinary index with a leading
., the memory data is reachable through the generic
ES-compat surface and the native router as well as through
/_memory, and the authorization layer has to cover all of
them — which is exactly what the test described in section 10 walks.
The console is a separate application merged onto the engine routers
after their layers are applied, so
authz_middleware never runs on
/_xerj-console/*. That is stated in the console test's own
header, and the fix is in the proxy rather than in the middleware: the
data-sources handlers refuse the reserved namespace themselves,
returning the same 404 they give .xerj_*
system indices so existence does not leak.
4. Essential Implementation Paths
- Store —
memory_api::store(:338): validate the namespace, authorizeWriteIndex, reject an entry with neithertextnorvector, create the backing index lazily with adense_vectorsized to the supplied embedding, run the dedup probe when asked, thenes_compat::index_docwith{text, stored_at, metadata?, vector?}. - Recall —
memory_api::recall(:530): authorizeReadIndex; reject a body that is not exactly one ofvectororquery; resolve the fusion strategy; over-fetch when recency blending or graph blending is on; authorize the edges index separately whengraphis present; fold a graphrestrictexpansion into the filter as anidsclause; thenes_compat::search. - Namespace validation —
validate_namespace(:106): lowercase, digits,_,-and.only, no.., 200 characters, and a refusal of any namespace ending-edges, because brainBkeeps its edges in.xerj-memory-{B}-edgesand a namespace calledkb-edgeswould write memories into another brain's edge index. - Edge assertion —
graph_api::link(:407): validate, authorize, reject self-edges, clampweightandconfidenceto[0, 1], parsevalid_atandcreated_atindependently, derive the id, write withinvalid_at/expired_atomitted. - Edge invalidation —
graph_api::unlink(:567): authorize before the existence probe so an unauthorized caller cannot tell "not yours" from "not there", read the document, refuse a second invalidation by reportingalready_invalid_at, re-index the same id with the pair added. - Audit —
audit_mw::audit_middleware(:356): classify the request, resolve the subject before the handler for a write and only on a 403 for a read, append op, subject, resource, outcome and note.
5. Memory Data Model
A memory document is small and flat:
{ "text": "...", "stored_at": 1757790000000, "metadata": {...}, "vector": [...] }
text is mapped as semantic_text, so the
engine auto-embeds it at ingest into a companion vector and server-side
semantic recall works without the caller embedding
anything. metadata is deliberately not explicitly
mapped, so arbitrary agent-supplied keys are accepted. There is one
timestamp. There is no namespace field on the document — the namespace
is the index name — and no status, source, or confidence field of any
kind.
An edge document is where the modelling effort went:
{ "edge_id": "...", "src": "...", "dst": "...", "type": "mentions",
"weight": 1.0, "confidence": 0.95, "detector": "wikilink@1",
"valid_at": 1753600000000, "created_at": 1757790000000,
"schema_version": 1, "src_file": "alpha.md",
"evidence": { "quote": "...", "source": "alpha.md", "offset": 0 } }
Three discipline decisions are worth copying.
invalid_at, expired_at and
src_file are omitted rather than set to null when
unset, because omission lands the row in the doc-values null bitmap, and
that bitmap is the "still valid" signal the hop reads. Scalars are plain
strings and numbers rather than nested objects, so the prefilter rides
doc values. And schema_version is on every row.
confidence is the field to look at twice. It is
validated, clamped, stored and returned in the ego response. A tree-wide
search for readers outside the writer, the autoindex and the MCP proxy
returns nothing: no query filters on it, no ranking consults it, no
threshold compares against it. The ranking uses weight.
confidence is a number the system records and never acts
on.
6. Retrieval Mechanics
_recall rejects unknown keys with a 400 under
deny_unknown_fields, and the comment says why: a typo'd key
used to be silently ignored, degrading the request to a match-all that
returned arbitrary memories at score 1.0. The same reasoning produced
the hard 400 on a body with both vector and
query, or neither. For a store an agent is meant to trust,
turning a malformed recall into a refusal rather than into plausible
noise is the correct trade.
Four single modes, tried in order: an explicit vector
runs kNN; semantic: true embeds query
server-side with the same embedder used at store time; plain
query runs BM25; and hybrid: true runs the
BM25 and semantic legs over the same query and fuses them
through the engine's hybrid query type, reciprocal rank by
default or fusion: "linear" on request.
fusion: "learned" is a 400 rather than a silent
substitution of RRF, "because substituting RRF would misrepresent the
ranking asked for".
filter is a standard ES clause applied as a
bool filter, so it narrows without scoring.
recency_weight blends
(1 - w) * norm_score + w * norm_recency with both terms
min-max normalised across the candidate set, and over-fetches
max(k * 4, 50) candidates first — because a re-rank can
only promote what was fetched.
Graph coupling has two modes. restrict expands the seeds
first and folds the reachable id set into the filter as an
ids clause, capped at 10,000 so the prefilter stays
bounded; recall itself is unchanged, it just runs over a graph-bounded
universe. blend lets graph proximity pull related memories
up, over the same widened candidate pool. Hops are capped at 1 or 2, and
a 3-hop request returns a 400 carrying a fixed sentence about not being
a graph database. A missing edges index is not an error — recall
proceeds ungated and flags it in-band, so an agent can opt into graph
recall before its first link exists.
7. Write Mechanics
Writes are synchronous and singular. One POST stores one
memory; there is no batching endpoint on the memory API, no queue, and
no background consolidation pass that rewrites stored memories. The
agent blocks for the duration of one index call.
The lag before a memory is retrievable is zero, which is unusual for
an ES-compatible surface and is stated in the parameter's own
documentation: refresh=true|wait_for is "accepted without
error; memtable is always visible". A memory stored in one turn is
recallable in the next with no refresh call.
dedup is opt-in and best-effort by construction. Any
probe error — an empty index, a schema mismatch — is treated as "no
duplicate", so dedup never blocks a legitimate write. It is off by
default so existing callers keep storing every entry.
The background writer is the autoindex daemon, and it writes edges
rather than memories. Eight deterministic detectors run with no model in
the path; the same corpus produces a byte-identical edge set on every
run given unchanged mtimes. The reconcile pass soft-invalidates edges
taught by files that left the corpus and edges pointing at a superseded
anchor, and a #868 assertion pins that an unchanged file's
own edges are left alone.
8. Agent Integration
xerj init wires the node into Claude Code or Cursor in
one command. The MCP server exposes eleven tools, six of them
memory-shaped: xerj_memory_store,
xerj_memory_recall, xerj_brain_ego,
xerj_brain_link, xerj_brain_unlink and
xerj_brain_overview.
Two details of that surface matter. The namespace is a model-supplied argument on both memory tools — the model names the scope it reads and writes — and what keeps that honest is that the credential the MCP server holds constrains which namespaces the request can reach at all. The scope key is on the credential, not in the arguments, so a model that asks for someone else's namespace gets a 403 rather than their memories.
The second is xerj_brain_link's schema, which exposes
valid_at to the model with the description "When the fact
became true: epoch-ms number or RFC3339 string (default now). Part of
the deterministic edge_id." It does not expose created_at;
build_brain_link forwards exactly evidence,
weight, confidence and valid_at.
So through MCP the agent chooses when a fact became true and the server
keeps when it was recorded — which is the clean division the bitemporal
mark asks for, achieved by leaving one field out of a tool schema.
9. Reliability, Safety, and Trust
Scope — earned, and not on the partition. A
namespace being its own index is a physical partition, which on its own
is a different mechanism from this mark. The predicate is above it: all
five /_memory handlers call
authorize_memory_namespace first, and a wildcard read is
expanded over the principal's visible set rather than refused. The
module comment records why refusal was rejected — it "cost every
legitimate logs-* grant" — and expanding is both usable and
closed.
Worth flagging for a reader who greps before running:
memory_api.rs's own "Authorization model" header says there
is "no per-namespace authorization" and that any
credential reaching the node can read and drop every namespace, pointing
at "the deferred RBAC enforcement" as the fix. At this commit that
paragraph understates the code beneath it — the five handlers authorize,
the edges index is authorized separately for graph coupling, and the
security-boundary suite asserts the denials with a minted non-admin key.
An operator who reads the warning and deploys accordingly would be
provisioning around a limitation that the tree no longer has.
Audit — earned, with the ring stated. Hash-chained,
restart-surviving, GET /_audit/_search and
/_audit/_verify. Reading the audit log gets its own op tag
rather than the generic search, "a refused attempt to read
the evidence is the denied read an auditor most wants to find, and it
must not read like a denied data search". The module doc is candid about
coverage: successful reads other than _search leave
nothing, unauthenticated attempts are not recorded because the
middleware runs inside authentication and otherwise anyone reaching the
port could flush the ring, and nothing older than 4,096 entries
survives.
One imprecision. _recall is absent from
is_read_shaped, so a recall falls into the mutating branch
and takes an entry with op memory.post on resource
_memory — the endpoint, not the namespace. The useful entry
for the same request comes from es_compat::search instead,
which appends op: "search" against the real
.xerj-memory-{ns} index name. An auditor gets the
namespace; they get it from the second entry, and the first spends a
ring slot recording a read as a write.
Tombstone — withheld, and this is the report's main
finding. The mark asks for a durable record of a rejected
value, keyed on the value, so later extraction cannot re-assert
it. unlink is keyed on the edge id, and the edge id is
xxh3_128(src, type, dst, valid_at) where
valid_at for a detector-taught edge is the source file's
mtime. Save the file and the id changes. The invalidated row stays on
disk, correct and queryable through as_of, describing an
identity nothing will compute again, while the same
(src, type, dst) claim is re-taught live beside it. A full
re-detection has the same effect by a second route:
detect::assemble builds each document with
invalid_at omitted and writes it with a bulk
index action, which is a full replace of whatever sat under
that id. No test in the tree covers a manually unlinked edge surviving a
re-index; the search is in the appendix.
This is worth separating from the reconcile sweep, which is sound. Edges taught by a departed file, and edges pointing at a superseded anchor, are invalidated on a fresh run, with a test carrying an explicit fail-before note. The gap is specifically between a human judgement about a claim and an identity derived from a file's timestamp.
Trust state — withheld. confidence is a
float, and a score is not a state; more to the point it is read by
nothing, so it does not even rank. A tree-wide search for a discrete
status on a memory or an edge returns only a console user account's
Pending.
Human review — withheld. There is no approval
surface. A search for approve, adjudicat and
curat across the console source returns nothing, and the
console's relationship to memory is the opposite of review: the
data-sources proxy is required to refuse the reserved namespace
outright.
10. Tests, Evals, and Benchmarks
brain_is_a_security_boundary.rs is 692 lines and is the
artifact to read. Its header explains that it is "the inverted
descendant of brain_is_not_a_security_boundary.rs, which
pinned the measured fact that one did not exist" — a committed
test that asserted the system's own absence of a boundary, later
rewritten assertion by assertion into its opposite. The predecessor is
not in the tree at this commit.
Its reasoning about scope is the part worth stealing: "the exposure
was never about the four /_graph/* handlers", because the
edges live in an ordinary index that IndexName::validate
admits, reachable through the ES-compat surface and the native router.
"An access check on /_graph/* alone would have left all of
that open — a boundary that only looks like one. So this test walks
every door, and a fix that closes only some of them fails here." The
doors include the percent-encoded spelling
/%2Exerj-memory-bob-edges/_search, "in case the check
compared raw path text".
It is not vacuous, and the guard is structural.
seed_two_brains writes a real edge into each of two brains
and refreshes bob's backing index, so the row is demonstrably searchable
before anything asserts it cannot be seen.
a_scoped_caller_retains_full_use_of_its_own_brain is a
dedicated positive control asserting alice's own edge comes back.
cat_indices_keeps_the_rows_it_should asserts both halves at
once: the caller's own index is present, and exactly one row
was returned.
One honest limit. The four wildcard assertions in section 2b are of
the form !resp.to_string().contains("bob") with the status
asserted 200, and no assertion in that loop that the expansion returned
any of alice's own rows. An expansion that returned nothing would
satisfy them. The positive control for that pruning path exists on
_cat/indices rather than on _search.
soft_invalidate_time_travel is the bitemporal case and
carries its own controls: the belief before the invalidation
instant is asserted at four edges with
expired_excluded == 0, the belief after at a specific
three-id list with expired_excluded == 1, and
include_expired: true returns the fourth carrying its pair.
Fixture instants are pinned rather than now "so a link and
the as_of that reads it back can never land in the same
millisecond".
The graph detectors carry a measurement rather than a benchmark
score. Over 240 arXiv PDFs in 24 topic folders, the seven structural
detectors produced 11,854 edges and sharedterm added 462,
and the project reports that a random pair of those documents shares a
top-level topic 17.0% of the time while shared_term links
do so 35.9% — "about twice chance. Read that as the honest strength of
the signal — useful, and no substitute for reading the evidence on the
edge." The same passage instructs against overclaiming its own feature:
"Describe it as shared vocabulary, never as semantic understanding."
No paper describes XERJ. A search of the README and
docs/ for arxiv, bibtex,
@article, citation and doi.org
returns only the arXiv corpus the detectors were measured against, and
there is no CITATION.cff. The suite was not run here: 21
dependency manifests changed within the screening cooldown.
11. Patterns Worth Stealing
Steal
- Two clocks from two sources. The reason the
bitemporal record here is real and not decorative is that
valid_atcomes from the file's mtime andcreated_atfrom the run's clock. A schema with both columns filled from onenowlooks identical and answers nothing. - Leave the record clock out of the agent's tool
schema.
xerj_brain_linkexposesvalid_atand notcreated_at. The agent says when the fact became true; the server says when it heard. One omitted property enforces the division. - Walk every door, including the encoded spelling. The security test's premise — that the interesting surface is the generic one underneath, not the feature-named handler — generalises to any system whose memory is stored in something addressable by another name.
- Refuse the malformed recall.
deny_unknown_fieldsplus a hard 400 on an ambiguous body, adopted because the lenient version returned arbitrary memories at score 1.0. - Omit rather than null.
invalid_atabsent puts the row in the null bitmap, which is the liveness signal, so the query needs no sentinel comparison. - Publish the signal's strength against chance. 35.9%
versus 17.0% says more about whether to trust a
shared_termedge than any pass/fail gate would.
Avoid
- Deriving identity from a mutable input, then keying
rejection on identity. Everything about the invalidation design
is right except what it is keyed to. Had
edge_idexcludedvalid_at, or had invalidation been keyed on(src, type, dst), the human judgement would have survived a file save. - Storing a confidence you never read. Validated, clamped, persisted, returned, and consulted by nothing.
- Letting a module's authorization comment rot. Here it rotted in the safe direction — the doc is more pessimistic than the code — but a reader's deployment decisions come from that paragraph.
- Classifying a read as a write by omission from a
list.
_recallmissing fromis_read_shapedcosts a ring slot per recall on a 4,096-entry log shared by the whole node.
Fit
Take this if you already want a search engine. The memory API is a thin, well-made adapter over one, and the whole argument for it is that you avoid running a vector database next to your search stack — which is only a saving if the search stack was going to exist. Adopting a 380,000-line Rust engine to get five memory endpoints is the wrong trade, and nothing in the repository argues otherwise.
The fit is strongest for a single operator or a small team running one node over their own corpus, wanting recall that works offline with no embedding service and no refresh lag. It is good for multi-agent setups on one trust boundary, where namespaces separate agents and the credential model keeps them separated. Be more careful with mutually distrusting tenants: the enforcement is real and tested, but the console had to be fixed separately because it bypasses the middleware entirely, and that is the shape of a boundary that has to be maintained at every new surface rather than enforced in one place.
Walk away if you need epistemic state on memories themselves. There is no status, no verification, no review, and no tombstone — the machinery for believing and un-believing things lives on the edges, and it is keyed on an identity that a text editor can change.
12. Antipatterns / Risks
- A rejected claim returns under a new identity. The
central finding, in section 9. Saving the source file re-teaches the
edge with a new
edge_id, and theinvalid_atstamped on the old one does not apply to it. - A re-detection overwrites an invalidation outright.
detect::assembleemits a full document withinvalid_atomitted, written with a bulkindexaction. Themust_not exists invalid_atquery in the autoindex is a counting query for the CLI summary, not a guard on the write path. - The audit ring is shared and small. 4,096 entries for the whole node, with every recall taking one. A busy agent evicts the security events the log exists for.
- No
fsyncper audit entry. Stated plainly by the module: the log survives a process restart includingkill -9, and a machine power loss can take the unflushed tail.sync_to_diskexists for callers that want the barrier, and the API-key operations use it. confidenceinvites a trust decision the code does not make. A reader seeingconfidence: 0.4on asamediredge may reasonably assume something filters on it.- Memory deletion is hard deletion.
forget_onedeletes the document. Unlike the edges, there is no soft-delete and noas_offor memories — the bitemporal care stops at the graph layer.
13. Build-vs-Borrow Takeaways
Borrow the whole thing if a search node is already in your architecture; the memory API costs one index per namespace and nothing else to operate.
Borrow the edge schema regardless of the rest. Three date fields with
the omission discipline, a schema_version, evidence carried
on the edge, and a detector tag is a good shape for any store that wants
to answer what it believed at a past instant — provided you key identity
and invalidation on the same thing, which is the one correction to make
while copying.
Borrow brain_is_a_security_boundary.rs as a template
even if you never run XERJ. The question it asks — what else addresses
this data, and does the check cover that name too — applies to every
memory system that stores its data in something with a general-purpose
API — a Postgres table, an S3 bucket, a shared vector collection.
Build your own if memories need status, review, or a durable rejection. Adding those here means adding fields to a document the engine treats as arbitrary and a read path that filters on them, which is work against the grain of an adapter whose whole premise is that it does not re-implement search.
14. Open Questions
- Is the invalidation gap known?
edge_idincludingvalid_atis deliberate and documented — "a DIFFERENTvalid_atcreates a distinct edge (bi-temporal ...)" — so the behaviour follows from a decision that was made on purpose for a different reason. Whether the interaction with manualunlinkwas considered is not recorded in the tree. - Will the stale authorization paragraph in
memory_api.rsbe corrected, and does anything in the project's process catch a module doc that describes a superseded posture? - Is
confidenceintended for a future filter, or is it a field that outlived its purpose? - Should
_recalljoinis_read_shaped, given that the search handler already audits the same request against the more useful resource name?
15. Appendix: File Index
Memory API
engine/crates/xerj-api/src/memory_api.rs— five endpoints, 2,306 lines (:106namespace validation,:338store,:530recall,:1257list,:1348forget,:1392drop)engine/crates/xerj-api/src/router.rs:885-891— route mounting
Graph and bitemporal record
engine/crates/xerj-api/src/graph_api.rs(:178-195edge id,:214-216mapping,:407link,:567unlink,:1237theas_oflive slice)engine/crates/xerj-engine/src/graph.rs— hop expansion,:69the hop cap sentenceengine/crates/xerj-autoindex/src/detect/mod.rs(:44-56edge id,:319the draft,:560-585assemble)
Authorization and audit
engine/crates/xerj-api/src/authz.rs(:140-200reserved namespace,:262-281the brain and namespace decisions,:588reserved privileges,:1277the middleware)engine/crates/xerj-api/src/audit_mw.rs(:108read-shaped ops,:148classify,:351-403the middleware)engine/crates/xerj-engine/src/audit.rs(:1-60the coverage statement,:89the capacity)
Tests
engine/crates/xerj-api/tests/brain_is_a_security_boundary.rsengine/crates/xerj-console-api/tests/console_cannot_read_a_brain.rsengine/crates/xerj-api/tests/snapshot_is_not_a_way_around_a_brain.rs,ml_datafeed_cannot_read_a_brain.rs,audit_records_writes_and_who.rs,scoped_keys_get_intact_responses.rsengine/crates/xerj-api/src/graph_api.rs:1687—soft_invalidate_time_travelengine/crates/xerj-autoindex/src/incremental_reconcile_http_tests.rs:3843
Docs
docs/recipes/agentic-memory.md,docs/SECOND_BRAIN.md,docs/design/SECOND_BRAIN_SPEC.md
Commands behind the absence claims
grep -rn 'confidence' engine/crates --include='*.rs' \
| grep -v 'graph_api.rs\|autoindex\|/tests/\|xerj-mcp'
grep -rni 'trust_state|"verified"|"approved"|"pending"|"rejected"|review_status' \
engine/crates --include='*.rs' | grep -v '/tests/'
grep -rni 'approve\|adjudicat\|curat' engine/crates/xerj-console-api/src --include='*.rs'
grep -rni 'tombstone\|do not re-add\|never re-assert\|deny.?list\|blocklist' \
engine/crates/xerj-api/src/memory_api.rs engine/crates/xerj-api/src/graph_api.rs
grep -rni 'unlink.*re-?index\|re-?index.*unlink\|manual.*invalid\|hand-invalidat' \
engine/crates --include='*.rs'
grep -rn 'invalid_at\|expired_at' engine/crates/xerj-autoindex/src --include='*.rs'
grep -rn -i 'arxiv\|bibtex\|@article\|@misc\|citation\|doi\.org' README.md docs/*.md CITATION.cff
find . -name 'brain_is_not_a_security_boundary*' -not -path './engine/target/*'
History
2026-09-13 — f54eead8…
— first reading. Screened first: 21 dependency manifests inside the
7-day cooldown (the tree was committed the same day), two
build.rs files cargo executes at build time, and two
uninstalled git hooks. Nothing was installed and no suite was run. Four
marks. bitemporal is earned on two clocks filled from
different sources — valid_at from the source file's mtime,
created_at from the indexing run — rather than from a
single now, with as_of applied as a range
filter on the read path and a test that asserts the belief both before
and after an invalidation instant. scope_enforced is earned
on the authorization predicate above the per-namespace index rather than
on the partition itself: a wildcard read is expanded over the
principal's visible set instead of refused. negative_eval
is earned on four suites asserting one tenant's memory does not come
back through any other door, with a separate positive control.
audit_log is earned on a hash-chained
audit.jsonl whose module states its own coverage
boundaries. tombstone is withheld because invalidation is
keyed on an edge_id derived from the source file's mtime,
so saving the file re-teaches the same claim under an id the
invalidation does not cover. trust_state is withheld
because confidence is a float that no query reads.
human_review is withheld because there is no approval
surface and the console is required to refuse the memory namespace
outright.