Files
echo/CHANGELOG.md
T
Jason Stedwell 446cd9a0ff 2.1.0 — quick-wins train: brief load, offline-durable capture, session-end verb
Three independently useful changes (IMPROVEMENT-PLANS #1/#3/#7), no schema changes:

- load --brief: token-budgeted cold-start digest (facts full, last-10 observations,
  scope+freshness, last session's key sections, agent-log lines, inbox count;
  ECHO_LOAD_BUDGET ~8000 chars, lowest-priority-first trimming). SessionStart hook
  injects the brief form; /echo-load keeps the full dump. All load reads (+ the
  sessions listing, which also feeds the heartbeat fallback) fetched in parallel.
- Offline capture durability: a fully-offline capture queues the WHOLE op as one
  semantic record; flush replays it through capture so routing/gate/aliasing re-run
  against the current index; a gate stop on replay is kept + flagged
  (needs_attention, surfaced by flush and load), never landed blind; update-path
  and ensure_daily_log writes ride safe_request; same-args captures dedupe.
- session-end: one call, one lock — session log -> agent-log line -> reflect
  proposals (gate-aware) -> optional scope set -> heartbeat LAST as the commit
  marker; dry-run default; ECHO_NOW pins HHMM; validation runs before any write.
  Stop hook nudge names the command and recognizes it as reflected.
- Fix: main() now catches the __main__ twin-module EchoError (via RuntimeError +
  .code) so helper-module errors exit with their intended codes, not tracebacks.

+19 e2e checks; all suites green. Plans renumbered: MCP container is 2.2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 22:34:42 -05:00

298 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
## 2.1.0
The "quick-wins train" from the 2026-07-28 review (`docs/IMPROVEMENT-PLANS.md` #1,
#3, #7). Three independently useful changes; no schema or layout changes.
### Added — `load --brief`: a token-budgeted cold-start digest
The SessionStart hook injected the full text of six files every session (measured
15.9KB and growing without bound — Observations and Scope History never shrink).
`load --brief` renders a digest instead: Fact / Pattern rules in full, the last ~10
Observations (with a `+N older` note), scope + `scope_updated` + sessions-since,
the heartbeat-pointed session log's key sections (Goal / Decisions Made / Open
Threads / Suggested Next Step), today's Agent Log lines, and the inbox as a
**count + oldest age** — everything the load-time reconcile needs. Over
`ECHO_LOAD_BUDGET` (default ~8000 chars), sections trim lowest-priority-first
(observations → agent log → session summary) with explicit `(truncated)` markers.
The hook now injects the brief form; `/echo-load` and the manual procedure keep the
full dump. All `load` reads (plus the sessions listing, which also feeds the
heartbeat-absent fallback) are fetched **in parallel** over the pooled connections.
### Fixed — capture is now offline-durable (the write path's inversion bug)
Low-level verbs queued on an outage, but the *default* write (`capture`) died: the
update path's POST/PATCH and `ensure_daily_log` bypassed the queue entirely, and an
unreachable index aborted the whole op. Now: (1) a fully-offline `capture` queues
the **whole operation as one semantic record** (title/kind/body/tags/…, plus the
capture-time date); `flush` replays it **through `capture`** so create-vs-update,
the duplicate gate, and aliasing re-run against the index as it is at replay time —
byte-level replay would freeze a decision made blind. (2) A duplicate-gate stop on
replay keeps the record, flags it `needs_attention` (surfaced by `flush` and
`load`), and does NOT block later records. (3) Mid-capture outages: the update
path and `ensure_daily_log` writes ride `safe_request` (accepted edge: a queued
agent-log PATCH 400-drops loudly if the daily note never existed — the trace line,
not the memory). Same-args offline captures dedupe by idem-key.
### Added — `session-end`: the one-call session close
`echo.py session-end <bundle.json> [--apply]` performs the whole end-of-session
sequence under ONE lock acquisition, in order: session log PUT → Agent Log line →
reflect proposals (via `capture`, gate-aware) → optional `scope set` → **heartbeat
LAST as the commit marker** (a part-way failure leaves the previous pointer intact,
never dangling). Dry-run by default previews the full plan including the reflect
classification. Filename HHMM from `$ECHO_NOW` (else local clock), validated
against the canonical session-log regex **before any write**. The Stop hook's nudge
now names the command, and recognizes a `session-end` invocation as
"already reflected".
### Fixed — helper-module errors exited 2 instead of their intended codes
When `echo.py` runs as `__main__`, helper modules' `import echo` creates a twin
module whose `EchoError` is a different class object, so `main()`'s handler missed
it and the raw traceback leaked. The handler now catches `RuntimeError` (both
classes subclass it) and honors `.code`.
Tests: +19 end-to-end checks (brief-load digest/budget, offline-capture queue /
replay-through-capture / gate-on-replay / idem-key dedup, session-end dry-run /
apply / ordering / validation-abort). All suites green.
## 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
historical `echo-memory-<version>.plugin` zips are deleted from the tree (git
history retains them).
- Versioned builds are published as **Gitea releases** on
`git.alwisp.com/jason/echo` — one release per `v<version>` tag, artifact
attached, starting with `v2.0.0`.
- `.gitignore` now ignores `*.plugin` except 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-tools` per skill** — the resolved `python3/python/py -3` script
invocations (plus the CoWork `ls /sessions/*` fallback probe) are
pre-authorized, so `/echo-load`, `/echo-health`, `/echo-recall` etc. stop
prompting.
- **`disable-model-invocation: true`** on the write-heavy entry points
(`/echo-sweep`, `/echo-triage`) — only the operator triggers them; the
read-side skills stay model-invocable.
- `argument-hint` / `$ARGUMENTS` carry over unchanged (same semantics in
SKILL.md bodies).
- The `$ECHO` path-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`):
- **`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 <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 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 `"<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()` 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-<ver>.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 <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/--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.