1
0
forked from jason/echo
Files
chorus/CHORUS-PLAN.md
T
jason f2bfd70dd4 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>
2026-07-21 15:41:58 -05:00

14 KiB
Raw Blame History

CHORUS — Group-Based Memory: Conversion Plan

Fork of: jason/echo (ECHO v1.5.1, schema 4) → jason/chorus Goal: convert the single-operator ECHO memory plugin into CHORUS — one shared Obsidian vault serving a group of members, with per-member identity, attribution, and isolation of the churn-prone per-person state, while keeping the shared knowledge graph (projects, areas, resources, decisions) communal.

Naming metaphor: ECHO is one voice reflected back; CHORUS is many voices in one performance.


1. What the review found

ECHO is ~4,800 LOC of pure-stdlib Python (18 scripts) + a large doc surface (SKILL.md ~456 lines, 6 references, 8 commands, 15 scaffold seeds). The architecture is already favorable for this conversion:

  • Identity is barely wired in. echo.py loads OWNER from config but never actually consumes it — attribution today is purely doc-driven ("write in third person about Jason"). Adding a member identity breaks nothing.
  • Routing is data-driven. routing.json is the single source of truth for "what may be written where"; vault_lint.py and check_routing.py enforce it. Per-member paths are new route rows, not new code paths.
  • The vault is self-describing. Bootstrap/migrate/sweep + schema_version in the marker give us a clean upgrade lever (schema 4 → 5).

The hard part is not identity plumbing — it's the five shared single-writer artifacts that make no sense (or clobber) with multiple concurrent users:

Artifact Today Problem in a group
_agent/memory/semantic/operator-preferences.md One profile Whose preferences?
_agent/context/current-context.md One active scope for the vault Members work on different things simultaneously
_agent/heartbeat/last-session.md One "last session" pointer (PUT overwrite) Race: lost updates; meaningless shared
inbox/captures/inbox.md One shared inbox (POST append) Append races; triage can't tell whose capture
journal/daily/YYYY-MM-DD.md ## Agent Log One shared heading Duplicate-heading races; unattributed lines

Also single-user by design: session filenames carry no author (YYYY-MM-DD-HHMM-<slug>.md — two members can collide on the same minute), frontmatter has no author: field anywhere, the offline queue is per-machine with no member attribution, and the advisory lock owner is a session id, not a person. Concurrency is advisory-only — vault_lock() warns and proceeds if it can't acquire (echo_concurrency.py:7881), which is acceptable for one person on two machines but not for N people.


2. Target design

2.1 Identity model

  • Config gains a member field: { "group": "...", "member": "jason", "endpoint": "https://chorusapi.mpm.to", "key": "..." }. Deployment endpoint: https://chorusapi.mpm.to — the CHORUS vault gets its own Obsidian + Local REST API instance exposed there (separate from ECHO's echoapi.alwisp.com; the personal ECHO vault is untouched). The endpoint stays machine-local/baked config, never in source, same as ECHO. ownergroup. Resolution order unchanged: baked > env (CHORUS_MEMBER etc.) > config file. Baked per-user builds (build.py --bake-key --label alice) already exist and become the primary onboarding mechanism: one artifact per member, member id baked in.
  • Every agent-written note gets author: <member> stamped in frontmatter (echo_ops._build_note), added to the canonical frontmatter block, to KIND_REQUIRED_FM for agent-written kinds, linted by vault_lint, and backfilled by sweep --apply (existing notes → the migrating member).
  • Lock owner and offline-queue entries carry the member id (auto_owner()f"{member}-{client}-{pid}"), so contention and replay are attributable.

Auth reality check: the Obsidian Local REST API has one bearer key per vault. Phase 14 ship with a shared key and honor-system attribution (fine for a trusted team). Real per-member credentials/revocation would need a thin auth proxy in front of chorusapi.mpm.to that maps per-member tokens → the vault key and can inject/verify the member id. That's deliberately out of scope here but the config shape (member + per-member baked artifacts) is designed so the proxy slots in later without another migration.

2.2 Vault layout changes (schema 5)

Per-member state moves under a member namespace; shared knowledge stays where it is.

_agent/
├── chorus-vault.md            ← renamed marker (schema_version: 5, managed_by: chorus-memory-plugin)
├── members/
│   └── <member>/
│       ├── preferences.md     ← was operator-preferences.md (one per member)
│       ├── current-context.md ← per-member scope + Scope History
│       ├── heartbeat.md       ← per-member last-session pointer
│       └── inbox.md           ← per-member quick-capture inbox
├── memory/
│   └── semantic/
│       └── group-profile.md   ← NEW: shared facts about the group itself
├── sessions/
│   └── <member>/YYYY-MM-DD-HHMM-<slug>.md   ← member-namespaced session logs
└── (context/, memory/, health/, index/, locks/, templates/, skills/ unchanged — shared)
  • Shared and unchanged: projects/, areas/, resources/, decisions/, journal/, semantic/episodic/working memory (now with author:), the entity index, recall index, health notes.
  • Daily Agent Log stays one shared heading (it's the group's timeline) but every line is member-prefixed: - HH:MM [alice] did X. ensure_daily_log keeps its GET→POST-heading→PATCH flow; idempotent whole-line append plus the member prefix makes interleaving safe and attributed. (Per-member daily notes were considered and rejected — the whole point of a group vault is one shared timeline.)
  • Inbox: per-member capture files (_agent/members/<id>/inbox.md) kill the append race and make triage member-aware, plus one shared inbox/captures/group.md for unowned "someone should look at this" items (member-prefixed lines; routed by whoever triages first). Triage covers both: accepted items route to shared homes; preference-type items route to that member's preferences.md. Processing-log lines carry the member id.
  • resources/people/<member>.md continues to exist for every member — the public "who is this person" note, distinct from their private-ish preferences.md (how the agent should behave for them).

2.3 Cold-start load (per member)

chorus.py load reads: marker → group-profilemy preferences → my current-context → my heartbeat/last-session (fallback: my sessions dir) → today's shared daily note → my inbox depth → group inbox depth. Optional --all-members flag for a group-wide orientation (e.g., "what has the team been doing"). Reconcile (inbox aging, scope drift) runs against the member's own state only.

2.4 Concurrency posture

Namespacing removes ~all of the contention (each member's churn-prone files are theirs alone). What remains shared-write:

  • Entity + recall indexes — keep vault_lock + atomic_index_update, but flip the lock to required (required=True) on index writes: block/retry briefly instead of warn-and-proceed. TTL reclaim unchanged.
  • Daily note / shared notes — idempotent member-prefixed appends; PATCH to headings is last-writer-wins per line, which is acceptable for log lines.
  • Duplicate gate — unchanged and global (dedup across the group is the feature: two members shouldn't create parallel notes for the same client). Gate messages name the author of the existing candidate.

2.5 Recall / reflection

  • Corpus unchanged (shared graph is the point). recall gains an optional --author <member> filter and surfaces author: in hits.
  • reflect proposals route preference items to the current member's preferences.md; everything else routes to shared homes with author: stamped. The Stop-hook nudge state stays per-machine (already is).

3. Renaming map (echo → chorus)

Category From To
Plugin echo-memory (plugin.json, skill dir, SKILL.md name) chorus-memory
Commands /echo-{load,save,recall,reflect,triage,sweep,health,doctor} /chorus-*
Scripts echo*.py (16 modules + imports everywhere) chorus*.py
Exception EchoError ChorusError
Env vars ECHO_OWNER/BASE/KEY/CONFIG/TODAY/VERIFY/LOCK_TTL/TIMEOUT/WORKERS/STATE_DIR/CLIENT/DUP_GATE/REFLECT_MIN_TURNS/FRESH_HALF_LIFE CHORUS_* (+ new CHORUS_MEMBER)
Config file ~/.claude/echo-memory/config.json ~/.claude/chorus-memory/config.json
State dir ~/.echo-memory/ ~/.chorus-memory/
Marker _agent/echo-vault.md (hardcoded in 8 files + routing.json) _agent/chorus-vault.md
Hooks hooks.json paths ×2 incl. CoWork fallback glob updated paths
Docs/branding README, SKILL.md, references, scaffold seeds, icons rewrite pass (cosmetic but large)

operator-preferences.md is renamed as part of the layout change (§2.2), not the branding pass.


4. Phased plan

Phase 0 — Baseline (½ day)

Repo hygiene on the fork: strip stale versioned .plugin artifacts from the tree (move to Gitea releases per ROADMAP-2.0), confirm eval/ suites green as-is, tag the fork point.

Phase 1 — Mechanical rename (12 days) — DONE

Everything in §3. All existing tests pass under the new names. No behavior change. Deliverable: installable chorus-memory plugin that is ECHO-with-a-new-name. (Decided post-Phase-1: CHORUS is a clean break — fresh vaults on fresh machines for new users, so no ECHO back-compat: legacy ECHO_* env vars, echo-memory configs, ~/.echo-memory state, and _agent/echo-vault.md markers are all ignored. Shims briefly existed and were removed.)

Phase 2 — Identity plumbing (12 days) — DONE

member in config (+ CHORUS_MEMBER, baked-tier support, config set --member with slug validation; ownergroup; member REQUIRED for is_configured), MEMBER global in chorus.py, author: stamping in _build_note + bootstrap {{MEMBER}} anchor stamping + templates, canonical frontmatter docs + a missing-author lint check (agent_written notes must carry author:; sweep does NOT backfill — attribution can't be guessed), member-aware lock owner (<member>-<client>-<pid>) and queue records, doctor reports member/group. Vault layout untouched. Deliverable: every new write is attributed; single-user behavior otherwise identical.

Phase 3 — Per-member namespacing (35 days — the core) — DONE

Schema 5. New LEAVES + seeds (members/<id>/…, group-profile.md), member-parameterized bootstrap (bootstraps your member subtree on first load if the vault exists but your namespace doesn't — new members self-onboard), cmd_load/cmd_scope/heartbeat/inbox/triage/reflect retargeted to member paths, session logs under sessions/<member>/, member-prefixed Agent Log lines, routing.json route rows (with <member> pattern segment) + retired old paths, lint updates, migrate.py 4→5 (for any alpha CHORUS vaults stood up between phases — there is no ECHO-vault adoption path; CHORUS vaults start fresh). Docs: vault-layout.md, routing-map.md, bootstrap.md rewritten. Deliverable: a real multi-member vault, bootstrapped from empty.

Phase 4 — Shared-write hardening (12 days) — DONE

Lock on index writes with bounded retry/backoff (unlocked fallback only after exhausted retries, loudly — a written note must never go unindexed), gate 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) — 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 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).


5. Decisions (settled with Jason, 2026-07-21)

  1. Journal rollups — shared. One group rollup per period (journal/weekly/YYYY-Www.md etc.) with member-attributed bullets inside. No per-member rollup trees.
  2. Preference privacy — group-visible, documented. Member preferences.md files live in the shared vault and are readable by every member (one vault, one key — no read ACLs possible). SKILL.md documents this explicitly; anything a member considers private belongs in their own personal ECHO vault, not CHORUS.
  3. Auth proxy — out of scope; shared key first. Phases 15 ship with the shared vault key and honor-system attribution. The per-member token proxy (namespace write ACL, server-side audit, per-member revocation) is a later standalone project; the config shape (member + per-member baked artifacts) already accommodates it with zero plugin changes.
  4. Commands — /chorus-*. Full-prefix names (/chorus-load, /chorus-save, …), consistent with the ECHO pattern and collision-free if both plugins are installed.
  5. Inboxes — per-member + shared group inbox. Each member gets _agent/members/<id>/inbox.md; a shared inbox/captures/group.md holds unowned items (member-prefixed lines, idempotent appends), routed by whoever triages first. Both are probed at load and covered by triage.
  6. Clean break from ECHO (added post-Phase-1). CHORUS vaults are fresh vaults on fresh machines for new users — no ECHO adoption path, no legacy ECHO_* env/config/state/marker compatibility. Jason's personal ECHO vault continues unchanged on its own plugin and endpoint. If ECHO history is ever wanted in CHORUS, it would be a one-off import exercise, out of scope.