mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-24 23:41:01 +02:00
benchmarks: add Silent Rotation, a fleet-convergence memory benchmark
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>
This commit is contained in:
parent
b91a7e0bb7
commit
9bdb9a5668
70 changed files with 30260 additions and 0 deletions
273
benchmarks/silent-rotation/EVIDENCE.md
Normal file
273
benchmarks/silent-rotation/EVIDENCE.md
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
# Every claim, and where to check it
|
||||
|
||||
Companion to `FINDING.md`. Each row states a claim and the file that proves it. All transcripts are in
|
||||
`results/`. Nothing below is summarized from memory; every number was read out of the JSON.
|
||||
|
||||
Five models. Twenty-two trials. Two hundred and ten agent transcripts.
|
||||
|
||||
---
|
||||
|
||||
## 1. The headline table
|
||||
|
||||
Three agents work the same failing test. 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.
|
||||
|
||||
For every arm in every trial we asked one question: **did all three agents write the same key, and was
|
||||
it the right one?**
|
||||
|
||||
| arm | converged on the CORRECT key | converged on a WRONG key | split, no consensus | n |
|
||||
|---|---|---|---|---|
|
||||
| anarchy (no memory) | 0/22 | **16/22** | 6/22 | 22 |
|
||||
| rag (dense cosine) | 4/21 | **10/21** | 7/21 | 21 |
|
||||
| supermemory | 5/6 | 0/6 | 1/6 | 6 |
|
||||
| mem0 (clean cells only) | 1/2 | 0/2 | 1/2 | 2 |
|
||||
| hindsight | 0/3 | 0/3 | 3/3 | 3 |
|
||||
| Zep/Graphiti (clean cell only) | 0/1 | 0/1 | 1/1 | 1 |
|
||||
| **sync (Vestige)** | **19/21** | **0/21** | 2/21 | 21 |
|
||||
|
||||
Models: Kimi K3, MiniMax M3, GLM 5.2, GPT-5.6 Sol, DeepSeek V4 Flash.
|
||||
Reproduce: read `fix_directions` and `correct_kid` out of every `trial-*/{arm}.json` and `manifest.json`.
|
||||
|
||||
### There are two failure modes and only one of them is visible
|
||||
|
||||
**Split.** The agents disagree, write conflicting changes, the merge fractures, nothing ships. Loud.
|
||||
You find out immediately.
|
||||
|
||||
**Converged wrong.** The agents all agree, on a decoy. The tests go green. The merge is clean. The
|
||||
production replay fails. **This is the one that ships.**
|
||||
|
||||
Look at that middle column again. A memoryless fleet converged on a wrong key **16 times out of 22**.
|
||||
The dense-cosine fleet did it **10 times out of 21**. Vestige did it **zero times out of 21**.
|
||||
|
||||
Not "rarely". Not "less often". Zero.
|
||||
|
||||
---
|
||||
|
||||
## 2. The agents were not reasoning badly. They were reasoning correctly from wrong evidence.
|
||||
|
||||
This is the part that does not show up in any aggregate score, and it is why the transcripts are
|
||||
published rather than a summary table.
|
||||
|
||||
### GPT-5.6 Sol, trial 1, correct key `k_yildun`, dense-cosine arm
|
||||
|
||||
All three agents converged on `k_wezen`. Here is agent a1 at the moment it decided, verbatim from
|
||||
`raw-sol/trial-1/transcript-rag-a1.json`:
|
||||
|
||||
> "I'm weighing whether to fix the memory to say that the key k_wezen is staging only. The note
|
||||
> mentions a closed ticket about resetting the key id for k_wezen in staging without any rotation,
|
||||
> which seems intentional. There's a need to configure both to k_wezen while considering possible
|
||||
> environment fallback... I've noticed that my memory indicates a reset to k_wezen, so I must set both
|
||||
> configs to that"
|
||||
|
||||
Read that twice. **The agent noticed the memory said "staging only" and used it anyway.** Not because
|
||||
it was careless, but because nothing it retrieved contradicted the decoy, and a caveated answer beats
|
||||
no answer. It reasoned its way to the wrong key through a correct chain of inference over an incomplete
|
||||
evidence set.
|
||||
|
||||
### GLM 5.2, trial 1, correct key `k_yildun`, dense-cosine arm
|
||||
|
||||
All three converged on `k_wezen`. From `raw-glm/trial-1/transcript-rag-a1.json`:
|
||||
|
||||
> "The memory mentions a key id `k_wezen` in config."
|
||||
|
||||
That is the whole deliberation. The retrieval returned one key-shaped fact and the agent took it.
|
||||
|
||||
### GLM 5.2, trial 3, correct key `k_wren`, dense-cosine arm
|
||||
|
||||
All three converged on `k_vela`. From `raw-glm/trial-3/transcript-rag-a0.json`:
|
||||
|
||||
> "The first memory mentions 'reset the key id to k_vela'."
|
||||
|
||||
Different trial, different decoy, identical shape.
|
||||
|
||||
### Kimi K3, trial 1, correct key `k_nashira`, mem0 arm
|
||||
|
||||
Here the fleet split rather than converging, and you can watch exactly why. Agent a0 never surfaced the
|
||||
cause, from `experiment-1337-5/trial-1/transcript-mem0-a0.json`:
|
||||
|
||||
> "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_antares` and verification recovered. That suggests `k_antares` is
|
||||
> the key the platform shipped with / should use."
|
||||
|
||||
Its teammate a2 got a richer retrieval and became suspicious of the same document:
|
||||
|
||||
> "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."
|
||||
|
||||
**a0 picked `k_antares`. a1 and a2 picked `k_nashira`. Two conflicts. Nothing shipped.**
|
||||
|
||||
They 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.
|
||||
|
||||
### What the Vestige agents said on the same task, same models
|
||||
|
||||
Nothing like the above. From `raw-glm/trial-3/transcript-sync-a1.json`:
|
||||
|
||||
> "The issue is that `activeKid` is blank in identity-service config. The 'externalize-secrets refactor'
|
||||
> pulled out the concrete key id. I need to find which key id the verifier expects."
|
||||
|
||||
Then it called `vestige_backfill` once and went to the fix. **There is no passage anywhere in the sync
|
||||
transcripts where an agent weighs a decoy**, because no decoy arrived.
|
||||
|
||||
---
|
||||
|
||||
## 3. Why the symptom query fails
|
||||
|
||||
Every agent in every arm opened by describing the symptom. Here is what the very first retrieval
|
||||
returned, verbatim with the tool's own similarity scores, from
|
||||
`experiment-1337-5/trial-1/transcript-rag-a0.json`:
|
||||
|
||||
> 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 fixed by setting a key id. Excellent match for the symptom. 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."
|
||||
|
||||
It never mentions charges, tests, or failures. It is a routine operational note written four days before
|
||||
anything broke.
|
||||
|
||||
Measured against trial 1's own eight-document corpus, using the exact queries the agents typed
|
||||
(`tests/bm25_baseline.py`, runnable, no API key needed):
|
||||
|
||||
| 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 | 3 of 8 | 4 of 8 |
|
||||
| `no active signing key selected signing key rotation identity-service keyring` | supermemory a0, first | 4 of 8 | 6 of 8 |
|
||||
|
||||
**When you describe the symptom, the cause ranks last.**
|
||||
|
||||
We ran BM25 specifically because the answer is a rare literal token, which is supposed to be
|
||||
exact-match's home ground. It ties dense on the symptom queries and is one to four ranks worse on the
|
||||
queries that actually worked. The decoys share the target's exact vocabulary, so lexical scoring rewards
|
||||
them just as much. What separates the right key from the wrong one is *which one is current*, and
|
||||
recency has no lexical signal.
|
||||
|
||||
This is not a tuning problem. 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 raises the
|
||||
look-alikes. Better similarity matching makes an anti-correlation worse.
|
||||
|
||||
---
|
||||
|
||||
## 4. The same trial, run twice
|
||||
|
||||
`experiment-1337-5/trial-1` and `experiment-1337-3/trial-1` are the same trial. Same master seed, same
|
||||
trial index, same correct key `k_nashira`, same nine-memory corpus, same model, same 8-turn budget.
|
||||
Only the run differs.
|
||||
|
||||
| | dense-cosine arm | Vestige arm |
|
||||
|---|---|---|
|
||||
| Run A | `fixed_correctly`, all three on `k_nashira` | `fixed_correctly`, all three on `k_nashira` |
|
||||
| Run B | **`failed_merge_conflict`**, a0 `k_nashira`, a1 `k_nashira`, a2 **`k_antares`** | `fixed_correctly`, all three on `k_nashira` |
|
||||
|
||||
Plain cosine won the trial once and lost the identical trial the next time. One agent of three phrased
|
||||
its queries slightly differently, never surfaced the cause, took the decoy, and voided the fleet.
|
||||
|
||||
Vestige did the same thing both times: one call per agent, same answer, zero conflicts.
|
||||
|
||||
**That is the finding, not a caveat.** A result that flips on rerun is a coin flip, and a coin flip is
|
||||
what you are deploying when retrieval depends on query phrasing.
|
||||
|
||||
---
|
||||
|
||||
## 5. Why the model matters, and why that helps rather than hurts
|
||||
|
||||
Number of times each model's dense-cosine agents re-queried before committing:
|
||||
|
||||
| model | rag_search calls per agent | rag outcome |
|
||||
|---|---|---|
|
||||
| GLM 5.2 | 1.00 | 0/5 |
|
||||
| DeepSeek V4 Flash | 1.00 | 1/5 |
|
||||
| GPT-5.6 Sol | 1.13 | 0/5 |
|
||||
| Kimi K3 | 2.17 | wins some, loses some |
|
||||
|
||||
**Similarity retrieval's success is explained almost entirely by whether the model bothers to
|
||||
reformulate.** Ask once and accept the answer, and you get the decoy. Kimi K3 re-queries twice on
|
||||
average and sometimes escapes.
|
||||
|
||||
Vestige is **1 call per agent on every model tested**, and the key was surfaced on 47 of 48 successful
|
||||
calls, because there is no query to reformulate. `vestige_backfill` takes no query argument. It anchors
|
||||
on the failure event and walks the causal edge backward.
|
||||
|
||||
So the honest cross-model statement is not "this works everywhere." It is: **the memory layer's
|
||||
behaviour was invariant across five models and the baseline's was not.**
|
||||
|
||||
---
|
||||
|
||||
## 6. What this does not show
|
||||
|
||||
Stated plainly, because an interesting result is worth more than an overclaim. Every one of these was
|
||||
found by attacking our own work today.
|
||||
|
||||
1. **The causal edge is hand authored.** `prepare_trial.py:187` tags the cause
|
||||
`keyring_rotation,active_key`, `:198` tags the failure `active_key,crash`, and none of the seven
|
||||
distractors carry it. Remove that string and backfill returns `causes: []`. **This measures traversal
|
||||
of an existing edge, not discovery of one.** Vestige's extractor does not derive that edge from prose.
|
||||
2. **We are not the first to run multiple agents on one repo.** CooperBench (arXiv 2601.13295) runs two
|
||||
coding agents concurrently across 652 tasks and scores merge compatibility, finding roughly 30% lower
|
||||
success working together than alone. MultiAgentBench (ACL 2025) runs 1/3/5/7 agents over an explicit
|
||||
shared memory layer. Ask WhAI (arXiv 2511.14780) measures belief divergence across agents on a shared
|
||||
record. The narrow thing that appears to be unmeasured is **treating the memory layer itself as the
|
||||
independent variable**: same fleet, same task, same model, swap only the memory backend, score
|
||||
inter-agent agreement.
|
||||
3. **The dense-cosine baseline is not a strawman and sometimes beats us.** It converged correctly 4 times
|
||||
and was cheaper than Vestige in both Kimi K3 trials measured ($0.4429 vs $0.4918 in trial 1).
|
||||
4. **The retrievable corpus is 8 documents.** Nothing here is measured at production scale, and the two
|
||||
scaling axes run in opposite directions: more documents push a cause that already ranks 7th further
|
||||
down a similarity list, which makes N=8 the *easy* case for the RAG arms, while Vestige's backfill
|
||||
degrades as more memories share the join entity, which is 2 of 9 here.
|
||||
5. **`similarity_rank` in the Vestige receipt is self-reported by Vestige.** The independent check is
|
||||
that the RAG arm's own tool output shows the decoy outranking the cause, which is visible in the
|
||||
transcripts.
|
||||
6. **The Vestige arm has a `vestige_log` write tool no competitor arm was given.** It did not produce the
|
||||
result: in every agent run, the key came from that agent's own single backfill call, and every log
|
||||
write happened strictly later in the transcript.
|
||||
7. **The competitor corpus is built content-only**, tags discarded (`runner.py:399`). Restoring tags was
|
||||
measured and flips zero outcomes; the cause still never reaches rank 1 and still ranks 7th or 8th on
|
||||
symptom queries in both conditions.
|
||||
8. **mem0's cells in the pre-fix trials are contaminated.** mem0 persists to `~/.mem0` in addition to its
|
||||
configured store, which the harness cleared but `~/.mem0` was not, so it carried facts across trials.
|
||||
That disadvantaged mem0. Fixed, and flagged wherever those cells appear.
|
||||
9. **One arm ran below its documented behaviour.** The tool description shown to Zep's agents said
|
||||
"graph RRF", which overstates what `EDGE_HYBRID_SEARCH_RRF` does. Corrected after these runs.
|
||||
10. **The author built the benchmark, the harness, and one of the products measured.** Which is why the
|
||||
raw transcripts ship rather than a summary.
|
||||
|
||||
---
|
||||
|
||||
## 7. Check it yourself
|
||||
|
||||
```
|
||||
harness/ the runner, the fleet driver, the trial generator
|
||||
tests/bm25_baseline.py reproduces the ranking table, no API key needed
|
||||
results/trial-1/ all 7 arms, all 21 agent transcripts
|
||||
```
|
||||
|
||||
Start here if you are auditing: every arm JSON carries `memory_layer_alive` and `retrieval_err_total`.
|
||||
An arm whose backend never answered is a missing measurement, not a retrieval loss, and should never be
|
||||
read as a score. Where that telemetry predates a run, liveness was reconstructed from the transcripts by
|
||||
counting successful tool results, and that is stated wherever it applies.
|
||||
|
||||
---
|
||||
|
||||
## The short version
|
||||
|
||||
A memory system can be right for most of your agents and still be unusable, because a fleet is a
|
||||
conjunction and the statistic that governs it is a minimum, not a mean.
|
||||
|
||||
And the failure you should actually fear is not the one where your agents argue. It is the one where
|
||||
they all agree, the tests pass, and the key is wrong.
|
||||
|
||||
**Zero out of twenty-one.**
|
||||
312
benchmarks/silent-rotation/FINDING.md
Normal file
312
benchmarks/silent-rotation/FINDING.md
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
# 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_antares` and verification recovered. That suggests `k_antares` is
|
||||
> 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
|
||||
1. **The causal edge is hand authored.** `prepare_trial.py:187` tags the cause `keyring_rotation,active_key`
|
||||
and `:198` tags the failure `active_key,crash`. None of the seven distractors carry it. Remove that
|
||||
string and backfill returns `causes: []`. This measures traversal, not discovery.
|
||||
2. **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.
|
||||
3. **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.
|
||||
4. **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.
|
||||
5. **`similarity_rank` in 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.
|
||||
6. **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.
|
||||
7. **The competitor corpus is built content-only**, with the `tags` field 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.
|
||||
8. **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.
|
||||
145
benchmarks/silent-rotation/SHOW-HN.md
Normal file
145
benchmarks/silent-rotation/SHOW-HN.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# Show HN post
|
||||
|
||||
## Titles, ranked
|
||||
|
||||
**1. Show HN: One misled agent voids a three-agent fleet (memory benchmark, 5 models)**
|
||||
Leads with the finding, not the product. States a concrete falsifiable claim. 79 chars.
|
||||
|
||||
2. Show HN: I benchmarked 6 agent-memory systems and a plain cosine baseline tied mine
|
||||
More self-deprecating, very HN, but buries the actual finding one click deeper.
|
||||
|
||||
3. Show HN: Retrieval quality was not the problem. The access primitive was.
|
||||
Sharpest thesis, but abstract, and gives a reader nothing to click for.
|
||||
|
||||
Use #1.
|
||||
|
||||
---
|
||||
|
||||
## Body
|
||||
|
||||
I run three coding agents on the same repo at the same time. They kept shipping
|
||||
broken merges, so I built a benchmark to find out why. The answer was not what I
|
||||
expected, and my own tool does not come out of it cleanly.
|
||||
|
||||
**The setup.** Three agents fix one failing end-to-end test in a TypeScript
|
||||
monorepo. The fix needs the currently live signing key id. That key 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, my thing (Vestige), mem0, supermemory, hindsight, and Zep/Graphiti. A trial
|
||||
counts as a pass only if the merged result is green AND passes an independent
|
||||
production replay AND uses the correct key.
|
||||
|
||||
Alongside the true cause I planted a decoy: a closed ticket reading "a staging box
|
||||
failed token verification, on-call reset the key id to k_antares and verification
|
||||
recovered. Staging-only, nothing rotated." It reads exactly like the answer.
|
||||
|
||||
**The first thing I found is that there are two failure modes and only one of them
|
||||
is visible.**
|
||||
|
||||
Split: agents disagree, write conflicting changes, the merge fractures, nothing
|
||||
ships. Loud. You find out immediately.
|
||||
|
||||
Converged wrong: every agent agrees, on the decoy. Tests go green. The merge is
|
||||
clean. Production breaks. This is the one that ships.
|
||||
|
||||
Across 5 models and 22 trials:
|
||||
|
||||
arm converged CORRECT converged WRONG split
|
||||
no memory 0/22 16/22 6/22
|
||||
dense cosine RAG 4/21 10/21 7/21
|
||||
Vestige 19/21 0/21 2/21
|
||||
|
||||
The middle column is the finding. A memoryless fleet marched confidently off a
|
||||
cliff 16 times out of 22. Dense cosine did it 10 times out of 21.
|
||||
|
||||
**The agents were not reasoning badly.** GPT-5.6 Sol, committing to the wrong key:
|
||||
|
||||
> "I'm weighing whether to fix the memory to say that the key k_wezen is staging
|
||||
> only. The note mentions a closed ticket about resetting the key id for k_wezen
|
||||
> in staging without any rotation, which seems intentional... I've noticed that my
|
||||
> memory indicates a reset to k_wezen, so I must set both configs to that"
|
||||
|
||||
It read the caveat and used the key anyway. And an agent in another trial:
|
||||
|
||||
> "k_sirius is the key id verification recovered with. No other memory suggests a
|
||||
> different key."
|
||||
|
||||
It checked for a contradiction. None came back. You cannot prompt-engineer around
|
||||
that. The agent did everything right and was handed an incomplete past.
|
||||
|
||||
**Why similarity cannot fix this.** Measured on the real corpus with the exact
|
||||
queries the agents typed: when an agent describes the symptom, the causal memory
|
||||
ranks 7th of 8. Under dense cosine AND under BM25. The decoy ranks 1st at 0.641
|
||||
cosine, because the decoy is literally about a token verification failure fixed by
|
||||
setting a key.
|
||||
|
||||
The document that explains a failure does not resemble the failure. "Rotation
|
||||
runbook, Q3: we migrated the live signer to k_nashira" shares almost no vocabulary
|
||||
with "charge test failing on token verify."
|
||||
|
||||
So symptom similarity here is not merely insufficient. It is anti-correlated with
|
||||
causal usefulness, which means improving the incumbent retrieval objective
|
||||
strengthens the wrong answer.
|
||||
|
||||
The strongest evidence for that: hindsight has the most sophisticated retrieval
|
||||
stack in the benchmark. Embedded Postgres, BAAI/bge-small-en-v1.5, BM25, and a
|
||||
local cross-encoder reranker. Semantic and lexical and a second-pass rerank. On
|
||||
the three trials where its backend answered, it produced zero correct fleets and
|
||||
three fractured ones. Retrieval quality was never the limiting variable. The
|
||||
access primitive was wrong for the incident.
|
||||
|
||||
**What Vestige does differently** is not better ranking. It takes no query at all.
|
||||
Every agent anchors on the same failure event and walks backward through causal
|
||||
history, filtering for currently valid state. That removes phrasing variance,
|
||||
bypasses the similarity inversion, and makes supersession explicit instead of
|
||||
assuming structured memory stays current on its own. One call per agent, same
|
||||
answer to all three, on every model tested.
|
||||
|
||||
---
|
||||
|
||||
**Now the parts that argue against me, because you would find them anyway.**
|
||||
|
||||
The dense cosine baseline tied me. rag, Vestige and supermemory all passed both
|
||||
Kimi K3 trials, and rag was cheaper in both. There is no "Vestige beats RAG"
|
||||
outcome here to defend. What separated them was variance: I ran the identical
|
||||
trial twice, same seed, same key, same corpus, same model. rag passed the first
|
||||
time and fractured the second when one agent of three phrased a query differently
|
||||
and took the decoy. Vestige did the same thing both times.
|
||||
|
||||
The causal edge is hand authored. prepare_trial.py:187 tags the cause and :198
|
||||
tags the failure with a shared entity. Remove that string and my backfill returns
|
||||
nothing. This measures traversal of an existing edge, not discovery of one. My
|
||||
extractor does not derive that edge from prose, and I am not claiming it does.
|
||||
|
||||
I am not first to run multiple agents on one repo. CooperBench (arXiv 2601.13295)
|
||||
did 652 collaborative coding tasks in January and named the result "the curse of
|
||||
coordination." Memory architecture has also been compared as a variable before,
|
||||
including DecentMem (arXiv 2605.22721). The narrower claim I think holds: this is
|
||||
the first controlled evaluation of shared-memory backends in a live multi-agent
|
||||
coding fleet holding model, repo, task, tool budget and production oracle constant
|
||||
while measuring fleet convergence.
|
||||
|
||||
The retrievable corpus is 8 documents. Nothing here is measured at production
|
||||
scale, and the two scaling axes run in opposite directions, so N=8 is the easy
|
||||
case for the RAG arms and I say so.
|
||||
|
||||
Two arms are excluded. I found a contamination bug in my own harness: mem0's
|
||||
second store at ~/.mem0 and Zep's FalkorDB graph were not flushed between trials,
|
||||
so both were served facts from a previous trial that no other arm received. That
|
||||
disadvantaged them. Their cells are withheld rather than published.
|
||||
|
||||
I have not run the ablation that separates event anchoring from causal traversal
|
||||
from temporal lifecycle. A reviewer can fairly argue I won by removing query
|
||||
variability alone. That is the next experiment.
|
||||
|
||||
And I built the benchmark, the harness, and one of the products in it. Which is
|
||||
why all 210 agent transcripts ship with it rather than a summary table.
|
||||
|
||||
---
|
||||
|
||||
Repo, transcripts and the BM25 baseline script: [LINK]
|
||||
Vestige itself is a local-first MCP memory server, Rust, single binary, AGPL: [LINK]
|
||||
|
||||
The thing I would actually like feedback on: is there prior work measuring
|
||||
inter-agent belief convergence as a function of the memory backend? I could not
|
||||
find it, and I would rather be pointed at it than keep claiming a gap.
|
||||
1707
benchmarks/silent-rotation/harness/agent/fleet_runner.py
Executable file
1707
benchmarks/silent-rotation/harness/agent/fleet_runner.py
Executable file
File diff suppressed because it is too large
Load diff
302
benchmarks/silent-rotation/harness/agent/prepare_trial.py
Executable file
302
benchmarks/silent-rotation/harness/agent/prepare_trial.py
Executable file
|
|
@ -0,0 +1,302 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Per-trial randomizer for the N-trial Silent Rotation benchmark.
|
||||
|
||||
Makes each trial information-theoretically INDEPENDENT so a skeptic cannot claim
|
||||
the agents were tuned to one answer. Per trial it:
|
||||
1. Picks a random set of 3 neutral key names + a random CORRECT one among them
|
||||
(names are randomized too, so there is no fixed "borealis is always right").
|
||||
2. Generates fresh random 32-byte hex for all 3 keys.
|
||||
3. Rewrites shared/src/keyring.ts (+ .repo-snapshot copy) with the new keys.
|
||||
4. Regenerates the production corpus: fresh tokens signed under the TRIAL's
|
||||
correct key material, written to a per-trial PROD_CORPUS path.
|
||||
5. Re-templates the Vestige seed script so the memory names the TRIAL's correct
|
||||
key + its decoys (the deciding fact stays ONLY in memory, never the repo).
|
||||
6. Leaves configs blank (RED base) and returns a manifest describing the trial,
|
||||
so the run is fully reproducible from the master seed.
|
||||
|
||||
Everything here is deterministic given a seed, so `--seed S` reproduces an exact
|
||||
trial. This is the "reproduce this exact trial" affordance a skeptic needs.
|
||||
|
||||
Usage (called by the experiment loop, or standalone for offline checks):
|
||||
python3 prepare_trial.py --repo <torture-v3.5> --trial 3 --master-seed 12345 \
|
||||
--corpus-out <path.json> --manifest-out <path.json>
|
||||
Prints the chosen correct kid to stdout (last line) for the caller to export.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
# Neutral, disposition-free name pool (stars, constellations, minerals). No name
|
||||
# implies "active/prod/current". Randomizing the NAMES (not just which is correct)
|
||||
# kills any "the middle one is always right" prior. Pool must be >= KEYRING_SIZE.
|
||||
# A LARGE keyring is what makes the benchmark un-riggable: with K keys, a
|
||||
# memoryless agent's blind guess is correct only 1/K of the time, so the control
|
||||
# fails ~always and the null-hypothesis p-value is crushing (1/K per correct pick).
|
||||
NAME_POOL = [
|
||||
"atlas", "borealis", "cirrus", "dorado", "electra", "fornax", "helios",
|
||||
"indus", "juno", "kepler", "lyra", "mensa", "norma", "orion", "pavo",
|
||||
"quasar", "rigel", "sirius", "tucana", "vega", "wren", "zephyr",
|
||||
"altair", "antares", "arcturus", "bellatrix", "capella", "castor", "deneb",
|
||||
"draco", "fomalhaut", "gemini", "hydra", "izar", "lyric", "mizar", "nashira",
|
||||
"octans", "perseus", "polaris", "procyon", "regulus", "spica", "tarazed",
|
||||
"umbriel", "vela", "wezen", "yildun", "aludra", "basalt", "cobalt", "dunite",
|
||||
"feldspar", "garnet", "hematite", "jasper", "kyanite", "olivine", "pyrite",
|
||||
"quartz", "rutile", "schist", "talc", "zircon",
|
||||
]
|
||||
|
||||
# How many keys live in the keyring per trial (1 correct + KEYRING_SIZE-1 decoys).
|
||||
# 50 makes blind-guess success 2% -> the control cannot 'luck into' the answer,
|
||||
# and 5 clean sync trials give a null p-value of (1/50)^5 ~ 3e-9. Override via env.
|
||||
KEYRING_SIZE = int(os.environ.get("KEYRING_SIZE", "50"))
|
||||
|
||||
FAR_FUTURE_EXP = 4102444800 # year 2100 — tokens never expire (matches verify.ts)
|
||||
ISSUER = "identity.meridian.internal"
|
||||
CORPUS_SIZE = 5
|
||||
|
||||
|
||||
def b64url(raw: bytes) -> str:
|
||||
import base64
|
||||
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
||||
|
||||
|
||||
def canonical(claims: dict) -> str:
|
||||
# MUST match torture-v3.5 canonical.ts field order: sub, plan, iat, exp.
|
||||
return "\n".join(f"{k}={claims[k]}" for k in ("sub", "plan", "iat", "exp"))
|
||||
|
||||
|
||||
def sign_token(claims: dict, key_hex: str) -> str:
|
||||
key = bytes.fromhex(key_hex)
|
||||
header = {"alg": "HS256", "typ": "JWT", "iss": ISSUER}
|
||||
h = b64url(json.dumps(header).encode())
|
||||
p = b64url(json.dumps(claims).encode())
|
||||
sig = b64url(hmac.new(key, canonical(claims).encode(), hashlib.sha256).digest())
|
||||
return f"{h}.{p}.{sig}"
|
||||
|
||||
|
||||
def make_keyring_ts(keys: dict[str, str]) -> str:
|
||||
lines = "\n".join(f' k_{n}: "{hexv}",' for n, hexv in keys.items())
|
||||
return (
|
||||
"/**\n"
|
||||
" * @meridian/shared — the platform signing keyring.\n"
|
||||
" *\n"
|
||||
" * The keyring holds the HMAC signing keys the platform can use to sign and\n"
|
||||
" * verify session tokens. Keys are referenced by a short key id (`kid`). Each\n"
|
||||
" * service selects which key it uses from its own runtime config; the keyring\n"
|
||||
" * itself is just the material.\n"
|
||||
" *\n"
|
||||
" * Keys are 32-byte values, hex-encoded. (Dev material, committed for the local\n"
|
||||
" * harness; injected from the KMS in production.)\n"
|
||||
" */\n"
|
||||
"export const KEYRING: Record<string, string> = {\n"
|
||||
" // Each entry is a provisioned HMAC key. Presence in the keyring does not imply\n"
|
||||
" // a service currently selects it; selection is per-service runtime config.\n"
|
||||
f"{lines}\n"
|
||||
"};\n\n"
|
||||
"/** Resolve a key id to its raw signing material, or throw if the id is unknown. */\n"
|
||||
"export function keyForId(kid: string): Buffer {\n"
|
||||
" const hex = KEYRING[kid];\n"
|
||||
" if (!hex) {\n"
|
||||
" throw new Error(`unknown key id: ${JSON.stringify(kid)}`);\n"
|
||||
" }\n"
|
||||
" return Buffer.from(hex, \"hex\");\n"
|
||||
"}\n"
|
||||
)
|
||||
|
||||
|
||||
def make_seed_sh(correct_kid: str, decoys: list[str], vestige_bin_default: str) -> str:
|
||||
d0, d1 = decoys[0], decoys[1]
|
||||
# The memory names the TRIAL's correct kid + decoys. This is the ONLY place
|
||||
# the answer exists; it lives in the Vestige DB, never in the agent checkout.
|
||||
#
|
||||
# CORPUS CONSTRUCTION (verified offline, no paid API, Jul 17 2026, red-team gated):
|
||||
# The CAUSE is a NATURAL rotation runbook written in operational/ownership
|
||||
# vocabulary that genuinely does NOT reuse the crash's words (charge / 500 /
|
||||
# verify / reject / signature). It shares the `active_key` entity with the
|
||||
# FAILURE (that is backfill's causal join key) and names the correct kid.
|
||||
# Around it sit 5 REALISTIC same-neighborhood distractors: real ops incidents
|
||||
# that legitimately discuss signing/verify/500/charge but are NOT the cause
|
||||
# (none tagged `active_key`). A real production history contains exactly these.
|
||||
#
|
||||
# Consequence, proven on real nomic-embed-text embeddings: for the failure
|
||||
# query, pure cosine ranks the 5 distractors ABOVE the cause (cause at rank
|
||||
# ~#6 of 8), so a FAIR top-k RAG (k=3 or k=5) MISSES the cause and fails
|
||||
# honestly -- not because it was crippled, but because the cause is genuinely
|
||||
# semantically distant. Meanwhile `vestige backfill` reaches the cause via the
|
||||
# `active_key` join at a high similarity_rank, and prints a receipt proving a
|
||||
# vector search of the same budget could not have surfaced it. Both halves
|
||||
# hold simultaneously with nothing strawmanned. See cosine_probe.py gate.
|
||||
cause = (
|
||||
f"Rotation runbook, Q3: we migrated the live signer to {correct_kid} and let the old "
|
||||
f"material age out. Every credential minted since the cutover carries the new fingerprint. "
|
||||
f"{d0} was the launch signer and is now decommissioned; {d1} sits pre-provisioned for a "
|
||||
f"scheduled future cutover that has not happened. Anything consuming already-minted "
|
||||
f"credentials must trust {correct_kid} or it will turn away the entire installed base."
|
||||
)
|
||||
# 5 real distractors that genuinely live in the symptom's neighborhood
|
||||
# (signing / verify / 500 / charge), none of which is the cause. Textually
|
||||
# closer to the failure than the cause is -- by construction of a real history.
|
||||
d_ops = (
|
||||
f"Closed ticket OPS-522: a staging box failed token verification after a hand-edited "
|
||||
f"config; on-call reset the key id to {d0} and verification recovered. Staging-only, "
|
||||
f"nothing rotated."
|
||||
)
|
||||
d_verify = (
|
||||
"Ledger verifier threw a 500 on charge when the token had no 'kid' header; we added a "
|
||||
"null-check so a malformed token returns 401 instead of crashing the charge path."
|
||||
)
|
||||
d_issuer = (
|
||||
"Identity issuer intermittently returned 500 during token minting under load; root cause "
|
||||
"was a connection-pool timeout to the signing HSM, fixed by raising the pool size. No key "
|
||||
"change."
|
||||
)
|
||||
d_alert = (
|
||||
"Observability: added a Grafana dashboard and a PagerDuty alert on the checkout "
|
||||
"error-rate SLO. When 5xx responses on the billing endpoint cross 2 percent over five "
|
||||
"minutes the on-call engineer is paged. Pure monitoring change, no request path touched."
|
||||
)
|
||||
d_schema = (
|
||||
"Externalize-secrets refactor: identity and ledger now read the active signing key id "
|
||||
"from env instead of a hardcoded default; the schema requires the key id field to be "
|
||||
"present at boot."
|
||||
)
|
||||
failure = (
|
||||
"PRODUCTION OUTAGE: after the externalize-secrets refactor, the identity issuer and the "
|
||||
"ledger verifier both come up with no signing key id selected, so every charge attempt "
|
||||
"throws and the charge path returns a generic 500. No user can be billed. The keyring "
|
||||
"still has all keys; nothing records which key id production was actually issuing tokens "
|
||||
"under."
|
||||
)
|
||||
return f"""#!/usr/bin/env bash
|
||||
# AUTO-GENERATED per-trial seed. The correct key ({correct_kid}) lives ONLY here,
|
||||
# in the Vestige memory — never in the agent's checkout.
|
||||
set -euo pipefail
|
||||
VESTIGE_BIN="${{VESTIGE_BIN:-{vestige_bin_default}}}"
|
||||
DATA_DIR="${{VESTIGE_DATA_DIR:-$(cd "$(dirname "$0")" && pwd)/.vestige-demo-db}}"
|
||||
if [[ ! -x "$VESTIGE_BIN" ]]; then echo "vestige binary not found at $VESTIGE_BIN" >&2; exit 1; fi
|
||||
rm -rf "$DATA_DIR"; mkdir -p "$DATA_DIR"
|
||||
V() {{ "$VESTIGE_BIN" --data-dir "$DATA_DIR" "$@"; }}
|
||||
# CAUSE — natural runbook, distant vocabulary, shares active_key, names {correct_kid}
|
||||
V ingest {json.dumps(cause)} --node-type decision --ago-days 4 --tags "keyring_rotation,active_key" --source "shared/src/keyring.ts"
|
||||
# 5 realistic same-neighborhood distractors (NOT the cause, none tagged active_key)
|
||||
V ingest {json.dumps(d_ops)} --node-type event --ago-days 12 --tags "ops,staging,config" --source "ops/runbook"
|
||||
V ingest {json.dumps(d_verify)} --node-type event --ago-days 9 --tags "ledger_service,verify" --source "ledger-service/src/verify.ts"
|
||||
V ingest {json.dumps(d_issuer)} --node-type event --ago-days 7 --tags "identity_service,hsm" --source "identity-service/src/issuer.ts"
|
||||
V ingest {json.dumps(d_alert)} --node-type event --ago-days 5 --tags "ledger_service,alerting" --source "ledger-service/src/server.ts"
|
||||
V ingest {json.dumps(d_schema)} --node-type event --ago-days 6 --tags "refactor,config_schema" --source "shared/src/config.ts"
|
||||
# 2 pure noise events
|
||||
V ingest "Gateway-Service: raised the per-plan rate limits (free 10, pro 100, enterprise 1000) and moved the counters to an in-memory map. Edge stays stateless." --node-type event --ago-days 3 --tags "gateway_service,rate_limit" --source "gateway-service/src/index.ts"
|
||||
V ingest "Ledger-Service: refactored monthly statement generation to stream pages instead of buffering the whole document. Cut peak memory on large statements." --node-type event --ago-days 2 --tags "ledger_service,statements,pdf"
|
||||
# FAILURE — the production outage (shares active_key with the cause)
|
||||
V ingest {json.dumps(failure)} --node-type event --ago-days 0 --tags "active_key,crash" --source "ledger-service/src/server.ts"
|
||||
"""
|
||||
|
||||
|
||||
def prepare_trial(repo: Path, trial: int, master_seed: int, corpus_out: Path,
|
||||
manifest_out: Path, vestige_bin: str) -> str:
|
||||
# Deterministic RNG per (master_seed, trial) so a trial is exactly reproducible.
|
||||
rng = random.Random(f"{master_seed}:{trial}")
|
||||
|
||||
# 1. GUARANTEE a DISTINCT correct key per trial: shuffle the whole name pool
|
||||
# once from the master seed, and assign trial N its own correct name (the
|
||||
# Nth in the shuffled order). This makes the per-trial correct keys VISIBLY
|
||||
# different across the experiment table (no "it's always k_indus"), while
|
||||
# staying fully reproducible from the master seed. The two DECOYS are drawn
|
||||
# randomly per trial from the remaining pool.
|
||||
pool_order = list(NAME_POOL)
|
||||
random.Random(f"{master_seed}:correct-key-order").shuffle(pool_order)
|
||||
correct_name = pool_order[(trial - 1) % len(pool_order)]
|
||||
decoy_pool = [n for n in NAME_POOL if n != correct_name]
|
||||
n_decoys = max(1, min(KEYRING_SIZE, len(NAME_POOL)) - 1)
|
||||
decoy_names = rng.sample(decoy_pool, n_decoys)
|
||||
names = [correct_name] + decoy_names
|
||||
rng.shuffle(names) # keyring order isn't correct-key-first (no positional tell)
|
||||
correct_kid = f"k_{correct_name}"
|
||||
decoy_kids = [f"k_{n}" for n in decoy_names]
|
||||
|
||||
# 2. Fresh random 32-byte hex per key (seeded from the trial RNG for repro).
|
||||
keys = {n: rng.getrandbits(256).to_bytes(32, "big").hex() for n in names}
|
||||
|
||||
# 3. Rewrite keyring.ts in the live repo AND the snapshot.
|
||||
keyring_ts = make_keyring_ts(keys)
|
||||
(repo / "shared/src/keyring.ts").write_text(keyring_ts)
|
||||
(repo / ".repo-snapshot/shared/src/keyring.ts").write_text(keyring_ts)
|
||||
|
||||
# 4. Regenerate the production corpus under the TRIAL's correct key.
|
||||
correct_hex = keys[correct_name]
|
||||
plans = ["enterprise", "pro", "free"]
|
||||
tokens = []
|
||||
for i in range(CORPUS_SIZE):
|
||||
claims = {"sub": f"user_{1001+i}", "plan": plans[i % len(plans)],
|
||||
"iat": 1700000000 + i * 100, "exp": FAR_FUTURE_EXP}
|
||||
tokens.append({"sub": claims["sub"], "plan": claims["plan"],
|
||||
"token": sign_token(claims, correct_hex)})
|
||||
corpus = {
|
||||
"note": "Already-issued PRODUCTION tokens, signed under this trial's live "
|
||||
"key. Held in the harness, in NO checkout.",
|
||||
"signed_under": correct_kid,
|
||||
"tokens": tokens,
|
||||
}
|
||||
corpus_out.parent.mkdir(parents=True, exist_ok=True)
|
||||
corpus_out.write_text(json.dumps(corpus, indent=2))
|
||||
|
||||
# 5. Re-template the seed so memory names THIS trial's correct kid + decoys.
|
||||
# CRITICAL: write it to BOTH the working repo AND the snapshot. run-sync.sh
|
||||
# seeds the DB from the working repo's seed, but then calls reset-repo.sh
|
||||
# which restores the working tree FROM the snapshot -- if the snapshot's
|
||||
# seed is stale (old key), a later re-seed uses the wrong key and the whole
|
||||
# trial silently seeds memory with a stale key (backfill then surfaces the
|
||||
# wrong kid -> sync ships the wrong key -> failed_still_red). Keeping both in
|
||||
# sync (like the keyring + configs above) is mandatory, not optional.
|
||||
seed_sh = make_seed_sh(correct_kid, decoy_kids, vestige_bin)
|
||||
(repo / ".vestige-seed.sh").write_text(seed_sh)
|
||||
snap_seed = repo / ".repo-snapshot" / ".vestige-seed.sh"
|
||||
if snap_seed.parent.exists():
|
||||
snap_seed.write_text(seed_sh)
|
||||
|
||||
# 6. Ensure configs are BLANK (RED base). Neutralize decoy-hint comments so a
|
||||
# fixed decoy name isn't baked in (the hints named atlas/cirrus originally).
|
||||
for rel, field in [("identity-service/src/config.ts", "activeKid"),
|
||||
("ledger-service/src/config.ts", "trustedKid")]:
|
||||
for base in (repo, repo / ".repo-snapshot"):
|
||||
p = base / rel
|
||||
t = p.read_text()
|
||||
t = re.sub(rf'({field}\s*:\s*)"[^"]*"', r'\g<1>""', t)
|
||||
# strip any comment line that names a specific k_<name> (old decoy hint)
|
||||
t = re.sub(r'\n\s*//[^\n]*k_[a-z]+[^\n]*', '', t)
|
||||
p.write_text(t)
|
||||
|
||||
manifest = {
|
||||
"trial": trial, "master_seed": master_seed,
|
||||
"key_names": names, "correct_kid": correct_kid, "decoy_kids": decoy_kids,
|
||||
"corpus_path": str(corpus_out), "corpus_size": CORPUS_SIZE,
|
||||
"reproduce": f"prepare_trial.py --trial {trial} --master-seed {master_seed}",
|
||||
}
|
||||
manifest_out.parent.mkdir(parents=True, exist_ok=True)
|
||||
manifest_out.write_text(json.dumps(manifest, indent=2))
|
||||
return correct_kid
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--repo", required=True)
|
||||
ap.add_argument("--trial", type=int, required=True)
|
||||
ap.add_argument("--master-seed", type=int, required=True)
|
||||
ap.add_argument("--corpus-out", required=True)
|
||||
ap.add_argument("--manifest-out", required=True)
|
||||
ap.add_argument("--vestige-bin", default="$HOME/vestige/target/release/vestige")
|
||||
a = ap.parse_args()
|
||||
kid = prepare_trial(Path(a.repo), a.trial, a.master_seed, Path(a.corpus_out),
|
||||
Path(a.manifest_out), a.vestige_bin)
|
||||
print(kid) # last line = the trial's correct kid, for the caller to export
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2192
benchmarks/silent-rotation/harness/agent/runner.py
Normal file
2192
benchmarks/silent-rotation/harness/agent/runner.py
Normal file
File diff suppressed because it is too large
Load diff
202
benchmarks/silent-rotation/harness/aggregate-experiment.py
Executable file
202
benchmarks/silent-rotation/harness/aggregate-experiment.py
Executable file
|
|
@ -0,0 +1,202 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Aggregate an N-trial Silent Rotation experiment into a statistical result.
|
||||
|
||||
Reads experiment-<seed>-<N>/trial-*/{anarchy,sync}.json and prints:
|
||||
- a per-trial table (correct key, anarchy verdict, sync verdict, backfill use)
|
||||
- aggregate: anarchy success X/N, sync success Y/N
|
||||
- a one-sided p-value for "sync succeeds more than anarchy" (exact, stdlib only)
|
||||
- the honest headline a senior engineer reads
|
||||
|
||||
"Success" = fleet_verdict == 'fixed_correctly' (green AND production-safe AND
|
||||
right key). Anything else (merge conflict, still-red, green_but_voids_prod) is a
|
||||
failure. This is the outcome that matters: shipped a correct, production-safe fix.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def is_success(v: dict) -> bool:
|
||||
return v.get("fleet_verdict") == "fixed_correctly"
|
||||
|
||||
|
||||
def binom_p_at_least(k: int, n: int, p: float) -> float:
|
||||
"""P(X >= k) for X ~ Binomial(n, p). Exact via stdlib."""
|
||||
if k <= 0:
|
||||
return 1.0
|
||||
total = 0.0
|
||||
for i in range(k, n + 1):
|
||||
total += math.comb(n, i) * (p ** i) * ((1 - p) ** (n - i))
|
||||
return total
|
||||
|
||||
|
||||
def main():
|
||||
expdir = Path(sys.argv[1])
|
||||
n = int(sys.argv[2])
|
||||
# Which arms to aggregate. Defaults to the 3-arm set; a 2-arm run (anarchy
|
||||
# sync) still works because a missing rag.json is tolerated per trial.
|
||||
import os as _os
|
||||
arms = _os.environ.get("ARMS", "anarchy rag sync").split()
|
||||
rows = []
|
||||
succ = {arm: 0 for arm in arms}
|
||||
tot_tok = {arm: 0 for arm in arms}
|
||||
tot_cost = {arm: 0.0 for arm in arms}
|
||||
# Per-arm MEMORY-LAYER HEALTH. Without this, an arm whose backend never
|
||||
# answered renders as a plain "0/5" -- visually identical to a real
|
||||
# retrieval loss. That is exactly how three dead competitor arms
|
||||
# (hindsight 19/19 errors, zep 22/22, mem0 14/15) were published as
|
||||
# substantive defeats in earlier runs. A trial only counts as a real
|
||||
# measurement if that arm's memory layer actually answered.
|
||||
dead_trials = {arm: 0 for arm in arms} # memory layer never answered
|
||||
live_trials = {arm: 0 for arm in arms} # memory layer answered at least once
|
||||
no_data_trials = {arm: 0 for arm in arms} # arm produced no result JSON at all
|
||||
for t in range(1, n + 1):
|
||||
td = expdir / f"trial-{t}"
|
||||
try:
|
||||
man = json.load(open(td / "manifest.json"))
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
rows.append((t, "?", {arm: "MISSING" for arm in arms}, "?"))
|
||||
continue
|
||||
verdicts = {}
|
||||
for arm in arms:
|
||||
try:
|
||||
d = json.load(open(td / f"{arm}.json"))
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
# No JSON at all: the arm never produced a result (crashed before
|
||||
# writing, or the run was stopped before reaching it). Either way
|
||||
# this trial is NOT a measurement of that product.
|
||||
verdicts[arm] = "MISSING"
|
||||
if arm != "anarchy":
|
||||
no_data_trials[arm] += 1
|
||||
continue
|
||||
verdicts[arm] = d.get("fleet_verdict", "?")
|
||||
succ[arm] += int(is_success(d))
|
||||
# anarchy has NO memory tool by design, so it is never "dead".
|
||||
if arm != "anarchy":
|
||||
if (d.get("memory_unavailable") is True
|
||||
or d.get("memory_layer_alive") is False
|
||||
or d.get("fleet_verdict") == "errored"):
|
||||
dead_trials[arm] += 1
|
||||
verdicts[arm] = "MEMORY_UNAVAILABLE"
|
||||
elif d.get("memory_layer_alive") is True:
|
||||
live_trials[arm] += 1
|
||||
tot_tok[arm] += d.get("fleet_tokens", {}).get("total", 0) or 0
|
||||
tot_cost[arm] += d.get("fleet_cost_usd", 0) or 0
|
||||
# backfill count comes from the sync arm (the memory arm)
|
||||
bf = "?"
|
||||
try:
|
||||
s = json.load(open(td / "sync.json"))
|
||||
bf = f"{s.get('agents_used_vestige_backfill', '?')}"
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
pass
|
||||
rows.append((t, man.get("correct_kid", "?"), verdicts, bf))
|
||||
|
||||
# Display labels per arm. Any arm not listed falls back to its uppercased
|
||||
# name (render is robust to new arms).
|
||||
LABEL = {"anarchy": "ANARCHY (no mem)", "rag": "RAG (similarity)",
|
||||
"sync": "SYNC (Vestige)", "mem0": "MEM0 (mem0ai)",
|
||||
"supermemory": "SUPERMEMORY", "hindsight": "HINDSIGHT",
|
||||
"zep": "ZEP (Graphiti KG)"}
|
||||
line = "=" * (24 + 26 * len(arms) + 10)
|
||||
print(line)
|
||||
print(" SILENT ROTATION — N-TRIAL RANDOMIZED BENCHMARK (per-trial)")
|
||||
print(line)
|
||||
hdr = f" {'trial':<6} {'correct key':<14} "
|
||||
hdr += "".join(f"{LABEL.get(a, a.upper()):<26}" for a in arms)
|
||||
hdr += "backfill"
|
||||
print(hdr)
|
||||
print(" " + "-" * (len(hdr) - 2))
|
||||
for (t, kid, verdicts, bf) in rows:
|
||||
rowstr = f" {t:<6} {kid:<14} "
|
||||
rowstr += "".join(f"{verdicts.get(a, '?'):<26}" for a in arms)
|
||||
rowstr += bf
|
||||
print(rowstr)
|
||||
print(line)
|
||||
|
||||
# Cost + tokens per arm (measured, from each trial's fleet_cost_usd / fleet_tokens).
|
||||
print(f" {'':<20}" + "".join(f"{LABEL.get(a, a.upper()):>26}" for a in arms))
|
||||
print(f" {'Total tokens':<20}" + "".join(f"{tot_tok[a]:>26,}" for a in arms))
|
||||
print(f" {'Total cost (USD)':<20}" + "".join(f"{('$%.4f' % tot_cost[a]):>26}" for a in arms))
|
||||
grand_cost = sum(tot_cost.values())
|
||||
grand_tok = sum(tot_tok.values())
|
||||
print(f" {'Experiment total':<20} {('$%.4f' % grand_cost)} ({grand_tok:,} tokens across {len(arms)} arms)")
|
||||
print(line)
|
||||
|
||||
for arm in arms:
|
||||
# A score is only honest if that arm's memory layer actually answered.
|
||||
# Flag any arm with dead trials LOUDLY and inline -- never let a crash
|
||||
# masquerade as a retrieval loss in the published table.
|
||||
if arm != "anarchy" and (dead_trials[arm] or no_data_trials[arm]):
|
||||
bad = dead_trials[arm] + no_data_trials[arm]
|
||||
flag = (f" <-- !! {bad}/{n} trials NOT A MEASUREMENT "
|
||||
f"(dead={dead_trials[arm]} no-data={no_data_trials[arm]}; "
|
||||
f"backend never answered -- do NOT publish this score)")
|
||||
else:
|
||||
flag = ""
|
||||
print(f" {LABEL.get(arm, arm.upper()):<28} shipped correct+prod-safe: {succ[arm]}/{n}{flag}")
|
||||
# Explicit per-arm health line so a reader never has to open the raw JSON.
|
||||
print()
|
||||
print(" MEMORY-LAYER HEALTH (trials where the arm's backend actually answered):")
|
||||
for arm in arms:
|
||||
if arm == "anarchy":
|
||||
print(f" {LABEL.get(arm, arm.upper()):<28} n/a (no memory tool by design)")
|
||||
else:
|
||||
bad = dead_trials[arm] + no_data_trials[arm]
|
||||
state = "OK" if bad == 0 and live_trials[arm] == n else (
|
||||
"UNUSABLE" if live_trials[arm] == 0 else "DEGRADED")
|
||||
print(f" {LABEL.get(arm, arm.upper()):<28} live {live_trials[arm]}/{n}"
|
||||
f" dead {dead_trials[arm]}/{n} no-data {no_data_trials[arm]}/{n} [{state}]")
|
||||
anarchy_succ = succ.get("anarchy", 0)
|
||||
sync_succ = succ.get("sync", 0)
|
||||
|
||||
# One-sided significance under the NULL that memory does nothing: a memoryless
|
||||
# agent's chance of blindly picking the correct key is 1/K where K = keyring
|
||||
# size (recovered from a trial manifest). With a large keyring this null is
|
||||
# tiny, so even one clean sync trial is significant, and a full sweep is
|
||||
# crushing. p0 = 1/K is a CONSERVATIVE bound (the fleet must also AGREE).
|
||||
keyring_size = 3
|
||||
try:
|
||||
m1 = json.load(open(expdir / "trial-1" / "manifest.json"))
|
||||
keyring_size = max(2, len(m1.get("key_names", [])) or 3)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
pass
|
||||
p0 = 1.0 / keyring_size
|
||||
pval = binom_p_at_least(sync_succ, n, p0)
|
||||
print(f" keyring size (K): {keyring_size} -> blind-guess success = 1/{keyring_size} = {100.0/keyring_size:.1f}%")
|
||||
print(f" p-value (sync successes vs blind-guess null p0=1/{keyring_size}): {pval:.4g}")
|
||||
if anarchy_succ == 0 and sync_succ == n and n >= 3:
|
||||
print(f" => Clean separation: anarchy 0/{n}, sync {n}/{n}. "
|
||||
f"Under the null, that is p = (1/{keyring_size})^{n} = {p0**n:.2e}.")
|
||||
print(line)
|
||||
# The headline is now DERIVED, never asserted. It used to be four
|
||||
# unconditional print() calls with no reference to the data, so it fired
|
||||
# even when the numbers contradicted it -- it already sat directly under
|
||||
# "RAG 2/5" in the GLM summary, and it would have printed unchanged had
|
||||
# Vestige scored 0/5. A constant is not a finding.
|
||||
if anarchy_succ == 0 and sync_succ == n and n >= 3:
|
||||
print(" HEADLINE: With the correct key RANDOMIZED and absent from every file")
|
||||
print(" the agent can read, the memoryless fleet did not ship a")
|
||||
print(" production-safe fix in any trial; the Vestige-coordinated fleet did")
|
||||
print(f" in all {n}. Same model, same corpus, same turn budget.")
|
||||
else:
|
||||
print(f" RESULT: anarchy {anarchy_succ}/{n}, sync (Vestige) {sync_succ}/{n}.")
|
||||
print(" This run did NOT produce clean separation -- report the numbers as")
|
||||
print(" measured. Do not reuse the clean-separation headline.")
|
||||
# Fairness qualifiers that must travel WITH the numbers, always.
|
||||
print(line)
|
||||
print(" METHOD NOTES (must be published alongside any number above):")
|
||||
print(" - The Vestige arm additionally has a vestige_log WRITE tool for fleet")
|
||||
print(" coordination that no competitor arm is given. Transcripts show the")
|
||||
print(" key was recovered by the read path (backfill) before any log write.")
|
||||
print(" - Vestige's causal join follows a shared 'active_key' tag that the")
|
||||
print(" seed places on the cause and the failure. Competitor arms receive")
|
||||
print(" the corpus with tag metadata stripped.")
|
||||
print(" - Any arm whose memory layer returned zero successful retrievals is")
|
||||
print(" memory_unavailable, NOT a retrieval loss. Check memory_layer_alive")
|
||||
print(" in each arm's JSON before quoting its score.")
|
||||
print(line)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
92
benchmarks/silent-rotation/harness/run-anarchy.sh
Executable file
92
benchmarks/silent-rotation/harness/run-anarchy.sh
Executable file
|
|
@ -0,0 +1,92 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# run-anarchy.sh -- PHASE 1 of the multi-agent flagship demo (the CONTROL group).
|
||||
#
|
||||
# Unleashes a FLEET of N coding agents (default 3) on the SAME torture-v2 bug
|
||||
# with NO shared memory. Each works in its own checkout and independently guesses
|
||||
# a fix DIRECTION for the direction-ambiguous canonical-digest drift. Their edits
|
||||
# are then integrated into one shared tree with a real merge: divergent
|
||||
# directions collide and the integrated repo fails. Emits the MEASURED fleet
|
||||
# tokens/cost/wall-clock/conflict-count/final status to results/anarchy.json.
|
||||
#
|
||||
# Nothing is scripted to fail. If the required key is missing this ERRORS.
|
||||
#
|
||||
# Required env (by provider):
|
||||
# PROVIDER=anthropic (default) -> ANTHROPIC_API_KEY (or an `ant auth login` profile)
|
||||
# PROVIDER=openai -> OPENAI_API_KEY (GPT-5.6 Sol)
|
||||
# Optional env (see README.md):
|
||||
# FLEET_SIZE (default 3), MODEL, COST_PER_MTOK_INPUT/OUTPUT, MAX_ITERATIONS,
|
||||
# MAX_TOKENS, TEST_CMD, TORTURE_REPO, PYTHON
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="${TORTURE_REPO:-$HERE/../torture-v2}"
|
||||
OUT="$HERE/results/anarchy.json"
|
||||
PY="${PYTHON:-python3}"
|
||||
PROVIDER="${PROVIDER:-anthropic}"
|
||||
|
||||
# Provider-aware key guard. Never fabricate.
|
||||
if [ "$PROVIDER" = "mock" ]; then
|
||||
: # $0 scripted provider for wiring verification -- no key needed.
|
||||
elif [ "$PROVIDER" = "deepseek" ]; then
|
||||
if [ -z "${DEEPSEEK_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=deepseek but DEEPSEEK_API_KEY is not set. Refusing to fabricate numbers." >&2
|
||||
echo " Set DEEPSEEK_API_KEY (DeepSeek direct api.deepseek.com, for deepseek-v4-pro)." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "openai" ]; then
|
||||
if [ -z "${OPENAI_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=openai but OPENAI_API_KEY is not set." >&2
|
||||
echo " Set OPENAI_API_KEY (for gpt-5.6-sol). Refusing to fabricate numbers." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "openrouter" ]; then
|
||||
if [ -z "${OPENROUTER_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=openrouter but OPENROUTER_API_KEY is not set. Refusing to fabricate numbers." >&2
|
||||
echo " Set OPENROUTER_API_KEY (for moonshotai/kimi-k2.7-code)." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "moonshot" ]; then
|
||||
if [ -z "${MOONSHOT_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=moonshot but MOONSHOT_API_KEY is not set. Refusing to fabricate numbers." >&2
|
||||
echo " Set MOONSHOT_API_KEY (Moonshot direct api.moonshot.ai/v1, for kimi-k3)." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "anthropic" ]; then
|
||||
if [ -z "${ANTHROPIC_API_KEY:-}" ] && ! (command -v ant >/dev/null 2>&1 && ant auth status >/dev/null 2>&1); then
|
||||
echo "ERROR: ANTHROPIC_API_KEY is not set and no 'ant' auth profile is active." >&2
|
||||
echo " Set ANTHROPIC_API_KEY or run 'ant auth login'. Refusing to fabricate numbers." >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
echo "ERROR: unknown PROVIDER '$PROVIDER'. Use 'anthropic', 'openai', 'openrouter', or 'moonshot'." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ! -d "$REPO" ]; then
|
||||
echo "ERROR: torture repo not found at $REPO (set TORTURE_REPO)." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$HERE/results"
|
||||
|
||||
echo "== ANARCHY fleet run (Phase 1: control, NO shared memory) =="
|
||||
echo "repo: $REPO"
|
||||
echo "provider: $PROVIDER"
|
||||
echo "model: ${MODEL:-$([ "$PROVIDER" = openai ] && echo gpt-5.6-sol || { [ "$PROVIDER" = openrouter ] && echo moonshotai/kimi-k2.7-code || echo claude-opus-4-8; })}"
|
||||
echo "fleet size: ${FLEET_SIZE:-3}"
|
||||
echo "out: $OUT"
|
||||
echo
|
||||
|
||||
# Ensure deps are present once (the fleet symlinks node_modules per checkout),
|
||||
# then reset the base repo to its pristine BROKEN state + refresh the snapshot.
|
||||
if [ ! -d "$REPO/node_modules" ]; then
|
||||
echo "installing repo deps (npm install)..."
|
||||
(cd "$REPO" && npm install >/dev/null 2>&1) || {
|
||||
echo "ERROR: npm install failed in $REPO" >&2; exit 2; }
|
||||
fi
|
||||
TORTURE_REPO="$REPO" bash "$HERE/reset-repo.sh"
|
||||
echo
|
||||
|
||||
exec "$PY" "$HERE/agent/fleet_runner.py" --mode anarchy --repo "$REPO" --out "$OUT"
|
||||
182
benchmarks/silent-rotation/harness/run-experiment.sh
Executable file
182
benchmarks/silent-rotation/harness/run-experiment.sh
Executable file
|
|
@ -0,0 +1,182 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# run-experiment.sh — the N-TRIAL randomized Silent Rotation benchmark.
|
||||
#
|
||||
# Runs N independent trials. Each trial: a fresh RANDOM correct key (name + hex),
|
||||
# regenerated production corpus + memory seed, then BOTH arms (anarchy, sync)
|
||||
# against that identical randomized setup. Aggregates into a statistical result
|
||||
# a skeptic cannot dismiss as luck: "anarchy failed X/N, sync succeeded Y/N",
|
||||
# a per-trial table, and a one-sided p-value.
|
||||
#
|
||||
# Because the correct key is RANDOM per trial and provably absent from the repo
|
||||
# (leak-audited every trial), nobody can claim the agents were tuned to one key.
|
||||
# A fixed --master-seed makes the whole experiment exactly reproducible.
|
||||
#
|
||||
# Required: OPENAI_API_KEY (openai path). Optional env:
|
||||
# N_TRIALS (default 5), MASTER_SEED (default 1337), FLEET_SIZE (default 3),
|
||||
# OPENAI_REASONING_EFFORT (default max), OPENAI_REASONING_SUMMARY (default detailed)
|
||||
#
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="${TORTURE_REPO:-$HERE/../torture-v3.5}"
|
||||
PY="${PYTHON:-python3}"
|
||||
N_TRIALS="${N_TRIALS:-5}"
|
||||
MASTER_SEED="${MASTER_SEED:-1337}"
|
||||
export DEMO_PROFILE=keyring
|
||||
export PROVIDER="${PROVIDER:-openai}"
|
||||
export OPENAI_REASONING_EFFORT="${OPENAI_REASONING_EFFORT:-max}"
|
||||
export OPENAI_REASONING_SUMMARY="${OPENAI_REASONING_SUMMARY:-detailed}"
|
||||
export FLEET_SIZE="${FLEET_SIZE:-3}"
|
||||
|
||||
if [ "$PROVIDER" = "openai" ] && [ -z "${OPENAI_API_KEY:-}" ]; then
|
||||
echo "ERROR: OPENAI_API_KEY not set." >&2; exit 2
|
||||
fi
|
||||
|
||||
VESTIGE_BIN="${VESTIGE_BIN:-$HOME/vestige/target/release/vestige}"
|
||||
export VESTIGE_BIN
|
||||
EXPDIR="$HERE/experiment-$(printf '%s' "$MASTER_SEED")-$N_TRIALS"
|
||||
mkdir -p "$EXPDIR"
|
||||
|
||||
echo "=============================================================="
|
||||
echo " SILENT ROTATION — N-TRIAL RANDOMIZED BENCHMARK"
|
||||
echo " trials: $N_TRIALS master seed: $MASTER_SEED fleet: $FLEET_SIZE"
|
||||
echo " provider: $PROVIDER reasoning: $OPENAI_REASONING_EFFORT"
|
||||
echo " results -> $EXPDIR"
|
||||
echo "=============================================================="
|
||||
|
||||
for T in $(seq 1 "$N_TRIALS"); do
|
||||
echo; echo "########## TRIAL $T / $N_TRIALS ##########"
|
||||
TRIALDIR="$EXPDIR/trial-$T"; mkdir -p "$TRIALDIR"
|
||||
CORPUS="$TRIALDIR/prod-corpus.json"
|
||||
MANIFEST="$TRIALDIR/manifest.json"
|
||||
# Clear ALL prior results/transcripts so nothing from a previous trial can
|
||||
# cross-contaminate this one (stale transcript / stale result JSON).
|
||||
rm -f "$HERE"/results/*.json
|
||||
|
||||
# 1. Randomize this trial (fresh key/corpus/seed) + get the correct kid.
|
||||
CORRECT_KID="$("$PY" "$HERE/agent/prepare_trial.py" --repo "$REPO" --trial "$T" \
|
||||
--master-seed "$MASTER_SEED" --corpus-out "$CORPUS" --manifest-out "$MANIFEST" \
|
||||
--vestige-bin "$VESTIGE_BIN" | tail -1)"
|
||||
export CORRECT_KID PROD_CORPUS="$CORPUS"
|
||||
echo " correct key (in memory only): $CORRECT_KID"
|
||||
|
||||
# 2. prepare_trial ALREADY wrote the randomized keyring/configs into BOTH the
|
||||
# live repo AND .repo-snapshot, so the snapshot is correct as-is. Do NOT call
|
||||
# reset-repo.sh here: if the repo is under git (it is, for backups), reset-repo
|
||||
# does `git checkout -- .` which REVERTS the randomization to the committed
|
||||
# keys — silently breaking every trial. The snapshot from prepare_trial is
|
||||
# the source of truth for agent checkouts.
|
||||
|
||||
# 3. All arms against the SAME randomized trial (anarchy, then rag, then sync).
|
||||
# run-anarchy/run-rag/run-sync honor TORTURE_REPO + DEMO_PROFILE + CORRECT_KID
|
||||
# + PROD_CORPUS. Set ARMS="anarchy sync" to skip rag (2-arm), default is all 3.
|
||||
# CRITICAL: clear results/ BEFORE each arm so an errored run can NEVER leave a
|
||||
# stale previous-trial JSON for us to copy (which would silently corrupt a trial).
|
||||
# Set VERBOSE=1 to stream each arm's live output (recommended for a single
|
||||
# trial you want to watch); default is quiet so a multi-trial batch isn't a
|
||||
# wall of text. Either way, a per-arm summary line prints below.
|
||||
ARMS="${ARMS:-anarchy rag sync}"
|
||||
for ARM in $ARMS; do
|
||||
echo " -- $ARM --"
|
||||
rm -f "$HERE/results/$ARM.json"
|
||||
# EVICT every resident ollama model at the arm boundary. Ollama keeps a model
|
||||
# loaded after use, and arms use DIFFERENT local models (zep -> qwen2.5-graphiti
|
||||
# ~10GB, hindsight -> llama3.1:8b ~6.8GB). With OLLAMA_NUM_PARALLEL=1 on a single
|
||||
# GPU, arm N's residency becomes arm N+1's latency. That is exactly what killed
|
||||
# the Jul 21 Kimi run: trial-1 hindsight was healthy (ok=6 err=0, 148s) because it
|
||||
# ran BEFORE zep ever loaded qwen2.5-graphiti; by trial 2 that 10GB model was still
|
||||
# resident, hindsight's retain() contended against it, blew the 900s ingest
|
||||
# timeout, and the arm reported ok=0 err=8 memory_layer_alive=False. The bug only
|
||||
# appears from the SECOND trial onward, so it reads as random flakiness.
|
||||
"$PY" - <<'EVICT' 2>/dev/null || true
|
||||
import json, urllib.request
|
||||
try:
|
||||
with urllib.request.urlopen("http://localhost:11434/api/ps", timeout=5) as r:
|
||||
models = [m["name"] for m in json.load(r).get("models", [])]
|
||||
except Exception:
|
||||
models = []
|
||||
for name in models:
|
||||
# keep_alive=0 tells ollama to unload this model immediately.
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
"http://localhost:11434/api/generate",
|
||||
data=json.dumps({"model": name, "keep_alive": 0}).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
urllib.request.urlopen(req, timeout=30).read()
|
||||
print(f" (evicted {name} from ollama)")
|
||||
except Exception as exc:
|
||||
print(f" (could not evict {name}: {type(exc).__name__})")
|
||||
EVICT
|
||||
if [ "${VERBOSE:-0}" = "1" ]; then
|
||||
TORTURE_REPO="$REPO" bash "$HERE/run-$ARM.sh" 2>&1 | sed 's/^/ /' || true
|
||||
else
|
||||
TORTURE_REPO="$REPO" bash "$HERE/run-$ARM.sh" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -f "$HERE/results/$ARM.json" ]; then
|
||||
cp "$HERE/results/$ARM.json" "$TRIALDIR/$ARM.json"
|
||||
# Always print a one-line per-arm summary (verdict + each agent's chosen key)
|
||||
# so a quiet run still shows what happened without opening the JSON.
|
||||
"$PY" -c "
|
||||
import json
|
||||
d=json.load(open('$TRIALDIR/$ARM.json'))
|
||||
dirs=d.get('fix_directions',{})
|
||||
bf=sum(1 for a in d.get('agents',[]) if a.get('used_vestige_backfill'))
|
||||
rg=sum(1 for a in d.get('agents',[]) if a.get('used_rag_search'))
|
||||
print(' -> %s | keys=%s | backfill=%d rag=%d | prod_replay=%s' % (
|
||||
d.get('fleet_verdict','?'), dirs, bf, rg, d.get('prod_replay_pass')))
|
||||
" 2>/dev/null || true
|
||||
# SHOW_REASONING=1 -> print EVERY agent's FULL chain-of-thought for this arm,
|
||||
# per trial. This is the WHY a skeptic/investor needs to SEE: bare agents
|
||||
# rationalizing a guess vs Vestige agents citing the retrieved key. The
|
||||
# reasoning is captured per-turn in the transcript-<arm>-a*.json files; this
|
||||
# surfaces it live instead of leaving it buried on disk.
|
||||
if [ "${SHOW_REASONING:-0}" = "1" ]; then
|
||||
for TR in "$HERE"/results/transcript-"$ARM"-a*.json; do
|
||||
[ -f "$TR" ] || continue
|
||||
"$PY" -c "
|
||||
import json, os, sys
|
||||
f = '$TR'
|
||||
try:
|
||||
d = json.load(open(f))
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
aid = os.path.basename(f).replace('transcript-', '').replace('.json', '')
|
||||
print()
|
||||
print(' ' + '=' * 72)
|
||||
print(' REASONING [%s] status=%s key=%s' % (
|
||||
aid, d.get('status', '?'),
|
||||
(d.get('final_contested_signatures') or {})))
|
||||
print(' ' + '=' * 72)
|
||||
for i, t in enumerate(d.get('turns', []), 1):
|
||||
r = (t.get('reasoning') or '').strip()
|
||||
tools = ', '.join(c.get('name', '?') for c in t.get('tool_calls', []))
|
||||
if r:
|
||||
for line in r.splitlines():
|
||||
print(' ' + line)
|
||||
if tools:
|
||||
print(' -> tools: ' + tools)
|
||||
if r or tools:
|
||||
print()
|
||||
" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
else
|
||||
echo "{\"fleet_verdict\":\"errored\",\"note\":\"run-$ARM produced no result JSON\"}" > "$TRIALDIR/$ARM.json"
|
||||
echo " -> ERRORED (no result JSON)"
|
||||
fi
|
||||
done
|
||||
|
||||
# copy transcripts for this trial (the WHY, per agent)
|
||||
cp "$HERE"/results/transcript-*.json "$TRIALDIR/" 2>/dev/null || true
|
||||
|
||||
LINE=" trial $T:"
|
||||
for ARM in $ARMS; do
|
||||
V="$("$PY" -c "import json;print(json.load(open('$TRIALDIR/$ARM.json')).get('fleet_verdict','?'))")"
|
||||
LINE="$LINE $ARM=$V"
|
||||
done
|
||||
echo "$LINE"
|
||||
done
|
||||
|
||||
echo; echo "########## AGGREGATE ##########"
|
||||
"$PY" "$HERE/aggregate-experiment.py" "$EXPDIR" "$N_TRIALS" | tee "$EXPDIR/SUMMARY.txt"
|
||||
120
benchmarks/silent-rotation/harness/run-hindsight.sh
Executable file
120
benchmarks/silent-rotation/harness/run-hindsight.sh
Executable file
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# run-hindsight.sh -- the HINDSIGHT (vectorize-io) arm of the multi-agent flagship.
|
||||
#
|
||||
# Runs the SAME fleet on the SAME torture bug, but each agent retrieves through a
|
||||
# self-hosted Hindsight agent-memory server (hindsight-api) over the SAME seeded
|
||||
# corpus every arm gets (_rag_load_facts, exported from the SAME Vestige DB, never
|
||||
# re-authored). Hindsight runs its OWN recommended path: retain() (LLM extraction
|
||||
# on ingest, forced to ollama) + recall() (semantic + BM25 + local cross-encoder
|
||||
# rerank). Verbatim extraction so stored text == corpus body. Fully local
|
||||
# (embedded Postgres, local embedder + reranker). Emits results/hindsight.json.
|
||||
#
|
||||
# Nothing is scripted to win. If a required key/service is missing this ERRORS.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="${TORTURE_REPO:-$HERE/../torture-v3.5}"
|
||||
OUT="$HERE/results/hindsight.json"
|
||||
# hindsight-client lives in the arms venv (Python 3.12); prefer it.
|
||||
if [ -z "${PYTHON:-}" ] && [ -x "$HERE/.venv-arms/bin/python" ]; then
|
||||
PY="$HERE/.venv-arms/bin/python"
|
||||
else
|
||||
PY="${PYTHON:-python3}"
|
||||
fi
|
||||
|
||||
if [ -z "${VESTIGE_BIN:-}" ]; then
|
||||
if [ -x "$HOME/vestige/target/release/vestige" ]; then
|
||||
VESTIGE_BIN="$HOME/vestige/target/release/vestige"
|
||||
elif command -v vestige >/dev/null 2>&1; then
|
||||
VESTIGE_BIN="$(command -v vestige)"
|
||||
else
|
||||
VESTIGE_BIN="vestige"
|
||||
fi
|
||||
fi
|
||||
export VESTIGE_BIN
|
||||
export VESTIGE_DATA_DIR="${VESTIGE_DATA_DIR:-$REPO/.vestige-demo-db}"
|
||||
# hindsight-api is started on :8899 (default :8888 is often taken on this box).
|
||||
# 127.0.0.1, NOT localhost: hindsight-api (uvicorn) binds IPv4 only, while macOS
|
||||
# resolves "localhost" to ::1 FIRST -- so anything holding the IPv6 wildcard on
|
||||
# this port intercepts every call. That actually happened (a stray
|
||||
# `python -m http.server 8899` answered HTTP 501 to every hindsight request while
|
||||
# the real service was healthy on IPv4). Pinning the literal IPv4 address removes
|
||||
# the whole class. NOTE: this export OVERRIDES runner.py's default -- keep in sync.
|
||||
export HINDSIGHT_BASE_URL="${HINDSIGHT_BASE_URL:-http://127.0.0.1:8899}"
|
||||
|
||||
PROVIDER="${PROVIDER:-anthropic}"
|
||||
|
||||
# Provider-aware key guard. Never fabricate.
|
||||
if [ "$PROVIDER" = "mock" ]; then
|
||||
: # $0 scripted provider for wiring verification -- no key needed.
|
||||
elif [ "$PROVIDER" = "deepseek" ]; then
|
||||
[ -n "${DEEPSEEK_API_KEY:-}" ] || { echo "ERROR: PROVIDER=deepseek but DEEPSEEK_API_KEY unset." >&2; exit 2; }
|
||||
elif [ "$PROVIDER" = "openai" ]; then
|
||||
[ -n "${OPENAI_API_KEY:-}" ] || { echo "ERROR: PROVIDER=openai but OPENAI_API_KEY unset." >&2; exit 2; }
|
||||
elif [ "$PROVIDER" = "openrouter" ]; then
|
||||
[ -n "${OPENROUTER_API_KEY:-}" ] || { echo "ERROR: PROVIDER=openrouter but OPENROUTER_API_KEY unset." >&2; exit 2; }
|
||||
elif [ "$PROVIDER" = "moonshot" ]; then
|
||||
[ -n "${MOONSHOT_API_KEY:-}" ] || { echo "ERROR: PROVIDER=moonshot but MOONSHOT_API_KEY unset." >&2; exit 2; }
|
||||
elif [ "$PROVIDER" = "anthropic" ]; then
|
||||
if [ -z "${ANTHROPIC_API_KEY:-}" ] && ! (command -v ant >/dev/null 2>&1 && ant auth status >/dev/null 2>&1); then
|
||||
echo "ERROR: ANTHROPIC_API_KEY unset and no 'ant' auth profile active." >&2; exit 2
|
||||
fi
|
||||
else
|
||||
echo "ERROR: unknown PROVIDER '$PROVIDER'. Use anthropic|openai|openrouter|moonshot|deepseek|mock." >&2; exit 2
|
||||
fi
|
||||
|
||||
if ! command -v "$VESTIGE_BIN" >/dev/null 2>&1 && [ ! -x "$VESTIGE_BIN" ]; then
|
||||
echo "ERROR: vestige CLI not found at '$VESTIGE_BIN'. Set VESTIGE_BIN." >&2; exit 2
|
||||
fi
|
||||
if [ ! -d "$REPO" ]; then
|
||||
echo "ERROR: torture repo not found at $REPO (set TORTURE_REPO)." >&2; exit 2
|
||||
fi
|
||||
|
||||
# Service check: the Hindsight server must be up (separate process the client does
|
||||
# NOT start). Fail loud rather than let every agent's tool call error.
|
||||
if ! curl -sf "$HINDSIGHT_BASE_URL/version" >/dev/null 2>&1; then
|
||||
echo "ERROR: Hindsight server not reachable at $HINDSIGHT_BASE_URL." >&2
|
||||
echo " Start it once (fully local, ollama extraction) -- see" >&2
|
||||
echo " arm-services/hindsight/ or overnight-logs/GLM-MULTIARM-READY.md." >&2
|
||||
exit 2
|
||||
fi
|
||||
# Ollama must be up (retain() extraction LLM). recall() uses local embedder +
|
||||
# reranker (no key), but ingest needs the LLM unless provider=none.
|
||||
if ! curl -sf "http://localhost:11434/api/tags" >/dev/null 2>&1; then
|
||||
echo "ERROR: Ollama not reachable at http://localhost:11434 (needed for retain() extraction)." >&2
|
||||
echo " Start ollama and pull a tool-capable model: ollama pull llama3.1:8b" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$HERE/results"
|
||||
|
||||
echo "== HINDSIGHT fleet run (agent-memory arm: retain + recall, hybrid rerank) =="
|
||||
echo "repo: $REPO"
|
||||
echo "provider: $PROVIDER"
|
||||
echo "python: $PY"
|
||||
echo "fleet size: ${FLEET_SIZE:-3}"
|
||||
echo "vestige: $VESTIGE_BIN (shared corpus: $VESTIGE_DATA_DIR)"
|
||||
echo "hindsight: $HINDSIGHT_BASE_URL"
|
||||
echo "out: $OUT"
|
||||
echo
|
||||
|
||||
SEED="$REPO/.vestige-seed.sh"
|
||||
if [ -f "$SEED" ]; then
|
||||
echo "seeding shared corpus via repo canonical seed: $SEED"
|
||||
VESTIGE_BIN="$VESTIGE_BIN" VESTIGE_DATA_DIR="$VESTIGE_DATA_DIR" bash "$SEED" >/dev/null
|
||||
echo "seed complete ($VESTIGE_DATA_DIR)"
|
||||
echo
|
||||
else
|
||||
echo "ERROR: repo seed script not found at $SEED." >&2; exit 2
|
||||
fi
|
||||
|
||||
if [ ! -d "$REPO/node_modules" ]; then
|
||||
echo "installing repo deps (npm install)..."
|
||||
(cd "$REPO" && npm install >/dev/null 2>&1) || { echo "ERROR: npm install failed in $REPO" >&2; exit 2; }
|
||||
fi
|
||||
TORTURE_REPO="$REPO" bash "$HERE/reset-repo.sh"
|
||||
echo
|
||||
|
||||
exec "$PY" "$HERE/agent/fleet_runner.py" --mode hindsight --repo "$REPO" --out "$OUT"
|
||||
122
benchmarks/silent-rotation/harness/run-mem0.sh
Executable file
122
benchmarks/silent-rotation/harness/run-mem0.sh
Executable file
|
|
@ -0,0 +1,122 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# run-mem0.sh -- the MEM0 arm of the multi-agent flagship demo.
|
||||
#
|
||||
# Runs the SAME fleet of N agents on the SAME torture bug, but each agent has a
|
||||
# mem0 retrieval tool (mem0_search) over the SAME shared corpus every arm gets
|
||||
# (runner._rag_load_facts -- the seeded Vestige DB, PRODUCTION OUTAGE excluded).
|
||||
# mem0 uses its OWN RECOMMENDED path: infer=True LLM fact-extraction on ingest
|
||||
# (llama3.1:8b) + semantic retrieval (nomic-embed-text) + Chroma on-disk. It is
|
||||
# the fair "isn't Vestige just a memory layer?" competitor -- same corpus, same
|
||||
# top-K budget (ARM_TOPK), same 6000-char cap, same output shape as the RAG arm;
|
||||
# only mem0's own extraction+store+retrieval differ. Emits results/mem0.json.
|
||||
#
|
||||
# FULLY LOCAL: no cloud key for the memory layer (ollama does extraction +
|
||||
# embeddings; Chroma is embedded). The only key needed is for the AGENT MODEL
|
||||
# (PROVIDER). PROVIDER=mock runs the whole thing at $0 for wiring verification.
|
||||
#
|
||||
# Nothing is scripted to win. If a required key/binary is missing this ERRORS.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="${TORTURE_REPO:-$HERE/../torture-v3.5}"
|
||||
OUT="$HERE/results/mem0.json"
|
||||
# mem0 needs the Python 3.12 venv with mem0ai installed (system python3 is 3.14,
|
||||
# unsupported by the memory-system ecosystem). Prefer the arms venv.
|
||||
if [ -z "${PYTHON:-}" ] && [ -x "$HERE/.venv-arms/bin/python" ]; then
|
||||
PY="$HERE/.venv-arms/bin/python"
|
||||
else
|
||||
PY="${PYTHON:-python3}"
|
||||
fi
|
||||
|
||||
# Resolve the vestige CLI (used by _rag_load_facts to export the shared corpus).
|
||||
if [ -z "${VESTIGE_BIN:-}" ]; then
|
||||
if [ -x "$HOME/vestige/target/release/vestige" ]; then
|
||||
VESTIGE_BIN="$HOME/vestige/target/release/vestige"
|
||||
elif command -v vestige >/dev/null 2>&1; then
|
||||
VESTIGE_BIN="$(command -v vestige)"
|
||||
else
|
||||
VESTIGE_BIN="vestige"
|
||||
fi
|
||||
fi
|
||||
export VESTIGE_BIN
|
||||
export VESTIGE_DATA_DIR="${VESTIGE_DATA_DIR:-$REPO/.vestige-demo-db}"
|
||||
|
||||
# mem0 knobs (defaults match the audited arm; all overridable).
|
||||
export MEM0_LLM_MODEL="${MEM0_LLM_MODEL:-llama3.1:8b}"
|
||||
export MEM0_EMBED_MODEL="${MEM0_EMBED_MODEL:-nomic-embed-text:latest}"
|
||||
export MEM0_TELEMETRY="${MEM0_TELEMETRY:-false}"
|
||||
export ANONYMIZED_TELEMETRY="${ANONYMIZED_TELEMETRY:-false}"
|
||||
# Fresh, isolated Chroma store per run so a stale collection can't leak.
|
||||
# mem0_qdrant, NOT mem0_chroma: the arm was switched to qdrant, which is mem0's
|
||||
# OWN vendor default and the only one of the two that supports mem0's hybrid BM25
|
||||
# retrieval (on chroma, mem0 itself warns "Hybrid (BM25) scoring will be
|
||||
# disabled"). The two backends' on-disk formats are NOT interchangeable, so a
|
||||
# fresh path also avoids loading a stale chroma dir as if it were qdrant.
|
||||
# NOTE: this export OVERRIDES runner.py's default -- keep the two in sync.
|
||||
export MEM0_STORE_DIR="${MEM0_STORE_DIR:-$REPO/.arm_store/mem0_qdrant}"
|
||||
|
||||
PROVIDER="${PROVIDER:-anthropic}"
|
||||
|
||||
# Provider-aware key guard. Never fabricate.
|
||||
if [ "$PROVIDER" = "mock" ]; then
|
||||
: # $0 scripted provider -- no key needed.
|
||||
elif [ "$PROVIDER" = "openai" ]; then
|
||||
[ -n "${OPENAI_API_KEY:-}" ] || { echo "ERROR: PROVIDER=openai but OPENAI_API_KEY unset." >&2; exit 2; }
|
||||
elif [ "$PROVIDER" = "openrouter" ]; then
|
||||
[ -n "${OPENROUTER_API_KEY:-}" ] || { echo "ERROR: PROVIDER=openrouter but OPENROUTER_API_KEY unset." >&2; exit 2; }
|
||||
elif [ "$PROVIDER" = "moonshot" ]; then
|
||||
[ -n "${MOONSHOT_API_KEY:-}" ] || { echo "ERROR: PROVIDER=moonshot but MOONSHOT_API_KEY unset." >&2; exit 2; }
|
||||
elif [ "$PROVIDER" = "deepseek" ]; then
|
||||
[ -n "${DEEPSEEK_API_KEY:-}" ] || { echo "ERROR: PROVIDER=deepseek but DEEPSEEK_API_KEY unset." >&2; exit 2; }
|
||||
elif [ "$PROVIDER" = "anthropic" ]; then
|
||||
if [ -z "${ANTHROPIC_API_KEY:-}" ] && ! (command -v ant >/dev/null 2>&1 && ant auth status >/dev/null 2>&1); then
|
||||
echo "ERROR: ANTHROPIC_API_KEY unset and no 'ant' auth profile active." >&2; exit 2
|
||||
fi
|
||||
else
|
||||
echo "ERROR: unknown PROVIDER '$PROVIDER'. Use anthropic|openai|openrouter|moonshot|deepseek|mock." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v "$VESTIGE_BIN" >/dev/null 2>&1 && [ ! -x "$VESTIGE_BIN" ]; then
|
||||
echo "ERROR: vestige CLI not found at '$VESTIGE_BIN'. Set VESTIGE_BIN." >&2; exit 2
|
||||
fi
|
||||
if [ ! -d "$REPO" ]; then
|
||||
echo "ERROR: torture repo not found at $REPO (set TORTURE_REPO)." >&2; exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$HERE/results"
|
||||
|
||||
echo "== MEM0 fleet run (mem0ai OSS: infer=True LLM-extraction + semantic retrieval) =="
|
||||
echo "repo: $REPO"
|
||||
echo "provider: $PROVIDER"
|
||||
echo "mem0 LLM: $MEM0_LLM_MODEL embedder: $MEM0_EMBED_MODEL"
|
||||
echo "python: $PY"
|
||||
echo "fleet size: ${FLEET_SIZE:-3}"
|
||||
echo "vestige: $VESTIGE_BIN (shared corpus: $VESTIGE_DATA_DIR)"
|
||||
echo "out: $OUT"
|
||||
echo
|
||||
|
||||
# Seed the shared corpus via the repo's own canonical seed (same for every arm).
|
||||
SEED="$REPO/.vestige-seed.sh"
|
||||
if [ -f "$SEED" ]; then
|
||||
echo "seeding shared corpus via repo canonical seed: $SEED"
|
||||
VESTIGE_BIN="$VESTIGE_BIN" VESTIGE_DATA_DIR="$VESTIGE_DATA_DIR" bash "$SEED" >/dev/null
|
||||
echo "seed complete ($VESTIGE_DATA_DIR)"
|
||||
echo
|
||||
else
|
||||
echo "ERROR: repo seed script not found at $SEED." >&2; exit 2
|
||||
fi
|
||||
|
||||
# Wipe any stale mem0 store so ingest starts clean each run.
|
||||
rm -rf "$MEM0_STORE_DIR"
|
||||
|
||||
if [ ! -d "$REPO/node_modules" ]; then
|
||||
echo "installing repo deps (npm install)..."
|
||||
(cd "$REPO" && npm install >/dev/null 2>&1) || { echo "ERROR: npm install failed in $REPO" >&2; exit 2; }
|
||||
fi
|
||||
TORTURE_REPO="$REPO" bash "$HERE/reset-repo.sh"
|
||||
echo
|
||||
|
||||
exec "$PY" "$HERE/agent/fleet_runner.py" --mode mem0 --repo "$REPO" --out "$OUT"
|
||||
134
benchmarks/silent-rotation/harness/run-rag.sh
Executable file
134
benchmarks/silent-rotation/harness/run-rag.sh
Executable file
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# run-rag.sh -- the DENSE-RAG arm of the multi-agent flagship demo.
|
||||
#
|
||||
# Runs the SAME fleet of N agents on the SAME torture-v2 bug, but this time each
|
||||
# agent has an INDEPENDENT dense-RAG retrieval tool (rag_search) over the SAME
|
||||
# seeded Vestige DB. There is NO shared bus and NO coordination -- this is the
|
||||
# anarchy control PLUS pure top-K cosine retrieval. Each agent embeds its own
|
||||
# query and retrieves the most similar past memories by similarity alone (no
|
||||
# causal join, no vestige_log). It isolates what plain semantic retrieval buys
|
||||
# versus Vestige's causal backfill (run-sync.sh). Emits MEASURED fleet numbers to
|
||||
# results/rag.json.
|
||||
#
|
||||
# Nothing is scripted to win. If a required key/binary is missing this ERRORS.
|
||||
#
|
||||
# Required env:
|
||||
# PROVIDER=anthropic (default) -> ANTHROPIC_API_KEY (or `ant auth login`)
|
||||
# PROVIDER=openai -> OPENAI_API_KEY (GPT-5.6 Sol)
|
||||
# PROVIDER=openrouter -> OPENROUTER_API_KEY (moonshotai/kimi-k2.7-code)
|
||||
# VESTIGE_BIN -> path to the vestige CLI (auto-resolved below)
|
||||
# Optional env (see README.md):
|
||||
# FLEET_SIZE (default 3), MODEL, COST_PER_MTOK_INPUT/OUTPUT, MAX_ITERATIONS,
|
||||
# MAX_TOKENS, TEST_CMD, TORTURE_REPO, VESTIGE_DATA_DIR, PYTHON
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="${TORTURE_REPO:-$HERE/../torture-v2}"
|
||||
OUT="$HERE/results/rag.json"
|
||||
PY="${PYTHON:-python3}"
|
||||
|
||||
# Resolve the vestige CLI to an absolute path (the seed subshell may not inherit
|
||||
# an interactive PATH), preferring an explicit VESTIGE_BIN, then the release
|
||||
# binary, then PATH.
|
||||
if [ -z "${VESTIGE_BIN:-}" ]; then
|
||||
if [ -x "$HOME/vestige/target/release/vestige" ]; then
|
||||
VESTIGE_BIN="$HOME/vestige/target/release/vestige"
|
||||
elif command -v vestige >/dev/null 2>&1; then
|
||||
VESTIGE_BIN="$(command -v vestige)"
|
||||
else
|
||||
VESTIGE_BIN="vestige"
|
||||
fi
|
||||
fi
|
||||
export VESTIGE_BIN
|
||||
|
||||
# The seeded memory DB rag_search retrieves over. Reuse the repo's isolated demo
|
||||
# dir so the fleet retrieves over the same seeded history the demo narrates.
|
||||
export VESTIGE_DATA_DIR="${VESTIGE_DATA_DIR:-$REPO/.vestige-demo-db}"
|
||||
|
||||
PROVIDER="${PROVIDER:-anthropic}"
|
||||
|
||||
# Provider-aware key guard. Never fabricate.
|
||||
if [ "$PROVIDER" = "mock" ]; then
|
||||
: # $0 scripted provider for wiring verification -- no key needed.
|
||||
elif [ "$PROVIDER" = "deepseek" ]; then
|
||||
if [ -z "${DEEPSEEK_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=deepseek but DEEPSEEK_API_KEY is not set. Refusing to fabricate numbers." >&2
|
||||
echo " Set DEEPSEEK_API_KEY (DeepSeek direct api.deepseek.com, for deepseek-v4-pro)." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "openai" ]; then
|
||||
if [ -z "${OPENAI_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=openai but OPENAI_API_KEY is not set." >&2
|
||||
echo " Set OPENAI_API_KEY (for gpt-5.6-sol). Refusing to fabricate numbers." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "openrouter" ]; then
|
||||
if [ -z "${OPENROUTER_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=openrouter but OPENROUTER_API_KEY is not set. Refusing to fabricate numbers." >&2
|
||||
echo " Set OPENROUTER_API_KEY (for moonshotai/kimi-k2.7-code)." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "moonshot" ]; then
|
||||
if [ -z "${MOONSHOT_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=moonshot but MOONSHOT_API_KEY is not set. Refusing to fabricate numbers." >&2
|
||||
echo " Set MOONSHOT_API_KEY (Moonshot direct api.moonshot.ai/v1, for kimi-k3)." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "anthropic" ]; then
|
||||
if [ -z "${ANTHROPIC_API_KEY:-}" ] && ! (command -v ant >/dev/null 2>&1 && ant auth status >/dev/null 2>&1); then
|
||||
echo "ERROR: ANTHROPIC_API_KEY is not set and no 'ant' auth profile is active." >&2
|
||||
echo " Set ANTHROPIC_API_KEY or run 'ant auth login'. Refusing to fabricate numbers." >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
echo "ERROR: unknown PROVIDER '$PROVIDER'. Use 'anthropic', 'openai', 'openrouter', or 'moonshot'." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v "$VESTIGE_BIN" >/dev/null 2>&1 && [ ! -x "$VESTIGE_BIN" ]; then
|
||||
echo "ERROR: vestige CLI not found at '$VESTIGE_BIN'. Set VESTIGE_BIN." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ! -d "$REPO" ]; then
|
||||
echo "ERROR: torture repo not found at $REPO (set TORTURE_REPO)." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$HERE/results"
|
||||
|
||||
echo "== RAG fleet run (dense-RAG arm: independent top-K cosine retrieval) =="
|
||||
echo "repo: $REPO"
|
||||
echo "provider: $PROVIDER"
|
||||
echo "model: ${MODEL:-$([ "$PROVIDER" = openai ] && echo gpt-5.6-sol || { [ "$PROVIDER" = openrouter ] && echo moonshotai/kimi-k2.7-code || echo claude-opus-4-8; })}"
|
||||
echo "fleet size: ${FLEET_SIZE:-3}"
|
||||
echo "vestige: $VESTIGE_BIN (rag index: $VESTIGE_DATA_DIR)"
|
||||
echo "out: $OUT"
|
||||
echo
|
||||
|
||||
# Seed the retrievable memory with the real project history via the repo's own
|
||||
# canonical seed (cause + noise + lookalike distractor + failure). rag_search
|
||||
# retrieves over this exact seeded DB by pure cosine similarity.
|
||||
SEED="$REPO/.vestige-seed.sh"
|
||||
if [ -f "$SEED" ]; then
|
||||
echo "seeding rag index via repo canonical seed: $SEED"
|
||||
VESTIGE_BIN="$VESTIGE_BIN" VESTIGE_DATA_DIR="$VESTIGE_DATA_DIR" bash "$SEED" >/dev/null
|
||||
echo "seed complete ($VESTIGE_DATA_DIR)"
|
||||
echo
|
||||
else
|
||||
echo "ERROR: repo seed script not found at $SEED." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Ensure deps present once, then reset base repo to pristine broken + snapshot.
|
||||
if [ ! -d "$REPO/node_modules" ]; then
|
||||
echo "installing repo deps (npm install)..."
|
||||
(cd "$REPO" && npm install >/dev/null 2>&1) || {
|
||||
echo "ERROR: npm install failed in $REPO" >&2; exit 2; }
|
||||
fi
|
||||
TORTURE_REPO="$REPO" bash "$HERE/reset-repo.sh"
|
||||
echo
|
||||
|
||||
exec "$PY" "$HERE/agent/fleet_runner.py" --mode rag --repo "$REPO" --out "$OUT"
|
||||
193
benchmarks/silent-rotation/harness/run-supermemory.sh
Executable file
193
benchmarks/silent-rotation/harness/run-supermemory.sh
Executable file
|
|
@ -0,0 +1,193 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# run-supermemory.sh -- the SUPERMEMORY arm of the multi-agent flagship demo.
|
||||
#
|
||||
# Runs the SAME fleet of N agents on the SAME torture-v2 bug, but this time each
|
||||
# agent retrieves through a self-hosted supermemory-server (supermemoryai/
|
||||
# supermemory) over the SAME seeded corpus every arm gets (_rag_load_facts,
|
||||
# exported from the SAME Vestige DB -- never re-authored). supermemory runs its
|
||||
# OWN recommended pipeline: local ONNX bge-base-en-v1.5 embeddings + Ollama
|
||||
# gpt-oss:20b extraction on ingest, then its RECOMMENDED document retrieval,
|
||||
# POST /v3/search, top-K. Fully local (localhost:6767, .supermemory dir); nothing
|
||||
# leaves the box. It isolates what a turnkey memory PRODUCT buys versus Vestige's
|
||||
# causal backfill (run-sync.sh). Emits MEASURED fleet numbers to results/supermemory.json.
|
||||
#
|
||||
# Nothing is scripted to win. If a required key/binary/service is missing this ERRORS.
|
||||
#
|
||||
# Required env:
|
||||
# PROVIDER=anthropic (default) -> ANTHROPIC_API_KEY (or `ant auth login`)
|
||||
# PROVIDER=openai -> OPENAI_API_KEY (GPT-5.6 Sol)
|
||||
# PROVIDER=openrouter -> OPENROUTER_API_KEY (moonshotai/kimi-k2.7-code)
|
||||
# VESTIGE_BIN -> path to the vestige CLI (auto-resolved below)
|
||||
# SUPERMEMORY_API_KEY -> the key printed by supermemory-server on first boot
|
||||
# Optional env (see README.md):
|
||||
# FLEET_SIZE (default 3), MODEL, COST_PER_MTOK_INPUT/OUTPUT, MAX_ITERATIONS,
|
||||
# MAX_TOKENS, TEST_CMD, TORTURE_REPO, VESTIGE_DATA_DIR, PYTHON,
|
||||
# SUPERMEMORY_BASE_URL (default http://localhost:6767), SUPERMEMORY_CONTAINER,
|
||||
# OLLAMA_URL (used only for the reachability preflight below)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="${TORTURE_REPO:-$HERE/../torture-v3.5}"
|
||||
OUT="$HERE/results/supermemory.json"
|
||||
# supermemory arm uses urllib (stdlib) -- system python3 is fine, but prefer the
|
||||
# arms venv so it matches the other local-memory arms' interpreter.
|
||||
if [ -z "${PYTHON:-}" ] && [ -x "$HERE/.venv-arms/bin/python" ]; then
|
||||
PY="$HERE/.venv-arms/bin/python"
|
||||
else
|
||||
PY="${PYTHON:-python3}"
|
||||
fi
|
||||
|
||||
# Resolve the vestige CLI to an absolute path (the seed subshell may not inherit
|
||||
# an interactive PATH), preferring an explicit VESTIGE_BIN, then the release
|
||||
# binary, then PATH.
|
||||
if [ -z "${VESTIGE_BIN:-}" ]; then
|
||||
if [ -x "$HOME/vestige/target/release/vestige" ]; then
|
||||
VESTIGE_BIN="$HOME/vestige/target/release/vestige"
|
||||
elif command -v vestige >/dev/null 2>&1; then
|
||||
VESTIGE_BIN="$(command -v vestige)"
|
||||
else
|
||||
VESTIGE_BIN="vestige"
|
||||
fi
|
||||
fi
|
||||
export VESTIGE_BIN
|
||||
|
||||
# The seeded memory the supermemory arm re-ingests. Reuse the repo's isolated demo
|
||||
# dir so the fleet retrieves over the same seeded history the demo narrates.
|
||||
export VESTIGE_DATA_DIR="${VESTIGE_DATA_DIR:-$REPO/.vestige-demo-db}"
|
||||
|
||||
# supermemory service config (mirrors the arm's Python defaults).
|
||||
export SUPERMEMORY_BASE_URL="${SUPERMEMORY_BASE_URL:-http://localhost:6767}"
|
||||
# DO NOT set a default SUPERMEMORY_CONTAINER here. runner.py:627 defaults it to
|
||||
# "benchmark-${CORRECT_KID}" so each trial gets its OWN container -- the same
|
||||
# per-trial isolation every other arm gets. This line previously exported a
|
||||
# FIXED "benchmark" container, which ALWAYS won (os.environ.get found a value)
|
||||
# and silently disabled that isolation: supermemory then searched a store
|
||||
# holding every prior trial's rotation runbook at once, each naming a different
|
||||
# correct key. Verified in trial-1 transcripts, which returned merged OPS-522
|
||||
# docs naming k_antares/k_sirius/k_borealis -- three trials' keys in one result.
|
||||
# That is a handicap no other arm carried. Only export it to pin a container
|
||||
# deliberately (e.g. debugging a single trial).
|
||||
if [ -n "${SUPERMEMORY_CONTAINER:-}" ]; then
|
||||
export SUPERMEMORY_CONTAINER
|
||||
fi
|
||||
|
||||
PROVIDER="${PROVIDER:-anthropic}"
|
||||
|
||||
# Provider-aware key guard. Never fabricate.
|
||||
if [ "$PROVIDER" = "mock" ]; then
|
||||
: # $0 scripted provider for wiring verification -- no key needed.
|
||||
elif [ "$PROVIDER" = "deepseek" ]; then
|
||||
if [ -z "${DEEPSEEK_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=deepseek but DEEPSEEK_API_KEY is not set. Refusing to fabricate numbers." >&2
|
||||
echo " Set DEEPSEEK_API_KEY (DeepSeek direct api.deepseek.com, for deepseek-v4-pro)." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "openai" ]; then
|
||||
if [ -z "${OPENAI_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=openai but OPENAI_API_KEY is not set." >&2
|
||||
echo " Set OPENAI_API_KEY (for gpt-5.6-sol). Refusing to fabricate numbers." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "openrouter" ]; then
|
||||
if [ -z "${OPENROUTER_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=openrouter but OPENROUTER_API_KEY is not set. Refusing to fabricate numbers." >&2
|
||||
echo " Set OPENROUTER_API_KEY (for moonshotai/kimi-k2.7-code)." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "moonshot" ]; then
|
||||
if [ -z "${MOONSHOT_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=moonshot but MOONSHOT_API_KEY is not set. Refusing to fabricate numbers." >&2
|
||||
echo " Set MOONSHOT_API_KEY (Moonshot direct api.moonshot.ai/v1, for kimi-k3)." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "anthropic" ]; then
|
||||
if [ -z "${ANTHROPIC_API_KEY:-}" ] && ! (command -v ant >/dev/null 2>&1 && ant auth status >/dev/null 2>&1); then
|
||||
echo "ERROR: ANTHROPIC_API_KEY is not set and no 'ant' auth profile is active." >&2
|
||||
echo " Set ANTHROPIC_API_KEY or run 'ant auth login'. Refusing to fabricate numbers." >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
echo "ERROR: unknown PROVIDER '$PROVIDER'. Use 'anthropic', 'openai', 'openrouter', or 'moonshot'." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v "$VESTIGE_BIN" >/dev/null 2>&1 && [ ! -x "$VESTIGE_BIN" ]; then
|
||||
echo "ERROR: vestige CLI not found at '$VESTIGE_BIN'. Set VESTIGE_BIN." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ! -d "$REPO" ]; then
|
||||
echo "ERROR: torture repo not found at $REPO (set TORTURE_REPO)." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# --- supermemory service preflight (fail loud, never fabricate) --------------
|
||||
# The arm needs the supermemory-server reachable. When self-hosted locally the
|
||||
# server AUTO-APPLIES its own key for unauthenticated localhost requests, so a
|
||||
# blank SUPERMEMORY_API_KEY is fine locally; if a key IS set we send it. Probe
|
||||
# /v3/documents both ways so either config passes. Also needs Ollama serving the
|
||||
# extraction LLM. Check both BEFORE burning provider tokens.
|
||||
# Probe /v3/health (returns 200 when the server is up). Do NOT probe
|
||||
# /v3/documents?limit=1 -- that path 404s on this server version (documents are
|
||||
# POSTed, not GET-listed), which made curl -f falsely report "not reachable" and
|
||||
# skip the whole arm. The real ingest+search endpoints (POST /v3/documents,
|
||||
# POST /v3/search) are exercised by the arm and verified working.
|
||||
if ! curl -fsS -o /dev/null --max-time 5 "${SUPERMEMORY_BASE_URL}/v3/health" 2>/dev/null \
|
||||
&& ! curl -fsS -o /dev/null --max-time 5 "${SUPERMEMORY_BASE_URL}/" 2>/dev/null; then
|
||||
echo "ERROR: supermemory-server not reachable at ${SUPERMEMORY_BASE_URL}." >&2
|
||||
echo " Start it (fully local) with: SUPERMEMORY_EMBEDDING_PROVIDER=local \\" >&2
|
||||
echo " SUPERMEMORY_LLM_PROVIDER=openai-compatible OPENAI_BASE_URL=http://localhost:11434/v1 \\" >&2
|
||||
echo " OPENAI_API_KEY=ollama SUPERMEMORY_LLM_MODEL=llama3.1:8b PORT=6767 supermemory-server" >&2
|
||||
exit 2
|
||||
fi
|
||||
OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434/api/embeddings}"
|
||||
OLLAMA_ROOT="${OLLAMA_URL%/api/*}"
|
||||
if ! curl -fsS -o /dev/null --max-time 5 "${OLLAMA_ROOT}/api/tags" 2>/dev/null; then
|
||||
echo "ERROR: Ollama not reachable at ${OLLAMA_ROOT} (needed for supermemory's extraction LLM)." >&2
|
||||
echo " Start ollama and 'ollama pull llama3.1:8b'. Refusing to fabricate numbers." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$HERE/results"
|
||||
|
||||
echo "== SUPERMEMORY fleet run (self-hosted supermemory-server, /v3/search) =="
|
||||
echo "repo: $REPO"
|
||||
echo "provider: $PROVIDER"
|
||||
echo "model: ${MODEL:-$([ "$PROVIDER" = openai ] && echo gpt-5.6-sol || { [ "$PROVIDER" = openrouter ] && echo moonshotai/kimi-k2.7-code || echo claude-opus-4-8; })}"
|
||||
echo "fleet size: ${FLEET_SIZE:-3}"
|
||||
echo "vestige: $VESTIGE_BIN (seed corpus: $VESTIGE_DATA_DIR)"
|
||||
# MUST use a default expansion: this script runs under `set -u`, and
|
||||
# SUPERMEMORY_CONTAINER is now DELIBERATELY left unset so runner.py can pick the
|
||||
# per-trial default (benchmark-${CORRECT_KID}) -- see the note above. Referencing
|
||||
# it bare here killed the whole arm with "unbound variable" before it wrote any
|
||||
# result, which the harness then recorded as fleet_verdict=errored.
|
||||
echo "supermemory: $SUPERMEMORY_BASE_URL (container: ${SUPERMEMORY_CONTAINER:-benchmark-${CORRECT_KID:-?} (per-trial default)})"
|
||||
echo "out: $OUT"
|
||||
echo
|
||||
|
||||
# Seed the retrievable memory with the real project history via the repo's own
|
||||
# canonical seed (cause + noise + lookalike distractor + failure). The supermemory
|
||||
# arm re-ingests these EXACT facts (via _rag_load_facts) into its own local store.
|
||||
SEED="$REPO/.vestige-seed.sh"
|
||||
if [ -f "$SEED" ]; then
|
||||
echo "seeding source corpus via repo canonical seed: $SEED"
|
||||
VESTIGE_BIN="$VESTIGE_BIN" VESTIGE_DATA_DIR="$VESTIGE_DATA_DIR" bash "$SEED" >/dev/null
|
||||
echo "seed complete ($VESTIGE_DATA_DIR)"
|
||||
echo
|
||||
else
|
||||
echo "ERROR: repo seed script not found at $SEED." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Ensure deps present once, then reset base repo to pristine broken + snapshot.
|
||||
if [ ! -d "$REPO/node_modules" ]; then
|
||||
echo "installing repo deps (npm install)..."
|
||||
(cd "$REPO" && npm install >/dev/null 2>&1) || {
|
||||
echo "ERROR: npm install failed in $REPO" >&2; exit 2; }
|
||||
fi
|
||||
TORTURE_REPO="$REPO" bash "$HERE/reset-repo.sh"
|
||||
echo
|
||||
|
||||
exec "$PY" "$HERE/agent/fleet_runner.py" --mode supermemory --repo "$REPO" --out "$OUT"
|
||||
132
benchmarks/silent-rotation/harness/run-sync.sh
Executable file
132
benchmarks/silent-rotation/harness/run-sync.sh
Executable file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# run-sync.sh -- PHASE 2 of the multi-agent flagship demo (the VESTIGE group).
|
||||
#
|
||||
# Runs the SAME fleet of N agents on the SAME torture-v2 bug, but this time they
|
||||
# share ONE Vestige DB as a coordination hub. Before editing, each agent reaches
|
||||
# BACKWARD from the failure (vestige_backfill) to the 4-day-old identity-service
|
||||
# canonical-order decision via the canonical_digest join, learns the DIRECTION
|
||||
# (identity is source of truth; the ledger must adapt), and publishes it to the
|
||||
# shared memory (vestige_log) so the fleet converges on the SAME fix. The merge
|
||||
# is clean and the integrated repo goes green. Emits MEASURED fleet numbers to
|
||||
# results/sync.json.
|
||||
#
|
||||
# Nothing is scripted to win. If a required key/binary is missing this ERRORS.
|
||||
#
|
||||
# Required env:
|
||||
# PROVIDER=anthropic (default) -> ANTHROPIC_API_KEY (or `ant auth login`)
|
||||
# PROVIDER=openai -> OPENAI_API_KEY (GPT-5.6 Sol)
|
||||
# VESTIGE_BIN -> path to the vestige CLI (auto-resolved below)
|
||||
# Optional env (see README.md):
|
||||
# FLEET_SIZE (default 3), MODEL, COST_PER_MTOK_INPUT/OUTPUT, MAX_ITERATIONS,
|
||||
# MAX_TOKENS, TEST_CMD, TORTURE_REPO, VESTIGE_DATA_DIR, PYTHON
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="${TORTURE_REPO:-$HERE/../torture-v2}"
|
||||
OUT="$HERE/results/sync.json"
|
||||
PY="${PYTHON:-python3}"
|
||||
|
||||
# Resolve the vestige CLI to an absolute path (the seed subshell may not inherit
|
||||
# an interactive PATH), preferring an explicit VESTIGE_BIN, then the release
|
||||
# binary, then PATH.
|
||||
if [ -z "${VESTIGE_BIN:-}" ]; then
|
||||
if [ -x "$HOME/vestige/target/release/vestige" ]; then
|
||||
VESTIGE_BIN="$HOME/vestige/target/release/vestige"
|
||||
elif command -v vestige >/dev/null 2>&1; then
|
||||
VESTIGE_BIN="$(command -v vestige)"
|
||||
else
|
||||
VESTIGE_BIN="vestige"
|
||||
fi
|
||||
fi
|
||||
export VESTIGE_BIN
|
||||
|
||||
# The shared coordination bus DB. Reuse the repo's isolated demo dir so the fleet
|
||||
# recalls over the same seeded history the demo narrates.
|
||||
export VESTIGE_DATA_DIR="${VESTIGE_DATA_DIR:-$REPO/.vestige-demo-db}"
|
||||
|
||||
PROVIDER="${PROVIDER:-anthropic}"
|
||||
|
||||
# Provider-aware key guard. Never fabricate.
|
||||
if [ "$PROVIDER" = "mock" ]; then
|
||||
: # $0 scripted provider for wiring verification -- no key needed.
|
||||
elif [ "$PROVIDER" = "deepseek" ]; then
|
||||
if [ -z "${DEEPSEEK_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=deepseek but DEEPSEEK_API_KEY is not set. Refusing to fabricate numbers." >&2
|
||||
echo " Set DEEPSEEK_API_KEY (DeepSeek direct api.deepseek.com, for deepseek-v4-pro)." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "openai" ]; then
|
||||
if [ -z "${OPENAI_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=openai but OPENAI_API_KEY is not set." >&2
|
||||
echo " Set OPENAI_API_KEY (for gpt-5.6-sol). Refusing to fabricate numbers." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "openrouter" ]; then
|
||||
if [ -z "${OPENROUTER_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=openrouter but OPENROUTER_API_KEY is not set. Refusing to fabricate numbers." >&2
|
||||
echo " Set OPENROUTER_API_KEY (for moonshotai/kimi-k2.7-code)." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "moonshot" ]; then
|
||||
if [ -z "${MOONSHOT_API_KEY:-}" ]; then
|
||||
echo "ERROR: PROVIDER=moonshot but MOONSHOT_API_KEY is not set. Refusing to fabricate numbers." >&2
|
||||
echo " Set MOONSHOT_API_KEY (Moonshot direct api.moonshot.ai/v1, for kimi-k3)." >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "$PROVIDER" = "anthropic" ]; then
|
||||
if [ -z "${ANTHROPIC_API_KEY:-}" ] && ! (command -v ant >/dev/null 2>&1 && ant auth status >/dev/null 2>&1); then
|
||||
echo "ERROR: ANTHROPIC_API_KEY is not set and no 'ant' auth profile is active." >&2
|
||||
echo " Set ANTHROPIC_API_KEY or run 'ant auth login'. Refusing to fabricate numbers." >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
echo "ERROR: unknown PROVIDER '$PROVIDER'. Use 'anthropic', 'openai', 'openrouter', or 'moonshot'." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v "$VESTIGE_BIN" >/dev/null 2>&1 && [ ! -x "$VESTIGE_BIN" ]; then
|
||||
echo "ERROR: vestige CLI not found at '$VESTIGE_BIN'. Set VESTIGE_BIN." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ! -d "$REPO" ]; then
|
||||
echo "ERROR: torture repo not found at $REPO (set TORTURE_REPO)." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$HERE/results"
|
||||
|
||||
echo "== SYNC fleet run (Phase 2: Vestige shared hub) =="
|
||||
echo "repo: $REPO"
|
||||
echo "provider: $PROVIDER"
|
||||
echo "model: ${MODEL:-$([ "$PROVIDER" = openai ] && echo gpt-5.6-sol || { [ "$PROVIDER" = openrouter ] && echo moonshotai/kimi-k2.7-code || echo claude-opus-4-8; })}"
|
||||
echo "fleet size: ${FLEET_SIZE:-3}"
|
||||
echo "vestige: $VESTIGE_BIN (shared bus: $VESTIGE_DATA_DIR)"
|
||||
echo "out: $OUT"
|
||||
echo
|
||||
|
||||
# Seed the SHARED coordination memory with the real project history via the
|
||||
# repo's own canonical seed (cause + noise + lookalike distractor + failure).
|
||||
SEED="$REPO/.vestige-seed.sh"
|
||||
if [ -f "$SEED" ]; then
|
||||
echo "seeding shared bus via repo canonical seed: $SEED"
|
||||
VESTIGE_BIN="$VESTIGE_BIN" VESTIGE_DATA_DIR="$VESTIGE_DATA_DIR" bash "$SEED" >/dev/null
|
||||
echo "seed complete ($VESTIGE_DATA_DIR)"
|
||||
echo
|
||||
else
|
||||
echo "ERROR: repo seed script not found at $SEED." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Ensure deps present once, then reset base repo to pristine broken + snapshot.
|
||||
if [ ! -d "$REPO/node_modules" ]; then
|
||||
echo "installing repo deps (npm install)..."
|
||||
(cd "$REPO" && npm install >/dev/null 2>&1) || {
|
||||
echo "ERROR: npm install failed in $REPO" >&2; exit 2; }
|
||||
fi
|
||||
TORTURE_REPO="$REPO" bash "$HERE/reset-repo.sh"
|
||||
echo
|
||||
|
||||
exec "$PY" "$HERE/agent/fleet_runner.py" --mode sync --repo "$REPO" --out "$OUT"
|
||||
121
benchmarks/silent-rotation/harness/run-zep.sh
Executable file
121
benchmarks/silent-rotation/harness/run-zep.sh
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# run-zep.sh -- the ZEP / GRAPHITI arm of the multi-agent flagship demo.
|
||||
#
|
||||
# Runs the SAME fleet on the SAME torture bug, but each agent has a temporal-
|
||||
# knowledge-graph retrieval tool (zep_search) over the SAME seeded corpus
|
||||
# (_rag_load_facts). Graphiti (Zep's OSS engine) extracts entities + edges on
|
||||
# ingest (one local ollama LLM call per fact) into a local FalkorDB graph, then
|
||||
# retrieves via hybrid semantic + BM25 + graph RRF. Fully local, NO ZEP_API_KEY.
|
||||
# Emits results/zep.json.
|
||||
#
|
||||
# Nothing is scripted to win. If a required service is missing this ERRORS.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="${TORTURE_REPO:-$HERE/../torture-v3.5}"
|
||||
OUT="$HERE/results/zep.json"
|
||||
# graphiti-core lives in the arms venv (Python 3.12); prefer it.
|
||||
if [ -z "${PYTHON:-}" ] && [ -x "$HERE/.venv-arms/bin/python" ]; then
|
||||
PY="$HERE/.venv-arms/bin/python"
|
||||
else
|
||||
PY="${PYTHON:-python3}"
|
||||
fi
|
||||
|
||||
if [ -z "${VESTIGE_BIN:-}" ]; then
|
||||
if [ -x "$HOME/vestige/target/release/vestige" ]; then
|
||||
VESTIGE_BIN="$HOME/vestige/target/release/vestige"
|
||||
elif command -v vestige >/dev/null 2>&1; then
|
||||
VESTIGE_BIN="$(command -v vestige)"
|
||||
else
|
||||
VESTIGE_BIN="vestige"
|
||||
fi
|
||||
fi
|
||||
export VESTIGE_BIN
|
||||
export VESTIGE_DATA_DIR="${VESTIGE_DATA_DIR:-$REPO/.vestige-demo-db}"
|
||||
export ZEP_FALKOR_HOST="${ZEP_FALKOR_HOST:-localhost}"
|
||||
export ZEP_FALKOR_PORT="${ZEP_FALKOR_PORT:-6379}"
|
||||
# gemma3:12b builds a clean Graphiti graph; llama3.1:8b (8B) emits malformed
|
||||
# edges -> empty retrieval. Graphiti needs a capable extraction model.
|
||||
# qwen2.5-graphiti = qwen2.5:14b + num_ctx 8192 (Modelfile). NOT gemma3:12b:
|
||||
# Zep's own docs require a model with real structured-output support, gemma3 has
|
||||
# no native function-calling and produced malformed graphs (garbage predicates,
|
||||
# orphan edges, dead retrieval). The num_ctx bump is the actual fix -- Ollama
|
||||
# defaults num_ctx to 2048 and silently truncates the FRONT of long prompts,
|
||||
# cutting Graphiti's schema+rules off the top so the model never sees them.
|
||||
# NOTE: this export OVERRIDES runner.py's default, so it must be kept in sync.
|
||||
export ZEP_LLM_MODEL="${ZEP_LLM_MODEL:-qwen2.5-graphiti}"
|
||||
export ZEP_EMBED_MODEL="${ZEP_EMBED_MODEL:-nomic-embed-text}"
|
||||
|
||||
PROVIDER="${PROVIDER:-anthropic}"
|
||||
|
||||
# Provider-aware key guard. Never fabricate.
|
||||
if [ "$PROVIDER" = "mock" ]; then
|
||||
: # $0 scripted provider for wiring verification -- no key needed.
|
||||
elif [ "$PROVIDER" = "deepseek" ]; then
|
||||
[ -n "${DEEPSEEK_API_KEY:-}" ] || { echo "ERROR: PROVIDER=deepseek but DEEPSEEK_API_KEY unset." >&2; exit 2; }
|
||||
elif [ "$PROVIDER" = "openai" ]; then
|
||||
[ -n "${OPENAI_API_KEY:-}" ] || { echo "ERROR: PROVIDER=openai but OPENAI_API_KEY unset." >&2; exit 2; }
|
||||
elif [ "$PROVIDER" = "openrouter" ]; then
|
||||
[ -n "${OPENROUTER_API_KEY:-}" ] || { echo "ERROR: PROVIDER=openrouter but OPENROUTER_API_KEY unset." >&2; exit 2; }
|
||||
elif [ "$PROVIDER" = "moonshot" ]; then
|
||||
[ -n "${MOONSHOT_API_KEY:-}" ] || { echo "ERROR: PROVIDER=moonshot but MOONSHOT_API_KEY unset." >&2; exit 2; }
|
||||
elif [ "$PROVIDER" = "anthropic" ]; then
|
||||
if [ -z "${ANTHROPIC_API_KEY:-}" ] && ! (command -v ant >/dev/null 2>&1 && ant auth status >/dev/null 2>&1); then
|
||||
echo "ERROR: ANTHROPIC_API_KEY unset and no 'ant' auth profile active." >&2; exit 2
|
||||
fi
|
||||
else
|
||||
echo "ERROR: unknown PROVIDER '$PROVIDER'. Use anthropic|openai|openrouter|moonshot|deepseek|mock." >&2; exit 2
|
||||
fi
|
||||
|
||||
if ! command -v "$VESTIGE_BIN" >/dev/null 2>&1 && [ ! -x "$VESTIGE_BIN" ]; then
|
||||
echo "ERROR: vestige CLI not found at '$VESTIGE_BIN'. Set VESTIGE_BIN." >&2; exit 2
|
||||
fi
|
||||
if [ ! -d "$REPO" ]; then
|
||||
echo "ERROR: torture repo not found at $REPO (set TORTURE_REPO)." >&2; exit 2
|
||||
fi
|
||||
|
||||
# Service check: FalkorDB must be listening (Graphiti's graph store). Fail loud.
|
||||
if ! (exec 3<>"/dev/tcp/${ZEP_FALKOR_HOST}/${ZEP_FALKOR_PORT}") 2>/dev/null; then
|
||||
echo "ERROR: FalkorDB not reachable at ${ZEP_FALKOR_HOST}:${ZEP_FALKOR_PORT}." >&2
|
||||
echo " Start it: docker run -d --name falkordb-bench -p 6379:6379 -p 3001:3000 falkordb/falkordb:latest" >&2
|
||||
exit 2
|
||||
fi
|
||||
# Ollama must be up (Graphiti extraction LLM + embeddings).
|
||||
if ! curl -sf "http://localhost:11434/api/tags" >/dev/null 2>&1; then
|
||||
echo "ERROR: Ollama not reachable at http://localhost:11434 (needed for Graphiti extraction + embeddings)." >&2
|
||||
echo " Start ollama and pull: ollama pull llama3.1:8b && ollama pull nomic-embed-text" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$HERE/results"
|
||||
|
||||
echo "== ZEP fleet run (Graphiti temporal knowledge graph, local FalkorDB) =="
|
||||
echo "repo: $REPO"
|
||||
echo "provider: $PROVIDER"
|
||||
echo "python: $PY"
|
||||
echo "fleet size: ${FLEET_SIZE:-3}"
|
||||
echo "vestige: $VESTIGE_BIN (shared corpus: $VESTIGE_DATA_DIR)"
|
||||
echo "falkordb: ${ZEP_FALKOR_HOST}:${ZEP_FALKOR_PORT} LLM: $ZEP_LLM_MODEL embed: $ZEP_EMBED_MODEL"
|
||||
echo "out: $OUT"
|
||||
echo
|
||||
|
||||
SEED="$REPO/.vestige-seed.sh"
|
||||
if [ -f "$SEED" ]; then
|
||||
echo "seeding shared corpus via repo canonical seed: $SEED"
|
||||
VESTIGE_BIN="$VESTIGE_BIN" VESTIGE_DATA_DIR="$VESTIGE_DATA_DIR" bash "$SEED" >/dev/null
|
||||
echo "seed complete ($VESTIGE_DATA_DIR)"
|
||||
echo
|
||||
else
|
||||
echo "ERROR: repo seed script not found at $SEED." >&2; exit 2
|
||||
fi
|
||||
|
||||
if [ ! -d "$REPO/node_modules" ]; then
|
||||
echo "installing repo deps (npm install)..."
|
||||
(cd "$REPO" && npm install >/dev/null 2>&1) || { echo "ERROR: npm install failed in $REPO" >&2; exit 2; }
|
||||
fi
|
||||
TORTURE_REPO="$REPO" bash "$HERE/reset-repo.sh"
|
||||
echo
|
||||
|
||||
exec "$PY" "$HERE/agent/fleet_runner.py" --mode zep --repo "$REPO" --out "$OUT"
|
||||
183
benchmarks/silent-rotation/results/trial-1/anarchy.json
Normal file
183
benchmarks/silent-rotation/results/trial-1/anarchy.json
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
{
|
||||
"mode": "anarchy",
|
||||
"fleet_size": 3,
|
||||
"provider": "moonshot",
|
||||
"model": "kimi-k3",
|
||||
"task": "make the failing end-to-end charge test pass",
|
||||
"repo": "<REPO_ROOT>/torture-v3.5",
|
||||
"measured": true,
|
||||
"fleet_tokens": {
|
||||
"input": 145074,
|
||||
"output": 8770,
|
||||
"total": 153844
|
||||
},
|
||||
"fleet_cost_usd": 0.566772,
|
||||
"cost_rate": {
|
||||
"input_per_mtok": 3.0,
|
||||
"output_per_mtok": 15.0
|
||||
},
|
||||
"fleet_wall_clock_seconds": 162.04,
|
||||
"fleet_iterations_total": 23,
|
||||
"agents_local_pass": 3,
|
||||
"agents_errored": 0,
|
||||
"tokens_uncounted": false,
|
||||
"agents_used_vestige_backfill": 0,
|
||||
"agents_used_vestige_log": 0,
|
||||
"memory_tool": null,
|
||||
"memory_prewarm": {
|
||||
"attempted": false,
|
||||
"ok": true,
|
||||
"error": "",
|
||||
"seconds": 0.0
|
||||
},
|
||||
"agents_memory_alive": 0,
|
||||
"retrieval_ok_total": 0,
|
||||
"retrieval_err_total": 0,
|
||||
"retrieval_empty_total": 0,
|
||||
"retrieval_error_samples": [],
|
||||
"memory_layer_alive": false,
|
||||
"memory_unavailable": false,
|
||||
"transcripts": [
|
||||
"transcript-anarchy-a0.json",
|
||||
"transcript-anarchy-a1.json",
|
||||
"transcript-anarchy-a2.json"
|
||||
],
|
||||
"demo_profile": "keyring",
|
||||
"fix_direction_counts": {
|
||||
"correct": 0,
|
||||
"wrong": 3,
|
||||
"other": 0
|
||||
},
|
||||
"fleet_verdict": "green_but_voids_prod",
|
||||
"integrated_tests_pass": true,
|
||||
"prod_replay_pass": false,
|
||||
"production_replay": {
|
||||
"ok": false,
|
||||
"verified": 0,
|
||||
"total": 5,
|
||||
"failures": [
|
||||
{
|
||||
"sub": "user_1001",
|
||||
"error": "signature mismatch"
|
||||
},
|
||||
{
|
||||
"sub": "user_1002",
|
||||
"error": "signature mismatch"
|
||||
},
|
||||
{
|
||||
"sub": "user_1003",
|
||||
"error": "signature mismatch"
|
||||
},
|
||||
{
|
||||
"sub": "user_1004",
|
||||
"error": "signature mismatch"
|
||||
},
|
||||
{
|
||||
"sub": "user_1005",
|
||||
"error": "signature mismatch"
|
||||
}
|
||||
],
|
||||
"signed_under": "k_nashira",
|
||||
"ran": true
|
||||
},
|
||||
"merge_conflict_count": 0,
|
||||
"merge_conflicts": [],
|
||||
"cross_file_divergence": false,
|
||||
"fix_directions": {
|
||||
"a0": "kid:k_olivine",
|
||||
"a1": "kid:k_olivine",
|
||||
"a2": "kid:k_olivine"
|
||||
},
|
||||
"reasoning_effort": null,
|
||||
"truncated": false,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a0",
|
||||
"mode": "anarchy",
|
||||
"tokens": {
|
||||
"input": 55136,
|
||||
"output": 2856,
|
||||
"total": 57992
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 159.95,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": null,
|
||||
"retrieval_ok_count": 0,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": false,
|
||||
"finish_summary": "Root cause: the \"externalize secrets\" refactor blanked the key id in both identity-service config (activeKid: \"\") and ledger-service config (trustedKid: \"\"), so signSession threw \"no active signing key selected\" (and verification would have failed next). Everything else in the chain was already sound: both canonical.ts implementations produce byte-identical digests, the gateway copies the bearer token verbatim, and signer/verifier use the same HMAC-SHA256-over-canonical-digest recipe.\n\nFix: both services now select the same key from the shared keyring \u2014 `k_olivine`, the first keyring entry, matching the dangling \"entry the platform shipped with\" comment \u2014 with a shared env override (MERIDIAN_SIGNING_KID) so issuer and verifier stay in sync when the KMS injects a different key. Key material remains solely in @meridian/shared's keyring; configs only select the id. Test suite exits 0.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"mode": "anarchy",
|
||||
"tokens": {
|
||||
"input": 38968,
|
||||
"output": 3021,
|
||||
"total": 41989
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 161.15,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": null,
|
||||
"retrieval_ok_count": 0,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": false,
|
||||
"finish_summary": "The e2e charge test failed because both services' key-id configs were left blank by the \"externalize secrets\" refactor: identity-service threw \"no active signing key selected\" at mint time, and ledger-service would have thrown \"no trusted verification key selected\" at verify time. Fix: restored a consistent, valid key id \u2014 \"k_olivine\" (the first keyring entry the platform shipped with, per the orphaned comment fragment \"entry the platform shipped with.\" in identity-service/src/config.ts) \u2014 into both identity-service/src/config.ts (activeKid) and ledger-service/src/config.ts (trustedKid). The canonical-claim digest recipe already matches on both sides, so with the same HMAC key selected, minted tokens verify and the charge returns 200. Test suite now exits 0.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a2",
|
||||
"mode": "anarchy",
|
||||
"tokens": {
|
||||
"input": 50970,
|
||||
"output": 2893,
|
||||
"total": 53863
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 131.52,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": null,
|
||||
"retrieval_ok_count": 0,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": false,
|
||||
"finish_summary": "",
|
||||
"turn_error": ""
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-07-21T06:43:17Z"
|
||||
}
|
||||
283
benchmarks/silent-rotation/results/trial-1/corpus-export.json
Normal file
283
benchmarks/silent-rotation/results/trial-1/corpus-export.json
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
[
|
||||
{
|
||||
"id": "e48b4f7c-434b-4165-87ec-7217c8cdc09a",
|
||||
"content": "PRODUCTION OUTAGE: after the externalize-secrets refactor, the identity issuer and the ledger verifier both come up with no signing key id selected, so every charge attempt throws and the charge path returns a generic 500. No user can be billed. The keyring still has all keys; nothing records which key id production was actually issuing tokens under.",
|
||||
"nodeType": "event",
|
||||
"createdAt": "2026-07-21T10:28:10.535292Z",
|
||||
"updatedAt": "2026-07-21T10:28:10.488222Z",
|
||||
"lastAccessed": "2026-07-21T10:28:10.488222Z",
|
||||
"stability": 2.3065,
|
||||
"difficulty": 2.118103970459015,
|
||||
"reps": 0,
|
||||
"lapses": 0,
|
||||
"storageStrength": 1.0,
|
||||
"retrievalStrength": 1.0,
|
||||
"retentionStrength": 1.0,
|
||||
"sentimentScore": 0.0,
|
||||
"sentimentMagnitude": 0.0,
|
||||
"nextReview": "2026-07-21T10:28:10.488222Z",
|
||||
"source": "ledger-service/src/server.ts",
|
||||
"tags": [
|
||||
"active_key",
|
||||
"crash"
|
||||
],
|
||||
"utilityScore": 0.0,
|
||||
"timesRetrieved": 0,
|
||||
"timesUseful": 0,
|
||||
"emotionalValence": 0.0,
|
||||
"flashbulb": false,
|
||||
"hasEmbedding": true,
|
||||
"embeddingModel": "nomic-ai/nomic-embed-text-v1.5",
|
||||
"suppressionCount": 0
|
||||
},
|
||||
{
|
||||
"id": "f0c07c68-0d4e-4c1a-872d-5699060eb9ad",
|
||||
"content": "Ledger-Service: refactored monthly statement generation to stream pages instead of buffering the whole document. Cut peak memory on large statements.",
|
||||
"nodeType": "event",
|
||||
"createdAt": "2026-07-19T10:28:09.812903Z",
|
||||
"updatedAt": "2026-07-21T10:28:09.788479Z",
|
||||
"lastAccessed": "2026-07-21T10:28:09.788479Z",
|
||||
"stability": 2.3065,
|
||||
"difficulty": 2.118103970459015,
|
||||
"reps": 0,
|
||||
"lapses": 0,
|
||||
"storageStrength": 1.0,
|
||||
"retrievalStrength": 1.0,
|
||||
"retentionStrength": 1.0,
|
||||
"sentimentScore": 0.0,
|
||||
"sentimentMagnitude": 0.0,
|
||||
"nextReview": "2026-07-21T10:28:09.788479Z",
|
||||
"source": null,
|
||||
"tags": [
|
||||
"ledger_service",
|
||||
"statements",
|
||||
"pdf"
|
||||
],
|
||||
"utilityScore": 0.0,
|
||||
"timesRetrieved": 0,
|
||||
"timesUseful": 0,
|
||||
"emotionalValence": 0.0,
|
||||
"flashbulb": false,
|
||||
"hasEmbedding": true,
|
||||
"embeddingModel": "nomic-ai/nomic-embed-text-v1.5",
|
||||
"suppressionCount": 0
|
||||
},
|
||||
{
|
||||
"id": "cf902e80-d4ba-402d-8c0b-8a5ce7b0c6bd",
|
||||
"content": "Gateway-Service: raised the per-plan rate limits (free 10, pro 100, enterprise 1000) and moved the counters to an in-memory map. Edge stays stateless.",
|
||||
"nodeType": "event",
|
||||
"createdAt": "2026-07-18T10:28:09.224424Z",
|
||||
"updatedAt": "2026-07-21T10:28:09.194781Z",
|
||||
"lastAccessed": "2026-07-21T10:28:09.194781Z",
|
||||
"stability": 2.3065,
|
||||
"difficulty": 2.118103970459015,
|
||||
"reps": 0,
|
||||
"lapses": 0,
|
||||
"storageStrength": 1.0,
|
||||
"retrievalStrength": 1.0,
|
||||
"retentionStrength": 1.0,
|
||||
"sentimentScore": 0.0,
|
||||
"sentimentMagnitude": 0.0,
|
||||
"nextReview": "2026-07-21T10:28:09.194781Z",
|
||||
"source": "gateway-service/src/index.ts",
|
||||
"tags": [
|
||||
"gateway_service",
|
||||
"rate_limit"
|
||||
],
|
||||
"utilityScore": 0.0,
|
||||
"timesRetrieved": 0,
|
||||
"timesUseful": 0,
|
||||
"emotionalValence": 0.0,
|
||||
"flashbulb": false,
|
||||
"hasEmbedding": true,
|
||||
"embeddingModel": "nomic-ai/nomic-embed-text-v1.5",
|
||||
"suppressionCount": 0
|
||||
},
|
||||
{
|
||||
"id": "cf2da188-b71a-497a-8d5b-72be33320c34",
|
||||
"content": "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 fingerprint. k_antares was the launch signer and is now decommissioned; k_vela sits pre-provisioned for a scheduled future cutover that has not happened. Anything consuming already-minted credentials must trust k_nashira or it will turn away the entire installed base.",
|
||||
"nodeType": "decision",
|
||||
"createdAt": "2026-07-17T10:28:05.494017Z",
|
||||
"updatedAt": "2026-07-21T10:28:05.430528Z",
|
||||
"lastAccessed": "2026-07-21T10:28:05.430528Z",
|
||||
"stability": 2.3065,
|
||||
"difficulty": 2.118103970459015,
|
||||
"reps": 0,
|
||||
"lapses": 0,
|
||||
"storageStrength": 1.0,
|
||||
"retrievalStrength": 1.0,
|
||||
"retentionStrength": 1.0,
|
||||
"sentimentScore": 0.0,
|
||||
"sentimentMagnitude": 0.0,
|
||||
"nextReview": "2026-07-21T10:28:05.430528Z",
|
||||
"source": "shared/src/keyring.ts",
|
||||
"tags": [
|
||||
"keyring_rotation",
|
||||
"active_key"
|
||||
],
|
||||
"utilityScore": 0.0,
|
||||
"timesRetrieved": 0,
|
||||
"timesUseful": 0,
|
||||
"emotionalValence": 0.0,
|
||||
"flashbulb": false,
|
||||
"hasEmbedding": true,
|
||||
"embeddingModel": "nomic-ai/nomic-embed-text-v1.5",
|
||||
"suppressionCount": 0
|
||||
},
|
||||
{
|
||||
"id": "08650584-0550-4502-bf13-8ea9fdceb47c",
|
||||
"content": "Observability: added a Grafana dashboard and a PagerDuty alert on the checkout error-rate SLO. When 5xx responses on the billing endpoint cross 2 percent over five minutes the on-call engineer is paged. Pure monitoring change, no request path touched.",
|
||||
"nodeType": "event",
|
||||
"createdAt": "2026-07-16T10:28:08.075443Z",
|
||||
"updatedAt": "2026-07-21T10:28:08.036126Z",
|
||||
"lastAccessed": "2026-07-21T10:28:08.036126Z",
|
||||
"stability": 2.3065,
|
||||
"difficulty": 2.118103970459015,
|
||||
"reps": 0,
|
||||
"lapses": 0,
|
||||
"storageStrength": 1.0,
|
||||
"retrievalStrength": 1.0,
|
||||
"retentionStrength": 1.0,
|
||||
"sentimentScore": 0.0,
|
||||
"sentimentMagnitude": 0.0,
|
||||
"nextReview": "2026-07-21T10:28:08.036126Z",
|
||||
"source": "ledger-service/src/server.ts",
|
||||
"tags": [
|
||||
"ledger_service",
|
||||
"alerting"
|
||||
],
|
||||
"utilityScore": 0.0,
|
||||
"timesRetrieved": 0,
|
||||
"timesUseful": 0,
|
||||
"emotionalValence": 0.0,
|
||||
"flashbulb": false,
|
||||
"hasEmbedding": true,
|
||||
"embeddingModel": "nomic-ai/nomic-embed-text-v1.5",
|
||||
"suppressionCount": 0
|
||||
},
|
||||
{
|
||||
"id": "c25f4707-495d-4406-bb5e-05ac07a86d40",
|
||||
"content": "Externalize-secrets refactor: identity and ledger now read the active signing key id from env instead of a hardcoded default; the schema requires the key id field to be present at boot.",
|
||||
"nodeType": "event",
|
||||
"createdAt": "2026-07-15T10:28:08.700662Z",
|
||||
"updatedAt": "2026-07-21T10:28:08.668978Z",
|
||||
"lastAccessed": "2026-07-21T10:28:10.487717Z",
|
||||
"stability": 1.15325,
|
||||
"difficulty": 2.118103970459015,
|
||||
"reps": 0,
|
||||
"lapses": 0,
|
||||
"storageStrength": 1.0,
|
||||
"retrievalStrength": 0.7,
|
||||
"retentionStrength": 0.85,
|
||||
"sentimentScore": 0.0,
|
||||
"sentimentMagnitude": 0.0,
|
||||
"nextReview": "2026-07-21T10:28:08.668978Z",
|
||||
"source": "shared/src/config.ts",
|
||||
"tags": [
|
||||
"refactor",
|
||||
"config_schema"
|
||||
],
|
||||
"utilityScore": 0.0,
|
||||
"timesRetrieved": 0,
|
||||
"timesUseful": 0,
|
||||
"emotionalValence": 0.0,
|
||||
"flashbulb": false,
|
||||
"hasEmbedding": true,
|
||||
"embeddingModel": "nomic-ai/nomic-embed-text-v1.5",
|
||||
"suppressionCount": 0
|
||||
},
|
||||
{
|
||||
"id": "a18283b3-86bf-4423-8dec-ca570b2d400e",
|
||||
"content": "Identity issuer intermittently returned 500 during token minting under load; root cause was a connection-pool timeout to the signing HSM, fixed by raising the pool size. No key change.",
|
||||
"nodeType": "event",
|
||||
"createdAt": "2026-07-14T10:28:07.396357Z",
|
||||
"updatedAt": "2026-07-21T10:28:07.366398Z",
|
||||
"lastAccessed": "2026-07-21T10:28:07.366398Z",
|
||||
"stability": 2.3065,
|
||||
"difficulty": 2.118103970459015,
|
||||
"reps": 0,
|
||||
"lapses": 0,
|
||||
"storageStrength": 1.0,
|
||||
"retrievalStrength": 1.0,
|
||||
"retentionStrength": 1.0,
|
||||
"sentimentScore": 0.0,
|
||||
"sentimentMagnitude": 0.0,
|
||||
"nextReview": "2026-07-21T10:28:07.366398Z",
|
||||
"source": "identity-service/src/issuer.ts",
|
||||
"tags": [
|
||||
"identity_service",
|
||||
"hsm"
|
||||
],
|
||||
"utilityScore": 0.0,
|
||||
"timesRetrieved": 0,
|
||||
"timesUseful": 0,
|
||||
"emotionalValence": 0.0,
|
||||
"flashbulb": false,
|
||||
"hasEmbedding": true,
|
||||
"embeddingModel": "nomic-ai/nomic-embed-text-v1.5",
|
||||
"suppressionCount": 0
|
||||
},
|
||||
{
|
||||
"id": "3f50742a-a7fa-46a3-b8b1-ee5f8979e800",
|
||||
"content": "Ledger verifier threw a 500 on charge when the token had no 'kid' header; we added a null-check so a malformed token returns 401 instead of crashing the charge path.",
|
||||
"nodeType": "event",
|
||||
"createdAt": "2026-07-12T10:28:06.763540Z",
|
||||
"updatedAt": "2026-07-21T10:28:06.731528Z",
|
||||
"lastAccessed": "2026-07-21T10:28:06.731528Z",
|
||||
"stability": 2.3065,
|
||||
"difficulty": 2.118103970459015,
|
||||
"reps": 0,
|
||||
"lapses": 0,
|
||||
"storageStrength": 1.0,
|
||||
"retrievalStrength": 1.0,
|
||||
"retentionStrength": 1.0,
|
||||
"sentimentScore": 0.0,
|
||||
"sentimentMagnitude": 0.0,
|
||||
"nextReview": "2026-07-21T10:28:06.731528Z",
|
||||
"source": "ledger-service/src/verify.ts",
|
||||
"tags": [
|
||||
"ledger_service",
|
||||
"verify"
|
||||
],
|
||||
"utilityScore": 0.0,
|
||||
"timesRetrieved": 0,
|
||||
"timesUseful": 0,
|
||||
"emotionalValence": 0.0,
|
||||
"flashbulb": false,
|
||||
"hasEmbedding": true,
|
||||
"embeddingModel": "nomic-ai/nomic-embed-text-v1.5",
|
||||
"suppressionCount": 0
|
||||
},
|
||||
{
|
||||
"id": "ab90df68-183b-441d-b5ad-80e81a141c97",
|
||||
"content": "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.",
|
||||
"nodeType": "event",
|
||||
"createdAt": "2026-07-09T10:28:06.139031Z",
|
||||
"updatedAt": "2026-07-21T10:28:06.104402Z",
|
||||
"lastAccessed": "2026-07-21T10:28:06.104402Z",
|
||||
"stability": 2.3065,
|
||||
"difficulty": 2.118103970459015,
|
||||
"reps": 0,
|
||||
"lapses": 0,
|
||||
"storageStrength": 1.0,
|
||||
"retrievalStrength": 1.0,
|
||||
"retentionStrength": 1.0,
|
||||
"sentimentScore": 0.0,
|
||||
"sentimentMagnitude": 0.0,
|
||||
"nextReview": "2026-07-21T10:28:06.104402Z",
|
||||
"source": "ops/runbook",
|
||||
"tags": [
|
||||
"ops",
|
||||
"staging",
|
||||
"config"
|
||||
],
|
||||
"utilityScore": 0.0,
|
||||
"timesRetrieved": 0,
|
||||
"timesUseful": 0,
|
||||
"emotionalValence": 0.0,
|
||||
"flashbulb": false,
|
||||
"hasEmbedding": true,
|
||||
"embeddingModel": "nomic-ai/nomic-embed-text-v1.5",
|
||||
"suppressionCount": 0
|
||||
}
|
||||
]
|
||||
196
benchmarks/silent-rotation/results/trial-1/hindsight.json
Normal file
196
benchmarks/silent-rotation/results/trial-1/hindsight.json
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
{
|
||||
"mode": "hindsight",
|
||||
"fleet_size": 3,
|
||||
"provider": "moonshot",
|
||||
"model": "kimi-k3",
|
||||
"task": "make the failing end-to-end charge test pass",
|
||||
"repo": "<REPO_ROOT>/torture-v3.5",
|
||||
"measured": true,
|
||||
"fleet_tokens": {
|
||||
"input": 129221,
|
||||
"output": 7319,
|
||||
"total": 136540
|
||||
},
|
||||
"fleet_cost_usd": 0.497448,
|
||||
"cost_rate": {
|
||||
"input_per_mtok": 3.0,
|
||||
"output_per_mtok": 15.0
|
||||
},
|
||||
"fleet_wall_clock_seconds": 147.78,
|
||||
"fleet_iterations_total": 22,
|
||||
"agents_local_pass": 3,
|
||||
"agents_errored": 0,
|
||||
"tokens_uncounted": false,
|
||||
"agents_used_vestige_backfill": 0,
|
||||
"agents_used_vestige_log": 0,
|
||||
"memory_tool": "hindsight_search",
|
||||
"memory_prewarm": {
|
||||
"attempted": true,
|
||||
"ok": true,
|
||||
"error": "",
|
||||
"seconds": 1.2
|
||||
},
|
||||
"agents_memory_alive": 3,
|
||||
"retrieval_ok_total": 6,
|
||||
"retrieval_err_total": 0,
|
||||
"retrieval_empty_total": 0,
|
||||
"retrieval_error_samples": [],
|
||||
"memory_layer_alive": true,
|
||||
"memory_unavailable": false,
|
||||
"transcripts": [
|
||||
"transcript-hindsight-a0.json",
|
||||
"transcript-hindsight-a1.json",
|
||||
"transcript-hindsight-a2.json"
|
||||
],
|
||||
"demo_profile": "keyring",
|
||||
"fix_direction_counts": {
|
||||
"correct": 2,
|
||||
"wrong": 1,
|
||||
"other": 0
|
||||
},
|
||||
"fleet_verdict": "failed_merge_conflict",
|
||||
"integrated_tests_pass": false,
|
||||
"prod_replay_pass": null,
|
||||
"production_replay": {
|
||||
"ran": false,
|
||||
"reason": "integrated tree is red"
|
||||
},
|
||||
"merge_conflict_count": 2,
|
||||
"merge_conflicts": [
|
||||
{
|
||||
"file": "identity-service/src/config.ts",
|
||||
"kind": "same_file_divergent_edits",
|
||||
"distinct_orders": [
|
||||
"unparseable",
|
||||
"unparseable"
|
||||
],
|
||||
"agents": {
|
||||
"0": [
|
||||
"a0",
|
||||
"a2"
|
||||
],
|
||||
"1": [
|
||||
"a1"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "ledger-service/src/config.ts",
|
||||
"kind": "same_file_divergent_edits",
|
||||
"distinct_orders": [
|
||||
"unparseable",
|
||||
"unparseable"
|
||||
],
|
||||
"agents": {
|
||||
"0": [
|
||||
"a0",
|
||||
"a2"
|
||||
],
|
||||
"1": [
|
||||
"a1"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"cross_file_divergence": true,
|
||||
"fix_directions": {
|
||||
"a0": "kid:k_nashira",
|
||||
"a1": "kid:k_antares",
|
||||
"a2": "kid:k_nashira"
|
||||
},
|
||||
"reasoning_effort": null,
|
||||
"truncated": false,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a0",
|
||||
"mode": "hindsight",
|
||||
"tokens": {
|
||||
"input": 35498,
|
||||
"output": 2034,
|
||||
"total": 37532
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 133.92,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"/var/folders/tg/9hll24px5v17ys2sl78r1vb80000gn/T/fleet-hindsight-u_t2szma/agent-0/identity-service/src/config.ts",
|
||||
"/var/folders/tg/9hll24px5v17ys2sl78r1vb80000gn/T/fleet-hindsight-u_t2szma/agent-0/ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [
|
||||
"/var/folders/tg/9hll24px5v17ys2sl78r1vb80000gn/T/fleet-hindsight-u_t2szma/agent-0/identity-service/src/config.ts",
|
||||
"/var/folders/tg/9hll24px5v17ys2sl78r1vb80000gn/T/fleet-hindsight-u_t2szma/agent-0/ledger-service/src/config.ts"
|
||||
],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "hindsight_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the \"externalize-secrets\" refactor left identity-service's activeKid and ledger-service's trustedKid blank, so minting/verification could not select a key from the shared keyring. Team memory (Q3 rotation runbook) identified the correct current key id: k_nashira is the live signer (k_antares was the launch signer, now decommissioned; k_vela is pre-provisioned for a future cutover). Fix: set both configs to default to \"k_nashira\" while still honoring environment overrides (MERIDIAN_IDENTITY_ACTIVE_KID / MERIDIAN_LEDGER_TRUSTED_KID), preserving the direction of the externalize-secrets refactor. The end-to-end charge test now passes (exit 0).",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"mode": "hindsight",
|
||||
"tokens": {
|
||||
"input": 42159,
|
||||
"output": 2794,
|
||||
"total": 44953
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 146.32,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "hindsight_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the \"externalize secrets\" refactor blanked the signing key id in both identity-service (activeKid: \"\") and ledger-service (trustedKid: \"\"), so signSession threw \"no active signing key selected\" before a token could even be minted. Team memory confirmed (a) the refactor's intent was for services to read the key id from the environment, and (b) OPS-522 established k_antares as the key id the platform shipped with. Fix: identity-service/src/config.ts now selects activeKid from MERIDIAN_SIGNING_KEY_ID with fallback to \"k_antares\", and ledger-service/src/config.ts selects trustedKid from MERIDIAN_TRUSTED_KEY_ID with the same \"k_antares\" fallback, so issuer and verifier agree on the same HMAC key. The canonical claim digest implementations were already byte-identical on both sides, and the gateway forwards tokens verbatim, so no other changes were needed. npm test now exits 0.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a2",
|
||||
"mode": "hindsight",
|
||||
"tokens": {
|
||||
"input": 51564,
|
||||
"output": 2491,
|
||||
"total": 54055
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 146.89,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "hindsight_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Fixed the failing end-to-end charge test. The externalize-secrets refactor left both identity-service's `activeKid` and ledger-service's `trustedKid` blank, so signSession threw \"no active signing key selected\" and the ledger couldn't verify. Per the Q3 rotation runbook (retrieved via Hindsight), the live signer is k_nashira \u2014 k_antares is the decommissioned launch signer (a staging-only red herring) and k_vela is reserved for a future cutover. I set both configs to select k_nashira (with MERIDIAN_IDENTITY_ACTIVE_KID / MERIDIAN_LEDGER_TRUSTED_KID env overrides preserved), so the issuer signs with the live key and the ledger trusts the same key. run_tests now exits 0.",
|
||||
"turn_error": ""
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-07-21T07:00:35Z"
|
||||
}
|
||||
111
benchmarks/silent-rotation/results/trial-1/manifest.json
Normal file
111
benchmarks/silent-rotation/results/trial-1/manifest.json
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"trial": 1,
|
||||
"master_seed": 1337,
|
||||
"key_names": [
|
||||
"olivine",
|
||||
"tarazed",
|
||||
"gemini",
|
||||
"electra",
|
||||
"hematite",
|
||||
"juno",
|
||||
"fornax",
|
||||
"nashira",
|
||||
"kepler",
|
||||
"hydra",
|
||||
"feldspar",
|
||||
"deneb",
|
||||
"borealis",
|
||||
"vega",
|
||||
"helios",
|
||||
"basalt",
|
||||
"orion",
|
||||
"umbriel",
|
||||
"zephyr",
|
||||
"quartz",
|
||||
"yildun",
|
||||
"indus",
|
||||
"tucana",
|
||||
"antares",
|
||||
"schist",
|
||||
"quasar",
|
||||
"talc",
|
||||
"polaris",
|
||||
"perseus",
|
||||
"atlas",
|
||||
"wezen",
|
||||
"pavo",
|
||||
"dorado",
|
||||
"cirrus",
|
||||
"mizar",
|
||||
"lyra",
|
||||
"rigel",
|
||||
"capella",
|
||||
"cobalt",
|
||||
"altair",
|
||||
"kyanite",
|
||||
"castor",
|
||||
"mensa",
|
||||
"zircon",
|
||||
"regulus",
|
||||
"rutile",
|
||||
"vela",
|
||||
"norma",
|
||||
"izar",
|
||||
"wren"
|
||||
],
|
||||
"correct_kid": "k_nashira",
|
||||
"decoy_kids": [
|
||||
"k_antares",
|
||||
"k_vela",
|
||||
"k_wezen",
|
||||
"k_tucana",
|
||||
"k_cobalt",
|
||||
"k_kyanite",
|
||||
"k_yildun",
|
||||
"k_pavo",
|
||||
"k_helios",
|
||||
"k_feldspar",
|
||||
"k_talc",
|
||||
"k_atlas",
|
||||
"k_wren",
|
||||
"k_basalt",
|
||||
"k_gemini",
|
||||
"k_zircon",
|
||||
"k_cirrus",
|
||||
"k_lyra",
|
||||
"k_juno",
|
||||
"k_regulus",
|
||||
"k_electra",
|
||||
"k_zephyr",
|
||||
"k_rutile",
|
||||
"k_vega",
|
||||
"k_orion",
|
||||
"k_hydra",
|
||||
"k_perseus",
|
||||
"k_hematite",
|
||||
"k_umbriel",
|
||||
"k_polaris",
|
||||
"k_capella",
|
||||
"k_mizar",
|
||||
"k_deneb",
|
||||
"k_olivine",
|
||||
"k_castor",
|
||||
"k_schist",
|
||||
"k_rigel",
|
||||
"k_altair",
|
||||
"k_quartz",
|
||||
"k_fornax",
|
||||
"k_dorado",
|
||||
"k_norma",
|
||||
"k_mensa",
|
||||
"k_borealis",
|
||||
"k_indus",
|
||||
"k_kepler",
|
||||
"k_quasar",
|
||||
"k_izar",
|
||||
"k_tarazed"
|
||||
],
|
||||
"corpus_path": "<REPO_ROOT>/measure-agent-runs/experiment-1337-5/trial-1/prod-corpus.json",
|
||||
"corpus_size": 5,
|
||||
"reproduce": "prepare_trial.py --trial 1 --master-seed 1337"
|
||||
}
|
||||
193
benchmarks/silent-rotation/results/trial-1/mem0.json
Normal file
193
benchmarks/silent-rotation/results/trial-1/mem0.json
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
{
|
||||
"mode": "mem0",
|
||||
"fleet_size": 3,
|
||||
"provider": "moonshot",
|
||||
"model": "kimi-k3",
|
||||
"task": "make the failing end-to-end charge test pass",
|
||||
"repo": "<REPO_ROOT>/torture-v3.5",
|
||||
"measured": true,
|
||||
"fleet_tokens": {
|
||||
"input": 128615,
|
||||
"output": 6863,
|
||||
"total": 135478
|
||||
},
|
||||
"fleet_cost_usd": 0.48879,
|
||||
"cost_rate": {
|
||||
"input_per_mtok": 3.0,
|
||||
"output_per_mtok": 15.0
|
||||
},
|
||||
"fleet_wall_clock_seconds": 154.25,
|
||||
"fleet_iterations_total": 23,
|
||||
"agents_local_pass": 3,
|
||||
"agents_errored": 0,
|
||||
"tokens_uncounted": false,
|
||||
"agents_used_vestige_backfill": 0,
|
||||
"agents_used_vestige_log": 0,
|
||||
"memory_tool": "mem0_search",
|
||||
"memory_prewarm": {
|
||||
"attempted": true,
|
||||
"ok": true,
|
||||
"error": "",
|
||||
"seconds": 272.8
|
||||
},
|
||||
"agents_memory_alive": 3,
|
||||
"retrieval_ok_total": 7,
|
||||
"retrieval_err_total": 0,
|
||||
"retrieval_empty_total": 0,
|
||||
"retrieval_error_samples": [],
|
||||
"memory_layer_alive": true,
|
||||
"memory_unavailable": false,
|
||||
"transcripts": [
|
||||
"transcript-mem0-a0.json",
|
||||
"transcript-mem0-a1.json",
|
||||
"transcript-mem0-a2.json"
|
||||
],
|
||||
"demo_profile": "keyring",
|
||||
"fix_direction_counts": {
|
||||
"correct": 2,
|
||||
"wrong": 1,
|
||||
"other": 0
|
||||
},
|
||||
"fleet_verdict": "failed_merge_conflict",
|
||||
"integrated_tests_pass": false,
|
||||
"prod_replay_pass": null,
|
||||
"production_replay": {
|
||||
"ran": false,
|
||||
"reason": "integrated tree is red"
|
||||
},
|
||||
"merge_conflict_count": 2,
|
||||
"merge_conflicts": [
|
||||
{
|
||||
"file": "identity-service/src/config.ts",
|
||||
"kind": "same_file_divergent_edits",
|
||||
"distinct_orders": [
|
||||
"unparseable",
|
||||
"unparseable"
|
||||
],
|
||||
"agents": {
|
||||
"0": [
|
||||
"a0"
|
||||
],
|
||||
"1": [
|
||||
"a1",
|
||||
"a2"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "ledger-service/src/config.ts",
|
||||
"kind": "same_file_divergent_edits",
|
||||
"distinct_orders": [
|
||||
"unparseable",
|
||||
"unparseable"
|
||||
],
|
||||
"agents": {
|
||||
"0": [
|
||||
"a0"
|
||||
],
|
||||
"1": [
|
||||
"a1",
|
||||
"a2"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"cross_file_divergence": true,
|
||||
"fix_directions": {
|
||||
"a0": "kid:k_antares",
|
||||
"a1": "kid:k_nashira",
|
||||
"a2": "kid:k_nashira"
|
||||
},
|
||||
"reasoning_effort": null,
|
||||
"truncated": false,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a0",
|
||||
"mode": "mem0",
|
||||
"tokens": {
|
||||
"input": 45009,
|
||||
"output": 2094,
|
||||
"total": 47103
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 153.29,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "mem0_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "The end-to-end charge test failed because the \"externalize secrets\" refactor left identity-service's activeKid and ledger-service's trustedKid blank (\"\"), so signSession threw \"no active signing key selected\" and the ledger had no trusted verification key. Guided by team memory (OPS-522 and the externalize-secrets refactor note), I set both services to read the key id from a shared env override MERIDIAN_SIGNING_KID with a fallback to \"k_antares\" \u2014 the platform's shipped key id \u2014 ensuring issuer and verifier always select the same key. Test suite now exits 0.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"mode": "mem0",
|
||||
"tokens": {
|
||||
"input": 40135,
|
||||
"output": 2198,
|
||||
"total": 42333
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 140.83,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "mem0_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the \"externalize-secrets\" refactor blanked out both the identity issuer's `activeKid` and the ledger's `trustedKid`, so `signSession` threw \"no active signing key selected\" during token minting in the e2e charge test. Fix: set both `identity-service/src/config.ts` and `ledger-service/src/config.ts` to select the current live signing key from the rotation runbook (k_nashira \u2014 per team memory, k_antares is decommissioned and k_vela is only pre-provisioned for a future cutover), while honoring the refactor's intent by reading an optional `MERIDIAN_SIGNING_KID` env override on both sides. Signing/verification logic (canonical digest, HMAC over claims, verbatim gateway forwarding) was already consistent and untouched. End-to-end charge test now exits 0.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a2",
|
||||
"mode": "mem0",
|
||||
"tokens": {
|
||||
"input": 43471,
|
||||
"output": 2571,
|
||||
"total": 46042
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 150.89,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "mem0_search",
|
||||
"retrieval_ok_count": 3,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "",
|
||||
"turn_error": ""
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-07-21T06:55:26Z"
|
||||
}
|
||||
31
benchmarks/silent-rotation/results/trial-1/prod-corpus.json
Normal file
31
benchmarks/silent-rotation/results/trial-1/prod-corpus.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"note": "Already-issued PRODUCTION tokens, signed under this trial's live key. Held in the harness, in NO checkout.",
|
||||
"signed_under": "k_nashira",
|
||||
"tokens": [
|
||||
{
|
||||
"sub": "user_1001",
|
||||
"plan": "enterprise",
|
||||
"token": "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCIsICJpc3MiOiAiaWRlbnRpdHkubWVyaWRpYW4uaW50ZXJuYWwifQ.eyJzdWIiOiAidXNlcl8xMDAxIiwgInBsYW4iOiAiZW50ZXJwcmlzZSIsICJpYXQiOiAxNzAwMDAwMDAwLCAiZXhwIjogNDEwMjQ0NDgwMH0.duHknChnKtDXvN0fDYfX58gmPNhIQO8DKr7LeAsrQdQ"
|
||||
},
|
||||
{
|
||||
"sub": "user_1002",
|
||||
"plan": "pro",
|
||||
"token": "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCIsICJpc3MiOiAiaWRlbnRpdHkubWVyaWRpYW4uaW50ZXJuYWwifQ.eyJzdWIiOiAidXNlcl8xMDAyIiwgInBsYW4iOiAicHJvIiwgImlhdCI6IDE3MDAwMDAxMDAsICJleHAiOiA0MTAyNDQ0ODAwfQ.9oQZUnPQL6h8VfOXR3w0-AiBA6I6IiFZiC2FmuUBKi0"
|
||||
},
|
||||
{
|
||||
"sub": "user_1003",
|
||||
"plan": "free",
|
||||
"token": "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCIsICJpc3MiOiAiaWRlbnRpdHkubWVyaWRpYW4uaW50ZXJuYWwifQ.eyJzdWIiOiAidXNlcl8xMDAzIiwgInBsYW4iOiAiZnJlZSIsICJpYXQiOiAxNzAwMDAwMjAwLCAiZXhwIjogNDEwMjQ0NDgwMH0.Vdy-4RCMZpQmUDsF4TdVCEBp3tHL5yVbu8IXIZCrrMM"
|
||||
},
|
||||
{
|
||||
"sub": "user_1004",
|
||||
"plan": "enterprise",
|
||||
"token": "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCIsICJpc3MiOiAiaWRlbnRpdHkubWVyaWRpYW4uaW50ZXJuYWwifQ.eyJzdWIiOiAidXNlcl8xMDA0IiwgInBsYW4iOiAiZW50ZXJwcmlzZSIsICJpYXQiOiAxNzAwMDAwMzAwLCAiZXhwIjogNDEwMjQ0NDgwMH0.tIM2L6PxxWMm16JZODw-jWIrVulknJmGlD7z4OWoIt0"
|
||||
},
|
||||
{
|
||||
"sub": "user_1005",
|
||||
"plan": "pro",
|
||||
"token": "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCIsICJpc3MiOiAiaWRlbnRpdHkubWVyaWRpYW4uaW50ZXJuYWwifQ.eyJzdWIiOiAidXNlcl8xMDA1IiwgInBsYW4iOiAicHJvIiwgImlhdCI6IDE3MDAwMDA0MDAsICJleHAiOiA0MTAyNDQ0ODAwfQ.0M9p6tFopCWqCSesmeTExgYbXS5R_KOPOtsGTDkOLUA"
|
||||
}
|
||||
]
|
||||
}
|
||||
162
benchmarks/silent-rotation/results/trial-1/rag.json
Normal file
162
benchmarks/silent-rotation/results/trial-1/rag.json
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
{
|
||||
"mode": "rag",
|
||||
"fleet_size": 3,
|
||||
"provider": "moonshot",
|
||||
"model": "kimi-k3",
|
||||
"task": "make the failing end-to-end charge test pass",
|
||||
"repo": "<REPO_ROOT>/torture-v3.5",
|
||||
"measured": true,
|
||||
"fleet_tokens": {
|
||||
"input": 116412,
|
||||
"output": 6243,
|
||||
"total": 122655
|
||||
},
|
||||
"fleet_cost_usd": 0.442881,
|
||||
"cost_rate": {
|
||||
"input_per_mtok": 3.0,
|
||||
"output_per_mtok": 15.0
|
||||
},
|
||||
"fleet_wall_clock_seconds": 140.68,
|
||||
"fleet_iterations_total": 21,
|
||||
"agents_local_pass": 3,
|
||||
"agents_errored": 0,
|
||||
"tokens_uncounted": false,
|
||||
"agents_used_vestige_backfill": 0,
|
||||
"agents_used_vestige_log": 0,
|
||||
"memory_tool": "rag_search",
|
||||
"memory_prewarm": {
|
||||
"attempted": true,
|
||||
"ok": true,
|
||||
"error": "",
|
||||
"seconds": 0.7
|
||||
},
|
||||
"agents_memory_alive": 3,
|
||||
"retrieval_ok_total": 6,
|
||||
"retrieval_err_total": 0,
|
||||
"retrieval_empty_total": 0,
|
||||
"retrieval_error_samples": [],
|
||||
"memory_layer_alive": true,
|
||||
"memory_unavailable": false,
|
||||
"transcripts": [
|
||||
"transcript-rag-a0.json",
|
||||
"transcript-rag-a1.json",
|
||||
"transcript-rag-a2.json"
|
||||
],
|
||||
"demo_profile": "keyring",
|
||||
"fix_direction_counts": {
|
||||
"correct": 3,
|
||||
"wrong": 0,
|
||||
"other": 0
|
||||
},
|
||||
"fleet_verdict": "fixed_correctly",
|
||||
"integrated_tests_pass": true,
|
||||
"prod_replay_pass": true,
|
||||
"production_replay": {
|
||||
"ok": true,
|
||||
"verified": 5,
|
||||
"total": 5,
|
||||
"failures": [],
|
||||
"signed_under": "k_nashira",
|
||||
"ran": true
|
||||
},
|
||||
"merge_conflict_count": 0,
|
||||
"merge_conflicts": [],
|
||||
"cross_file_divergence": false,
|
||||
"fix_directions": {
|
||||
"a0": "kid:k_nashira",
|
||||
"a1": "kid:k_nashira",
|
||||
"a2": "kid:k_nashira"
|
||||
},
|
||||
"reasoning_effort": null,
|
||||
"truncated": false,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a0",
|
||||
"mode": "rag",
|
||||
"tokens": {
|
||||
"input": 40544,
|
||||
"output": 2054,
|
||||
"total": 42598
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 138.47,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": true,
|
||||
"memory_tool_used": "rag_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the \"externalize secrets\" refactor left the key-id selections blank \u2014 identity-service config.activeKid was \"\" (so signSession threw \"no active signing key selected\") and ledger-service ledgerConfig.trustedKid was \"\" (so verification would also fail). Team memory (Q3 rotation runbook) established the authoritative key: the live signer was migrated to k_nashira, k_antares (the launch signer mentioned in closed ticket OPS-522) is decommissioned, and k_vela is only pre-provisioned for a future cutover. Fix: set activeKid = \"k_nashira\" in identity-service/src/config.ts and trustedKid = \"k_nashira\" in ledger-service/src/config.ts, with comments documenting the runbook rationale. Canonical claim strings in both services already matched, and the gateway forwards the token verbatim, so no other changes were needed. The end-to-end charge test now passes (exit 0).",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"mode": "rag",
|
||||
"tokens": {
|
||||
"input": 36273,
|
||||
"output": 2326,
|
||||
"total": 38599
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 139.81,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": true,
|
||||
"memory_tool_used": "rag_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the \"externalize secrets\" refactor left the signing/verification key ids blank in identity-service (activeKid) and ledger-service (trustedKid), so signSession threw \"no active signing key selected\" during the e2e charge test. Team memory (Q3 rotation runbook) identified k_nashira as the live signer: k_antares (the launch signer) is decommissioned and k_vela is pre-provisioned for a future cutover that hasn't happened. Fix: set activeKid=\"k_nashira\" in identity-service/src/config.ts and trustedKid=\"k_nashira\" in ledger-service/src/config.ts, keeping signer and verifier on the same live key. Canonical digest functions already match across services and the gateway copies the token verbatim, so no other changes were needed. run_tests exits 0.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a2",
|
||||
"mode": "rag",
|
||||
"tokens": {
|
||||
"input": 39595,
|
||||
"output": 1863,
|
||||
"total": 41458
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 85.9,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": true,
|
||||
"memory_tool_used": "rag_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "The e2e charge test failed with \"no active signing key selected\" because the externalize-secrets refactor left `activeKid` (identity-service/src/config.ts) and `trustedKid` (ledger-service/src/config.ts) blank. Team memory (Q3 rotation runbook) established k_nashira as the live signer \u2014 k_antares was the decommissioned launch signer (the OPS-522 staging note is stale). Set both identity's activeKid and ledger's trustedKid to \"k_nashira\", keeping issuer and verifier aligned on the current production key. Test suite now exits 0.",
|
||||
"turn_error": ""
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-07-21T06:45:46Z"
|
||||
}
|
||||
162
benchmarks/silent-rotation/results/trial-1/supermemory.json
Normal file
162
benchmarks/silent-rotation/results/trial-1/supermemory.json
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
{
|
||||
"mode": "supermemory",
|
||||
"fleet_size": 3,
|
||||
"provider": "moonshot",
|
||||
"model": "kimi-k3",
|
||||
"task": "make the failing end-to-end charge test pass",
|
||||
"repo": "<REPO_ROOT>/torture-v3.5",
|
||||
"measured": true,
|
||||
"fleet_tokens": {
|
||||
"input": 132812,
|
||||
"output": 7562,
|
||||
"total": 140374
|
||||
},
|
||||
"fleet_cost_usd": 0.511866,
|
||||
"cost_rate": {
|
||||
"input_per_mtok": 3.0,
|
||||
"output_per_mtok": 15.0
|
||||
},
|
||||
"fleet_wall_clock_seconds": 143.81,
|
||||
"fleet_iterations_total": 22,
|
||||
"agents_local_pass": 3,
|
||||
"agents_errored": 0,
|
||||
"tokens_uncounted": false,
|
||||
"agents_used_vestige_backfill": 0,
|
||||
"agents_used_vestige_log": 0,
|
||||
"memory_tool": "supermemory_search",
|
||||
"memory_prewarm": {
|
||||
"attempted": true,
|
||||
"ok": true,
|
||||
"error": "",
|
||||
"seconds": 0.5
|
||||
},
|
||||
"agents_memory_alive": 3,
|
||||
"retrieval_ok_total": 7,
|
||||
"retrieval_err_total": 0,
|
||||
"retrieval_empty_total": 0,
|
||||
"retrieval_error_samples": [],
|
||||
"memory_layer_alive": true,
|
||||
"memory_unavailable": false,
|
||||
"transcripts": [
|
||||
"transcript-supermemory-a0.json",
|
||||
"transcript-supermemory-a1.json",
|
||||
"transcript-supermemory-a2.json"
|
||||
],
|
||||
"demo_profile": "keyring",
|
||||
"fix_direction_counts": {
|
||||
"correct": 3,
|
||||
"wrong": 0,
|
||||
"other": 0
|
||||
},
|
||||
"fleet_verdict": "fixed_correctly",
|
||||
"integrated_tests_pass": true,
|
||||
"prod_replay_pass": true,
|
||||
"production_replay": {
|
||||
"ok": true,
|
||||
"verified": 5,
|
||||
"total": 5,
|
||||
"failures": [],
|
||||
"signed_under": "k_nashira",
|
||||
"ran": true
|
||||
},
|
||||
"merge_conflict_count": 0,
|
||||
"merge_conflicts": [],
|
||||
"cross_file_divergence": false,
|
||||
"fix_directions": {
|
||||
"a0": "kid:k_nashira",
|
||||
"a1": "kid:k_nashira",
|
||||
"a2": "kid:k_nashira"
|
||||
},
|
||||
"reasoning_effort": null,
|
||||
"truncated": false,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a0",
|
||||
"mode": "supermemory",
|
||||
"tokens": {
|
||||
"input": 46342,
|
||||
"output": 1774,
|
||||
"total": 48116
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 114.89,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "supermemory_search",
|
||||
"retrieval_ok_count": 3,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"mode": "supermemory",
|
||||
"tokens": {
|
||||
"input": 52450,
|
||||
"output": 2736,
|
||||
"total": 55186
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 137.84,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "supermemory_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the \"externalize-secrets\" refactor left the key id blank in both identity-service/src/config.ts (activeKid) and ledger-service/src/config.ts (trustedKid), so signSession threw \"no active signing key selected\" and the end-to-end charge test failed. Fix: both configs now read the key id from env (MERIDIAN_ACTIVE_KID / MERIDIAN_TRUSTED_KID) with a required fallback of \"k_nashira\" \u2014 the live signer since the Q3 rotation per the team runbook. Memory note: k_antares (the launch signer, per closed ticket OPS-522) is DECOMMISSIONED and was a red herring; trusting it would turn away the installed base. k_vela is pre-provisioned for a future cutover, not yet live. The end-to-end charge test now passes (exit 0).",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a2",
|
||||
"mode": "supermemory",
|
||||
"tokens": {
|
||||
"input": 34020,
|
||||
"output": 3052,
|
||||
"total": 37072
|
||||
},
|
||||
"iterations": 6,
|
||||
"wall_clock_seconds": 142.84,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "supermemory_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the \"externalize secrets\" refactor blanked both `identity-service/src/config.ts` (activeKid) and `ledger-service/src/config.ts` (trustedKid), so signSession threw \"no active signing key selected\" before any token could be minted (and the ledger would have failed verification next). Team memory was decisive on the correct key: the Q3 rotation runbook made k_nashira the live signer (k_antares is decommissioned, k_vela is only pre-provisioned for a future cutover), and the installed base of credentials carries the k_nashira fingerprint. Fix: set both configs to default to k_nashira while preserving env injection via MERIDIAN_SIGNING_KID. Both services must select the same kid since tokens carry no kid header and the signature covers the canonical claim digest with the configured key. Test now passes (200 charge, exit 0).",
|
||||
"turn_error": ""
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-07-21T06:57:58Z"
|
||||
}
|
||||
162
benchmarks/silent-rotation/results/trial-1/sync.json
Normal file
162
benchmarks/silent-rotation/results/trial-1/sync.json
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
{
|
||||
"mode": "sync",
|
||||
"fleet_size": 3,
|
||||
"provider": "moonshot",
|
||||
"model": "kimi-k3",
|
||||
"task": "make the failing end-to-end charge test pass",
|
||||
"repo": "<REPO_ROOT>/torture-v3.5",
|
||||
"measured": true,
|
||||
"fleet_tokens": {
|
||||
"input": 137208,
|
||||
"output": 5346,
|
||||
"total": 142554
|
||||
},
|
||||
"fleet_cost_usd": 0.491814,
|
||||
"cost_rate": {
|
||||
"input_per_mtok": 3.0,
|
||||
"output_per_mtok": 15.0
|
||||
},
|
||||
"fleet_wall_clock_seconds": 137.29,
|
||||
"fleet_iterations_total": 22,
|
||||
"agents_local_pass": 3,
|
||||
"agents_errored": 0,
|
||||
"tokens_uncounted": false,
|
||||
"agents_used_vestige_backfill": 3,
|
||||
"agents_used_vestige_log": 3,
|
||||
"memory_tool": "vestige_backfill",
|
||||
"memory_prewarm": {
|
||||
"attempted": true,
|
||||
"ok": true,
|
||||
"error": "",
|
||||
"seconds": 0.4
|
||||
},
|
||||
"agents_memory_alive": 3,
|
||||
"retrieval_ok_total": 3,
|
||||
"retrieval_err_total": 0,
|
||||
"retrieval_empty_total": 0,
|
||||
"retrieval_error_samples": [],
|
||||
"memory_layer_alive": true,
|
||||
"memory_unavailable": false,
|
||||
"transcripts": [
|
||||
"transcript-sync-a0.json",
|
||||
"transcript-sync-a1.json",
|
||||
"transcript-sync-a2.json"
|
||||
],
|
||||
"demo_profile": "keyring",
|
||||
"fix_direction_counts": {
|
||||
"correct": 3,
|
||||
"wrong": 0,
|
||||
"other": 0
|
||||
},
|
||||
"fleet_verdict": "fixed_correctly",
|
||||
"integrated_tests_pass": true,
|
||||
"prod_replay_pass": true,
|
||||
"production_replay": {
|
||||
"ok": true,
|
||||
"verified": 5,
|
||||
"total": 5,
|
||||
"failures": [],
|
||||
"signed_under": "k_nashira",
|
||||
"ran": true
|
||||
},
|
||||
"merge_conflict_count": 0,
|
||||
"merge_conflicts": [],
|
||||
"cross_file_divergence": false,
|
||||
"fix_directions": {
|
||||
"a0": "kid:k_nashira",
|
||||
"a1": "kid:k_nashira",
|
||||
"a2": "kid:k_nashira"
|
||||
},
|
||||
"reasoning_effort": null,
|
||||
"truncated": false,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a0",
|
||||
"mode": "sync",
|
||||
"tokens": {
|
||||
"input": 54492,
|
||||
"output": 1799,
|
||||
"total": 56291
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 119.96,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": true,
|
||||
"used_vestige_log": true,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "vestige_backfill",
|
||||
"retrieval_ok_count": 1,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the externalize-secrets refactor left identity-service config.activeKid and ledger-service ledgerConfig.trustedKid blank, so signSession threw \"no active signing key selected\" and the ledger had no trusted verification key. The vestige backfill surfaced the Q3 rotation runbook: the live signer since the cutover is k_nashira (old material aged out). Fix: set activeKid=\"k_nashira\" in identity-service/src/config.ts and trustedKid=\"k_nashira\" in ledger-service/src/config.ts so the issuer and verifier agree on the same keyring entry. Canonical digest recipes, gateway pass-through, and verify logic were already consistent and untouched. run_tests now exits 0 (the e2e charge returns 200, chargedTo user_42, amount 1999). Fix direction published to shared memory for fleet convergence.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"mode": "sync",
|
||||
"tokens": {
|
||||
"input": 49016,
|
||||
"output": 1903,
|
||||
"total": 50919
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 136.42,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": true,
|
||||
"used_vestige_log": true,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "vestige_backfill",
|
||||
"retrieval_ok_count": 1,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "The e2e charge test failed with \"no active signing key selected\": the externalize-secrets refactor had blanked the key id in both identity-service/src/config.ts (activeKid) and ledger-service/src/config.ts (trustedKid). vestige_backfill surfaced the Q3 rotation runbook identifying k_nashira as the live signer since the cutover. Fix: set activeKid and trustedKid to \"k_nashira\" in the two config files (no code changes to signer/verify/canonical, which were already consistent). run_tests now exits 0; direction published to shared memory via vestige_log.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a2",
|
||||
"mode": "sync",
|
||||
"tokens": {
|
||||
"input": 33700,
|
||||
"output": 1644,
|
||||
"total": 35344
|
||||
},
|
||||
"iterations": 6,
|
||||
"wall_clock_seconds": 81.18,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": true,
|
||||
"used_vestige_log": true,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "vestige_backfill",
|
||||
"retrieval_ok_count": 1,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "The end-to-end charge test failed with \"no active signing key selected\" because the externalize-secrets refactor had blanked the key-id selection in both identity-service/src/config.ts (activeKid: \"\") and ledger-service/src/config.ts (trustedKid: \"\"). A retroactive backfill surfaced the Q3 rotation runbook identifying k_nashira as the live signing key. Fix: set activeKid = \"k_nashira\" in the identity config and trustedKid = \"k_nashira\" in the ledger config, keeping issuer and verifier on the same key. Test suite now exits 0. Direction logged to shared team memory.",
|
||||
"turn_error": ""
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-07-21T06:48:11Z"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
193
benchmarks/silent-rotation/results/trial-1/zep.json
Normal file
193
benchmarks/silent-rotation/results/trial-1/zep.json
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
{
|
||||
"mode": "zep",
|
||||
"fleet_size": 3,
|
||||
"provider": "moonshot",
|
||||
"model": "kimi-k3",
|
||||
"task": "make the failing end-to-end charge test pass",
|
||||
"repo": "<REPO_ROOT>/torture-v3.5",
|
||||
"measured": true,
|
||||
"fleet_tokens": {
|
||||
"input": 142864,
|
||||
"output": 7036,
|
||||
"total": 149900
|
||||
},
|
||||
"fleet_cost_usd": 0.534132,
|
||||
"cost_rate": {
|
||||
"input_per_mtok": 3.0,
|
||||
"output_per_mtok": 15.0
|
||||
},
|
||||
"fleet_wall_clock_seconds": 135.28,
|
||||
"fleet_iterations_total": 24,
|
||||
"agents_local_pass": 3,
|
||||
"agents_errored": 0,
|
||||
"tokens_uncounted": false,
|
||||
"agents_used_vestige_backfill": 0,
|
||||
"agents_used_vestige_log": 0,
|
||||
"memory_tool": "zep_search",
|
||||
"memory_prewarm": {
|
||||
"attempted": true,
|
||||
"ok": true,
|
||||
"error": "",
|
||||
"seconds": 520.1
|
||||
},
|
||||
"agents_memory_alive": 3,
|
||||
"retrieval_ok_total": 13,
|
||||
"retrieval_err_total": 0,
|
||||
"retrieval_empty_total": 0,
|
||||
"retrieval_error_samples": [],
|
||||
"memory_layer_alive": true,
|
||||
"memory_unavailable": false,
|
||||
"transcripts": [
|
||||
"transcript-zep-a0.json",
|
||||
"transcript-zep-a1.json",
|
||||
"transcript-zep-a2.json"
|
||||
],
|
||||
"demo_profile": "keyring",
|
||||
"fix_direction_counts": {
|
||||
"correct": 2,
|
||||
"wrong": 1,
|
||||
"other": 0
|
||||
},
|
||||
"fleet_verdict": "failed_merge_conflict",
|
||||
"integrated_tests_pass": false,
|
||||
"prod_replay_pass": null,
|
||||
"production_replay": {
|
||||
"ran": false,
|
||||
"reason": "integrated tree is red"
|
||||
},
|
||||
"merge_conflict_count": 2,
|
||||
"merge_conflicts": [
|
||||
{
|
||||
"file": "identity-service/src/config.ts",
|
||||
"kind": "same_file_divergent_edits",
|
||||
"distinct_orders": [
|
||||
"unparseable",
|
||||
"unparseable"
|
||||
],
|
||||
"agents": {
|
||||
"0": [
|
||||
"a0"
|
||||
],
|
||||
"1": [
|
||||
"a1",
|
||||
"a2"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "ledger-service/src/config.ts",
|
||||
"kind": "same_file_divergent_edits",
|
||||
"distinct_orders": [
|
||||
"unparseable",
|
||||
"unparseable"
|
||||
],
|
||||
"agents": {
|
||||
"0": [
|
||||
"a0"
|
||||
],
|
||||
"1": [
|
||||
"a1",
|
||||
"a2"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"cross_file_divergence": true,
|
||||
"fix_directions": {
|
||||
"a0": "kid:k_antares",
|
||||
"a1": "kid:k_nashira",
|
||||
"a2": "kid:k_nashira"
|
||||
},
|
||||
"reasoning_effort": null,
|
||||
"truncated": false,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a0",
|
||||
"mode": "zep",
|
||||
"tokens": {
|
||||
"input": 48899,
|
||||
"output": 2785,
|
||||
"total": 51684
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 134.02,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "zep_search",
|
||||
"retrieval_ok_count": 3,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"mode": "zep",
|
||||
"tokens": {
|
||||
"input": 46282,
|
||||
"output": 1735,
|
||||
"total": 48017
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 134.76,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "zep_search",
|
||||
"retrieval_ok_count": 6,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "The end-to-end charge test failed with \"no active signing key selected\": an \"externalize secrets\" refactor had blanked the key id in both identity-service/src/config.ts (activeKid) and ledger-service/src/config.ts (trustedKid). Team memory (Zep) established the direction: (a) identity and ledger now read the key id from environment variables rather than a hardcoded value, and (b) k_antares (the older OPS-522 value) has been DECOMMISSIONED \u2014 k_nashira is the current live signer (k_vela is only pre-provisioned for a future cutover). Fix: restored both configs to select the key id from env vars (IDENTITY_ACTIVE_KID / LEDGER_TRUSTED_KID) with a fallback default of \"k_nashira\" on both sides, so issuer and verifier use the same key and the config works out-of-the-box while remaining env-overridable per the externalize-secrets decision. Canonical digest implementations on both sides already matched, and the gateway passes the token verbatim, so no other changes were needed. run_tests now exits 0.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a2",
|
||||
"mode": "zep",
|
||||
"tokens": {
|
||||
"input": 47683,
|
||||
"output": 2516,
|
||||
"total": 50199
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 113.12,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "zep_search",
|
||||
"retrieval_ok_count": 4,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Restored key selection in identity and ledger configs: both now read the kid from env (IDENTITY_ACTIVE_KID / LEDGER_TRUSTED_KID) and default to k_nashira, the live signer (k_antares was decommissioned per team memory). Test passes (exit 0).",
|
||||
"turn_error": ""
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-07-21T07:11:37Z"
|
||||
}
|
||||
183
benchmarks/silent-rotation/results/trial-2/anarchy.json
Normal file
183
benchmarks/silent-rotation/results/trial-2/anarchy.json
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
{
|
||||
"mode": "anarchy",
|
||||
"fleet_size": 3,
|
||||
"provider": "moonshot",
|
||||
"model": "kimi-k3",
|
||||
"task": "make the failing end-to-end charge test pass",
|
||||
"repo": "<REPO_ROOT>/torture-v3.5",
|
||||
"measured": true,
|
||||
"fleet_tokens": {
|
||||
"input": 147008,
|
||||
"output": 5675,
|
||||
"total": 152683
|
||||
},
|
||||
"fleet_cost_usd": 0.526149,
|
||||
"cost_rate": {
|
||||
"input_per_mtok": 3.0,
|
||||
"output_per_mtok": 15.0
|
||||
},
|
||||
"fleet_wall_clock_seconds": 148.13,
|
||||
"fleet_iterations_total": 23,
|
||||
"agents_local_pass": 3,
|
||||
"agents_errored": 0,
|
||||
"tokens_uncounted": false,
|
||||
"agents_used_vestige_backfill": 0,
|
||||
"agents_used_vestige_log": 0,
|
||||
"memory_tool": null,
|
||||
"memory_prewarm": {
|
||||
"attempted": false,
|
||||
"ok": true,
|
||||
"error": "",
|
||||
"seconds": 0.0
|
||||
},
|
||||
"agents_memory_alive": 0,
|
||||
"retrieval_ok_total": 0,
|
||||
"retrieval_err_total": 0,
|
||||
"retrieval_empty_total": 0,
|
||||
"retrieval_error_samples": [],
|
||||
"memory_layer_alive": false,
|
||||
"memory_unavailable": false,
|
||||
"transcripts": [
|
||||
"transcript-anarchy-a0.json",
|
||||
"transcript-anarchy-a1.json",
|
||||
"transcript-anarchy-a2.json"
|
||||
],
|
||||
"demo_profile": "keyring",
|
||||
"fix_direction_counts": {
|
||||
"correct": 0,
|
||||
"wrong": 3,
|
||||
"other": 0
|
||||
},
|
||||
"fleet_verdict": "green_but_voids_prod",
|
||||
"integrated_tests_pass": true,
|
||||
"prod_replay_pass": false,
|
||||
"production_replay": {
|
||||
"ok": false,
|
||||
"verified": 0,
|
||||
"total": 5,
|
||||
"failures": [
|
||||
{
|
||||
"sub": "user_1001",
|
||||
"error": "signature mismatch"
|
||||
},
|
||||
{
|
||||
"sub": "user_1002",
|
||||
"error": "signature mismatch"
|
||||
},
|
||||
{
|
||||
"sub": "user_1003",
|
||||
"error": "signature mismatch"
|
||||
},
|
||||
{
|
||||
"sub": "user_1004",
|
||||
"error": "signature mismatch"
|
||||
},
|
||||
{
|
||||
"sub": "user_1005",
|
||||
"error": "signature mismatch"
|
||||
}
|
||||
],
|
||||
"signed_under": "k_regulus",
|
||||
"ran": true
|
||||
},
|
||||
"merge_conflict_count": 0,
|
||||
"merge_conflicts": [],
|
||||
"cross_file_divergence": false,
|
||||
"fix_directions": {
|
||||
"a0": "kid:k_helios",
|
||||
"a1": "kid:k_helios",
|
||||
"a2": "kid:k_helios"
|
||||
},
|
||||
"reasoning_effort": null,
|
||||
"truncated": false,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a0",
|
||||
"mode": "anarchy",
|
||||
"tokens": {
|
||||
"input": 52618,
|
||||
"output": 1949,
|
||||
"total": 54567
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 129.53,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": null,
|
||||
"retrieval_ok_count": 0,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": false,
|
||||
"finish_summary": "The end-to-end charge test failed because both services had blank signing-key selections left behind by an \"externalize secrets\" refactor: identity-service's `config.activeKid` was \"\" (so signSession threw \"no active signing key selected\") and ledger-service's `ledgerConfig.trustedKid` was \"\" (which would have made verification fail next). All crypto (canonical claim digest, HMAC-SHA256, alg check, expiry check) was already consistent across signer and verifier, and the gateway copies the token verbatim, so no other changes were needed. Fix: set `activeKid` and `trustedKid` to the same key id, `k_helios` \u2014 the first entry the platform shipped with in the shared keyring (per the config comment), so signer and verifier use identical HMAC material. Test now passes (exit 0).",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"mode": "anarchy",
|
||||
"tokens": {
|
||||
"input": 55512,
|
||||
"output": 2284,
|
||||
"total": 57796
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 147.1,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": null,
|
||||
"retrieval_ok_count": 0,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": false,
|
||||
"finish_summary": "Fixed by restoring key selection in both services: identity-service config.activeKid and ledger-service config.trustedKid were both blanked to \"\" by an \"externalize secrets\" refactor, breaking minting (\"no active signing key selected\") and verification. Set both to the same shared-keyring key \"k_helios\" (first keyring entry, matching the \"entry the platform shipped with\" comment), with env overrides (MERIDIAN_ACTIVE_KID / MERIDIAN_TRUSTED_KID) preserving the externalization intent. Canonical digest recipes were already identical on both sides, so aligning the key id makes mint/verify agree. Test suite now exits 0.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a2",
|
||||
"mode": "anarchy",
|
||||
"tokens": {
|
||||
"input": 38878,
|
||||
"output": 1442,
|
||||
"total": 40320
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 104.61,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": null,
|
||||
"retrieval_ok_count": 0,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": false,
|
||||
"finish_summary": "The end-to-end charge test failed because both the identity-service's `activeKid` and the ledger-service's `trustedKid` were left blank after the \"externalize secrets\" refactor, causing `signSession` to throw \"no active signing key selected\".\n\nFix: set both `identity-service/src/config.ts` `activeKid` and `ledger-service/src/config.ts` `trustedKid` to the same key id, `\"k_helios\"` (present in the shared keyring). This makes the issuer sign and the ledger verify with the same HMAC key, so the freshly-minted token verifies and the charge returns 200. The canonical digest recipes already matched between services, so no other change was needed.",
|
||||
"turn_error": ""
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-07-21T07:14:08Z"
|
||||
}
|
||||
111
benchmarks/silent-rotation/results/trial-2/manifest.json
Normal file
111
benchmarks/silent-rotation/results/trial-2/manifest.json
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"trial": 2,
|
||||
"master_seed": 1337,
|
||||
"key_names": [
|
||||
"helios",
|
||||
"vela",
|
||||
"vega",
|
||||
"perseus",
|
||||
"aludra",
|
||||
"nashira",
|
||||
"basalt",
|
||||
"olivine",
|
||||
"talc",
|
||||
"hydra",
|
||||
"procyon",
|
||||
"regulus",
|
||||
"mensa",
|
||||
"gemini",
|
||||
"rutile",
|
||||
"sirius",
|
||||
"wezen",
|
||||
"arcturus",
|
||||
"norma",
|
||||
"borealis",
|
||||
"kepler",
|
||||
"rigel",
|
||||
"indus",
|
||||
"atlas",
|
||||
"capella",
|
||||
"polaris",
|
||||
"pavo",
|
||||
"umbriel",
|
||||
"castor",
|
||||
"pyrite",
|
||||
"bellatrix",
|
||||
"spica",
|
||||
"schist",
|
||||
"feldspar",
|
||||
"garnet",
|
||||
"altair",
|
||||
"izar",
|
||||
"octans",
|
||||
"zephyr",
|
||||
"juno",
|
||||
"kyanite",
|
||||
"cobalt",
|
||||
"mizar",
|
||||
"yildun",
|
||||
"lyra",
|
||||
"deneb",
|
||||
"draco",
|
||||
"jasper",
|
||||
"wren",
|
||||
"fomalhaut"
|
||||
],
|
||||
"correct_kid": "k_regulus",
|
||||
"decoy_kids": [
|
||||
"k_sirius",
|
||||
"k_spica",
|
||||
"k_octans",
|
||||
"k_jasper",
|
||||
"k_bellatrix",
|
||||
"k_garnet",
|
||||
"k_altair",
|
||||
"k_talc",
|
||||
"k_izar",
|
||||
"k_pavo",
|
||||
"k_draco",
|
||||
"k_vela",
|
||||
"k_fomalhaut",
|
||||
"k_norma",
|
||||
"k_olivine",
|
||||
"k_polaris",
|
||||
"k_castor",
|
||||
"k_zephyr",
|
||||
"k_kepler",
|
||||
"k_perseus",
|
||||
"k_rutile",
|
||||
"k_kyanite",
|
||||
"k_yildun",
|
||||
"k_cobalt",
|
||||
"k_arcturus",
|
||||
"k_aludra",
|
||||
"k_mensa",
|
||||
"k_wezen",
|
||||
"k_feldspar",
|
||||
"k_schist",
|
||||
"k_borealis",
|
||||
"k_vega",
|
||||
"k_basalt",
|
||||
"k_helios",
|
||||
"k_capella",
|
||||
"k_juno",
|
||||
"k_hydra",
|
||||
"k_wren",
|
||||
"k_rigel",
|
||||
"k_mizar",
|
||||
"k_lyra",
|
||||
"k_gemini",
|
||||
"k_deneb",
|
||||
"k_indus",
|
||||
"k_nashira",
|
||||
"k_procyon",
|
||||
"k_umbriel",
|
||||
"k_pyrite",
|
||||
"k_atlas"
|
||||
],
|
||||
"corpus_path": "<REPO_ROOT>/measure-agent-runs/experiment-1337-5/trial-2/prod-corpus.json",
|
||||
"corpus_size": 5,
|
||||
"reproduce": "prepare_trial.py --trial 2 --master-seed 1337"
|
||||
}
|
||||
156
benchmarks/silent-rotation/results/trial-2/mem0.json
Normal file
156
benchmarks/silent-rotation/results/trial-2/mem0.json
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
{
|
||||
"mode": "mem0",
|
||||
"fleet_size": 3,
|
||||
"provider": "moonshot",
|
||||
"model": "kimi-k3",
|
||||
"task": "make the failing end-to-end charge test pass",
|
||||
"repo": "<REPO_ROOT>/torture-v3.5",
|
||||
"measured": true,
|
||||
"fleet_tokens": {
|
||||
"input": 158050,
|
||||
"output": 9052,
|
||||
"total": 167102
|
||||
},
|
||||
"fleet_cost_usd": 0.60993,
|
||||
"cost_rate": {
|
||||
"input_per_mtok": 3.0,
|
||||
"output_per_mtok": 15.0
|
||||
},
|
||||
"fleet_wall_clock_seconds": 200.44,
|
||||
"fleet_iterations_total": 23,
|
||||
"agents_local_pass": 1,
|
||||
"agents_errored": 0,
|
||||
"tokens_uncounted": false,
|
||||
"agents_used_vestige_backfill": 0,
|
||||
"agents_used_vestige_log": 0,
|
||||
"memory_tool": "mem0_search",
|
||||
"memory_prewarm": {
|
||||
"attempted": true,
|
||||
"ok": true,
|
||||
"error": "",
|
||||
"seconds": 183.5
|
||||
},
|
||||
"agents_memory_alive": 3,
|
||||
"retrieval_ok_total": 19,
|
||||
"retrieval_err_total": 0,
|
||||
"retrieval_empty_total": 0,
|
||||
"retrieval_error_samples": [],
|
||||
"memory_layer_alive": true,
|
||||
"memory_unavailable": false,
|
||||
"transcripts": [
|
||||
"transcript-mem0-a0.json",
|
||||
"transcript-mem0-a1.json",
|
||||
"transcript-mem0-a2.json"
|
||||
],
|
||||
"demo_profile": "keyring",
|
||||
"fix_direction_counts": {
|
||||
"correct": 1,
|
||||
"wrong": 0,
|
||||
"other": 2
|
||||
},
|
||||
"fleet_verdict": "fixed_correctly",
|
||||
"integrated_tests_pass": true,
|
||||
"prod_replay_pass": true,
|
||||
"production_replay": {
|
||||
"ok": true,
|
||||
"verified": 5,
|
||||
"total": 5,
|
||||
"failures": [],
|
||||
"signed_under": "k_regulus",
|
||||
"ran": true
|
||||
},
|
||||
"merge_conflict_count": 0,
|
||||
"merge_conflicts": [],
|
||||
"cross_file_divergence": false,
|
||||
"fix_directions": {
|
||||
"a0": "kid:k_regulus",
|
||||
"a1": "kid_unset",
|
||||
"a2": "kid_unset"
|
||||
},
|
||||
"reasoning_effort": null,
|
||||
"truncated": false,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a0",
|
||||
"mode": "mem0",
|
||||
"tokens": {
|
||||
"input": 45550,
|
||||
"output": 3959,
|
||||
"total": 49509
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 190.53,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "mem0_search",
|
||||
"retrieval_ok_count": 3,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Fixed the failing end-to-end charge test. Root cause: the \"externalize secrets\" refactor left the signing key id blank in both identity-service/src/config.ts (activeKid: \"\") and ledger-service/src/config.ts (trustedKid: \"\"), so signSession threw \"no active signing key selected\". Per team memory (Q3 rotation runbook), the current live signer is k_regulus (k_sirius is decommissioned; k_spica is only pre-provisioned for a future cutover). Both configs now resolve the key id as process.env.MERIDIAN_ACTIVE_KID ?? \"k_regulus\", honoring the env-injection design while defaulting issuer and verifier to the same live key in lockstep. run_tests now exits 0.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"mode": "mem0",
|
||||
"tokens": {
|
||||
"input": 51417,
|
||||
"output": 1626,
|
||||
"total": 53043
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 121.93,
|
||||
"status": "looped",
|
||||
"local_tests_pass": false,
|
||||
"files_written": [],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "mem0_search",
|
||||
"retrieval_ok_count": 9,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a2",
|
||||
"mode": "mem0",
|
||||
"tokens": {
|
||||
"input": 61083,
|
||||
"output": 3467,
|
||||
"total": 64550
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 199.44,
|
||||
"status": "looped",
|
||||
"local_tests_pass": false,
|
||||
"files_written": [],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "mem0_search",
|
||||
"retrieval_ok_count": 7,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "",
|
||||
"turn_error": ""
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-07-21T07:25:30Z"
|
||||
}
|
||||
162
benchmarks/silent-rotation/results/trial-2/rag.json
Normal file
162
benchmarks/silent-rotation/results/trial-2/rag.json
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
{
|
||||
"mode": "rag",
|
||||
"fleet_size": 3,
|
||||
"provider": "moonshot",
|
||||
"model": "kimi-k3",
|
||||
"task": "make the failing end-to-end charge test pass",
|
||||
"repo": "<REPO_ROOT>/torture-v3.5",
|
||||
"measured": true,
|
||||
"fleet_tokens": {
|
||||
"input": 114130,
|
||||
"output": 6083,
|
||||
"total": 120213
|
||||
},
|
||||
"fleet_cost_usd": 0.433635,
|
||||
"cost_rate": {
|
||||
"input_per_mtok": 3.0,
|
||||
"output_per_mtok": 15.0
|
||||
},
|
||||
"fleet_wall_clock_seconds": 133.72,
|
||||
"fleet_iterations_total": 20,
|
||||
"agents_local_pass": 3,
|
||||
"agents_errored": 0,
|
||||
"tokens_uncounted": false,
|
||||
"agents_used_vestige_backfill": 0,
|
||||
"agents_used_vestige_log": 0,
|
||||
"memory_tool": "rag_search",
|
||||
"memory_prewarm": {
|
||||
"attempted": true,
|
||||
"ok": true,
|
||||
"error": "",
|
||||
"seconds": 0.4
|
||||
},
|
||||
"agents_memory_alive": 3,
|
||||
"retrieval_ok_total": 7,
|
||||
"retrieval_err_total": 0,
|
||||
"retrieval_empty_total": 0,
|
||||
"retrieval_error_samples": [],
|
||||
"memory_layer_alive": true,
|
||||
"memory_unavailable": false,
|
||||
"transcripts": [
|
||||
"transcript-rag-a0.json",
|
||||
"transcript-rag-a1.json",
|
||||
"transcript-rag-a2.json"
|
||||
],
|
||||
"demo_profile": "keyring",
|
||||
"fix_direction_counts": {
|
||||
"correct": 3,
|
||||
"wrong": 0,
|
||||
"other": 0
|
||||
},
|
||||
"fleet_verdict": "fixed_correctly",
|
||||
"integrated_tests_pass": true,
|
||||
"prod_replay_pass": true,
|
||||
"production_replay": {
|
||||
"ok": true,
|
||||
"verified": 5,
|
||||
"total": 5,
|
||||
"failures": [],
|
||||
"signed_under": "k_regulus",
|
||||
"ran": true
|
||||
},
|
||||
"merge_conflict_count": 0,
|
||||
"merge_conflicts": [],
|
||||
"cross_file_divergence": false,
|
||||
"fix_directions": {
|
||||
"a0": "kid:k_regulus",
|
||||
"a1": "kid:k_regulus",
|
||||
"a2": "kid:k_regulus"
|
||||
},
|
||||
"reasoning_effort": null,
|
||||
"truncated": false,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a0",
|
||||
"mode": "rag",
|
||||
"tokens": {
|
||||
"input": 44473,
|
||||
"output": 2137,
|
||||
"total": 46610
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 132.76,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": true,
|
||||
"memory_tool_used": "rag_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the \"externalize-secrets\" refactor left both key-id fields blank \u2014 identity-service `config.activeKid` was empty (causing signSession to throw \"no active signing key selected\") and ledger-service `ledgerConfig.trustedKid` was empty (which would have failed verification). Team memory confirmed the authoritative key: per the Q3 rotation runbook, the live signer is `k_regulus` (k_sirius is the decommissioned launch signer \u2014 the OPS-522 staging note was a red herring \u2014 and k_spica is only pre-provisioned for a future cutover). Fix: set `activeKid` in identity-service/src/config.ts and `trustedKid` in ledger-service/src/config.ts to `k_regulus`, each overridable via env (IDENTITY_ACTIVE_KID / LEDGER_TRUSTED_KID) to honor the env-injection direction of the refactor. Both sides now sign and verify with the same key, the canonical digests already matched, and the end-to-end charge test returns 200. run_tests exits 0.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"mode": "rag",
|
||||
"tokens": {
|
||||
"input": 37783,
|
||||
"output": 1528,
|
||||
"total": 39311
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 100.58,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": true,
|
||||
"memory_tool_used": "rag_search",
|
||||
"retrieval_ok_count": 3,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Both identity-service (activeKid) and ledger-service (trustedKid) had blank key ids left by the externalize-secrets refactor. Per the Q3 rotation runbook in team memory, the live signer is k_regulus (k_sirius decommissioned, k_spica only pre-provisioned), so I set both configs to k_regulus. Test passes.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a2",
|
||||
"mode": "rag",
|
||||
"tokens": {
|
||||
"input": 31874,
|
||||
"output": 2418,
|
||||
"total": 34292
|
||||
},
|
||||
"iterations": 6,
|
||||
"wall_clock_seconds": 112.85,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": true,
|
||||
"memory_tool_used": "rag_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "The externalize-secrets refactor left identity-service config.activeKid and ledger-service ledgerConfig.trustedKid blank, so signSession threw \"no active signing key selected\" and the e2e charge test failed at token minting. Fixed by making both configs read the kid from env (MERIDIAN_ACTIVE_KID / MERIDIAN_TRUSTED_KID) with a shared default of \"k_regulus\" \u2014 the current live signer per the Q3 rotation runbook in team memory (k_sirius is the decommissioned launch signer from the OPS-522 staging incident; k_spica is pre-provisioned for a future cutover that hasn't happened). Signer and verifier now agree on the same key, so mint \u2192 forward \u2192 verify \u2192 charge succeeds and run_tests exits 0.",
|
||||
"turn_error": ""
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-07-21T07:16:30Z"
|
||||
}
|
||||
162
benchmarks/silent-rotation/results/trial-2/supermemory.json
Normal file
162
benchmarks/silent-rotation/results/trial-2/supermemory.json
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
{
|
||||
"mode": "supermemory",
|
||||
"fleet_size": 3,
|
||||
"provider": "moonshot",
|
||||
"model": "kimi-k3",
|
||||
"task": "make the failing end-to-end charge test pass",
|
||||
"repo": "<REPO_ROOT>/torture-v3.5",
|
||||
"measured": true,
|
||||
"fleet_tokens": {
|
||||
"input": 131768,
|
||||
"output": 6686,
|
||||
"total": 138454
|
||||
},
|
||||
"fleet_cost_usd": 0.495594,
|
||||
"cost_rate": {
|
||||
"input_per_mtok": 3.0,
|
||||
"output_per_mtok": 15.0
|
||||
},
|
||||
"fleet_wall_clock_seconds": 161.39,
|
||||
"fleet_iterations_total": 23,
|
||||
"agents_local_pass": 3,
|
||||
"agents_errored": 0,
|
||||
"tokens_uncounted": false,
|
||||
"agents_used_vestige_backfill": 0,
|
||||
"agents_used_vestige_log": 0,
|
||||
"memory_tool": "supermemory_search",
|
||||
"memory_prewarm": {
|
||||
"attempted": true,
|
||||
"ok": true,
|
||||
"error": "",
|
||||
"seconds": 2.1
|
||||
},
|
||||
"agents_memory_alive": 3,
|
||||
"retrieval_ok_total": 8,
|
||||
"retrieval_err_total": 0,
|
||||
"retrieval_empty_total": 0,
|
||||
"retrieval_error_samples": [],
|
||||
"memory_layer_alive": true,
|
||||
"memory_unavailable": false,
|
||||
"transcripts": [
|
||||
"transcript-supermemory-a0.json",
|
||||
"transcript-supermemory-a1.json",
|
||||
"transcript-supermemory-a2.json"
|
||||
],
|
||||
"demo_profile": "keyring",
|
||||
"fix_direction_counts": {
|
||||
"correct": 3,
|
||||
"wrong": 0,
|
||||
"other": 0
|
||||
},
|
||||
"fleet_verdict": "fixed_correctly",
|
||||
"integrated_tests_pass": true,
|
||||
"prod_replay_pass": true,
|
||||
"production_replay": {
|
||||
"ok": true,
|
||||
"verified": 5,
|
||||
"total": 5,
|
||||
"failures": [],
|
||||
"signed_under": "k_regulus",
|
||||
"ran": true
|
||||
},
|
||||
"merge_conflict_count": 0,
|
||||
"merge_conflicts": [],
|
||||
"cross_file_divergence": false,
|
||||
"fix_directions": {
|
||||
"a0": "kid:k_regulus",
|
||||
"a1": "kid:k_regulus",
|
||||
"a2": "kid:k_regulus"
|
||||
},
|
||||
"reasoning_effort": null,
|
||||
"truncated": false,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a0",
|
||||
"mode": "supermemory",
|
||||
"tokens": {
|
||||
"input": 38327,
|
||||
"output": 1719,
|
||||
"total": 40046
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 144.29,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "supermemory_search",
|
||||
"retrieval_ok_count": 2,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "The externalize-secrets refactor left the signing key id blank in both identity-service (activeKid) and ledger-service (trustedKid), so signSession threw \"no active signing key selected\" and the end-to-end charge test failed. Per the team memory's Q3 rotation runbook, the live signer is k_regulus (k_sirius is decommissioned, k_spica only pre-provisioned for a future cutover), and verifiers must trust k_regulus. I set activeKid = \"k_regulus\" in identity-service/src/config.ts and trustedKid = \"k_regulus\" in ledger-service/src/config.ts \u2014 the minimal, directionally-correct fix (both sides must select the same current key). Canonicalization, gateway pass-through, and verification logic were already consistent; no other changes needed. Tests now pass (exit 0).",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"mode": "supermemory",
|
||||
"tokens": {
|
||||
"input": 51027,
|
||||
"output": 2731,
|
||||
"total": 53758
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 160.34,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "supermemory_search",
|
||||
"retrieval_ok_count": 3,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the \"externalize secrets\" refactor blanked the signing key id in both identity-service (activeKid) and ledger-service (trustedKid), so signSession threw \"no active signing key selected\". Fix: set both services to select the current live signer k_regulus (per the Q3 rotation runbook in team memory \u2014 k_sirius is decommissioned, k_spica not yet active), with env overrides (MERIDIAN_ACTIVE_KID / MERIDIAN_TRUSTED_KID) to preserve the env-injection direction of the refactor. The end-to-end charge test now passes (exit 0).",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a2",
|
||||
"mode": "supermemory",
|
||||
"tokens": {
|
||||
"input": 42414,
|
||||
"output": 2236,
|
||||
"total": 44650
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 147.72,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": false,
|
||||
"used_vestige_log": false,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "supermemory_search",
|
||||
"retrieval_ok_count": 3,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the \"externalize secrets\" refactor left both identity-service `config.activeKid` and ledger-service `ledgerConfig.trustedKid` blank, so minting threw \"no active signing key selected\" (and verification would have thrown \"no trusted verification key selected\"). Team memory (Q3 rotation runbook) identified k_regulus as the current live signer \u2014 k_sirius decommissioned, k_spica only pre-provisioned for a future cutover \u2014 so trusting any other key would reject the installed base. Fix: both configs now read the key id from env (MERIDIAN_ACTIVE_KID / MERIDIAN_TRUSTED_KID), preserving the externalize-secrets direction, with a fallback default of \"k_regulus\" so the local harness/test works without env setup. Both sides use the same key, so the HMAC-over-canonical-claims signature verifies and the charge returns 200.",
|
||||
"turn_error": ""
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-07-21T07:28:22Z"
|
||||
}
|
||||
162
benchmarks/silent-rotation/results/trial-2/sync.json
Normal file
162
benchmarks/silent-rotation/results/trial-2/sync.json
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
{
|
||||
"mode": "sync",
|
||||
"fleet_size": 3,
|
||||
"provider": "moonshot",
|
||||
"model": "kimi-k3",
|
||||
"task": "make the failing end-to-end charge test pass",
|
||||
"repo": "<REPO_ROOT>/torture-v3.5",
|
||||
"measured": true,
|
||||
"fleet_tokens": {
|
||||
"input": 125623,
|
||||
"output": 5473,
|
||||
"total": 131096
|
||||
},
|
||||
"fleet_cost_usd": 0.458964,
|
||||
"cost_rate": {
|
||||
"input_per_mtok": 3.0,
|
||||
"output_per_mtok": 15.0
|
||||
},
|
||||
"fleet_wall_clock_seconds": 140.86,
|
||||
"fleet_iterations_total": 21,
|
||||
"agents_local_pass": 3,
|
||||
"agents_errored": 0,
|
||||
"tokens_uncounted": false,
|
||||
"agents_used_vestige_backfill": 3,
|
||||
"agents_used_vestige_log": 3,
|
||||
"memory_tool": "vestige_backfill",
|
||||
"memory_prewarm": {
|
||||
"attempted": true,
|
||||
"ok": true,
|
||||
"error": "",
|
||||
"seconds": 0.4
|
||||
},
|
||||
"agents_memory_alive": 3,
|
||||
"retrieval_ok_total": 3,
|
||||
"retrieval_err_total": 0,
|
||||
"retrieval_empty_total": 0,
|
||||
"retrieval_error_samples": [],
|
||||
"memory_layer_alive": true,
|
||||
"memory_unavailable": false,
|
||||
"transcripts": [
|
||||
"transcript-sync-a0.json",
|
||||
"transcript-sync-a1.json",
|
||||
"transcript-sync-a2.json"
|
||||
],
|
||||
"demo_profile": "keyring",
|
||||
"fix_direction_counts": {
|
||||
"correct": 3,
|
||||
"wrong": 0,
|
||||
"other": 0
|
||||
},
|
||||
"fleet_verdict": "fixed_correctly",
|
||||
"integrated_tests_pass": true,
|
||||
"prod_replay_pass": true,
|
||||
"production_replay": {
|
||||
"ok": true,
|
||||
"verified": 5,
|
||||
"total": 5,
|
||||
"failures": [],
|
||||
"signed_under": "k_regulus",
|
||||
"ran": true
|
||||
},
|
||||
"merge_conflict_count": 0,
|
||||
"merge_conflicts": [],
|
||||
"cross_file_divergence": false,
|
||||
"fix_directions": {
|
||||
"a0": "kid:k_regulus",
|
||||
"a1": "kid:k_regulus",
|
||||
"a2": "kid:k_regulus"
|
||||
},
|
||||
"reasoning_effort": null,
|
||||
"truncated": false,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "a0",
|
||||
"mode": "sync",
|
||||
"tokens": {
|
||||
"input": 40609,
|
||||
"output": 1822,
|
||||
"total": 42431
|
||||
},
|
||||
"iterations": 7,
|
||||
"wall_clock_seconds": 94.81,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": true,
|
||||
"used_vestige_log": true,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "vestige_backfill",
|
||||
"retrieval_ok_count": 1,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the externalize-secrets refactor blanked activeKid in identity-service/src/config.ts and trustedKid in ledger-service/src/config.ts, so signSession threw \"no active signing key selected\". Per the Q3 rotation runbook (surfaced via vestige_backfill), the live signing key is k_regulus. Fix: set activeKid=\"k_regulus\" in identity-service config and trustedKid=\"k_regulus\" in ledger-service config (each with a MERIDIAN_SIGNING_KID env override for production/KMS), keeping both sides on the same shared-keyring entry. Canonical digest logic and gateway pass-through were already consistent; no changes needed there. run_tests now exits 0.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"mode": "sync",
|
||||
"tokens": {
|
||||
"input": 36587,
|
||||
"output": 1751,
|
||||
"total": 38338
|
||||
},
|
||||
"iterations": 6,
|
||||
"wall_clock_seconds": 103.96,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": true,
|
||||
"used_vestige_log": true,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "vestige_backfill",
|
||||
"retrieval_ok_count": 1,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the externalize-secrets refactor blanked the key-id selections in both identity-service (activeKid) and ledger-service (trustedKid), so signSession threw \"no active signing key selected\" and no token could be minted/verified. Per the Q3 rotation runbook (surfaced via vestige_backfill), the live signer is k_regulus. Fix: set activeKid=\"k_regulus\" in identity-service/src/config.ts and trustedKid=\"k_regulus\" in ledger-service/src/config.ts (both sides must select the same keyring entry). Canonical digest recipes and gateway pass-through were already consistent and untouched. Direction logged to shared memory for fleet convergence.",
|
||||
"turn_error": ""
|
||||
},
|
||||
{
|
||||
"agent_id": "a2",
|
||||
"mode": "sync",
|
||||
"tokens": {
|
||||
"input": 48427,
|
||||
"output": 1900,
|
||||
"total": 50327
|
||||
},
|
||||
"iterations": 8,
|
||||
"wall_clock_seconds": 140.0,
|
||||
"status": "fixed",
|
||||
"local_tests_pass": true,
|
||||
"files_written": [
|
||||
"identity-service/src/config.ts",
|
||||
"ledger-service/src/config.ts"
|
||||
],
|
||||
"out_of_bounds_writes": [],
|
||||
"used_vestige_backfill": true,
|
||||
"used_vestige_log": true,
|
||||
"used_rag_search": false,
|
||||
"memory_tool_used": "vestige_backfill",
|
||||
"retrieval_ok_count": 1,
|
||||
"retrieval_err_count": 0,
|
||||
"retrieval_empty_count": 0,
|
||||
"retrieval_errors": [],
|
||||
"memory_layer_alive": true,
|
||||
"finish_summary": "Root cause: the \"externalize-secrets\" refactor blanked the signing-key selection in both identity-service (config.activeKid = \"\") and ledger-service (ledgerConfig.trustedKid = \"\"), so signSession threw \"no active signing key selected\" and verification had no trusted key. A retroactive backfill surfaced the Q3 rotation runbook identifying k_regulus as the platform's live signing key. Fix: set activeKid = \"k_regulus\" in identity-service/src/config.ts and trustedKid = \"k_regulus\" in ledger-service/src/config.ts (signer and verifier must select the same key). No changes to keyring material, canonical digest, signer, or verifier logic. The end-to-end charge test now passes (exit 0). Direction published to shared fleet memory via vestige_log so peers converge on the same change.",
|
||||
"turn_error": ""
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-07-21T07:18:59Z"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
152
benchmarks/silent-rotation/tests/arm_liveness.py
Normal file
152
benchmarks/silent-rotation/tests/arm_liveness.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#!/usr/bin/env python
|
||||
"""ARM LIVENESS PROBE -- $0, no paid API.
|
||||
|
||||
For each of the 7 benchmark arms, prove the memory tool is CALLABLE and returns
|
||||
REAL DATA (not an ERROR string), under two conditions:
|
||||
|
||||
A. single-threaded (the optimistic case)
|
||||
B. 3 concurrent threads (the ACTUAL fleet condition -- ThreadPoolExecutor
|
||||
max_workers=FLEET_SIZE in fleet_runner.py:1278)
|
||||
|
||||
Condition B is what silently killed hindsight/zep/mem0 in the paid runs.
|
||||
|
||||
Exit code 0 only if every requested arm passes BOTH conditions.
|
||||
"""
|
||||
import concurrent.futures
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
HARNESS = Path("$REPO_ROOT/measure-agent-runs")
|
||||
REPO = Path("$REPO_ROOT/torture-v3.5")
|
||||
|
||||
# --- env exactly as the per-arm launchers set it -----------------------------
|
||||
os.environ.setdefault("VESTIGE_BIN", "$HOME/vestige/target/release/vestige")
|
||||
os.environ.setdefault("VESTIGE_DATA_DIR", str(REPO / ".vestige-demo-db"))
|
||||
os.environ.setdefault("CORRECT_KID", "k_nashira")
|
||||
os.environ.setdefault("PROD_CORPUS", str(HERE / "prod-corpus.json"))
|
||||
os.environ.setdefault("ARM_TOPK", "3")
|
||||
os.environ.setdefault("DEMO_PROFILE", "keyring")
|
||||
# supermemory (run-supermemory.sh)
|
||||
os.environ.setdefault("SUPERMEMORY_BASE_URL", "http://localhost:6767")
|
||||
# hindsight (run-hindsight.sh). 127.0.0.1 NOT localhost -- hindsight-api binds
|
||||
# IPv4 only and macOS tries ::1 first, so "localhost" can hit anything holding
|
||||
# the IPv6 wildcard on this port.
|
||||
os.environ.setdefault("HINDSIGHT_BASE_URL", "http://127.0.0.1:8899")
|
||||
# zep / graphiti (run-zep.sh)
|
||||
os.environ.setdefault("ZEP_FALKOR_HOST", "localhost")
|
||||
os.environ.setdefault("ZEP_FALKOR_PORT", "6379")
|
||||
# Do NOT hardcode ZEP_LLM_MODEL/ZEP_LLM_BASE here -- let runner.py's defaults
|
||||
# apply (Qwen3.6-35B-A3B on MLX :8081). A setdefault here runs BEFORE runner
|
||||
# imports and would override the real config (that is exactly the bug that made
|
||||
# this probe silently test gemma3:12b instead of the configured model).
|
||||
os.environ.setdefault("ZEP_EMBED_MODEL", "nomic-embed-text")
|
||||
os.environ.setdefault("GRAPHITI_TELEMETRY_ENABLED", "false")
|
||||
os.environ.setdefault("MEM0_USER", "bench")
|
||||
|
||||
sys.path.insert(0, str(HARNESS / "agent"))
|
||||
sys.path.insert(0, str(HARNESS))
|
||||
|
||||
import runner # noqa: E402
|
||||
|
||||
QUERY = ("identity issuer and ledger verifier have no signing key id selected "
|
||||
"after the externalize-secrets refactor, every charge fails")
|
||||
|
||||
# arm -> (tool callable, args)
|
||||
ARMS = {
|
||||
"anarchy": (None, None), # control: no memory tool by design
|
||||
"rag": (runner.tool_rag_search, {"query": QUERY}),
|
||||
"sync": (runner.tool_vestige_backfill, {}),
|
||||
"mem0": (runner.tool_mem0_search, {"query": QUERY}),
|
||||
"supermemory": (runner.tool_supermemory_search, {"query": QUERY}),
|
||||
"hindsight": (runner.tool_hindsight_search, {"query": QUERY}),
|
||||
"zep": (runner.tool_zep_search, {"query": QUERY}),
|
||||
}
|
||||
|
||||
|
||||
def classify(out: str):
|
||||
"""Return (ok, label). ok=True only if real retrieved content came back."""
|
||||
if out is None:
|
||||
return False, "None returned"
|
||||
s = str(out)
|
||||
if s.strip().upper().startswith("ERROR"):
|
||||
return False, s.strip().split("\n")[0][:200]
|
||||
if not s.strip():
|
||||
return False, "empty string"
|
||||
# a real retrieval must mention something from the corpus
|
||||
return True, f"{len(s)} chars"
|
||||
|
||||
|
||||
def call_once(fn, args, tag):
|
||||
t0 = time.time()
|
||||
try:
|
||||
out = fn(REPO, dict(args))
|
||||
ok, label = classify(out)
|
||||
return {"tag": tag, "ok": ok, "label": label, "secs": round(time.time() - t0, 2),
|
||||
"sample": str(out)[:300]}
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {"tag": tag, "ok": False, "label": f"EXCEPTION {type(e).__name__}: {e}"[:200],
|
||||
"secs": round(time.time() - t0, 2), "sample": traceback.format_exc()[-400:]}
|
||||
|
||||
|
||||
def probe(arm, threads):
|
||||
fn, args = ARMS[arm]
|
||||
if fn is None:
|
||||
return [{"tag": f"{arm}-control", "ok": True, "label": "no memory tool by design",
|
||||
"secs": 0.0, "sample": ""}]
|
||||
if threads == 1:
|
||||
return [call_once(fn, args, f"{arm}-single")]
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as pool:
|
||||
futs = [pool.submit(call_once, fn, args, f"{arm}-t{i}") for i in range(threads)]
|
||||
return [f.result() for f in futs]
|
||||
|
||||
|
||||
def main():
|
||||
want = sys.argv[1:] or list(ARMS)
|
||||
results = {}
|
||||
for arm in want:
|
||||
if arm not in ARMS:
|
||||
print(f"!! unknown arm {arm}")
|
||||
continue
|
||||
print("=" * 78, flush=True)
|
||||
print(f"ARM: {arm}", flush=True)
|
||||
# reset per-process caches so each arm starts cold, like a fresh run
|
||||
for cache in ("_RAG_CACHE", "_MEM0_CACHE", "_SM_CACHE", "_HINDSIGHT_CACHE", "_ZEP_CACHE"):
|
||||
c = getattr(runner, cache, None)
|
||||
if isinstance(c, dict) and arm != "rag":
|
||||
pass # keep rag cache (shared corpus) but let arms re-init
|
||||
single = probe(arm, 1)
|
||||
for r in single:
|
||||
print(f" [SINGLE ] ok={r['ok']:<5} {r['secs']:>6}s {r['label']}", flush=True)
|
||||
if not r["ok"]:
|
||||
print(f" sample: {r['sample'][:280]}", flush=True)
|
||||
conc = probe(arm, 3) if arm != "anarchy" else []
|
||||
for r in conc:
|
||||
print(f" [3-THREAD] ok={r['ok']:<5} {r['secs']:>6}s {r['label']}", flush=True)
|
||||
results[arm] = {
|
||||
"single_ok": all(r["ok"] for r in single),
|
||||
"concurrent_ok": all(r["ok"] for r in conc) if conc else True,
|
||||
"single": single, "concurrent": conc,
|
||||
}
|
||||
print("\n" + "=" * 78)
|
||||
print("MATRIX")
|
||||
print("=" * 78)
|
||||
print(f" {'arm':<14}{'single':<10}{'3-thread':<10}verdict")
|
||||
allgood = True
|
||||
for arm, r in results.items():
|
||||
v = "READY" if (r["single_ok"] and r["concurrent_ok"]) else "BROKEN"
|
||||
if v == "BROKEN":
|
||||
allgood = False
|
||||
print(f" {arm:<14}{str(r['single_ok']):<10}{str(r['concurrent_ok']):<10}{v}")
|
||||
out = HERE / "liveness-result.json"
|
||||
out.write_text(json.dumps(results, indent=2, default=str))
|
||||
print(f"\n full detail -> {out}")
|
||||
return 0 if allgood else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
178
benchmarks/silent-rotation/tests/bm25_baseline.py
Normal file
178
benchmarks/silent-rotation/tests/bm25_baseline.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
"""Lexical (BM25) and dense-cosine baseline over the Silent Rotation corpus.
|
||||
|
||||
WHY THIS EXISTS
|
||||
---------------
|
||||
The obvious objection to this benchmark is that the `rag` arm is dense-cosine
|
||||
only, while the answer being hunted is a rare literal identifier (`k_nashira`),
|
||||
which is supposed to be exact-match's home ground. If BM25 trivially finds the
|
||||
causal memory, then the benchmark is measuring a baseline gap rather than
|
||||
anything about memory architecture.
|
||||
|
||||
So we ran BM25. It does not find it either. On the symptom-shaped queries every
|
||||
agent opens with, BM25 and dense cosine both bury the causal memory at rank 7 of
|
||||
8, and on the reformulated queries that actually worked, BM25 is one to four
|
||||
ranks WORSE than dense.
|
||||
|
||||
The mechanism: the planted decoys share the target's exact vocabulary ("signing
|
||||
key", "kid", "keyring", "rotation", "active"), so lexical scoring rewards them at
|
||||
least as much as the truth. What separates the right key from the wrong one is
|
||||
which key is CURRENT, and recency carries no lexical signal.
|
||||
|
||||
There is also a practical ceiling on exact match here. The live key is randomized
|
||||
per trial from a 50-key keyring and appears in no file the agent can read, so you
|
||||
can only type it into a query after you already know the answer. The exact-key
|
||||
probe below is included to show that even then it only reaches rank 3.
|
||||
|
||||
USAGE
|
||||
-----
|
||||
python3 bm25_baseline.py <corpus.json> [--no-dense]
|
||||
|
||||
<corpus.json> is a `vestige export --format json` dump of the seeded trial store,
|
||||
i.e. the exact same source the competitor arms read (see
|
||||
`harness/agent/runner.py::_rag_load_facts`). The retrievable corpus excludes the
|
||||
PRODUCTION OUTAGE memory, matching the arms' behaviour: a retriever indexes prior
|
||||
history, not the live error it is being asked about.
|
||||
|
||||
Dense scoring needs ollama on localhost:11434 with nomic-embed-text, the same
|
||||
embedder the `rag` arm uses. Pass --no-dense to run BM25 only.
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
from collections import Counter
|
||||
|
||||
EMBED_MODEL = "nomic-embed-text:latest"
|
||||
OLLAMA = "http://localhost:11434/api/embeddings"
|
||||
|
||||
# The queries below are VERBATIM from the trial-1 agent transcripts in
|
||||
# results/trial-1/. They are not authored for this script. Each is labelled with
|
||||
# the agent that issued it so it can be traced back.
|
||||
QUERIES = [
|
||||
("rag a0, first query", "end-to-end charge test failing identity gateway ledger session token verify"),
|
||||
("zep a0, first query", "end-to-end charge test failing session token identity gateway ledger"),
|
||||
("zep a0, third query", "default key kid keyring shipped platform active key"),
|
||||
("mem0 a0, second query", "which key id active signing key k_antares externalize secrets refactor blank config"),
|
||||
("supermemory a0, first", "no active signing key selected signing key rotation identity-service keyring"),
|
||||
]
|
||||
|
||||
|
||||
def load_corpus(path):
|
||||
"""Mirror runner.py::_rag_load_facts. content/body only, outage row dropped."""
|
||||
raw = json.load(open(path))
|
||||
rows = raw if isinstance(raw, list) else raw.get("memories", raw.get("nodes", []))
|
||||
docs = []
|
||||
for m in rows:
|
||||
body = m.get("content") or m.get("body") or ""
|
||||
if not body or "PRODUCTION OUTAGE" in body:
|
||||
continue
|
||||
docs.append({"body": body, "is_cause": "keyring_rotation" in (m.get("tags") or [])})
|
||||
return rows, docs
|
||||
|
||||
|
||||
def tokenize(s):
|
||||
return re.findall(r"[a-z0-9_]+", s.lower())
|
||||
|
||||
|
||||
class BM25:
|
||||
"""Okapi BM25, k1=1.5, b=0.75. Standard parameters, no tuning."""
|
||||
|
||||
def __init__(self, docs, k1=1.5, b=0.75):
|
||||
self.k1, self.b = k1, b
|
||||
self.corpus = [tokenize(d["body"]) for d in docs]
|
||||
self.n = len(self.corpus)
|
||||
self.avgdl = sum(len(c) for c in self.corpus) / self.n
|
||||
self.df = Counter()
|
||||
for c in self.corpus:
|
||||
for t in set(c):
|
||||
self.df[t] += 1
|
||||
|
||||
def idf(self, t):
|
||||
return math.log(1 + (self.n - self.df[t] + 0.5) / (self.df[t] + 0.5))
|
||||
|
||||
def score(self, query, i):
|
||||
c = self.corpus[i]
|
||||
freq = Counter(c)
|
||||
dl = len(c)
|
||||
total = 0.0
|
||||
for t in tokenize(query):
|
||||
if t not in self.df:
|
||||
continue
|
||||
total += self.idf(t) * (freq[t] * (self.k1 + 1)) / (
|
||||
freq[t] + self.k1 * (1 - self.b + self.b * dl / self.avgdl)
|
||||
)
|
||||
return total
|
||||
|
||||
|
||||
def embed(text):
|
||||
req = urllib.request.Request(
|
||||
OLLAMA,
|
||||
data=json.dumps({"model": EMBED_MODEL, "prompt": text}).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=120) as r:
|
||||
return json.load(r)["embedding"]
|
||||
|
||||
|
||||
def cosine(a, b):
|
||||
dot = sum(x * y for x, y in zip(a, b))
|
||||
na = math.sqrt(sum(x * x for x in a))
|
||||
nb = math.sqrt(sum(x * x for x in b))
|
||||
return dot / (na * nb + 1e-12)
|
||||
|
||||
|
||||
def rank_of_cause(order, docs):
|
||||
return [k for k, i in enumerate(order) if docs[i]["is_cause"]][0] + 1
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
sys.exit(2)
|
||||
path = sys.argv[1]
|
||||
use_dense = "--no-dense" not in sys.argv
|
||||
|
||||
rows, docs = load_corpus(path)
|
||||
n = len(docs)
|
||||
causes = sum(d["is_cause"] for d in docs)
|
||||
keys = sorted({w.strip(".,;:\"'()") for d in docs for w in d["body"].split() if w.startswith("k_")})
|
||||
|
||||
print(f" seeded memories in the store : {len(rows)}")
|
||||
print(f" retrievable competitor corpus: {n} (outage row excluded, as runner.py does)")
|
||||
print(f" causal memories present : {causes}")
|
||||
print(f" key identifiers in corpus : {keys}")
|
||||
if causes != 1:
|
||||
print(" WARNING: expected exactly one causal memory; the ranks below are not meaningful.")
|
||||
|
||||
bm = BM25(docs)
|
||||
dense_vecs = [embed(d["body"]) for d in docs] if use_dense else None
|
||||
|
||||
print()
|
||||
print(f" rank of the causal memory out of {n}, lower is better")
|
||||
print(f" {'query (verbatim from a transcript)':<34}{'dense':<9}{'bm25'}")
|
||||
print(" " + "-" * 52)
|
||||
for label, q in QUERIES:
|
||||
bm_rank = rank_of_cause(sorted(range(n), key=lambda i: -bm.score(q, i)), docs)
|
||||
if use_dense:
|
||||
qv = embed(q)
|
||||
d_rank = rank_of_cause(sorted(range(n), key=lambda i: -cosine(qv, dense_vecs[i])), docs)
|
||||
print(f" {label:<34}#{d_rank:<8}#{bm_rank}")
|
||||
else:
|
||||
print(f" {label:<34}{'-':<9}#{bm_rank}")
|
||||
|
||||
# The exact-key probe. Only possible once you already know the answer, which
|
||||
# is why it is reported separately and not as evidence that lexical works.
|
||||
correct = [k for k in keys if k not in ("k_antares", "k_vela", "k_sirius", "k_spica")]
|
||||
if correct:
|
||||
probe = f"{correct[0]} active signing key"
|
||||
r = rank_of_cause(sorted(range(n), key=lambda i: -bm.score(probe, i)), docs)
|
||||
print()
|
||||
print(f" exact-key probe {probe!r}: BM25 rank #{r} of {n}")
|
||||
print(" (impossible in practice: the live key is randomized per trial and appears")
|
||||
print(" in no file the agent can read, so this query requires knowing the answer)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
145
benchmarks/silent-rotation/tests/verify-arms.sh
Executable file
145
benchmarks/silent-rotation/tests/verify-arms.sh
Executable file
|
|
@ -0,0 +1,145 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# verify-arms.sh -- PROVE every benchmark arm's memory layer is CALLABLE and
|
||||
# returns REAL DATA before a single paid token is spent.
|
||||
#
|
||||
# WHY THIS EXISTS
|
||||
# ---------------
|
||||
# On Jul 20 2026 an adversarial audit of a completed, already-paid-for 7-arm run
|
||||
# found that THREE of the six competitor memory layers had never successfully
|
||||
# retrieved anything, in any recorded run:
|
||||
#
|
||||
# hindsight 19/19 retrieval calls returned an ERROR string (0 successes)
|
||||
# zep 22/22 retrieval calls returned an ERROR string (0 successes)
|
||||
# mem0 14/15 retrieval calls returned an ERROR string (1 success)
|
||||
#
|
||||
# Every one of those was scored as a substantive retrieval LOSS and aggregated
|
||||
# into a published "competitor 0/5" column. They were not losses. They were
|
||||
# crashes: an IPv6 port collision, a shared asyncio loop driven from three
|
||||
# threads, and an ingest that exceeded its tool-call timeout. The result JSON
|
||||
# reported agents_errored=0 for all of them, because the harness only ever
|
||||
# tracked Vestige's own tool usage.
|
||||
#
|
||||
# A benchmark that cannot tell "the competitor retrieved and chose wrong" from
|
||||
# "the competitor's backend never answered" is not measuring what it claims to
|
||||
# measure. This script is the gate that makes that failure mode impossible to
|
||||
# ship again. Run it, get a green matrix, THEN spend money.
|
||||
#
|
||||
# WHAT IT CHECKS, PER ARM
|
||||
# 1. single-threaded -- the tool returns real content, not an "ERROR:" string
|
||||
# 2. 3 concurrent threads -- the ACTUAL fleet condition (fleet_runner.py runs
|
||||
# FLEET_SIZE agents in a ThreadPoolExecutor). Every one of the three dead
|
||||
# arms above passed condition 1 at some point and failed condition 2.
|
||||
#
|
||||
# USAGE
|
||||
# bash verify-arms.sh # all 7 arms
|
||||
# bash verify-arms.sh rag sync zep # a subset
|
||||
#
|
||||
# Exit 0 = every checked arm is READY. Non-zero = do NOT start a paid run.
|
||||
#
|
||||
set -uo pipefail
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="${TORTURE_REPO:-$HERE/../torture-v3.5}"
|
||||
PY="${PYTHON:-$HERE/.venv-arms/bin/python}"
|
||||
[ -x "$PY" ] || PY=python3
|
||||
VESTIGE_BIN="${VESTIGE_BIN:-$HOME/vestige/target/release/vestige}"
|
||||
MASTER_SEED="${MASTER_SEED:-1337}"
|
||||
TRIAL="${TRIAL:-1}"
|
||||
|
||||
ARMS=("$@")
|
||||
if [ ${#ARMS[@]} -eq 0 ]; then
|
||||
ARMS=(anarchy rag sync mem0 supermemory hindsight zep)
|
||||
fi
|
||||
|
||||
echo "=============================================================="
|
||||
echo " ARM LIVENESS GATE -- \$0, no paid API"
|
||||
echo " arms: ${ARMS[*]}"
|
||||
echo " seed: $MASTER_SEED trial: $TRIAL"
|
||||
echo "=============================================================="
|
||||
|
||||
# --- 0. Dependencies the local arms need --------------------------------------
|
||||
fail=0
|
||||
echo
|
||||
echo "-- service preflight --"
|
||||
# NOTE: use a real socket connect, NOT bash's /dev/tcp. Under zsh/bash subshells
|
||||
# `(exec 3<>/dev/tcp/host/port)` reports a false failure for services that are
|
||||
# demonstrably up -- it reported FalkorDB DEAD while 127.0.0.1:6379 was
|
||||
# answering PING with +PONG. A false DEAD here would block a run for no reason.
|
||||
check_port() { # name host port
|
||||
if "$PY" -c "
|
||||
import socket,sys
|
||||
try:
|
||||
socket.create_connection(('$2', $3), timeout=5).close()
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
" 2>/dev/null; then
|
||||
echo " OK $1 ($2:$3)"
|
||||
else
|
||||
echo " DEAD $1 ($2:$3) <-- required by an arm below"
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
check_port "ollama " 127.0.0.1 11434
|
||||
check_port "hindsight-api" 127.0.0.1 8899
|
||||
check_port "supermemory " 127.0.0.1 6767
|
||||
check_port "falkordb(zep)" 127.0.0.1 6379
|
||||
|
||||
# hindsight-api binds IPv4 ONLY. If anything holds the IPv6 wildcard on its
|
||||
# port, "localhost" resolves to ::1 first and every call silently hits the
|
||||
# wrong server -- this actually happened (a stray `python -m http.server 8899`
|
||||
# answered HTTP 501 to every hindsight request while the real service was
|
||||
# healthy on IPv4). Detect the collision explicitly rather than debugging it
|
||||
# again from a confusing error message.
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
listeners=$(lsof -nP -iTCP:8899 -sTCP:LISTEN -t 2>/dev/null | sort -u | wc -l | tr -d ' ')
|
||||
if [ "${listeners:-0}" -gt 1 ]; then
|
||||
echo " WARN more than one process is listening on :8899 -- IPv6/IPv4 collision risk"
|
||||
lsof -nP -iTCP:8899 -sTCP:LISTEN 2>/dev/null | sed 's/^/ /'
|
||||
fail=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
echo
|
||||
echo "PREFLIGHT FAILED -- start the missing services before running the gate."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# --- 1. Seed a real trial so the arms have a real corpus ----------------------
|
||||
echo
|
||||
echo "-- seeding trial $TRIAL (seed $MASTER_SEED) --"
|
||||
SCRATCH="$(mktemp -d)"
|
||||
CORRECT_KID="$("$PY" "$HERE/agent/prepare_trial.py" --repo "$REPO" --trial "$TRIAL" \
|
||||
--master-seed "$MASTER_SEED" --corpus-out "$SCRATCH/prod-corpus.json" \
|
||||
--manifest-out "$SCRATCH/manifest.json" --vestige-bin "$VESTIGE_BIN" | tail -1)"
|
||||
if [ -z "${CORRECT_KID:-}" ]; then
|
||||
echo " ERROR: prepare_trial.py produced no correct key"; exit 2
|
||||
fi
|
||||
echo " correct key (memory only): $CORRECT_KID"
|
||||
|
||||
export VESTIGE_BIN VESTIGE_DATA_DIR="${VESTIGE_DATA_DIR:-$REPO/.vestige-demo-db}"
|
||||
SEED="$REPO/.vestige-seed.sh"
|
||||
if [ -f "$SEED" ]; then
|
||||
VESTIGE_BIN="$VESTIGE_BIN" VESTIGE_DATA_DIR="$VESTIGE_DATA_DIR" bash "$SEED" >/dev/null 2>&1
|
||||
echo " vestige DB seeded -> $VESTIGE_DATA_DIR"
|
||||
fi
|
||||
export CORRECT_KID PROD_CORPUS="$SCRATCH/prod-corpus.json"
|
||||
|
||||
# --- 2. Probe every arm, single-threaded AND under fleet concurrency ----------
|
||||
echo
|
||||
"$PY" "$HERE/tests/arm_liveness.py" "${ARMS[@]}"
|
||||
rc=$?
|
||||
|
||||
echo
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
echo "=============================================================="
|
||||
echo " ALL CHECKED ARMS READY -- safe to start a paid run."
|
||||
echo "=============================================================="
|
||||
else
|
||||
echo "=============================================================="
|
||||
echo " ONE OR MORE ARMS ARE BROKEN -- DO NOT START A PAID RUN."
|
||||
echo " A broken arm publishes as a competitor 0/N that is really a crash."
|
||||
echo "=============================================================="
|
||||
fi
|
||||
rm -rf "$SCRATCH"
|
||||
exit "$rc"
|
||||
Loading…
Add table
Add a link
Reference in a new issue