# Changelog ## 1.5.1 ### Fixed — duplicate gate over-firing on vault-common tokens and cross-kind names Live 1.5.0 use surfaced a false positive: creating the decision "echo-memory 1.5.0 improvement set" was blocked by the archived project `echo-v-05` (score 1.0), because `score = overlap / min(|tokens|)` lets any entity whose single distinctive token appears in the new title score 1.0 — and "echo" appears across many entities in an echo-centric vault. New `echo_index.gate_candidates()` (backed by `token_df()`) applies two precision rules before blocking; `capture` gates through it. `fuzzy_candidates()` — the advisory warning list — is unchanged: - **Same-kind only.** A cross-kind name collision (a decision titled after its project, a meeting named for a company) is intentional naming, not a duplicate. Cross-kind lookalikes still warn, never block. - **No single-common-token blocks.** A lone shared token gates only when it is unique to that entity (vault df == 1). Multi-token overlaps still gate even when the individual tokens are common. Exit-76 / `--merge-into` / `--force` semantics unchanged. Tests: 3 new offline unit tests (`gate_candidates`), end-to-end gate cases restructured + 2 new (cross-kind no-gate, common-token no-gate); verified against the live vault (the original false positive now creates cleanly; a same-kind multi-token lookalike still gates). ## 1.5.0 Six improvements from the July 2026 review + the frontmatter-drift field report. ### Fixed — capture-on-update kept only the first body line (silent data loss) `capture` on an existing entity appended `- : ` and **discarded the rest of the body**. It now appends the whole body as a dated block — first line on the bullet, continuation lines indented under it. Idempotency (bullet-per-day) unchanged. ### Added — frontmatter completeness (prevent → tool → detect → remediate) A field audit found 18/71 entity notes missing `status`/`tags`; the drift was created at write time and invisible to the tooling. Four coordinated changes, driven by one data source (`KIND_STATUS` / `KIND_REQUIRED_FM` in `echo_index.py`): - **`capture`** stamps a kind-appropriate default `status` (all kinds, not just projects) and seeds `tags` with the note's kind; new `--tags a,b` enriches it. Reflect/triage proposals accept a `"tags"` field. - **`echo.py fm`** is now **create-or-replace**: on `400 invalid-target` (missing key) it surgically inserts the one line into the frontmatter block and re-PUTs, verified — it previously failed on exactly the notes that needed repair. - **`vault_lint.py`** gains a kind-scoped `incomplete-frontmatter` check (missing/empty `status`/`tags` on entity notes; block-style lists handled). - **`sweep.py --apply`** backfills flagged notes (kind-default status + baseline tag). ### Added — pre-write duplicate gate in capture A strong fuzzy candidate (score ≥ 0.6, env `ECHO_DUP_GATE`) now **stops** `capture` before it creates a near-duplicate (exit `76` + candidate list) instead of warning after the file exists. `--merge-into ` routes the capture as an update to the existing entity (mention learned as an alias); `--force` creates anyway. Weak resemblances still create + warn as before. ### Changed — recall corpus + ranking (recall-index schema 2) - The BM25 corpus now includes **session logs and journal notes** (down-weighted 0.5 / 0.4 so entity notes win ties) — past discussions are findable without having been promoted into an entity note. `echo.py put` keeps the index current for corpus paths. - Ranking fuses BM25 with **freshness** (half-life decay on `updated:`, floored at 0.6, env `ECHO_FRESH_HALF_LIFE`) and **status** (`active` ×1.1, `on-hold` ×0.9, `archived` ×0.75) — a stale archived note no longer outranks the live one. - Hits print `updated:`/`status:`; `recall --json` emits structured results. - The recall index carries per-doc meta (schema 2); an old index is discarded and rebuilt automatically on the next recall/sweep. ### Added — session hooks (self-firing load & reflect) New `hooks/hooks.json` + two hook scripts. **SessionStart** runs `echo.py load` and injects the output as context (skips resume/compact; NOT-CONFIGURED degrades to an instruction, never an error). **Stop** nudges once per substantive session (≥5 user turns, env `ECHO_REFLECT_MIN_TURNS`) when nothing was reflected or session-logged — reflection itself still previews and asks before writing. Both use the 1.4.2 CoWork path fallback and always exit 0. ### Added — one-tap inbox triage + `--json` on read verbs New `echo.py triage`: `--list --json` parses the inbox into `{line, date, text, age_days}`; a proposals file (reflect schema + `"line"`) is classified → previewed → applied via `capture`, with the `inbox/processing-log/` audit line written per move (originals never deleted). `/echo-triage` rewritten around it. Also: `--json` for `recall`, `link`, and `scope show` (`resolve`/`capture` already had it). Tests: +22 feature cases (mock end-to-end incl. hooks), patch-semantics updated for the new `fm` contract; all suites green. Docs: SKILL.md, README, command docs updated. ## 1.4.2 ### Fixed — CoWork sandbox plugin-path resolution In a remote CoWork sandbox, `${CLAUDE_PLUGIN_ROOT}` can resolve to a host path the sandbox can't reach (e.g. `/var/folders/…`), while the plugin is actually mounted under `…/mnt/.remote-plugins/…`. Every `python3 "${CLAUDE_PLUGIN_ROOT}/…"` invocation then failed until the agent manually located the real path. (Root cause is the harness env var, not the plugin — the scripts always used the variable correctly, never a hardcoded path — but the plugin now self-heals.) - SKILL.md and all eight slash commands resolve the scripts path with a fallback: prefer `${CLAUDE_PLUGIN_ROOT}`, else locate the copy under `/sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/`. Reused via `$ECHO`/`$LINT`/`$SWEEP`/`$SDIR`; on a normal host the primary path always wins, so behaviour is unchanged. - `echo-health` / `echo-sweep` `allowed-tools` broadened to match the resolved (variable-dir) invocation and the `ls`/`dirname` resolver steps. ## 1.4.1 ### Fixed — baked credentials now win unconditionally A per-user baked artifact could still be prompted for credentials on install. The baked `DEFAULT_*` tier was lowest priority, so a stale `ECHO_*` env var or a leftover/placeholder `~/.claude/echo-memory/config.json` shadowed the baked key — even an unedited `""` value won over it, flipping `is_configured()` to false and triggering the first-run "ask the operator" flow despite valid baked creds. - `echo_config.load()` now treats a **complete** baked set (endpoint + key both present) as authoritative: it wins over env vars and the config file and cannot be shadowed. The generic (unbaked) plugin keeps its env → file → first-run flow. - Added `baked_complete()`; `source()` reports `baked-in (plugin)` for that tier. ### Security — untracked leaked baked artifacts - Two labeled baked artifacts (`echo-memory-1.4.0-jason.plugin`, `echo-memory-1.4.0-gretchen.plugin`), each carrying a live vault bearer token, had been committed at the repo root. They are now `git rm --cached`-removed and `.gitignore` gains `echo-memory-*-*.plugin` so labeled builds can't be re-committed (the version-only pointer `echo-memory.plugin` and `echo-memory-.plugin` stay tracked). Tokens remain in prior history — rotate if the repo was ever shared. ## 1.4.0 ### Added — per-user baked-key builds (CoWork zero-touch) The plugin is the only thing reliably mounted into a CoWork session (the sandbox's `~/.claude` is synthetic, not your real one), so the config now *can* travel inside the artifact. `echo_config` gains a lowest-priority fallback tier — the `DEFAULT_OWNER` / `DEFAULT_BASE` / `DEFAULT_KEY` constants — **empty in source**, so the committed tree still ships zero credentials. - `build.py --bake-key --from [--label ]` substitutes a single user's owner/endpoint/key into those constants, producing a per-user artifact that needs **no config file and no per-session paste**. Values may also come from `--owner/--endpoint/--key` or `ECHO_OWNER/ECHO_BASE/ECHO_KEY`. - Baked builds default to `dist/` (gitignored) and never touch the shared `echo-memory.plugin` pointer. - `build.py --strip-key` now force-blanks the same constants (guaranteed token-free). - `config show` / `doctor` report `[baked-in default]` when a field resolves from the baked tier. ### Changed — artifact hygiene - `.gitignore` excludes `dist/` and the filled-in `echo-memory.config.json`, where baked builds and the key file are meant to live. A baked artifact carries a vault key and must be **delivered directly to that one user, never committed, pushed, or published.** (Note: labeled baked artifacts built at the repo *root* were not yet guarded — see 1.4.1.) ### Resolution order (unchanged for hosts) `env` → `$ECHO_CONFIG` file → canonical `~/.claude/echo-memory/config.json` → **baked `DEFAULT_*`**. On a normal desktop the empty defaults mean behaviour is identical to 1.3.x; the baked tier only matters in per-user artifacts.