1
0
forked from jason/echo

ver 1.3 and 1.3.1

This commit is contained in:
Jason Stedwell
2026-06-23 22:17:39 -05:00
parent 1881d2b105
commit b4605a52f2
53 changed files with 549 additions and 297 deletions
+12 -8
View File
@@ -1,12 +1,12 @@
# echo-memory — v1.2.0
# echo-memory — v1.3.1
Persistent memory for Claude / CoWork sessions via the **ECHO** 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/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`.
Built for **Jason Stedwell** (Director of Technical Services / Systems Engineer, MPM / ALABAMA wISP), who is both the **operator** and the **architect** of this vault. This is a personal plugin, not for distribution.
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](#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.2 in one line:** one-call `capture` routes and crosslinks a memory automatically, `recall` fuses BM25 over note bodies with graph expansion, and alias-aware entity resolution routes shortened names to the right note instead of spawning duplicates — all on a cross-platform, **connection-pooled** Python client (concurrent full-vault reads, so a sweep/lint pass that used to time out now finishes in under a second) with an offline write-ahead queue, a linter-enforced routing manifest, and graph-health checks. See the [version history](#version-history) for how it got here.
**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.
---
@@ -26,11 +26,12 @@ Three consequences follow:
## Configuration
The plugin is hardcoded for a single personal endpoint:
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`):
- **Server:** `https://echoapi.alwisp.com` (reverse proxy → backend Obsidian Local REST API). This is the **only** valid endpoint — never use LAN addresses (`10.x`, `192.168.x`, `:27124`); those belong to retired memory systems. Override per-run with `ECHO_BASE`.
- **Auth:** a bearer token, resolved by `scripts/echo.py` (via `echo_secrets`) in order: **`ECHO_KEY` env → `~/.echo-memory/credentials` → a deprecated baked-in fallback** (warns until you set one of the first two). Set it up per **[API-KEY-SETUP.md](API-KEY-SETUP.md)** (Windows & macOS) or run `echo.py write-key <token>`. **Never** put the key in a vault note.
- **TLS:** the endpoint presents a valid certificate, so `-k` is not needed.
- **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.
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.
@@ -68,7 +69,8 @@ echo-v.05/
│ ├── 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)
│ ├── 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)
@@ -365,6 +367,8 @@ If the API returns a connection error, timeout, or `502` (usually Obsidian / the
| Version | Highlights |
|---------|-----------|
| **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`. |