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>
This commit is contained in:
Jason Stedwell
2026-07-28 22:34:42 -05:00
parent 1deef7299e
commit 446cd9a0ff
16 changed files with 702 additions and 81 deletions
@@ -605,9 +605,136 @@ def cmd_scope(subcommand: str, text: str | None = None, as_json: bool = False) -
return 0
def cmd_load() -> int:
"""Cold-start orientation: the canonical 6 reads in one call. 404s on today's
daily note and the inbox are normal (printed as absent, not errors)."""
def _fetch_statuses(paths: list[str]) -> dict[str, tuple[int, bytes]]:
"""Concurrently GET vault paths keeping (status, body) per path — unlike read_many,
the 404-vs-offline distinction survives, which cmd_load's cache logic needs."""
if not paths:
return {}
workers = min(MAX_WORKERS, len(paths))
with ThreadPoolExecutor(max_workers=workers) as pool:
return dict(zip(paths, pool.map(lambda p: request("GET", vault_url(p)), paths)))
def _bullet_lines(text: str) -> list[str]:
return [ln for ln in text.splitlines() if ln.lstrip().startswith("- ")]
def _fm_line(text: str, field: str) -> str:
for ln in text.splitlines():
if ln.startswith(f"{field}:"):
return ln.split(":", 1)[1].strip().strip('"').strip("'")
return ""
def _render_brief(texts: dict[str, str | None], listing_files: list[str],
offline: bool) -> str:
"""The token-budgeted cold-start digest (2.1.0). Selection over compression: Fact /
Pattern rules in full, recent Observations only, scope + freshness (not the whole
history), the last session's key sections, today's Agent Log lines, inbox COUNT.
Sections are trimmed lowest-priority-first when over ECHO_LOAD_BUDGET."""
budget = int(os.environ.get("ECHO_LOAD_BUDGET", "8000"))
S: list[tuple[str, str]] = [] # (section-name, text) in display order
marker = texts.get("marker")
if marker is None:
S.append(("marker", "marker: _agent/echo-vault.md ABSENT — vault not bootstrapped "
"(run bootstrap.py before relying on memory)"))
else:
ver = _fm_line(marker, "schema_version") or "?"
S.append(("marker", f"marker: _agent/echo-vault.md — bootstrapped, schema_version {ver}"))
ctx = texts.get("context")
if ctx:
scope = extract_heading(ctx, "Scope") or "(empty)"
upd = _fm_line(ctx, "scope_updated") or "unknown"
since = sum(1 for f in listing_files if f.endswith(".md") and f[:10] > upd) \
if upd != "unknown" else None
head = f"scope (updated {upd}"
if since is not None:
head += f", {since} session(s) logged since"
S.append(("scope", head + "):\n" + scope))
else:
S.append(("scope", "scope: current-context.md absent"))
prefs = texts.get("preferences")
if prefs:
fact = extract_heading(prefs, "Fact / Pattern")
if fact:
S.append(("facts", "preferences — Fact / Pattern:\n" + fact))
obs = _bullet_lines(extract_heading(prefs, "Observations"))
if obs:
shown = obs[-10:]
head = f"preferences — Observations (last {len(shown)} of {len(obs)}):"
S.append(("observations", head + "\n" + "\n".join(shown)))
hb = texts.get("heartbeat")
if hb and hb.strip():
pointer = hb.strip().splitlines()[0]
sess_path = pointer.split(" @ ", 1)[0].strip()
part = [f"last session: {pointer}"]
log_text = texts.get("_pointed_session")
if log_text:
for h in ("Goal", "Decisions Made", "Open Threads", "Suggested Next Step"):
sec = extract_heading(log_text, h)
if sec and sec.strip("() \n"):
part.append(f" {h}: " + " / ".join(ln.strip() for ln in sec.splitlines()
if ln.strip())[:400])
elif sess_path:
part.append(" (session log not readable — see the path above)")
S.append(("session", "\n".join(part)))
elif listing_files:
recent = sorted((f for f in listing_files if f.endswith(".md")), reverse=True)[:3]
S.append(("session", "last session: heartbeat absent — recent logs:\n"
+ "\n".join(f" _agent/sessions/{f}" for f in recent)))
today_note = texts.get("today")
if today_note:
log_lines = _bullet_lines(extract_heading(today_note, "Agent Log"))
S.append(("agent-log", "today's Agent Log:\n" + ("\n".join(log_lines) or " (empty)")))
else:
S.append(("agent-log", "today's Agent Log: (no daily note yet)"))
inbox = texts.get("inbox")
if inbox:
items = re.findall(r"(?m)^\s*-\s*(\d{4}-\d{2}-\d{2})", inbox)
if items:
try:
oldest = (dt.date.fromisoformat(today()) - dt.date.fromisoformat(min(items))).days
S.append(("inbox", f"inbox: {len(items)} capture(s), oldest {oldest}d — "
"offer triage if any are older than ~7 days"))
except ValueError:
S.append(("inbox", f"inbox: {len(items)} capture(s)"))
else:
S.append(("inbox", "inbox: empty"))
else:
S.append(("inbox", "inbox: empty"))
def total() -> int:
return sum(len(t) + 2 for _, t in S)
# Over budget -> trim lowest-priority sections first, with explicit markers.
for name, keep in (("observations", 4), ("agent-log", 4), ("session", 9)):
if total() <= budget:
break
for i, (n, t) in enumerate(S):
if n == name:
lines = t.splitlines()
if len(lines) > keep:
S[i] = (n, "\n".join(lines[:keep]) + "\n (truncated — /echo-load for full)")
out = f"ECHO load (brief) — {today()}\n\n" + "\n\n".join(t for _, t in S)
if len(out) > budget:
out = out[:budget] + "\n(truncated — /echo-load for full)"
if offline:
out += ("\n\nNOTE: vault unreachable — context above is last-known-good cache; "
"writes this session will be queued and synced when the vault returns.")
return out
def cmd_load(brief: bool = False) -> int:
"""Cold-start orientation: the canonical 6 reads in one call (fetched in parallel).
404s on today's daily note and the inbox are normal (printed as absent, not errors).
`brief` renders the token-budgeted digest the SessionStart hook injects; the default
full mode (and /echo-load) prints the raw sections unchanged."""
if not echo_config.is_configured(_CFG):
print("echo: NOT CONFIGURED — this machine has no usable ECHO key file yet.")
print(f"Expected at: {echo_config.config_path()}")
@@ -631,35 +758,77 @@ def cmd_load() -> int:
synced = echo_queue.flush()
if synced:
print(f"(synced {synced} write(s) queued during a prior offline session)\n")
flagged = echo_queue.needs_attention()
if flagged:
print(f"NOTE: {len(flagged)} queued write(s) need attention (e.g. a capture "
"stopped at the duplicate gate on replay) — resolve with capture "
"--merge-into/--force; `echo.py flush` re-lists them.\n")
except Exception as exc: # noqa: BLE001 — never let queue upkeep block a load
print(f"(queue flush skipped: {exc})\n", file=sys.stderr)
# All orientation reads (+ the sessions listing) fetched in parallel over the
# warm connection pool — the listing feeds both the brief digest's scope-freshness
# count and the heartbeat-absent fallback, so it's no longer a second round-trip.
listing_path = "_agent/sessions/"
results = _fetch_statuses([p for _, p in targets] + [listing_path])
marker_missing = False
heartbeat_absent = False
offline = False
texts: dict[str, str | None] = {}
for label, path in targets:
status, body = request("GET", vault_url(path))
status, body = results[path]
if status == 200:
echo_queue.cache_put(path, body) # refresh last-known-good for offline fallback
texts[label] = body.decode(errors="replace")
elif status == 0: # vault unreachable -> degrade to last-known-good cache
offline = True
cached = echo_queue.cache_get(path)
texts[label] = cached.decode(errors="replace") if cached is not None else None
else:
texts[label] = None
if status == 404:
if label == "marker":
marker_missing = True
if label == "heartbeat":
heartbeat_absent = True
lst_status, lst_body = results[listing_path]
listing_files: list[str] = []
if lst_status == 200:
try:
listing_files = list(json.loads(lst_body).get("files", []))
except json.JSONDecodeError:
pass
if brief:
hb = texts.get("heartbeat")
if hb and hb.strip(): # follow-up: the pointed session log's key sections
sess_path = hb.strip().splitlines()[0].split(" @ ", 1)[0].strip()
if sess_path:
st2, b2 = request("GET", vault_url(sess_path))
if st2 == 200:
texts["_pointed_session"] = b2.decode(errors="replace")
print(_render_brief(texts, listing_files, offline))
return 0
# ---- full mode: raw sections, output format unchanged ----
for label, path in targets:
status, body = results[path]
if status == 200:
print(f"===== {label}: {path} (HTTP 200) =====")
text = body.decode(errors="replace")
text = texts[label] or ""
sys.stdout.write(text)
if not text.endswith("\n"):
sys.stdout.write("\n")
elif status == 404:
print(f"===== {label}: {path} (HTTP 404) =====")
print("(absent — fine)")
if label == "marker":
marker_missing = True
if label == "heartbeat":
heartbeat_absent = True
elif status == 0: # vault unreachable -> degrade to last-known-good cache
offline = True
cached = echo_queue.cache_get(path)
if cached is not None:
elif status == 0:
if texts[label] is not None:
print(f"===== {label}: {path} (OFFLINE — serving stale cache) =====")
sys.stdout.write(cached.decode(errors="replace"))
if not cached.endswith(b"\n"):
sys.stdout.write(texts[label])
if not texts[label].endswith("\n"):
sys.stdout.write("\n")
else:
print(f"===== {label}: {path} (OFFLINE — no cache) =====")
@@ -669,20 +838,14 @@ def cmd_load() -> int:
print(f"(error HTTP {status}: {body.decode(errors='replace')[:200]})")
print()
# M3: heartbeat pointer missing/stale -> fall back to the recent sessions listing
# (matches the documented loading procedure) so orientation works without the pointer.
if heartbeat_absent and not offline:
st, body = request("GET", vault_url("_agent/sessions/"))
if st == 200:
try:
files = sorted((f for f in json.loads(body).get("files", []) if f.endswith(".md")),
reverse=True)
except json.JSONDecodeError:
files = []
if files:
print("===== recent sessions (heartbeat absent — fallback) =====")
for f in files[:5]:
print(f" _agent/sessions/{f}")
print()
# (already fetched in the batch) so orientation works without the pointer.
if heartbeat_absent and not offline and listing_files:
files = sorted((f for f in listing_files if f.endswith(".md")), reverse=True)
if files:
print("===== recent sessions (heartbeat absent — fallback) =====")
for f in files[:5]:
print(f" _agent/sessions/{f}")
print()
if offline:
print("NOTE: vault unreachable — context above is last-known-good cache; writes this "
"session will be queued and synced when the vault returns.")
@@ -732,7 +895,8 @@ def cmd_config(args) -> int:
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Validated cross-platform ECHO vault client")
sub = parser.add_subparsers(dest="cmd", required=True)
sub.add_parser("load")
p = sub.add_parser("load")
p.add_argument("--brief", action="store_true") # token-budgeted digest (hook default)
sub.add_parser("flush") # H2: replay writes queued during a prior outage
sub.add_parser("doctor") # M3: one-call readiness check
sub.add_parser("get").add_argument("path")
@@ -761,6 +925,9 @@ def build_parser() -> argparse.ArgumentParser:
p = sub.add_parser("reflect") # H5: apply a JSON proposal set (dry-run unless --apply)
p.add_argument("file", nargs="?")
p.add_argument("--apply", action="store_true")
p = sub.add_parser("session-end") # 2.1.0: one-call session end (log+agent-log+reflect+scope+heartbeat)
p.add_argument("file", nargs="?") # bundle JSON; - or stdin
p.add_argument("--apply", action="store_true")
p = sub.add_parser("triage") # one-tap inbox triage (reflect pipeline + audit log)
p.add_argument("file", nargs="?") # proposals JSON; omit to list
p.add_argument("--list", action="store_true", dest="list_inbox")
@@ -796,21 +963,27 @@ def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
if args.cmd == "load":
return cmd_load()
return cmd_load(brief=args.brief)
if args.cmd == "doctor":
import echo_doctor
return echo_doctor.run()
if args.cmd == "flush":
import echo_queue
n = echo_queue.flush()
remaining = len(echo_queue.pending())
remaining = echo_queue.pending()
flagged = [r for r in remaining if r.get("needs_attention")]
if n:
print(f"ok: flushed {n} queued write(s)"
+ (f"; {remaining} still queued (vault unreachable)" if remaining else ""))
+ (f"; {len(remaining)} still queued" if remaining else ""))
elif remaining:
print(f"ok: {remaining} write(s) still queued (vault unreachable)")
print(f"ok: {len(remaining)} write(s) still queued")
else:
print("ok: queue empty")
for r in flagged:
what = (f"capture '{(r.get('args') or {}).get('title')}'" if r.get("op") == "capture"
else f"{r.get('method')} {r.get('url')}")
print(f" needs attention ({r['needs_attention']}): {what} — resolve "
"(e.g. capture --merge-into <slug> / --force), it will not auto-land")
return 0
if args.cmd == "config":
return cmd_config(args)
@@ -852,6 +1025,14 @@ def main(argv: list[str] | None = None) -> int:
if not isinstance(proposals, list):
raise EchoError("reflect: proposals must be a JSON array of objects", 2)
return echo_reflect.apply(proposals, confirm=args.apply)
if args.cmd == "session-end":
import echo_session
raw = read_body(args.file).decode("utf-8", errors="replace")
try:
bundle = json.loads(raw)
except json.JSONDecodeError as exc:
raise EchoError(f"session-end: bundle must be a JSON object ({exc})", 2)
return echo_session.session_end(bundle, apply=args.apply)
if args.cmd == "triage":
import echo_triage
if args.list_inbox or not args.file:
@@ -880,9 +1061,12 @@ def main(argv: list[str] | None = None) -> int:
date=args.date, domain=args.domain, inbox=args.inbox, no_log=args.no_log,
as_json=args.json, dry_run=args.dry_run,
force=args.force, merge_into=args.merge_into)
except EchoError as exc:
except RuntimeError as exc:
# Catches EchoError from THIS module and from helper modules' own `import echo`
# twin (when echo.py runs as __main__ the two class objects differ, but both
# subclass RuntimeError and carry .code).
print(f"echo.py: {exc}", file=sys.stderr)
return exc.code
return getattr(exc, "code", 1)
return 2