Skip to content

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.

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:

DirectoryWritten byContents
projects/vault_note.sh projectone note per registered project (board URL, channel, slots)
runs/vault_note.sh runone note per worker run (issue, PR, outcome, model, lane, duration)
needs-human/vault_note.sh needs-humanone note per open escalation (archived: false)
lessons/vault_note.sh lessonsa mirror of the project’s LESSONS.md

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.

  • run writes runs/<date>-<project>-issue-<N>.md with an outcome (done / needs_human), the deploy status, and — lifted from the run’s run-info.json — the model, worker_lane, and a computed duration_s.
  • needs-human writes needs-human/<project>-<N>.md with the reason. When the item is later resolved, archive flips its archived: false frontmatter to true rather than deleting the note.
  • project and lessons snapshot the project’s board coordinates and its LESSONS.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.

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.

The vault block (per-project, defaults shown):

"vault": {
"distill": true,
"distillMaxBytes": 40000,
"distillModel": "claude-haiku-4-5"
}
  • distill — set to false to disable the LLM summary while keeping the deterministic notes.
  • distillMaxBytes — the trailing-bytes cap on the compacted transcript (default 40000).
  • distillModel — the model for the summary (default claude-haiku-4-5); overridable per-invocation with ECHO_DISTILL_MODEL. Keeping the id in config means distillation can’t silently die when a dated model id retires.