Jason Stedwell 3b6c3053cb ver 1.5.1 — duplicate-gate precision (common tokens, cross-kind)
Live 1.5.0 use caught the gate blocking a decision note over the single
vault-common token "echo" (min-normalized overlap gives any one-token
entity a 1.0 score). New echo_index.gate_candidates() + token_df():

- gate blocks same-kind candidates only; cross-kind name collisions
  (a decision titled after its project) warn instead of blocking
- a lone shared token blocks only when unique to that entity (df == 1);
  multi-token overlaps still block

fuzzy_candidates (advisory warnings) and exit-76/--merge-into/--force
semantics unchanged. +3 offline unit tests, gate e2e cases restructured
+2 new; verified live both directions. All suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 13:03:07 -05:00
2026-06-20 23:21:40 -05:00
2026-06-26 15:39:17 -05:00
2026-06-25 17:54:03 -05:00
2026-06-25 17:54:03 -05:00
2026-06-25 17:54:03 -05:00
2026-06-25 17:54:03 -05:00
2026-06-25 17:54:03 -05:00
2026-06-26 15:39:17 -05:00
2026-06-26 15:39:17 -05:00
2026-06-22 17:44:37 -05:00
2026-06-22 23:55:19 -05:00
2026-06-22 23:55:19 -05:00
2026-06-22 09:27:36 -05:00

echo-memory — v1.5.1

Persistent memory for Claude / CoWork sessions via the ECHO Obsidian vault, driven over the Obsidian Local REST API. The skill makes direct REST calls through a bundled validated client (scripts/echo.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. As of v1.3 the plugin is user-agnostic: it ships no owner, endpoint, or API key — each machine supplies them through a local config file (see Configuration), so the same plugin works for any owner and any vault.

This repository (jason/echo-v.05) holds the plugin source (tracked tree at echo-memory.plugin.src/), the built echo-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 for how it got here.


Core design principle — the plugin is the single source of truth

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/.

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.

Three consequences follow:

  • 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.

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):

  • 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.

Per-user baked-key builds (v1.4.0) — for CoWork

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:

python build.py --bake-key --from alice-config.json --label alice
# -> dist/echo-memory-1.4.1-alice.plugin  (carries Alice's 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.


Requirements

  • Python 3 in the session environment (stdlib only; invoke as 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.

Repository layout

echo-v.05/
├── echo-memory.plugin              ← built, installable plugin (zip artifact, rebuilt on version bump)
├── echo-memory-<version>.plugin    ← versioned build artifacts (history)
├── eval/                           ← credential-free A/B eval harness (0.6 vs 0.7); not bundled
│   ├── mock_olrapi.py              ← deterministic mock of the REST API + fault injection
│   ├── run_eval.py                 ← orchestrator (runs the real echo.py vs modeled raw curl)
│   └── README.md
└── echo-memory.plugin.src/         ← tracked source tree (the plugin)
    ├── .claude-plugin/plugin.json  ← manifest (name, version, description)
    ├── README.md                   ← plugin-level README
    ├── commands/                   ← slash commands: echo-load, echo-save, echo-recall, echo-triage, echo-health, echo-sweep, echo-reflect, echo-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 + high-level CLI (capture/resolve/recall/link/load/scope/lock/config)
        │   ├── echo_config.py          ← resolves owner/endpoint/key from the machine-local config (env-overridable)
        │   ├── echo_index.py           ← entity index (registry, resolve, name->path map)
        │   ├── echo_links.py           ← cross-link primitives (Related parsing, bidirectional linking)
        │   ├── echo_ops.py             ← high-level ops (capture, recall, resolve, link, agent-log)
        │   ├── routing.json            ← canonical machine-readable route manifest (linter enforces it)
        │   ├── vault_lint.py           ← read-only invariant + graph-health checker (Vault Health)
        │   ├── check_routing.py        ← verifies routing docs stay in sync with routing.json (offline)
        │   ├── test_echo_client.py     ← offline regression tests + the routing-sync guard
        │   ├── bootstrap.py            ← deterministic, idempotent vault setup/repair
        │   ├── migrate.py              ← deterministic schema migration (dry-run by default)
        │   └── sweep.py                ← bring an upgraded vault up to spec (build index + symmetrize links)
        └── 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

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.


Vault layout (data only, root-addressed)

/vault/
├── README.md          ← thin 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/)
└── _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)

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

Operating procedures (logic)

Cold-start loading

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 switchecho.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.


Write-safety rules

  • 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.

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):

---
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]]
---

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).
  • ## Observationstimestamped 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.
  • Repairbootstrap.py again: GET-probes each path and creates only what's missing; malformed files are flagged, never replaced.
  • Migrationsmigrate.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 specsweep.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.


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 triageecho.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.
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) Concurrencyvault_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 captureecho.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). (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).
S
Description
ECHO — persistent memory for Claude/CoWork sessions in an Obsidian vault via the Local REST API. Pure-Python plugin: user-agnostic, self-bootstrapping, one-call capture/recall.
Readme 4 MiB
1.5.1 Latest
2026-07-03 14:17:07 -05:00
Languages
Python 95.6%
HTML 4.2%
Shell 0.2%