bump to 0.7.1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "echo-memory",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"description": "Persistent memory via the ECHO Obsidian vault over the Local REST API \u2014 no MCP server. Self-bootstrapping: the plugin carries the full vault scaffold and all control logic, so it stands up an empty vault and ports to any other. Ships a validated API client (echo.sh), a machine-readable routing manifest the linter enforces, deterministic bootstrap/migrate scripts, and /echo-load|save|triage|health commands. Reads/writes notes across Claude/CoWork sessions. Jason's personal memory vault.",
|
||||
"author": {
|
||||
"name": "Jason"
|
||||
|
||||
@@ -106,7 +106,7 @@ Do not read every session log — older sessions are reachable via `POST /search
|
||||
**Reconcile at load (do this every cold start, after the batch returns).** The batch already fetched everything needed for a cheap self-check — run it before diving into the work so memory maintains itself instead of drifting:
|
||||
|
||||
1. **Inbox depth (Inbox Triage).** If `inbox/captures/inbox.md` (GET #6) holds dated capture lines older than ~7 days that were never routed, surface the count once and offer to triage — see **Inbox Triage** below. This is the load-time trigger that makes triage self-firing rather than something you only run when asked.
|
||||
2. **Scope drift.** Compare `## Scope` in `current-context.md` (GET #3) against what Jason just asked for. If they diverge, follow **Scope Switching** to record the prior scope and set the new one.
|
||||
2. **Scope drift (state it, don't just check it).** Scope is the most churn-prone state — Jason runs several sessions a day across different topics, so the recorded `## Scope` is frequently stale at load. **Silently working under a stale scope is the default failure mode.** To prevent it, at load read the active scope and its freshness in one call — `echo.sh scope show` (prints `## Scope`, `scope_updated`, and how many sessions have been logged since) — and form a one-line judgment: *does this session's request match the recorded scope?* If it diverges, switch **before** doing the work via `echo.sh scope set "<new scope>"` (see **Scope Switching**). If `scope show` reports several sessions logged since the last switch, treat the recorded scope as suspect and confirm with Jason rather than trusting it.
|
||||
|
||||
Keep the reconcile to a single short line to Jason (e.g. "3 inbox captures from last week are still un-routed — triage now?"); don't let it crowd out the actual request.
|
||||
|
||||
@@ -307,16 +307,22 @@ curl -s -X POST \
|
||||
|
||||
## Scope Switching (`current-context.md`)
|
||||
|
||||
`_agent/context/current-context.md` tracks a single active scope. Jason routinely shifts scope within a day (echo plugin → MPM brand → WISP docs).
|
||||
`_agent/context/current-context.md` tracks a single active scope. Jason routinely shifts scope within a day (echo plugin → MPM brand → WISP docs), so this is high-churn — switch deliberately, every time the work changes topic.
|
||||
|
||||
When scope changes:
|
||||
**Preferred — one command:**
|
||||
|
||||
1. PATCH `prepend` a dated bullet to `## Scope History` capturing the **prior** scope (one line: `- 2026-06-06: <prior scope summary>`). If `## Scope History` doesn't exist yet, POST the heading first, same pattern as the daily-note Agent Log.
|
||||
2. PATCH `replace` `## Scope` with the new scope.
|
||||
3. PATCH the frontmatter `updated:` field.
|
||||
```bash
|
||||
"$ECHO" scope set "<new scope summary>"
|
||||
```
|
||||
|
||||
`scope set` does the whole switch atomically and correctly: it archives the **prior** scope to `## Scope History` (dated, truncated), replaces `## Scope` with the new text, and stamps the `scope_updated:` frontmatter timestamp. That timestamp is the **freshness signal** — it's what `echo.sh scope show` and the `vault-lint.sh` drift check read to tell whether the recorded scope still reflects current work. Always switch through `scope set` so `scope_updated` stays honest; a hand-edited `## Scope` that skips the stamp reintroduces silent drift.
|
||||
|
||||
**Manual fallback** (only if `echo.sh` is unavailable): PATCH `prepend` the prior scope to `## Scope History`, PATCH `replace` `## Scope`, then PATCH the frontmatter `scope_updated:` (and `updated:`) to today. Note `scope_updated` must already exist in frontmatter — a `PATCH replace` on a missing field returns `400 invalid-target`; run `bootstrap.sh` repair to add it.
|
||||
|
||||
This keeps a rolling trail of recent scopes in one file instead of spawning separate stash notes. Trim Scope History to the last ~10 entries when it grows past that.
|
||||
|
||||
**Drift backstop:** `vault-lint.sh` flags when ≥ `SCOPE_STALE_SESSIONS` (default 3) session logs are dated after `scope_updated` — i.e. work happened without a scope switch. It's advisory (surfaced in Vault Health / `/echo-health`), the mechanical safety net under the load-time judgment above.
|
||||
|
||||
## Journal Rollups (the journal is one continuum)
|
||||
|
||||
The journal is a single append-only chronological stream. Rollups are just coarser-grained journal entries over the same timeline, so they **all live under `journal/`** — there is no separate `reviews/` tree. One place to read the whole time-series story, daily through annual.
|
||||
|
||||
@@ -7,6 +7,7 @@ tags: [agent, context]
|
||||
agent_written: true
|
||||
source_notes: []
|
||||
scope:
|
||||
scope_updated: {{DATE}}
|
||||
refresh_strategy: on-demand
|
||||
---
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
# echo.sh delete <path> # DELETE (destructive; explicit use only)
|
||||
# echo.sh lock <owner-id> # acquire advisory lock (exit 75 if held by someone else & fresh)
|
||||
# echo.sh unlock <owner-id> # release advisory lock if owned by <owner-id>
|
||||
# echo.sh scope show # print active scope, its freshness, and sessions-since
|
||||
# echo.sh scope set "<text>" # switch scope atomically (history + replace + stamp scope_updated)
|
||||
#
|
||||
# Exit codes: 0 ok · 44 not-found(404) · 75 lock-held · 2 usage · 1 other HTTP/transport error.
|
||||
|
||||
@@ -194,6 +196,50 @@ case "$cmd" in
|
||||
[ "$HTTP" = "404" ] || _check "unlock"
|
||||
echo "ok: unlocked" ;;
|
||||
|
||||
scope)
|
||||
# scope show | scope set "<new scope text>"
|
||||
# 'set' archives the prior scope to ## Scope History, replaces ## Scope, and stamps
|
||||
# the scope_updated freshness timestamp — one command instead of three error-prone PATCHes.
|
||||
sub="${1:-show}"; shift || true
|
||||
ccpath="_agent/context/current-context.md"
|
||||
case "$sub" in
|
||||
show)
|
||||
_curl GET "$(_vault_url "$ccpath")"; _check "scope show"
|
||||
cur="$RESP"
|
||||
echo "── Active scope ──"
|
||||
awk '/^## Scope[[:space:]]*$/{f=1;next} /^## /{if(f)exit} f' "$cur"
|
||||
su="$(sed -n 's/^scope_updated:[[:space:]]*//p' "$cur" | head -1)"
|
||||
su="${su//\"/}"
|
||||
echo "scope_updated: ${su:-<missing — drift cannot be detected; run scope set or repair>}"
|
||||
_curl GET "$(_vault_url "_agent/sessions/")"
|
||||
if [ "$HTTP" = "200" ] && [ -n "$su" ]; then
|
||||
n="$(python3 -c "import json,sys;f=json.load(open('$RESP'))['files'];print(sum(1 for x in f if x.endswith('.md') and x[:10]>'$su'))" 2>/dev/null || echo '?')"
|
||||
echo "sessions logged since: ${n}"
|
||||
fi ;;
|
||||
set)
|
||||
[ $# -ge 1 ] || die "scope set needs the new scope text"
|
||||
new="$1"
|
||||
_curl GET "$(_vault_url "$ccpath")"; _check "scope set(read)"
|
||||
prior="$(awk '/^## Scope[[:space:]]*$/{f=1;next} /^## /{if(f)exit} f' "$RESP" \
|
||||
| tr '\n' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | cut -c1-140)"
|
||||
BODY="$(mktemp)"; printf -- '- %s: %s\n' "$(_today)" "${prior:-(prior scope)}" > "$BODY"
|
||||
_curl PATCH "$(_vault_url "$ccpath")" -H 'Operation: prepend' -H 'Target-Type: heading' \
|
||||
-H 'Target: Current Context::Scope History' -H 'Content-Type: text/markdown' --data-binary @"$BODY"
|
||||
_check "scope set(history)"
|
||||
BODY="$(mktemp)"; printf '%s\n' "$new" > "$BODY"
|
||||
_curl PATCH "$(_vault_url "$ccpath")" -H 'Operation: replace' -H 'Target-Type: heading' \
|
||||
-H 'Target: Current Context::Scope' -H 'Content-Type: text/markdown' --data-binary @"$BODY"
|
||||
_check "scope set(replace)"
|
||||
BODY="$(mktemp)"; printf '"%s"' "$(_today)" > "$BODY"
|
||||
_curl PATCH "$(_vault_url "$ccpath")" -H 'Operation: replace' -H 'Target-Type: frontmatter' \
|
||||
-H 'Target: scope_updated' -H 'Content-Type: application/json' --data-binary @"$BODY"
|
||||
if [ "${HTTP:-000}" -ge 400 ]; then
|
||||
die "scope set: body switched, but scope_updated frontmatter is missing (run bootstrap.sh repair to add it) [HTTP $HTTP]"
|
||||
fi
|
||||
echo "ok: scope switched (prior archived to Scope History; scope_updated=$(_today))" ;;
|
||||
*) die "scope: use 'show' or 'set \"<text>\"'" ;;
|
||||
esac ;;
|
||||
|
||||
""|-h|--help|help) usage ;;
|
||||
*) die "unknown command '$cmd' (try: get map ls search put post append patch fm bump delete lock unlock)" ;;
|
||||
*) die "unknown command '$cmd' (try: get map ls search put post append patch fm bump delete lock unlock scope)" ;;
|
||||
esac
|
||||
|
||||
@@ -28,9 +28,11 @@ ECHO_BASE="${ECHO_BASE:-https://echoapi.alwisp.com}"
|
||||
ECHO_KEY="${ECHO_KEY:-241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab}"
|
||||
STALE_DAYS="${STALE_DAYS:-30}"
|
||||
INBOX_DAYS="${INBOX_DAYS:-14}"
|
||||
SCOPE_STALE_SESSIONS="${SCOPE_STALE_SESSIONS:-3}"
|
||||
ECHO_TODAY="${ECHO_TODAY:-$(date +%Y-%m-%d)}"
|
||||
|
||||
ECHO_BASE="$ECHO_BASE" ECHO_KEY="$ECHO_KEY" STALE_DAYS="$STALE_DAYS" INBOX_DAYS="$INBOX_DAYS" \
|
||||
SCOPE_STALE_SESSIONS="$SCOPE_STALE_SESSIONS" \
|
||||
ECHO_TODAY="$ECHO_TODAY" ROUTING_JSON="$SCRIPT_DIR/routing.json" \
|
||||
python3 - <<'PY'
|
||||
import os, sys, json, re, datetime, urllib.request, urllib.error
|
||||
@@ -39,6 +41,7 @@ BASE = os.environ["ECHO_BASE"].rstrip("/")
|
||||
KEY = os.environ["ECHO_KEY"]
|
||||
STALE_DAYS = int(os.environ["STALE_DAYS"])
|
||||
INBOX_DAYS = int(os.environ["INBOX_DAYS"])
|
||||
SCOPE_STALE_SESSIONS = int(os.environ["SCOPE_STALE_SESSIONS"])
|
||||
TODAY = datetime.date.fromisoformat(os.environ["ECHO_TODAY"])
|
||||
ROUTING_JSON = os.environ["ROUTING_JSON"]
|
||||
LIFECYCLES = ["active", "incubating", "on-hold", "archived"]
|
||||
@@ -260,6 +263,26 @@ for line in inbox.splitlines():
|
||||
if d and (TODAY - d).days > INBOX_DAYS:
|
||||
flag("aging-inbox", f"inbox capture {d} ({(TODAY-d).days}d): {line.strip()[:80]}")
|
||||
|
||||
# ---- Scope freshness (drift detector) ----------------------------------------
|
||||
# Scope is the most churn-prone state (Jason runs several sessions/day across topics).
|
||||
# It has no natural staleness signal, so drift is otherwise invisible. Rule: if N+ session
|
||||
# logs are dated AFTER current-context's scope_updated, the recorded scope may no longer
|
||||
# reflect current work — surface it for a human glance (advisory, like every health finding).
|
||||
cc = get("_agent/context/current-context.md")
|
||||
if cc is not None:
|
||||
_, ccfm = parse_fm(cc)
|
||||
su = parse_date(ccfm.get("scope_updated"))
|
||||
if su is None:
|
||||
flag("scope-no-timestamp",
|
||||
"_agent/context/current-context.md: no scope_updated frontmatter — scope drift cannot be detected; add it (bootstrap.sh repair) and switch scope via `echo.sh scope set`")
|
||||
else:
|
||||
since = [p for p in all_files
|
||||
if (m := re.match(r"^_agent/sessions/(\d{4}-\d{2}-\d{2})", p))
|
||||
and (d := parse_date(m.group(1))) and d > su]
|
||||
if len(since) >= SCOPE_STALE_SESSIONS:
|
||||
flag("scope-stale",
|
||||
f"scope set {su}; {len(since)} session(s) logged since without a switch — confirm it still reflects current work (or run `echo.sh scope set`)")
|
||||
|
||||
# ---- Report ------------------------------------------------------------------
|
||||
if not violations:
|
||||
print("vault-lint: clean — all invariants hold.")
|
||||
@@ -283,6 +306,8 @@ labels = {
|
||||
"future-date": "updated date is in the future",
|
||||
"source-notes-wikilink": "Wikilink in source_notes (must be plain paths)",
|
||||
"routing-manifest": "routing.json problem",
|
||||
"scope-no-timestamp": "current-context has no scope_updated (drift undetectable)",
|
||||
"scope-stale": f"Scope may have drifted (>= {SCOPE_STALE_SESSIONS} sessions since last switch)",
|
||||
}
|
||||
for check, msgs in by.items():
|
||||
print(f"## {labels.get(check, check)}")
|
||||
|
||||
Reference in New Issue
Block a user