Persistent memory for Claude / CoWork sessions via the **ECHO** Obsidian vault, driven over the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api). No MCP server — the skill makes direct REST calls.
Built for **Jason Stedwell** (Director of Technical Services / Systems Engineer, MPM / ALABAMA wISP), who is both the **operator** and the **architect** of this vault. This is a personal plugin, not for distribution.
This repository (`jason/echo-v.05`) holds the **v0.5 source** of the plugin: the tracked source tree at `echo-memory.plugin.src/` and the built `echo-memory.plugin` package artifact rebuilt from it on each version bump.
---
## Core design principle — the plugin is the single source of truth
Everything that defines how ECHO behaves — bootstrap/repair logic, the operating contract, the taxonomy, frontmatter conventions, the routing map, and the canonical note templates — ships **inside the plugin** under `skills/echo-memory/`.
The vault itself holds **data only**. There are no `CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md` control docs in it. The single in-vault control artifact is a one-line marker, `_agent/echo-vault.md`, that records the schema version.
Three consequences follow:
- **Self-bootstrapping** — point the REST API at an empty Obsidian vault and the plugin stands up the full folder tree, templates, anchors, and marker from its bundled `scaffold/`.
- **Easy to update** — change behavior by updating the plugin, not by editing files scattered through the vault.
- **Portable** — the same plugin brings any empty vault online identically; nothing essential depends on files pre-existing in the vault.
---
## Configuration
The plugin is hardcoded for a single personal endpoint:
- **Server:** `https://echoapi.alwisp.com` (reverse proxy → backend Obsidian Local REST API). This is the **only** valid endpoint — never use LAN addresses (`10.x`, `192.168.x`, `:27124`); those belong to retired memory systems.
- **Auth:** a bearer token stored in the skill / `references`. The key lives only in the plugin — **never inside a vault note** (per the vault's own safety rules).
- **TLS:** the endpoint presents a valid certificate, so `-k` is not needed.
Vault paths are addressed **at the root** (e.g. `GET /vault/_agent/context/current-context.md`).
---
## Requirements
- Obsidian running on the backend with the Local REST API plugin enabled.
- HTTPS reachability to `https://echoapi.alwisp.com` from the Claude / CoWork session environment.
---
## Repository layout
```
echo-v.05/
├── echo-memory.plugin ← built, installable plugin (zip artifact, rebuilt on version bump)
**Division of responsibility:**`SKILL.md` owns day-to-day *procedure* (loading order, search-first, triage, scope switching, PATCH rules). `references/operating-contract.md` owns the durable, client-independent *principles and safety rules*. The other references are the canonical layout, API, and bootstrap specs.
---
## Vault layout (data only, root-addressed)
```
/vault/
├── README.md ← thin human signpost (NOT read for routing)
Load memory at the start of any substantive conversation (anything beyond a single quick factual question). The cold-start reads are **issued in parallel** (one batch of 5–6 GETs):
1.`_agent/echo-vault.md` — the bootstrap marker. `404` → vault not set up (run bootstrap); `200` → check `schema_version` and migrate if older than the plugin's.
4.`_agent/heartbeat/last-session.md` then `_agent/sessions/` — read the heartbeat pointer first (O(1) jump to the latest log); fall back to the listing's ~5 most recent by reverse lexical sort.
6.`inbox/captures/inbox.md` — inbox-depth probe for the load-time reconcile.
After the batch, **reconcile**: if the inbox holds captures older than ~7 days, surface the count and offer to triage (so triage self-fires rather than waiting to be invoked); and if the active scope diverges from what Jason just asked for, run Scope Switching.
If a specific project is in play, follow up with a `search/simple/` across **all** lifecycle subfolders, querying **both the slug and any human title** used in conversation, then read the match at `projects/<lifecycle>/<slug>.md`. Loading is not narrated to the operator.
### Inbox triage
`inbox/captures/inbox.md` is the catch-all. At session start, GET it; if it holds lines older than ~7 days that were never routed, surface a count once and offer to triage. Accepted items route to their proper home (preference → operator-preferences; project idea → `projects/incubating/`; durable fact → semantic; person fact → `resources/people/`), with the move recorded one-line in `inbox/processing-log/YYYY-MM-DD.md` as an audit trail. Originals aren't deleted unless explicitly asked.
### Project lifecycle
Projects move through four folders; the folder name and the `status:` frontmatter field **MUST agree** — they are two views of one state. A file in `projects/active/` with `status: on-hold` is broken state.
| Folder | `status:` | Meaning |
|--------|-----------|---------|
| `incubating/` | `incubating` | Idea captured; not actively worked |
| `active/` | `active` | Current work (default for anything in motion) |
| `on-hold/` | `on-hold` | Paused but still tracked; resumable |
| `archived/` | `archived` | Done, abandoned, or rolled up — not deleted |
Promotion = move the file **and** update `status:` in the same change.
### Scope switching
`_agent/context/current-context.md` tracks one active scope. When scope changes: (1) PATCH-prepend a dated bullet capturing the **prior** scope to `## Scope History`; (2) PATCH-replace `## Scope` with the new scope; (3) bump the frontmatter `updated:`. Scope History is trimmed to the last ~10 entries.
### Daily note — Agent Log
After substantive activity, append a one-line entry to today's daily note's `## Agent Log`. The procedure is resilient: GET the note → if `404`, PUT it from the template → if `200` but the heading is missing, POST the heading. **Heading detection greps the raw markdown for an anchored `^## Agent Log`** — not the document-map JSON, whose headings are `::`-delimited paths (a bug fixed in 0.4.1 that previously appended duplicate headings).
### Session logging
At the end of substantive conversations (those producing decisions, artifacts, or commitments), write a session log to `_agent/sessions/YYYY-MM-DD-HHMM-<slug>.md`. **The four-digit HHMM component is canonical, not optional** — it makes filenames lex-sort in true chronological order, which cold-start loading depends on. New writes are validated against `^\d{4}-\d{2}-\d{2}-\d{4}-[a-z0-9-]+\.md$`.
The journal is one append-only time-series stream; rollups are coarser-grained entries on the same timeline, so they live under `journal/` — there is no separate `reviews/` tree.
Agent self-maintenance (not a journal entry), written to `_agent/health/YYYY-MM-vault-health.md`. The bundled `scripts/vault-lint.sh` mechanically asserts the invariants — folder↔status mismatch, duplicate slugs across lifecycle folders, wikilinks in frontmatter, duplicate `## Agent Log` headings, stale active projects (`updated:` > 30 days), and aging inbox items (> 14 days). Findings are reported, not auto-fixed.
- **Search first (mandatory for new notes).** Before creating any slug-addressed note, `POST /search/simple/?query=<slug>` across the **whole** vault (and search the human title too) — listing one folder misses duplicates elsewhere. On a match: promote/merge from a non-active subfolder, update-in-place in the same folder, or ask which is canonical if elsewhere.
- **Read before append (idempotency).** `POST` is not idempotent — a retry duplicates lines. Before any POST that adds an entry (inbox, Agent Log, an Observations/Log heading), GET the target and skip if the exact line is already present. (Does not apply to PUT or PATCH-replace.)
- **Bump `updated:` on substance.** PATCH the frontmatter `updated:` to today after a meaningful content change (status update, decision, scope switch). Skip it for routine log appends — bump on substance, not heartbeat.
- **Preserve `created:`.** It is the earliest known date the entity was tracked anywhere — never reset it to "today" when merging.
- **No `[[wikilinks]]` in frontmatter** — YAML parses them as nested lists and they break. Cross-references go in a `## Related` body section. `source_notes` holds plain relative-path strings (backward links to inputs only).
---
## REST API operations
Server `https://echoapi.alwisp.com`, bearer auth, root-addressed paths.
| Delete | `DELETE /vault/<path>` | Only on explicit operator request |
**PATCH heading targets must be the full `::`-delimited path from the top-level heading** (e.g. `Operator Preferences::Fact / Pattern`) — a bare subheading name returns `400 invalid-target` (errorCode 40080). GET the document map first when unsure of the exact path, and copy the heading string verbatim.
created:# YYYY-MM-DD — earliest known date the entity was tracked
updated:# YYYY-MM-DD — bumped on meaningful change
tags:[]
agent_written:# true on agent-generated notes
source_notes:[]# plain relative paths (backward links) — NEVER [[wikilinks]]
---
```
`agent_written: true` + a populated `source_notes` is the key signal separating agent-managed content from human-authored content.
### operator-preferences.md — Rules vs Observations
-`## Fact / Pattern` — promoted, deduped, **timeless** rules (no date prefix).
-`## Observations` — **timestamped** raw observations (`- 2026-06-06: ...`); the default landing zone for new evidence.
Observations are promoted into Fact / Pattern (dropping the date) once a pattern stabilizes, and trimmed to the last ~30 entries.
---
## Bootstrap, repair & migration
The plugin carries everything needed to stand up a vault in `scaffold/`; there is no dependency on any in-vault control doc.
- **Probe** — GET `_agent/echo-vault.md`. `200` → bootstrapped (check `schema_version`); `404` → run a fresh bootstrap.
- **Fresh bootstrap** (idempotent, additive — never overwrites): create the folder tree (a one-line README seeds each leaf since Obsidian/git ignore empty dirs) → PUT the 8 templates to their mirrored vault paths → PUT the 3 anchor seeds only if absent (the operator-preferences seed is deliberately empty — no fabricated facts) → PUT the thin vault README → PUT the marker **last** (so the vault is only flagged "set up" once everything is in place) → write a first-run trace (daily note + session log).
- **Repair** — same steps, GET-probing each path and creating only what's missing; malformed files are flagged in the session log, never replaced.
- **Migrations** — when the marker's `schema_version` is older than the plugin's, apply each intervening migration then PATCH the marker. `0 → 1` removed the old in-vault control docs (`CLAUDE.md` / `BOOTSTRAP.md` / `STRUCTURE.md` / `index.md`) in favor of the plugin-as-source-of-truth model.
---
## Skill triggers
| Skill | Triggers |
|-------|----------|
| `echo-memory` | "remember that", "save to memory", "what do you know about me", "load my profile", "check my notes", "log this decision", "add to my inbox" — and proactively at the start of substantive conversations |
---
## Safety rules (operating contract)
- Do not fabricate facts, relationships, or prior decisions.
- Do not mass-restructure the vault unless explicitly asked.
- Do not delete notes unless deletion is explicitly requested and clearly safe.
- Never store secrets or API keys inside a vault note.
- Default to additive updates and explicit status changes over destructive edits.
- Write in **third person** about Jason ("Jason prefers X"). Do not cross-write with other vaults (Bryan's `goldbrain` is separate).
If the API returns a connection error, timeout, or `502` (usually Obsidian / the REST plugin not running), tell Jason once that the vault is unreachable and proceed without memory — don't retry repeatedly.
---
## Version history
| Version | Highlights |
|---------|-----------|
| **0.3.0** | Source promoted from zip-only to a tracked tree (`echo-memory.plugin.src/`); `.plugin` becomes a build artifact. All 7 skill-improvement items applied: search-first before writes, resilient daily Agent Log, `created:` semantics, project lifecycle + folder↔status rule, canonical HHMM session filenames, read-most-recent-N sessions, `source_notes` defined as backward links. |
| **0.4.1** | Bugfix: daily-note Agent Log heading detection now greps raw markdown for `^## Agent Log` instead of the `::`-delimited doc-map JSON (which never matched and appended duplicate headings). Added Scope Switching cold-start test harness. |
| **0.5.0** | Self-bootstrap + control-logic-in-plugin. Plugin becomes the single source of truth: bundled `scaffold/` (8 templates, 3 anchor seeds, thin vault README, marker) bootstraps an empty vault with no external/local-path dependency. New `operating-contract.md` (principles + safety from the old in-vault `CLAUDE.md`); `bootstrap.md` rewritten as a portable bootstrap/repair/migrate manifest. Cold-start probe moved from `/vault/BOOTSTRAP.md` to `_agent/echo-vault.md` (carries `schema_version`). Live vault migrated to data-only. |
| **0.5.1** | Routing-doc consistency pass: decision-mirror heading unified to `## Key Decisions`; stale `Current status` PATCH examples corrected to `Status`; vault-layout inline project example refreshed to the real template. All 17 `projects/active/` notes normalized losslessly to the canonical template heading set; `android-mqtt-shell` moved to `incubating/` (was broken `status: upcoming` in active). Plugin repackaged (21 files). |
| **0.6.0** | Schema 2. **#8 Inbox auto-fire:** the Loading procedure adds an inbox-depth GET and a load-time *Reconcile* step (inbox triage + scope-drift), so triage self-fires. **#10 Routing:** `reviews/` retired — weekly/monthly/quarterly/annual rollups fold into `journal/{weekly,monthly,quarterly,annual}/`, vault-health moves to `_agent/health/`; new `references/routing-map.md` is the complete audited endpoint→logic map. **Recs:** heartbeat pointer operationalized (read first at load, written at session end); new `scripts/vault-lint.sh` mechanically checks vault invariants. Dead refs pruned (`archive/`, `_agent/outputs/`, `resources/source-material`). Migration `1 → 2` in `bootstrap.md`. |