1
0
forked from jason/echo

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>
This commit is contained in:
2026-07-21 15:29:55 -05:00
parent 33a2d73a22
commit 2ddf199850
14 changed files with 302 additions and 38 deletions
@@ -35,11 +35,12 @@ You are an agent operating against an Obsidian vault that functions as a shared,
Assume clients may operate without filesystem access, through the Obsidian Local REST API. Keep paths predictable, frontmatter parseable, titles stable, and all state stored in notes rather than hidden conversation memory. Structure must stay portable: a fresh, empty vault is brought fully online by the plugin's bootstrap (`references/bootstrap.md`), so nothing essential should depend on files existing in the vault ahead of time.
## Concurrency (shared vault)
## Concurrency (N members, one vault)
The vault is a **shared** substrate — Claude Code, CoWork, and other REST API clients may operate on it concurrently. The REST API offers no transactions, so writers coordinate cooperatively:
The vault is a **group** substrate — every member's Claude Code and CoWork sessions may operate on it concurrently, from different machines. The REST API offers no transactions, so safety is layered structurally:
- Single-line, overwrite-style files (`_agent/members/<member>/heartbeat.md`, `current-context.md::Scope`) and append targets (`inbox.md`, `## Agent Log`) assume **one writer at a time**. Two sessions writing at once can clobber or duplicate.
- Before a burst of writes in a session that may overlap another, take the advisory lock (`chorus.py lock <id>``_agent/locks/vault.lock`) and release it at session end. The lock is read-back-confirmed and cooperative with a TTL (stale locks are reclaimable); it is a courtesy, not a hard mutex.
- Idempotent append (read-before-POST, whole-line match, via `chorus.py append`) is the second line of defense against duplicate lines from retries or overlapping sessions.
- **Namespacing is the first defense (schema 5).** All churn-prone single-writer state is per member (`_agent/members/<member>/{preferences,current-context,heartbeat,inbox}.md`, `_agent/sessions/<member>/`), so members never contend on each other's pointers by construction. Only your own sessions write your namespace.
- **What remains shared-write** is designed for interleaving: the daily `## Agent Log` and the group inbox take **member-tagged, whole-line-idempotent appends** (`- YYYY-MM-DD [member]: …` via `chorus.py append` — read-before-POST, exact-line match), so concurrent appends interleave without clobbering or duplicating; shared entity notes take member-tagged dated blocks the same way.
- **The machine indexes** (`_agent/index/*.json`, full-file read-modify-write) are guarded by the advisory lock with **bounded retry/backoff** (`CHORUS_LOCK_RETRIES`, default 4): index writes hold the lock sub-second, so a retrying writer virtually always acquires it, and the update re-reads the index fresh inside the critical section so concurrent captures merge instead of clobber. Only after exhausted retries does it proceed unlocked — loudly — because a note that was already written must never be stranded unindexed.
- The advisory lock (`chorus.py lock <id>``_agent/locks/vault.lock`) is read-back-confirmed and cooperative with a TTL (stale locks are reclaimable). Lock owner ids carry the member (`<member>-<client>-<pid>`), so contention is attributable to a person. For a burst of writes to *shared* files that may overlap another member's session, take it manually and release at session end; it is a courtesy, not a hard mutex.
- Status-check every write. A write that returns `>= 400` did **not** land — surface it rather than assuming success.