Memory behavior unchanged; the major signals a distribution/install break. - Legacy commands/ directory DELETED (skills-only; the 1.6.0 skills were verified on desktop + CoWork and already took precedence). Pre-skills clients install 1.6.0 from the release history. - Artifact policy: the repo tracks only the echo-memory.plugin pointer; the 15 historical versioned zips leave the tree (git history retains them); versioned builds ship as Gitea release assets, one release per v<version> tag, starting v2.0.0. .gitignore: *.plugin except the pointer. - README: post-2.0 repository layout; "packaging for other agent runtimes" note (ports are build targets from the canonical tree, never a second source tree). - ROADMAP-2.0 items ticked; manifest -> 2.0.0. All suites green (25/25 unit + 4 e2e suites); artifact rebuilt skills-only (79 entries, no commands/). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 KiB
Changelog
2.0.0
Packaging & structure major — nothing about memory behavior changes. The major version signals "reinstall, re-clone expectations": how the plugin is distributed and installed is what breaks.
Removed — the legacy commands/ directory (BREAKING for pre-skills clients)
The plugin is skills-only. The eight /echo-* entry points live exclusively at
skills/<name>/SKILL.md (introduced 1.6.0, verified on desktop + CoWork
2026-07-28, where they already took precedence). A Claude Code old enough to lack
skills support loses the slash commands — install 1.6.0 from the release history
instead.
Changed — artifact policy (BREAKING for anyone pulling zips from the repo)
- The repo tracks only
echo-memory.plugin(the current installable). The 15 historicalecho-memory-<version>.pluginzips are deleted from the tree (git history retains them). - Versioned builds are published as Gitea releases on
git.alwisp.com/jason/echo— one release perv<version>tag, artifact attached, starting withv2.0.0. .gitignorenow ignores*.pluginexcept the pointer.
Added — "packaging for other agent runtimes" README note
The retired Codex tree stays retired; the README now documents what a port needs (manifest shape, skill entry point, script invocation + hooks) and the rule that a port must be a build target from the canonical source tree, never a second hand-maintained copy.
1.6.0
Changed — the eight slash commands migrated to the skills format
Each commands/<name>.md now has a skills/<name>/SKILL.md twin with the same
invocation name and a byte-identical body — confirmed against the current docs
that a same-name skill takes precedence over the legacy command, so both
formats coexist safely in this release (deleting commands/ is the 2.0
packaging break). Done for the features, not the deprecation notice:
allowed-toolsper skill — the resolvedpython3/python/py -3script invocations (plus the CoWorkls /sessions/*fallback probe) are pre-authorized, so/echo-load,/echo-health,/echo-recalletc. stop prompting.disable-model-invocation: trueon the write-heavy entry points (/echo-sweep,/echo-triage) — only the operator triggers them; the read-side skills stay model-invocable.argument-hint/$ARGUMENTScarry over unchanged (same semantics in SKILL.md bodies).- The
$ECHOpath-resolution block stays per-skill: the skills format has no cross-skill snippet sharing (each skill directory is self-contained per the official docs), and the block is already the 2-line minimum.
Fixed — lint blind spot: retired trees masked by the leaf-README route
vault_lint.py checked retired patterns only for paths matching no route,
so the permissive leaf-readme route (^(.+/)?README\.md$) absolved any seed
README inside a retired tree — archive/ and _agent/outputs/ survived the
1.5.0 live cleanup unflagged. Retired patterns are now checked first: a
path in a retired tree flags retired-path regardless of what else matches.
New end-to-end test seeds archive/notes/README.md and asserts the flag
(fails against the pre-fix linter, passes now).
Added — plugin.json completeness
homepage + repository → https://git.alwisp.com/jason/echo; manifest →
1.6.0.
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 - <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.