agent-memory-benchAMB / AMB
Method

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.

01

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.

02

The arms

what each one removes

An 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.

armwhat it isthe 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.

03

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:

referenceencodesmust
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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. The checker runs the artifact against oracle inputs the sandbox never contained. For ts-tz-utc that 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.

04

The corpus

corpus/ · sha256 manifest

The 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.

05

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.

conditionthe corpus holdscorrect behaviourdamage 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.

06

The admission gate

discard, never score

Silent 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:

integrationrequired proof
MCP servertools listed at session init, in the session's own record
lifecycle hookshooks demonstrably fired, with output
sandbox filesdigest-verified against the frozen bundle
isolationno 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.

07

Scoring and costs

harness/costs.py

The 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.

08

Diagnostic reference tracks

unranked, by design

Task 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_memory − claude_md

Oracle headroom: how much this task set can reward correct evidence at all.

recall − claude_md

Natural memory lift: the product as an agent actually experiences it.

recall_prefetch − claude_md

Prefetch memory lift: retrieval quality with query formulation removed.

oracle_memory − recall

Access gap: everything lost between perfect evidence and the live memory path.

recall_prefetch − recall

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.

09

Why this is a test of memory

and not of retrieval prose

Most 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.

10

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.

The honest half, by convention

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.