The result page states what happened. This page states how, so you can decide whether to believe it: the fixtures, the arms and why those arms, the isolation proof, the gate that refuses a rigged benchmark, two bugs our own grader had, and the parts of this nobody outside can reproduce yet.
A task needs some facts. The always-loaded summary asserts a value for each. Somewhere on disk, a dated note records the value that actually replaced it. Before answering, the agent either verifies โ spend a few tool calls, get the true value โ or trusts โ spend nothing, emit what the summary says.
It verifies only when the expected cost of being wrong exceeds the cost of looking. Both sides of that comparison are the agent's own estimate, and the first one is the problem: a confident, self-consistent summary drives its perceived chance of being stale toward zero, whatever the true rate is. Staleness is not underweighted. It is invisible. We call this the answer-before-doubt barrier, and everything on the results page is a consequence of it.
Every fixture is built around Marlin, a product that does not exist. Its facts and their values are invented, which closes the obvious hole: an agent cannot be scoring well from pretraining, and a stale answer cannot have come from anywhere but the channel we put it in.
The corpus is generated with a filler parameter n: n topically plausible filler notes, plus one dated note per fact recording both the superseded value and the one that replaced it. The two corpus sizes are easy to confuse, so they are stated explicitly wherever they appear.
| fixture | what the agent must produce | facts | corpus | graded by |
|---|---|---|---|---|
| 6-fact prose facts.mjs | a product one-pager, using the facts fluidly rather than answering a checklist | 6 | 76 notes (n=70) | keyword oracle with negation and cross-contamination guards |
| corpus-size control | identical | 6 | 13 notes (n=7) | identical |
| 20-fact graded facts-hard.mjs | answer 20 questions across five trap tiers | 20 | 100 notes | per-tier oracle, oracle-hard.mjs |
| 6-construct code facts-code.mjs | a working client that creates a contract check and polls it | 6 | 46 notes | exact code tokens โ create_board() against create_contract() |
The prose task deliberately does not ask "what is the pricing?" A checklist manufactures doubt; a one-pager does not. The coding fixture exists because a prose deliverable invites the objection that this is a writing problem: there, the always-loaded file is an API quick-reference stamped "last verified 2026-01-15" asserting six obsolete constructs, and a wrong answer is a call that returns 410.
Most treatments list mitigations as independent tools โ RAG, tiered memory, a decision log. That list does not predict anything. Sorting them by which actor is made responsible for the trusted context being correct, and when does: each family fails exactly where its actor defaults, and the two stressors on the results page are not a grid we picked, they are the two ways an actor defaults.
| class | arm | responsible actor | fails when |
|---|---|---|---|
| C0 | floor โ the stale summary, nothing else | nobody | always; it is the baseline |
| C1 | verify โ one line: check the notes first | the agent, on doubt | the model declines to search |
| C2 | RAG โ retrieve from the raw notes | the agent, on doubt | the model declines to search |
| C3 | TierMem โ escalate when the summary looks insufficient | the agent, on insufficiency | the summary is complete and wrong |
| C4 | Memory Bank ยท ADR ยท spec โ a curated store | a human, at write time | maintenance lags |
| C4′ | CUPMem โ adjudicate at write time | a model, at write time | compression happens before the question |
| C5 | mla push โ the in-force values delivered into context | the system, at delivery | coverage or precedence fails |
| โ | no-summary โ strip the assertions out | โ | not deployable; it isolates the cause |
The two stressors follow directly. A weaker model tests whether the C1โC3 actor will act. Maintenance lag โ the curated store missing the two most recent supersessions, while the raw notes still carry them โ tests whether the C4 actor has. A study that varies neither reports a tie, and that tie is an artifact of testing only the conditions in which every actor behaves.
Claude models run under the native claude -p CLI with the fixture's CLAUDE.md auto-loaded. Gemini and OpenAI models run through a REST agentic loop given the same file and list_dir / read_file / write_file tools, graded on the answer.txt it writes, identically. The only push in either harness is the context file itself.
| study | trials per cell | what that supports |
|---|---|---|
| primary matrix, Claude | 3 | the floor has zero variance, so N=3 is enough to establish it |
| primary matrix, Gemini and GPT-5.4 | 6 | pooled over two independent runs |
| primary matrix, GPT-5.5 and GPT-5.6 | 3 | same as Claude |
| adoption study (coverage and precedence) | 2โ3 | an existence result only โ see the caveat below |
| Opus 5 floor | 12 | a rate estimate with a wide interval, against an 8-trial same-build control |
| coding fixture | 3 | agreement with the prose fixture, not a rate |
| external pilot | 1 | a signature reproducing, not a mean |
Every arm runs in a subprocess whose only project instruction is the fixture's own CLAUDE.md. That is easy to assert and worth very little unless it is checked, so it is checked structurally, by the CLI's own report, and from both sides with a canary. All seven checks pass on both Claude models; the REST harnesses expose nothing but the injected context.
| check | what it rules out |
|---|---|
| structural โ the config directory is bare | no global context file, no hooks, no access to our internal tooling |
| init event โ the CLI's own startup record | 0 servers connected, 0 mcp__meetless* tools available. The system's report, not the model's claim |
| positive canary โ a unique marker in the arm's own file | proves the fixture's instructions actually loaded, rather than nothing loading at all |
| negative canary โ six secret markers from our real environment | none appear in any output; no private tool name, server name or product term leaked in |
Re-run it yourself with validate-isolation.mjs. The logs from our runs are kept.
The easiest way to win a benchmark is to build one whose answer lives only in the channel your own arm holds. We did that three times without noticing. One retrieval arm scored 3/3 on a corpus the answer had been stripped from, which is not a result, it is a leak.
All three are retracted, and derivability.mjs now refuses any benchmark whose graded answer is not recoverable from every arm's corpus. Without that gate, the winner is decided before a model runs.
Collapsing "wrong" and "missing" into one number destroys the finding. A miss that is an omission is a system that declined to answer; a miss that is a commission is a system that asserted a superseded value into a deliverable. Across every push run in this study the misses are omissions and the count of commissions is zero, while every other arm's misses are stale values. Accuracy alone cannot see that difference.
| claim | reproducible? |
|---|---|
| The barrier itself โ a confident stale summary suppresses the search: 0/6, zero tool calls | Yes. A folder of fabricated notes, a summary file, and a plain claude -p run. No product involved; one arm takes a couple of minutes. |
| The comparisons โ Memory Bank, ADR, spec, RAG, TierMem, CUPMem | Yes. Same shape. The CUPMem store is rebuilt by a script, so you can confirm it was not hand-written to lose. |
| The adoption cells โ coverage and precedence | Yes, with the artifact gap noted in section 04 on one cell. |
| The mla push rows | No โ and not because of the repository. That arm does not run mla. It is a fixed block of current facts placed in the prompt, standing in for what a delivery mechanism would supply. Installing mla and activating a folder would not reproduce these rows, because the product is not in the loop. |
git clone https://github.com/Meetless/stale-context-bench
cd stale-context-bench
node validate-isolation.mjs --model claude-opus-4-8 # prove the clean room
node matrix.mjs --trials 3 --conc 3 # every arm x {opus, haiku}
OPENAI_API_KEY=... node openai-matrix.mjs --trials 3 # cross-vendor
node run.mjs --arm hard-push --model claude-opus-4-8 --trials 2 # 20-fact graded fixture
node run.mjs --arm mf-fr-correction6 --model claude-haiku-4-5 # an adoption cell
node cupmem-adjudicate.mjs --arm arms/hard-cupmem # rebuild the CUPMem store, then run it