1
0
forked from jason/echo

Phase 5: group-voice docs, onboarding runbook, CHORUS icon — v2.0.0-rc.1

The final phase of CHORUS-PLAN.md. Code- and doc-complete for group use;
rc pends live deployment at chorusapi.mpm.to.

- Repo README rewritten for CHORUS: group model (schema 5), concurrency
  posture, configuration, baked-build onboarding, layout, testing, 2.0
  version history (ECHO history referenced at the fork tag).
- Operator→member voice sweep across SKILL.md (incl. the trigger
  description, now group-aware), all references, 8 command docs, plugin
  README; member preferences / semantic layer / triage guidance describe
  the group model.
- docs/ONBOARDING.md: member runbook — vault stand-up, per-member config,
  bake, deliver, self-onboarding first load, norms, offboarding.
- docs/USAGE.md rewritten as the member usage guide.
- New icon: chorus-icon.svg + 1024/512/64 PNGs (three voices, one center).
- Removed ECHO-era assets (icons, spec PDF, perf briefs); ECHO planning
  docs retired to docs/history/. All preserved at echo-fork-point.
- eval/README documents test_multimember.py. Manifest -> 2.0.0-rc.1;
  rebuilt chorus-memory.plugin.

Verified: routing-sync, 25/25 unit, scaffold, 5 mock e2e suites,
run_eval metrics green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 15:41:58 -05:00
parent 1a7690426f
commit f2bfd70dd4
36 changed files with 324 additions and 807 deletions
+27
View File
@@ -1,5 +1,32 @@
# Changelog
## 2.0.0-rc.1 — Phase 5 (docs, onboarding, icon) — plan complete
The last phase of the CHORUS conversion plan. The plugin is code- and doc-complete
for group use; rc status pends only live deployment at `chorusapi.mpm.to`.
### Changed
- **Group-voice docs everywhere.** Repo README fully rewritten for CHORUS (group
model, concurrency posture, configuration, baked-build onboarding, schema-5
layout, testing, 2.0 version history — ECHO history now referenced at the fork
tag). SKILL.md description + remaining prose, all references, commands, and the
plugin README swept operator→member; each member's `preferences.md` section,
the semantic layer, and the triage routing guidance now describe the group model.
- `docs/USAGE.md` rewritten as the member usage guide (automatic behaviors,
commands incl. `--author`/`--all-members`, group etiquette, troubleshooting).
### Added
- **`docs/ONBOARDING.md`** — the member-onboarding runbook: vault stand-up,
per-member config, `build.py --bake-key`, delivery, self-onboarding first load,
first-session norms, offboarding = key rotation, troubleshooting table.
- **CHORUS icon** — `chorus-icon.svg` (+ 1024/512/64 PNGs): three voices (teal /
violet / amber) radiating waves around one shared center.
### Removed
- ECHO-era assets: echo icons, the ECHO spec PDF and performance briefs, and the
ECHO planning docs (`TODO-1.6.md`, `ROADMAP-2.0.md``docs/history/`). All
remain at the `echo-fork-point` tag and in `jason/echo`.
## 2.0.0-alpha.4 — Phase 4 (shared-write hardening + group lenses)
### Added
+12 -10
View File
@@ -216,17 +216,19 @@ messages carry the existing entity's author, `recall --author`,
`load --all-members`, concurrency docs (operating-contract.md) rewritten for
N writers. Two-member e2e suite committed as `eval/test_multimember.py`.
### Phase 5 — Docs, onboarding & eval (23 days)
SKILL.md full rewrite (operator → member voice; drop the "single owner"
safety rule, replace with group rules: don't edit another member's
`preferences.md` except via their session; shared notes are fair game),
8 command docs, README. Onboarding runbook: per-member baked builds. Extend
`eval/`: two-member mock scenarios (concurrent capture, interleaved daily-log
appends, index race, cross-member dedup gate, member-scoped load) + rerun the
retrieval/durability metrics. New icon.
### Phase 5 — Docs, onboarding & eval (23 days) — DONE
SKILL.md rewrite (operator → member voice; group safety rules: don't edit
another member's namespace, shared notes are fair game), 8 command docs, repo +
plugin READMEs, `docs/USAGE.md` member guide. Onboarding runbook
(`docs/ONBOARDING.md`): per-member baked builds end to end. Eval: the
two-member scenarios shipped early (Phase 4) as `eval/test_multimember.py`;
all metrics re-run green. New icon (`chorus-icon.svg` + PNGs).
**Total: roughly 915 working days**, shippable at every phase boundary
(1 = rename, 2 = attribution, 3 = groups).
**Total: roughly 915 working days planned — all five phases delivered
2026-07-21 (v2.0.0-rc.1).** Remaining before 2.0.0 final: stand up the live
vault at `chorusapi.mpm.to` (Obsidian + Local REST API + proxy), bootstrap it,
bake and deliver the first member artifacts (see `docs/ONBOARDING.md`) — plus
the deferred auth proxy as its own later project (decision #3).
---
Binary file not shown.
+82 -385
View File
@@ -1,167 +1,92 @@
# chorus-memory — v2.0.0-alpha.4
# chorus-memory — v2.0.0-rc.1
> **CHORUS is a group-based fork of [ECHO](https://git.alwisp.com/jason/echo)** — one shared
> Obsidian vault serving a group of members instead of a single operator. The conversion
> plan and settled design decisions live in [CHORUS-PLAN.md](CHORUS-PLAN.md). The pre-fork
> ECHO v1.5.1 state is preserved at the `echo-fork-point` tag. **Phases 14 (rename, identity, per-member
> namespacing, shared-write hardening) are done**; the body of this README below the fold still describes the
> inherited single-operator behavior and is rewritten in Phase 5.
> Deployment endpoint: `https://chorusapi.mpm.to`. CHORUS is a **clean break** from
> ECHO — fresh vaults on fresh machines; it carries no ECHO back-compat and never
> reads ECHO configs, state dirs, or vault markers.
**CHORUS** is shared **group memory** for Claude / CoWork sessions: one Obsidian vault serving a whole team, driven over the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api). Every member's sessions read the same knowledge graph and write to it with full attribution — where ECHO reflected one voice back, CHORUS is many voices in one performance.
Persistent memory for Claude / CoWork sessions via the **CHORUS** Obsidian vault, driven over the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api). The skill makes direct REST calls through a bundled validated client (`scripts/chorus.py`). The whole toolchain is **pure Python** (stdlib only), so it runs identically on Windows, macOS, and Linux — no bash, no platform-specific `date`.
Architected by **Jason Stedwell**. CHORUS is a group-based fork of [ECHO](https://git.alwisp.com/jason/echo) (the single-operator original, preserved at the `echo-fork-point` tag) and a **clean break** from it: fresh vaults on fresh machines, no ECHO back-compat. The conversion plan and settled design decisions live in [CHORUS-PLAN.md](CHORUS-PLAN.md); deployment endpoint: `https://chorusapi.mpm.to`.
Architected by **Jason Stedwell**. The plugin is **user-agnostic**: it ships no owner, endpoint, or API key — each machine supplies them through a local config file (see [Configuration](#configuration)), so the same plugin works for any group and any vault.
The skill makes direct REST calls through a bundled validated client (`scripts/chorus.py`). The whole toolchain is **pure Python** (stdlib only), so it runs identically on Windows, macOS, and Linux — no bash, no platform-specific `date`.
This repository (`jason/chorus`) holds the plugin **source** (tracked tree at `chorus-memory.plugin.src/`), the built `chorus-memory.plugin` package artifact (rebuilt on each version bump), and a credential-free A/B `eval/` harness.
**1.3 in one line:** the plugin is now **user-agnostic** — the vault owner, endpoint, and API key live in a machine-local config (`~/.claude/echo-memory/config.json`), never in the source; an unconfigured machine **prompts for the key file on load** and installs it with one `config import`. Underneath: one-call `capture` routes and crosslinks memory, `recall` fuses BM25 over note bodies with graph expansion, alias-aware resolution avoids duplicate notes, and a connection-pooled client reads the whole vault concurrently. See the [version history](#version-history) for how it got here.
This repository (`jason/chorus`) holds the plugin **source** (tracked tree at `chorus-memory.plugin.src/`), the built `chorus-memory.plugin` artifact (rebuilt on each version bump; versioned builds ship as Gitea releases), and a credential-free `eval/` test harness.
---
## Core design principle — the plugin is the single source of truth
## The group model (schema 5)
Everything that defines how ECHO behaves — bootstrap/repair logic, the operating contract, the taxonomy, frontmatter conventions, the routing map, and the canonical note templates — ships **inside the plugin** under `skills/echo-memory/`.
One vault, many members. Each machine is configured with the **group** and its own **member id** (a kebab-case slug like `jason`), and every agent write is stamped `author: <member>`.
The vault itself holds **data only**. There are no `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md` control docs in it. The single in-vault control artifact is a one-line marker, `_agent/echo-vault.md`, that records the schema version.
- **Shared by the group:** projects, areas, resources, decisions, the journal (one team timeline — every `## Agent Log` line is member-tagged `- YYYY-MM-DD [member]: …`), semantic/episodic/working memory including `group-profile.md` (what the team is + a `## Members` roster), a shared group inbox for unowned items, the entity/recall indexes, and the duplicate gate (two members can't create parallel notes for the same client — a blocked capture names whose entity it collided with).
- **Per member, no contention by construction:** `_agent/members/<member>/{preferences,current-context,heartbeat,inbox}.md` and `_agent/sessions/<member>/` — each member's profile, active scope, last-session pointer, personal captures, and session logs are theirs alone.
- **Self-onboarding:** a new member installs their plugin and runs a session — `load` detects the missing namespace, seeds it, and adds them to the roster.
- **Group lenses:** `recall --author <member>` filters the graph to one member's notes; `load --all-members` shows every member's scope and last session.
- **Visibility is by design:** one vault, one key — everything in it (including member preferences) is readable by the whole group. Truly private material belongs in a personal vault, not CHORUS.
Three consequences follow:
## Concurrency posture
- **Self-bootstrapping** — point the REST API at an empty Obsidian vault and the plugin stands up the full folder tree, templates, anchors, and marker from its bundled `scaffold/`.
- **Easy to update** — change behavior by updating the plugin, not by editing files scattered through the vault.
- **Portable** — the same plugin brings any empty vault online identically; nothing essential depends on files pre-existing in the vault.
Namespacing removes most contention outright. What stays shared-write is built for interleaving: member-tagged whole-line-idempotent appends for logs/inboxes, and the machine indexes are guarded by an advisory lock with **bounded retry/backoff** (`CHORUS_LOCK_RETRIES`, default 4) plus a fresh re-read inside the critical section, so concurrent captures merge instead of clobber. Lock owner ids carry the member (`<member>-<client>-<pid>`), making contention attributable. Full model: `skills/chorus-memory/references/operating-contract.md`.
---
## Configuration
The plugin ships **no** owner, endpoint, or key — it is user-agnostic. Each machine supplies them through a machine-local JSON config (resolution lives in `scripts/echo_config.py`):
The plugin ships **no** group, member, endpoint, or key — each machine supplies them via a machine-local JSON config (resolution in `scripts/chorus_config.py`):
- **File:** `~/.claude/echo-memory/config.json` — honors `$CLAUDE_CONFIG_DIR`; the file path itself can be overridden with `$ECHO_CONFIG`. Shape: `{ "owner": …, "endpoint": "https://…", "key": "…" }`.
- **Per-field env override:** `ECHO_OWNER`, `ECHO_BASE` (endpoint), and `ECHO_KEY` take precedence over the file.
- **Set up a machine:** `echo.py config import <file>` adopts a config you've been handed; `echo.py config set --owner … --endpoint … --key …` writes one from values; `echo.py config init` scaffolds a blank template to edit; `echo.py config` prints the resolved config (key redacted) and where each field came from.
- **First run:** an unconfigured machine — or one left on the placeholder template — reports `NOT CONFIGURED` (`echo.py load` prints a banner and exits `78`; other verbs exit `2`; `/echo-doctor` flags it red), and the skill asks the operator for the key file before doing any memory work. The config is **never committed** and **never written into a vault note**; the key stays out of the plugin tree entirely.
- **File:** `~/.claude/chorus-memory/config.json` — honors `$CLAUDE_CONFIG_DIR`; path overridable with `$CHORUS_CONFIG`. Shape: `{ "group": …, "member": "<kebab-slug>", "endpoint": "https://…", "key": "…" }`. **`member` is required** — it attributes every write.
- **Per-field env override:** `CHORUS_GROUP`, `CHORUS_MEMBER`, `CHORUS_BASE` (endpoint), `CHORUS_KEY`.
- **Set up a machine:** `chorus.py config import <file>` adopts a config you've been handed; `chorus.py config set --group … --member … --endpoint … --key …` writes one from values; `config init` scaffolds a template; `chorus.py config` prints the resolved config (key redacted) with per-field sources.
- **First run:** an unconfigured machine reports `NOT CONFIGURED` (`load` exits `78`; `/chorus-doctor` flags it red) and the skill asks the member for their config before doing memory work. The config is never committed and never written into a vault note.
### Per-user baked-key builds (v1.4.0) — for CoWork
### Per-member baked builds — the onboarding mechanism
The CoWork sandbox does **not** bridge your real `~/.claude` (the mounted `.claude` is the sandbox's own synthetic view), so a config file on your machine can't be read there. The only thing reliably mounted into every session is the **plugin itself**. So for a small set of known users, bake each user's credentials directly into a per-user artifact:
The CoWork sandbox doesn't bridge your real `~/.claude`, but the plugin itself is mounted every session — so each member's credentials travel *inside* their artifact:
```bash
python build.py --bake-key --from alice-config.json --label alice
# -> dist/echo-memory-1.4.1-alice.plugin (carries Alice's vault key)
# -> dist/chorus-memory-2.0.0-rc.1-alice.plugin (carries alice's member id + the vault key)
```
Resolution gains a **baked** tier (`DEFAULT_OWNER/BASE/KEY` in `echo_config.py`) that is **empty in source** — the committed tree still ships zero credentials. As of **v1.4.1** a *complete* baked set (endpoint **and** key both present) is **authoritative**: it wins over `ECHO_*` env vars and any `~/.claude` config file and cannot be shadowed, so a delivered per-user artifact "just works" even on a machine carrying a stale or placeholder config. When nothing is baked (the generic published plugin), host behaviour is unchanged — env → config-file → first-run prompt. `build.py --bake-key` fills those constants from a `config.json` (or `--owner/--endpoint/--key`, or `ECHO_*` env). The end user just installs the artifact — **no config file, no per-session paste**, works on desktop and in every CoWork session.
> **A baked artifact contains a vault bearer token.** Baked builds land in `dist/` (gitignored) and never touch the shared `echo-memory.plugin` pointer. Deliver each one **directly** to its single user; **never commit, push, or publish it.** Use `--strip-key` (or just a clean source build) for a token-free artifact. Rotation = rebuild + reinstall.
Vault paths are addressed **at the root** (e.g. `GET /vault/_agent/context/current-context.md`). Prefer `scripts/echo.py` over raw `curl`: it injects auth, checks HTTP status (a failed write exits non-zero instead of looking like success), retries transient 5xx, verifies PUTs, and does idempotent appends.
A *complete* baked set (endpoint + key + member) is **authoritative** — it can't be shadowed by stale env vars or leftover configs. The committed source ships zero credentials; baked artifacts land in `dist/` (gitignored) and are delivered directly to their one member, **never committed or published**. The full member-onboarding runbook is [docs/ONBOARDING.md](docs/ONBOARDING.md).
---
## Requirements
- **Python 3** in the session environment (stdlib only; invoke as `python3`, or `python` / `py -3` on Windows).
- **Python 3** in the session environment (stdlib only; `python3`, or `python` / `py -3` on Windows).
- Obsidian running on the backend with the Local REST API plugin enabled.
- HTTPS reachability to `https://echoapi.alwisp.com` from the Claude / CoWork session environment.
- HTTPS reachability to `https://chorusapi.mpm.to` from the Claude / CoWork session environment.
---
## Repository layout
```
echo-v.05/
├── README.md · CHANGELOG.md ← this file + per-release change log
├── TODO-1.6.md · ROADMAP-2.0.md ← next-minor working list · packaging/structure major plan
├── docs/history/historical inputs (e.g. the 1.5.0 frontmatter field report)
├── build.py deterministic .plugin builder (--bake-key/--strip-key/--label/--outdir)
├── echo-memory.plugin ← built, installable plugin (zip artifact, rebuilt on version bump)
├── echo-memory-<version>.pluginversioned build artifacts (history; moves to Gitea releases in 2.0)
├── dist/per-user baked artifacts (secret-bearing) — GITIGNORED, never committed
├── eval/ ← credential-free eval + test harness; not bundled
│ ├── mock_olrapi.py ← deterministic mock of the REST API + fault injection
│ ├── mock_olrapi_hifi.py ← higher-fidelity mock (real PATCH heading/frontmatter semantics)
│ ├── run_eval.py ← current-version metrics (retrieval / dedup / write-safety / durability)
── test_*.py ← end-to-end suites (features, reflect, offline queue, PATCH semantics)
│ ├── results/latest.json machine-readable output of the last eval run
── README.md
└── echo-memory.plugin.src/ ← tracked source tree (the plugin — single canonical tree)
├── .claude-plugin/plugin.json ← manifest (name, version, description)
├── README.mdplugin-level README
── hooks/hooks.json ← session hooks: SessionStart auto-load · Stop reflection nudge
├── commands/ slash commands (legacy format; skills-format migration = TODO-1.6):
│ echo-load|save|recall|triage|health|sweep|reflect|doctor
└── skills/echo-memory/
├── SKILL.md ← operating procedure (authoritative)
── references/
│ ├── operating-contract.md ← durable principles + safety rules + concurrency
│ ├── bootstrap.md ← bootstrap / repair / migrate manifest
│ ├── vault-layout.md ← canonical layout + frontmatter conventions
│ ├── routing-map.md ← complete endpoint→logic routing map (human authority)
│ ├── api-reference.md ← REST endpoint patterns + routing map
│ └── session-log-template.md
├── scripts/ ← executable logic — pure Python (run: python3, or python / py -3)
│ ├── echo.py ← validated client + CLI (capture/resolve/recall/link/triage/load/scope/lock/config/reflect/flush/doctor)
│ ├── echo_config.py ← owner/endpoint/key resolution (baked > env > machine-local config file)
│ ├── echo_index.py ← entity index + kind maps (KIND_STATUS/KIND_REQUIRED_FM) + duplicate-gate scoring
│ ├── echo_links.py ← cross-link primitives (Related parsing, bidirectional linking)
│ ├── echo_ops.py ← high-level ops (capture with dup-gate, resolve, link, agent-log)
│ ├── echo_recall.py ← hybrid BM25 + graph recall over entities + sessions/journal (freshness/status priors)
│ ├── echo_reflect.py ← session-reflection pipeline (validate/classify/preview/apply)
│ ├── echo_triage.py ← one-tap inbox triage (reflect pipeline + processing-log audit)
│ ├── echo_queue.py ← offline write-ahead outbox + last-known-good read cache
│ ├── echo_concurrency.py ← advisory-lock context manager + atomic index updates
│ ├── echo_quality.py ← auto-link confidence + slug-collision guards
│ ├── echo_output.py ← --json result envelopes
│ ├── echo_doctor.py ← one-call readiness check (/echo-doctor)
│ ├── echo_hook_session_start.py · echo_hook_stop.py ← the session hooks (fail-safe)
│ ├── routing.json ← canonical machine-readable route manifest (linter enforces it)
│ ├── vault_lint.py ← read-only invariant + graph-health + frontmatter-completeness checker
│ ├── check_routing.py ← verifies routing docs stay in sync with routing.json (offline)
│ ├── test_echo_client.py ← offline unit tests + the routing-sync guard
│ ├── bootstrap.py ← deterministic, idempotent vault setup/repair
│ ├── migrate.py ← deterministic schema migration (dry-run by default)
│ └── sweep.py ← bring a vault up to spec (indexes + frontmatter backfill + link symmetrize)
└── scaffold/ ← verbatim files the bootstrap writes into the vault
├── echo-vault.md ← bootstrap marker
├── README.vault.md ← thin human signpost
├── anchors/ ← operator-preferences, current-context, inbox seeds
└── templates/ ← 8 canonical note templates
chorus/
├── README.md · CHANGELOG.md · CHORUS-PLAN.md ← this file · release log · conversion plan + decisions
├── docs/ ← ONBOARDING.md (member runbook) · USAGE.md · history/
├── build.py deterministic .plugin builder (--bake-key/--strip-key/--label/--outdir)
├── chorus-memory.pluginbuilt, installable plugin (current pointer; versioned builds → Gitea releases)
├── chorus-memory.config.template.json ← blank config template (group/member/endpoint/key)
├── dist/ per-member baked artifacts (secret-bearing) — GITIGNORED, never committed
├── eval/ credential-free test harness; not bundled
│ ├── mock_olrapi.py · mock_olrapi_hifi.py ← deterministic REST-API mocks (+ fault injection / real PATCH semantics)
│ ├── run_eval.py retrieval / dedup / write-safety / durability metrics
│ ├── test_features.py · test_reflect.py · test_offline_queue.py · test_patch_semantics.py
│ ├── test_multimember.py ← 26-check two-member end-to-end suite (the group behavior)
── results/latest.json
└── chorus-memory.plugin.src/tracked source tree (the plugin — single canonical tree)
── .claude-plugin/plugin.json ← manifest (name, version, description)
├── README.md ← plugin-level README (configuration, tooling)
├── hooks/hooks.json ← SessionStart auto-load · Stop reflection nudge
├── commands/ /chorus-{load,save,recall,triage,health,sweep,reflect,doctor}
── skills/chorus-memory/
├── SKILL.mdoperating procedure (authoritative)
├── references/ ← operating-contract · bootstrap · vault-layout · routing-map · api-reference
├── scripts/ ← pure-Python toolchain (chorus.py client, bootstrap/migrate/sweep,
vault_lint, routing.json manifest, capture/recall/triage/reflect ops)
── scaffold/ ← verbatim seeds bootstrap writes into the vault
(group-profile, group-inbox, member-* anchors, templates, marker)
```
**Division of responsibility:** `SKILL.md` owns day-to-day *procedure* (loading order, search-first, triage, scope switching, PATCH rules) and points at the bundled tooling. `references/operating-contract.md` owns the durable, client-independent *principles, safety rules, and concurrency model*. `scripts/routing.json` is the machine-readable source of truth for routing; `references/routing-map.md` is its human-readable authority. The other references are the canonical layout, API, and bootstrap specs.
---
## Bundled tooling
Executable logic ships under `skills/echo-memory/scripts/`; the agent prefers it over hand-built curl. Everything is **pure Python** (stdlib only) — invoke with `python3` (Windows: `python` / `py -3`).
| Tool | Purpose |
|------|---------|
| `echo.py` | The validated client + high-level CLI. The network layer is **keep-alive + connection-pooled** (one persistent connection per thread, reused across requests) with a concurrent `read_many()` bulk-GET (`ECHO_WORKERS`, default 8) — so full-vault passes that used to time out finish in under a second. Low-level verbs `get/ls/map/search/put/post/append/patch/fm/bump/delete/lock/unlock/scope/load` inject auth, **check HTTP status** (non-zero exit on ≥400), retry transient 5xx (and reconnect a stale pooled connection), read-back-verify PUT, idempotent whole-line `append`, correct `::` heading targets. High-level ops **`capture/resolve/recall/link`** do the routing/linking for you (see below). |
| `capture / resolve / recall / link / triage` | The input-reducing layer. **`capture "<title>" --kind <k> [--tags a,b]`** routes via the entity index, stamps **complete** frontmatter (kind-default `status`, kind-seeded `tags`), indexes, auto-links mentioned entities, and writes the Agent-Log line in one call; on an existing entity it appends the **whole body** as a dated block; a strong fuzzy candidate **stops creation** (exit 76 — `--merge-into <slug>` / `--force` resolve it). **`resolve "<mention>"`** → canonical path (alias-aware). **`recall "<query>" [--json]`** → ranked matches + one-hop neighbourhood over entities **and sessions/journal**, fused with freshness + status priors. **`link A B`** → reciprocal `## Related` links. **`triage`** → one-tap inbox routing with an automatic processing-log audit trail. |
| `echo_index.py` | The **entity index** (`_agent/index/entities.json`): slug→{path, kind, title, aliases, last_seen}. Makes routing/resolve an O(1), alias-aware lookup and supplies the name→path map for linking and recall. `resolve()` matches on slug/title/alias; a `fuzzy_candidates()` "did-you-mean" fallback (1.2) surfaces near-matches for shortened names without auto-merging. Aliases are auto-derived from titles, learned from mentions on update, stored in note frontmatter, and folded back in by `sweep.py`. Rebuilt automatically by `capture` and `sweep.py`. |
| `echo_links.py` / `echo_ops.py` | Cross-link primitives (parse/add `## Related`, bidirectional linking) and the high-level ops layer (capture/recall/resolve/link/agent-log). |
| `routing.json` | The **canonical machine-readable** route manifest — one regex pattern per valid destination plus retired paths. The single source of truth for "what may be written where"; `vault_lint.py` enforces it against the vault, `check_routing.py` against the docs. |
| `vault_lint.py` | Read-only invariant + **graph-health** checker (Vault Health). Tolerant frontmatter parsing, clock injected via `ECHO_TODAY`, exits `3` if the vault isn't bootstrapped. Checks scope-drift, **broken wikilinks, orphan notes, and entity-index drift**. |
| `sweep.py` | Brings an upgraded vault up to spec: creates `_agent/index/`, rebuilds the entity index from existing notes, symmetrizes `## Related` cross-links (adds only the missing reciprocal direction), and stamps `schema_version`. Dry-run by default; `--apply` to write. |
| `check_routing.py` | Offline routing-doc consistency check: every vault path named in `SKILL.md` / `routing-map.md` / `api-reference.md` must match a route (or retired pattern) in `routing.json`, and every route must appear in the canonical map. No network. The `test_echo_client.py` suite runs it as a guard. |
| `bootstrap.py` | Deterministic, idempotent, probe-before-write vault setup/repair (`--dry-run` to preview). Resolves the scaffold relative to itself, so it works from any CWD. |
| `migrate.py` | Deterministic schema migration. Dry-run by default; destructive steps gated behind `--apply` and printed first. |
### Slash commands
`/echo-load` (cold-start read), `/echo-save <text>` (route + persist via `capture`), `/echo-recall <query>` (recall a topic's neighbourhood), `/echo-triage` (one-tap inbox routing via `echo.py triage`), `/echo-health` (run the linter), `/echo-sweep` (bring the vault up to spec), `/echo-reflect` (extract→preview→apply durable items from the session), `/echo-doctor` (readiness check: Python, vault reachability, auth, bootstrap/schema, key source) — explicit, reproducible entry points to the procedures below.
### Session hooks (v1.5)
The plugin ships `hooks/hooks.json`: a **SessionStart** hook runs `echo.py load` and injects the orientation reads as context (cold-start loading no longer depends on the model remembering), and a **Stop** hook nudges **once per substantive session** to run `/echo-reflect` + the session log when nothing was reflected yet (reflection still previews and asks before writing). Both are fail-safe — unconfigured/unreachable vaults degrade to a note, never an error — and use the CoWork path fallback.
### Concurrency (shared vault)
ECHO is read/written by multiple clients (Claude Code **and** CoWork). Single-line files (`heartbeat/last-session.md`, `current-context.md::Scope`) and append targets (`inbox.md`, `## Agent Log`) assume one writer at a time. Before an overlapping burst of writes, take the cooperative advisory lock (`echo.py lock <id>``_agent/locks/vault.lock`, TTL-reclaimable) and release it at session end. Idempotent append and status-checked writes are the second line of defense.
**Division of responsibility:** `SKILL.md` owns day-to-day procedure; `references/operating-contract.md` owns durable principles, safety rules, and the N-writer concurrency model; `scripts/routing.json` is the machine-readable source of truth for what may be written where (`references/routing-map.md` is its human authority; `check_routing.py` keeps them in sync mechanically).
---
@@ -169,281 +94,53 @@ ECHO is read/written by multiple clients (Claude Code **and** CoWork). Single-li
```
/vault/
├── README.mdthin human signpost (NOT read for routing)
├── inbox/
│ ├── captures/ ← quick captures (inbox.md), date-prefixed lines
│ ├── imports/ ← raw imported material
│ └── processing-log/
├── journal/ one append-only time-series stream; rollups are coarser journal entries (no separate reviews/ tree)
│ ├── daily/ ← YYYY-MM-DD.md (has an "## Agent Log" section)
│ ├── weekly/ ← YYYY-Www.md (ISO week, opt-in rollup)
│ ├── monthly/ ← YYYY-MM.md (monthly rollup)
│ ├── quarterly/ ← YYYY-Qn.md (manual / on request)
│ ├── annual/ ← YYYY.md (manual / on request)
│ └── templates/
├── projects/ ← lifecycle: incubating → active → on-hold / archived
│ ├── active/ incubating/ on-hold/ archived/
│ └── project-template.md
├── areas/ ← business / personal / learning / systems
├── resources/
│ ├── companies/ ← <slug>.md — organizations (clients, vendors, partners, employers)
│ ├── concepts/ references/ meetings/
│ └── people/ ← <name>.md
├── decisions/
│ ├── by-date/ ← YYYY-MM-DD-<slug>.md (ADR-style) — the canonical home
│ └── decision-template.md (by-project/ is retired, not created)
│ (reviews/ is retired — journal rollups live under journal/; vault-health under _agent/health/)
├── inbox/ captures/group.md (unowned, member-tagged) · imports/ · processing-log/
├── journal/ ← daily/ (shared team timeline, member-tagged Agent Log) · weekly/ monthly/ quarterly/ annual/
├── projects/ ← active/ incubating/ on-hold/ archived/ (folder == status:)
├── areas/ ← business/ personal/ learning/ systems/
├── resources/ ← people/ companies/ concepts/ references/ meetings/
├── decisions/by-date/ (ADR-style)
└── _agent/
├── echo-vault.md ← bootstrap marker: schema_version + date (plugin-owned probe)
├── context/ ← current-context.md and task bundles
├── memory/
│ ├── working/ ← transient, time-boxed
│ ├── episodic/ ← what happened, when
│ └── semantic/ ← durable facts / patterns (operator-preferences.md)
├── sessions/ ← YYYY-MM-DD-HHMM-<slug>.md
├── health/ ← YYYY-MM-vault-health.md (monthly self-maintenance audit)
├── templates/ ← canonical note templates (seeded from the plugin's scaffold/)
├── skills/ ← active / archived capability catalog
├── heartbeat/ ← last-session.md pointer (read at load, written at session end)
├── index/ ← entities.json — machine-maintained slug→{path,kind,aliases} registry
└── locks/ ← vault.lock — cooperative advisory multi-writer lock (echo.py lock/unlock)
├── chorus-vault.md ← bootstrap marker (schema_version: 5, plugin-owned)
├── members/<member>/ ← preferences.md · current-context.md · heartbeat.md · inbox.md (per member)
├── sessions/<member>/ ← YYYY-MM-DD-HHMM-<slug>.md (member-namespaced session logs)
├── memory/ ← working/ · episodic/ · semantic/ (incl. group-profile.md)
├── context/ health/ templates/ skills/ index/ locks/
```
### Memory model
| Layer | Path | What it holds |
|-------|------|---------------|
| Working | `_agent/memory/working/` | Transient, time-boxed state |
| Episodic | `_agent/memory/episodic/` | What happened, when |
| Semantic | `_agent/memory/semantic/` | Durable facts, patterns, preferences (`operator-preferences.md`) |
| Context | `_agent/context/` | Task-focused reading lists + active scope |
The complete map — every write destination with its trigger and why it's distinct — is `skills/chorus-memory/references/routing-map.md`. Frontmatter conventions (incl. the required `author:` on agent-written notes) are in `references/vault-layout.md`.
---
## Operating procedures (logic)
## Bundled tooling & commands
### Cold-start loading
`scripts/chorus.py` is the validated client: keep-alive connection-pooled, HTTP-status-checked (failed writes exit non-zero), retrying, read-back-verified PUTs, whole-line idempotent appends. High-level ops collapse the write discipline into one call: **`capture`** (route + complete frontmatter incl. `author:` + index + auto-link + member-tagged log line, with a pre-write duplicate gate), **`resolve`**, **`recall [--author <member>]`** (hybrid BM25 + graph over the shared corpus), **`link`**, **`triage`** (both inboxes, attributed audit trail), **`load [--all-members]`** (8-read cold start + self-onboarding), plus `scope`, `lock/unlock`, `config`, `doctor`, and the vault maintenance scripts (`bootstrap.py`, `migrate.py`, `sweep.py`, `vault_lint.py`).
Load memory at the start of any substantive conversation (anything beyond a single quick factual question). One command — `python3 echo.py load` — performs all six orientation reads and prints each labeled section (absent today's-note / inbox show as such, not errors):
1. `_agent/echo-vault.md` — the bootstrap marker. `404` → vault not set up (run bootstrap); `200` → check `schema_version` and migrate if older than the plugin's.
2. `_agent/memory/semantic/operator-preferences.md` — Jason's profile.
3. `_agent/context/current-context.md` — active scope + Scope History.
4. `_agent/heartbeat/last-session.md` then `_agent/sessions/` — read the heartbeat pointer first (O(1) jump to the latest log); fall back to the listing's ~5 most recent by reverse lexical sort.
5. `journal/daily/YYYY-MM-DD.md` — today's note (`404` is fine).
6. `inbox/captures/inbox.md` — inbox-depth probe for the load-time reconcile.
After the batch, **reconcile**: if the inbox holds captures older than ~7 days, surface the count and offer to triage (so triage self-fires rather than waiting to be invoked); and if the active scope diverges from what Jason just asked for, run Scope Switching.
If a specific project is in play, follow up with a `search/simple/` across **all** lifecycle subfolders, querying **both the slug and any human title** used in conversation, then read the match at `projects/<lifecycle>/<slug>.md`. Loading is not narrated to the operator.
### Inbox triage
`inbox/captures/inbox.md` is the catch-all. At session start, GET it; if it holds lines older than ~7 days that were never routed, surface a count once and offer to triage. Accepted items route to their proper home (preference → operator-preferences; project idea → `projects/incubating/`; durable fact → semantic; person fact → `resources/people/`), with the move recorded one-line in `inbox/processing-log/YYYY-MM-DD.md` as an audit trail. Originals aren't deleted unless explicitly asked.
### Project lifecycle
Projects move through four folders; the folder name and the `status:` frontmatter field **MUST agree** — they are two views of one state. A file in `projects/active/` with `status: on-hold` is broken state.
| Folder | `status:` | Meaning |
|--------|-----------|---------|
| `incubating/` | `incubating` | Idea captured; not actively worked |
| `active/` | `active` | Current work (default for anything in motion) |
| `on-hold/` | `on-hold` | Paused but still tracked; resumable |
| `archived/` | `archived` | Done, abandoned, or rolled up — not deleted |
Promotion = move the file **and** update `status:` in the same change.
### Scope switching
`_agent/context/current-context.md` tracks one active scope. It is the **most churn-prone state** — several sessions a day across different topics — so without care a new session silently inherits a stale scope (the same failure class as inbox auto-fire). 0.7.1 hardens this three ways:
- **Freshness signal** — a `scope_updated:` frontmatter timestamp records when scope last changed.
- **One-command switch** — `echo.py scope set "<new scope>"` does it atomically: archive the prior scope to `## Scope History` (dated, truncated), replace `## Scope`, and stamp `scope_updated`. (Manual fallback: prepend history → replace `## Scope` → PATCH `scope_updated`; the field must already exist or PATCH returns `400 invalid-target`.) Scope History is trimmed to the last ~10 entries.
- **Drift detection** — at load the agent runs `echo.py scope show` (prints the scope, its `scope_updated`, and how many sessions have been logged since) and *states + confirms* scope before working, switching if it diverges. As a backstop, `vault_lint.py` flags when ≥ `SCOPE_STALE_SESSIONS` (default 3) session logs postdate `scope_updated` — surfaced in `/echo-health`, so drift is mechanically **evaluable** rather than invisible.
### Daily note — Agent Log
After substantive activity, append a one-line entry to today's daily note's `## Agent Log`. The procedure is resilient: GET the note → if `404`, PUT it from the template → if `200` but the heading is missing, POST the heading. **Heading detection greps the raw markdown for an anchored `^## Agent Log`** — not the document-map JSON, whose headings are `::`-delimited paths (a bug fixed in 0.4.1 that previously appended duplicate headings).
### Session logging
At the end of substantive conversations (those producing decisions, artifacts, or commitments), write a session log to `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md`. **The four-digit HHMM component is canonical, not optional** — it makes filenames lex-sort in true chronological order, which cold-start loading depends on. New writes are validated against `^\d{4}-\d{2}-\d{2}-\d{4}-[a-z0-9-]+\.md$`.
### Journal rollups
The journal is one append-only time-series stream; rollups are coarser-grained entries on the same timeline, so they live under `journal/` — there is no separate `reviews/` tree.
- **Weekly** — opt-in; offered on the first substantive session of a new ISO week, written only if accepted, to `journal/weekly/YYYY-Www.md`.
- **Monthly** — offered alongside the Vault Health pass on the first session of a calendar month, to `journal/monthly/YYYY-MM.md`.
- **Quarterly / annual** — manual / on request only, to `journal/quarterly/YYYY-Qn.md` and `journal/annual/YYYY.md`.
### Vault Health (monthly)
Agent self-maintenance (not a journal entry), written to `_agent/health/YYYY-MM-vault-health.md`. Run `scripts/vault_lint.py` (or `/echo-health`) with `ECHO_TODAY` = the conversation's date so stale/aging math uses one clock. It mechanically asserts: folder↔status mismatch, duplicate slugs across lifecycle folders, wikilinks in frontmatter (swept across all folders), duplicate `## Agent Log` headings, stale active projects (`updated:` > 30 days), aging inbox items (> 14 days), **paths matching no route in `routing.json` or sitting at a retired path**, **frontmatter integrity** (missing required fields, `updated` < `created`, future dates, wikilinks leaking into `source_notes`), **incomplete entity frontmatter** (missing/empty `status`/`tags` per the kind-scoped `KIND_REQUIRED_FM` map — `sweep.py --apply` backfills), and **scope drift** (≥ `SCOPE_STALE_SESSIONS` session logs dated after `scope_updated`). Exit codes: `0` clean · `1` violations · `2` unreachable · `3` not bootstrapped. Findings are reported, not auto-fixed.
Slash commands: `/chorus-load`, `/chorus-save`, `/chorus-recall`, `/chorus-triage`, `/chorus-health`, `/chorus-sweep`, `/chorus-reflect`, `/chorus-doctor`. Session hooks auto-load memory at SessionStart and nudge reflection once per substantive session at Stop.
---
## Write-safety rules
## Testing
- **Search first (mandatory for new notes).** Before creating any slug-addressed note, `POST /search/simple/?query=<slug>` across the **whole** vault (and search the human title too) — listing one folder misses duplicates elsewhere. On a match: promote/merge from a non-active subfolder, update-in-place in the same folder, or ask which is canonical if elsewhere.
- **Read before append (idempotency).** `POST` is not idempotent — a retry duplicates lines. Before any POST that adds an entry (inbox, Agent Log, an Observations/Log heading), GET the target and skip if the exact line is already present. (Does not apply to PUT or PATCH-replace.)
- **Bump `updated:` on substance.** PATCH the frontmatter `updated:` to today after a meaningful content change (status update, decision, scope switch). Skip it for routine log appends — bump on substance, not heartbeat.
- **Preserve `created:`.** It is the earliest known date the entity was tracked anywhere — never reset it to "today" when merging.
- **No `[[wikilinks]]` in frontmatter** — YAML parses them as nested lists and they break. Cross-references go in a `## Related` body section. `source_notes` holds plain relative-path strings (backward links to inputs only).
- **Check the HTTP status (or use `echo.py`).** A `PATCH` to a missing heading returns `400 invalid-target` (40080) and the write is *silently lost*; a transient `503` or an accepted-but-unpersisted `PUT` can do the same. `echo.py` enforces this (exit ≠ 0, retry, read-back verify); raw `curl` must branch on `-w "%{http_code}"`. Treat ≥ 400 as a failed op — surface it, don't continue.
Everything runs offline against deterministic mocks — no credentials, no live vault:
---
## REST API operations
Server `https://echoapi.alwisp.com`, bearer auth, root-addressed paths.
| Operation | Method | Notes |
|-----------|--------|-------|
| Read file | `GET /vault/<path>` | Raw markdown; `404` = absent |
| Read heading | `GET /vault/<path>/heading/<H1>::<H2>` | `::`-delimited, URL-encode spaces as `%20` |
| List dir | `GET /vault/<dir>/` | Trailing slash; returns `{files, folders}` |
| Document map | `GET` + `Accept: application/vnd.olrapi.document-map+json` | Discover exact heading / block / frontmatter targets |
| Append | `POST /vault/<path>` | Appends to end-of-file; creates if absent |
| Create / overwrite | `PUT /vault/<path>` | Auto-creates intermediate directories |
| Patch section | `PATCH /vault/<path>` | `Operation: append\|prepend\|replace`, `Target-Type: heading\|frontmatter\|block` |
| Search | `POST /search/simple/?query=<terms>` | Returns `[{filename, score, matches}]` |
| Delete | `DELETE /vault/<path>` | Only on explicit operator request |
**PATCH heading targets must be the full `::`-delimited path from the top-level heading** (e.g. `Operator Preferences::Fact / Pattern`) — a bare subheading name returns `400 invalid-target` (errorCode 40080). GET the document map first when unsure of the exact path, and copy the heading string verbatim.
---
## Memory routing map
| Situation | Vault path | Method |
|-----------|-----------|--------|
| Quick capture / unsorted | `inbox/captures/inbox.md` (date-prefixed line) | POST |
| Raw imported material | `inbox/imports/` | PUT |
| Operator preference / durable fact | `_agent/memory/semantic/operator-preferences.md` | PATCH |
| Other durable facts, patterns | `_agent/memory/semantic/<slug>.md` | PUT |
| Event record (what happened) | `_agent/memory/episodic/<slug>.md` | PUT |
| Short-lived, time-boxed state | `_agent/memory/working/<slug>.md` | PUT |
| Task-scoped context / focus | `_agent/context/current-context.md` | PATCH / PUT |
| Working-session log | `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md` | PUT |
| Long-running project state | `projects/<lifecycle>/<slug>.md` (folder and `status:` MUST agree) | PUT + PATCH |
| Standing area of responsibility (no end state) | `areas/<domain>/<slug>.md` (`business`/`personal`/`learning`/`systems`) | PUT |
| Non-obvious decision (ADR) | `decisions/by-date/YYYY-MM-DD-<slug>.md` (mirror into a project's `## Key Decisions`; else skip) | PUT |
| Person context | `resources/people/<name>.md` | PUT / PATCH |
| Concept / reference note | `resources/concepts/` or `resources/references/` | PUT |
| Meeting notes / call recap | `resources/meetings/YYYY-MM-DD-<slug>.md` | PUT |
| Skill / plugin capability entry | `_agent/skills/active/<slug>.md` (→ `archived/` when retired) | PUT |
| Daily activity / Agent Log | `journal/daily/YYYY-MM-DD.md` | POST / PATCH |
| Journal rollup | `journal/{weekly/YYYY-Www,monthly/YYYY-MM,quarterly/YYYY-Qn,annual/YYYY}.md` | PUT |
| Vault-health audit (agent self-maintenance) | `_agent/health/YYYY-MM-vault-health.md` | PUT |
| Session-end orientation pointer | `_agent/heartbeat/last-session.md` | PUT |
| Entity index (machine-maintained) | `_agent/index/entities.json` (rebuilt by `capture` / `sweep.py`) | PUT |
| Bootstrap marker (plugin-owned) | `_agent/echo-vault.md` (`schema_version`, date) | GET / PUT |
The complete endpoint→logic map — every path with its trigger and why it's distinct — is `skills/echo-memory/references/routing-map.md`.
**Slug rules:** kebab-case, ASCII, ~40 chars max. Every file carries canonical YAML frontmatter.
---
## Frontmatter conventions
Every note opens with this block (fill what applies; leave the rest empty rather than guessing):
```yaml
---
type: # daily-note | project | area | concept | reference | person |
# meeting | decision | review | session-log | working-memory |
# episodic-memory | semantic-memory | context-bundle | skill | ...
status: # active | draft | done | archived | complete | incubating | on-hold
created: # YYYY-MM-DD — earliest known date the entity was tracked
updated: # YYYY-MM-DD — bumped on meaningful change
tags: []
agent_written: # true on agent-generated notes
source_notes: [] # plain relative paths (backward links) — NEVER [[wikilinks]]
---
```bash
python3 chorus-memory.plugin.src/skills/chorus-memory/scripts/test_chorus_client.py # unit + routing-sync guard
python3 chorus-memory.plugin.src/skills/chorus-memory/scripts/test_v1_scaffold.py # module interfaces + config
python3 eval/test_features.py # capture/resolve/recall/link/sweep end-to-end
python3 eval/test_multimember.py # 26-check two-member group lifecycle
python3 eval/test_offline_queue.py eval/test_patch_semantics.py eval/test_reflect.py
python3 eval/run_eval.py # retrieval / dedup / write-safety / durability metrics
```
`agent_written: true` + a populated `source_notes` is the key signal separating agent-managed content from human-authored content.
### operator-preferences.md — Rules vs Observations
- `## Fact / Pattern` — promoted, deduped, **timeless** rules (no date prefix).
- `## Observations`**timestamped** raw observations (`- 2026-06-06: ...`); the default landing zone for new evidence.
Observations are promoted into Fact / Pattern (dropping the date) once a pattern stabilizes, and trimmed to the last ~30 entries.
---
## Bootstrap, repair & migration
The plugin carries everything needed to stand up a vault in `scaffold/`; there is no dependency on any in-vault control doc. These are deterministic scripts (`scripts/bootstrap.py`, `scripts/migrate.py`, `scripts/sweep.py`), not hand-run curl loops — the prose in `references/bootstrap.md` documents what they do and serves as the fallback.
- **Probe** — GET `_agent/echo-vault.md`. `200` → bootstrapped (check `schema_version`); `404` → run a fresh bootstrap.
- **Fresh bootstrap** (`bootstrap.py`; idempotent, additive — never overwrites): create the folder tree (a one-line README seeds each leaf since Obsidian/git ignore empty dirs) → PUT the 8 templates to their mirrored vault paths → PUT the 3 anchor seeds only if absent (the operator-preferences seed is deliberately empty — no fabricated facts) → PUT the thin vault README → PUT the marker **last** (so the vault is only flagged "set up" once everything is in place) → write a first-run trace (daily note + session log). `--dry-run` previews.
- **Repair** — `bootstrap.py` again: GET-probes each path and creates only what's missing; malformed files are flagged, never replaced.
- **Migrations** — `migrate.py` reads the marker's `schema_version`, applies each intervening migration (dry-run by default; `--apply` to perform moves/deletes), and stamps the marker. `0 → 1` removed the old in-vault control docs (`CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md`); `1 → 2` folded `reviews/` into `journal/` + `_agent/health/`; `2 → 3` adds the `_agent/index/` entity registry.
- **Bring up to spec** — `sweep.py` back-fills an upgraded vault: builds `_agent/index/entities.json` from existing notes and symmetrizes `## Related` cross-links (dry-run by default; `--apply` to write). Run it after `migrate.py`, or any time `/echo-health` reports index drift.
---
## Skill triggers
| Skill | Triggers |
|-------|----------|
| `echo-memory` | "remember that", "save to memory", "what do you know about me", "load my profile", "check my notes", "log this decision", "add to my inbox" — and proactively at the start of substantive conversations |
---
## Safety rules (operating contract)
- Do not fabricate facts, relationships, or prior decisions.
- Do not mass-restructure the vault unless explicitly asked.
- Do not delete notes unless deletion is explicitly requested and clearly safe.
- Never store secrets or API keys inside a vault note.
- Default to additive updates and explicit status changes over destructive edits.
- Write in **third person** about Jason ("Jason prefers X"). Do not cross-write with other vaults (Bryan's `goldbrain` is separate).
If the API returns a connection error, timeout, or `502` (usually Obsidian / the REST plugin not running), tell Jason once that the vault is unreachable and proceed without memory — don't retry repeatedly.
---
## Eval metrics (v1.5.1, 2026-07-03)
From the credential-free harness (`eval/run_eval.py` against the deterministic mock — no live vault; full detail in `eval/results/latest.json`). Baseline = pre-1.5 behavior: keyword substring ranking over entity notes only, no gate.
| Metric | v1.5.1 | pre-1.5 baseline |
|---|---|---|
| Retrieval recall@5 / MRR (8-query gold set) | **1.00 / 1.00** | 0.75 / 0.75 |
| Queries answerable only from sessions/journal | **2/2** | 0/2 (not in corpus) |
| Freshness: live note outranks stale archived twin | **yes** | no |
| Duplicate notes created (3 renamed-entity captures) | **0** (gate blocks) | 3 |
| Legitimate captures wrongly blocked | **0** | 0 |
| Silent write failures (4 fault scenarios) | **0** (failures loud) | — |
| Offline writes lost / duplicated on re-flush | **0 / 0** (3/3 queued + landed) | — |
## Version history
| Version | Highlights |
|---------|-----------|
| **1.5.1** | **Duplicate-gate precision.** Live use caught the gate blocking a decision note because it shared the single vault-common token "echo" with an archived project (min-normalized scoring gives any single-token entity a 1.0 match). New `gate_candidates()` blocks only on **same-kind** candidates (cross-kind name collisions — a decision titled after its project — warn instead), and a **lone shared token blocks only when unique to that entity** (vault df == 1); multi-token overlaps still block. Advisory warnings (`fuzzy_candidates`) unchanged; exit-76/`--merge-into`/`--force` unchanged. +5 tests; verified against the live vault both ways (false positive creates cleanly, true lookalike still gates). |
| **1.5.0** | **Six-improvement pass: retention, routing, and self-firing memory.** (1) **Capture-update keeps the whole body** — the update path previously appended only the first line of a multi-line body (silent data loss); now the full body rides under the dated bullet. (2) **Frontmatter completeness** — capture stamps a kind-default `status` and kind-seeded `tags` (`--tags` enriches); `fm` is create-or-replace (missing keys are surgically inserted instead of 400-failing); `vault_lint` gains a kind-scoped `incomplete-frontmatter` check; `sweep --apply` backfills (fixes the 18/71-notes field-audit drift, one data source: `KIND_STATUS`/`KIND_REQUIRED_FM`). (3) **Pre-write duplicate gate** — a strong fuzzy candidate stops `capture` (exit 76 + candidates) before the duplicate exists; `--merge-into <slug>` updates the canonical entity, `--force` overrides. (4) **Recall corpus + ranking** — sessions and journal notes join the BM25 corpus (down-weighted); scores fuse freshness (half-life on `updated:`) and status (`active` boosted, `archived` demoted); hits show `updated:`/`status:`; `recall --json`; recall-index schema 2 (auto-rebuilds). (5) **Session hooks** — SessionStart auto-loads memory into context, Stop nudges reflection once per substantive session; fail-safe, CoWork-fallback-aware. (6) **One-tap triage**`echo.py triage` lists the inbox structured, then classifies → previews → routes accepted items via `capture` with automatic processing-log audit lines; `--json` also lands on `link`/`scope show`. +22 mock end-to-end tests; all suites green. |
| **1.4.2** | **CoWork sandbox path resilience.** In a remote CoWork sandbox `${CLAUDE_PLUGIN_ROOT}` can point at a host path the sandbox can't reach (`/var/folders/…`) while the plugin is mounted under `…/mnt/.remote-plugins/…`, so script invocations failed until the agent found the real path by hand. SKILL.md and all eight slash commands now resolve the scripts dir with a fallback — prefer `${CLAUDE_PLUGIN_ROOT}`, else locate the mounted copy under `/sessions/*/mnt/.remote-plugins/*/…` — reused via `$ECHO`/`$LINT`/`$SWEEP`/`$SDIR`. On a normal host the primary path always wins (no behaviour change). `echo-health`/`echo-sweep` `allowed-tools` broadened to match the resolved invocation. (The scripts never hardcoded a path — root cause is the harness env var — but the plugin now self-heals.) |
| **1.4.1** | **Baked credentials now win unconditionally.** A per-user baked artifact could still be prompted for credentials on install: the baked `DEFAULT_*` tier was lowest priority, so a stale `ECHO_*` env var or a leftover/placeholder `~/.claude/echo-memory/config.json` shadowed the baked key — even an unedited `"<placeholder>"` value beat it, flipping `is_configured()` to false and triggering the first-run "ask the operator" flow despite valid baked creds. `echo_config.load()` now treats a **complete** baked set (endpoint + key both present) as authoritative — it wins over env vars and the config file and cannot be shadowed; the generic (unbaked) plugin keeps its env → file → first-run flow. New `baked_complete()`; `source()` reports `baked-in (plugin)`. Per-user artifacts rebaked; manifest → 1.4.1. |
| **1.4.0** | **Per-user baked-key builds — zero-touch CoWork install.** The CoWork sandbox doesn't bridge your real `~/.claude`, but the plugin itself is mounted every session — so credentials can travel *inside* the artifact. `echo_config` gains a baked tier (`DEFAULT_OWNER/BASE/KEY`, **empty in source** so the committed tree still ships zero credentials). New `build.py --bake-key --from <config.json> [--label <name>]` substitutes one user's owner/endpoint/key into those constants, producing a per-user artifact that needs **no config file and no per-session paste** (values may also come from `--owner/--endpoint/--key` or `ECHO_*` env). Baked builds default to `dist/` (gitignored) and never touch the shared `echo-memory.plugin` pointer; `--strip-key` force-blanks the constants for a guaranteed token-free build. `config show`/`doctor` report the baked source. |
| **1.3.1** | **Prompts for the key on load.** A fresh or just-upgraded machine has no config yet, so the plugin detects that and asks for it instead of failing opaquely. `echo_config.is_configured()` treats a missing **or still-placeholder** config as unconfigured; `echo.py load` prints a `NOT CONFIGURED` banner and exits `78` (other verbs exit `2`; `/echo-doctor` reports it red). New **`echo.py config import <file>`** adopts a config you've been handed — it validates the file and writes `~/.claude/echo-memory/config.json`. `SKILL.md` gains a **First run** step: on the not-configured signal the agent asks the operator for their key file and installs it, then proceeds. One-time per machine — the config lives in `~/.claude/`, so later plugin updates don't disturb it. |
| **1.3.0** | **User-agnostic — owner/endpoint/key out of the source.** The plugin previously baked in an API key and hard-coded the endpoint; both are gone. Owner, endpoint, and key are now **machine-local**, resolved by the new `echo_config.py` from `~/.claude/echo-memory/config.json` (env `ECHO_OWNER`/`ECHO_BASE`/`ECHO_KEY` override per field); `echo_secrets.py` removed. New `echo.py config` subcommand (`show`/`init`/`set`). All personal details (owner, role/employer), the live endpoint, and the literal key were **stripped** from the plugin source, references, commands, and eval harness — the only name that remains is the architect credit. A placeholder `config.template.json` is provided for new users/vaults. Routing manifest unchanged; offline test suites pass. Manifest → 1.3.0. |
| **1.2.0** | **Entity-resolution overhaul — fewer duplicate notes.** Resolution was exact-match only, so a shortened or expanded mention (e.g. "echo memory" for the `echo`-slugged active project) returned *no match* and the writer created a parallel note. Now `resolve()` matches on slug / title / **aliases**, with a `fuzzy_candidates()` "did-you-mean" fallback ranked by shared distinctive tokens (guarded by `echo_quality` so generic words like "data"/"api"/"the" can't drive a match). Aliases are **auto-derived from titles** (hyphen/space/case variants), **learned from mentions** on update, and stored in each note's `aliases:` frontmatter (the Obsidian-native home), then folded back into the entity index by `sweep` — so the graph gets better at resolution over time, while exact-match safety still prevents wrong auto-merges. Ships with the v1.1 connection-pool + concurrency work. 33 automated tests pass across Win/macOS/Linux; linter clean. See the [performance brief](echo-memory-performance-brief.html). |
| **1.1.0** | **Performance / network-layer rebuild — "from timing out to sub-second."** Full-vault scripts (`sweep.py`, `vault_lint.py`, recall rebuild) were making hundreds of *serial* requests, each opening a fresh TLS connection and re-reading every note 23×; on the constrained agent sandbox a single pass exceeded the ~120 s tool timeout and was killed mid-run, dropping the session/thread. Three structural fixes in `echo.py`: (1) **connection reuse (keep-alive)** — one persistent pooled connection per thread, reused across requests instead of a TCP+TLS handshake per file (**~4.5× faster per request**, the dominant win; benefits load/capture/recall for free); (2) **concurrent bulk reads** — new `read_many()` fans GETs across a thread pool (`ECHO_WORKERS`, default 8) for ~2× on full-vault scale; (3) **single-pass shared cache** — each note fetched *once* and reused across all passes (~23× fewer requests), eliminating `sweep`'s per-link-target re-fetch storm. Net effect: a full-vault pass that used to **time out now finishes in <1 s** (vault_lint 0.90 s, sweep plan 0.85 s on 186 notes); the practical change is fails → completes. Stdlib-only, no new dependencies. |
| **1.0.0** | Schema 4. **"Memory you can trust and retrieve."** (H1) **Hybrid recall** — local BM25 over note bodies fused with decayed graph expansion (`echo_recall.py`, `_agent/index/recall-index.json`), maintained on `capture`, rebuilt by `sweep`; replaces keyword-only recall. (H2) **Offline durability** — write verbs queue to a local write-ahead outbox on an outage and report "queued"; `flush` (+ flush-on-`load`) replays idempotently and **re-bases to the current endpoint**; `load` degrades to a last-known-good read cache (`echo_queue.py`). (H3) **Concurrency**`vault_lock` + `atomic_index_update` (lock + fresh re-read-merge) close the concurrent-`capture` entity-loss race (`echo_concurrency.py`). (H4) **Trust** — higher-fidelity PATCH mock + offline/reflect/patch-semantics suites gated in GitHub Actions across Win/macOS/Linux × Py 3.10/3.12. (H5) **Reflection capture**`echo.py reflect` / `/echo-reflect` extract→dedup→preview→apply durable items (`echo_reflect.py`). (M1) **Secret hardening** — key resolves env → `~/.echo-memory/credentials` → deprecated fallback; `write-key` CLI; token scrubbed from docs (see [API-KEY-SETUP.md](API-KEY-SETUP.md)). (M2) confident auto-linking + slug-collision guard (`echo_quality.py`). (M3) `echo.py doctor` / `/echo-doctor` + heartbeat-less `load` fallback. (M4) `capture --json` / `--dry-run`. (M5) manifest → 1.0.0, `.gitignore`. |
| **0.3.0** | Source promoted from zip-only to a tracked tree (`echo-memory.plugin.src/`); `.plugin` becomes a build artifact. All 7 skill-improvement items applied: search-first before writes, resilient daily Agent Log, `created:` semantics, project lifecycle + folder↔status rule, canonical HHMM session filenames, read-most-recent-N sessions, `source_notes` defined as backward links. |
| **0.4.0** | Efficiency + robustness pass: parallel cold-start loading, idempotent POST (read-before-append), doc-map-before-first-PATCH, scoped `updated:` bump, Inbox Triage, Scope Switching, monthly Vault Health, Rules-vs-Observations split, formal deprecation of `decisions/by-project/`, heartbeat pointer. |
| **0.4.1** | Bugfix: daily-note Agent Log heading detection now greps raw markdown for `^## Agent Log` instead of the `::`-delimited doc-map JSON (which never matched and appended duplicate headings). Added Scope Switching cold-start test harness. |
| **0.5.0** | Self-bootstrap + control-logic-in-plugin. Plugin becomes the single source of truth: bundled `scaffold/` (8 templates, 3 anchor seeds, thin vault README, marker) bootstraps an empty vault with no external/local-path dependency. New `operating-contract.md` (principles + safety from the old in-vault `CLAUDE.md`); `bootstrap.md` rewritten as a portable bootstrap/repair/migrate manifest. Cold-start probe moved from `/vault/BOOTSTRAP.md` to `_agent/echo-vault.md` (carries `schema_version`). Live vault migrated to data-only. |
| **0.5.1** | Routing-doc consistency pass: decision-mirror heading unified to `## Key Decisions`; stale `Current status` PATCH examples corrected to `Status`; vault-layout inline project example refreshed to the real template. All 17 `projects/active/` notes normalized losslessly to the canonical template heading set; `android-mqtt-shell` moved to `incubating/` (was broken `status: upcoming` in active). Plugin repackaged (21 files). |
| **0.6.0** | Schema 2. **#8 Inbox auto-fire:** the Loading procedure adds an inbox-depth GET and a load-time *Reconcile* step (inbox triage + scope-drift), so triage self-fires. **#10 Routing:** `reviews/` retired — weekly/monthly/quarterly/annual rollups fold into `journal/{weekly,monthly,quarterly,annual}/`, vault-health moves to `_agent/health/`; new `references/routing-map.md` is the complete audited endpoint→logic map. **Recs:** heartbeat pointer operationalized (read first at load, written at session end); new `scripts/vault_lint.py` mechanically checks vault invariants. Dead refs pruned (`archive/`, `_agent/outputs/`, `resources/source-material`). Migration `1 → 2` in `bootstrap.md`. |
| **0.7.0** | Schema 2 (unchanged layout). Hardening pass — gave the prose-and-curl skill an executable spine. **S2** `scripts/echo.py`: one validated client wrapping every verb with auth, HTTP-status checking (failed writes exit non-zero instead of looking like success), one bounded retry on 5xx, read-back-verified PUT, and idempotent `append`. **S3** `scripts/routing.json`: canonical machine-readable route manifest; `vault_lint.py` enforces it (flags unknown/retired paths). **S4** deterministic `scripts/bootstrap.py` + `scripts/migrate.py` (idempotent, dry-run, probe-before-write; fixes the old CWD-relative `@scaffold/...` empty-body bug). **S5** cooperative advisory lock (`_agent/locks/vault.lock`) + documented multi-writer model. **M1/M2/M5** linter rewrite: real YAML parsing, injected clock (`ECHO_TODAY`), exits `3` (not "clean") on an un-bootstrapped vault, plus routing-membership + frontmatter-integrity checks. **M3** status-check guidance throughout. **M4** four slash commands (`/echo-load`, `/echo-save`, `/echo-triage`, `/echo-health`). Added a credential-free A/B `eval/` harness (mock REST API + fault injection): isolates a **76% generated-token** I/O layer and **4 → 0 silent write failures** vs 0.6. |
| **0.7.1** | **Scope-drift fix.** Scope is the most churn-prone state (several sessions/day) and had no freshness signal, so sessions silently ran under stale scope (same failure class as #8). Added a `scope_updated:` frontmatter timestamp (maintained automatically), an `echo.py scope show` / `scope set` command (atomic switch: archive prior → replace → stamp), and a `vault_lint.py` **drift check** (flags when ≥ `SCOPE_STALE_SESSIONS`, default 3, session logs are dated after `scope_updated`) — making drift mechanically *evaluable* via `/echo-health`. Tightened the SKILL load-reconcile to *state and confirm* scope every session and switch before working. (Also fixed a bash nested-quote parse bug found while building `scope`, where `show` could fall through into `set`.) |
| **0.8.0** | **Cross-platform + reliability pass.** Ported the entire toolchain from bash to **pure Python** (`echo.py`, `vault_lint.py`, `bootstrap.py`, `migrate.py`) so it runs identically on Windows/macOS/Linux — removes the bash and macOS-only `date` dependencies (the lock's TTL parse previously degraded silently on unsupported platforms); stdout is UTF-8-safe so non-ASCII output can't crash a legacy console. **Correctness:** `append` now skips only on an exact **whole-line** match (a substring no longer causes a false skip that silently drops a write); the advisory lock is **read-back-confirmed** after PUT so a racing writer backs off instead of proceeding under a false lock. **Routing consistency (`check_routing.py`):** mechanically verifies the routing docs against `routing.json` in both directions, run as a guard in `test_echo_client.py`; added the missing locks / leaf-README / decision-template rows to the canonical map. **Usability:** new one-call `echo.py load` cold start; `SKILL.md` slimmed — the duplicated raw-curl recipes now live only in `api-reference.md` (the *nix fallback). |
| **0.9.0** | Schema 3. **Recall + routing intelligence — five layered features aimed at less input to route and richer links to recall.** (1) **Entity index** (`_agent/index/entities.json`): a slug→{path,kind,title,aliases} registry so routing/`resolve` is an O(1), alias-aware lookup instead of a fuzzy search (dedup at the source). (2) **`recall`**: search + one-hop expansion along `## Related` and `source_notes` — returns a topic's connected neighbourhood, not an isolated note. (3) **Bidirectional auto-linking**: `link A B` and link-on-`capture` keep the graph symmetric. (4) **`capture`**: one call that routes, stamps canonical frontmatter, indexes, auto-links mentioned entities, and writes the Agent-Log line — collapsing the whole write discipline. (5) **Graph-health checks** in `vault_lint.py`: broken wikilinks, orphan notes, entity-index drift. Plus **`sweep.py`** to bring an existing vault up to spec (build the index + symmetrize links + stamp schema), `migrate.py` 2→3, and `/echo-recall` + `/echo-sweep` commands. New modules `echo_index.py` / `echo_links.py` / `echo_ops.py`; offline + mock-integration tests (`eval/test_features.py`). |
| **2.0.0-rc.1** | Phase 5: full group-voice docs, member onboarding runbook, CHORUS icon. Plan complete — pending live deployment at `chorusapi.mpm.to`. |
| **2.0.0-alpha.4** | Phase 4 hardening: bounded lock retry on index writes, attributed duplicate gate, `recall --author`, `load --all-members`, N-writer concurrency contract, committed two-member e2e suite. |
| **2.0.0-alpha.3** | Phase 3 (the core): schema 5 per-member namespacing, group profile + group inbox, self-onboarding `load`, member-tagged shared writes, per-member lint, migrate 4→5. |
| **2.0.0-alpha.2** | Phase 2: `{group, member, endpoint, key}` config (member required), `author:` stamped on every agent write, `missing-author` lint, member-attributed locks/queue, per-member baked builds. |
| **2.0.0-alpha.1** | Phase 1: mechanical echo→chorus rename; clean break from ECHO (no back-compat). |
The pre-fork ECHO history (v0.3 → v1.5.1) lives in the [`jason/echo`](https://git.alwisp.com/jason/echo) repo and at this repo's `echo-fork-point` tag.
Binary file not shown.

After

Width:  |  Height:  |  Size: 775 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

+33
View File
@@ -0,0 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#141433"/>
<stop offset="1" stop-color="#232355"/>
</linearGradient>
</defs>
<!-- background -->
<rect x="0" y="0" width="1024" height="1024" rx="224" fill="url(#bg)"/>
<!-- three voices: overlapping wave arcs radiating from three offset origins -->
<g fill="none" stroke-linecap="round">
<!-- voice 1 — teal -->
<g stroke="#3fd8c2">
<circle cx="392" cy="452" r="70" stroke-width="34" opacity="0.95"/>
<circle cx="392" cy="452" r="160" stroke-width="26" opacity="0.55"/>
<circle cx="392" cy="452" r="250" stroke-width="20" opacity="0.28"/>
</g>
<!-- voice 2 — violet -->
<g stroke="#8f7bff">
<circle cx="632" cy="452" r="70" stroke-width="34" opacity="0.95"/>
<circle cx="632" cy="452" r="160" stroke-width="26" opacity="0.55"/>
<circle cx="632" cy="452" r="250" stroke-width="20" opacity="0.28"/>
</g>
<!-- voice 3 — amber -->
<g stroke="#ffb347">
<circle cx="512" cy="640" r="70" stroke-width="34" opacity="0.95"/>
<circle cx="512" cy="640" r="160" stroke-width="26" opacity="0.55"/>
<circle cx="512" cy="640" r="250" stroke-width="20" opacity="0.28"/>
</g>
</g>
<!-- shared center where the three voices meet -->
<circle cx="512" cy="512" r="46" fill="#f4f2ff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.
@@ -1,6 +1,6 @@
{
"name": "chorus-memory",
"version": "2.0.0-alpha.4",
"version": "2.0.0-rc.1",
"description": "Group-based persistent memory via the shared CHORUS Obsidian vault over the Obsidian Local REST API. Cross-platform Python client: one-call capture/resolve/recall/link/triage over an entity index, hybrid BM25 + graph recall spanning entities + sessions/journal (recency/status-aware), a pre-write duplicate gate, complete-frontmatter capture, session hooks that self-fire load/reflect, offline write-ahead queue, lock-guarded concurrency, linter-enforced routing, and /chorus-* commands.",
"author": {
"name": "Jason Stedwell"
+5 -5
View File
@@ -2,7 +2,7 @@
Persistent memory for Claude via the **CHORUS** Obsidian vault, using the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api).
Reads and writes notes across Claude/CoWork sessions through direct REST calls, routed through a bundled validated client (`scripts/chorus.py`) that status-checks every write. The toolchain is **pure Python**, so it runs identically on Windows, macOS, and Linux (only a Python 3 interpreter is needed — no bash). The plugin is **user-agnostic**: the vault owner, endpoint, and API key are not shipped in it — each machine supplies them through a local config file (see **Configuration**).
Reads and writes notes across Claude/CoWork sessions through direct REST calls, routed through a bundled validated client (`scripts/chorus.py`) that status-checks every write. The toolchain is **pure Python**, so it runs identically on Windows, macOS, and Linux (only a Python 3 interpreter is needed — no bash). The plugin is **group-agnostic**: the group, member id, endpoint, and API key are not shipped in it — each machine supplies them through a local config file (see **Configuration**).
Architected by **Jason Stedwell**.
@@ -10,7 +10,7 @@ Architected by **Jason Stedwell**.
## What it does
- Loads operator preferences, current context, and relevant project notes at the start of substantive conversations
- Loads the group profile, your member preferences, current context, and relevant project notes at the start of substantive conversations
- **One-call `capture`** routes a memory to its canonical home, stamps **complete** frontmatter (kind-default `status`, kind-seeded `tags`), indexes it, auto-links any entity it mentions, and logs it — driven by a machine-maintained **entity index** so routing is an alias-aware lookup, not a fuzzy search. Updates keep the **whole body**; a title that strongly resembles an existing entity **stops at a duplicate gate** (exit 76 — `--merge-into <slug>` / `--force`) instead of spawning a near-duplicate
- **`recall`** returns a topic's matching notes *and* their one-hop linked neighbourhood (Related links + `source_notes`) — the corpus spans the entity graph **plus session logs and journal notes** (down-weighted), ranked by BM25 × freshness × status so live notes outrank stale ones; **`resolve`** maps any mention to its canonical path; **`link`** adds reciprocal cross-links; **`triage`** routes aging inbox captures with an automatic processing-log audit trail
- Logs working sessions so future conversations can pick up where they left off
@@ -83,7 +83,7 @@ skills/chorus-memory/
│ └── session-log-template.md
├── scripts/ ← pure Python; run with python3 (Windows: python / py -3)
│ ├── chorus.py ← validated client + high-level CLI (capture/resolve/recall/link/load/scope/lock/config)
│ ├── chorus_config.py ← resolves owner/endpoint/key from the machine-local config (env-overridable)
│ ├── chorus_config.py ← resolves group/member/endpoint/key from the machine-local config (env-overridable)
│ ├── chorus_index.py ← entity index (registry, resolve, name→path map)
│ ├── chorus_links.py ← cross-link primitives (Related parsing, bidirectional linking)
│ ├── chorus_ops.py ← high-level ops (capture, recall, resolve, link, agent-log)
@@ -97,7 +97,7 @@ skills/chorus-memory/
└── scaffold/ ← verbatim files the bootstrap writes into the vault
├── README.vault.md chorus-vault.md
├── templates/ (8 note templates)
└── anchors/ (operator-preferences, current-context, inbox seeds)
└── anchors/ (group-profile, group-inbox + member preferences/context/inbox seeds)
```
## Skills & commands
@@ -122,4 +122,4 @@ skills/chorus-memory/
- **Python 3** available in the session environment (the scripts use only the standard library; invoke as `python3`, or `python` / `py -3` on Windows)
- Obsidian running on the backend with the [Local REST API plugin](https://github.com/coddingtonbear/obsidian-local-rest-api) enabled
- HTTPS access from the Claude/CoWork session environment to the endpoint configured in `~/.claude/chorus-memory/config.json`
- A machine-local config (`~/.claude/chorus-memory/config.json`) supplying the vault owner, endpoint, and API key — see **Configuration**
- A machine-local config (`~/.claude/chorus-memory/config.json`) supplying the group, member id, endpoint, and API key — see **Configuration**
@@ -16,5 +16,5 @@ bootstrapped (+ `schema_version`), and the **config source** for owner/endpoint/
(per-field: env override `CHORUS_OWNER`/`CHORUS_BASE`/`CHORUS_KEY`, then the machine-local
`~/.claude/chorus-memory/config.json` — or `baked-in (plugin)` on a per-user baked build,
which is authoritative and reported as such). Exits non-zero if anything is red — if the config is
missing (or still the placeholder template), ask the operator for their chorus-memory config file and install it with `chorus.py config import <path>` (or `config set --owner … --endpoint … --key …`). For the full vault-invariant lint, run
missing (or still the placeholder template), ask the member for their chorus-memory config file and install it with `chorus.py config import <path>` (or `config set --owner … --endpoint … --key …`). For the full vault-invariant lint, run
`/chorus-health`.
@@ -14,4 +14,4 @@ CHORUS_TODAY=<currentDate> python3 "$SDIR/vault_lint.py"
Exit codes: `0` clean · `1` violations (printed, grouped by check) · `2` vault unreachable · `3` vault not bootstrapped (run `bootstrap.py`).
Summarize the violations grouped by category and propose fixes, but **do not auto-fix** without the operator's go-ahead. If this is the first substantive session of a calendar month, offer to write the findings to `_agent/health/YYYY-MM-vault-health.md` (per the skill's **Vault Health** section).
Summarize the violations grouped by category and propose fixes, but **do not auto-fix** without the member's go-ahead. If this is the first substantive session of a calendar month, offer to write the findings to `_agent/health/YYYY-MM-vault-health.md` (per the skill's **Vault Health** section).
@@ -14,6 +14,6 @@ python3 "$CHORUS" load
`load` prints all eight sections (and self-onboards your member namespace on a first visit) (404s on today's note / inbox are shown as absent, not errors) and flags an un-bootstrapped vault. Follow up with `chorus.py scope show` if you need the sessions-since-switch count, and a project search if a specific project is in play.
**If `load` prints `NOT CONFIGURED` (exit 78)**, this machine has no usable key file yet. Don't proceed with memory — follow **First run** in `SKILL.md`: tell the operator CHORUS isn't configured, ask for their chorus-memory config file (group/member/endpoint/key), install it with `chorus.py config import <path>` (or `config set`), then re-run `load`.
**If `load` prints `NOT CONFIGURED` (exit 78)**, this machine has no usable key file yet. Don't proceed with memory — follow **First run** in `SKILL.md`: tell the member CHORUS isn't configured, ask for their chorus-memory config file (group/member/endpoint/key), install it with `chorus.py config import <path>` (or `config set`), then re-run `load`.
Do not narrate the reads. End with a one-line orientation: who/what/where the active scope is, and any reconcile prompt.
@@ -7,7 +7,7 @@ Use the **chorus-memory** skill to run **session reflection** (H5). Scan this co
things worth remembering across sessions, then propose them — don't write blindly.
1. **Extract** durable items from the conversation: new facts, preferences, decisions,
commitments, people/companies/projects introduced, and anything the operator said to remember.
commitments, people/companies/projects introduced, and anything the member said to remember.
Skip the ephemeral. Anchor relative dates on the conversation's `currentDate`.
2. **Emit a JSON array** of proposals (one per item), each:
`{"title","kind","body","aliases","tags","sources","confidence"}``kind`
@@ -15,7 +15,7 @@ things worth remembering across sessions, then propose them — don't write blin
working, skill, decision`; set `"inbox": true` when the home is genuinely unknown;
`confidence` 01 (items below 0.6 are dropped — send those to the inbox instead).
Write the array to a file with the Write tool (cross-platform; no heredoc).
3. **Preview, then apply.** Dry-run first (writes nothing) and show the operator the plan; only
3. **Preview, then apply.** Dry-run first (writes nothing) and show the member the plan; only
apply after they confirm.
```bash
@@ -16,7 +16,7 @@ python3 "$CHORUS" capture "<title>" <bodyfile> --kind <kind> [--aliases a,b] [--
python3 "$CHORUS" capture "<title>" --inbox # unknown home -> idempotent inbox line
```
**If capture exits `76` (duplicate gate):** the title strongly resembles an existing entity — do NOT retry blindly. Show the operator the printed candidates and either update the existing note (`capture ... --merge-into <slug>`) or, only after they confirm it's genuinely distinct, re-run with `--force`.
**If capture exits `76` (duplicate gate):** the title strongly resembles an existing entity — do NOT retry blindly. Show the member the printed candidates and either update the existing note (`capture ... --merge-into <slug>`) or, only after they confirm it's genuinely distinct, re-run with `--force`.
Drop to the low-level verbs only for shapes `capture` doesn't model (a project `## Status` replace, an ADR mirror, a daily-note edit):
@@ -3,7 +3,7 @@ description: Bring the CHORUS vault up to the current feature spec (entity index
allowed-tools: Bash(python3 *sweep.py*), Bash(python *sweep.py*), Bash(ls /sessions/*), Bash(dirname *)
---
Use the **chorus-memory** skill to bring the vault up to the current spec. Run the sweep **dry-run first**, show the operator the plan, and only `--apply` on their go-ahead:
Use the **chorus-memory** skill to bring the vault up to the current spec. Run the sweep **dry-run first**, show the member the plan, and only `--apply` on their go-ahead:
```bash
SDIR="${CLAUDE_PLUGIN_ROOT}/skills/chorus-memory/scripts"
@@ -11,13 +11,13 @@ python3 "$CHORUS" triage --list --json
# Windows: use `python` or `py -3` if `python3` is not on PATH.
```
1. **List** — the JSON gives each capture as `{line, date, text, age_days}`. Surface the aging ones (≥ ~7 days) to the operator and ask which to route.
2. **Propose** — for accepted items, write a proposals JSON with the Write tool (reflect schema: `{"title","kind","body",...}` **plus `"line"`: the original inbox line**, echoed into the audit log). Route per the map: preference/pattern → `semantic` (or a direct PATCH to `operator-preferences.md::Observations`); project idea → `project` with `--status incubating`; durable fact → `semantic`; person fact → `person`.
3. **Preview, then apply** with the operator's go-ahead:
1. **List** — the JSON gives each capture as `{line, date, text, age_days}`. Surface the aging ones (≥ ~7 days) to the member and ask which to route.
2. **Propose** — for accepted items, write a proposals JSON with the Write tool (reflect schema: `{"title","kind","body",...}` **plus `"line"`: the original inbox line**, echoed into the audit log). Route per the map: preference/pattern → `semantic` (a preference about the capturing member goes to their own `_agent/members/<member>/preferences.md::Observations` via direct PATCH); project idea → `project` with `--status incubating`; durable fact → `semantic`; person fact → `person`.
3. **Preview, then apply** with the member's go-ahead:
```bash
python3 "$CHORUS" triage proposals.json # dry-run: classify + preview, writes nothing
python3 "$CHORUS" triage proposals.json --apply # route via capture + log each move
```
`--apply` records every move in `inbox/processing-log/YYYY-MM-DD.md` (`- <original> → <destination>`) automatically. Do **not** delete the original captures unless the operator explicitly asks — the processing log is the audit trail. A proposal that stops at the duplicate gate (exit note in the summary) should be re-proposed with the existing entity's title.
`--apply` records every move in `inbox/processing-log/YYYY-MM-DD.md` (`- <original> → <destination>`) automatically. Do **not** delete the original captures unless the member explicitly asks — the processing log is the audit trail. A proposal that stops at the duplicate gate (exit note in the summary) should be re-proposed with the existing entity's title.
@@ -1,11 +1,11 @@
---
name: chorus-memory
description: Use the CHORUS Obsidian vault as the operator's persistent memory across Claude/CoWork sessions. Use whenever the operator asks to remember, save, note, log, or capture anything durable — facts, preferences, decisions, schedule changes, commitments — or asks what Claude knows about them, what was discussed or decided before, to check their notes, load their memory/profile/context, add to their inbox, or to track a new project. Trigger even without memory phrasing when the request implies recalling or persisting state across sessions ("pick up where we left off", "anything on X before my meeting?"). Also use proactively at the start of substantive work sessions to load context and at the end to log outcomes. Do NOT use for a different owner's vault, Obsidian/REST-API development questions, "memory" meaning RAM, timed reminders, email or local-file lookups, generic second-brain advice, or notes the operator routes to a specific other file or app.
description: Use the shared CHORUS Obsidian vault as the group's persistent memory across Claude/CoWork sessions — one vault, many members, every write attributed to the configured member. Use whenever the member asks to remember, save, note, log, or capture anything durable — facts, preferences, decisions, schedule changes, commitments — or asks what Claude knows about them or the team, what was discussed or decided before (by anyone in the group), to check their notes, load their memory/profile/context, add to their or the group's inbox, to see what other members have been working on, or to track a new project. Trigger even without memory phrasing when the request implies recalling or persisting state across sessions ("pick up where we left off", "anything on X before my meeting?", "did anyone already look into this?"). Also use proactively at the start of substantive work sessions to load context and at the end to log outcomes. Do NOT use for vaults outside this group, Obsidian/REST-API development questions, "memory" meaning RAM, timed reminders, email or local-file lookups, generic second-brain advice, or notes the member routes to a specific other file or app.
---
# CHORUS Memory
Use the **CHORUS** Obsidian vault as persistent memory. Read context accumulated across sessions; write things the operator asks to be remembered.
Use the **CHORUS** Obsidian vault as persistent memory. Read context accumulated across sessions; write things the member asks to be remembered.
The vault is shared **group memory**: one vault, many members. Each machine is configured with the **group** (who shares the vault) and this machine's **member id** (who is writing — stamped as `author:` on every agent write), in `~/.claude/chorus-memory/config.json`. Write memory in third person about people ("Jason prefers X", not "I prefer X") so the vault stays readable by every member and agent.
@@ -109,7 +109,7 @@ python3 "$CHORUS" lock "cc-<session-id>" # exit 75 if another session hold
python3 "$CHORUS" unlock "cc-<session-id>"
```
Use a stable `<session-id>` for both calls (any unique string for this session). The lock is read-back-confirmed (after PUT, `chorus.py` re-reads and backs off if another writer won the race) and cooperative (a stale lock past `CHORUS_LOCK_TTL`, default 15 min, is reclaimable); it lives at `_agent/locks/vault.lock`. It is a courtesy, not a hard mutex — if you can't take it, tell the operator another session may be active rather than racing it.
Use a stable `<session-id>` for both calls (any unique string for this session). The lock is read-back-confirmed (after PUT, `chorus.py` re-reads and backs off if another writer won the race) and cooperative (a stale lock past `CHORUS_LOCK_TTL`, default 15 min, is reclaimable); it lives at `_agent/locks/vault.lock`. It is a courtesy, not a hard mutex — if you can't take it, tell the member another session may be active rather than racing it.
### Slash commands
@@ -120,7 +120,7 @@ Use a stable `<session-id>` for both calls (any unique string for this session).
The plugin ships `hooks/hooks.json`, so the two most drift-prone behaviors no longer depend on remembering them:
- **SessionStart** runs `chorus.py load` and injects the output as context — cold-start loading is deterministic. Still do the **reconcile** (inbox depth, scope confirm) on that injected context before working.
- **Stop** fires **once per substantive session** when nothing has been reflected or session-logged yet: it blocks with a reminder to run the `/chorus-reflect` flow and write the session log + heartbeat. Reflection still previews and asks the operator before writing — the hook only ensures the question gets asked. If nothing durable emerged, just finish; never invent memories to satisfy the nudge.
- **Stop** fires **once per substantive session** when nothing has been reflected or session-logged yet: it blocks with a reminder to run the `/chorus-reflect` flow and write the session log + heartbeat. Reflection still previews and asks the member before writing — the hook only ensures the question gets asked. If nothing durable emerged, just finish; never invent memories to satisfy the nudge.
If the hooks are disabled or unavailable, the manual procedures below are unchanged and still authoritative.
@@ -137,7 +137,7 @@ Full contract (principles, agent role, memory model): `references/operating-cont
## When to Load Memory
Load at the start of any substantive conversation — anything beyond a single quick factual question. The signal: the operator is starting work, planning, asking for help with something that has state, or referencing prior discussions.
Load at the start of any substantive conversation — anything beyond a single quick factual question. The signal: the member is starting work, planning, asking for help with something that has state, or referencing prior discussions.
### Loading procedure
@@ -159,9 +159,9 @@ Do not read every session log — older sessions are reachable via `POST /search
**Reconcile at load (do this every cold start, after the batch returns).** The batch already fetched everything needed for a cheap self-check — run it before diving into the work so memory maintains itself instead of drifting:
1. **Inbox depth (Inbox Triage).** If your inbox (GET #7) or the group inbox (GET #8) holds dated capture lines older than ~7 days that were never routed, surface the count once and offer to triage — see **Inbox Triage** below. This is the load-time trigger that makes triage self-firing rather than something you only run when asked.
2. **Scope drift (state it, don't just check it).** Scope is the most churn-prone state — the operator runs several sessions a day across different topics, so the recorded `## Scope` is frequently stale at load. **Silently working under a stale scope is the default failure mode.** To prevent it, at load read the active scope and its freshness in one call — `python3 "$CHORUS" scope show` (prints `## Scope`, `scope_updated`, and how many sessions have been logged since) — and form a one-line judgment: *does this session's request match the recorded scope?* If it diverges, switch **before** doing the work via `python3 "$CHORUS" scope set "<new scope>"` (see **Scope Switching**). If `scope show` reports several sessions logged since the last switch, treat the recorded scope as suspect and confirm with the operator rather than trusting it.
2. **Scope drift (state it, don't just check it).** Scope is the most churn-prone state — the member runs several sessions a day across different topics, so the recorded `## Scope` is frequently stale at load. **Silently working under a stale scope is the default failure mode.** To prevent it, at load read the active scope and its freshness in one call — `python3 "$CHORUS" scope show` (prints `## Scope`, `scope_updated`, and how many sessions have been logged since) — and form a one-line judgment: *does this session's request match the recorded scope?* If it diverges, switch **before** doing the work via `python3 "$CHORUS" scope set "<new scope>"` (see **Scope Switching**). If `scope show` reports several sessions logged since the last switch, treat the recorded scope as suspect and confirm with the member rather than trusting it.
Keep the reconcile to a single short line to the operator (e.g. "3 inbox captures from last week are still un-routed — triage now?"); don't let it crowd out the actual request.
Keep the reconcile to a single short line to the member (e.g. "3 inbox captures from last week are still un-routed — triage now?"); don't let it crowd out the actual request.
**If a specific topic/project/person is in play**, pull its connected context in one call:
@@ -191,7 +191,7 @@ python3 "$CHORUS" triage proposals.json # dry-run: classify + previe
python3 "$CHORUS" triage proposals.json --apply # route via capture + log each move
```
Routing targets are the usual homes (a preference/pattern about YOU → your own `_agent/members/<member>/preferences.md` under `Preferences::Observations` via a direct PATCH; project idea → `project` with `--status incubating`; durable group fact → `semantic`; person fact → `person`). Never route another member's preference into your own preferences file — put it in theirs only if their own session confirms it, else `resources/people/`. `--apply` records each move in `inbox/processing-log/YYYY-MM-DD.md` (`- <original line> → <destination path>`) automatically. Originals are **never deleted** unless the operator explicitly asks — the processing log is the audit trail.
Routing targets are the usual homes (a preference/pattern about YOU → your own `_agent/members/<member>/preferences.md` under `Preferences::Observations` via a direct PATCH; project idea → `project` with `--status incubating`; durable group fact → `semantic`; person fact → `person`). Never route another member's preference into your own preferences file — put it in theirs only if their own session confirms it, else `resources/people/`. `--apply` records each move in `inbox/processing-log/YYYY-MM-DD.md` (`- <original line> → <destination path>`) automatically. Originals are **never deleted** unless the member explicitly asks — the processing log is the audit trail.
## Project Lifecycle
@@ -210,14 +210,14 @@ Projects move through four folders under `projects/`. The folder name and the `s
## When to Write Memory
Write when the operator:
Write when the member:
- States a fact, preference, or commitment worth keeping ("I prefer X", "we use uv not pip", "standup is Tuesday at 10")
- Makes a non-obvious decision worth recording
- Says "remember that", "save this", "log this", "add to memory", "note that"
- Finishes a meaningful working session future sessions should pick up
Write in third person about the operator. Every note carries the canonical frontmatter (see below). Agent-generated notes set `agent_written: true` and `author: <member>` (your configured member id).
Write in third person about the member. Every note carries the canonical frontmatter (see below). Agent-generated notes set `agent_written: true` and `author: <member>` (your configured member id).
**Default write path: `capture`.** For a routed memory (a fact about a person/company/project/concept/etc.), prefer `python3 "$CHORUS" capture "<title>" <bodyfile> --kind <kind>` — it does the search-first, path derivation, frontmatter, indexing, auto-linking, and Agent-Log line in one call (see **High-level memory ops**). Drop to the lower-level `put`/`patch`/`append` verbs below only for shapes `capture` doesn't model (a project `## Status` replacement, an ADR mirror, a daily-note edit) or to inspect/repair.
@@ -233,9 +233,9 @@ python3 "$CHORUS" search <slug> # fallback when the index isn't trusted
If a match is found:
- In a non-active project subfolder (`on-hold/`, `incubating/`, `archived/`): **promote/merge** — PUT the merged content to `projects/active/<slug>.md` with `status: active`, then DELETE the old location. Preserve the earliest `created:` date.
- In the same folder you intended to write: **update in place** (PATCH or merged PUT). Never silently overwrite — fold the existing content in first.
- Elsewhere (e.g. a stale duplicate under `resources/`): tell the operator and ask which should be canonical before writing.
- Elsewhere (e.g. a stale duplicate under `resources/`): tell the member and ask which should be canonical before writing.
**Search both the slug AND any human title** the operator used (e.g. slug `chorus-memory` and title `CHORUS plugin`). Slug-only searches miss notes filed under a different naming scheme. Two cheap `POST /search/simple/?query=...` calls beat one expensive cleanup pass later.
**Search both the slug AND any human title** the member used (e.g. slug `chorus-memory` and title `CHORUS plugin`). Slug-only searches miss notes filed under a different naming scheme. Two cheap `POST /search/simple/?query=...` calls beat one expensive cleanup pass later.
Only after the search comes back empty (or you've decided to merge) is it safe to create a new note. This rule prevents the most common duplication bug: a note exists in `on-hold/` but the agent only checked `active/` and created a parallel record.
@@ -276,7 +276,7 @@ Every PUT body needs the canonical YAML frontmatter (see `references/vault-layou
## Scope Switching (`current-context.md`)
`_agent/members/<member>/current-context.md` tracks a single active scope. The operator routinely shifts scope within a day (one project → another → docs), so this is high-churn — switch deliberately, every time the work changes topic.
`_agent/members/<member>/current-context.md` tracks a single active scope. The member routinely shifts scope within a day (one project → another → docs), so this is high-churn — switch deliberately, every time the work changes topic.
**Preferred — one command:**
@@ -310,7 +310,7 @@ A weekly/monthly rollup is a **light digest** — open threads across `projects/
## Vault Health (monthly)
On the first substantive session of a calendar month, run a quick health pass and write findings to `_agent/health/YYYY-MM-vault-health.md`. This is **agent self-maintenance, not a journal entry** — it lives under `_agent/` because it's about the vault's mechanical integrity, not the operator's work narrative. Don't auto-fix without asking.
On the first substantive session of a calendar month, run a quick health pass and write findings to `_agent/health/YYYY-MM-vault-health.md`. This is **agent self-maintenance, not a journal entry** — it lives under `_agent/` because it's about the vault's mechanical integrity, not the member's work narrative. Don't auto-fix without asking.
Run the bundled linter first — it mechanically checks the invariants below so you don't eyeball them. **Pass `CHORUS_TODAY` = the conversation's `currentDate`** so stale/aging math uses the same clock you write with (not the runner's machine date):
@@ -346,7 +346,7 @@ python3 "$SWEEP" --apply # write
Run it after `migrate.py` (which handles structural/schema changes) — or any time `/chorus-health` reports index drift. It is idempotent; re-running is safe.
The pass is cheap and pays for itself by catching drift before it requires a reorg. Write the findings as a digest; act on them only with the operator's go-ahead.
The pass is cheap and pays for itself by catching drift before it requires a reorg. Write the findings as a digest; act on them only with the member's go-ahead.
**Performance.** Full-vault scripts (`sweep.py`, `vault_lint.py`, recall rebuild) are connection-pooled (keep-alive) and read the whole vault concurrently via `chorus.read_many`, so they finish in about a second on a few-hundred-note vault instead of timing out on hundreds of serial TLS handshakes. Tune with `CHORUS_WORKERS` (default 8, bulk-read concurrency) and `CHORUS_TIMEOUT` (default 30s, per-request). For a vault large enough that even the concurrent pass nears the tool timeout, run the script with the harness's background-execution option rather than blocking the turn.
@@ -397,11 +397,11 @@ python3 "$CHORUS" patch journal/daily/<currentDate>.md append heading "<currentD
**Routing triggers — areas / meetings / skills:**
- **Area** (`areas/`): a standing responsibility the operator maintains with no finish line. Decision rule: has an end state → `projects/`; never "done" → `areas/`. Subfolder is the domain (`business`/`personal`/`learning`/`systems`); create it on first write. `type: area`.
- **Area** (`areas/`): a standing responsibility the member maintains with no finish line. Decision rule: has an end state → `projects/`; never "done" → `areas/`. Subfolder is the domain (`business`/`personal`/`learning`/`systems`); create it on first write. `type: area`.
- **Meeting** (`resources/meetings/`): notes, recaps, or action items tied to a specific meeting or call. Mirror decisions to `decisions/by-date/` and commitments to the relevant project. `type: meeting`.
- **Skill** (`_agent/skills/`): the operator authors, installs, or retires a skill/plugin and wants it tracked as a capability — distinct from `projects/`, which tracks the build effort. Active entries in `active/`, retired ones in `archived/`. `type: skill`.
- **Skill** (`_agent/skills/`): the member authors, installs, or retires a skill/plugin and wants it tracked as a capability — distinct from `projects/`, which tracks the build effort. Active entries in `active/`, retired ones in `archived/`. `type: skill`.
Never delete files unless the operator explicitly asks. Memory is append-friendly; deletion is destructive.
Never delete files unless the member explicitly asks. Memory is append-friendly; deletion is destructive.
## Session Logging
@@ -427,11 +427,11 @@ python3 "$CHORUS" put _agent/members/<member>/heartbeat.md <bodyfile>
## Vault Unreachable
If the API returns a connection error, timeout, or `502`, tell the operator once that the memory vault is unreachable (a `502` usually means Obsidian/the REST plugin is not running on the backend), then proceed without memory. Do not retry repeatedly.
If the API returns a connection error, timeout, or `502`, tell the member once that the memory vault is unreachable (a `502` usually means Obsidian/the REST plugin is not running on the backend), then proceed without memory. Do not retry repeatedly.
## Style Rules
- Write in third person about the operator: "the operator prefers X", not "I prefer X".
- Write in third person about the member: "the member prefers X", not "I prefer X".
- This vault holds the GROUP's shared memory — every member reads and writes it, and everything in it is visible to the whole group. Do not edit another member's `_agent/members/<id>/` namespace (their preferences/scope/heartbeat/inbox belong to their sessions); facts about them as a person go in `resources/people/`. Do not cross-write with vaults outside this group.
- **Anchor relative dates on the conversation's `currentDate`** before writing. "Today" → `currentDate`. "Thursday" / "next week" → resolve to an absolute `YYYY-MM-DD`. Never guess from training-data knowledge of the current year.
- Every memory file has canonical YAML frontmatter — see `references/vault-layout.md`.
@@ -440,15 +440,15 @@ If the API returns a connection error, timeout, or `502`, tell the operator once
- **`source_notes` lists the note(s) that *triggered* or *supplied content for* this one** — e.g. the session log that produced a project update, or the daily note where a captured fact originated. It is a *backward* link to inputs. Forward links (this note → other notes it references) belong in the `## Related` section in the note body, never in frontmatter.
- **Never put `[[wikilinks]]` in frontmatter** — YAML parses them as nested lists and the links break in Obsidian's reading view. Put all cross-references in a `## Related` section in the note **body** as a bulleted list of `[[links]]`.
- Use Obsidian wiki links (`[[note name]]`) freely in the note **body** for cross-references.
- Keep entries short and focused. Fewer, sharper entries beat many noisy ones — the operator explicitly prefers concision.
- About to write something large or sensitive? Show the operator the content first and confirm.
- Keep entries short and focused. Fewer, sharper entries beat many noisy ones — the member explicitly prefers concision.
- About to write something large or sensitive? Show the member the content first and confirm.
## operator-preferences.md — Rules vs Observations
## preferences.md — Rules vs Observations
`_agent/members/<member>/preferences.md` separates two kinds of content:
Each member's `_agent/members/<member>/preferences.md` separates two kinds of content:
- `## Fact / Pattern`**promoted, deduped rules.** No date prefix. These are timeless: "the operator prefers concise communication." Append here only when a rule is stable.
- `## Observations`**timestamped raw observations.** Date-prefixed: `- 2026-06-06: the operator chose X over Y because Z.` This is where new evidence goes by default.
- `## Fact / Pattern`**promoted, deduped rules.** No date prefix. These are timeless: "the member prefers concise communication." Append here only when a rule is stable.
- `## Observations`**timestamped raw observations.** Date-prefixed: `- 2026-06-06: the member chose X over Y because Z.` This is where new evidence goes by default.
During monthly Vault Health or when an observation stabilizes, promote it from `## Observations` into `## Fact / Pattern` (drop the date) and remove the duplicate from Observations. Trim Observations to the last ~30 entries when it grows past that — the rest live in session logs.
@@ -120,7 +120,7 @@ curl -s -X PUT \
```bash
cat > /tmp/obs_patch.md << 'OBSEOF'
The operator prefers concise status updates — lead with the decision.
The member prefers concise status updates — lead with the decision.
OBSEOF
curl -s -X PATCH \
@@ -195,12 +195,12 @@ curl -s -X DELETE \
"$CHORUS_BASE/vault/inbox/imports/old-note.md"
```
Only on explicit operator request. Deletion is destructive.
Only on explicit member request. Deletion is destructive.
> **DELETE removes files only — never directories.** Deleting the last file in a folder
> leaves the empty folder orphaned on disk; the REST listing then 404s (nothing left to
> list), which falsely reads as "the tree is gone", but Obsidian's file explorer still
> shows it. After removing a folder's last file, tell the operator the empty folder
> shows it. After removing a folder's last file, tell the member the empty folder
> needs manual deletion in Obsidian — do not claim the tree is gone from a 404 listing.
> (Learned 2026-07-03 removing the retired `archive/` and `_agent/outputs/` trees.)
@@ -35,7 +35,7 @@ curl -s -o /dev/null -w "%{http_code}" -H "$AUTH" "$BASE/vault/_agent/chorus-vau
```
- **200** → bootstrapped. Read the marker's `schema_version`; if it is **less than** the plugin's current schema (4), run a migration pass (see *Migrations* below), otherwise proceed straight to the loading procedure in `SKILL.md`.
- **404** → empty/unconfigured vault. Run **Fresh bootstrap** below. (If you expected an existing vault, confirm with the operator once that the REST API is pointed at the right vault before seeding.)
- **404** → empty/unconfigured vault. Run **Fresh bootstrap** below. (If you expected an existing vault, confirm with the member once that the REST API is pointed at the right vault before seeding.)
---
@@ -137,7 +137,7 @@ Substitute `{{DATE}}`, then PUT `scaffold/chorus-vault.md` → `/vault/_agent/ch
### 6. First-run trace
Create today's daily note from `journal/templates/daily-note-template.md` (resolve the `{{date:…}}` tokens to today), append a one-line `## Agent Log` entry noting the bootstrap, and write a session log in `_agent/sessions/YYYY-MM-DD-HHMM-bootstrap.md`. Tell the operator briefly what was created.
Create today's daily note from `journal/templates/daily-note-template.md` (resolve the `{{date:…}}` tokens to today), append a one-line `## Agent Log` entry noting the bootstrap, and write a session log in `_agent/sessions/YYYY-MM-DD-HHMM-bootstrap.md`. Tell the member briefly what was created.
---
@@ -153,6 +153,6 @@ Run the same steps 15, but GET-probe each path first and **only create what i
When the marker's `schema_version` is older than the plugin's, apply the migration steps for each intervening version, then PATCH the marker's `schema_version` frontmatter to the new value.
- **0 → 1** (control-docs-in-plugin): the vault previously carried root control docs (`CLAUDE.md`, `BOOTSTRAP.md`, `STRUCTURE.md`, `index.md`). Back them up outside the vault, DELETE them, PUT the thin `scaffold/README.vault.md` over the old verbose `README.md`, write the `_agent/chorus-vault.md` marker, and scrub now-dangling `[[CLAUDE]]`/`[[BOOTSTRAP]]`/`[[STRUCTURE]]`/`[[index]]` links from the `## Related` sections of `operator-preferences.md` and `current-context.md` (leave historical session logs alone). Confirm with the operator before deleting.
- **1 → 2** (reviews-folded-into-journal): the `reviews/` tree is retired. (a) For each note under `reviews/weekly/` and `reviews/monthly/`, MOVE it into `journal/weekly/` (rename `YYYY-Www-review.md``YYYY-Www.md`) and `journal/monthly/` respectively, preserving the earliest `created:`. (b) Move any `reviews/monthly/YYYY-MM-vault-health.md` to `_agent/health/`. (c) Move `reviews/quarterly|annual/` artifacts to `journal/quarterly|annual/`. (d) Update inbound `[[reviews/...]]` wikilinks in `## Related` sections to the new paths. (e) DELETE the now-empty `reviews/` tree. Confirm with the operator before deleting; leave historical session logs alone. *(This step covers any vault bootstrapped under schema 1.)*
- **0 → 1** (control-docs-in-plugin): the vault previously carried root control docs (`CLAUDE.md`, `BOOTSTRAP.md`, `STRUCTURE.md`, `index.md`). Back them up outside the vault, DELETE them, PUT the thin `scaffold/README.vault.md` over the old verbose `README.md`, write the `_agent/chorus-vault.md` marker, and scrub now-dangling `[[CLAUDE]]`/`[[BOOTSTRAP]]`/`[[STRUCTURE]]`/`[[index]]` links from the `## Related` sections of `operator-preferences.md` and `current-context.md` (leave historical session logs alone). Confirm with the member before deleting.
- **1 → 2** (reviews-folded-into-journal): the `reviews/` tree is retired. (a) For each note under `reviews/weekly/` and `reviews/monthly/`, MOVE it into `journal/weekly/` (rename `YYYY-Www-review.md``YYYY-Www.md`) and `journal/monthly/` respectively, preserving the earliest `created:`. (b) Move any `reviews/monthly/YYYY-MM-vault-health.md` to `_agent/health/`. (c) Move `reviews/quarterly|annual/` artifacts to `journal/quarterly|annual/`. (d) Update inbound `[[reviews/...]]` wikilinks in `## Related` sections to the new paths. (e) DELETE the now-empty `reviews/` tree. Confirm with the member before deleting; leave historical session logs alone. *(This step covers any vault bootstrapped under schema 1.)*
- **2 → 3** (entity index + cross-linking): adds the `_agent/index/` folder for the machine-maintained entity registry. `migrate.py` creates the folder; the **data back-fill is `sweep.py`** — after migrating, run `python3 scripts/sweep.py --apply` to build `_agent/index/entities.json` from the notes already in the vault and to symmetrize existing `## Related` cross-links (it adds only the missing reciprocal direction, never invents links). `sweep.py` is idempotent and dry-run by default; re-run it any time `/chorus-health` reports index drift.
@@ -20,7 +20,7 @@ You are an agent operating against an Obsidian vault that functions as a shared,
- **Working** → `_agent/memory/working/` — transient, time-boxed.
- **Episodic** → `_agent/memory/episodic/` — what happened, when.
- **Semantic** → `_agent/memory/semantic/` — durable facts, patterns, preferences (`operator-preferences.md`).
- **Semantic** → `_agent/memory/semantic/` — durable group facts and patterns (incl. `group-profile.md`); each member's preferences live in their own `_agent/members/<member>/preferences.md`.
- **Context bundles** → `_agent/context/` — task-focused reading lists and active state.
## Safety rules
@@ -21,7 +21,7 @@ Views of the same truth: `scripts/routing.json` is the **machine-readable canoni
| `inbox/imports/<slug>.md` | Raw external material dropped in wholesale (export, paste, dump) | The raw artifact, unedited | Holds un-triaged *bulk*, vs captures which hold single lines | PUT |
| `inbox/processing-log/YYYY-MM-DD.md` | An inbox item is routed to its real home | One line: `<original> → <destination path>` | Audit trail of moves — never holds memory itself, only the record of routing | POST |
Captures and imports are temporary by contract. Triage drains them into the homes below and logs the move; the original is left until the operator okays deletion.
Captures and imports are temporary by contract. Triage drains them into the homes below and logs the move; the original is left until the member okays deletion.
## journal/ — the one append-only time-series stream
@@ -53,7 +53,7 @@ Lifecycle folders; `status:` frontmatter MUST equal the folder name (the linter
| Path | Trigger | What lands | Distinct because | Method |
|------|---------|------------|------------------|--------|
| `areas/<domain>/<slug>.md` | Ongoing domain the operator maintains indefinitely (`<domain>`: business/personal/learning/systems) | Area note | No end state — the one thing that disqualifies it from `projects/` | PUT |
| `areas/<domain>/<slug>.md` | Ongoing domain the member maintains indefinitely (`<domain>`: business/personal/learning/systems) | Area note | No end state — the one thing that disqualifies it from `projects/` | PUT |
## resources/ — reference material about the world
@@ -61,7 +61,7 @@ Lifecycle folders; `status:` frontmatter MUST equal the folder name (the linter
|------|---------|------------|------------------|--------|
| `resources/people/<name>.md` | A fact about a specific person | Person note (kebab-case slug) | Keyed to a person, not a topic or event | PUT / PATCH |
| `resources/companies/<slug>.md` | A fact about an organization (client, vendor, partner, employer) | Company note (kebab-case slug) | Keyed to an organization, not an individual (`people/`) or an external source (`references/`) | PUT / PATCH |
| `resources/concepts/<slug>.md` | A reusable concept/idea the operator wants on file | Concept note | An idea, vs a `reference` which is an external source | PUT |
| `resources/concepts/<slug>.md` | A reusable concept/idea the member wants on file | Concept note | An idea, vs a `reference` which is an external source | PUT |
| `resources/references/<slug>.md` | An external source/link worth keeping | Reference note | Points outward (a source), vs `concepts` (an idea) | PUT |
| `resources/meetings/YYYY-MM-DD-<slug>.md` | Notes/recap tied to a specific meeting or call | Meeting note; mirror decisions to `decisions/by-date/`, commitments to the project | Event-anchored to a meeting, vs a project's ongoing thread | PUT |
@@ -90,7 +90,7 @@ Lifecycle folders; `status:` frontmatter MUST equal the folder name (the linter
| `_agent/sessions/<member>/YYYY-MM-DD-HHMM-<slug>.md` | A substantive session ends (decisions/artifacts/commitments) | Session log (see template) | Per-session record, namespaced by member; the unit loading's heartbeat fallback scans | PUT |
| `_agent/health/YYYY-MM-vault-health.md` | First substantive session of a month (Vault Health pass) | Health-audit findings | Agent self-maintenance about vault integrity — NOT a work narrative, so not in `journal/` | PUT |
| `_agent/templates/` | Bootstrap only (seeded from plugin masters) | Canonical note templates | Holds templates, not memory; never a runtime routing target | PUT (seed) |
| `_agent/skills/active/<slug>.md` | The operator authors/installs a skill and wants it catalogued | Skill capability entry | Catalogs a *capability*, vs `projects/` which tracks the *build effort* | PUT |
| `_agent/skills/active/<slug>.md` | The member authors/installs a skill and wants it catalogued | Skill capability entry | Catalogs a *capability*, vs `projects/` which tracks the *build effort* | PUT |
| `_agent/skills/archived/<slug>.md` | A catalogued skill is retired | Skill entry (moved from `active/`) | Terminal state of the skill catalog | PUT |
| `_agent/locks/vault.lock` | Advisory multi-writer lock acquire/release | One line: `<owner> @ <ISO-timestamp>` | Concurrency coordination, not memory; managed only by `chorus.py lock/unlock` | PUT / DELETE |
| `_agent/index/entities.json` | Rebuilt on every `capture` and on `sweep` | Slug→{path, kind, title, aliases, last_seen} registry | Machine-maintained routing/recall index, not a note; the one JSON in the vault | PUT |
@@ -127,7 +127,7 @@ Listed so they are recognised as dead and never recreated. Any one of these appe
## Routing decision tree (the calls that get mis-made)
1. **Destination unknown right now?**`inbox/captures/`. Known? → route directly; never park a known fact in the inbox.
2. **Is it about the operator's work over time?**`journal/` (pick the grain by cadence). **About the vault's mechanical health?**`_agent/health/`. These two look similar monthly but answer different questions.
2. **Is it about the member's work over time?**`journal/` (pick the grain by cadence). **About the vault's mechanical health?**`_agent/health/`. These two look similar monthly but answer different questions.
3. **Does the effort have an end state?**`projects/` (folder = `status:`). **No finish line?**`areas/`.
4. **A fact about the world:** timeless → `semantic/`; a thing that happened → `episodic/`; needed only for now → `working/`. A fact about a *person*`resources/people/`; a fact about an *organization*`resources/companies/`.
5. **A decision:** always `decisions/by-date/`; mirror into a project only if one already exists.
@@ -80,7 +80,7 @@ client: claude-code
Build and package the chorus-memory CoWork plugin against the live vault.
## Notes Read
- [[BOOTSTRAP]], [[STRUCTURE]], [[_agent/memory/semantic/operator-preferences]]
- [[BOOTSTRAP]], [[STRUCTURE]], [[_agent/members/<member>/preferences]]
## Actions Taken
Verified the REST API end-to-end, confirmed the scaffold copied into the live vault,
@@ -119,18 +119,18 @@ change `updated:` or `status:`, use PATCH with `Target-Type: frontmatter`.
## File-Specific Conventions
### operator-preferences.md (`_agent/memory/semantic/`)
### preferences.md (`_agent/members/<member>/`)
The profile analog. Canonical headings:
Each member's profile analog — how the agent should work FOR that member. Group-visible by design (one vault, one key); only that member's own sessions edit it. Canonical headings:
- `## Operator` — who the vault owner is (one paragraph)
- `## Member` — who this member is (one paragraph)
- `## Fact / Pattern`**promoted, deduped rules.** No date prefix. Timeless.
- `## Observations`**timestamped raw observations.** Date-prefixed lines (`- 2026-06-06: ...`). Default landing zone for new evidence.
- `## Evidence` — citations/links supporting the rules
- `## Recommendation or Implication` — how the rules should shape behavior
- `## Review Notes` — confidence / last review date
Append observed facts under `## Observations` by default. Promote to `## Fact / Pattern` (dropping the date) once a pattern stabilizes. "The operator" is the vault owner — refer to them generically and in the third person (the display name is the runtime `owner` config value).
Append observed facts under `## Observations` by default. Promote to `## Fact / Pattern` (dropping the date) once a pattern stabilizes. Write in third person by name ("Jason prefers X"), so the note reads correctly to every member and agent.
### projects/active/\<slug\>.md
@@ -189,11 +189,11 @@ ADR-style: Context → Decision → Consequences. If the decision belongs to an
### companies/\<slug\>.md
`type: company`. An organization the vault owner works with — client, vendor, partner, or employer (e.g. `acme.md`). Distinct from `people/` (individuals, who *belong to* companies) and `references/` (external sources). Lowercase kebab-case slug.
`type: company`. An organization the group works with — client, vendor, partner, or employer (e.g. `acme.md`). Distinct from `people/` (individuals, who *belong to* companies) and `references/` (external sources). Lowercase kebab-case slug.
---
## Cross-References
Use Obsidian wiki links freely: `[[note-name]]` or `[[folder/note]]`. The REST API
doesn't resolve them, but Obsidian does when the operator browses the vault.
doesn't resolve them, but Obsidian does when the member browses the vault.
+66
View File
@@ -0,0 +1,66 @@
# CHORUS Member Onboarding
How to add a member to the group vault — from nothing to their first attributed write. The vault admin does steps 13; the member does step 4; the plugin does the rest.
## 0. One-time: stand up the vault (admin, once per group)
1. Run Obsidian with the **Local REST API** plugin on the backend host, pointed at an empty vault dedicated to the group.
2. Expose it at the deployment endpoint (**`https://chorusapi.mpm.to`**) behind the reverse proxy with a valid cert. Note the REST API's bearer key — this is the **vault key** every member's build will carry.
3. Configure your own machine (`chorus.py config set --group "…" --member "<your-id>" --endpoint "https://chorusapi.mpm.to" --key "…"`), then run `python3 scripts/bootstrap.py` — it stands up the folder tree, templates, group profile, group inbox, your member namespace, and the schema-5 marker. Follow with `sweep.py --apply` and `/chorus-doctor` to confirm green.
## 1. Create the member's config (admin)
Each member needs four values. Copy `chorus-memory.config.template.json` and fill it in:
```json
{
"group": "MPM",
"member": "alice",
"endpoint": "https://chorusapi.mpm.to",
"key": "<the vault key>"
}
```
`member` is a **kebab-case slug** (`^[a-z0-9][a-z0-9-]{0,31}$`) — it becomes the `author:` stamp on every one of their writes and their `_agent/members/alice/` namespace, so pick it once and keep it. `group` is the display name shared by everyone.
## 2. Bake their artifact (admin)
```bash
python build.py --bake-key --from alice-config.json --label alice
# -> dist/chorus-memory-<version>-alice.plugin
```
The baked artifact carries their member id, the endpoint, and the vault key inside the plugin, so it works on desktop **and** in every CoWork session with zero setup — no config file, no per-session paste. A complete baked set is authoritative and can't be shadowed by stale configs.
> **The artifact contains the live vault bearer token.** `dist/` is gitignored — deliver the file directly to that one member and never commit, push, or publish it. Offboarding/rotation = rotate the vault key, rebuild every member's artifact, redeliver.
## 3. Deliver (admin)
Send the member their `.plugin` file over a private channel. That file is their whole setup.
## 4. Install and start (member)
Install the plugin (Claude Code: `/plugin install <file>`; CoWork: add it to the session). Then just start a substantive session — the SessionStart hook runs `load`, and on that first load CHORUS **self-onboards** the member:
- creates `_agent/members/alice/` (preferences, current-context, inbox) and `_agent/sessions/alice/`,
- adds `- alice (joined YYYY-MM-DD)` to the group profile's `## Members` roster.
Nothing else to do. Sanity checks if wanted: `/chorus-doctor` (all green, member id shown with its source) and `load --all-members` (they appear in the roster).
## 5. First-session norms (member)
- Tell the agent a little about yourself when convenient — it lands in **your** `preferences.md` (`## Member` / `## Observations`). Preferences are group-visible by design; truly private material belongs in a personal vault, not CHORUS.
- Your quick captures go to **your** inbox; drop "someone should look at this" items in the **group inbox** (`inbox/captures/group.md`) as `- YYYY-MM-DD [you]: …`.
- Shared knowledge (projects, people, companies, decisions) is everyone's — the duplicate gate will stop you from creating a parallel note for something a teammate already tracks, and will tell you whose note it is.
- Never edit another member's `_agent/members/<id>/` namespace. Facts about a person as a person go in `resources/people/`.
## Troubleshooting
| Symptom | Meaning / fix |
|---|---|
| `NOT CONFIGURED` (exit 78) on load | The artifact isn't baked / config missing. Install the baked plugin, or `chorus.py config import <file>`. |
| `config set: --member must be a kebab-case slug` | Fix the member id — lowercase letters/digits/hyphens only. |
| Doctor: `member id configured — invalid` | Same as above; the config's `member` value isn't a valid slug. |
| Doctor: `vault reachable — FAIL` | Obsidian / Local REST API / the proxy at `chorusapi.mpm.to` isn't up. Not a config problem. |
| Load says `marker absent — vault not bootstrapped` | The vault itself was never stood up — run step 0.3 (admin). |
| `/chorus-health` reports `missing-author` | A note with `agent_written: true` lacks `author:` — someone hand-wrote it; add their member id. |
+40 -61
View File
@@ -1,89 +1,68 @@
# ECHO Memory — Usage Guide
# CHORUS Memory — Usage Guide
*Written for Bryan (and anyone else coming back to ECHO after a break). Current as of v1.5.1, July 2026.*
*Written for any group member coming to CHORUS (or back to it after a break). Current as of v2.0.0-rc.1, July 2026. Setup itself is in [ONBOARDING.md](ONBOARDING.md).*
ECHO gives Claude persistent memory across sessions. Everything durable — people, companies, projects, decisions, preferences, session history — lives as markdown notes in an Obsidian vault, read and written over the Obsidian Local REST API. The plugin ships the whole toolchain (pure Python, stdlib only — works the same on Windows/macOS/Linux) plus the operating procedure Claude follows.
CHORUS gives Claude persistent **group memory** across sessions. Everything durable — people, companies, projects, decisions, session history, each member's preferences — lives as markdown notes in one shared Obsidian vault, read and written over the Obsidian Local REST API. The whole team's sessions work against the same knowledge graph, and every agent write is attributed (`author: <member>`), so you always know who knows what.
The short version of how to use it: **you mostly don't have to do anything.** Since v1.5 the system loads itself at session start and nudges itself to save at session end. The slash commands below are for when you want to drive it explicitly.
The short version of how to use it: **you mostly don't have to do anything.** The system loads itself at session start (and onboards you automatically on your very first session), and nudges itself to save at session end. The slash commands below are for when you want to drive it explicitly.
---
## One-time setup
If Jason handed you a **per-user baked plugin** (`echo-memory-1.x.x-bryan.plugin`), just install it. Your vault credentials are baked into the artifact — no config file, no key pasting, works on desktop and in every CoWork session. This is the normal path.
You were handed a **per-member baked plugin** (`chorus-memory-2.x-<you>.plugin`). Just install it — your member id, the endpoint, and the vault key are baked in. No config file, no key pasting, works on desktop and in every CoWork session. Treat the file like a password: it contains the live vault key. Don't share or commit it.
If you have the **generic plugin** instead, it will report `NOT CONFIGURED` on first use and ask for your config file. Install it once with:
```bash
python3 echo.py config import <your-config.json>
```
That writes `~/.claude/echo-memory/config.json` (owner, endpoint, API key). It survives plugin updates — one time per machine.
**Verify either way:** run `/echo-doctor`. Green across the board (Python, vault reachable, auth, bootstrapped, key source) means you're live.
**Verify:** run `/chorus-doctor`. Green across the board (Python, endpoint, key, **member id**, group, vault reachable, bootstrapped schema 5) means you're live. Your first `load` creates your `_agent/members/<you>/` namespace and adds you to the group roster — nothing to do on your side.
---
## Day-to-day usage
### What happens automatically (v1.5+)
### What happens automatically
- **Session start** — a hook runs the cold-start load and injects your memory into context: your profile/preferences, current scope, the last session log, today's journal note, and inbox depth. You don't have to ask Claude to "load memory" anymore.
- **Session end** — if the session was substantive and nothing was saved, a hook nudges (once) to run `/echo-reflect` and write the session log. Reflection always previews before writing — nothing lands without your OK.
- **If the vault is unreachable**, writes queue to a local outbox and replay automatically next time it's up; reads fall back to a cached last-known-good. An outage degrades gracefully instead of erroring.
- **Session start** — a hook runs the cold-start load and injects memory into context: the **group profile**, *your* preferences, *your* current scope, *your* last session, today's **shared** journal note, and both inbox depths (yours + the group's).
- **Session end** — if the session was substantive and nothing was saved, a hook nudges (once) to run `/chorus-reflect` and write the session log. Reflection always previews before writing — nothing lands without your OK.
- **If the vault is unreachable**, writes queue to a local outbox and replay automatically next time it's up; reads fall back to a cached last-known-good.
### The slash commands
| Command | When to use it |
|---|---|
| `/echo-load` | Manually load memory context. Rarely needed now — the session-start hook does this — but useful mid-session after a lot of vault writes, or if the hook didn't fire. |
| `/echo-save <thing>` | Save something right now: "remember the Henagar tower password is in the vault", "log that we decided X". Routes it to the right note automatically. |
| `/echo-recall <topic>` | Ask what memory knows about a topic. Returns ranked matches **plus their linked neighbourhood** — a project comes back with its people, decisions, and recent session mentions attached. |
| `/echo-reflect` | End-of-session sweep: extracts durable facts, decisions, and new entities from the conversation, shows you the proposed writes, applies on confirm. The main way memory grows. |
| `/echo-triage` | Empty the inbox: classifies each quick capture, previews destinations, routes accepted items with an audit trail. Run when load reports the inbox is getting deep. |
| `/echo-doctor` | Readiness check — Python, vault reachability, auth, bootstrap/schema version, where the key came from. First stop when anything seems off. |
| `/echo-health` | Full vault lint: routing violations, broken wikilinks, orphan notes, stale scope, incomplete frontmatter, index drift. Run occasionally or when things feel inconsistent. |
| `/echo-sweep` | Bring the vault up to the current plugin spec after an upgrade — rebuilds the entity index, backfills frontmatter, fixes one-way links. Dry-run by default; **run this once right after updating from an old version.** |
| `/chorus-load` | Manually load memory context (the session-start hook usually does this). `load --all-members` adds a group-wide view: every member's scope and last session. |
| `/chorus-save <thing>` | Save something right now: "remember the Henagar tower password is in the vault", "log that we decided X". Routes it to the right note automatically, attributed to you. |
| `/chorus-recall <topic>` | Ask what the GROUP's memory knows about a topic — including things teammates saved. Returns ranked matches plus their linked neighbourhood; add `--author <member>` to see one person's notes only. |
| `/chorus-reflect` | End-of-session sweep: extracts durable facts, decisions, and new entities from the conversation, previews, applies on confirm. The main way memory grows. |
| `/chorus-triage` | Empty the inboxes — **yours and the group's**: classifies each capture, previews destinations, routes accepted items with a who-routed-what audit trail. |
| `/chorus-doctor` | Readiness check — Python, vault reachability, auth, member id, bootstrap/schema, key source. First stop when anything seems off. |
| `/chorus-health` | Full vault lint: routing violations, broken wikilinks, orphans, per-member stale scopes and aging inboxes, unattributed agent notes, index drift. |
| `/chorus-sweep` | Bring the vault up to the current plugin spec after an upgrade — rebuilds indexes, backfills frontmatter, fixes one-way links. Dry-run by default. |
### A typical session
1. Start a conversation — memory loads itself. Claude knows who you are, what's active, and what happened last session.
2. Work normally. Say "save that" / "remember this" whenever something durable comes up (or `/echo-save` explicitly). Not sure where something belongs? Save it anyway — it lands in the inbox and `/echo-triage` sorts it later.
3. At the end, accept the reflection nudge (or run `/echo-reflect` yourself). Confirm the preview. Done — next session picks up from here.
1. Start a conversation — memory loads itself. Claude knows who *you* are, what the group is, what you were doing, and what happened in your last session.
2. Work normally. Say "save that" / "remember this" whenever something durable comes up. Not sure where it belongs? It lands in **your** inbox; something the *team* should see lands in the **group inbox**.
3. At the end, accept the reflection nudge (or run `/chorus-reflect`). Confirm the preview. Done — your next session picks up from here, and your teammates' sessions can already see what you saved.
### Group etiquette (the short list)
- **Shared knowledge is everyone's.** Projects, people, companies, decisions — read them, update them (your updates are member-tagged), link to them.
- **Namespaces are personal.** Never edit another member's `_agent/members/<id>/` files. A fact about a person goes in `resources/people/`; a teammate's *preference* is theirs to record.
- **The duplicate gate is a teammate detector.** If a save is blocked with "likely already exists — created by jason", that's the system telling you the group already tracks it. Merge into the existing note.
- **Everything is group-visible.** One vault, one key. Truly private material belongs in a personal vault, not CHORUS.
---
## What the memory system can do (current capabilities)
## What the memory system can do
**Smart, deduplicating capture.** One `capture` call routes content to its canonical home via an entity index, stamps complete frontmatter (status, tags, timestamps), cross-links every entity it mentions, and logs the write. Names resolve through aliases and fuzzy matching, so "echo memory" finds the `echo` project instead of spawning a duplicate note. If a new title strongly resembles an existing **same-kind** entity, a pre-write duplicate gate *stops* the write and shows you the candidates — you choose merge or create. Duplicates get blocked before they exist, not cleaned up after.
**Smart, attributed, deduplicating capture.** One `capture` call routes content to its canonical home via a shared entity index, stamps complete frontmatter (status, tags, timestamps, **author**), cross-links every entity it mentions, and writes a member-tagged line on the team's daily log. Names resolve through aliases and fuzzy matching; a pre-write duplicate gate stops near-duplicates *before* they exist and names whose entity you collided with.
**Real retrieval, not keyword grep.** Recall fuses BM25 full-text search with graph expansion along note links, over entities *and* session logs *and* journal entries. Ranking is freshness- and status-aware: recently updated and `active` notes float up, `archived` sinks. You get a topic's connected neighbourhood, not one isolated file.
**Real retrieval over the whole group's knowledge.** Recall fuses BM25 full-text search with graph expansion along note links, over entities *and* every member's session logs *and* the journal. Ranking is freshness- and status-aware. Filter to one member with `--author`.
**Structured memory model.** Working (transient) / episodic (what happened, when) / semantic (durable facts and preferences) memory layers, plus a current-context scope, per-session logs, an append-only journal with rollups, and PARA-style projects/areas/resources/decisions. A machine-readable routing manifest defines what may be written where, and the linter enforces it.
**Structured memory model.** Working / episodic / semantic layers plus the group profile, per-member scope + preferences + inbox + sessions, an append-only shared journal with rollups, and PARA-style projects/areas/resources/decisions. A machine-readable routing manifest defines what may be written where, and the linter enforces it.
**Reflection.** `/echo-reflect` turns a conversation into memory: extract → classify → dedup against the index → confidence-filter → preview → apply. Low-confidence items go to the inbox instead of polluting the graph.
**Durability and safety.** Every write is HTTP-status-checked and read-back-verified. Appends are idempotent. Offline writes queue and replay. Shared indexes are lock-guarded with retry, and lock owners are member-attributed. The per-member namespacing means teammates' sessions can't clobber your scope or heartbeat even in principle.
**Durability and safety.** Every write is HTTP-status-checked and read-back-verified (a failed write fails loudly, never silently). Appends are idempotent — retries can't double-write. Offline writes queue and replay. A cooperative advisory lock keeps Claude Code and CoWork from trampling each other on the shared vault.
**Self-maintaining.** The vault bootstraps itself from an empty Obsidian vault, migrates its own schema on version bumps, and `/echo-sweep` + `/echo-health` keep the graph honest (index rebuilds, link symmetry, frontmatter backfill, orphan/broken-link detection).
**Fast.** Connection pooling + concurrent reads mean full-vault operations that used to time out in the sandbox now finish in under a second.
---
## What's new since 0.7 (your last consistent version)
You left off right after the toolchain got its executable spine (`echo.py`, routing manifest, linter). Since then, in rough order of what you'll actually notice:
- **You don't route anything by hand anymore.** 0.7 was "pick the path, write with the right verb." Now `capture`/`recall`/`resolve`/`link` do routing, frontmatter, indexing, and cross-linking in one call — and in practice you just talk to Claude and it uses them.
- **Memory loads and saves itself** (v1.5 hooks). No more remembering to run the loading procedure or write the session log.
- **Recall is actually good now** (v0.91.5): entity index + hybrid BM25/graph search + freshness/status ranking, spanning sessions and journal too.
- **Duplicates get blocked at write time** (v1.5 gate) instead of accumulating.
- **Offline resilience** (v1.0): vault down ≠ data lost; writes queue and replay.
- **Everything is pure Python** (v0.8): no bash, works identically on Windows.
- **Credentials moved out of the plugin** (v1.31.4): generic builds prompt once per machine; your baked build carries them invisibly. Treat a baked `.plugin` file like a password — it contains your vault key. Don't share or commit it.
**After installing the new version, run `/echo-doctor`, then `/echo-sweep` once** to bring your vault up to the current schema (it dry-runs first and shows the plan).
**Self-maintaining, self-onboarding.** The vault bootstraps itself from empty, onboards new members on their first load, migrates its own schema on version bumps, and `/chorus-sweep` + `/chorus-health` keep the graph honest.
---
@@ -91,9 +70,9 @@ You left off right after the toolchain got its executable spine (`echo.py`, rout
| Symptom | Fix |
|---|---|
| `NOT CONFIGURED` banner / doctor shows red config | Generic build with no config on this machine — `echo.py config import <file>`, or ask Jason for your baked build. |
| Vault unreachable | Check that Obsidian + the Local REST API plugin are running on the backend and the endpoint is reachable. Meanwhile writes queue safely and replay on reconnect. |
| Save blocked with "duplicate gate" (exit 76) | Not an error — it found a likely-existing entity. Review the candidates: merge into the existing note, or force-create if it's genuinely distinct. |
| Memory feels stale or inconsistent | `/echo-health` to see what's off, `/echo-sweep` to repair index/links/frontmatter. |
| Inbox keeps getting mentioned at load | `/echo-triage` — one pass empties it. |
| Anything else weird | `/echo-doctor` first, then ask Jason. |
| `NOT CONFIGURED` banner / doctor shows red config | Your build isn't baked or the config is missing this machine — see [ONBOARDING.md](ONBOARDING.md), or ask the vault admin for your baked build. |
| Vault unreachable | Obsidian + the Local REST API + the proxy at `chorusapi.mpm.to` must be up. Meanwhile writes queue safely and replay on reconnect. |
| Save blocked with "duplicate gate … created by \<member\>" (exit 76) | Not an error — the group already tracks that entity. Merge into the existing note (`--merge-into <slug>`), or force-create if genuinely distinct. |
| Memory feels stale or inconsistent | `/chorus-health` to see what's off, `/chorus-sweep` to repair. |
| Inboxes keep getting mentioned at load | `/chorus-triage` — one pass empties both. |
| Anything else weird | `/chorus-doctor` first, then ask the vault admin. |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

-66
View File
@@ -1,66 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Squircle background: Deep Navy -> Royal -> Signal -->
<linearGradient id="bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#25335C"/>
<stop offset="0.55" stop-color="#204C84"/>
<stop offset="1" stop-color="#1A6E96"/>
</linearGradient>
<!-- Orb glow: teal core -->
<radialGradient id="orb" cx="0.42" cy="0.40" r="0.70">
<stop offset="0" stop-color="#7FF0EC"/>
<stop offset="0.35" stop-color="#1FC9C4"/>
<stop offset="1" stop-color="#009B98"/>
</radialGradient>
<!-- Vault enclosure subtle gradient -->
<linearGradient id="vault" x1="106" y1="160" x2="406" y2="406" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#2A86AE"/>
<stop offset="1" stop-color="#155D80"/>
</linearGradient>
<filter id="soft" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="14"/>
</filter>
<filter id="softer" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="26"/>
</filter>
<clipPath id="squircle">
<path d="M256 8
C 120 8, 8 120, 8 256
C 8 392, 120 504, 256 504
C 392 504, 504 392, 504 256
C 504 120, 392 8, 256 8 Z"/>
</clipPath>
</defs>
<!-- Background -->
<g clip-path="url(#squircle)">
<rect x="0" y="0" width="512" height="512" fill="url(#bg)"/>
<!-- ambient teal glow from the orb spilling into the navy -->
<circle cx="256" cy="262" r="150" fill="#009B98" opacity="0.18" filter="url(#softer)"/>
</g>
<!-- Concentric ripple rings (echo / memory) -->
<g fill="none" stroke="#3FE0DB" stroke-linecap="round">
<circle cx="256" cy="262" r="96" stroke-width="3" opacity="0.55"/>
<circle cx="256" cy="262" r="120" stroke-width="2.5" opacity="0.32"/>
<circle cx="256" cy="262" r="142" stroke-width="2" opacity="0.18"/>
</g>
<!-- Vault / open container: ring open at the top, cradling the orb -->
<path d="M 370.9 165.6 A 150 150 0 1 1 141.1 165.6"
fill="none" stroke="url(#vault)" stroke-width="26" stroke-linecap="round"/>
<!-- inner highlight on the vault -->
<path d="M 370.9 165.6 A 150 150 0 1 1 141.1 165.6"
fill="none" stroke="#5BC7E6" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Orb glow halo -->
<circle cx="256" cy="262" r="74" fill="#00C2BE" opacity="0.55" filter="url(#soft)"/>
<!-- Orb -->
<circle cx="256" cy="262" r="62" fill="url(#orb)"/>
<!-- Orb specular highlight -->
<ellipse cx="236" cy="240" rx="22" ry="16" fill="#EAFFFE" opacity="0.45"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

-222
View File
@@ -1,222 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ECHO Memory — Performance Update</title>
<style>
:root{
--ink:#16181d; --ink-soft:#3a3f4a; --muted:#6b7280;
--line:#e4e7ec; --line-soft:#eef1f4;
--bg:#ffffff; --panel:#f7f8fa; --panel-2:#f0f4f9;
--accent:#2f6df6; --accent-deep:#1b3a8f; --accent-soft:#e8f0ff;
--good:#0f9d6b; --good-soft:#e6f6ef; --bad:#d64545; --bad-soft:#fdecec;
--warn:#b7791f; --code-bg:#0f1320; --code-ink:#e6e9f2;
--radius:14px;
}
*{box-sizing:border-box;}
html{-webkit-print-color-adjust:exact; print-color-adjust:exact;}
body{
margin:0; color:var(--ink); background:#eceef1;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
line-height:1.55; font-size:15px;
}
.page{
max-width:880px; margin:32px auto; background:var(--bg);
padding:0 0 56px; border-radius:18px; overflow:hidden;
box-shadow:0 10px 40px rgba(20,24,35,.10);
}
.hero{
background:linear-gradient(135deg,#101a3a 0%,#1b3a8f 55%,#2f6df6 100%);
color:#fff; padding:48px 56px 40px; position:relative;
}
.hero .kicker{font-size:12px; letter-spacing:.18em; text-transform:uppercase; opacity:.82; margin:0 0 14px;}
.hero h1{font-size:38px; line-height:1.08; margin:0 0 10px; font-weight:750; letter-spacing:-.5px;}
.hero .sub{font-size:16.5px; opacity:.92; max-width:640px; margin:0 0 22px;}
.hero .meta{display:flex; flex-wrap:wrap; gap:10px 26px; font-size:13px; opacity:.92; border-top:1px solid rgba(255,255,255,.18); padding-top:16px;}
.hero .meta b{font-weight:650;}
.pill{display:inline-block; background:rgba(255,255,255,.16); border:1px solid rgba(255,255,255,.28); padding:3px 11px; border-radius:999px; font-size:12px; font-weight:600; margin-right:6px;}
.body{padding:8px 56px 0;}
section{padding:28px 0; border-bottom:1px solid var(--line-soft);}
section:last-child{border-bottom:none;}
h2{font-size:13px; letter-spacing:.14em; text-transform:uppercase; color:var(--accent-deep); margin:0 0 4px; font-weight:700;}
.h2sub{font-size:22px; font-weight:700; letter-spacing:-.3px; margin:0 0 16px; color:var(--ink);}
h3{font-size:15.5px; margin:20px 0 7px; font-weight:680;}
p{margin:0 0 13px; color:var(--ink-soft);}
strong{color:var(--ink);}
code{background:var(--panel-2); padding:1px 6px; border-radius:5px; font-family:"SF Mono",ui-monospace,Menlo,Consolas,monospace; font-size:12.5px; color:var(--accent-deep);}
.lede{font-size:16.5px; color:var(--ink); background:var(--accent-soft); border-left:4px solid var(--accent); padding:15px 19px; border-radius:0 10px 10px 0; margin:0;}
.metrics{display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin:4px 0 6px;}
.metric{background:var(--panel); border:1px solid var(--line); border-radius:var(--radius); padding:16px 13px; text-align:center;}
.metric .num{font-size:30px; font-weight:780; letter-spacing:-1px; line-height:1; color:var(--accent-deep);}
.metric .num.good{color:var(--good);}
.metric .lbl{font-size:11.5px; color:var(--muted); margin-top:7px; line-height:1.35;}
.grid{display:grid; gap:13px;}
.g2{grid-template-columns:repeat(2,1fr);}
.card{background:var(--panel); border:1px solid var(--line); border-radius:var(--radius); padding:15px 16px;}
.card h4{margin:0 0 5px; font-size:14.5px; font-weight:680;}
.card p{margin:0; font-size:13px; color:var(--muted); line-height:1.5;}
table{width:100%; border-collapse:collapse; font-size:13px; margin:8px 0 6px;}
th,td{text-align:left; padding:9px 11px; border-bottom:1px solid var(--line);}
th{background:var(--panel-2); font-size:11px; letter-spacing:.05em; text-transform:uppercase; color:var(--ink-soft); font-weight:680;}
td.n{font-variant-numeric:tabular-nums; text-align:right; white-space:nowrap;}
tr td:first-child{font-weight:600; color:var(--ink);}
.delta{font-weight:700; color:var(--good);}
.muted-row td{color:var(--muted);}
/* bar chart */
.bars{margin:14px 0 4px;}
.bar{display:flex; align-items:center; gap:12px; margin:0 0 9px;}
.bar .name{flex:0 0 168px; font-size:12.5px; color:var(--ink-soft); text-align:right;}
.bar .track{flex:1; background:var(--panel-2); border-radius:7px; height:26px; position:relative; overflow:hidden;}
.bar .fill{height:100%; border-radius:7px; background:linear-gradient(90deg,#2f6df6,#1b3a8f); display:flex; align-items:center; justify-content:flex-end; padding-right:9px; color:#fff; font-size:12px; font-weight:700;}
.bar .fill.old{background:linear-gradient(90deg,#e87f7f,#d64545);}
.bar .fill.best{background:linear-gradient(90deg,#23b783,#0f9d6b);}
.note{background:var(--good-soft); border:1px solid #bfe6d4; border-radius:10px; padding:12px 16px; font-size:12.5px; color:#0c5c43; margin:14px 0 0;}
.warn{background:#fff6e6; border:1px solid #f0d9a8; color:#7a5712;}
.foot{padding:22px 56px 0; color:var(--muted); font-size:11.5px; line-height:1.5;}
.foot b{color:var(--ink-soft);}
@media print{
body{background:#fff;}
.page{margin:0; box-shadow:none; border-radius:0; max-width:none; padding-bottom:24px;}
@page{ size:A4; margin:13mm; }
section,.metrics,.bars,table,.grid{break-inside:avoid;}
.hero{border-radius:0;}
}
</style>
</head>
<body>
<div class="page">
<div class="hero">
<p class="kicker">ECHO Memory · Performance Update</p>
<h1>Full-vault operations: from timing out to sub-second</h1>
<p class="sub">Releases v1.1.0 and v1.2.0 rebuilt the vault's network layer and entity
resolution. Long operations that used to exceed the agent tool timeout and drop the
session now complete in under a second — and shortened entity names resolve instead of
spawning duplicates.</p>
<div class="meta">
<span><span class="pill">v1.1.0</span><span class="pill">v1.2.0</span></span>
<span><b>Prepared</b> 2026-06-22</span>
<span><b>Vault</b> 186 notes · 119 entities</span>
<span><b>Endpoint</b> echoapi.alwisp.com</span>
</div>
</div>
<div class="body">
<section>
<p class="lede">Scripts that sweep the whole vault — <code>sweep.py</code>,
<code>vault_lint.py</code>, recall rebuild — were making hundreds of <em>serial</em>
requests, each opening a fresh TLS connection, and re-reading every note 23 times.
On the constrained agent sandbox that pushed a single pass past the ~120-second tool
timeout, so the process was killed mid-run and the session/thread was dropped. The
fix attacks the cause on three axes; the headline change is reliability:
<strong>a full-vault pass that used to fail now finishes in well under a second.</strong></p>
</section>
<section>
<h2>At a glance</h2>
<div class="metrics">
<div class="metric"><div class="num">4.5&times;</div><div class="lbl">faster per request<br>(connection reuse)</div></div>
<div class="metric"><div class="num">~2&times;</div><div class="lbl">added by concurrency<br>(full-vault scale)</div></div>
<div class="metric"><div class="num">23&times;</div><div class="lbl">fewer requests<br>(single-pass cache)</div></div>
<div class="metric"><div class="num good">&lt;1 s</div><div class="lbl">full-vault pass<br>(was: timeout)</div></div>
</div>
<p style="margin-top:6px">These factors compound. A full-vault pass pays
<strong>4.5&times;</strong> less per request, issues <strong>~23&times;</strong> fewer
of them, and runs the rest <strong>~2&times;</strong> in parallel — a modeled
<strong>~1827&times;</strong> reduction in total request-time, before counting the
higher connection latency of the production sandbox where the old path actually failed.</p>
</section>
<section>
<h2>What changed</h2>
<div class="h2sub">Three structural fixes, one resolution overhaul</div>
<div class="grid g2">
<div class="card"><h4>Connection reuse (keep-alive)</h4><p>One persistent
connection per thread, reused across every request — instead of a fresh TCP+TLS
handshake per file. The single biggest win; benefits every operation
(load, capture, recall) for free.</p></div>
<div class="card"><h4>Concurrent bulk reads</h4><p>A new <code>read_many()</code>
fans GETs across a thread pool (8 workers). I/O-bound work, so it parallelizes
cleanly; the win grows with vault size and network latency.</p></div>
<div class="card"><h4>Single-pass shared cache</h4><p>Each note is fetched
<em>once</em> and reused across all passes — eliminating the 23&times; re-reads and
the per-link-target re-fetch storm in <code>sweep</code>.</p></div>
<div class="card"><h4>Entity resolution (v1.2.0)</h4><p>Shortened names now resolve
via aliases + a fuzzy fallback, so a mention like &ldquo;echo memory&rdquo; finds the
existing project instead of silently creating a duplicate note.</p></div>
</div>
</section>
<section>
<h2>Measured — per request</h2>
<div class="h2sub">Connection reuse is the dominant win</div>
<p>Micro-benchmark: 25 real notes, best of 3 runs, against the live endpoint.</p>
<div class="bars">
<div class="bar"><div class="name">Fresh connection / GET <em>(old)</em></div>
<div class="track"><div class="fill old" style="width:100%">30.3 ms</div></div></div>
<div class="bar"><div class="name">Pooled keep-alive (serial)</div>
<div class="track"><div class="fill" style="width:21.8%">6.6 ms</div></div></div>
<div class="bar"><div class="name">Pooled + concurrent <em>(new)</em></div>
<div class="track"><div class="fill best" style="width:18.8%">5.7 ms</div></div></div>
</div>
<table>
<tr><th>Mode</th><th class="n">ms / request</th><th class="n">vs old</th></tr>
<tr><td>Fresh connection per GET (old)</td><td class="n">30.3</td><td class="n">1.0&times;</td></tr>
<tr><td>Pooled keep-alive, serial</td><td class="n">6.6</td><td class="n delta">4.5&times;</td></tr>
<tr><td>Pooled + concurrent (new default)</td><td class="n">5.7</td><td class="n delta">5.3&times;</td></tr>
</table>
</section>
<section>
<h2>Measured — full vault (186 notes)</h2>
<div class="h2sub">Serial vs concurrent, both pooled</div>
<table>
<tr><th>Operation</th><th class="n">serial (1 worker)</th><th class="n">concurrent (8)</th><th class="n">speedup</th></tr>
<tr><td>vault_lint.py</td><td class="n">1.77 s</td><td class="n">0.90 s</td><td class="n delta">2.0&times;</td></tr>
<tr><td>sweep.py (plan)</td><td class="n">1.64 s</td><td class="n">0.85 s</td><td class="n delta">1.9&times;</td></tr>
<tr><td>sweep.py --apply</td><td class="n">&mdash;</td><td class="n">1.07 s</td><td class="n">&mdash;</td></tr>
<tr class="muted-row"><td>single GET · doctor</td><td class="n">&mdash;</td><td class="n">0.28 s · 0.21 s</td><td class="n">&mdash;</td></tr>
</table>
<div class="note warn"><strong>Before the fix:</strong> the same passes exceeded the
~120-second agent tool ceiling and were terminated mid-run, dropping the session.
The numbers above are the <em>new</em> floor; the practical change is
<strong>fails&nbsp;&rarr;&nbsp;completes</strong>.</div>
</section>
<section>
<h2>Efficiency — entity resolution (v1.2.0)</h2>
<div class="h2sub">Fewer duplicates, less wasted work</div>
<p>Resolution was exact-match only, so a shortened or expanded name returned
<em>nothing</em> — and the writer would create a parallel note. The active project
slugged <code>echo</code> was invisible to the mention &ldquo;echo memory.&rdquo; Now:</p>
<table>
<tr><th>Lookup</th><th>Before</th><th>After</th></tr>
<tr><td>echo memory · echo plugin · echo-memory</td><td><span class="delta" style="color:var(--bad)">no match</span></td><td class="delta">&rarr; projects/active/echo.md</td></tr>
<tr><td>non-aliased shortened name</td><td><span style="color:var(--bad);font-weight:700">no match</span></td><td class="delta">ranked candidates</td></tr>
<tr><td>update under a new name</td><td>second slug, same note</td><td class="delta">canonical slug + alias learned</td></tr>
</table>
<p style="margin-top:10px">Aliases are auto-derived from titles, learned from mentions on
update, and stored in note frontmatter (folded back into the index on sweep) — so the
graph gets <em>better</em> at resolution over time, while exact-match safety prevents
wrong auto-merges. The payoff is avoided duplicate entities and the cleanup/merge passes
they trigger.</p>
</section>
</div>
<div class="foot">
<p><b>Methodology.</b> Measured on the maintainer's workstation against the live
<code>echoapi.alwisp.com</code> endpoint (low latency). The production agent sandbox has
higher per-request connection latency, so the connection-reuse gain — and the reliability
fix — is larger there than the local figures show. The ~1827&times; compound is a model of
the three measured factors (4.5&times; reuse &times; ~2&times; concurrency &times; ~23&times; fewer reads),
not a single end-to-end timing. Stdlib-only; no new dependencies.</p>
<p><b>Artifacts.</b> echo-memory v1.2.0 &mdash; <code>echo-memory-1.2.0.plugin</code>.
33 automated tests pass; vault-health linter clean of regressions.</p>
</div>
</div>
</body>
</html>
Binary file not shown.
+1
View File
@@ -29,6 +29,7 @@ python3 test_features.py # end-to-end features vs the mock (capture/reca
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)
python3 test_multimember.py # 26-check two-member group lifecycle (bootstrap → self-onboard → attributed writes)
# plus, in the plugin tree: scripts/test_chorus_client.py (offline unit + routing-sync guard)
```