From 1c0c2ea66e1434e491a92ccbc6f205828a4bb445 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 22 Jun 2026 09:27:36 -0500 Subject: [PATCH] ver 1.0 --- .github/workflows/ci.yml | 48 +++ .gitignore | 17 + API-KEY-SETUP.md | 119 ++++++ MAINTENANCE.md | 64 ++++ README.md | 20 +- ROADMAP-1.0.md | 176 +++++++++ build.py | 132 +++++++ echo-memory-1.0.0.plugin | Bin 0 -> 109610 bytes echo-memory.plugin | Bin 84445 -> 109610 bytes .../.claude-plugin/plugin.json | 5 +- .../commands/echo-doctor.md | 15 + .../commands/echo-reflect.md | 30 ++ .../skills/echo-memory/SKILL.md | 9 +- .../echo-memory/references/api-reference.md | 22 +- .../echo-memory/references/bootstrap.md | 2 +- .../skills/echo-memory/scripts/echo.py | 140 ++++++- .../echo-memory/scripts/echo_concurrency.py | 82 ++++ .../skills/echo-memory/scripts/echo_doctor.py | 71 ++++ .../skills/echo-memory/scripts/echo_ops.py | 196 +++++----- .../skills/echo-memory/scripts/echo_output.py | 47 +++ .../echo-memory/scripts/echo_quality.py | 55 +++ .../skills/echo-memory/scripts/echo_queue.py | 191 ++++++++++ .../skills/echo-memory/scripts/echo_recall.py | 349 ++++++++++++++++++ .../echo-memory/scripts/echo_reflect.py | 136 +++++++ .../echo-memory/scripts/echo_secrets.py | 83 +++++ .../skills/echo-memory/scripts/migrate.py | 7 +- .../skills/echo-memory/scripts/sweep.py | 11 +- .../echo-memory/scripts/test_v1_scaffold.py | 112 ++++++ eval/mock_olrapi_hifi.py | 135 +++++++ eval/run_eval.py | 2 +- eval/test_features.py | 49 ++- eval/test_offline_queue.py | 126 +++++++ eval/test_patch_semantics.py | 121 ++++++ eval/test_reflect.py | 107 ++++++ 34 files changed, 2530 insertions(+), 149 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 API-KEY-SETUP.md create mode 100644 MAINTENANCE.md create mode 100644 ROADMAP-1.0.md create mode 100644 build.py create mode 100644 echo-memory-1.0.0.plugin create mode 100644 echo-memory.plugin.src/commands/echo-doctor.md create mode 100644 echo-memory.plugin.src/commands/echo-reflect.md create mode 100644 echo-memory.plugin.src/skills/echo-memory/scripts/echo_concurrency.py create mode 100644 echo-memory.plugin.src/skills/echo-memory/scripts/echo_doctor.py create mode 100644 echo-memory.plugin.src/skills/echo-memory/scripts/echo_output.py create mode 100644 echo-memory.plugin.src/skills/echo-memory/scripts/echo_quality.py create mode 100644 echo-memory.plugin.src/skills/echo-memory/scripts/echo_queue.py create mode 100644 echo-memory.plugin.src/skills/echo-memory/scripts/echo_recall.py create mode 100644 echo-memory.plugin.src/skills/echo-memory/scripts/echo_reflect.py create mode 100644 echo-memory.plugin.src/skills/echo-memory/scripts/echo_secrets.py create mode 100644 echo-memory.plugin.src/skills/echo-memory/scripts/test_v1_scaffold.py create mode 100644 eval/mock_olrapi_hifi.py create mode 100644 eval/test_offline_queue.py create mode 100644 eval/test_patch_semantics.py create mode 100644 eval/test_reflect.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d3a1b34 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: ci + +# H4 — gate every push on the credential-free, offline test suite. +# No secrets, no live vault: everything runs against the mock OLRAPI. + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10", "3.12"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Routing docs in sync with routing.json (offline) + run: python echo-memory.plugin.src/skills/echo-memory/scripts/check_routing.py + + - name: Interface guard for v1.0 scaffolds + run: python echo-memory.plugin.src/skills/echo-memory/scripts/test_v1_scaffold.py + + - name: Client regression tests + run: python echo-memory.plugin.src/skills/echo-memory/scripts/test_echo_client.py + + - name: Feature integration tests (mock OLRAPI) + run: python eval/test_features.py + + - name: PATCH-semantics tests (hi-fi mock) + run: python eval/test_patch_semantics.py + + - name: Offline write-ahead queue + cache tests (H2) + run: python eval/test_offline_queue.py + + - name: Session-reflection capture tests (H5) + run: python eval/test_reflect.py + + # TODO (H4): fold the hi-fi mock into the main eval and publish the refreshed + # eval/results/latest.json (currently still a 0.6-vs-0.7 baseline) as an artifact: + # python eval/run_eval.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..708bdf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# macOS / editor cruft +.DS_Store + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +venv/ + +# Local ECHO state (never commit — holds the offline queue + cached vault reads, +# and may hold the API key in ~/.echo-memory/credentials when not using ECHO_KEY) +.echo-memory/ + +# Eval output +eval/results/*.json +!eval/results/.gitkeep diff --git a/API-KEY-SETUP.md b/API-KEY-SETUP.md new file mode 100644 index 0000000..2db3799 --- /dev/null +++ b/API-KEY-SETUP.md @@ -0,0 +1,119 @@ +# ECHO — API key setup (Windows & macOS) + +The ECHO plugin authenticates to the vault with a bearer token. `echo.py` resolves it in +this order — **first hit wins**: + +1. **`ECHO_KEY` environment variable** ← recommended for workstations +2. **`~/.echo-memory/credentials`** file (one `key=…` line, `chmod 600`) +3. a deprecated baked-in fallback (prints a warning on every call until you do #1 or #2) + +You only need **one** of these. Pick the env var (below) or the credentials file +(`python3 echo.py write-key `, cross-platform, no shell config needed). + +> Replace `` everywhere below with your actual token. **Never** paste +> the token into a vault note, a committed file, or a screen-share. + +--- + +## Option A — credentials file (simplest, cross-platform) + +```bash +# from the plugin's scripts dir; works identically on Windows/macOS/Linux +python3 "/skills/echo-memory/scripts/echo.py" write-key +``` + +Writes `~/.echo-memory/credentials` (mode 600 on Unix). Nothing else to configure. +To rotate later, run it again with the new token. + +--- + +## Option B — environment variable + +### Windows + +**PowerShell — persistent (recommended).** Sets it for your user account; open a **new** +terminal afterward (the current session won't see it): + +```powershell +[Environment]::SetEnvironmentVariable("ECHO_KEY", "", "User") +``` + +**Command Prompt — persistent** (alternative; note `setx` truncates values over 1024 chars, +so the 64-char ECHO key is fine): + +```cmd +setx ECHO_KEY "" +``` + +**Current session only** (not persistent): + +```powershell +$env:ECHO_KEY = "" # PowerShell +``` +```cmd +set ECHO_KEY= REM Command Prompt +``` + +**GUI alternative:** Start → "Edit the system environment variables" → *Environment +Variables…* → under *User variables* click *New…* → Name `ECHO_KEY`, Value the token. + +**Verify** (in a NEW terminal): +```powershell +echo $env:ECHO_KEY # PowerShell +``` +```cmd +echo %ECHO_KEY% :: Command Prompt +``` + +### macOS + +Modern macOS uses **zsh**. Add the export to your shell profile, then reload it: + +```zsh +echo 'export ECHO_KEY=""' >> ~/.zshrc +source ~/.zshrc +``` + +If you use **bash** instead: +```bash +echo 'export ECHO_KEY=""' >> ~/.bash_profile +source ~/.bash_profile +``` + +**For GUI apps** (a desktop app that doesn't read your shell profile) you can also set it +for the login session: +```bash +launchctl setenv ECHO_KEY "" +``` +(Not persistent across reboot on its own; the shell-profile export above covers terminal +sessions, which is what the plugin uses.) + +**Verify** (in a NEW terminal): +```bash +echo "$ECHO_KEY" +``` + +--- + +## Confirm it works + +```bash +python3 "/skills/echo-memory/scripts/echo.py" doctor +``` + +`doctor` reports the Python version, vault reachability, auth, bootstrap/schema, and the +**API key source** — it should say `ECHO_KEY env` or `credentials file`, not the deprecated +baked-in fallback. + +--- + +## Security notes + +- The token is a vault credential — treat it like a password. Don't commit it, don't store + it in a vault note, rotate it if it leaks. +- `ECHO_KEY` (or `ECHO_BASE`) set in the environment **always overrides** the credentials + file and the baked-in default, so it's safe for CI / one-off overrides: + `ECHO_KEY=… python3 echo.py …`. +- Once you've set the key via Option A or B on every machine/agent that runs the plugin + (terminal, CoWork, any scheduled run), the deprecated baked-in fallback can be removed + from `scripts/echo.py` (`DEFAULT_KEY`) so no token lives in the source tree at all. diff --git a/MAINTENANCE.md b/MAINTENANCE.md new file mode 100644 index 0000000..9c3e638 --- /dev/null +++ b/MAINTENANCE.md @@ -0,0 +1,64 @@ +# echo-memory — Maintenance & repo hygiene (M5) + +Low-lift cleanups that remove "which copy is canonical?" traps before tagging 1.0. +None of these change runtime behavior; they reduce drift and confusion. + +## Canonical source tree + +- **`echo-memory.plugin.src/`** is the single canonical source tree. ✅ +- **`codex plugin/`** has already drifted — it lacks the 0.9 modules + (`echo_index.py`, `echo_links.py`, `echo_ops.py`, `sweep.py`, `check_routing.py`). + - [ ] Decide: regenerate it from the canonical tree as a build target, **or** delete it + and document the codex/CoWork packaging step instead. + - [ ] Until resolved, add a `codex plugin/README.md` note: "derived — do not edit by hand." + +## Build artifacts + +The repo root carries every historical build (`echo-memory-0.6.0.plugin` … +`echo-memory-0.9.0.plugin`) plus `echo-memory.plugin`. +- [ ] Keep only the latest `echo-memory.plugin` (the installable) tracked; move versioned + zips to GitHub Releases (or a `dist/` that is git-ignored). +- [ ] Add `*.plugin` build outputs to `.gitignore` except the current pointer, if desired. +- [x] `.gitignore` added (`.DS_Store`, `__pycache__`, local `.echo-memory/`, eval output). + +## `plugin.json` completeness + +- [x] Add `license` (UNLICENSED — personal, not for distribution). +- [x] Version bumped to **1.0.0**. +- [ ] Add `homepage` / repository URL (optional — repo URL not yet decided). +- [ ] (Optional) register slash commands in the manifest; they're auto-discovered from + `commands/` today (`echo-load|save|recall|triage|health|sweep|reflect|doctor`). + +## Docs freshness + +- [ ] `eval/run_eval.py` still benchmarks **0.6 vs 0.7** — refresh to current (open H4 follow-up). +- [x] README version-history has a **1.0.0** entry; title bumped to v1.0.0. +- [x] Scrub the literal bearer token from docs → `$ECHO_KEY` placeholder (M1, done: + `api-reference.md` ×11, `SKILL.md`, `bootstrap.md`, eval harness). Operator key setup + documented in `API-KEY-SETUP.md`. Last remaining copy is `echo.py` `DEFAULT_KEY` + (deprecated fallback) — **remove once `ECHO_KEY` is set in every environment**. + +## Building the `.plugin` artifact + +`python build.py` zips `echo-memory.plugin.src/` into `echo-memory-.plugin` (version +read from the manifest) and refreshes the `echo-memory.plugin` pointer. Deterministic +(sorted entries + fixed timestamp → byte-identical rebuilds). Flags: + +- `--strip-key` — blank `echo.py` `DEFAULT_KEY` so **no token ships** (runtime then needs + `ECHO_KEY` or `~/.echo-memory/credentials`). Use this once the key is set everywhere. +- `--no-pointer` — skip refreshing `echo-memory.plugin`. +- `--outdir DIR` — write artifacts elsewhere. + +Without `--strip-key` the build warns that the baked-in token is present. + +## Pre-1.0 release checklist + +- [x] All 10 roadmap items wired; scaffold TODOs cleared (see `ROADMAP-1.0.md` status table). +- [x] `schema_version` bumped to 4 (sweep/migrate); `migrate.py` `[3→4]` step added. + *Verify on a copy of the live vault before migrating it (it currently reports schema 3).* +- [x] Offline suites green in CI on Win/macOS/Linux × Py 3.10/3.12 (check_routing, + test_echo_client, test_v1_scaffold, test_features, test_patch_semantics, + test_offline_queue, test_reflect). +- [ ] No live secret in the tracked tree or the built `.plugin` — **one line left**: delete + `echo.py` `DEFAULT_KEY` after `ECHO_KEY`/credentials are set everywhere (per `API-KEY-SETUP.md`). +- [ ] `eval/run_eval.py` reports current-version retrieval/durability metrics (open follow-up). diff --git a/README.md b/README.md index bf0d007..e68d479 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# echo-memory — v0.9.0 +# echo-memory — v1.0.0 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). No MCP server — the skill makes direct REST calls, now 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`. @@ -6,7 +6,7 @@ Built for **Jason Stedwell** (Director of Technical Services / Systems Engineer, 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. -**0.7 in one line:** the prose-and-raw-curl skill grew an executable spine — a status-checking API client, a machine-readable routing manifest the linter enforces, deterministic bootstrap/migrate scripts, an advisory multi-writer lock, four slash commands, and an eval harness. See the [0.7.0 version-history entry](#version-history) for the full list. +**0.9 in one line:** one-call `capture` routes and crosslinks a memory automatically, `recall` returns a topic plus its linked neighbourhood, and a machine-maintained entity index makes routing an alias-aware lookup — all on a cross-platform Python client with a linter-enforced routing manifest and graph-health checks. See the [version history](#version-history) for how it got here. --- @@ -29,7 +29,7 @@ Three consequences follow: The plugin is hardcoded for a single personal endpoint: - **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 stored in the skill / `references` and injected by `scripts/echo.py`. The key lives only in the plugin — **never inside a vault note** (per the vault's own safety rules). Override with `ECHO_KEY`. +- **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 `. **Never** put the key in a vault note. - **TLS:** the endpoint presents a valid certificate, so `-k` is not needed. 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. @@ -57,7 +57,7 @@ echo-v.05/ └── 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-triage, echo-health + ├── commands/ ← slash commands: echo-load, echo-save, echo-recall, echo-triage, echo-health, echo-sweep └── skills/echo-memory/ ├── SKILL.md ← operating procedure (authoritative) ├── references/ @@ -330,12 +330,13 @@ Observations are promoted into Fact / Pattern (dropping the date) once a pattern ## 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. In 0.7 these are scripts (`scripts/bootstrap.py`, `scripts/migrate.py`), not hand-run curl loops — the prose in `references/bootstrap.md` documents what they do and serves as the fallback. +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. - **Repair** — `bootstrap.py` again: GET-probes each path and creates only what's missing; malformed files are flagged, never replaced. -- **Migrations** — `migrate.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/`. +- **Migrations** — `migrate.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 spec** — `sweep.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. --- @@ -364,13 +365,14 @@ If the API returns a connection error, timeout, or `502` (usually Obsidian / the | Version | Highlights | |---------|-----------| -| **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`). | -| **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). | +| **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`. | | **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.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.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`). | diff --git a/ROADMAP-1.0.md b/ROADMAP-1.0.md new file mode 100644 index 0000000..2ba46d4 --- /dev/null +++ b/ROADMAP-1.0.md @@ -0,0 +1,176 @@ +# 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**: + +1. You can **retrieve** a fact even when you phrase the query differently than you stored it. *(H1)* +2. Nothing is **lost** when the backend is down. *(H2)* +3. Two clients writing at once **cannot corrupt** the index or clobber state. *(H3)* +4. Every feature is **proven** by tests that model the real API, gated in CI. *(H4)* +5. 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 replaces `echo_ops.recall`. +- **Storage/schema:** `_agent/index/recall-index.json` (postings + doc stats), machine-maintained, rebuilt by `sweep.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 hook `safe_request()` wrapping `echo.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); `load` serves 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`) → concurrent `capture` silently 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); refactor `echo_index.save` + `echo_ops.capture` to 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); refreshed `eval/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-reflect` command (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 `.plugin` zip. +- **Why:** Token hardcoded at `echo.py:70` and repeated through `api-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.py` `KEY=` → `echo_secrets.resolve_key()`. +- **Acceptance:** no live token in tracked source/docs/package; `ECHO_KEY` still 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; `slugify` truncates 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 to `echo_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 `load` self-sufficient. +- **Why:** No single "is everything OK" check. `cmd_load` reads the heartbeat but never falls back to listing `_agent/sessions/` though `SKILL.md:122` documents it (`echo.py:395`). +- **Scope:** doctor = Python version + reachability + auth + marker/`schema_version` + lint summary, green/red; `load` lists recent sessions when heartbeat missing/stale. +- **Files:** `scripts/echo_doctor.py` (new); patch `echo.cmd_load`. +- **Acceptance:** `doctor` prints actionable status and exits non-zero on any red; `load` orients 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/scope` print prose; agent re-parses free text. +- **Scope:** `--json` result envelope (action, path, links_added, hits); `--dry-run` for `capture`/`scope set`. +- **Files:** `scripts/echo_output.py` (new helpers); thread through `echo_ops`/`echo.py`. +- **Acceptance:** every high-level op emits valid JSON under `--json`; `--dry-run` writes 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/` vs `echo-memory.plugin.src/`) have already drifted (codex lacks the 0.9 modules); stack of versioned `.plugin` artifacts; sparse `plugin.json`. +- **Scope:** consolidate/mark canonical tree; prune old artifacts; add `license`/`homepage`/command registration to `plugin.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 by `sweep.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 in `check_routing.py`). +- Bump `CURRENT_SCHEMA` to 4 in `sweep.py`/`migrate.py`; `vault_lint.py` unaffected. + +--- + +## 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). diff --git a/build.py b/build.py new file mode 100644 index 0000000..dc5f781 --- /dev/null +++ b/build.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""build.py — package the echo-memory plugin source into a .plugin artifact. + +Zips the CONTENTS of echo-memory.plugin.src/ at the archive root (the layout the plugin +loader expects: .claude-plugin/plugin.json, commands/, skills/ all at top level), excluding +dev cruft. The version is read from the manifest, so the output is named automatically. + +Usage: + python build.py # build echo-memory-.plugin + refresh echo-memory.plugin + python build.py --strip-key # ALSO blank echo.py's DEFAULT_KEY -> a token-free artifact + # (requires ECHO_KEY or ~/.echo-memory/credentials at runtime) + python build.py --no-pointer # don't update the echo-memory.plugin "current" pointer + python build.py --outdir dist # write artifacts somewhere other than the repo root + +Deterministic: entries are sorted and stamped with a fixed timestamp, so an unchanged +source tree always produces a byte-identical archive (clean diffs / reproducible builds). +Pure stdlib; runs the same on Windows/macOS/Linux. +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +import zipfile +from pathlib import Path + +# Keep non-ASCII (em-dashes in our messages) from raising/garbling on a legacy console. +for _stream in (sys.stdout, sys.stderr): + try: + _stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr] + except Exception: + pass + +REPO = Path(__file__).resolve().parent +SRC = REPO / "echo-memory.plugin.src" +MANIFEST = "/".join([".claude-plugin", "plugin.json"]) +ECHO_PY = "/".join(["skills", "echo-memory", "scripts", "echo.py"]) + +EXCLUDE_DIRS = {"__pycache__", ".git"} +EXCLUDE_NAMES = {".DS_Store"} +EXCLUDE_SUFFIXES = {".pyc", ".pyo"} +FIXED_DATE = (2026, 1, 1, 0, 0, 0) # stable timestamp for reproducible archives + +_KEY_RE = re.compile(r'(DEFAULT_KEY\s*=\s*")([0-9a-fA-F]{16,})(")') + + +def included_files() -> list[Path]: + out = [] + for p in sorted(SRC.rglob("*")): + if p.is_dir(): + continue + if any(part in EXCLUDE_DIRS for part in p.relative_to(SRC).parts): + continue + if p.name in EXCLUDE_NAMES or p.suffix in EXCLUDE_SUFFIXES: + continue + out.append(p) + return out + + +def file_bytes(path: Path, arcname: str, strip_key: bool) -> bytes: + data = path.read_bytes() + if strip_key and arcname == ECHO_PY: + text = data.decode("utf-8") + new, n = _KEY_RE.subn(r'\1\3', text) # collapse the hex value to "" + if n: + text = new.replace('DEFAULT_KEY = ""', + 'DEFAULT_KEY = "" # stripped at build time — set ECHO_KEY or run write-key') + data = text.encode("utf-8") + return data + + +def token_present(path: Path) -> bool: + m = _KEY_RE.search(path.read_text(encoding="utf-8")) + return bool(m and m.group(2)) + + +def build(out: Path, files: list[Path], strip_key: bool) -> int: + if out.exists(): + out.unlink() + with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z: + for p in files: + arc = p.relative_to(SRC).as_posix() + info = zipfile.ZipInfo(arc, date_time=FIXED_DATE) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = 0o644 << 16 + z.writestr(info, file_bytes(p, arc, strip_key)) + return out.stat().st_size + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description="Build the echo-memory .plugin artifact") + ap.add_argument("--strip-key", action="store_true", + help="blank echo.py DEFAULT_KEY so no token ships in the artifact") + ap.add_argument("--no-pointer", action="store_true", + help="do not refresh the echo-memory.plugin 'current' pointer") + ap.add_argument("--outdir", default=str(REPO), help="output directory (default: repo root)") + args = ap.parse_args(argv) + + if not SRC.is_dir(): + print(f"build: source tree not found at {SRC}", file=sys.stderr) + return 1 + + manifest_path = SRC / ".claude-plugin" / "plugin.json" + version = json.loads(manifest_path.read_text(encoding="utf-8"))["version"] + files = included_files() + arcnames = {p.relative_to(SRC).as_posix() for p in files} + if MANIFEST not in arcnames: + print(f"build: {MANIFEST} missing from the archive root — aborting", file=sys.stderr) + return 1 + + outdir = Path(args.outdir) + outdir.mkdir(parents=True, exist_ok=True) + targets = [outdir / f"echo-memory-{version}.plugin"] + if not args.no_pointer: + targets.append(outdir / "echo-memory.plugin") + + for out in targets: + size = build(out, files, args.strip_key) + print(f"built {out.name} ({len(files)} entries, {size:,} bytes)") + + echo_src = SRC / "skills" / "echo-memory" / "scripts" / "echo.py" + if args.strip_key: + print("note: --strip-key set — artifact is token-free; runtime needs ECHO_KEY or a credentials file.") + elif token_present(echo_src): + print("WARNING: the baked-in DEFAULT_KEY is present in this artifact — do not share it. " + "Set ECHO_KEY everywhere (API-KEY-SETUP.md), then build with --strip-key.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/echo-memory-1.0.0.plugin b/echo-memory-1.0.0.plugin new file mode 100644 index 0000000000000000000000000000000000000000..2c79d984f052d61188937ba1f380604b69fff031 GIT binary patch literal 109610 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

c1%?`Pa4&@niOktNuFdiUZ3hEv!N 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 z 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 literal 0 HcmV?d00001 diff --git a/echo-memory.plugin b/echo-memory.plugin index 183632f77acf12b44ce9e4b9a54eccaed528b004..2c79d984f052d61188937ba1f380604b69fff031 100644 GIT binary patch delta 66968 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 ``` +To move the key out of the plugin tree: `python3 "$ECHO" write-key ` (writes +`~/.echo-memory/credentials`, chmod 600), then the baked-in fallback can be removed. + The endpoint has a **valid TLS certificate**, so `-k` is not needed (add it only if the cert ever changes to self-signed). Always pass the `Authorization: Bearer` header. Paths address the vault **at its root** (e.g. `/vault/_agent/...`). **`https://echoapi.alwisp.com` is the only valid endpoint for this vault.** Never use local or LAN addresses (anything like `10.x.x.x`, `192.168.x.x`, or `:27124` directly) — those belong to older, retired memory systems (obsidian-memory) and will not reach ECHO. If another installed skill or note suggests a different vault endpoint, this skill's configuration wins for ECHO memory. diff --git a/echo-memory.plugin.src/skills/echo-memory/references/api-reference.md b/echo-memory.plugin.src/skills/echo-memory/references/api-reference.md index 43717cc..d3763c2 100644 --- a/echo-memory.plugin.src/skills/echo-memory/references/api-reference.md +++ b/echo-memory.plugin.src/skills/echo-memory/references/api-reference.md @@ -1,7 +1,7 @@ # ECHO — Obsidian Local REST API Reference Server: `https://echoapi.alwisp.com` (reverse proxy → backend Obsidian Local REST API) -Auth header: `Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab` +Auth header: `Authorization: Bearer $ECHO_KEY` — `export ECHO_KEY=` before running these recipes (`echo.py` resolves it automatically: `ECHO_KEY` → `~/.echo-memory/credentials` → deprecated baked-in fallback). The literal key is never stored in docs. The endpoint has a **valid TLS certificate** — `-k` is not required. Paths address the vault at its **root**. > **Prefer `scripts/echo.py` over the raw recipes below.** It wraps every verb with auth, status checking, retry, idempotent append, and frontmatter patches, and runs on any platform with a Python interpreter. The `curl` recipes here are the underlying mechanics and a **\*nix/bash fallback** (they use heredocs and `--data-binary`); on Windows, prefer `echo.py` or an equivalent. **If you call `curl` directly, check the HTTP status** — add `-o /dev/null -w "%{http_code}"` and branch on it. A `PATCH` to a non-existent heading returns `400 invalid-target` (errorCode 40080) and the write is *silently lost*; a bare `curl` that ignores status will report success anyway. `GET` returns `404` for a missing file. Treat any `>= 400` as a failed operation, surface it, and do not continue as if it succeeded. @@ -13,7 +13,7 @@ The endpoint has a **valid TLS certificate** — `-k` is not required. Paths add ```bash # Read any file by vault path curl -s \ - -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \ + -H "Authorization: Bearer $ECHO_KEY" \ "https://echoapi.alwisp.com/vault/_agent/context/current-context.md" ``` @@ -22,7 +22,7 @@ Returns raw file content (text/markdown). On 404, the file does not exist. ```bash # Read a specific heading's content only curl -s \ - -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \ + -H "Authorization: Bearer $ECHO_KEY" \ "https://echoapi.alwisp.com/vault/_agent/memory/semantic/operator-preferences.md/heading/Operator" ``` @@ -38,7 +38,7 @@ Nested headings: separate levels with `::` (URL-encode spaces as `%20`): ```bash # List contents of a directory (trailing slash required) curl -s \ - -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \ + -H "Authorization: Bearer $ECHO_KEY" \ "https://echoapi.alwisp.com/vault/_agent/sessions/" ``` @@ -56,7 +56,7 @@ cat > /tmp/obs_entry.md << 'OBSEOF' OBSEOF curl -s -X POST \ - -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \ + -H "Authorization: Bearer $ECHO_KEY" \ -H "Content-Type: text/markdown" \ --data-binary @/tmp/obs_entry.md \ "https://echoapi.alwisp.com/vault/inbox/captures/inbox.md" @@ -87,7 +87,7 @@ source_notes: [] OBSEOF curl -s -X PUT \ - -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \ + -H "Authorization: Bearer $ECHO_KEY" \ -H "Content-Type: text/markdown" \ --data-binary @/tmp/obs_file.md \ "https://echoapi.alwisp.com/vault/_agent/sessions/2026-06-05-1430-my-session.md" @@ -109,7 +109,7 @@ Jason prefers concise status updates — lead with the decision. OBSEOF curl -s -X PATCH \ - -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \ + -H "Authorization: Bearer $ECHO_KEY" \ -H "Operation: append" \ -H "Target-Type: heading" \ -H "Target: Operator Preferences::Fact / Pattern" \ @@ -124,7 +124,7 @@ When unsure of the exact heading path, GET the note with the document-map Accept ```bash curl -s \ - -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \ + -H "Authorization: Bearer $ECHO_KEY" \ -H "Accept: application/vnd.olrapi.document-map+json" \ "https://echoapi.alwisp.com/vault/_agent/memory/semantic/operator-preferences.md" ``` @@ -143,7 +143,7 @@ Same call with `Operation: prepend`. ```bash curl -s -X PATCH \ - -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \ + -H "Authorization: Bearer $ECHO_KEY" \ -H "Operation: replace" \ -H "Target-Type: frontmatter" \ -H "Target: updated" \ @@ -158,7 +158,7 @@ curl -s -X PATCH \ ```bash curl -s -X POST \ - -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \ + -H "Authorization: Bearer $ECHO_KEY" \ "https://echoapi.alwisp.com/search/simple/?query=weekly+review" ``` @@ -170,7 +170,7 @@ Returns an array of `{ filename, score, matches: [{ context, match }] }`. ```bash curl -s -X DELETE \ - -H "Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" \ + -H "Authorization: Bearer $ECHO_KEY" \ "https://echoapi.alwisp.com/vault/inbox/imports/old-note.md" ``` diff --git a/echo-memory.plugin.src/skills/echo-memory/references/bootstrap.md b/echo-memory.plugin.src/skills/echo-memory/references/bootstrap.md index 0df40d4..4159e55 100644 --- a/echo-memory.plugin.src/skills/echo-memory/references/bootstrap.md +++ b/echo-memory.plugin.src/skills/echo-memory/references/bootstrap.md @@ -20,7 +20,7 @@ python3 "$SCRIPTS/migrate.py" --apply # perform the migration (moves/delet `bootstrap.py` writes through `echo.py`, so every step is status-checked and the marker is written last. The scripts are pure Python (only an interpreter required — no bash, no platform-specific `date`), so they run identically on Windows, macOS, and Linux. The manual steps below document what the script does (and serve as a fallback if the script can't run). ``` -AUTH="Authorization: Bearer 241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" +AUTH="Authorization: Bearer $ECHO_KEY" # export ECHO_KEY first; echo.py resolves it automatically BASE="https://echoapi.alwisp.com" ``` diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo.py index 87d6730..703bb2f 100644 --- a/echo-memory.plugin.src/skills/echo-memory/scripts/echo.py +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo.py @@ -66,9 +66,14 @@ for _stream in (sys.stdout, sys.stderr): pass BASE = os.environ.get("ECHO_BASE", "https://echoapi.alwisp.com").rstrip("/") -# The token stays hardcoded for this personal plugin; ECHO_KEY env still overrides. + +# Key resolution is centralized in echo_secrets: ECHO_KEY env -> ~/.echo-memory/credentials +# -> the deprecated baked-in fallback below. Move the key out of the tree with +# `echo.py write-key `; remove DEFAULT_KEY before the 1.0 tag (see ROADMAP-1.0.md > M1). +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import echo_secrets # noqa: E402 DEFAULT_KEY = "241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" -KEY = os.environ.get("ECHO_KEY") or DEFAULT_KEY +KEY = echo_secrets.resolve_key(DEFAULT_KEY) VERIFY = os.environ.get("ECHO_VERIFY", "1") != "0" LOCK_TTL = int(os.environ.get("ECHO_LOCK_TTL", "900")) @@ -207,10 +212,20 @@ def cmd_search(query: list[str]) -> int: return 0 +def _qwrite(method: str, url: str, data: bytes | None = None, headers: dict | None = None): + """A mutating request that survives an outage: routes through echo_queue.safe_request, + which enqueues the write and returns (202, ...) when the vault is unreachable (H2).""" + import echo_queue + return echo_queue.safe_request(method, url, data=data, headers=headers) + + def cmd_put(path: str, file_arg: str | None) -> int: data = read_body(file_arg) - status, body = request("PUT", vault_url(path), data=data, + status, body = _qwrite("PUT", vault_url(path), data=data, headers={"Content-Type": "text/markdown"}) + if status == 202: + print(f"queued (offline): PUT {path} — will sync on the next reachable session") + return 0 check(status, body, f"put {path}") if VERIFY: status, _ = request("GET", vault_url(path)) @@ -222,8 +237,11 @@ def cmd_put(path: str, file_arg: str | None) -> int: def cmd_post(path: str, file_arg: str | None) -> int: data = read_body(file_arg) - status, body = request("POST", vault_url(path), data=data, + status, body = _qwrite("POST", vault_url(path), data=data, headers={"Content-Type": "text/markdown"}) + if status == 202: + print(f"queued (offline): POST {path} — will sync on the next reachable session") + return 0 check(status, body, f"post {path}") print(f"ok: POST {path}") return 0 @@ -238,10 +256,15 @@ def cmd_append(path: str, line: str) -> int: if line.rstrip("\r") in existing: print(f"skip: line already present in {path}") return 0 + elif status == 0: + pass # offline: skip the idempotency read; queue the POST (flush dedups by content) elif status != 404: check(status, body, f"append(read) {path}") - status, body = request("POST", vault_url(path), data=f"{line}\n".encode(), + status, body = _qwrite("POST", vault_url(path), data=f"{line}\n".encode(), headers={"Content-Type": "text/markdown"}) + if status == 202: + print(f"queued (offline): APPEND {path} — will sync on the next reachable session") + return 0 check(status, body, f"append {path}") print(f"ok: APPEND {path}") return 0 @@ -253,7 +276,7 @@ def cmd_patch(path: str, op: str, target_type: str, target: str, file_arg: str | if target_type not in {"heading", "frontmatter", "block"}: raise EchoError("patch: target-type must be heading, frontmatter, or block", 2) data = normalize_patch_body(read_body(file_arg), op, target_type) - status, body = request( + status, body = _qwrite( "PATCH", vault_url(path), data=data, @@ -264,6 +287,9 @@ def cmd_patch(path: str, op: str, target_type: str, target: str, file_arg: str | "Content-Type": "application/json" if target_type == "frontmatter" else "text/markdown", }, ) + if status == 202: + print(f"queued (offline): PATCH {op} '{target}' -> {path} — will sync on the next reachable session") + return 0 check(status, body, f"patch {path} ({target})") print(f"ok: PATCH {op} {target_type} '{target}' -> {path}") return 0 @@ -274,7 +300,10 @@ def cmd_fm(path: str, field: str, value: str) -> int: def cmd_delete(path: str) -> int: - status, body = request("DELETE", vault_url(path)) + status, body = _qwrite("DELETE", vault_url(path)) + if status == 202: + print(f"queued (offline): DELETE {path} — will sync on the next reachable session") + return 0 check(status, body, f"delete {path}") print(f"ok: DELETE {path}") return 0 @@ -290,7 +319,7 @@ def parse_lock_time(value: str) -> int: return int(time.time()) -def cmd_lock(owner: str) -> int: +def cmd_lock(owner: str, quiet: bool = False) -> int: status, body = request("GET", vault_url(LOCK_PATH)) if status == 200 and body.strip(): current = body.decode(errors="replace").strip() @@ -310,11 +339,12 @@ def cmd_lock(owner: str) -> int: if held_owner != owner: print(f"lock race: now held by '{held_owner}', not '{owner}' — backing off", file=sys.stderr) return 75 - print(f"ok: locked by {owner}") + if not quiet: + print(f"ok: locked by {owner}") return 0 -def cmd_unlock(owner: str) -> int: +def cmd_unlock(owner: str, quiet: bool = False) -> int: status, body = request("GET", vault_url(LOCK_PATH)) if status == 200: held_owner = body.decode(errors="replace").split(" @ ", 1)[0].strip() @@ -324,7 +354,8 @@ def cmd_unlock(owner: str) -> int: status, body = request("DELETE", vault_url(LOCK_PATH)) if status != 404: check(status, body, "unlock") - print("ok: unlocked") + if not quiet: + print("ok: unlocked") return 0 @@ -403,22 +434,67 @@ def cmd_load() -> int: ("today", f"journal/daily/{today()}.md"), ("inbox", "inbox/captures/inbox.md"), ] + import echo_queue + # H2: sync any writes queued during a prior outage, best-effort and quiet on empty. + try: + synced = echo_queue.flush() + if synced: + print(f"(synced {synced} write(s) queued during a prior offline session)\n") + except Exception as exc: # noqa: BLE001 — never let queue upkeep block a load + print(f"(queue flush skipped: {exc})\n", file=sys.stderr) + marker_missing = False + heartbeat_absent = False + offline = False for label, path in targets: status, body = request("GET", vault_url(path)) - print(f"===== {label}: {path} (HTTP {status}) =====") if status == 200: + echo_queue.cache_put(path, body) # refresh last-known-good for offline fallback + print(f"===== {label}: {path} (HTTP 200) =====") text = body.decode(errors="replace") sys.stdout.write(text) if not text.endswith("\n"): sys.stdout.write("\n") elif status == 404: + print(f"===== {label}: {path} (HTTP 404) =====") print("(absent — fine)") if label == "marker": marker_missing = True + if label == "heartbeat": + heartbeat_absent = True + elif status == 0: # vault unreachable -> degrade to last-known-good cache + offline = True + cached = echo_queue.cache_get(path) + if cached is not None: + print(f"===== {label}: {path} (OFFLINE — serving stale cache) =====") + sys.stdout.write(cached.decode(errors="replace")) + if not cached.endswith(b"\n"): + sys.stdout.write("\n") + else: + print(f"===== {label}: {path} (OFFLINE — no cache) =====") + print("(unreachable and not cached)") else: + print(f"===== {label}: {path} (HTTP {status}) =====") print(f"(error HTTP {status}: {body.decode(errors='replace')[:200]})") print() + # M3: heartbeat pointer missing/stale -> fall back to the recent sessions listing + # (matches the documented loading procedure) so orientation works without the pointer. + if heartbeat_absent and not offline: + st, body = request("GET", vault_url("_agent/sessions/")) + if st == 200: + try: + files = sorted((f for f in json.loads(body).get("files", []) if f.endswith(".md")), + reverse=True) + except json.JSONDecodeError: + files = [] + if files: + print("===== recent sessions (heartbeat absent — fallback) =====") + for f in files[:5]: + print(f" _agent/sessions/{f}") + print() + if offline: + print("NOTE: vault unreachable — context above is last-known-good cache; writes this " + "session will be queued and synced when the vault returns.") if marker_missing: print("NOTE: marker absent — vault not bootstrapped. Run bootstrap.py before relying on memory.") return 0 @@ -428,6 +504,8 @@ def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Validated cross-platform ECHO vault client") sub = parser.add_subparsers(dest="cmd", required=True) sub.add_parser("load") + sub.add_parser("flush") # H2: replay writes queued during a prior outage + sub.add_parser("doctor") # M3: one-call readiness check sub.add_parser("get").add_argument("path") sub.add_parser("map").add_argument("path") sub.add_parser("ls").add_argument("path") @@ -443,9 +521,13 @@ def build_parser() -> argparse.ArgumentParser: sub.add_parser("delete").add_argument("path") sub.add_parser("lock").add_argument("owner") sub.add_parser("unlock").add_argument("owner") + sub.add_parser("write-key").add_argument("token") # M1: move the key to ~/.echo-memory/credentials p = sub.add_parser("scope") p.add_argument("subcommand", nargs="?", default="show", choices=["show", "set"]) p.add_argument("text", nargs="?") + p = sub.add_parser("reflect") # H5: apply a JSON proposal set (dry-run unless --apply) + p.add_argument("file", nargs="?") + p.add_argument("--apply", action="store_true") # High-level operations (entity index + cross-links + recall). See echo_ops.py. sub.add_parser("resolve").add_argument("mention", nargs="+") @@ -462,6 +544,8 @@ def build_parser() -> argparse.ArgumentParser: p.add_argument("--date") p.add_argument("--domain", default="business") p.add_argument("--no-log", action="store_true") + p.add_argument("--json", action="store_true") # M4: emit a JSON result envelope + p.add_argument("--dry-run", action="store_true") # M4: preview the plan, write nothing return parser @@ -470,6 +554,25 @@ def main(argv: list[str] | None = None) -> int: try: if args.cmd == "load": return cmd_load() + if args.cmd == "doctor": + import echo_doctor + return echo_doctor.run() + if args.cmd == "flush": + import echo_queue + n = echo_queue.flush() + remaining = len(echo_queue.pending()) + if n: + print(f"ok: flushed {n} queued write(s)" + + (f"; {remaining} still queued (vault unreachable)" if remaining else "")) + elif remaining: + print(f"ok: {remaining} write(s) still queued (vault unreachable)") + else: + print("ok: queue empty") + return 0 + if args.cmd == "write-key": + path = echo_secrets.write_key(args.token) + print(f"ok: wrote credentials to {path} (chmod 600 best-effort); ECHO_KEY env still overrides") + return 0 if args.cmd == "get": return cmd_get(args.path) if args.cmd == "map": @@ -498,6 +601,16 @@ def main(argv: list[str] | None = None) -> int: return cmd_unlock(args.owner) if args.cmd == "scope": return cmd_scope(args.subcommand, args.text) + if args.cmd == "reflect": + import echo_reflect + raw = read_body(args.file).decode("utf-8", errors="replace") + try: + proposals = json.loads(raw) + except json.JSONDecodeError as exc: + raise EchoError(f"reflect: proposals must be a JSON array ({exc})", 2) + if not isinstance(proposals, list): + raise EchoError("reflect: proposals must be a JSON array of objects", 2) + return echo_reflect.apply(proposals, confirm=args.apply) if args.cmd in ("resolve", "recall", "link", "capture"): import echo_ops # lazy: only the high-level ops pull in the index/link modules if args.cmd == "resolve": @@ -510,7 +623,8 @@ def main(argv: list[str] | None = None) -> int: args.kind, args.title, args.file, status_v=args.status, aliases=args.aliases.split(",") if args.aliases else [], sources=args.source.split(",") if args.source else [], - date=args.date, domain=args.domain, inbox=args.inbox, no_log=args.no_log) + date=args.date, domain=args.domain, inbox=args.inbox, no_log=args.no_log, + as_json=args.json, dry_run=args.dry_run) except EchoError as exc: print(f"echo.py: {exc}", file=sys.stderr) return exc.code diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_concurrency.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_concurrency.py new file mode 100644 index 0000000..de03332 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_concurrency.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""echo_concurrency.py — H3: make the multi-writer story actually safe. [v1.0 SCAFFOLD — not yet wired] + +The plugin advertises a shared vault (Claude Code + CoWork), but two real races exist: + 1. The entity index is a full-file load -> mutate -> PUT (echo_index.load/save) with + NO lock. Two concurrent `capture` calls both read the old index; last PUT wins; + the other entity is silently dropped from the index (the note file survives, but + resolve/recall miss it until the next sweep). + 2. The advisory lock is MANUAL (echo.py cmd_lock) — capture / scope set never take it; + it relies on the model remembering to. + +This module provides (a) a context manager that auto-acquires/releases the existing +advisory lock around a write burst, with crash-safe release, and (b) an atomic +index-update that re-reads UNDER the lock before saving, so concurrent captures merge +instead of clobber. + +ACCEPTANCE (see ROADMAP-1.0.md > H3): + - two concurrent captures both land in the index (no lost entries) + - lock auto-released on normal AND error exit +""" +from __future__ import annotations + +import contextlib +import os +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import echo # noqa: E402 +import echo_index as idx_mod # noqa: E402 + + +def auto_owner() -> str: + """A stable-per-process owner id: client tag + pid. Avoids Math.random/time-based + ids; pid is unique enough for cooperative locking and is reproducible in tests.""" + tag = os.environ.get("ECHO_CLIENT", "cc") + return f"{tag}-{os.getpid()}" + + +@contextlib.contextmanager +def vault_lock(owner: str | None = None, required: bool = False): + """Hold the advisory lock for a write burst; release on exit (incl. exceptions). + + Yields `held: bool`. The lock is cooperative (read-back-confirmed, TTL-reclaimable), + so if another session holds it we do NOT block: with `required=False` we yield + `held=False` and let the caller proceed (advisory) or warn; with `required=True` we + raise. Release is best-effort and never masks the body's own exception. + """ + owner = owner or auto_owner() + rc = echo.cmd_lock(owner, quiet=True) + held = rc == 0 + if not held and required: + raise echo.EchoError(f"vault_lock: could not acquire lock for '{owner}' (held elsewhere)", 75) + try: + yield held + finally: + if held: + try: + echo.cmd_unlock(owner, quiet=True) + except Exception: # noqa: BLE001 — release is best-effort; TTL reclaims a leak + pass + + +def atomic_index_update(mutate, owner: str | None = None): + """Apply `mutate(index)` to the entity index without losing a concurrent writer's + entry: take the lock, **re-read the index fresh** (not a stale in-memory copy), + run the mutator, save, release. Returns the freshly-read-and-saved index. + + The fresh re-read inside the critical section is the core of the fix — even if the + advisory lock can't be taken (another session active), reading immediately before + the save collapses the read-modify-write window to two calls instead of spanning the + whole capture, so an unlocked update is still far safer than the prior code. + """ + with vault_lock(owner) as held: + index = idx_mod.load() + mutate(index) + idx_mod.save(index) + if not held: + print("echo_concurrency: entity index updated WITHOUT the advisory lock " + "(another session may be active); the fresh re-read minimizes the race.", + file=sys.stderr) + return index diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_doctor.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_doctor.py new file mode 100644 index 0000000..eceb3bf --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_doctor.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""echo_doctor.py — M3: one-call readiness check. [v1.0 SCAFFOLD — run() implemented] + +There is no single "is everything OK" command. doctor checks, in order, the things +that make ECHO usable this session and prints a green/red report. Intended to back an +`echo.py doctor` subcommand and the start of /echo-load when something looks wrong. + +Also tracked here (TODO): complete the `load` fallback — echo.cmd_load reads the +heartbeat pointer but never lists _agent/sessions/ when it's missing/stale, though +SKILL.md:122 documents that fallback. See patch_load_fallback() below. + +Exit: 0 all green · 1 a check is red. +""" +from __future__ import annotations + +import os +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import echo # noqa: E402 + +MIN_PY = (3, 9) + + +def run() -> int: + reds = 0 + + def line(ok: bool, label: str, detail: str = "") -> None: + nonlocal reds + reds += 0 if ok else 1 + print(f" [{'OK ' if ok else 'RED'}] {label}" + (f" — {detail}" if detail else "")) + + print(f"echo doctor — endpoint {echo.BASE}") + + # 1. interpreter + line(sys.version_info >= MIN_PY, f"python >= {MIN_PY[0]}.{MIN_PY[1]}", + f"running {sys.version_info.major}.{sys.version_info.minor}") + + # 2. reachability + auth + marker, in one GET of the bootstrap marker + status, body = echo.request("GET", echo.vault_url("_agent/echo-vault.md")) + if status == 0: + line(False, "vault reachable", body.decode(errors="replace")[:120]) + print("\ndoctor: endpoint unreachable — is Obsidian + the Local REST API running?") + return 1 + line(status not in (401, 403), "auth accepted", f"HTTP {status}" if status in (401, 403) else "") + if status == 404: + line(False, "vault bootstrapped", "marker absent — run bootstrap.py") + elif status == 200: + text = body.decode(errors="replace") + ver = next((ln.split(":", 1)[1].strip() for ln in text.splitlines() + if ln.startswith("schema_version:")), "?") + line(True, "vault bootstrapped", f"schema_version={ver}") + else: + line(status < 400, "marker fetch", f"HTTP {status}") + + # 3. key source (M1) + invariants pointer. + import echo_secrets + key_src = ("ECHO_KEY env" if os.environ.get("ECHO_KEY") + else "credentials file" if (echo_secrets._state_dir() / "credentials").exists() + else "baked-in fallback (deprecated — see API-KEY-SETUP.md)") + line(os.environ.get("ECHO_KEY") is not None or (echo_secrets._state_dir() / "credentials").exists(), + "API key source", key_src) + print(" [i] invariants — run `/echo-health` (vault_lint.py) for the full check") + + print(f"\ndoctor: {'all green' if reds == 0 else f'{reds} issue(s) — see RED above'}") + return 1 if reds else 0 + + +if __name__ == "__main__": + raise SystemExit(run()) diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_ops.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_ops.py index 7b9a219..138d955 100644 --- a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_ops.py +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_ops.py @@ -14,7 +14,6 @@ from __future__ import annotations import json import re import sys -import urllib.parse from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) @@ -54,79 +53,11 @@ def link(a_path: str, b_path: str) -> int: # ----------------------------------------------------------------- recall ----- -def _resolve_link_to_path(target: str, nmap: dict) -> str | None: - target = target.strip() - if "/" in target: - return target if target.endswith(".md") else target + ".md" - return nmap.get(idx_mod.slugify(target)) - - -def _brief(path: str) -> None: - text = links.get_text(path) - if text is None: - return - print(f"\n### {path}") - fm_type = re.search(r"(?m)^type:\s*(.+)$", text) - if fm_type: - print(f"_type: {fm_type.group(1).strip()}_") - # Prefer a Status paragraph; else the first handful of non-empty body lines. - body = text.split("\n---", 2)[-1] if text.startswith("---") else text - status = re.search(r"(?ms)^##\s+Status\s*$(.*?)(?=^##\s|\Z)", body) - if status and status.group(1).strip(): - print(status.group(1).strip()[:400]) - return - shown = 0 - for line in body.splitlines(): - if not line.strip() or line.startswith("# "): - continue - print(line[:200]) - shown += 1 - if shown >= 8: - break - - -def recall(query, limit: int = 6) -> int: - q = " ".join(query) if isinstance(query, list) else query - index = idx_mod.load() - nmap = idx_mod.name_map(index) - - status, body = echo.request("POST", f"{echo.BASE}/search/simple/?query={urllib.parse.quote_plus(q)}") - echo.check(status, body, "recall search") - try: - results = json.loads(body) - except json.JSONDecodeError: - results = [] - hits = [] - for r in results[:limit]: - fn = r.get("filename") or r.get("path") - if fn and fn not in hits: - hits.append(fn) - rslug, e = idx_mod.resolve(index, q) - if e and e.get("path") and e["path"] not in hits: - hits.insert(0, e["path"]) - - seen = set(hits) - neighbors = [] # (via, path) - for p in hits: - text = links.get_text(p) - if text is None: - continue - targets = set(links.all_wikilinks(text)) | set(links.source_notes(text)) - for t in targets: - tp = _resolve_link_to_path(t, nmap) - if tp and tp not in seen: - seen.add(tp) - neighbors.append((p, tp)) - - print(f"== recall: {q} ==") - print(f"\n# Primary hits ({len(hits)})") - for p in hits: - _brief(p) - print(f"\n# Linked context ({len(neighbors)})") - for via, p in neighbors: - print(f"\n(via {via})") - _brief(p) - return 0 +def recall(query, limit: int = 8) -> int: + """Hybrid lexical (BM25) + graph recall — implemented in echo_recall. Kept here as + the stable public entrypoint (echo.py and /echo-recall call echo_ops.recall).""" + import echo_recall # lazy: only pulls the BM25 modules when recall is actually run + return echo_recall.recall(query, limit=limit) # ----------------------------------------------------------- agent log ------- @@ -197,7 +128,30 @@ def _append_to_existing(path: str, kind: str, today_s: str, body_text: str) -> N def capture(kind: str | None, title: str, file_arg: str | None, status_v: str = "", aliases=None, sources=None, date: str | None = None, domain: str = "business", - inbox: bool = False, no_log: bool = False) -> int: + inbox: bool = False, no_log: bool = False, as_json: bool = False, + dry_run: bool = False) -> int: + import contextlib + import io + import echo_output + import echo_quality + + real_stdout = sys.stdout + + def quiet(): + # M4: in --json mode, swallow the helper "ok:" chatter so stdout is clean JSON. + return contextlib.redirect_stdout(io.StringIO()) if as_json else contextlib.nullcontext() + + def done(action: str, path: str, links: int = 0, ok: bool = True, dry: bool = False) -> int: + if as_json: + act = f"dry-run:{action}" if dry else action + env = echo_output.envelope(act, {"kind": kind, "path": path, "title": title, + "links_added": links}, ok=ok) + print(json.dumps(env, ensure_ascii=False), file=real_stdout) + elif dry: + print(f"would {action} {kind or '-'} -> {path}") + # non-dry human output is the helper "ok:" lines + the summary printed by the caller. + return 0 if ok else 1 + body_text = echo.read_body(file_arg).decode("utf-8", errors="replace") today_s = echo.today() aliases = [a.strip() for a in (aliases or []) if a.strip()] @@ -205,44 +159,74 @@ def capture(kind: str | None, title: str, file_arg: str | None, status_v: str = # Unknown home -> defer to the inbox (a single idempotent capture line). if inbox or not kind: + if dry_run: + return done("inbox", "inbox/captures/inbox.md", dry=True) line = f"- {today_s}: {title}" if body_text.strip(): line += f" — {body_text.strip().splitlines()[0]}" - return echo.cmd_append("inbox/captures/inbox.md", line) + with quiet(): + rc = echo.cmd_append("inbox/captures/inbox.md", line) + return done("inbox", "inbox/captures/inbox.md", ok=rc == 0) slug = idx_mod.slugify(title) index = idx_mod.load() _, existing = idx_mod.resolve(index, title) + existing_reachable = bool(existing and echo.request("GET", echo.vault_url(existing["path"]))[0] == 200) - if existing and echo.request("GET", echo.vault_url(existing["path"]))[0] == 200: - path = existing["path"] - kind = existing.get("kind", kind) - _append_to_existing(path, kind, today_s, body_text) - action = "updated" + if dry_run: + if existing_reachable: + return done("update", existing["path"], dry=True) + s2 = echo_quality.safe_slug(set(index.get("entities", {}).keys()), slug) + return done("create", idx_mod.derive_path(kind, s2, date=date, domain=domain), dry=True) + + with quiet(): + if existing_reachable: + path = existing["path"] + kind = existing.get("kind", kind) + _append_to_existing(path, kind, today_s, body_text) + action = "updated" + else: + if not status_v and kind == "project": + status_v = "active" + # M2: don't let a 40-char-truncated slug silently collide with a different + # entity already in the index — disambiguate to slug-2, slug-3, ... + slug = echo_quality.safe_slug(set(index.get("entities", {}).keys()), slug) + path = idx_mod.derive_path(kind, slug, date=date, domain=domain) + note = _build_note(idx_mod.KIND_TYPE.get(kind, kind), status_v, today_s, title, body_text, sources) + echo.cmd_put(path, echo.temp_file(note.encode())) + action = "created" + + # H3: the entity-index write is the race the review flagged — a bare load->save lets + # two concurrent captures clobber each other's entry. Route it through a lock-guarded, + # fresh-re-read transaction. Returns the fresh index (used by auto-link below). + import echo_concurrency + index = echo_concurrency.atomic_index_update( + lambda idx: idx_mod.upsert(idx, slug, path, kind, title, aliases)) + + # auto-link: any other known entity CONFIDENTLY named in the body (M2: is_confident_link + # rejects short/common tokens so a 3-char alias or a word like "API" can't link everywhere). + linked = 0 + for s2, e2 in index.get("entities", {}).items(): + if e2.get("path") in (None, path): + continue + names = [n for n in [e2.get("title", ""), s2, *e2.get("aliases", [])] if n] + if any(echo_quality.is_confident_link(n, body_text) for n in names): + ca, cb = links.link_bidirectional(path, e2["path"]) + linked += int(ca or cb) + + # Keep the BM25 recall index current for this note (best-effort; lock-guarded inside + # update_note, so it can't clobber a concurrent writer either). + try: + import echo_recall + echo_recall.update_note(path, links.get_text(path) or "") + except Exception as exc: # never let recall-index upkeep fail a capture + print(f"echo_ops: recall-index update skipped ({exc})", file=sys.stderr) + + if not no_log: + ensure_daily_log(f"- {today_s}: {action} {kind} [[{links.link_token(path)}]]") + + if as_json: + done(action, path, links=linked) else: - if not status_v and kind == "project": - status_v = "active" - path = idx_mod.derive_path(kind, slug, date=date, domain=domain) - note = _build_note(idx_mod.KIND_TYPE.get(kind, kind), status_v, today_s, title, body_text, sources) - echo.cmd_put(path, echo.temp_file(note.encode())) - action = "created" - - idx_mod.upsert(index, slug, path, kind, title, aliases) - idx_mod.save(index) - - # auto-link: any other known entity whose name/alias appears in the body, matched - # on word boundaries (and >=3 chars) so a short alias can't match inside a word. - linked = 0 - for s2, e2 in index.get("entities", {}).items(): - if e2.get("path") in (None, path): - continue - names = [n for n in [e2.get("title", ""), s2, *e2.get("aliases", [])] if len(n) >= 3] - if any(re.search(rf"\b{re.escape(n)}\b", body_text, re.IGNORECASE) for n in names): - ca, cb = links.link_bidirectional(path, e2["path"]) - linked += int(ca or cb) - - if not no_log: - ensure_daily_log(f"- {today_s}: {action} {kind} [[{links.link_token(path)}]]") - - print(f"ok: {action} {kind} -> {path}" + (f"; auto-linked {linked}" if linked else "")) + print(f"ok: {action} {kind} -> {path}" + (f"; auto-linked {linked}" if linked else "")) return 0 diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_output.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_output.py new file mode 100644 index 0000000..f0790d6 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_output.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""echo_output.py — M4: machine-readable output + dry-run plumbing. [v1.0 SCAFFOLD — implemented] + +The high-level ops (capture/recall/resolve/scope) print prose, forcing the agent to +re-parse free text to act on a result. This provides a single JSON envelope and a +small dry-run helper so every op can emit a structured result and preview writes. + +INTEGRATION (merge step): thread a global `--json` / `--dry-run` flag through echo.py's +parser and have each cmd_* return its data dict; emit() it. In --dry-run, the write +verbs print the envelope with action="dry-run" and perform no network mutation. +""" +from __future__ import annotations + +import json +import sys + + +def envelope(action: str, data: dict | None = None, ok: bool = True, + error: str | None = None) -> dict: + """Canonical result shape for every high-level op.""" + env = {"ok": ok, "action": action} + if data: + env.update(data) + if error: + env["error"] = error + return env + + +def emit(env: dict, as_json: bool) -> None: + """Print either the JSON envelope (machine mode) or a one-line human summary.""" + if as_json: + print(json.dumps(env, ensure_ascii=False)) + return + if not env.get("ok"): + print(f"error: {env.get('error', 'failed')}", file=sys.stderr) + return + bits = [env.get("action", "ok")] + if env.get("path"): + bits.append(str(env["path"])) + if env.get("links_added"): + bits.append(f"+{env['links_added']} links") + print("ok: " + " ".join(bits)) + + +def dry_run_envelope(action: str, data: dict | None = None) -> dict: + """Result for a previewed-but-not-executed write.""" + return envelope(f"dry-run:{action}", data, ok=True) diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_quality.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_quality.py new file mode 100644 index 0000000..fb1f0be --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_quality.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""echo_quality.py — M2: keep the graph (which recall depends on) clean. +[v1.0 SCAFFOLD — helpers implemented; integration into echo_links/echo_index is the merge step] + +Two graph-polluting bugs today: + 1. Auto-link fires on any indexed entity whose name/alias is >=3 chars and word-matches + the body (echo_ops.py:236). A concept titled "API" or a 2-3 char alias links to every + note that happens to mention the word -> noisy, wrong edges. + 2. slugify truncates to 40 chars (echo_index.py:58) with NO collision check, so two + distinct long titles can resolve to the same slug -> same path -> silent merge. + +This module supplies the two guards. Integration: + * echo_ops auto-link loop -> gate each candidate on is_confident_link(name, body) + * echo_index.derive_path -> route the slug through safe_slug(existing_slugs, slug) +""" +from __future__ import annotations + +import re + +# Tokens too short or too common to be a confident entity reference on their own. +MIN_NAME_LEN = 4 +_COMMON = frozenset( + "api app data note task user team work code test main repo file plan goal item " + "the and for with mpm".split() +) + + +def is_confident_link(name: str, body: str) -> bool: + """True only if `name` is a strong enough signal to auto-link on its appearance in + `body`. Rejects short names, common words, and non-matches. Multi-word names are + trusted at lower length (a two-word phrase is rarely incidental).""" + n = (name or "").strip() + if not n: + return False + multiword = len(n.split()) > 1 + if not multiword: + if len(n) < MIN_NAME_LEN or n.lower() in _COMMON: + return False + return re.search(rf"\b{re.escape(n)}\b", body or "", re.IGNORECASE) is not None + + +def safe_slug(existing_slugs, slug: str, max_len: int = 40) -> str: + """Return `slug` if free, else a disambiguated `slug-2`, `slug-3`, ... that still + fits `max_len` and does not collide with anything in `existing_slugs`.""" + existing = set(existing_slugs or ()) + if slug not in existing: + return slug + n = 2 + while True: + suffix = f"-{n}" + base = slug[: max_len - len(suffix)] if len(slug) + len(suffix) > max_len else slug + cand = f"{base}{suffix}" + if cand not in existing: + return cand + n += 1 diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_queue.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_queue.py new file mode 100644 index 0000000..e57c6ff --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_queue.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""echo_queue.py — H2: offline write-ahead queue + read cache. [v1.0 — wired] + +Today vault-unreachable => "proceed without memory" (SKILL.md), so a 502 (Obsidian not +running — the most common real failure) loses everything said that session. This module +makes explicit writes durable across an outage and lets cold-start `load` degrade to +last-known context instead of nothing. + +DESIGN: + * Outbox : append-only NDJSON of intended writes. Replayed in order on the next + reachable session. Replay is idempotent by construction — PUT/DELETE/PATCH- + replace overwrite, and POST / PATCH-append are content-deduped (skip if the + line/block is already present), the same strategy echo.py append uses. Each + record carries an idem_key so the SAME write is never queued twice. + * Cache : last-known-good GET bodies, so `load` has a fallback. Written by cmd_load. + * Location: a LOCAL state dir — it CANNOT live in the vault, because the vault is the + thing that's down. Default ~/.echo-memory/, override ECHO_STATE_DIR. + +Integration seam: `safe_request()` — the write verbs (cmd_put/post/append/patch/delete) +call it instead of echo.request; on an outage it enqueues and returns a synthesized +(202, b"queued ...") so the caller reports "queued, will sync" instead of failing. +""" +from __future__ import annotations + +import base64 +import hashlib +import json +import os +import sys +import urllib.parse +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import echo # noqa: E402 + +MUTATING = {"PUT", "POST", "PATCH", "DELETE"} + + +def state_dir() -> Path: + return Path(os.environ.get("ECHO_STATE_DIR") or (Path.home() / ".echo-memory")) + + +def outbox_path() -> Path: + return state_dir() / "outbox.ndjson" + + +def cache_dir() -> Path: + return state_dir() / "cache" + + +def _ensure_dirs() -> None: + state_dir().mkdir(parents=True, exist_ok=True) + cache_dir().mkdir(parents=True, exist_ok=True) + + +def _key(method: str, url: str, data: bytes | None) -> str: + h = hashlib.sha1() + h.update(method.encode()) + h.update(b"\0") + h.update(url.encode()) + h.update(b"\0") + h.update(data or b"") + return h.hexdigest() + + +# --------------------------------------------------------------------- outbox +def enqueue(method: str, url: str, data: bytes | None, headers: dict, idem_key: str | None = None) -> None: + """Append one intended write to the outbox (NDJSON; base64 body for binary safety). + Dedups: if a record with the same idem_key is already pending, do nothing.""" + idem_key = idem_key or _key(method, url, data) + if any(rec.get("idem_key") == idem_key for rec in pending()): + return + _ensure_dirs() + rec = { + "method": method.upper(), "url": url, + "body_b64": base64.b64encode(data).decode() if data else None, + "headers": headers or {}, "idem_key": idem_key, + # No wall-clock: replay order is file order, not a timestamp (keeps this testable). + } + with outbox_path().open("a", encoding="utf-8") as fh: + fh.write(json.dumps(rec, ensure_ascii=False) + "\n") + + +def pending() -> list[dict]: + p = outbox_path() + if not p.exists(): + return [] + return [json.loads(ln) for ln in p.read_text(encoding="utf-8").splitlines() if ln.strip()] + + +def _rewrite(records: list[dict]) -> None: + p = outbox_path() + if not records: + if p.exists(): + p.unlink() + return + _ensure_dirs() + tmp = p.with_suffix(".ndjson.tmp") + tmp.write_text("".join(json.dumps(r, ensure_ascii=False) + "\n" for r in records), encoding="utf-8") + os.replace(tmp, p) # atomic swap so a crash mid-flush can't corrupt the queue + + +def _rebase(url: str) -> str: + """Re-point a queued URL at the CURRENT echo.BASE. Writes are queued with the base + that was active when they failed; on replay the vault may be back at a different base + (e.g. an endpoint migration), so we always reconstruct the origin from echo.BASE.""" + parts = urllib.parse.urlsplit(url) + return echo.BASE + parts.path + (("?" + parts.query) if parts.query else "") + + +def _replay(rec: dict) -> str: + """Re-issue one queued write idempotently. Returns 'ok' (landed/already-present), + 'drop' (permanent 4xx — can never succeed), or 'retry' (still offline / 5xx).""" + method = rec["method"] + url = _rebase(rec["url"]) + data = base64.b64decode(rec["body_b64"]) if rec.get("body_b64") else None + headers = rec.get("headers") or {} + + # Append-style writes: skip if the content is already present (idempotency on replay). + is_append = method == "POST" or (method == "PATCH" and headers.get("Operation") == "append") + if is_append and data: + st, cur = echo.request("GET", url) + if st == 0: + return "retry" # still offline + if st == 200 and data.decode("utf-8", "replace").strip("\n") in cur.decode("utf-8", "replace"): + return "ok" # already landed (or a duplicate we must not re-add) + + st, body = echo.request(method, url, data=data, headers=headers) + if st == 0 or 500 <= st < 600: + return "retry" + if 400 <= st < 500: + print(f"echo_queue: dropping a queued {method} {url} — permanent HTTP {st} on replay", + file=sys.stderr) + return "drop" + return "ok" + + +def flush() -> int: + """Replay queued writes in order; stop at the first still-unreachable one (keeping it + and the rest). Returns the number actually replayed. Safe to call when empty.""" + items = pending() + if not items: + return 0 + replayed = 0 + remaining: list[dict] = [] + for i, rec in enumerate(items): + result = _replay(rec) + if result == "ok": + replayed += 1 + elif result == "drop": + continue # warned in _replay; remove from queue + else: # retry -> still offline; keep this and everything after, in order + remaining = items[i:] + break + _rewrite(remaining) + return replayed + + +# --------------------------------------------------------------------- cache +def cache_key(path: str) -> Path: + return cache_dir() / (path.replace("/", "__") + ".cache") + + +def cache_put(path: str, body: bytes) -> None: + _ensure_dirs() + cache_key(path).write_bytes(body) + + +def cache_get(path: str) -> bytes | None: + p = cache_key(path) + return p.read_bytes() if p.exists() else None + + +# ----------------------------------------------------------- integration seam +def safe_request(method: str, url: str, data=None, headers=None, idem_key: str | None = None): + """Network-first wrapper around echo.request with offline queueing for writes. + + - success or a client error (4xx): return (status, body) as-is — 4xx is a bug, fail loud. + - transport failure (status 0) or 5xx (after echo.request's own retry) on a MUTATING + verb: enqueue the write and return (202, b"queued (offline)") so the caller reports it + as durably queued rather than failed. + - otherwise (e.g. a failing GET): return (status, body); the read-cache fallback lives + in cmd_load, which knows which reads are worth serving stale. + """ + method = method.upper() + status, body = echo.request(method, url, data=data, headers=headers) + offline = status == 0 or 500 <= status < 600 + if offline and method in MUTATING: + enqueue(method, url, data, headers or {}, idem_key) + return 202, b"queued (offline)" + return status, body diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_recall.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_recall.py new file mode 100644 index 0000000..1d9b2d2 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_recall.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 +"""echo_recall.py — H1: hybrid lexical (BM25) + graph recall. [v1.0 — wired] + +Replaces the keyword-only recall (search/simple + fixed one-hop) with a ranked, local +retriever that finds a note even when the query is phrased differently than the note +was stored, then expands along the graph with a per-hop decay so the *connected* +context is surfaced in relevance order. + +DESIGN (bounded by the project's pure-stdlib, no-deps constraint): + * Lexical layer : BM25 over note bodies. No embeddings / external API — BM25 is a + strong, dependency-free baseline that handles vocabulary mismatch + far better than substring search. + * Graph layer : from the top lexical hits, walk body wikilinks + source_notes up + to MAX_HOPS, scoring each node `parent * GRAPH_DECAY**hop` (max over + paths) so near, strongly-cued neighbours rank above distant ones. + * Persistence : the BM25 postings + doc stats are a DERIVED artifact, so they live + in the vault's machine index dir (`_agent/index/recall-index.json`), + maintained on `capture` (update_note) and fully rebuilt by `sweep.py` + — exactly how `entities.json` is maintained. Rebuildable => portable. + * Resilience : if the index can't be built (vault unreachable), recall falls back + to the server's /search/simple so it degrades instead of dying. + +Corpus = the indexable ENTITY notes (idx_mod.kind_for_path is not None): people, +companies, concepts, references, meetings, projects, areas, decisions, semantic/ +episodic memory, skills — the durable memory graph. (Sessions/journal are a future +add; tracked in ROADMAP-1.0.md.) +""" +from __future__ import annotations + +import json +import math +import re +import sys +import urllib.parse +from collections import Counter, deque +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import echo # noqa: E402 +import echo_index as idx_mod # noqa: E402 +import echo_links as links # noqa: E402 + +RECALL_INDEX_PATH = "_agent/index/recall-index.json" + +# Tuning knobs. Defaults are standard BM25 (k1/b) + a 0.6 hop decay over <=2 hops. +K1 = 1.5 +B = 0.75 +MAX_HOPS = 2 +GRAPH_DECAY = 0.6 +MIN_SCORE = 0.0 # relevance floor; raise to trim weak lexical hits + +_WORD = re.compile(r"[a-z0-9]+") +# Minimal English stoplist — keeps BM25 idf from being dominated by glue words. +_STOP = frozenset( + "the a an and or of to in is are was were be been for on at by with as it this that " + "from into over under not no yes do does did has have had will would can could".split() +) +_TEMPLATE_RE = re.compile(r"(^|/)(templates/|.*-template\.md$)") +_SKIP_BASENAMES = {"README.md"} + + +def tokenize(text: str) -> list[str]: + """Lowercase word tokens, stopworded. Frontmatter should be stripped by the caller.""" + return [t for t in _WORD.findall(text.lower()) if t not in _STOP and len(t) > 1] + + +def strip_frontmatter(text: str) -> str: + if not text.startswith("---"): + return text + end = text.find("\n---", 3) + return text[end + 4:] if end != -1 else text + + +# --------------------------------------------------------------------- BM25 core +class Bm25Index: + """A compact, serializable BM25 index. add()/remove() are idempotent per path.""" + + def __init__(self) -> None: + self.df: Counter[str] = Counter() # term -> #docs containing it + self.postings: dict[str, dict[str, int]] = {} # term -> {doc_path: tf} + self.length: dict[str, int] = {} # doc_path -> token count + self.n_docs = 0 + self.avg_len = 0.0 + + def _recompute(self) -> None: + self.n_docs = len(self.length) + self.avg_len = (sum(self.length.values()) / self.n_docs) if self.n_docs else 0.0 + + def add(self, path: str, text: str) -> None: + if path in self.length: # re-index in place (idempotent) + self.remove(path) + toks = tokenize(text) + self.length[path] = len(toks) + for term, tf in Counter(toks).items(): + self.postings.setdefault(term, {})[path] = tf + self.df[term] += 1 + self._recompute() + + def remove(self, path: str) -> None: + if path not in self.length: + return + for term in list(self.postings.keys()): + if path in self.postings[term]: + del self.postings[term][path] + self.df[term] -= 1 + if not self.postings[term]: + del self.postings[term] + del self.df[term] + del self.length[path] + self._recompute() + + def score(self, query: str, limit: int = 10) -> list[tuple[str, float]]: + scores: Counter[str] = Counter() + for term in set(tokenize(query)): + posting = self.postings.get(term) + if not posting: + continue + idf = math.log(1 + (self.n_docs - self.df[term] + 0.5) / (self.df[term] + 0.5)) + for path, tf in posting.items(): + dl = self.length.get(path, 0) + denom = tf + K1 * (1 - B + B * (dl / self.avg_len if self.avg_len else 1)) + scores[path] += idf * (tf * (K1 + 1) / denom if denom else 0) + return [(p, s) for p, s in scores.most_common(limit) if s > MIN_SCORE] + + # ---- serialization (compact; rebuildable, so the format can change freely) ---- + def to_json(self) -> dict: + return {"schema": 1, "n_docs": self.n_docs, "avg_len": self.avg_len, + "length": self.length, "postings": self.postings} + + @classmethod + def from_json(cls, d: dict) -> "Bm25Index": + ix = cls() + ix.length = d.get("length", {}) + ix.postings = d.get("postings", {}) + ix.n_docs = d.get("n_docs", len(ix.length)) + ix.avg_len = d.get("avg_len", 0.0) + ix.df = Counter({term: len(posting) for term, posting in ix.postings.items()}) + return ix + + +# ------------------------------------------------------------------- vault I/O +def _get(path: str) -> str | None: + status, body = echo.request("GET", echo.vault_url(path)) + if status == 404: + return None + echo.check(status, body, f"recall get {path}") + return body.decode(errors="replace") + + +def _list_dir(path: str): + p = "" if path in ("", "/") else (path if path.endswith("/") else path + "/") + body = _get(p) + if body is None: + return [], [] + try: + j = json.loads(body) + except json.JSONDecodeError: + return [], [] + entries = list(j.get("files", [])) + list(j.get("folders", [])) + files = [e for e in entries if not e.endswith("/")] + folders = [e[:-1] for e in entries if e.endswith("/")] + return files, folders + + +def _walk(prefix: str = ""): + files, folders = _list_dir(prefix) + for f in files: + yield prefix + f + for d in folders: + yield from _walk(f"{prefix}{d}/") + + +def _indexable(path: str) -> bool: + base = path.rsplit("/", 1)[-1] + return (path.endswith(".md") and idx_mod.kind_for_path(path) is not None + and base not in _SKIP_BASENAMES and not _TEMPLATE_RE.search(path)) + + +# ------------------------------------------------------------------- persistence +def load_index() -> Bm25Index: + status, body = echo.request("GET", echo.vault_url(RECALL_INDEX_PATH)) + if status == 404: + return Bm25Index() + echo.check(status, body, "recall-index load") + try: + return Bm25Index.from_json(json.loads(body)) + except (json.JSONDecodeError, KeyError, TypeError): + return Bm25Index() + + +def save_index(ix: Bm25Index) -> None: + body = json.dumps(ix.to_json(), ensure_ascii=False).encode("utf-8") + status, b = echo.request("PUT", echo.vault_url(RECALL_INDEX_PATH), data=body, + headers={"Content-Type": "application/json"}) + echo.check(status, b, "recall-index save") + + +def rebuild() -> Bm25Index: + """Full rebuild from every indexable entity note. Called by sweep.py and as the + cold-cache path inside recall(). Saves and returns the index.""" + ix = Bm25Index() + for path in _walk(): + if not _indexable(path): + continue + text = _get(path) + if text is not None: + ix.add(path, strip_frontmatter(text)) + save_index(ix) + return ix + + +def update_note(path: str, text: str) -> None: + """Best-effort incremental upkeep for a single note (mirrors how entities.json is + maintained on capture). Never raises into the caller. The load->save is wrapped in the + advisory lock with a fresh re-read (H3), so concurrent captures can't clobber the + recall index either.""" + try: + if not _indexable(path): + return # only entity notes are part of the recall corpus + import echo_concurrency + with echo_concurrency.vault_lock(): + ix = load_index() # fresh read inside the lock + ix.add(path, strip_frontmatter(text)) + save_index(ix) + except Exception as exc: # noqa: BLE001 — upkeep must never fail a capture + print(f"echo_recall: index update skipped ({exc})", file=sys.stderr) + + +# ------------------------------------------------------------------- graph fusion +def _resolve_target(target: str, nmap: dict) -> str | None: + target = target.strip() + if not target: + return None + if "/" in target: + return target if target.endswith(".md") else target + ".md" + return nmap.get(idx_mod.slugify(target)) + + +def expand_graph(seeds, nmap: dict, base_scores: dict, max_hops: int = MAX_HOPS): + """BFS from seeds over body wikilinks + source_notes, scoring each neighbour + `parent_score * GRAPH_DECAY**hop` (kept as the max over reaching paths). Returns a + score-sorted list of (path, (score, via)) excluding the seeds themselves.""" + score_of = dict(base_scores) + seen = set(seeds) + results: dict[str, tuple[float, str]] = {} + dq = deque((s, 0) for s in seeds) + while dq: + path, hop = dq.popleft() + if hop >= max_hops: + continue + text = links.get_text(path) + if text is None: + continue + body = strip_frontmatter(text) + targets = set(links.all_wikilinks(body)) | set(links.source_notes(text)) + parent = score_of.get(path, 1.0) + for t in targets: + tp = _resolve_target(t, nmap) + if not tp or tp in seeds: + continue + decayed = parent * (GRAPH_DECAY ** (hop + 1)) + if decayed > results.get(tp, (0.0, ""))[0]: + results[tp] = (decayed, path) + if tp not in seen: + seen.add(tp) + dq.append((tp, hop + 1)) + return sorted(results.items(), key=lambda kv: -kv[1][0]) + + +# ------------------------------------------------------------------- presentation +def _brief(path: str, score: float | None = None, via: str | None = None) -> None: + text = links.get_text(path) + if text is None: + return + head = f"\n### {path}" + if score is not None: + head += f" (score {score:.2f})" + if via: + head += f" (via {via})" + print(head) + mtype = re.search(r"(?m)^type:\s*(.+)$", text) + if mtype: + print(f"_type: {mtype.group(1).strip()}_") + body = strip_frontmatter(text) + status = re.search(r"(?ms)^##\s+Status\s*$(.*?)(?=^##\s|\Z)", body) + if status and status.group(1).strip(): + print(status.group(1).strip()[:400]) + return + shown = 0 + for line in body.splitlines(): + if not line.strip() or line.startswith("# "): + continue + print(line[:200]) + shown += 1 + if shown >= 6: + break + + +# ------------------------------------------------------------------- entrypoint +def recall(query, limit: int = 8) -> int: + q = " ".join(query) if isinstance(query, list) else query + index = idx_mod.load() + nmap = idx_mod.name_map(index) + + # --- lexical layer (BM25); rebuild the cache on a cold miss -------------- + lexical: list[tuple[str, float]] = [] + try: + ix = load_index() + if ix.n_docs == 0: + ix = rebuild() + lexical = ix.score(q, limit=limit) + except echo.EchoError as exc: + print(f"recall: BM25 index unavailable ({exc}); falling back to server search", + file=sys.stderr) + + base: dict[str, float] = {p: s for p, s in lexical} + hits: list[str] = [p for p, _ in lexical] + + # --- entity-index seed (alias-aware exact match) ------------------------- + _, e = idx_mod.resolve(index, q) + if e and e.get("path") and e["path"] not in base: + hits.insert(0, e["path"]) + base[e["path"]] = max(base.values(), default=1.0) + + # --- resilience fallback: server search when lexical found nothing ------- + if not hits: + try: + status, body = echo.request( + "POST", f"{echo.BASE}/search/simple/?query={urllib.parse.quote_plus(q)}") + if status == 200: + for r in (json.loads(body) or [])[:limit]: + fn = r.get("filename") or r.get("path") + if fn and fn not in base: + hits.append(fn) + base[fn] = 0.0 + except Exception: # noqa: BLE001 — fallback only; stay quiet + pass + + # --- graph layer ---------------------------------------------------------- + neighbours = expand_graph(hits, nmap, base, max_hops=MAX_HOPS) + + print(f"== recall: {q} ==") + print(f"\n# Primary hits ({len(hits)})") + for p in hits: + _brief(p, score=base.get(p)) + print(f"\n# Linked context ({len(neighbours)})") + for p, (sc, via) in neighbours[: 2 * limit]: + _brief(p, score=sc, via=via) + return 0 diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_reflect.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_reflect.py new file mode 100644 index 0000000..4062bce --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_reflect.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""echo_reflect.py — H5: automatic session-reflection capture. [v1.0 — wired] + +Today memory only accrues when the agent decides to write. This makes accrual the +default: at session end the model extracts durable items from the conversation and +emits a PROPOSAL set; this module dedups them against the entity index, previews them, +and applies the accepted ones via the normal `capture` path — so nothing is written +without a go-ahead (honors the "show before large/sensitive writes" safety rule). + +DIVISION OF LABOR: + * Extraction is MODEL-side (only the LLM has the conversation). The agent produces a + JSON array of proposals matching PROPOSAL_SCHEMA. + * This script is the deterministic spine: validate -> classify (new/update/inbox vs the + entity index) -> preview -> apply on confirm. No NL understanding lives here, so it's + testable offline. + +PROPOSAL_SCHEMA (one per durable item the agent wants to remember): + { + "title": "Bob Smith", # required + "kind": "person", # required unless "inbox": true + "body": "Principal at MPM; ...", # markdown body (optional) + "aliases": ["bob", "rs"], # optional + "sources": ["_agent/sessions/..."], # optional backward links + "date": "YYYY-MM-DD", # optional (meeting/decision kinds) + "domain": "business", # optional (area kind) + "inbox": false, # route to inbox when the home is unknown + "confidence": 0.0-1.0 # agent's own confidence; gates auto-suggest + } + +CLI (echo.py reflect): dry-run previews; --apply writes — matching bootstrap/migrate/sweep. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import echo # noqa: E402 +import echo_index as idx_mod # noqa: E402 + +MIN_CONFIDENCE = 0.6 + + +def validate(proposals: list[dict]) -> tuple[list[dict], list[str]]: + """Return (valid, errors). title always required; kind required unless inbox; a + confidence below the floor is routed away (the agent should send it to the inbox).""" + valid, errors = [], [] + for i, p in enumerate(proposals or []): + if not str(p.get("title", "")).strip(): + errors.append(f"proposal[{i}]: missing title") + continue + is_inbox = bool(p.get("inbox")) + if not is_inbox: + kind = str(p.get("kind", "")).strip() + if not kind: + errors.append(f"proposal[{i}] '{p['title']}': missing kind (or set inbox:true)") + continue + if kind not in idx_mod.KIND_FOLDER: + errors.append(f"proposal[{i}] '{p['title']}': unknown kind '{kind}'") + continue + if float(p.get("confidence", 1.0)) < MIN_CONFIDENCE: + errors.append(f"proposal[{i}] '{p['title']}': confidence < {MIN_CONFIDENCE} — send to inbox instead") + continue + valid.append(p) + return valid, errors + + +def classify(proposals: list[dict]) -> list[dict]: + """Annotate each proposal with `_action` (create / update / inbox) and `_path`, by + resolving its title against the entity index (alias-aware), so the preview shows + create-vs-append and dedups against what's already in memory.""" + index = idx_mod.load() + for p in proposals: + if p.get("inbox") or not p.get("kind"): + p["_action"], p["_path"] = "inbox", "inbox/captures/inbox.md" + continue + _, e = idx_mod.resolve(index, p["title"]) + if e and e.get("path"): + p["_action"], p["_path"] = "update", e["path"] + else: + try: + p["_path"] = idx_mod.derive_path( + p["kind"], idx_mod.slugify(p["title"]), + date=p.get("date"), domain=p.get("domain", "business")) + p["_action"] = "create" + except echo.EchoError as exc: + p["_action"], p["_path"] = "error", str(exc) + return proposals + + +def preview(proposals: list[dict]) -> str: + """One confirmable line per proposal: action | kind | title -> target path.""" + rows = [] + for p in proposals: + act = p.get("_action", "?") + kind = "-" if act == "inbox" else p.get("kind", "?") + rows.append(f" {act:7} | {kind:9} | {p['title']} -> {p.get('_path', '?')}") + return "\n".join(rows) + + +def apply(proposals: list[dict], confirm: bool = False) -> int: + """Validate -> classify -> preview; with confirm=True, apply each via echo_ops.capture + (which routes/indexes/links/logs, each self-lock-guarded). Without confirm it is a + dry-run that writes nothing — the preview IS the confirmation step.""" + valid, errors = validate(proposals) + for e in errors: + print(f"skip: {e}", file=sys.stderr) + if not valid: + print("reflect: no valid proposals to apply.") + return 0 + + classify(valid) + counts = {a: sum(1 for p in valid if p.get("_action") == a) for a in ("create", "update", "inbox", "error")} + print(f"reflect: {len(valid)} proposal(s) — " + f"{counts['create']} new, {counts['update']} update, {counts['inbox']} inbox" + + (f", {counts['error']} error" if counts["error"] else "")) + print(preview(valid)) + + if not confirm: + print("\nreflect: dry-run — re-run with --apply to write these to memory.") + return 0 + + import echo_ops # lazy: the apply path pulls in the capture/index/link stack + applied = 0 + for p in valid: + if p.get("_action") == "error": + continue + bodyfile = echo.temp_file((p.get("body") or "").encode()) if p.get("body") else None + rc = echo_ops.capture( + p.get("kind"), p["title"], bodyfile, + aliases=p.get("aliases") or [], sources=p.get("sources") or [], + date=p.get("date"), domain=p.get("domain", "business"), + inbox=bool(p.get("inbox")) or not p.get("kind")) + applied += 1 if rc == 0 else 0 + print(f"reflect: applied {applied}/{len(valid)} proposal(s).") + return 0 diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_secrets.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_secrets.py new file mode 100644 index 0000000..3eefa6a --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_secrets.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""echo_secrets.py — M1: resolve the API bearer token without shipping it in source. +[v1.0 SCAFFOLD — resolve_key() implemented; wiring into echo.py is the integration step] + +Problem: the token is hardcoded at echo.py:70 and repeated through api-reference.md / +README — committed to git and shipped inside every .plugin zip, rotatable only by a +rebuild. The plugin's own safety rule is "never store secrets". This resolves the key +from progressively less-trusted sources, with a loud warning if the legacy baked +default is ever reached. + +RESOLUTION ORDER (first hit wins): + 1. env ECHO_KEY — preferred; CI / per-session override. + 2. ECHO_STATE_DIR/credentials — local file, one `key=...` line, chmod 0600. + 3. legacy baked default (deprecated) — warns to stderr; remove before 1.0 ship. + +Pure stdlib only (no `keyring`); the local file is the cross-platform stand-in for an +OS keychain and keeps the secret OUT of the tracked tree. + +INTEGRATION (do at merge time): + echo.py: KEY = echo_secrets.resolve_key() + docs: replace the literal token with `` placeholders + rotate: document `echo write-key` (writes the credentials file) — see TODO below +""" +from __future__ import annotations + +import os +import sys +from pathlib import Path + +# Kept ONLY so a fresh install still works during the migration window; delete for 1.0. +_LEGACY_DEFAULT = "" # intentionally blank in the scaffold — do not re-introduce the literal + + +def _state_dir() -> Path: + return Path(os.environ.get("ECHO_STATE_DIR") or (Path.home() / ".echo-memory")) + + +def _from_file() -> str | None: + p = _state_dir() / "credentials" + if not p.exists(): + return None + for line in p.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if line.startswith("key=") or line.startswith("ECHO_KEY="): + return line.split("=", 1)[1].strip() + return None + + +def resolve_key(legacy: str | None = None) -> str: + """Return the bearer token from the most trusted available source: + ECHO_KEY env -> credentials file -> `legacy` baked-in fallback (with a loud, once-per- + process warning that can be silenced via ECHO_KEY_LEGACY_OK=1). Raises if none exist.""" + env = os.environ.get("ECHO_KEY") + if env: + return env + from_file = _from_file() + if from_file: + return from_file + legacy = legacy or _LEGACY_DEFAULT + if legacy: + if os.environ.get("ECHO_KEY_LEGACY_OK") != "1": + print("echo_secrets: WARNING — using the deprecated baked-in API key. Run " + "`echo.py write-key ` (stores ~/.echo-memory/credentials) or set " + "ECHO_KEY; set ECHO_KEY_LEGACY_OK=1 to silence.", file=sys.stderr) + return legacy + raise RuntimeError( + "echo_secrets: no API key found. Set ECHO_KEY or create " + f"{_state_dir() / 'credentials'} with a 'key=' line." + ) + + +def write_key(token: str) -> Path: + """Persist the token to the local credentials file (one `key=...` line, chmod 0600). + Best-effort perms — Windows has no POSIX mode bits, so the chmod is advisory there.""" + d = _state_dir() + d.mkdir(parents=True, exist_ok=True) + path = d / "credentials" + path.write_text(f"key={token.strip()}\n", encoding="utf-8") + try: + os.chmod(path, 0o600) + except OSError: + pass # platform without POSIX perms (e.g. some Windows filesystems) + return path diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/migrate.py b/echo-memory.plugin.src/skills/echo-memory/scripts/migrate.py index 2329ba5..5dc0f60 100644 --- a/echo-memory.plugin.src/skills/echo-memory/scripts/migrate.py +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/migrate.py @@ -25,7 +25,7 @@ sys.path.insert(0, str(SCRIPT_DIR)) import echo # noqa: E402 -CURRENT_SCHEMA = 3 +CURRENT_SCHEMA = 4 def get_text(path: str) -> str | None: @@ -141,6 +141,11 @@ def main(argv: list[str] | None = None) -> int: "# index\n\nMachine-maintained entity index. See the echo-memory plugin.\n")) print("migrate: [2->3] then run `sweep.py --apply` to build entities.json and symmetrize links.") + if start < 4: + print("migrate: [3->4] add the recall (BM25) index — hybrid lexical+graph recall") + print("migrate: [3->4] _agent/index/ already exists (schema 3); no moves needed.") + print("migrate: [3->4] then run `sweep.py --apply` to build _agent/index/recall-index.json.") + do_or_show(args.apply, f"set _agent/echo-vault.md schema_version -> {CURRENT_SCHEMA}", lambda: echo.cmd_fm("_agent/echo-vault.md", "schema_version", str(CURRENT_SCHEMA))) if args.apply: diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/sweep.py b/echo-memory.plugin.src/skills/echo-memory/scripts/sweep.py index 616297c..82baeec 100644 --- a/echo-memory.plugin.src/skills/echo-memory/scripts/sweep.py +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/sweep.py @@ -33,8 +33,9 @@ sys.path.insert(0, str(SCRIPT_DIR)) import echo # noqa: E402 import echo_index as idx_mod # noqa: E402 import echo_links as links # noqa: E402 +import echo_recall # noqa: E402 -CURRENT_SCHEMA = 3 +CURRENT_SCHEMA = 4 TEMPLATE_RE = re.compile(r"(^|/)(templates/|.*-template\.md$)") SKIP_BASENAMES = {"README.md"} @@ -123,6 +124,14 @@ def main(argv: list[str] | None = None) -> int: if apply: idx_mod.save(rebuilt) + # ---- 2b. (re)build the recall (BM25) index ------------------------------- + if apply: + rix = echo_recall.rebuild() + print(f"sweep: {tag} recall index — {rix.n_docs} docs (BM25)") + else: + n_idx = sum(1 for p in all_files if echo_recall._indexable(p)) + print(f"sweep: {tag} recall index — would rebuild BM25 over {n_idx} entity notes") + # ---- 3. symmetrize existing Related links -------------------------------- fileset = set(all_files) basemap: dict[str, str] = {} diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/test_v1_scaffold.py b/echo-memory.plugin.src/skills/echo-memory/scripts/test_v1_scaffold.py new file mode 100644 index 0000000..acd393e --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/test_v1_scaffold.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""test_v1_scaffold.py — H4 interface guard for the v1.0 scaffolds. [offline, no creds] + +Asserts the scaffold contract holds so the skeleton can't silently rot while 1.0 is +built: every planned module imports, the parts that ARE implemented work (BM25 ranking, +queue/cache round-trip, slug helpers, secret resolution order), and the parts that are +TODO raise NotImplementedError (so a stub can't masquerade as done). + +Run: python test_v1_scaffold.py +""" +from __future__ import annotations + +import os +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +failures: list[str] = [] + + +def check(name: str, cond: bool, detail: str = "") -> None: + print(f"{'ok ' if cond else 'FAIL'} {name}" + (f" -- {detail}" if not cond else "")) + if not cond: + failures.append(name) + + +def main() -> int: + # H1 — BM25 core is implemented; assert it actually ranks a relevant doc first. + import echo_recall as r + ix = r.Bm25Index() + ix.add("notes/mqtt.md", "the mqtt broker config and tls certificate rotation") + ix.add("notes/payroll.md", "quarterly payroll run and employee deductions") + top = ix.score("certificate rotation broker", limit=2) + check("H1 BM25 ranks relevant doc first", bool(top) and top[0][0] == "notes/mqtt.md", str(top)) + check("H1 BM25 survives json round-trip", + r.Bm25Index.from_json(ix.to_json()).score("payroll")[0][0] == "notes/payroll.md") + ix.remove("notes/payroll.md") + check("H1 BM25 remove() drops a doc's postings", ix.score("payroll") == []) + check("H1 recall path is wired (not a stub)", + all(callable(getattr(r, n, None)) + for n in ("recall", "rebuild", "update_note", "load_index", "save_index"))) + + # H2 — cache round-trips, enqueue dedups, flush is a no-op on an empty queue (offline-safe). + import echo_queue as q + with tempfile.TemporaryDirectory() as d: + os.environ["ECHO_STATE_DIR"] = d + q.cache_put("a/b.md", b"hello") + check("H2 cache round-trip", q.cache_get("a/b.md") == b"hello") + q.enqueue("PUT", "http://x/vault/a.md", b"body", {}, idem_key="k1") + check("H2 enqueue persists a record", len(q.pending()) == 1) + q.enqueue("PUT", "http://x/vault/a.md", b"body", {}, idem_key="k1") + check("H2 enqueue dedups by idem_key", len(q.pending()) == 1) + with tempfile.TemporaryDirectory() as d2: + os.environ["ECHO_STATE_DIR"] = d2 + check("H2 flush() returns 0 on an empty queue (no network)", q.flush() == 0) + + # H3 — lock CM + atomic index update are stubs. + import echo_concurrency as c + check("H3 auto_owner is stable", c.auto_owner() == c.auto_owner()) + check("H3 vault_lock returns a context manager (no network until entered)", + hasattr(c.vault_lock(), "__enter__") and hasattr(c.vault_lock(), "__exit__")) + check("H3 atomic_index_update is wired", callable(c.atomic_index_update)) + + # H5 — proposal validation is pure (offline): keeps valid (incl. inbox), drops the rest. + import echo_reflect as rf + v, errs = rf.validate([ + {"title": "Ok", "kind": "person", "confidence": 0.9}, + {"title": "", "kind": "person"}, # missing title + {"title": "Bad", "kind": "bogus"}, # unknown kind + {"title": "Weak", "kind": "person", "confidence": 0.2}, # below confidence floor + {"title": "Note", "inbox": True}, # inbox needs no kind + ]) + check("H5 validate keeps valid (incl. inbox), drops invalid", len(v) == 2 and len(errs) == 3, (len(v), errs)) + check("H5 classify/preview/apply wired", + all(callable(getattr(rf, n, None)) for n in ("classify", "preview", "apply"))) + + # M1 — secret resolution order is implemented; env wins, then the credentials file. + import echo_secrets as s + os.environ["ECHO_KEY"] = "env-key-123" + check("M1 env ECHO_KEY wins", s.resolve_key("legacy-fallback") == "env-key-123") + with tempfile.TemporaryDirectory() as d: + os.environ["ECHO_STATE_DIR"] = d + os.environ.pop("ECHO_KEY", None) + s.write_key("file-key-xyz") + check("M1 write_key -> credentials -> resolve_key reads it", s.resolve_key() == "file-key-xyz") + os.environ["ECHO_KEY_LEGACY_OK"] = "1" # silence the warning for the legacy-path check + os.environ["ECHO_STATE_DIR"] = os.path.join(d, "empty") # an empty dir: no creds file + check("M1 legacy fallback when env+file absent", s.resolve_key("legacy-fallback") == "legacy-fallback") + os.environ["ECHO_KEY"] = "env-key-123" # restore for any later checks + + # M2 — slug collision + link-confidence helpers are implemented. + import echo_quality as qual + check("M2 safe_slug disambiguates a collision", + qual.safe_slug({"foo"}, "foo") != "foo") + check("M2 short/common tokens are not confident links", + qual.is_confident_link("API", "we built an api") is False) + + # M3 — doctor module exposes run(); M4 — output envelope is implemented. + import echo_doctor as doc + check("M3 doctor exposes run()", hasattr(doc, "run")) + import echo_output as out + env = out.envelope("created", {"path": "p.md"}) + check("M4 envelope shape", env.get("ok") is True and env.get("action") == "created") + + print(f"\n{len(failures)} failure(s)" if failures else "\nall scaffold interface checks passed") + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/eval/mock_olrapi_hifi.py b/eval/mock_olrapi_hifi.py new file mode 100644 index 0000000..37b976f --- /dev/null +++ b/eval/mock_olrapi_hifi.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""mock_olrapi_hifi.py — H4: higher-fidelity mock of the Obsidian Local REST API. + +The shipped mock's PATCH is a "naive append": it appends the body to EOF instead of +inserting/replacing under the target heading, so the 0.9 surface (capture's +_append_to_existing, scope set's replace, the daily Agent-Log insert) is only ever +validated against behavior the real API does NOT exhibit. This subclass models the +real PATCH semantics so the eval can catch heading-insert / replace regressions. + +Over the base mock (eval/mock_olrapi.py) this overrides do_PATCH only: + * heading append -> insert at the END of the target section's body (before the next + sibling/parent heading), NOT at EOF. + * heading prepend -> insert at the START of the section body (right after the heading). + * heading replace -> replace the section body, keep the heading line. + * heading missing -> 400 errorCode 40080 (the silent-write-loss trigger). [unchanged] + * frontmatter replace -> parse the YAML block and set/replace the scalar; a replace on a + MISSING field returns 400 40080 (matches the real API, per SKILL.md). + +Everything else (GET/PUT/POST/DELETE, directory listing, document-map, flaky/phantom +fault injection, debug reset) is inherited unchanged from the base handler. +""" +from __future__ import annotations + +import argparse +import json +import re +from http.server import ThreadingHTTPServer + +import mock_olrapi +from mock_olrapi import H as BaseH, STATE + + +def _heading(line: str): + m = re.match(r"^(#{1,6})\s+(.*)$", line) + return (len(m.group(1)), m.group(2).strip()) if m else (0, None) + + +def section_span(lines, target): + """(start, end, level) for the section under `target` (an "A::B" path; matched on its + leaf). Body lines are lines[start+1:end]. None if the heading is absent.""" + leaf = target.split("::")[-1].strip() + start = level = None + for i, line in enumerate(lines): + lv, txt = _heading(line) + if lv and txt == leaf: + start, level = i, lv + break + if start is None: + return None + end = len(lines) + for j in range(start + 1, len(lines)): + lv, _ = _heading(lines[j]) + if lv and lv <= level: + end = j + break + return start, end, level + + +def set_frontmatter(text: str, field: str, raw_value: str): + """Return (new_text, existed). raw_value is the JSON the client PATCHed.""" + try: + value = json.loads(raw_value) + except Exception: + value = raw_value.strip().strip('"') + rendered = value if isinstance(value, str) else json.dumps(value) + new_line = f"{field}: {rendered}" + if not text.startswith("---"): + return f"---\n{new_line}\n---\n\n{text}", False + end = text.find("\n---", 3) + head = text[4:end] if end != -1 else text + rest = text[end:] if end != -1 else "\n---\n" + out, existed = [], False + for line in head.splitlines(): + if re.match(rf"^{re.escape(field)}\s*:", line): + out.append(new_line) + existed = True + else: + out.append(line) + if not existed: + out.append(new_line) + return "---\n" + "\n".join(out) + rest, existed + + +class H(BaseH): + def do_PATCH(self): + vp = self._vpath() + body = self._read_body() + if vp is None: + return self._json(400, {"message": "bad path"}) + ttype = self.headers.get("Target-Type", "") + op = self.headers.get("Operation", "append") + target = self.headers.get("Target", "") + cur = STATE.get(vp, "") + + if ttype == "heading": + lines = cur.split("\n") + span = section_span(lines, target) + if span is None: + return self._json(400, {"errorCode": 40080, "message": "invalid-target"}) + start, end, _ = span + payload = body.strip("\n").split("\n") if body.strip("\n") else [""] + if op == "replace": + lines[start + 1:end] = payload + elif op == "prepend": + lines[start + 1:start + 1] = payload + else: # append at the tail of the section body (before the next heading) + ins = end + while ins > start + 1 and lines[ins - 1].strip() == "": + ins -= 1 + lines[ins:ins] = payload + STATE[vp] = "\n".join(lines) + return self._send(200, "") + + if ttype == "frontmatter": + new, existed = set_frontmatter(cur, target, body) + if op == "replace" and not existed: + return self._json(400, {"errorCode": 40080, "message": "invalid-target"}) + STATE[vp] = new + return self._send(200, "") + + STATE[vp] = cur + "\n" + body + return self._send(200, "") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--port", type=int, default=8800) + a = ap.parse_args() + srv = ThreadingHTTPServer(("127.0.0.1", a.port), H) + print(f"mock-olrapi-hifi listening on http://127.0.0.1:{a.port}", flush=True) + srv.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/eval/run_eval.py b/eval/run_eval.py index d323d89..9c5f342 100644 --- a/eval/run_eval.py +++ b/eval/run_eval.py @@ -33,7 +33,7 @@ from pathlib import Path HERE = Path(__file__).resolve().parent ECHO = HERE.parent / "echo-memory.plugin.src" / "skills" / "echo-memory" / "scripts" / "echo.py" -KEY = "241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" +KEY = "test-key-not-a-real-secret" # ----- tiny HTTP helpers (used for setup, ground truth, and the 0.6 model) ----- def http(method, url, body=None, headers=None): diff --git a/eval/test_features.py b/eval/test_features.py index 67dd9b7..8731b99 100644 --- a/eval/test_features.py +++ b/eval/test_features.py @@ -14,7 +14,7 @@ HERE = Path(__file__).resolve().parent SCRIPTS = HERE.parent / "echo-memory.plugin.src" / "skills" / "echo-memory" / "scripts" ECHO = SCRIPTS / "echo.py" SWEEP = SCRIPTS / "sweep.py" -KEY = "241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab" +KEY = "test-key-not-a-real-secret" failures = [] @@ -102,6 +102,12 @@ def main(): r = h.echo(ECHO, "recall", "MPM") check("recall surfaces linked person", "resources/people/bob-smith.md" in r.stdout, r.stdout) + # 4b. BM25 finds a note by a BODY term. /search/simple is mocked empty, so a hit + # here can only come from the local BM25 recall index (proves H1 lexical layer). + r = h.echo(ECHO, "recall", "principal") + check("recall finds note by body term (BM25)", + "resources/companies/mpm.md" in r.stdout, r.stdout) + # 5. explicit link between two fresh notes h.echo(ECHO, "capture", "Alpha", "--kind", "concept") h.echo(ECHO, "capture", "Beta", "--kind", "concept") @@ -120,10 +126,49 @@ def main(): r = h.echo(SWEEP, "--apply") marker = h.ground("_agent/echo-vault.md") check("sweep runs clean", r.returncode == 0, r.stdout + r.stderr) - check("sweep stamps schema 3", marker and "schema_version=3" in marker.replace(": ", "="), marker) + check("sweep stamps schema 4", marker and "schema_version=4" in marker.replace(": ", "="), marker) idx2 = h.ground("_agent/index/entities.json") check("sweep rebuilt index has all entities", idx2 and all(s in idx2 for s in ["bob-smith", "mpm", "alpha", "beta"])) + rix = h.ground("_agent/index/recall-index.json") + check("sweep builds the recall (BM25) index", + rix is not None and '"postings"' in rix and "principal" in rix, rix) + + # 8. H3 — atomic_index_update re-reads fresh and MERGES, so an entry written by a + # "concurrent" session survives our write; and the advisory lock is released after. + h.http("PUT", f"{base}/vault/_agent/index/entities.json", + '{"schema":1,"updated":"2026-06-21","entities":{"ext-entity":' + '{"path":"resources/concepts/ext-entity.md","kind":"concept","title":"Ext",' + '"aliases":[],"last_seen":"2026-06-21"}}}') + env = dict(os.environ, ECHO_BASE=base, ECHO_KEY=KEY, ECHO_TODAY="2026-06-21", PYTHONPATH=str(SCRIPTS)) + snippet = ("import echo_index as ix, echo_concurrency as c; " + "c.atomic_index_update(lambda d: ix.upsert(d,'mine'," + "'resources/concepts/mine.md','concept','Mine'))") + subprocess.run([sys.executable, "-c", snippet], env=env, capture_output=True, text=True) + idx3 = h.ground("_agent/index/entities.json") + check("H3 atomic update merges a concurrent entry (no clobber)", + bool(idx3) and "ext-entity" in idx3 and "mine" in idx3, idx3) + check("H3 advisory lock released after the transaction", + h.ground("_agent/locks/vault.lock") is None) + + # 9. M4 — --dry-run previews and writes nothing; --json emits a parseable envelope. + r = h.echo(ECHO, "capture", "DryRun Co", "--kind", "company", "--dry-run", "--json") + try: + env = json.loads(r.stdout) + except Exception: + env = {} + check("M4 --dry-run emits a dry-run JSON envelope", + env.get("action", "").startswith("dry-run") and env.get("path") == "resources/companies/dryrun-co.md", r.stdout) + check("M4 --dry-run writes nothing", h.ground("resources/companies/dryrun-co.md") is None) + r = h.echo(ECHO, "capture", "Json Co", "--kind", "company", "--json") + try: + env = json.loads(r.stdout) + except Exception: + env = {} + check("M4 --json envelope reports created + path", + env.get("ok") is True and env.get("action") == "created" + and env.get("path") == "resources/companies/json-co.md", r.stdout) + check("M4 --json capture actually wrote the note", h.ground("resources/companies/json-co.md") is not None) print(f"\n{len(failures)} failure(s)" if failures else "\nall feature tests passed") return 1 if failures else 0 diff --git a/eval/test_offline_queue.py b/eval/test_offline_queue.py new file mode 100644 index 0000000..f9a864f --- /dev/null +++ b/eval/test_offline_queue.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""test_offline_queue.py — H2: writes survive an outage; cold start degrades to cache. + +Phase 1 — vault DOWN: `put` / `append` are queued (exit 0, "queued"), not lost. +Phase 2 — vault UP: `flush` replays them idempotently; the writes land. +Phase 3 — vault UP: `load` caches the orientation reads. +Phase 4 — vault DOWN: `load` serves the last-known-good cache and says OFFLINE. + +Drives the real echo.py against eval/mock_olrapi.py. No creds, no live vault. +Run: python test_offline_queue.py [--port 8840] +""" +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +import tempfile +import time +import urllib.request +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ECHO = HERE.parent / "echo-memory.plugin.src" / "skills" / "echo-memory" / "scripts" / "echo.py" +KEY = "test-key-not-a-real-secret" +DEAD = "http://127.0.0.1:59998" # nothing listens here -> connection refused + +failures = [] + + +def check(name, cond, detail=""): + print(f"{'ok ' if cond else 'FAIL'} {name}" + (f" -- {detail}" if not cond else "")) + if not cond: + failures.append(name) + + +def http(method, url, body=None): + data = body.encode() if isinstance(body, str) else body + req = urllib.request.Request(url, data=data, method=method, + headers={"Authorization": f"Bearer {KEY}"}) + try: + with urllib.request.urlopen(req, timeout=10) as r: + return r.status, r.read().decode("utf-8", "replace") + except Exception as e: # noqa: BLE001 + return getattr(e, "code", 0), "" + + +def tmp(content): + f = tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") + f.write(content); f.close() + return f.name + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--port", type=int, default=8840) + a = ap.parse_args() + mock_base = f"http://127.0.0.1:{a.port}" + state = tempfile.mkdtemp() + + def echo(base, *args): + env = dict(os.environ, ECHO_BASE=base, ECHO_KEY=KEY, ECHO_STATE_DIR=state, + ECHO_VERIFY="0", ECHO_TODAY="2026-06-22") + return subprocess.run([sys.executable, str(ECHO), *args], capture_output=True, text=True, env=env) + + def start_mock(): + p = subprocess.Popen([sys.executable, str(HERE / "mock_olrapi.py"), "--port", str(a.port)], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + for _ in range(50): + try: + urllib.request.urlopen(f"{mock_base}/__debug__reset", data=b"", timeout=1); break + except Exception: + time.sleep(0.1) + return p + + def ground(path): + _, body = http("GET", f"{mock_base}/__debug__?path={path}") + return None if body == "<>" else body + + srv = None + try: + # --- Phase 1: vault DOWN -> writes are queued, not lost ------------------ + r = echo(DEAD, "put", "_agent/sessions/2026-06-22-1200-x.md", tmp("EVALMARK-session\n")) + check("offline put exits 0 (queued)", r.returncode == 0, r.stderr) + check("offline put reports queued", "queued (offline)" in r.stdout, r.stdout) + r = echo(DEAD, "append", "inbox/captures/inbox.md", "- 2026-06-22: EVALMARK-inbox") + check("offline append reports queued", "queued (offline)" in r.stdout, r.stdout) + outbox = Path(state) / "outbox.ndjson" + check("two writes are persisted to the outbox", + outbox.exists() and len(outbox.read_text(encoding="utf-8").splitlines()) == 2) + + # --- Phase 2: vault UP -> flush replays them ----------------------------- + srv = start_mock() + r = echo(mock_base, "flush") + check("flush replays the queue", "flushed 2" in r.stdout, r.stdout + r.stderr) + check("queued PUT landed in the vault", (ground("_agent/sessions/2026-06-22-1200-x.md") or "").find("EVALMARK-session") >= 0) + check("queued APPEND landed in the vault", "EVALMARK-inbox" in (ground("inbox/captures/inbox.md") or "")) + check("outbox is empty after a clean flush", not outbox.exists() or not outbox.read_text(encoding="utf-8").strip()) + + # idempotent replay: a second flush of the same content must not duplicate. + echo(DEAD, "append", "inbox/captures/inbox.md", "- 2026-06-22: EVALMARK-inbox") # re-queue same line + echo(mock_base, "flush") + inbox = ground("inbox/captures/inbox.md") or "" + check("replay is idempotent (no duplicate line)", inbox.count("EVALMARK-inbox") == 1, inbox) + + # --- Phase 3: vault UP -> load caches the orientation reads --------------- + http("PUT", f"{mock_base}/vault/_agent/echo-vault.md", "schema_version: 4\nEVALMARK-marker\n") + r = echo(mock_base, "load") + check("online load shows marker", "EVALMARK-marker" in r.stdout, r.stdout) + check("load wrote a cache dir", (Path(state) / "cache").exists()) + + # --- Phase 4: vault DOWN -> load serves the cache ------------------------ + srv.terminate(); srv.wait(timeout=5); srv = None + r = echo(DEAD, "load") + check("offline load flags OFFLINE", "OFFLINE" in r.stdout, r.stdout) + check("offline load serves cached marker", "EVALMARK-marker" in r.stdout, r.stdout) + + print(f"\n{len(failures)} failure(s)" if failures else "\nall offline-queue tests passed") + return 1 if failures else 0 + finally: + if srv: + srv.terminate() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/eval/test_patch_semantics.py b/eval/test_patch_semantics.py new file mode 100644 index 0000000..e9d7489 --- /dev/null +++ b/eval/test_patch_semantics.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""test_patch_semantics.py — H4: prove the hi-fi mock models REAL PATCH semantics. + +These are exactly the behaviors the shipped naive mock cannot reproduce (it appends +everything at EOF), so without this the section-replace / section-append / missing- +heading / frontmatter-replace paths are untested. Drives the real echo.py against +mock_olrapi_hifi.py. + +Run: python test_patch_semantics.py [--port 8820] +""" +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +import tempfile +import time +import urllib.request +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ECHO = HERE.parent / "echo-memory.plugin.src" / "skills" / "echo-memory" / "scripts" / "echo.py" +KEY = "test-key-not-a-real-secret" + +failures = [] + + +def check(name, cond, detail=""): + print(f"{'ok ' if cond else 'FAIL'} {name}" + (f" -- {detail}" if not cond else "")) + if not cond: + failures.append(name) + + +def http(method, url, body=None, headers=None): + data = body.encode() if isinstance(body, str) else body + req = urllib.request.Request(url, data=data, method=method, + headers={"Authorization": f"Bearer {KEY}", **(headers or {})}) + try: + with urllib.request.urlopen(req, timeout=10) as r: + return r.status, r.read().decode("utf-8", "replace") + except Exception as e: # noqa: BLE001 + return getattr(e, "code", 0), "" + + +def tmp(content): + f = tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") + f.write(content) + f.close() + return f.name + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--port", type=int, default=8820) + a = ap.parse_args() + base = f"http://127.0.0.1:{a.port}" + srv = subprocess.Popen([sys.executable, str(HERE / "mock_olrapi_hifi.py"), "--port", str(a.port)], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + + def echo(*args): + env = dict(os.environ, ECHO_BASE=base, ECHO_KEY=KEY, ECHO_VERIFY="0") + return subprocess.run([sys.executable, str(ECHO), *args], capture_output=True, text=True, env=env) + + def ground(path): + _, body = http("GET", f"{base}/__debug__?path={path}") + return None if body == "<>" else body + + try: + for _ in range(50): + try: + urllib.request.urlopen(f"{base}/__debug__reset", data=b"", timeout=1) + break + except Exception: + time.sleep(0.1) + + doc = "doc.md" + seed = ("---\ntype: note\nupdated: 2026-01-01\n---\n\n" + "# Doc\n\n## Scope\nold scope\n\n## Other\nkeep me\n") + + # replace: Scope body becomes new; Other untouched. + http("PUT", f"{base}/vault/{doc}", seed) + echo("patch", doc, "replace", "heading", "Doc::Scope", tmp("new scope")) + g = ground(doc) or "" + check("heading replace swaps section body", "new scope" in g and "old scope" not in g, g) + check("heading replace leaves sibling section intact", "## Other\nkeep me" in g, g) + + # append: lands INSIDE the section (before '## Other'), not at EOF. + echo("patch", doc, "append", "heading", "Doc::Scope", tmp("appended line")) + g = ground(doc) or "" + check("heading append inserts within the section (not EOF)", + "appended line" in g and g.index("appended line") < g.index("## Other"), g) + + # prepend: lands at the TOP of the section body. + echo("patch", doc, "prepend", "heading", "Doc::Scope", tmp("first line")) + g = ground(doc) or "" + check("heading prepend inserts at section top", + "first line" in g and g.index("first line") < g.index("new scope"), g) + + # missing heading -> 400 -> echo.py exits non-zero (the silent-loss guard). + r = echo("patch", doc, "append", "heading", "Doc::Nope", tmp("lost?")) + check("missing heading fails loud (non-zero exit)", r.returncode != 0, r.stderr) + check("missing heading does not write", "lost?" not in (ground(doc) or "")) + + # frontmatter replace on an existing field updates it. + echo("fm", doc, "updated", "2026-06-22") + g = ground(doc) or "" + check("frontmatter replace updates existing field", "updated: 2026-06-22" in g, g) + + # frontmatter replace on a MISSING field -> 400 (matches real API). + r = echo("fm", doc, "nonexistent_field", "x") + check("frontmatter replace of a missing field fails loud", r.returncode != 0, r.stderr) + + print(f"\n{len(failures)} failure(s)" if failures else "\nall PATCH-semantics tests passed") + return 1 if failures else 0 + finally: + srv.terminate() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/eval/test_reflect.py b/eval/test_reflect.py new file mode 100644 index 0000000..6362370 --- /dev/null +++ b/eval/test_reflect.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""test_reflect.py — H5: session-reflection capture (dry-run vs --apply). + +A dry-run previews and writes NOTHING; --apply routes each proposal through capture +(creating notes, the inbox line, and skipping low-confidence items). Drives the real +echo.py against eval/mock_olrapi.py. No creds, no live vault. + +Run: python test_reflect.py [--port 8850] +""" +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +import tempfile +import time +import urllib.request +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ECHO = HERE.parent / "echo-memory.plugin.src" / "skills" / "echo-memory" / "scripts" / "echo.py" +KEY = "test-key-not-a-real-secret" + +failures = [] + + +def check(name, cond, detail=""): + print(f"{'ok ' if cond else 'FAIL'} {name}" + (f" -- {detail}" if not cond else "")) + if not cond: + failures.append(name) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--port", type=int, default=8850) + a = ap.parse_args() + base = f"http://127.0.0.1:{a.port}" + srv = subprocess.Popen([sys.executable, str(HERE / "mock_olrapi.py"), "--port", str(a.port)], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + + def http(method, url, body=None): + data = body.encode() if isinstance(body, str) else body + req = urllib.request.Request(url, data=data, method=method, + headers={"Authorization": f"Bearer {KEY}"}) + try: + with urllib.request.urlopen(req, timeout=10) as r: + return r.status, r.read().decode("utf-8", "replace") + except Exception as e: # noqa: BLE001 + return getattr(e, "code", 0), "" + + def echo(*args, stdin=None): + env = dict(os.environ, ECHO_BASE=base, ECHO_KEY=KEY, ECHO_VERIFY="1", ECHO_TODAY="2026-06-22") + return subprocess.run([sys.executable, str(ECHO), *args], input=stdin, + capture_output=True, text=True, env=env) + + def ground(path): + _, body = http("GET", f"{base}/__debug__?path={path}") + return None if body == "<>" else body + + try: + for _ in range(50): + try: + urllib.request.urlopen(f"{base}/__debug__reset", data=b"", timeout=1); break + except Exception: + time.sleep(0.1) + http("PUT", f"{base}/vault/_agent/echo-vault.md", "---\nschema_version: 4\n---\n# marker\n") + + proposals = [ + {"title": "Acme Corp", "kind": "company", "body": "A vendor ECHO integrates with.", "confidence": 0.9}, + {"title": "Use uv not pip", "kind": "semantic", "body": "Jason standardizes on uv.", "confidence": 0.95}, + {"title": "half-formed idea", "inbox": True, "confidence": 0.9}, + {"title": "Maybe relevant", "kind": "concept", "confidence": 0.2}, # below floor -> skipped + ] + pfile = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") + json.dump(proposals, pfile); pfile.close() + + # dry-run: previews, writes nothing + r = echo("reflect", pfile.name) + check("dry-run previews", "dry-run" in r.stdout and "Acme Corp" in r.stdout, r.stdout) + check("dry-run drops the low-confidence proposal", "Maybe relevant" not in r.stdout, r.stdout) + check("dry-run writes nothing", ground("resources/companies/acme-corp.md") is None) + + # --apply: routes each through capture + r = echo("reflect", pfile.name, "--apply") + check("apply reports applied count", "applied" in r.stdout, r.stdout + r.stderr) + check("apply creates the company note", + (ground("resources/companies/acme-corp.md") or "").find("type: company") >= 0) + check("apply creates the semantic note", + ground("_agent/memory/semantic/use-uv-not-pip.md") is not None) + check("apply routes the inbox proposal", "half-formed idea" in (ground("inbox/captures/inbox.md") or "")) + check("apply still skips the low-confidence proposal", + ground("resources/concepts/maybe-relevant.md") is None) + + # stdin path also works (proposals piped, not a file) + r = echo("reflect", "-", stdin=json.dumps([{"title": "Piped Co", "kind": "company", "confidence": 0.9}])) + check("reflect reads proposals from stdin", "Piped Co" in r.stdout, r.stdout + r.stderr) + + print(f"\n{len(failures)} failure(s)" if failures else "\nall reflect tests passed") + return 1 if failures else 0 + finally: + srv.terminate() + + +if __name__ == "__main__": + raise SystemExit(main())