Three agents fix one failing test concurrently. The fix needs a signing key id that is randomized per trial from a 50-key keyring and appears in no file the agents can read. It exists only in the memory layer. Seven arms: a no-memory control, dense cosine RAG, Vestige, mem0, supermemory, hindsight, Zep/Graphiti. The measured result is about the first retrieval, not the final score. Across 5 models and 22 trials, a query-based arm's FIRST memory call surfaced the correct key 6 times out of 87. Vestige, which takes no query argument, did it 56 out of 56. Every downstream number follows from that. Ships the raw evidence rather than a summary: all agent transcripts, the harness, and tests/bm25_baseline.py, which reproduces the ranking table offline with no API key. Two arms were contaminated by a flush bug in this harness that served them a previous trial's facts; both were fixed and re-run, and the contaminated cells are withheld rather than published. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
16 KiB
One misled agent voids a three-agent fleet
What happens when three AI agents fix the same bug, and only two of them know the truth.
Trial 1 of the Silent Rotation benchmark. Kimi K3, seven memory configurations, one randomized secret.
Every number and every quotation below comes from the raw agent transcripts, which are published
alongside this document in results/trial-1/.
The result in one paragraph
Three agents work the same failing test. The fix requires knowing which cryptographic signing key is currently live. That key is randomized per trial and appears in no file the agents can read. It exists only in the memory layer.
Four of the six memory configurations put the correct answer in front of at least two of the three agents. Three of them lost anyway. In each losing case exactly one agent out of three never received the correct fact, confidently adopted a planted decoy, and wrote a conflicting change. The merge fractured and nothing shipped.
Two agents right plus one misled equals zero production-safe fleets.
Why this is not a retrieval story
The reflex when a memory system loses is to say its retrieval was bad. That is not what happened here. In every losing arm the correct memory was in the store, and most agents found it.
Here is the complete trial-1 outcome table. fixed_correctly means the tests went green and an
independent production replay passed and the key was right.
| arm | what it is | verdict | retrievals | fleet cost | wall clock |
|---|---|---|---|---|---|
| anarchy | no memory tool at all (control) | green_but_voids_prod | 0 | $0.5668 | 162.04s |
| rag | dense cosine, nomic-embed-text, top 3 | fixed_correctly | 6 | $0.4429 | 140.68s |
| sync | Vestige causal backfill | fixed_correctly | 3 | $0.4918 | 137.29s |
| mem0 | mem0ai OSS, LLM extraction on ingest | failed_merge_conflict | 7 | $0.4888 | 154.25s |
| supermemory | supermemory, vendor defaults | fixed_correctly | 7 | $0.5119 | 143.81s |
| hindsight | semantic + BM25 + cross-encoder rerank | failed_merge_conflict | 6 | $0.4974 | 147.78s |
| zep | Graphiti temporal knowledge graph | failed_merge_conflict | 13 | $0.5341 | 135.28s |
Read the losing rows by agent instead of by arm and the pattern is identical every time.
| arm | agent a0 | agent a1 | agent a2 | conflicts |
|---|---|---|---|---|
| mem0 | k_antares | k_nashira | k_nashira | 2 |
| hindsight | k_nashira | k_antares | k_nashira | 2 |
| zep | k_antares | k_nashira | k_nashira | 2 |
The correct key was k_nashira. In all three arms, two agents out of three got it. In all three arms,
the third agent took k_antares, a decoy planted in a closed staging ticket. Different arm, different
agent index, same shape.
The agent that failed was not reasoning badly
This is the part that matters, and it is why the transcripts are published rather than summarized.
The agent that got it wrong was not sloppy. It reasoned carefully and correctly from the evidence it was handed. Here is mem0's agent a0, in its own words, at the moment it committed:
"Both configs have blank key ids, left blank by an 'externalize secrets' refactor. The memory said: OPS-522 the key id was reset to
k_antaresand verification recovered. That suggestsk_antaresis the key the platform shipped with / should use."
That is a good inference. The retrieval had returned exactly one key-related fact, and that fact said
k_antares fixed a verification failure. a0 searched twice. Neither search returned the rotation
runbook naming k_nashira.
Now here is its teammate a2, on the same arm, in the same trial, working from a retrieval that did surface more:
"Memory said: 'OPS-522: a staging box failed token verification after a hand-edited config; on-call reset the key id to k_antares and verification recovered, noting it was staging-only with no rotation.' Hmm, that says k_antares was staging-only. That memory may be a hint that the production key id is something else, or it might be a red herring. Let me search memory more specifically."
a2 got suspicious and kept looking. a0 had no reason to be suspicious, because nothing it retrieved contradicted the decoy.
The two agents did not disagree about how to interpret a shared reality. Their memory layer gave them
different realities. One of them was working from a past in which k_antares was the answer.
hindsight's a1 shows the same thing from the other side. It searched twice, and its second query was well formed:
query:
externalize secrets refactor blanked key id which kid did the platform ship with activeKid trustedKid
That query never surfaced k_nashira either. Its reasoning then walks straight into the decoy:
"The question: which kid should both sides use? The config comment says 'entry the platform shipped with' dangling comment. The hindsight memory says: 'Closed ticket OPS-522 ... on-call reset the key id to k_antares and verification recovered.'"
Same arm, same corpus, same model, same turn budget. Its two teammates asked differently and got the truth.
Why the symptom query fails
Every agent in every arm opened with a query describing the symptom. Here is what the very first retrieval returned for the RAG arm's a0, verbatim, with the similarity scores the tool reported:
Top-3 memories by semantic similarity (cosine, nomic-embed-text): [1] (similarity 0.641) Closed ticket OPS-522: a staging box failed token verification after a hand-edited config; on-call reset the key id to k_antares and verification recovered. Staging-only, nothing rotated. [2] (similarity 0.629) Ledger verifier threw a 500 on charge when the token had no 'kid' header...
The top hit for "my charge test is failing on token verification" is a document about a token verification failure that was fixed by setting a key id. It is an excellent match for the symptom. It is also the wrong answer.
The document that actually explains the failure reads:
"Rotation runbook, Q3: we migrated the live signer to k_nashira and let the old material age out. Every credential minted since the cutover carries the new kid."
Notice that it shares almost no surface vocabulary with "end-to-end charge test failing on session token verify." It does not mention charges, tests, or failures. It is a routine operational note written four days before anything broke.
We measured this directly. Against the eight-document retrievable corpus, using the exact queries the agents actually typed:
| query (verbatim from a transcript) | who asked it | dense cosine | BM25 |
|---|---|---|---|
end-to-end charge test failing identity gateway ledger session token verify |
rag a0, first query | 7 of 8 | 7 of 8 |
end-to-end charge test failing session token identity gateway ledger |
zep a0, first query | 7 of 8 | 7 of 8 |
default key kid keyring shipped platform active key |
zep a0, third query | 3 of 8 | 7 of 8 |
which key id active signing key k_antares externalize secrets refactor blank config |
mem0 a0, second query | 3 of 8 | 4 of 8 |
no active signing key selected signing key rotation identity-service keyring |
supermemory a0, first query | 4 of 8 | 6 of 8 |
Measured against trial 1's own eight-document corpus, exported from the seeded store. Okapi BM25 with k1=1.5 and b=0.75; dense is nomic-embed-text, the same embedder the RAG arm used.
When you describe the symptom, the cause ranks last. Not sixth. Last, or next to last, out of eight.
This is not a tuning problem, and it is worth being precise about why. The cause is not merely dissimilar to the failure. The corpus contains look-alike documents that are more similar to the failure than the cause is, because they describe the same symptom. Sharpening the embedding pushes the look-alikes up, not the cause. Better similarity matching makes an anti-correlation worse, not better.
We ran the BM25 lexical baseline specifically because the answer is a rare literal token, which is supposed to be exact-match's home ground. It is not better. It ties dense cosine on the symptom queries, where both bury the cause at rank 7 of 8, and it is worse by one to four ranks on every query that actually worked. The decoys share the target's exact vocabulary, so lexical scoring rewards them just as much. The thing that separates the right key from the wrong one is which one is current, and recency has no lexical signal at all.
There is also a practical limit on exact match here. The live key is randomized per trial from a 50-key keyring and appears in no readable file, so you can only type it into a query after you already know the answer.
What the winning arms actually did
Three arms shipped a correct, production-safe fix. They did it in visibly different ways.
rag and supermemory won by reformulating. Every one of their agents opened with a symptom query,
got the decoy, and then wrote a second query that described the shape of the answer rather than the
shape of the problem. supermemory's a0 opened with no active signing key selected signing key rotation identity-service keyring. It had already guessed that a rotation was involved before it searched.
That works. It also means the model is spending reasoning budget compensating for retrieval, and that it has to suspect the answer's category before it can find it.
sync, the Vestige arm, did not formulate a query. vestige_backfill takes no query argument. Each
agent called it exactly once, and the tool returned this:
{
"causes": [{
"content_preview": "Rotation runbook, Q3: we migrated the live signer to k_nashira ...",
"age_days_before_failure": 4.0,
"shared_entities": ["active_key"],
"similarity_rank": 6,
"backfill_score": 1.51,
"reason": "Reached back 4.0d to a quiet memory sharing 1 entity (active_key) with the
failure; it ranked #6 on similarity, so semantic search would have missed it."
}]
}
Three agents, three calls, three identical results. Zero merge conflicts. The receipt states its own similarity rank, which is the tool reporting that the fact it just returned was one a similarity search would have buried.
Be precise about what that demonstrates. The active_key entity link between the failure and the
cause is authored by the benchmark harness (prepare_trial.py:187 and :198). Vestige's extractor does
not derive it from the prose. So this shows that traversing an existing causal edge returns the cause
in one call with no query formulation. It does not show that the edge can be discovered
automatically. That is a separate, unproven claim, and this benchmark does not test it.
The control that makes the point
The most useful row in the table is the one with no memory at all.
anarchy had all three agents converge unanimously on k_olivine. Zero merge conflicts. Perfect
agreement. The tests went green. The production replay failed.
So convergence is not the prize. A fleet can agree perfectly and be perfectly wrong. What the memory layer has to deliver is convergence on the truth, and the failing arms show that delivering it to two out of three agents is worth exactly as much as delivering it to none.
anarchy also did something worth its own study: with no memory, it did not fail loudly. It shipped a green test suite that breaks production. That result has nothing to do with any memory product, which is why we find it credible.
Why the usual metrics cannot see this
Retrieval quality is normally reported as recall@k, MRR, or nDCG. All three are means over queries.
Fleet success is not a mean. It is a conjunction. Every agent editing shared state has to hold the same correct belief, or the merge breaks. The statistic that governs the outcome is the minimum over agents, not the average.
If each agent independently has a 90% chance of retrieving the deciding fact:
| fleet size | probability every agent gets it |
|---|---|
| 1 | 0.90 |
| 3 | 0.73 |
| 5 | 0.59 |
| 10 | 0.35 |
A memory layer that benchmarks at 90% recall looks excellent and produces a coin flip at fleet size 5. Nothing in the standard metric suite surfaces that, because the standard metric suite was designed for one agent asking one question.
The measurements a fleet actually needs are different:
- Correct convergence. Did every agent independently reach the production-safe answer?
- Retrieval consistency. How many distinct purported truths did the memory layer hand out across agents working the same incident?
- Query sensitivity. How much did the returned evidence change with small changes in wording?
- Fleet-safe completion. Did the merged result stay conflict free and pass an external replay?
- Cost per converged safe fix. Retrievals, turns, tokens, seconds and dollars until the whole fleet was correct.
What this does not show
Stated plainly, because the interesting result is worth more than an overclaim.
- The causal edge is hand authored.
prepare_trial.py:187tags the causekeyring_rotation,active_keyand:198tags the failureactive_key,crash. None of the seven distractors carry it. Remove that string and backfill returnscauses: []. This measures traversal, not discovery. - The outcome column does not separate the winners. rag, sync and supermemory all scored
fixed_correctly. Plain dense cosine over eight documents tied the memory products, and it was the cheapest arm in the trial at $0.4429. There is no "Vestige beats RAG" result here to defend. - The retrievable corpus is eight documents. Nothing here is measured at production scale, and the two scaling axes run in opposite directions. More documents can only push a cause that already ranks seventh further down a similarity list, which makes N=8 the easy case for the RAG arms. Vestige's backfill degrades as more memories share the join entity, which is two of nine here.
- This is one trial of one scenario on one model. The per-agent divergence pattern repeated across three independent arms within it, and reproduced in a later run, but n is small and it is one bug shape.
similarity_rankin the receipt is self-reported by Vestige. The independent check on it is that the RAG arm's own tool output showed the decoy outranking the cause on symptom queries, which is visible in the transcripts.- The Vestige arm has a write-back coordination tool (
vestige_log) that no competitor arm was given. It did not produce the result: in all six agent runs across both trials, each agent got the key from its own single backfill call, and every log write happened strictly later in the transcript. - The competitor corpus is built content-only, with the
tagsfield discarded (runner.py:399). Restoring tags to competitors was measured and flips zero outcomes; the cause still never reaches rank 1, and on symptom queries it still ranks seventh or eighth in both conditions. - The author built the benchmark, the harness, and one of the products measured. Which is why the raw transcripts are published rather than a summary table.
Reproducing it
Everything needed to check this is in the repository.
harness/ the runner, the fleet driver, and the trial generator
harness/agent/prepare_trial.py seeds the corpus and randomizes the key
results/trial-1/ all 7 arm results and all 21 agent transcripts
tests/ the arm liveness gate
The claims above map to files. The outcome table is results/trial-1/{arm}.json, field
fleet_verdict. The per-agent key choices are fix_directions in the same files. Every quotation is in
results/trial-1/transcript-{arm}-a{n}.json under turns[].reasoning and turns[].tool_results.
One thing to check first if you are auditing this: each arm's JSON carries memory_layer_alive and
retrieval_err_total. An arm whose backend never answered is not a retrieval loss, it is a missing
measurement, and it should never be read as a score. In trial 1 all six memory arms were alive with
zero retrieval errors.
The short version
Similarity retrieval asks every agent to guess the vocabulary of an answer it has not seen yet. Most of them guess well. In a fleet, most is not enough.
Two agents found the truth. One found the decoy. Nothing shipped.