16 KiB
echo-memory — Roadmap to v1.0
Status: draft / in progress. Current shipping version: 0.9.0 (schema 3). Target: 1.0.0 (schema 4) — "memory you can actually trust and retrieve."
This roadmap turns the 10 code-review suggestions into a sequenced plan and tracks the
scaffold for each. It is the single index: every v1.0 workstream maps to one or more
files under echo-memory.plugin.src/skills/echo-memory/scripts/ (plus tests and CI).
1.0 thesis
0.x made the plugin correct and self-describing: status-checked writes, idempotent
appends, a routing manifest the linter enforces, an entity index, and one-call
capture/recall. 1.0 makes it trustworthy and genuinely recall-driven:
- You can retrieve a fact even when you phrase the query differently than you stored it. (H1)
- Nothing is lost when the backend is down. (H2)
- Two clients writing at once cannot corrupt the index or clobber state. (H3)
- Every feature is proven by tests that model the real API, gated in CI. (H4)
- Memory accrues on its own instead of only when the agent remembers to write. (H5)
…on a base that is secure (M1), clean-linking (M2), self-diagnosing (M3), machine-parseable (M4), and maintainable (M5).
Non-negotiable constraints (these bound every solution)
These are inherited from the 0.x design and must not regress:
| Constraint | Consequence for 1.0 |
|---|---|
| Pure-Python stdlib, no third-party deps | H1 recall = local BM25 + graph fusion, not an embedding API. No numpy, keyring, requests. |
| Cross-platform (Win/macOS/Linux) | No bash, no platform date; pathlib, os, UTF-8-safe streams. |
| Plugin is the single source of truth; vault holds data only | Derived artifacts (recall index) live in _agent/index/ (machine-maintained, rebuildable) — never new control docs in the vault. |
| Fail loud, never silent | New write paths keep status-checking and non-zero exits. |
| Local state can't depend on the vault being up | H2 queue/cache lives in ECHO_STATE_DIR (default ~/.echo-memory/), not the vault. |
Build order vs. scaffold order
The user asked to scaffold heaviest → lightest (H1→M5). That is the order the files below are stubbed. The recommended build/merge order is different — safety and proof first, so the heavy features land on a tested, secure base:
Phase A (land first): M1 secrets · H4 tests+CI
Phase B (correctness): H3 concurrency · M2 link/slug quality
Phase C (durability): H2 offline queue + cache
Phase D (intelligence): H1 hybrid recall · H5 reflection capture
Phase E (polish): M3 doctor · M4 --json · M5 hygiene → tag 1.0.0
Scaffolds are independent stubs, so stubbing heaviest-first is safe; only the
integration (wiring into echo.py, schema bump, migration) follows Phase order.
Workstreams (heaviest → lightest)
Each item: goal · why · scope · files · storage/schema · acceptance · depends-on · lift.
H1 — Hybrid/semantic recall · lift: ●●●●●
- Goal:
recall "X"returns relevant notes even when the wording differs, ranked, with scored multi-hop graph expansion. - Why: Recall is the product. Today it's keyword
/search/simple+ fixed one-hop (echo_ops.py:88). Differently-phrased memory is unfindable → dead weight. - Scope: local BM25 index over note bodies (stdlib only); fuse lexical score + graph distance (decay per hop); relevance threshold; configurable hops.
- Files:
scripts/echo_recall.py(new) → later replacesecho_ops.recall. - Storage/schema:
_agent/index/recall-index.json(postings + doc stats), machine-maintained, rebuilt bysweep.py. Schema 3→4. - Acceptance: retrieval precision/recall measured in eval beats keyword-only baseline; recall returns scored, deduped hits + neighbourhood; index rebuildable offline.
- Depends-on: entity index (have), H4 eval harness for the precision/recall numbers.
H2 — Offline durability: write-ahead queue + read cache · lift: ●●●●●
- Goal: No durable write is lost when the API/Obsidian is down; cold-start degrades to last-known context instead of "no memory."
- Why: Today vault-unreachable = silently proceed without memory (
SKILL.md:383). 502 (Obsidian not running) is the most common real failure. - Scope: append-only NDJSON outbox; replay on next reachable session (idempotency makes replay safe); last-known-good read cache for
load. - Files:
scripts/echo_queue.py(new); integration hooksafe_request()wrappingecho.request. - Storage/schema: local
ECHO_STATE_DIR/outbox.ndjson+ECHO_STATE_DIR/cache/. No vault schema change. - Acceptance: kill the API mid-session → writes queue; restart → replay lands exactly once (no dupes);
loadserves cache when offline and says so. - Depends-on: existing idempotent-append discipline; H4 for fault-injection tests.
H3 — Concurrency correctness · lift: ●●●●○
- Goal: Make the advertised multi-writer (Claude + CoWork) story actually safe.
- Why: Index is full-file load→PUT with no lock (
echo_index.py:105,echo_ops.py:230) → concurrentcapturesilently drops an entity. Lock is manual/advisory only (echo.py:293). - Scope: auto-lock context manager around
capture/scope set/index writes; atomic index update (re-read under lock, merge, save); per-resource locks; crash-safe TTL reclaim (have). - Files:
scripts/echo_concurrency.py(new); refactorecho_index.save+echo_ops.captureto route through it. - Storage/schema: reuse
_agent/locks/. No schema change. - Acceptance: two concurrent captures both land in the index; no lost entries; lock auto-released on normal + error exit.
- Depends-on: none (pure refactor); H4 to prove it.
H4 — High-fidelity tests + CI + current eval · lift: ●●●●○
- Goal: Prove every 0.9 + 1.0 feature against an API model that reproduces real failure modes; gate on CI.
- Why: Mock PATCH is "naive append" (
mock_olrapi.py:138) — can't model heading replace/insert; eval still benchmarks 0.6 vs 0.7 (two versions stale). - Scope: higher-fidelity mock (real heading/frontmatter PATCH semantics) or containerized real Obsidian Local REST API; refresh eval to measure retrieval precision/recall, link correctness, dup rate; wire
test_echo_client.py+check_routing.py+test_features.py+ new module tests into GitHub Actions. - Files:
.github/workflows/ci.yml(new);eval/mock_olrapi_hifi.py(new);scripts/test_v1_scaffold.py(interface guard, new); refreshedeval/run_eval.py. - Acceptance: CI green on every push; eval reports current-version metrics; mock reproduces the 40080 invalid-target and replace/insert paths.
- Depends-on: none — foundational, build in Phase A.
H5 — Automatic session-reflection capture · lift: ●●●●○
- Goal: At session end, propose durable captures extracted from the conversation for one-tap confirm — memory that fills itself.
- Why: Memory only accrues when the agent decides to write. "As useful as possible" means not depending on that judgment firing.
- Scope: model-side extraction emits a JSON proposal set; script dedups against the entity index, previews, and applies on confirm (respects "show before large writes" safety rule).
- Files:
scripts/echo_reflect.py(new);/echo-reflectcommand (later). - Storage/schema: none new (routes through
capture). - Acceptance: given a transcript, proposals are deduped vs index, previewed, and only applied on explicit confirm; nothing written without go-ahead.
- Depends-on: H1 index/recall for dedup quality; H3 for safe concurrent apply.
M1 — Harden secret handling · lift: ●●○○○ (do now — Phase A)
- Goal: Stop shipping a live bearer token in source, docs, and every
.pluginzip. - Why: Token hardcoded at
echo.py:70and repeated throughapi-reference.md/README — committed to git, only rotatable by rebuild. Violates the plugin's own "never store secrets" rule. - Scope: resolve key env → local
ECHO_STATE_DIR/credentials(0600) → deprecated baked default with a loud warning; scrub literal from docs to a placeholder; document rotation. - Files:
scripts/echo_secrets.py(new);echo.pyKEY=→echo_secrets.resolve_key(). - Acceptance: no live token in tracked source/docs/package;
ECHO_KEYstill overrides; rotation documented. - Depends-on: none.
M2 — Tame auto-link false positives & slug collisions · lift: ●●○○○
- Goal: Keep the graph (which recall depends on) clean.
- Why: Auto-link fires on any ≥3-char name/alias word-match (
echo_ops.py:236) → a concept "API" or alias "rs" links everywhere;slugifytruncates to 40 chars with no collision check (echo_index.py:58) → distinct titles silently share a note. - Scope: raise alias floor / require multi-token or kind-aware match / stopword guard; collision-aware slug disambiguation in
derive_path. - Files:
scripts/echo_quality.py(new); patches toecho_links/echo_index. - Acceptance: known false-positive cases no longer link; colliding titles get distinct slugs; covered by tests.
- Depends-on: none.
M3 — echo.py doctor + complete load fallback · lift: ●●○○○
- Goal: One-call readiness check; make
loadself-sufficient. - Why: No single "is everything OK" check.
cmd_loadreads the heartbeat but never falls back to listing_agent/sessions/thoughSKILL.md:122documents it (echo.py:395). - Scope: doctor = Python version + reachability + auth + marker/
schema_version+ lint summary, green/red;loadlists recent sessions when heartbeat missing/stale. - Files:
scripts/echo_doctor.py(new); patchecho.cmd_load. - Acceptance:
doctorprints actionable status and exits non-zero on any red;loadorients with no heartbeat. - Depends-on: none.
M4 — Machine output (--json) + --dry-run · lift: ●○○○○
- Goal: Let the agent act on structured results and preview writes.
- Why:
capture/recall/resolve/scopeprint prose; agent re-parses free text. - Scope:
--jsonresult envelope (action, path, links_added, hits);--dry-runforcapture/scope set. - Files:
scripts/echo_output.py(new helpers); thread throughecho_ops/echo.py. - Acceptance: every high-level op emits valid JSON under
--json;--dry-runwrites nothing. - Depends-on: none.
M5 — Repo hygiene & manifest completeness · lift: ●○○○○
- Goal: Remove "which tree is canonical?" traps; complete the manifest.
- Why: Two plugin trees (
codex plugin/vsecho-memory.plugin.src/) have already drifted (codex lacks the 0.9 modules); stack of versioned.pluginartifacts; sparseplugin.json. - Scope: consolidate/mark canonical tree; prune old artifacts; add
license/homepage/command registration toplugin.json; refresh version-history + eval references. - Files:
MAINTENANCE.md(new checklist);plugin.json. - Acceptance: one canonical source tree; complete manifest; docs reference current version.
- Depends-on: none.
Schema 3 → 4 migration (owned by H1/H2)
- Add
_agent/index/recall-index.json(H1), rebuilt bysweep.py. migrate.py: add[3→4]step (create recall-index placeholder; no destructive moves).routing.json: add a route for_agent/index/recall-index.json(already covered by the generic^_agent/index/[^/]+\.json$route — verify incheck_routing.py).- Bump
CURRENT_SCHEMAto 4 insweep.py/migrate.py;vault_lint.pyunaffected.
Scaffold status
| # | Workstream | Lift | Scaffold file(s) | State |
|---|---|---|---|---|
| H1 | Hybrid recall | ●●●●● | scripts/echo_recall.py |
✅ WIRED — BM25 + vault crawl + recall-index.json persistence + graph fusion; maintained on capture, rebuilt by sweep; replaces echo_ops.recall; schema 4. Covered by test_features.py. |
| H2 | Offline queue/cache | ●●●●● | scripts/echo_queue.py, eval/test_offline_queue.py |
✅ WIRED — write verbs (put/post/append/patch/delete) queue on outage and report "queued"; flush (+ flush-on-load) replays idempotently and re-bases to the current endpoint (survives base migration); load degrades to last-known-good cache and flags OFFLINE. Proven end-to-end (down→queue→up→flush→land, idempotent, cache fallback). Follow-up: route capture's internal index/link writes through the queue for full offline-atomic captures. |
| H3 | Concurrency | ●●●●○ | scripts/echo_concurrency.py |
✅ WIRED — vault_lock CM (auto acquire/release, crash-safe, advisory) + atomic_index_update (lock + fresh re-read-merge). capture entity write and echo_recall.update_note both routed through it. Proven by test_features.py (merge-no-clobber + lock-release). Follow-up: deeper resolve-level title-collision detection; true concurrent stress test. |
| H4 | Tests + CI | ●●●●○ | .github/workflows/ci.yml, eval/mock_olrapi_hifi.py, eval/test_patch_semantics.py, scripts/test_v1_scaffold.py |
✅ hi-fi PATCH mock + semantics test + CI matrix (Win/macOS/Linux × 3.10/3.12) live. Remaining: refresh run_eval.py (still 0.6-vs-0.7) → publish current metrics. |
| H5 | Reflection capture | ●●●●○ | scripts/echo_reflect.py, eval/test_reflect.py, commands/echo-reflect.md |
✅ WIRED — validate/classify/preview/apply + echo.py reflect (dry-run unless --apply, reads file or stdin) + /echo-reflect command. Dedups proposals vs the entity index, drops below-confidence, routes inbox, applies via capture (indexed/linked/logged, self-lock-guarded). Proven by test_reflect.py. |
| M1 | Secret handling | ●●○○○ | scripts/echo_secrets.py |
✅ WIRED — resolve_key (env → ~/.echo-memory/credentials → deprecated fallback + loud warning), write-key CLI, docs scrubbed. Live token now only in echo.py DEFAULT_KEY (remove at 1.0; operator env already sets ECHO_KEY). |
| M2 | Link/slug quality | ●●○○○ | scripts/echo_quality.py |
✅ WIRED — is_confident_link gates capture auto-linking (rejects <4-char / common tokens, trusts multi-word names); safe_slug disambiguates a colliding 40-char-truncated slug in the create path. Follow-up: resolve-level title collision (two long titles → same slug → false merge). |
| M3 | Doctor + load fallback | ●●○○○ | scripts/echo_doctor.py, commands/echo-doctor.md |
✅ WIRED — echo.py doctor (Python/reachability/auth/bootstrap/schema/key-source) + /echo-doctor; cmd_load now falls back to a recent-sessions listing when the heartbeat pointer is absent. Verified against the live vault. |
| M4 | --json / --dry-run |
●○○○○ | scripts/echo_output.py |
✅ WIRED — capture --json emits a clean result envelope (helper chatter redirected) and --dry-run previews the create/update plan without writing; resolve already emits JSON. Proven by test_features.py. Follow-up: --json for recall/scope/link. |
| M5 | Hygiene | ●○○○○ | plugin.json, .gitignore, MAINTENANCE.md |
✅ DONE — plugin.json → 1.0.0 + license; .gitignore (.DS_Store, __pycache__, local .echo-memory/ state, eval output). Remaining (operator judgment, not code): consolidate/retire the drifted codex plugin/ tree and prune old .plugin artifacts — see MAINTENANCE.md. |
Wiring: scaffolds are not imported by the live echo.py yet — 0.9.0 behavior is unchanged. Integration happens per Phase, each gated by its tests (H4).