One neutral feed in.
One executed artifact out.
Every arm receives the same recorded experience, works the same tasks in the same sandbox, and is graded by the same checkers. The only variable is the memory layer. Everything below is enforced by the harness, not by convention.
The pipeline
harness/The corpus is fed to every adapter as identical bytes. Each product ingests it through its own published write path, so what its extraction pipeline keeps, and what it throws away, is part of what is measured. The agent then works each task in a fresh sandbox with that arm's integration installed and nothing else.
Before a session is scored it must pass the admission gate. After it, the artifact the agent left behind is run against an executable checker whose oracle inputs the sandbox never contained. The result, with every token spent on both ingestion and the session, lands in one per-arm ledger.
The arms
what each one removesAn arm is not a contestant. Each one exists to remove a competing explanation for what the product does, so that a difference at the end has one story left that fits it. Read down the table and the design is the argument: by the last row, "the memory layer did it" is the only surviving account.
| arm | what it is | the explanation it removes |
|---|---|---|
| bare | no memory layer, no CLAUDE.md, nothing but the repository and the prompt | "the task was solvable anyway." It is the floor, it runs in every cell, and
damage is defined against it: an arm damaged a cell when it failed
what bare solved. Without it, harm has no referent |
| placebo | project-shaped prose with no memory content, matched to the baseline bundle on line count and whitespace tokens | "any extra context would have helped." If placebo moves the number,
the treatment was volume of plausible text, not retrieval. This is the
control most memory benchmarks omit, and omitting it is how context stuffing gets
reported as memory |
| claude_md | a curated static instruction bundle, the file a careful team already maintains | "a memory product beats having nothing." The honest competitor is not nothing, it is a good README. It costs zero tokens per query and never goes stale mid-session, so a memory layer that cannot beat it has not earned its retrieval budget. It is the baseline, and every delta is quoted against it |
| fs_grep | the whole corpus on disk, and grep | "a memory product beats searching the transcripts." The cheap answer costs no index, no embedding and no server. A product is worth its infrastructure only if it beats this, and it is a non-memory retrieval baseline, so a gap here is about the memory layer rather than about having the text at all |
| recall | MCP server, 8 read and navigation tools including its reasoning and graph surface | product under test |
| qualified hook arm | an official lifecycle-hook integration with its own pinned plugin and local service | treatment presence and hook timing are checked, but the arm is held out of the public product ranking until its disclosure window and preregistered comparison are complete |
| recall_prefetch | the same retrieval, run in the harness with the task prompt already in hand | "the product cannot find it." Query formulation is removed, so this is the ceiling the live arm is reaching for. It is a reference track, never ranked |
Write tools and write-side hooks are withheld. The product ships them and would use them, but the corpus has to be frozen across arms and cells: a session that wrote would change what the next seed reads, and one that solved a task could write its answer where the next cell retrieves it. That limitation is stated in the scope line above every ranking rather than in a footnote: this measures retrieval, not memory formation, and gives no credit for extraction or consolidation at write time.
Each product carries its own shipped instruction. Equalising the text measures a denominator no vendor ships; this measures what a user installs. Per-arm instruction sizes are published with every run so the asymmetry is visible.
Tasks
tasks/<id>/ · oracles/<id>/A task is a fixture repository, a task spec, and an executable checker. Success depends
on something learned in earlier sessions: a recorded decision, a convention, a constraint
that lives in the corpus and not in the prompt. The suite holds 34 tasks
(identifiers like ts-tz-utc, ts-stable-sort,
ts-log-mask). Three of them, the xs- set, state their governing
rule across two sessions rather than one, so no single document answers them: a suite where
combining sessions is never necessary cannot detect a product that combines them.
Every task ships two reference solutions, asserted in CI on every commit:
| reference | encodes | must |
|---|---|---|
| naive/ | the plausible solution an agent produces without the recorded knowledge | fail the checker |
| informed/ | the solution that uses what the corpus knows | pass the checker |
If the naive solution passes, the task is not measuring memory and is rejected. A do-nothing session scores zero by construction: there is no partial credit from a judge, because there is no judge.
How one cell actually runs. A cell is one task at one seed, and every
arm runs it. Take ts-tz-utc, whose governing fact is a timezone convention
this project settled months ago and wrote down nowhere in the code.
- The corpus is ingested once per condition, before any session, through each product's own write path. It is then frozen: no arm writes to its store again for the rest of the run.
- A fresh sandbox is built from the task's fixture repository. It contains the code and
nothing else: no oracle, no reference solution, no corpus on disk. The sandbox cannot
reach the benchmark repository, because a single
cd ..would otherwise reach the answers. - The agent gets the prompt and that arm's integration, and only that arm's. The prompt never states the governing fact. It is answerable from the corpus, or from nothing.
- The session ends. Before it can be scored it passes the admission gate, which checks that the arm's tools were actually listed at session init and that no arm held another arm's tools.
- The checker runs the artifact against oracle inputs the sandbox never contained. For
ts-tz-utcthat means feeding timestamps whose correct handling depends on the convention, and comparing output. Pass or fail. That is the score.
The two reference solutions are what make the task about memory rather than
about competence. naive/ is the good-faith answer an able engineer writes
without the recorded knowledge, and CI asserts it fails. informed/
uses the recorded fact and CI asserts it passes. If the naive solution ever
starts passing, the task has stopped measuring memory and is rejected rather than kept.
The official run works the subset that carries planted corpus conditions: 73
task-conditions in all. One task cannot express contradictory
observably and says so in its own directory rather than being quietly dropped.
The corpus
corpus/ · sha256 manifestThe experience feed is verbatim recorded agent session transcripts, pinned by a sha256 manifest. It is deliberately not a curated fact list: it carries the noise, the dead ends and the distractor sessions real agent history carries. No arm gets a different feed, and the only asymmetry a product can gain is what it extracts.
It is 4,900 documents per condition, and it was made harder on purpose.
The 196-document feed every earlier run used was saturated: hit@10 was 1.000, so every
memory arm found the governing session every time and the grid could not separate "the
product retrieved badly" from "the agent never searched". On the current corpus
bm25 hit@1 is 0.182 against 0.485, and voyage hit@10 is 0.879
against 1.000. Containment is checked against the built corpus rather than trusted: no fact
term of any task appears in any synthetic document, or the absent condition
would be silently broken for that task.
That change breaks comparability. No number measured on this corpus may be differenced against a number from any earlier run.
The harm suite
does memory ever make it worse?Every task described above places its governing fact in the corpus, so that suite can only ask whether memory helps. It is structurally incapable of detecting harm, and a layer that helps 20% of cells while harming 15% publishes the same headline as one that helps 20% and harms 2%.
So a second suite varies what the corpus contains, never what a product
does about it. Every arm ingests identical bytes under each condition. Whether a
system copes through supersession metadata, recency weighting, reranking, a refusal
threshold, or not at all, is the thing being measured rather than the thing assumed. Damage
is defined against the memory-free arm: failing a cell that bare solved.
| condition | the corpus holds | correct behaviour | damage signature |
|---|---|---|---|
| absent | no governing fact for this task | solve from the repository, or say it is unknown | invents a convention and applies it |
| superseded | the old fact and the newer one, both dated | apply the current fact | ships the stale convention |
| contradictory | two undated memos that disagree, neither marked | surface the conflict rather than choose | chooses silently |
| adjacent | a confident, high-similarity memo governing a different subsystem | recognise that it does not apply here | applies the other subsystem's rule |
| present | the governing fact, plainly, with nothing done to it | find it and apply it | none; this is the condition memory should win |
The fifth condition exists because the other four all vary how the
evidence is bad, which made never searching a dominant strategy: an arm that ignored
its memory entirely forfeited nothing. present is the identity transform, and it
is what a degenerate strategy loses. A plant is measurable only if every reading of it gives a
different observable outcome, which is why one task carries two damage conditions rather than
four: its adjacent damage would be byte-identical to the factless answer. Eleven
tasks carry all four.
The admission gate
discard, never scoreSilent failure is the standing hazard of agent benchmarks: an MCP server that never attached, a hook that never fired, a sandbox missing its files. A session in that state measures nothing, and scoring it poisons the average in whichever direction luck chooses.
So a grid cell is discarded, not scored, unless every arm proves its treatment was applied:
| integration | required proof |
|---|---|
| MCP server | tools listed at session init, in the session's own record |
| lifecycle hooks | hooks demonstrably fired, with output |
| sandbox files | digest-verified against the frozen bundle |
| isolation | no arm holding another arm's tools |
Discard counts are published per arm, so a product that only runs cleanly half the time cannot hide it in a smaller denominator.
Two consequences the gate cannot state for itself. Only an arm
with a memory surface can fail to wire, so the rule protects one class of arm's worst outcome
and no other's, and every headline is published beside an intention-to-treat column over all
complete cells. And a startup failure is a transient, not an outcome: a preflight speaks the
protocol to the server before a session is paid for, and a session whose treatment failed to
wire is retried under a rule that reads the admission surface and never reads
success, the checker verdict, or anything the model did.
Scoring and costs
harness/costs.pyThe primary endpoint is task success: the checker passes or it does
not. Analysis is paired per task, arms are contrasted against the
claude_md baseline, and deltas below the preregistered minimum effect are
reported as noise rather than dressed up as findings.
Costs are end-to-end. Ingestion tokens, session tokens, wall time and negative-transfer counts land in one per-arm ledger beside the success rate, because a layer that buys two points for triple the tokens is a different product than its headline suggests. No arm has been run at a matched budget, so the ledger carries success per million tokens and reports the asymmetry rather than smoothing it.
Two details that sound like bookkeeping and are not. Input is not one price: fresh input, cache reads and cache creation are metered separately, because one arm's input can be two-thirds cache reads while a baseline's is under half, and a single rate then overstates spend unevenly between exactly the two arms being compared. Prices are stated, never defaulted: a live run refuses to start without them. Compare runs on tokens.
An arm that ingests with a model on the benchmark host reports zero hosted tokens and names the model, so its zero is never read as zero cost.
Diagnostic reference tracks
unranked, by designTask success says whether the memory path worked, not which part failed. A memory arm can
lose three ways: the agent never searched, it searched badly, or the store did not hold the
answer. recall_prefetch separates the first from the rest by running retrieval
in the harness with the exact task prompt and injecting what comes back. It is
in the official run, as an upper bound; it is never ranked against the products, because an
arm that cannot lose does not belong in a ranking.
oracle_memory, which injects the exact evidence and removes
retrieval altogether, is not in the run. Its bundles are keyed by task and
carry no corpus condition, so under absent it would supply an answer that
condition is defined not to contain. It returns when its bundles are condition-aware. The
contrasts below are the decomposition it belongs to, stated so its absence is a recorded
choice.
Oracle headroom: how much this task set can reward correct evidence at all.
Natural memory lift: the product as an agent actually experiences it.
Prefetch memory lift: retrieval quality with query formulation removed.
Access gap: everything lost between perfect evidence and the live memory path.
Prefetch gap: how much is lost to the agent's own decision to search, and its query.
A gap is evidence about a causal path, not a claim that a product is first or unique. Diagnostic arms never enter the product ranking.
Why this is a test of memory
and not of retrieval proseMost memory benchmarks ask a model questions about synthetic conversations and score the answers with another model: a needle someone planted, found under a judge whose failure modes correlate with the thing being judged. Five properties here are chosen against that.
The grade is execution. A checker runs the artifact against oracle inputs the sandbox never contained. No partial credit, no rubric, no judge in the primary endpoint, so a fluent wrong answer scores what a silent one does: zero.
The corpus is raw. Verbatim recorded sessions, dead ends and noise included. Distilling signal from that stream is what a memory product claims to do, so the benchmark refuses to do it for anyone. The only asymmetry available is what a product keeps.
Harm is measured. A suite where every governing fact sits in the corpus can only ask whether memory helps, and a layer that helps 20% of cells while harming 15% publishes the same headline as one that helps 20% and harms 2%. The corpus conditions ask whether the product notices stale, contradictory and off-subsystem evidence.
The comparison is paired. Every arm meets the same task, seed and fixture, so a cell is a within-subject comparison rather than a difference of averages across arms that met different work. Arm order is randomised per cell and recorded.
Treatment is proved. A stdio server that fails to start is invisible in a transcript: no memory tool calls, which looks exactly like a model that chose not to search. The gate discards any cell where an arm cannot prove its treatment applied.
The result is a benchmark that can return a null and survive it. If memory layers do not beat a good static instruction file on real coding work, that is the finding, and it is published as readily as the opposite.
Preregistration
preregistration/Every measured run is preregistered: the question, the predictions, the endpoints, the contrast families, the exclusion rules and the sizing are committed before the first session starts. The run scripts enforce the mechanical half: they refuse to start while the preregistration directory is dirty.
Never edit a number in a committed preregistration. Not a prediction, not a measured value, not a date. Append a correction underneath.
Results are appended below the frozen prediction, under a marked line, in the same file, so prediction and outcome are read together.
Falsified predictions stay. The gap between expected and measured is the only part of a result that teaches anything.