1
0
forked from jason/echo
Files
chorus/CHANGELOG.md
T
jason 2ddf199850 Phase 4: shared-write hardening + group lenses
- vault_lock gains bounded retry/backoff (CHORUS_LOCK_RETRIES=4,
  CHORUS_LOCK_BACKOFF=0.5s); atomic_index_update and recall-index upkeep
  retry acquisition — unlocked fallback only after exhausted retries,
  loudly, so an already-written note is never stranded unindexed.
- Duplicate-gate candidates carry the existing note's author; STOP
  message says whose entity the capture collided with.
- recall --author <member> filters hits + linked context to one member's
  notes; recall briefs and --json surface the author: stamp.
- load --all-members appends a group-wide view (every member's scope +
  last-session heartbeat); roster parsing accepts folders as
  trailing-slash file entries (the REST API's actual shape).
- operating-contract.md concurrency section rewritten for N members.
- Two-member e2e suite committed as eval/test_multimember.py (26 checks
  incl. all four Phase-4 behaviors).
- Manifest -> 2.0.0-alpha.4; rebuilt chorus-memory.plugin.

Verified: 25/25 unit, scaffold, routing-sync, 5 mock e2e suites (incl.
new multimember), run_eval metrics unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:29:55 -05:00

301 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
## 2.0.0-alpha.4 — Phase 4 (shared-write hardening + group lenses)
### Added
- **Bounded lock retry on index writes.** `vault_lock` gains `retries`/`backoff`
(linear); `atomic_index_update` and the recall-index upkeep now retry acquisition
(`CHORUS_LOCK_RETRIES`, default 4 / `CHORUS_LOCK_BACKOFF`, default 0.5s). Index
lock holds are sub-second, so a retrying writer virtually always acquires; only
after exhausted retries does it proceed unlocked — loudly — because an
already-written note must never be stranded unindexed.
- **Duplicate-gate attribution.** Gate candidates now carry the existing note's
`author`; the STOP message reads `candidate: acme-rockets -> … (score 1.0,
created by jason)`, so a blocked member knows whose entity they collided with.
- **`recall --author <member>`** — filter hits and linked context to one member's
notes (a lens, not a wall); recall briefs and `--json` summaries now surface the
`author:` stamp alongside updated/status.
- **`load --all-members`** — appends a compact group-wide orientation: every
member's active scope + last-session heartbeat ("what has the team been doing").
- `references/operating-contract.md` concurrency section rewritten for N members
(namespacing as first defense; member-tagged idempotent appends; retried index
lock; member-attributed lock owners).
- Eval: the two-member end-to-end suite is now committed as
`eval/test_multimember.py` (26 checks, incl. the four Phase-4 behaviors).
### Fixed
- `load --all-members` roster parsing accepts folders as trailing-slash entries in
`files` (the shape the REST API actually returns) as well as a `folders` key.
## 2.0.0-alpha.3 — Phase 3 (per-member namespacing — schema 5)
The core of the group conversion: one shared vault, per-member namespaces for all
churn-prone state. A CHORUS vault now bootstraps multi-member from empty.
### Added — vault layout (schema 5)
- **`_agent/members/<member>/`** — each member's `preferences.md`,
`current-context.md` (scope + history), `heartbeat.md` (last-session pointer,
written at session end), and `inbox.md`. Members never contend on each other's
churn-prone files.
- **`_agent/sessions/<member>/YYYY-MM-DD-HHMM-<slug>.md`** — member-namespaced
session logs (no same-minute collisions, filterable per member).
- **`_agent/memory/semantic/group-profile.md`** — shared group facts, conventions,
and a `## Members` roster (members are rostered automatically at onboarding).
- **`inbox/captures/group.md`** — shared inbox for unowned member-tagged items;
whoever triages first routes them.
### Added — behavior
- **Self-onboarding**: `chorus.py load` detects a bootstrapped vault that lacks THIS
member's namespace and stands it up automatically (anchors + sessions dir +
roster line). New members install their baked plugin and just start.
- `load` is now 8 orientation reads: marker → group profile → my preferences /
context / heartbeat → today's shared daily note → my inbox → group inbox; the
heartbeat fallback lists MY sessions dir.
- `scope show/set` operates on the member's own current-context; drift counts only
that member's sessions.
- **Member-tagged shared writes**: daily `## Agent Log` lines and entity-update
dated blocks are written as `- YYYY-MM-DD [member]: …`; `capture --inbox` lands
in the member's own inbox; triage lists BOTH inboxes (tagged mine/group) and
writes member-attributed audit lines.
- `bootstrap.py` seeds group anchors + the configured member's namespace
(`member_bootstrap()` is reusable); the marker is schema 5; `migrate.py` gains a
4→5 step for alpha vaults (moves pre-5 anchors into the configured member's
namespace; no ECHO adoption path).
- `routing.json`: new `member-anchors`, `inbox-group`, member-segmented sessions
routes; the old single-user anchor paths (shared inbox, operator-preferences,
current-context, heartbeat, flat sessions) are **retired**. `vault_lint` runs
aging-inbox and scope-drift checks per member across all members found in the
vault. Docs (SKILL, vault-layout, routing-map, bootstrap) updated to match.
### Verified
- All suites green (25/25 unit, scaffold, routing-sync ×36 routes, 4 mock e2e,
run_eval metrics unchanged) plus a 19-check two-member end-to-end smoke:
jason bootstraps → alice self-onboards via load → interleaved member-tagged
daily log → cross-member entity update attributed → independent scopes →
triage separates personal vs group items → lint clean.
## 2.0.0-alpha.2 — Phase 2 (identity plumbing)
Every write is now attributed to a member. Vault layout unchanged (that's Phase 3).
### Added
- **Config schema is now `{group, member, endpoint, key}`** (was `{owner, endpoint,
key}`). `member` is REQUIRED — a kebab-case slug (`^[a-z0-9][a-z0-9-]{0,31}$`)
identifying who this machine writes as; `group` is the descriptive display name for
the team sharing the vault. Env: `CHORUS_GROUP` / `CHORUS_MEMBER`. `config set`
gains `--group`/`--member` (with slug validation); `config`/`doctor` report both
with sources; an unconfigured member fails loud (`NOT CONFIGURED`, exit 78).
- **`author:` frontmatter on every agent write.** `capture` (`_build_note`) stamps
the configured member; bootstrap stamps seeded anchors via a new `{{MEMBER}}`
substitution; all 8 scaffold templates gain an `author:` line; the canonical
frontmatter block (vault-layout.md, SKILL.md) documents it.
- **`missing-author` lint check** — `/chorus-health` flags any `agent_written: true`
note without an `author:` (the plugin-owned bootstrap marker is exempt).
- **Member-attributed concurrency**: the advisory-lock owner id is now
`<member>-<client>-<pid>` (`auto_owner`), and offline-queue outbox records carry
a `member` field for audit.
- **Per-member baked builds**: `build.py --bake-key` now bakes group/member/endpoint/
key (`--group`/`--member` flags; member required + slug-validated), so each
delivered artifact knows who it writes as.
### Verified
- All suites green (25/25 unit incl. new config/member-validation checks, scaffold,
routing-sync, 4 mock e2e incl. a capture-stamps-author assertion, run_eval metrics
unchanged) plus 8 end-to-end smoke checks (bootstrap stamping, lint flagging,
doctor reporting, NOT-CONFIGURED without member, slug rejection).
## 2.0.0-alpha.1 — CHORUS fork, Phase 1 (mechanical rename)
CHORUS is a group-based fork of ECHO (`jason/echo`, v1.5.1, schema 4 — preserved at the
`echo-fork-point` tag). Phase 1 is the identity rename with no behavior change; see
`CHORUS-PLAN.md` for the full conversion plan.
### Changed
- Plugin renamed `echo-memory` → `chorus-memory`; skill dir, all 16 `echo*.py` modules
(`chorus.py`, `chorus_config.py`, …), `EchoError` → `ChorusError`, the eight slash
commands (`/echo-*` → `/chorus-*`), hook script paths, and all docs/scaffold seeds.
- Env vars renamed `ECHO_*` → `CHORUS_*`; config moves to
`~/.claude/chorus-memory/config.json`; local state dir to `~/.chorus-memory/`;
bootstrap marker to `_agent/chorus-vault.md`; manifest version → 2.0.0-alpha.1.
- Docs' endpoint references now `chorusapi.mpm.to`.
### No ECHO back-compat — clean break
CHORUS deploys as fresh vaults on fresh machines for new users, so it carries **no**
ECHO compatibility: `ECHO_*` env vars, `~/.claude/echo-memory/config.json`,
`~/.echo-memory/`, and a vault's `_agent/echo-vault.md` marker are all ignored.
(Shims for these were built during Phase 1 and then removed by decision.)
### Verified
- 25/25 client unit tests, scaffold checks, routing-doc sync, and all four mock
end-to-end suites green; `run_eval.py` metrics unchanged (0 dupes / 0 false blocks /
0 silent failures / 0 lost offline writes).
## 1.5.1
### Fixed — duplicate gate over-firing on vault-common tokens and cross-kind names
Live 1.5.0 use surfaced a false positive: creating the decision "echo-memory 1.5.0
improvement set" was blocked by the archived project `echo-v-05` (score 1.0), because
`score = overlap / min(|tokens|)` lets any entity whose single distinctive token
appears in the new title score 1.0 — and "echo" appears across many entities in an
echo-centric vault.
New `echo_index.gate_candidates()` (backed by `token_df()`) applies two precision
rules before blocking; `capture` gates through it. `fuzzy_candidates()` — the advisory
warning list — is unchanged:
- **Same-kind only.** A cross-kind name collision (a decision titled after its
project, a meeting named for a company) is intentional naming, not a duplicate.
Cross-kind lookalikes still warn, never block.
- **No single-common-token blocks.** A lone shared token gates only when it is unique
to that entity (vault df == 1). Multi-token overlaps still gate even when the
individual tokens are common.
Exit-76 / `--merge-into` / `--force` semantics unchanged. Tests: 3 new offline unit
tests (`gate_candidates`), end-to-end gate cases restructured + 2 new (cross-kind
no-gate, common-token no-gate); verified against the live vault (the original false
positive now creates cleanly; a same-kind multi-token lookalike still gates).
## 1.5.0
Six improvements from the July 2026 review + the frontmatter-drift field report.
### Fixed — capture-on-update kept only the first body line (silent data loss)
`capture` on an existing entity appended `- <date>: <first line>` and **discarded the
rest of the body**. It now appends the whole body as a dated block — first line on the
bullet, continuation lines indented under it. Idempotency (bullet-per-day) unchanged.
### Added — frontmatter completeness (prevent → tool → detect → remediate)
A field audit found 18/71 entity notes missing `status`/`tags`; the drift was created
at write time and invisible to the tooling. Four coordinated changes, driven by one
data source (`KIND_STATUS` / `KIND_REQUIRED_FM` in `echo_index.py`):
- **`capture`** stamps a kind-appropriate default `status` (all kinds, not just
projects) and seeds `tags` with the note's kind; new `--tags a,b` enriches it.
Reflect/triage proposals accept a `"tags"` field.
- **`echo.py fm`** is now **create-or-replace**: on `400 invalid-target` (missing key)
it surgically inserts the one line into the frontmatter block and re-PUTs, verified —
it previously failed on exactly the notes that needed repair.
- **`vault_lint.py`** gains a kind-scoped `incomplete-frontmatter` check
(missing/empty `status`/`tags` on entity notes; block-style lists handled).
- **`sweep.py --apply`** backfills flagged notes (kind-default status + baseline tag).
### Added — pre-write duplicate gate in capture
A strong fuzzy candidate (score ≥ 0.6, env `ECHO_DUP_GATE`) now **stops** `capture`
before it creates a near-duplicate (exit `76` + candidate list) instead of warning
after the file exists. `--merge-into <slug>` routes the capture as an update to the
existing entity (mention learned as an alias); `--force` creates anyway. Weak
resemblances still create + warn as before.
### Changed — recall corpus + ranking (recall-index schema 2)
- The BM25 corpus now includes **session logs and journal notes** (down-weighted 0.5 /
0.4 so entity notes win ties) — past discussions are findable without having been
promoted into an entity note. `echo.py put` keeps the index current for corpus paths.
- Ranking fuses BM25 with **freshness** (half-life decay on `updated:`, floored at 0.6,
env `ECHO_FRESH_HALF_LIFE`) and **status** (`active` ×1.1, `on-hold` ×0.9,
`archived` ×0.75) — a stale archived note no longer outranks the live one.
- Hits print `updated:`/`status:`; `recall --json` emits structured results.
- The recall index carries per-doc meta (schema 2); an old index is discarded and
rebuilt automatically on the next recall/sweep.
### Added — session hooks (self-firing load & reflect)
New `hooks/hooks.json` + two hook scripts. **SessionStart** runs `echo.py load` and
injects the output as context (skips resume/compact; NOT-CONFIGURED degrades to an
instruction, never an error). **Stop** nudges once per substantive session (≥5 user
turns, env `ECHO_REFLECT_MIN_TURNS`) when nothing was reflected or session-logged —
reflection itself still previews and asks before writing. Both use the 1.4.2 CoWork
path fallback and always exit 0.
### Added — one-tap inbox triage + `--json` on read verbs
New `echo.py triage`: `--list --json` parses the inbox into `{line, date, text,
age_days}`; a proposals file (reflect schema + `"line"`) is classified → previewed →
applied via `capture`, with the `inbox/processing-log/` audit line written per move
(originals never deleted). `/echo-triage` rewritten around it. Also: `--json` for
`recall`, `link`, and `scope show` (`resolve`/`capture` already had it).
Tests: +22 feature cases (mock end-to-end incl. hooks), patch-semantics updated for
the new `fm` contract; all suites green. Docs: SKILL.md, README, command docs updated.
## 1.4.2
### Fixed — CoWork sandbox plugin-path resolution
In a remote CoWork sandbox, `${CLAUDE_PLUGIN_ROOT}` can resolve to a host path the
sandbox can't reach (e.g. `/var/folders/…`), while the plugin is actually mounted under
`…/mnt/.remote-plugins/…`. Every `python3 "${CLAUDE_PLUGIN_ROOT}/…"` invocation then
failed until the agent manually located the real path. (Root cause is the harness env
var, not the plugin — the scripts always used the variable correctly, never a hardcoded
path — but the plugin now self-heals.)
- SKILL.md and all eight slash commands resolve the scripts path with a fallback:
prefer `${CLAUDE_PLUGIN_ROOT}`, else locate the copy under
`/sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/`. Reused via
`$ECHO`/`$LINT`/`$SWEEP`/`$SDIR`; on a normal host the primary path always wins, so
behaviour is unchanged.
- `echo-health` / `echo-sweep` `allowed-tools` broadened to match the resolved
(variable-dir) invocation and the `ls`/`dirname` resolver steps.
## 1.4.1
### Fixed — 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 won over 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.
- Added `baked_complete()`; `source()` reports `baked-in (plugin)` for that tier.
### Security — untracked leaked baked artifacts
- Two labeled baked artifacts (`echo-memory-1.4.0-jason.plugin`,
`echo-memory-1.4.0-gretchen.plugin`), each carrying a live vault bearer token, had
been committed at the repo root. They are now `git rm --cached`-removed and
`.gitignore` gains `echo-memory-*-*.plugin` so labeled builds can't be re-committed
(the version-only pointer `echo-memory.plugin` and `echo-memory-<ver>.plugin` stay
tracked). Tokens remain in prior history — rotate if the repo was ever shared.
## 1.4.0
### Added — per-user baked-key builds (CoWork zero-touch)
The plugin is the only thing reliably mounted into a CoWork session (the sandbox's
`~/.claude` is synthetic, not your real one), so the config now *can* travel inside
the artifact. `echo_config` gains a lowest-priority fallback tier — the
`DEFAULT_OWNER` / `DEFAULT_BASE` / `DEFAULT_KEY` constants — **empty in source**, so
the committed tree still ships zero credentials.
- `build.py --bake-key --from <config.json> [--label <name>]` substitutes a single
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_OWNER/ECHO_BASE/ECHO_KEY`.
- Baked builds default to `dist/` (gitignored) and never touch the shared
`echo-memory.plugin` pointer.
- `build.py --strip-key` now force-blanks the same constants (guaranteed token-free).
- `config show` / `doctor` report `[baked-in default]` when a field resolves from the
baked tier.
### Changed — artifact hygiene
- `.gitignore` excludes `dist/` and the filled-in `echo-memory.config.json`, where
baked builds and the key file are meant to live. A baked artifact carries a vault
key and must be **delivered directly to that one user, never committed, pushed, or
published.** (Note: labeled baked artifacts built at the repo *root* were not yet
guarded — see 1.4.1.)
### Resolution order (unchanged for hosts)
`env` → `$ECHO_CONFIG` file → canonical `~/.claude/echo-memory/config.json` →
**baked `DEFAULT_*`**. On a normal desktop the empty defaults mean behaviour is
identical to 1.3.x; the baked tier only matters in per-user artifacts.