1. capture-update keeps the whole body (was truncating to first line) 2. frontmatter completeness: kind-default status + kind-seeded tags at capture, fm create-or-replace, incomplete-frontmatter lint, sweep backfill (one data source: KIND_STATUS/KIND_REQUIRED_FM) 3. pre-write duplicate gate (exit 76, --merge-into/--force) 4. recall corpus + ranking: sessions/journal indexed (down-weighted), BM25 x freshness x status fusion, recall --json, index schema 2 5. session hooks: SessionStart auto-load, Stop reflection nudge 6. one-tap triage verb with processing-log audit; --json on read verbs +22 mock end-to-end tests; all suites green. Docs current (README, CHANGELOG, SKILL.md, commands, references, MAINTENANCE, eval README). Drops tracked .DS_Store (gitignored); retires README-gretchen.md (moved to gitignored dist/); adds the field report that drove item 2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.7 KiB
Changelog
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 - <date>: <first line> 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):
capturestamps a kind-appropriate defaultstatus(all kinds, not just projects) and seedstagswith the note's kind; new--tags a,benriches it. Reflect/triage proposals accept a"tags"field.echo.py fmis now create-or-replace: on400 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.pygains a kind-scopedincomplete-frontmattercheck (missing/emptystatus/tagson entity notes; block-style lists handled).sweep.py --applybackfills 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 <slug> 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 putkeeps the index current for corpus paths. - Ranking fuses BM25 with freshness (half-life decay on
updated:, floored at 0.6, envECHO_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 --jsonemits 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-sweepallowed-toolsbroadened to match the resolved (variable-dir) invocation and thels/dirnameresolver 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 "<placeholder>" 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()reportsbaked-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 nowgit rm --cached-removed and.gitignoregainsecho-memory-*-*.pluginso labeled builds can't be re-committed (the version-only pointerecho-memory.pluginandecho-memory-<ver>.pluginstay 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 <config.json> [--label <name>]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/--keyorECHO_OWNER/ECHO_BASE/ECHO_KEY.- Baked builds default to
dist/(gitignored) and never touch the sharedecho-memory.pluginpointer. build.py --strip-keynow force-blanks the same constants (guaranteed token-free).config show/doctorreport[baked-in default]when a field resolves from the baked tier.
Changed — artifact hygiene
.gitignoreexcludesdist/and the filled-inecho-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.