The echo-vault
The echo-vault is an optional, write-only record of everything Echo does,
laid down as Obsidian-native Markdown (Dataview frontmatter plus [[wikilinks]]).
It is a human-readable audit trail — never load-bearing. If no vault is
configured, every vault call is a silent no-op and the pipeline runs exactly the
same.
Enabling the vault
Section titled “Enabling the vault”The vault path is a registry global, not per-project — one vault collects
notes from every registered project. Set globals.vaultPath in the registry (or
override per-invocation with ECHO_VAULT_PATH). Unset means disabled.
Notes land in four subdirectories under the vault root:
| Directory | Written by | Contents |
|---|---|---|
projects/ | vault_note.sh project | one note per registered project (board URL, channel, slots) |
runs/ | vault_note.sh run | one note per worker run (issue, PR, outcome, model, lane, duration) |
needs-human/ | vault_note.sh needs-human | one note per open escalation (archived: false) |
lessons/ | vault_note.sh lessons | a mirror of the project’s LESSONS.md |
vault_note.sh — the write-side notes
Section titled “vault_note.sh — the write-side notes”vault_note.sh <subcommand> is best-effort by construction: an unresolved or
unset vault exits 0 immediately, and it always exits 0. It’s called from the
terminal transitions — reconcile.sh, finalize_watcher.sh, and
flip_to_needs_human.sh — so the vault tracks the same state changes the board
does.
runwritesruns/<date>-<project>-issue-<N>.mdwith anoutcome(done/needs_human), the deploy status, and — lifted from the run’srun-info.json— themodel,worker_lane, and a computedduration_s.needs-humanwritesneeds-human/<project>-<N>.mdwith the reason. When the item is later resolved,archiveflips itsarchived: falsefrontmatter totruerather than deleting the note.projectandlessonssnapshot the project’s board coordinates and itsLESSONS.md.
Worker-controlled values are sanitized before they reach a note: a non-numeric
issue/pr is blanked (so a crafted value can’t inject YAML keys or corrupt the
filename), and free-text reason/project fields are JSON-encoded (valid YAML)
before landing in frontmatter.
distill_run.sh — the LLM run summary
Section titled “distill_run.sh — the LLM run summary”A deterministic run note records what happened; distill_run.sh layers on
what was learned. It reads the worker’s worker.jsonl transcript and asks a
small model to summarize it under four headings — Decisions, Patterns,
Solutions, Blockers — then splices that summary into the existing run
note between <!-- echo:distill:start --> / <!-- echo:distill:end --> markers.
The splice is idempotent: a prior marker block is stripped before the fresh one
is appended, so re-runs don’t accumulate.
Like the notes themselves, distillation is never load-bearing — it silently
no-ops on any of: no vault, vault.distill: false, no executable claude
binary, a missing/empty transcript, or empty model output. The deterministic
note is left untouched in every case.
Token hygiene and secret safety: the transcript handed to the model is
compacted to assistant text plus tool-use names only — no tool inputs, no
tool-result payloads — and truncated to the last vault.distillMaxBytes. That
both bounds tokens and prevents a secret that flowed through a tool result from
being exfiltrated into a note.
Configuration
Section titled “Configuration”The vault block (per-project, defaults shown):
"vault": { "distill": true, "distillMaxBytes": 40000, "distillModel": "claude-haiku-4-5"}distill— set tofalseto disable the LLM summary while keeping the deterministic notes.distillMaxBytes— the trailing-bytes cap on the compacted transcript (default40000).distillModel— the model for the summary (defaultclaude-haiku-4-5); overridable per-invocation withECHO_DISTILL_MODEL. Keeping the id in config means distillation can’t silently die when a dated model id retires.
See also
Section titled “See also”- The run lifecycle — the terminal transitions that write notes.
- Config reference — the
vaultblock andglobals.vaultPath.