One shared vault, per-member namespaces for all churn-prone state:
- _agent/members/<member>/{preferences,current-context,heartbeat,inbox}.md
+ _agent/sessions/<member>/ session logs; shared group-profile.md
(with auto-rostered ## Members) and inbox/captures/group.md.
- Self-onboarding: load detects a bootstrapped vault lacking THIS
member's namespace and seeds it (bootstrap.member_bootstrap()).
- load = 8 reads (marker, group profile, my prefs/context/heartbeat,
shared daily, my inbox, group inbox); heartbeat fallback lists my
sessions dir; scope show/set is per member.
- Member-tagged shared writes: daily Agent Log lines and entity-update
dated blocks are '- YYYY-MM-DD [member]: …'; capture --inbox targets
my inbox; triage lists both inboxes and writes attributed audit lines.
- routing.json: member-anchors / inbox-group / member-segmented session
routes; single-user anchor paths retired (schema 5). vault_lint runs
aging-inbox + scope-drift per member. migrate.py 4->5 for alpha vaults.
- Scaffold: member-* seeds ({{MEMBER}}-stamped), group-profile/group-inbox
seeds, marker schema 5. Docs (SKILL, vault-layout, routing-map,
bootstrap) synced; manifest -> 2.0.0-alpha.3; rebuilt plugin (81 files).
Verified: all suites green (25/25 unit, scaffold, routing-sync 36 routes,
4 mock e2e, run_eval unchanged) + 19-check two-member smoke: bootstrap ->
self-onboard -> interleaved tagged daily log -> cross-member update
attribution -> independent scopes -> mine/group triage -> lint clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chorus-memory eval — current-version metrics harness
A reproducible, credential-free eval of the shipped plugin against the deterministic
mock REST API. run_eval.py measures the four things the plugin claims (results print
as a table and land in results/latest.json):
- Retrieval — hybrid recall (BM25 over entities and sessions/journal, fused with freshness/status priors) vs a keyword-only baseline modeling pre-1.5 recall (per-word substring ranking over entity notes only). Precision@5 / recall@5 / MRR, session/journal answerability, freshness top-1.
- Dedup — duplicate notes created replaying a capture stream with the pre-write
gate ON (default) vs OFF (
CHORUS_DUP_GATE=99≈ pre-1.5 warn-only), plus legitimate captures wrongly blocked (must be 0). - Write safety — silent write failures under fault injection (transient 503, phantom PUT, PATCH to a missing heading, replayed append). Must be 0; must be loud.
- Durability — writes during an outage queue, replay exactly once, never duplicate on re-flush.
The original 0.6-vs-0.7 A/B harness (token cost + silent-failure comparison that motivated the 0.7 client) lives in git history —
git log -- eval/run_eval.py.
Current test suites (run these for any change)
The A/B harness below is the historical 0.6-vs-0.7 comparison. The current-version suites live alongside it and are what CI gates on:
python3 test_features.py # end-to-end features vs the mock (capture/recall/gate/triage/hooks/…)
python3 test_reflect.py # reflection pipeline
python3 test_offline_queue.py # H2 outage queue + read cache
python3 test_patch_semantics.py # real PATCH semantics vs mock_olrapi_hifi.py (incl. fm create-or-replace)
# plus, in the plugin tree: scripts/test_chorus_client.py (offline unit + routing-sync guard)
Run it (historical A/B)
cd eval
python3 run_eval.py # default params
python3 run_eval.py --recovery 2500 # sensitivity-test the recovery assumption
python3 run_eval.py --cpt 3.5 # different chars/token proxy
No network, no API key, no live vault. Pure stdlib Python 3 (the shipped client is now Python — no bash required).
Results table prints to stdout and a machine-readable copy lands in results/latest.json.
How it works
mock_olrapi.py— a deterministic mock of the Obsidian Local REST API surface the plugin uses, reproducing its real behaviors and quirks (404 shape, the/vault//double-slash 400, directory listings withdir/entries,PATCHheading targets that return400 invalid-target / 40080when the heading is absent). Faults are triggered by path markers so one server serves every scenario:flakyin the path → first write returns503, then succeeds (tests retry).phantomin the path →PUTreturns200but does not persist (tests read-back verify).- a
PATCHto a missing heading →400(the silent-write-loss trigger).
run_eval.py— for each scenario, runs both methods against a freshly reset + re-seeded server (so faults are identical for both), then reads ground truth back independently from the mock. The 0.7 side executes the actual shippedchorus.py; the 0.6 side faithfully models the documented recipe (real HTTP, but no status check, no retry, no verify, no dedupe).
Metrics
| metric | meaning |
|---|---|
gen_tokens |
output tokens the model must generate for the op (len(emitted)/cpt proxy) |
silent_failure |
method reported success but ground truth is wrong (lost write or dup) — and nobody noticed |
detected |
the method surfaced the failure (nonzero exit) instead of hiding it |
effective_tokens |
gen + silent_failures*recovery + detected*detect_cost |
silent-error-free ops |
the headline accuracy number |
writes actually persisted |
did the single op land (separate from "was it silent") |
Scenarios
- agent-log-missing-heading —
PATCHappend to a note lacking the target heading (400). - scope-switch — clean
PATCH replace(no fault; pure token comparison). - inbox-capture-replayed — same capture issued twice (retry/replay): dedup vs duplicate.
- session-log-flaky-network — one-time
503: retry vs single-shot. - heartbeat-phantom-write — accepted-but-not-persisted: read-back verify vs none.
- cold-start-load-6-reads — 6 GETs (no fault; pure token comparison).
Representative result (defaults)
generated tokens 723 -> 174 (+76% fewer)
silent failures 4 -> 0 (-4)
duplicate lines 1 -> 0 (-1)
silent-error-free ops 1/5 -> 5/5
effective tokens (assumed) 6723 -> 334
Honest caveats
- Mechanics, not reasoning. This measures the deterministic plumbing differences. It does not measure model judgment (routing choices, prose quality) — that needs a live model.
recoveryanddetect-costare assumptions, not measurements. The headline "silent failures: 4 → 0" is a hard count from ground truth; theeffective_tokensfigure is a model on top of it — tune--recoveryto see the sensitivity.gen_tokensis achars/cptproxy for the I/O layer only, not a tokenizer count, and excludes the one-time+12%SKILL.md context cost noted in the analysis (that's a per-session context cost, not per-op).
Extending to a live-model run (optional)
To measure real model behavior and true token counts:
- Define the same six scenarios as natural-language tasks (e.g. "log a session note for X").
- Run each twice — once with the 0.6 skill files, once with 0.7 — through the Agent SDK
against the mock server (point
CHORUS_BASEat it) so faults stay deterministic. - Record
usage.output_tokensper task from the API and whether the vault ended correct (same ground-truth read used here).
The mock + ground-truth checks in this harness are reusable as-is for that; only the driver changes from "scripted ops" to "model-driven ops".