From 446cd9a0ff8da28f7b720033970890efb749fa99 Mon Sep 17 00:00:00 2001 From: Jason Stedwell Date: Tue, 28 Jul 2026 22:34:42 -0500 Subject: [PATCH] =?UTF-8?q?2.1.0=20=E2=80=94=20quick-wins=20train:=20brief?= =?UTF-8?q?=20load,=20offline-durable=20capture,=20session-end=20verb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 58 ++++ README.md | 3 +- docs/IMPROVEMENT-PLANS.md | 11 +- docs/MCP-SERVER-SPEC.md | 2 +- echo-memory.plugin | Bin 184316 -> 192635 bytes .../.claude-plugin/plugin.json | 2 +- .../skills/echo-memory/SKILL.md | 29 +- .../skills/echo-memory/scripts/echo.py | 256 +++++++++++++++--- .../scripts/echo_hook_session_start.py | 2 +- .../echo-memory/scripts/echo_hook_stop.py | 9 +- .../skills/echo-memory/scripts/echo_ops.py | 79 ++++-- .../skills/echo-memory/scripts/echo_queue.py | 90 +++++- .../echo-memory/scripts/echo_session.py | 144 ++++++++++ .../skills/echo-reflect/SKILL.md | 7 +- eval/test_features.py | 57 ++++ eval/test_offline_queue.py | 34 +++ 16 files changed, 702 insertions(+), 81 deletions(-) create mode 100644 echo-memory.plugin.src/skills/echo-memory/scripts/echo_session.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e1bffe..8cb9f21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,63 @@ # Changelog +## 2.1.0 + +The "quick-wins train" from the 2026-07-28 review (`docs/IMPROVEMENT-PLANS.md` #1, +#3, #7). Three independently useful changes; no schema or layout changes. + +### Added — `load --brief`: a token-budgeted cold-start digest + +The SessionStart hook injected the full text of six files every session (measured +15.9KB and growing without bound — Observations and Scope History never shrink). +`load --brief` renders a digest instead: Fact / Pattern rules in full, the last ~10 +Observations (with a `+N older` note), scope + `scope_updated` + sessions-since, +the heartbeat-pointed session log's key sections (Goal / Decisions Made / Open +Threads / Suggested Next Step), today's Agent Log lines, and the inbox as a +**count + oldest age** — everything the load-time reconcile needs. Over +`ECHO_LOAD_BUDGET` (default ~8000 chars), sections trim lowest-priority-first +(observations → agent log → session summary) with explicit `(truncated)` markers. +The hook now injects the brief form; `/echo-load` and the manual procedure keep the +full dump. All `load` reads (plus the sessions listing, which also feeds the +heartbeat-absent fallback) are fetched **in parallel** over the pooled connections. + +### Fixed — capture is now offline-durable (the write path's inversion bug) + +Low-level verbs queued on an outage, but the *default* write (`capture`) died: the +update path's POST/PATCH and `ensure_daily_log` bypassed the queue entirely, and an +unreachable index aborted the whole op. Now: (1) a fully-offline `capture` queues +the **whole operation as one semantic record** (title/kind/body/tags/…, plus the +capture-time date); `flush` replays it **through `capture`** so create-vs-update, +the duplicate gate, and aliasing re-run against the index as it is at replay time — +byte-level replay would freeze a decision made blind. (2) A duplicate-gate stop on +replay keeps the record, flags it `needs_attention` (surfaced by `flush` and +`load`), and does NOT block later records. (3) Mid-capture outages: the update +path and `ensure_daily_log` writes ride `safe_request` (accepted edge: a queued +agent-log PATCH 400-drops loudly if the daily note never existed — the trace line, +not the memory). Same-args offline captures dedupe by idem-key. + +### Added — `session-end`: the one-call session close + +`echo.py session-end [--apply]` performs the whole end-of-session +sequence under ONE lock acquisition, in order: session log PUT → Agent Log line → +reflect proposals (via `capture`, gate-aware) → optional `scope set` → **heartbeat +LAST as the commit marker** (a part-way failure leaves the previous pointer intact, +never dangling). Dry-run by default previews the full plan including the reflect +classification. Filename HHMM from `$ECHO_NOW` (else local clock), validated +against the canonical session-log regex **before any write**. The Stop hook's nudge +now names the command, and recognizes a `session-end` invocation as +"already reflected". + +### Fixed — helper-module errors exited 2 instead of their intended codes + +When `echo.py` runs as `__main__`, helper modules' `import echo` creates a twin +module whose `EchoError` is a different class object, so `main()`'s handler missed +it and the raw traceback leaked. The handler now catches `RuntimeError` (both +classes subclass it) and honors `.code`. + +Tests: +19 end-to-end checks (brief-load digest/budget, offline-capture queue / +replay-through-capture / gate-on-replay / idem-key dedup, session-end dry-run / +apply / ordering / validation-abort). All suites green. + ## 2.0.0 **Packaging & structure major — nothing about memory behavior changes.** The major diff --git a/README.md b/README.md index 9fa48c8..e17007f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# echo-memory — v2.0.0 +# echo-memory — v2.1.0 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). The skill makes direct REST calls through a bundled validated client (`scripts/echo.py`). The whole toolchain is **pure Python** (stdlib only), so it runs identically on Windows, macOS, and Linux — no bash, no platform-specific `date`. @@ -421,6 +421,7 @@ From the credential-free harness (`eval/run_eval.py` against the deterministic m | Version | Highlights | |---------|-----------| +| **2.1.0** | **Quick-wins train: cheaper sessions, durable capture, one-call session end.** (1) **`load --brief`** — a token-budgeted cold-start digest (Fact/Pattern in full, last ~10 Observations, scope+freshness, last session's key sections, Agent-Log lines, inbox *count*; `ECHO_LOAD_BUDGET` default ~8000 chars) now injected by the SessionStart hook, replacing the full six-file dump that grew unboundedly; all `load` reads are fetched in parallel. (2) **Offline capture durability** — `capture` on an unreachable vault queues the *whole operation* 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, never landed blind; `ensure_daily_log`/update-path writes ride the queue too. (3) **`session-end`** — one call (one lock) writes session log → Agent-Log line → reflect proposals → optional scope switch → **heartbeat last as the commit marker**; dry-run by default; `ECHO_NOW` pins the HHMM. Also fixes the `__main__` twin-module trap so helper-module `EchoError`s exit with their intended codes. +19 end-to-end checks. | | **2.0.0** | **Packaging & structure major — memory behavior unchanged.** Skills-only: the legacy `commands/` directory is deleted (breaking for pre-skills clients; the 1.6.0-verified skills are the sole entry points). Artifact policy: the repo tracks only the `echo-memory.plugin` pointer; the 15 historical versioned zips leave the tree and versioned builds ship as **Gitea releases** (one per `v` tag) from `v2.0.0` on; `.gitignore` blocks `*.plugin` except the pointer. README gains the "packaging for other agent runtimes" port note (build-target rule, never a second source tree). | | **1.6.0** | **Skills-format migration + lint blind-spot fix.** The eight slash commands gain `skills//SKILL.md` twins (same names, byte-identical bodies; a same-name skill takes precedence, so `commands/` coexists until its 2.0 deletion): per-skill `allowed-tools` end the permission prompts, `disable-model-invocation: true` makes `/echo-sweep` + `/echo-triage` operator-only. `vault_lint` checks retired patterns **before** routes, so a seed README inside a retired tree (`archive/…`) is flagged instead of being absolved by the leaf-README route (+ regression test). `plugin.json` gains `homepage`/`repository`. | | **1.5.1** | **Duplicate-gate precision.** Live use caught the gate blocking a decision note because it shared the single vault-common token "echo" with an archived project (min-normalized scoring gives any single-token entity a 1.0 match). New `gate_candidates()` blocks only on **same-kind** candidates (cross-kind name collisions — a decision titled after its project — warn instead), and a **lone shared token blocks only when unique to that entity** (vault df == 1); multi-token overlaps still block. Advisory warnings (`fuzzy_candidates`) unchanged; exit-76/`--merge-into`/`--force` unchanged. +5 tests; verified against the live vault both ways (false positive creates cleanly, true lookalike still gates). | diff --git a/docs/IMPROVEMENT-PLANS.md b/docs/IMPROVEMENT-PLANS.md index 6749d42..1976898 100644 --- a/docs/IMPROVEMENT-PLANS.md +++ b/docs/IMPROVEMENT-PLANS.md @@ -345,8 +345,9 @@ manifest-verification pass). | Release | Items | Rationale | |---|---|---| | **1.6.0** | TODO-1.6: skills-format migration (+ per-skill `allowed-tools`, `disable-model-invocation` on write-heavy entries, `$ECHO` block dedupe) · lint blind-spot fix (retired-dir/leaf-README) · plugin.json `homepage` | Restructures the command/SKILL files later work edits; allowed-tools removes prompt friction for every verb added below. Vault link repointing (TODO-1.6 §3) is vault data, not plugin code — any session. | -| **1.6.x quick wins** | #1 brief load · #3 offline capture · #7 session-end (+ return-not-print refactor, MCP spec Phase 0, if it fits) | Small, correctness/efficiency, no schema changes. #7 unblocks the MCP tool. Users get these without a reinstall — don't gate them behind 2.0. | -| **2.0** | ROADMAP-2.0 unchanged: delete legacy `commands/` · artifact policy (Gitea releases, gitignore, tags) · Codex note · README layout | Packaging only, no feature riders — a bad feature must never force reverting a packaging break. | -| **2.1** | MCP server (containerized, `echomcp.alwisp.com`) — `docs/MCP-SERVER-SPEC.md` | Needs #7 + Phase 0. Lands after 2.0 so its docs target one command format and the Gitea release channel exists. Deployed via CI + PORT, not the plugin manifest. | -| **2.2 index train** | #2 local-first recall (CLI-path half) · #4 incremental sweep · #8 stemming+expansion | One schema-bump train; all touch the same index/sweep loop. The container already keeps its index warm in memory (spec §7), so #2 here covers the CLI fallback path; #4's fast sweep also becomes the container's background job. | -| **2.3 memory train** | #5 lifecycle/decay · #6 supersession · #9 resolve-miss learning | Capability tier; #6 after #5 (status vocabulary). | +| **1.6.0 — SHIPPED 2026-07-28** | Skills migration + lint fix + homepage | Verified on desktop + CoWork. | +| **2.0.0 — SHIPPED 2026-07-28** | ROADMAP-2.0: commands/ deleted · release-based artifacts · gitignore · Codex note | Packaging-only major; skills-only install re-verified. | +| **2.1.0 quick wins — SHIPPED 2026-07-28** | #1 brief load · #3 offline capture · #7 session-end | All three landed with +19 e2e checks; see CHANGELOG 2.1.0. The MCP spec's Phase 0 (return-not-print) did NOT ride along — it's the first phase of the MCP build session. | +| **2.2** | MCP server (containerized, `echomcp.alwisp.com`) — `docs/MCP-SERVER-SPEC.md` | Needs Phase 0 (return-not-print refactor, first step of the build session). #7 session-end shipped, so `echo_log_session` wraps a real verb. Deployed via CI + PORT, not the plugin manifest. | +| **2.3 index train** | #2 local-first recall (CLI-path half) · #4 incremental sweep · #8 stemming+expansion | One schema-bump train; all touch the same index/sweep loop. The container already keeps its index warm in memory (spec §7), so #2 here covers the CLI fallback path; #4's fast sweep also becomes the container's background job. | +| **2.4 memory train** | #5 lifecycle/decay · #6 supersession · #9 resolve-miss learning | Capability tier; #6 after #5 (status vocabulary). | diff --git a/docs/MCP-SERVER-SPEC.md b/docs/MCP-SERVER-SPEC.md index 79a6a41..5bc1f8d 100644 --- a/docs/MCP-SERVER-SPEC.md +++ b/docs/MCP-SERVER-SPEC.md @@ -75,7 +75,7 @@ monitor, PORT deploy/rollback. Server down ⇒ exactly today's behavior. | Tool prefix | `echo_` | Namespace safety next to other servers. | | Result shape | `structuredContent` (the `echo_output.envelope` dict) + a 1–3 line human text block | Envelope shape already exists. | | Statefulness | Stateless protocol; warm in-process caches | See §7. | -| Version target | **2.1** (after the 1.6.x train and the 2.0 packaging major) | Needs #7 + Phase 0; docs then target the single post-2.0 command format. | +| Version target | **2.2** (1.6.0, 2.0.0, and the 2.1.0 quick-wins train all shipped 2026-07-28; `session-end` exists) | Remaining prerequisite: Phase 0 (return-not-print), the build session's first step. | | Local stdio variant | **Dropped for v1** (documented, not built) | The CLI/skill fallback covers the no-server case; two transports = two test matrices for little gain. Phase 0 keeps the door open — the `*_op` core is transport-agnostic. | | Result budgets | Every read tool takes an explicit size budget (`budget_chars` / `max_chars`) and truncates with a marker + "call X for more" | The single biggest token lever: one right-sized answer instead of follow-up fetches. | | Tool profiles | `ECHO_MCP_TOOLS=core\|full` env: `core` exposes only load/recall/capture/triage_inbox/log_session/health | Tool schemas cost context on every surface that lists them; the claude.ai connector doesn't need the escape hatches. Desktop registers `full`. | diff --git a/echo-memory.plugin b/echo-memory.plugin index 7c5d32f0f49181925953027bd4eeb914a003b4f0..8659cd4460e6fc252fb4f61a1ce49eecefa55d2d 100644 GIT binary patch delta 52546 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

|54w_PAVb9Ab+TfTJPg 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

sROT`RVJk`1`Z-ZTsEhb=z_0#P9ZRSBuY~ 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 RgOowquqVFwS^#G0{{bP7@Am)z diff --git a/echo-memory.plugin.src/.claude-plugin/plugin.json b/echo-memory.plugin.src/.claude-plugin/plugin.json index eb3357b..5f57621 100644 --- a/echo-memory.plugin.src/.claude-plugin/plugin.json +++ b/echo-memory.plugin.src/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "echo-memory", - "version": "2.0.0", + "version": "2.1.0", "homepage": "https://git.alwisp.com/jason/echo", "repository": "https://git.alwisp.com/jason/echo", "description": "Persistent memory via the ECHO Obsidian vault over the Obsidian Local REST API. Cross-platform Python client: one-call capture/resolve/recall/link/triage over an entity index, hybrid BM25 + graph recall spanning entities + sessions/journal (recency/status-aware), a pre-write duplicate gate, complete-frontmatter capture, session hooks that self-fire load/reflect, offline write-ahead queue, lock-guarded concurrency, linter-enforced routing, and /echo-* commands.", diff --git a/echo-memory.plugin.src/skills/echo-memory/SKILL.md b/echo-memory.plugin.src/skills/echo-memory/SKILL.md index 0b141b0..fe60e63 100644 --- a/echo-memory.plugin.src/skills/echo-memory/SKILL.md +++ b/echo-memory.plugin.src/skills/echo-memory/SKILL.md @@ -140,7 +140,7 @@ Load at the start of any substantive conversation — anything beyond a single q ### Loading procedure -**Run `python3 "$ECHO" load`** — one call that performs the six orientation reads below and prints each labeled section (404s on today's note / inbox show as absent, not errors; an absent marker is flagged). This replaces issuing six separate GETs by hand. The table documents what `load` reads and why: +**Run `python3 "$ECHO" load`** — one call that performs the six orientation reads below (fetched in parallel) and prints each labeled section (404s on today's note / inbox show as absent, not errors; an absent marker is flagged). This replaces issuing six separate GETs by hand. **`load --brief`** renders a token-budgeted digest instead — Fact/Pattern rules in full, the last ~10 Observations, scope + freshness, the last session's key sections, today's Agent Log lines, and the inbox as a *count* (`ECHO_LOAD_BUDGET`, default ~8000 chars, trims lowest-priority sections first). The SessionStart hook injects the brief form; `/echo-load` and this procedure use the full form. The table documents what `load` reads and why: | # | GET | Notes | |---|-----|-------| @@ -405,25 +405,40 @@ At the end of substantive conversations (ones that produced decisions, artifacts **Filename format is canonical: `_agent/sessions/YYYY-MM-DD-HHMM-.md`.** Always include the four-digit local-time HHMM component — it makes filenames lexically sort in true chronological order, which is what Step 3 of loading relies on. Older session logs without the HHMM part exist; leave them alone, but every new one must use the full form. -Write the body (see `references/session-log-template.md`) to a file with the Write tool, then: +**Preferred — one call (`session-end`).** Write a bundle JSON with the Write tool and run it; it performs the whole session-end sequence under one lock, in order, with the **heartbeat written last as the commit marker** (a failure part-way leaves the previous pointer intact): + +```json +{ "slug": "", + "log_body": "", + "agent_log_line": "- : ", + "scope": "", + "reflect": [ { "title": "…", "kind": "…", "body": "…", "confidence": 0.9 } ] } +``` + +```bash +python3 "$ECHO" session-end bundle.json # dry-run: previews the full plan +ECHO_NOW= python3 "$ECHO" session-end bundle.json --apply +``` + +`--apply` writes: session log → Agent Log line → reflect proposals (via `capture`, gate-aware) → optional scope switch → heartbeat. Set `ECHO_NOW` to the conversation's local HHMM so the filename sorts truthfully. Reflect proposals still follow the reflect contract — preview with the dry-run and apply only with the operator's go-ahead. Offline, every step queues durably. + +**Manual fallback** (only if `session-end` is unavailable) — the same sequence by hand: ```bash python3 "$ECHO" put _agent/sessions/-HHMM-.md ``` -Then add a one-line entry to today's daily note via the **Daily Note — Agent Log** procedure above. - -Finally, update the heartbeat pointer so the next session can orient in one read (this closes the loop with loading-procedure Step 4 — a pointer nobody writes is a pointer nobody can read). It is a single line, ` @ `; write it to a file and PUT it: +Then add a one-line entry to today's daily note via the **Daily Note — Agent Log** procedure above, and finally update the heartbeat pointer so the next session can orient in one read (a pointer nobody writes is a pointer nobody can read) — a single line, ` @ `: ```bash python3 "$ECHO" put _agent/heartbeat/last-session.md ``` -`last-session.md` is overwritten (PUT) each session end — never appended, so it can't grow or duplicate. +`last-session.md` is overwritten (PUT) each session end — never appended, so it can't grow or duplicate. Keep the heartbeat write **last** in the manual sequence too. ## Vault Unreachable -If the API returns a connection error, timeout, or `502`, tell the operator once that the memory vault is unreachable (a `502` usually means Obsidian/the REST plugin is not running on the backend), then proceed without memory. Do not retry repeatedly. +If the API returns a connection error, timeout, or `502`, tell the operator once that the memory vault is unreachable (a `502` usually means Obsidian/the REST plugin is not running on the backend), then proceed — reads degrade to the last-known-good cache at load, and **writes queue durably**: the low-level verbs queue the request, and `capture` queues the whole operation as one semantic record that replays *through capture* (routing + duplicate gate re-run against the index at replay time) on the next reachable session. A queued capture that stops at the duplicate gate on replay is kept and flagged (`flush` lists it) rather than landed blind. Do not retry repeatedly. ## Style Rules diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo.py index 0f3b1ec..30eed46 100644 --- a/echo-memory.plugin.src/skills/echo-memory/scripts/echo.py +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo.py @@ -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 / --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 diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_hook_session_start.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_hook_session_start.py index 688cf68..642c117 100644 --- a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_hook_session_start.py +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_hook_session_start.py @@ -42,7 +42,7 @@ def main() -> int: try: import echo with contextlib.redirect_stdout(buf): - rc = echo.cmd_load() + rc = echo.cmd_load(brief=True) # token-budgeted digest; /echo-load stays full text = buf.getvalue() if rc == 78: context = ("[echo-memory] ECHO is NOT CONFIGURED on this machine. Before " diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_hook_stop.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_hook_stop.py index dc20a35..1cbc9f8 100644 --- a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_hook_stop.py +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_hook_stop.py @@ -30,9 +30,11 @@ MIN_TURNS = int(os.environ.get("ECHO_REFLECT_MIN_TURNS", "5")) REASON = ( "[echo-memory] Session-end reflection has not run yet. If this session produced " "durable facts, decisions, commitments, or artifacts worth remembering: run the " - "/echo-reflect flow (extract -> preview -> apply only with the operator's confirm) " - "and write the session log + heartbeat per the echo-memory skill. If nothing " - "durable emerged, simply finish — do not invent memories. " + "/echo-reflect flow (extract -> preview -> apply only with the operator's confirm), " + "then finish with ONE call — `echo.py session-end --apply` — which " + "writes the session log, Agent Log line, reflect proposals, optional scope switch, " + "and the heartbeat (last, as the commit marker) together. If nothing durable " + "emerged, simply finish — do not invent memories. " "(This reminder fires once per session.)" ) @@ -64,6 +66,7 @@ def already_reflected(raw: str) -> bool: """Transcript evidence that reflection or session logging already happened.""" return ("/echo-reflect" in raw or re.search(r'echo\.py"?\s+reflect\b', raw) is not None + or re.search(r'echo\.py"?\s+session-end\b', raw) is not None or "put _agent/sessions/" in raw or "put _agent/heartbeat/last-session.md" in raw) diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_ops.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_ops.py index 72b2e3c..b6abf6d 100644 --- a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_ops.py +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_ops.py @@ -83,8 +83,13 @@ def recall(query, limit: int = 8, as_json: bool = False) -> int: # ----------------------------------------------------------- agent log ------- def ensure_daily_log(line: str) -> None: """Resilient: ensure today's daily note + its `## Agent Log` heading exist, then - idempotently append the line. Best-effort — never raises into the caller.""" + idempotently append the line. Best-effort — never raises into the caller. Writes go + through the offline queue (2.1.0), so a mid-session outage queues the line instead + of silently dropping it. Offline edge accepted: if the daily note still doesn't + exist at replay time, the queued PATCH 400-drops with a loud warning — the agent-log + line is a convenience trace, not the memory itself.""" try: + import echo_queue date = echo.today() path = f"journal/daily/{date}.md" status, body = echo.request("GET", echo.vault_url(path)) @@ -92,21 +97,22 @@ def ensure_daily_log(line: str) -> None: ts, tb = echo.request("GET", echo.vault_url("journal/templates/daily-note-template.md")) tmpl = tb.decode(errors="replace") if ts == 200 else f"# {date}\n\n## Agent Log\n\n## Related\n" tmpl = tmpl.replace("{{date:YYYY-MM-DD}}", date).replace("{{DATE}}", date) - echo.request("PUT", echo.vault_url(path), data=tmpl.encode(), - headers={"Content-Type": "text/markdown"}) + echo_queue.safe_request("PUT", echo.vault_url(path), data=tmpl.encode(), + headers={"Content-Type": "text/markdown"}) text = tmpl else: - text = body.decode(errors="replace") - if not re.search(r"(?m)^## Agent Log\s*$", text): - echo.request("POST", echo.vault_url(path), data=b"\n\n## Agent Log\n", - headers={"Content-Type": "text/markdown"}) - status, body = echo.request("GET", echo.vault_url(path)) - if status == 200 and line in body.decode(errors="replace"): + text = body.decode(errors="replace") if status == 200 else "" + if status == 200 and not re.search(r"(?m)^## Agent Log\s*$", text): + echo_queue.safe_request("POST", echo.vault_url(path), data=b"\n\n## Agent Log\n", + headers={"Content-Type": "text/markdown"}) + st2, body2 = echo.request("GET", echo.vault_url(path)) + if st2 == 200 and line in body2.decode(errors="replace"): return - echo.request("PATCH", echo.vault_url(path), - data=echo.normalize_patch_body((line + "\n").encode(), "append", "heading"), - headers={"Operation": "append", "Target-Type": "heading", - "Target": f"{date}::Agent Log", "Content-Type": "text/markdown"}) + echo_queue.safe_request( + "PATCH", echo.vault_url(path), + data=echo.normalize_patch_body((line + "\n").encode(), "append", "heading"), + headers={"Operation": "append", "Target-Type": "heading", + "Target": f"{date}::Agent Log", "Content-Type": "text/markdown"}) except Exception as exc: print(f"echo_ops: agent-log skipped ({exc})", file=sys.stderr) @@ -148,20 +154,25 @@ def _dated_block(today_s: str, body_text: str) -> tuple[str, str]: def _append_to_existing(path: str, kind: str, today_s: str, body_text: str) -> None: + # Writes route through the offline queue (2.1.0) so a mid-capture outage queues the + # update instead of silently losing it. (A fully-offline capture never reaches here — + # the short-circuit in capture() queues the whole op as one semantic record.) + import echo_queue text = links.get_text(path) or "" h1 = links.first_h1(text) or path.rsplit("/", 1)[-1][:-3] heading = LOG_HEADING.get(kind, "Notes") if not re.search(rf"(?m)^##\s+{re.escape(heading)}\s*$", text): - echo.request("POST", echo.vault_url(path), data=f"\n\n## {heading}\n".encode(), - headers={"Content-Type": "text/markdown"}) + echo_queue.safe_request("POST", echo.vault_url(path), data=f"\n\n## {heading}\n".encode(), + headers={"Content-Type": "text/markdown"}) bullet, block = _dated_block(today_s, body_text) status, body = echo.request("GET", echo.vault_url(path)) if status == 200 and bullet in body.decode(errors="replace"): return - echo.request("PATCH", echo.vault_url(path), - data=echo.normalize_patch_body((block + "\n").encode(), "append", "heading"), - headers={"Operation": "append", "Target-Type": "heading", - "Target": f"{h1}::{heading}", "Content-Type": "text/markdown"}) + echo_queue.safe_request( + "PATCH", echo.vault_url(path), + data=echo.normalize_patch_body((block + "\n").encode(), "append", "heading"), + headers={"Operation": "append", "Target-Type": "heading", + "Target": f"{h1}::{heading}", "Content-Type": "text/markdown"}) echo.cmd_fm(path, "updated", json.dumps(today_s)) @@ -213,7 +224,35 @@ def capture(kind: str | None, title: str, file_arg: str | None, status_v: str = return done("inbox", "inbox/captures/inbox.md", ok=rc == 0) slug = idx_mod.slugify(title) - index = idx_mod.load() + # Offline short-circuit (2.1.0). The routed path needs the entity index and several + # round-trips; when the vault is unreachable, queue the WHOLE capture as one semantic + # record — flush replays it back through this function against the index as it is at + # replay time, so routing, the duplicate gate, and aliasing re-run with fresh state. + # (Byte-level request replay would freeze a create-vs-update decision made blind.) + try: + index = idx_mod.load() + except echo.EchoError as exc: + if "unreachable" not in str(exc): + raise + if dry_run: + print("offline: vault unreachable — a real run would queue this capture " + "for replay on the next reachable session.", file=real_stdout) + return 0 + import echo_queue + echo_queue.enqueue_capture({ + "kind": kind, "title": title, "body_text": body_text, "status_v": status_v, + "aliases": aliases, "sources": sources, "tags": tags, "date": date, + "domain": domain, "inbox": False, "no_log": no_log, + "force": force, "merge_into": merge_into, "today": today_s, + }) + if as_json: + env = echo_output.envelope("queued:capture", + {"kind": kind, "title": title, "queued": True}) + print(json.dumps(env, ensure_ascii=False), file=real_stdout) + else: + print(f"queued (offline): capture {kind} '{title}' — will replay through " + "capture on the next reachable session", file=real_stdout) + return 0 match_slug, existing = idx_mod.resolve(index, title) # --merge-into: the operator has already identified the canonical entity (e.g. after # a duplicate-gate stop) — route this capture as an UPDATE to it, whatever the title. diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_queue.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_queue.py index e57c6ff..9b01f3e 100644 --- a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_queue.py +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_queue.py @@ -81,6 +81,23 @@ def enqueue(method: str, url: str, data: bytes | None, headers: dict, idem_key: fh.write(json.dumps(rec, ensure_ascii=False) + "\n") +def enqueue_capture(args: dict) -> None: + """Queue a whole capture as ONE semantic record (2.1.0). Byte-level replay is wrong + for the routed capture path: the create-vs-update decision, duplicate gate, and + aliasing must re-run against the index AS IT IS AT REPLAY TIME, so flush re-invokes + echo_ops.capture with the recorded arguments instead of replaying stale requests. + `args` carries body_text inline (never a temp-file path) plus `today` (the capture- + time ECHO_TODAY) so replayed frontmatter/log dates reflect when it was said.""" + key = "capture:" + hashlib.sha1( + json.dumps(args, sort_keys=True, ensure_ascii=False).encode("utf-8")).hexdigest() + if any(rec.get("idem_key") == key for rec in pending()): + return + _ensure_dirs() + rec = {"op": "capture", "args": args, "idem_key": key} + with outbox_path().open("a", encoding="utf-8") as fh: + fh.write(json.dumps(rec, ensure_ascii=False) + "\n") + + def pending() -> list[dict]: p = outbox_path() if not p.exists(): @@ -88,6 +105,12 @@ def pending() -> list[dict]: return [json.loads(ln) for ln in p.read_text(encoding="utf-8").splitlines() if ln.strip()] +def needs_attention() -> list[dict]: + """Queued records that replay could not land automatically (e.g. a duplicate-gate + stop) — surfaced by `flush` and `load` so the operator resolves them deliberately.""" + return [rec for rec in pending() if rec.get("needs_attention")] + + def _rewrite(records: list[dict]) -> None: p = outbox_path() if not records: @@ -108,9 +131,57 @@ def _rebase(url: str) -> str: return echo.BASE + parts.path + (("?" + parts.query) if parts.query else "") +def _replay_capture(rec: dict) -> str: + """Replay a queued semantic capture through echo_ops.capture against the CURRENT + index. Returns 'ok', 'retry' (still offline), or 'gated' (duplicate gate / needs + the operator — the record is kept and flagged, later records still replay).""" + # Cheap reachability probe FIRST: if the vault is still down, echo_ops.capture's + # own offline short-circuit would try to re-enqueue this very record — probe and + # bail as 'retry' instead of recursing into the queue. + st, _ = echo.request("GET", echo.vault_url("_agent/echo-vault.md")) + if st == 0: + return "retry" + import echo_ops + a = dict(rec.get("args") or {}) + body_text = a.pop("body_text", "") or "" + day = a.pop("today", None) + bodyfile = echo.temp_file(body_text.encode("utf-8")) if body_text else None + prev = os.environ.get("ECHO_TODAY") + try: + if day: + os.environ["ECHO_TODAY"] = day # replayed dates reflect when it was said + rc = echo_ops.capture( + a.get("kind"), a.get("title") or "(untitled)", bodyfile, + status_v=a.get("status_v", ""), aliases=a.get("aliases") or [], + sources=a.get("sources") or [], tags=a.get("tags") or [], + date=a.get("date"), domain=a.get("domain", "business"), + inbox=bool(a.get("inbox")), no_log=bool(a.get("no_log")), + force=bool(a.get("force")), merge_into=a.get("merge_into")) + except Exception as exc: # noqa: BLE001 — a broken record must not wedge the queue + print(f"echo_queue: queued capture '{a.get('title')}' failed on replay ({exc}) " + "— kept and flagged", file=sys.stderr) + return "gated" + finally: + if day: + if prev is None: + os.environ.pop("ECHO_TODAY", None) + else: + os.environ["ECHO_TODAY"] = prev + if rc == 0: + return "ok" + if rc == 76: + print(f"echo_queue: queued capture '{a.get('title')}' stopped at the duplicate " + "gate on replay — resolve with capture --merge-into or --force, " + "then remove it from the outbox", file=sys.stderr) + return "gated" + + def _replay(rec: dict) -> str: """Re-issue one queued write idempotently. Returns 'ok' (landed/already-present), - 'drop' (permanent 4xx — can never succeed), or 'retry' (still offline / 5xx).""" + 'drop' (permanent 4xx — can never succeed), 'gated' (kept + flagged for the + operator), or 'retry' (still offline / 5xx).""" + if rec.get("op") == "capture": + return _replay_capture(rec) method = rec["method"] url = _rebase(rec["url"]) data = base64.b64decode(rec["body_b64"]) if rec.get("body_b64") else None @@ -137,21 +208,30 @@ def _replay(rec: dict) -> str: def flush() -> int: """Replay queued writes in order; stop at the first still-unreachable one (keeping it - and the rest). Returns the number actually replayed. Safe to call when empty.""" + and everything after). A 'gated' record (duplicate gate / replay error) is kept and + flagged but does NOT block later records — they are independent writes. Returns the + number actually replayed. Safe to call when empty.""" items = pending() if not items: return 0 replayed = 0 remaining: list[dict] = [] - for i, rec in enumerate(items): + stopped = False + for rec in items: + if stopped: + remaining.append(rec) + continue result = _replay(rec) if result == "ok": replayed += 1 elif result == "drop": continue # warned in _replay; remove from queue + elif result == "gated": + rec["needs_attention"] = rec.get("needs_attention") or "replay-gated" + remaining.append(rec) else: # retry -> still offline; keep this and everything after, in order - remaining = items[i:] - break + remaining.append(rec) + stopped = True _rewrite(remaining) return replayed diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_session.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_session.py new file mode 100644 index 0000000..3bc614b --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_session.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""echo_session.py — the session-end bundle: one call ends a session correctly. [2.1.0] + +Ending a substantive session used to take four-plus separate invocations (session-log +PUT, Agent Log append, heartbeat PUT, optional scope set, optional reflect apply) — +a cost per session, and a partial failure left broken orientation state (log written +but heartbeat stale). This verb does all of it in one call, under ONE advisory-lock +acquisition, in a fixed order with the heartbeat written LAST as the commit marker: +a failure part-way leaves the previous pointer intact, never a dangling one. + +Bundle shape (JSON object): + { + "slug": "echo-mcp-spec", # required, kebab-case + "log_body": "", # required + "agent_log_line": "- 2026-07-28: ...", # optional; derived when omitted + "scope": "new scope text", # optional -> scope set + "reflect": [ { ...PROPOSAL_SCHEMA... } ] # optional -> routed via capture + } + +Dry-run by default (previews the whole plan, reflect included); --apply writes. +Times: the filename HHMM comes from $ECHO_NOW (HHMM) else the local clock; the date +from ECHO_TODAY via echo.today(). Offline: every step rides the offline queue, so a +session end during an outage queues durably instead of failing. + +CLI: echo.py session-end [--apply] +""" +from __future__ import annotations + +import datetime as dt +import json +import os +import re +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import echo # noqa: E402 + +SESSION_RE = re.compile(r"^\d{4}-\d{2}-\d{2}-\d{4}-[a-z0-9-]+\.md$") +SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$") + + +def _hhmm() -> str: + v = os.environ.get("ECHO_NOW", "").strip() + if v: + if not re.match(r"^\d{4}$", v): + raise echo.EchoError(f"session-end: $ECHO_NOW must be HHMM, got '{v}'", 2) + return v + return dt.datetime.now().strftime("%H%M") + + +def validate(bundle: dict) -> tuple[str, str]: + """Return (session_path, agent_log_line) or raise EchoError(2). Validation runs + BEFORE any write — a bad bundle writes nothing at all.""" + if not isinstance(bundle, dict): + raise echo.EchoError("session-end: bundle must be a JSON object", 2) + slug = str(bundle.get("slug", "")).strip() + if not SLUG_RE.match(slug): + raise echo.EchoError(f"session-end: slug must be kebab-case, got '{slug}'", 2) + if not str(bundle.get("log_body", "")).strip(): + raise echo.EchoError("session-end: log_body is required (the full session-log markdown)", 2) + reflect = bundle.get("reflect") + if reflect is not None and not isinstance(reflect, list): + raise echo.EchoError("session-end: reflect must be a JSON array of proposals", 2) + fname = f"{echo.today()}-{_hhmm()}-{slug}.md" + if not SESSION_RE.match(fname): + raise echo.EchoError(f"session-end: derived filename '{fname}' violates the " + "canonical YYYY-MM-DD-HHMM-.md form", 2) + path = f"_agent/sessions/{fname}" + line = str(bundle.get("agent_log_line") or "").strip() \ + or f"- {echo.today()}: session logged [[{path[:-3]}]]" + return path, line + + +def session_end(bundle: dict, apply: bool = False) -> int: + import echo_reflect + path, line = validate(bundle) + scope = str(bundle.get("scope") or "").strip() + proposals = bundle.get("reflect") or [] + + valid, errors = echo_reflect.validate(proposals) + for e in errors: + print(f"skip: {e}", file=sys.stderr) + + print(f"session-end plan ({'APPLY' if apply else 'dry-run'}):") + print(f" 1. session log -> {path}") + print(f" 2. agent-log line: {line}") + if valid: + echo_reflect.classify(valid) + print(f" 3. reflect: {len(valid)} proposal(s)") + print(echo_reflect.preview(valid)) + else: + print(" 3. reflect: (none)") + print(f" 4. scope set: {scope!r}" if scope else " 4. scope: (unchanged)") + print(f" 5. heartbeat -> {path} @ (written LAST — the commit marker)") + + if not apply: + print("\nsession-end: dry-run — re-run with --apply to write.") + return 0 + + import echo_concurrency + import echo_ops + steps: dict[str, str] = {} + with echo_concurrency.vault_lock(): + # 1. The session log itself. A hard failure here aborts the whole bundle — + # nothing after it (including the heartbeat) runs, so orientation state + # can never point at a log that was never written. + echo.cmd_put(path, echo.temp_file(str(bundle["log_body"]).encode("utf-8"))) + steps["session_log"] = "ok" + # 2. Agent-log line (best-effort by contract; queues itself when offline). + echo_ops.ensure_daily_log(line) + steps["agent_log"] = "ok" + # 3. Reflect proposals through the normal capture path (gate-aware). + if valid: + gated = 0 + for p in valid: + if p.get("_action") == "error": + continue + bodyfile = echo.temp_file((p.get("body") or "").encode()) if p.get("body") else None + rc = echo_ops.capture( + p.get("kind"), p["title"], bodyfile, + aliases=p.get("aliases") or [], sources=p.get("sources") or [], + tags=p.get("tags") or [], date=p.get("date"), + domain=p.get("domain", "business"), + inbox=bool(p.get("inbox")) or not p.get("kind")) + gated += 1 if rc == 76 else 0 + steps["reflect"] = f"{len(valid) - gated}/{len(valid)} applied" \ + + (f", {gated} gated (re-propose with --merge-into)" if gated else "") + else: + steps["reflect"] = "skipped (none)" + # 4. Scope switch (optional). + if scope: + echo.cmd_scope("set", scope) + steps["scope"] = "ok" + else: + steps["scope"] = "unchanged" + # 5. Heartbeat LAST — the commit marker for the whole bundle. + echo.cmd_put("_agent/heartbeat/last-session.md", + echo.temp_file(f"{path} @ {echo.now_iso()}\n".encode("utf-8"))) + steps["heartbeat"] = "ok" + + print("\nsession-end: done — " + + "; ".join(f"{k}: {v}" for k, v in steps.items())) + return 0 diff --git a/echo-memory.plugin.src/skills/echo-reflect/SKILL.md b/echo-memory.plugin.src/skills/echo-reflect/SKILL.md index 628d649..9500857 100644 --- a/echo-memory.plugin.src/skills/echo-reflect/SKILL.md +++ b/echo-memory.plugin.src/skills/echo-reflect/SKILL.md @@ -30,4 +30,9 @@ python3 "$ECHO" reflect proposals.json --apply # write — routes each via ca `reflect` dedups every proposal against the entity index (so it shows create-vs-update), skips below-confidence items, and routes `inbox` proposals to the capture inbox. Each applied item goes through the normal `capture` path — canonical frontmatter, auto-linking, -and the recall index — and the writes are lock-guarded. $ARGUMENTS +and the recall index — and the writes are lock-guarded. + +**Finishing the session?** Prefer the one-call bundle — `python3 "$ECHO" session-end +bundle.json --apply` — which applies the reflect proposals AND writes the session log, +Agent Log line, optional scope switch, and the heartbeat (last, as the commit marker) +together. See the echo-memory skill's **Session Logging** section for the bundle shape. $ARGUMENTS diff --git a/eval/test_features.py b/eval/test_features.py index 3046a5c..8c40610 100644 --- a/eval/test_features.py +++ b/eval/test_features.py @@ -345,6 +345,63 @@ def main(): check("v1.5 session-start hook stays quiet on resume", r.returncode == 0 and not r.stdout.strip(), r.stdout) + # 18. (2.1.0) brief load: digest form, budget respected, key facts present. + h.seed("_agent/memory/semantic/operator-preferences.md", + "---\ntype: semantic-memory\ncreated: 2026-06-01\n---\n# Operator Preferences\n\n" + "## Fact / Pattern\n- prefers concise output\n\n## Observations\n" + + "\n".join(f"- 2026-06-{i:02d}: observation {i}" for i in range(1, 15)) + "\n") + h.seed("inbox/captures/inbox.md", "- 2026-06-01: old capture\n- 2026-06-20: new capture\n") + r = h.echo(ECHO, "load", "--brief") + check("v2.1 brief load renders the digest header", + "ECHO load (brief)" in r.stdout, r.stdout[:200]) + check("v2.1 brief load keeps Fact / Pattern", + "prefers concise output" in r.stdout, r.stdout[:800]) + check("v2.1 brief load trims observations to the last 10", + "last 10 of 14" in r.stdout and "observation 14" in r.stdout + and "observation 2" not in r.stdout.replace("observation 2026", ""), r.stdout[-1200:]) + check("v2.1 brief load reports inbox as a count", + "inbox: 2 capture(s), oldest 20d" in r.stdout, r.stdout[-400:]) + check("v2.1 brief load includes the marker line", + "echo-vault.md" in r.stdout and "marker" in r.stdout, r.stdout[:300]) + + # 19. (2.1.0) session-end: dry-run writes nothing; apply lands all steps with + # the heartbeat LAST; a bad bundle aborts before any write. + se_env = dict(os.environ, ECHO_BASE=base, ECHO_KEY=KEY, ECHO_VERIFY="1", + ECHO_TODAY="2026-06-21", ECHO_NOW="2359") + bundle = {"slug": "quickwins-ship", + "log_body": "---\ntype: session-log\nstatus: complete\ncreated: 2026-06-21\n---\n" + "# Session Log\n\n## Goal\nShip the quick wins.\n", + "agent_log_line": "- 2026-06-21: shipped the quick wins", + "reflect": [{"title": "Quark Preference", "kind": "semantic", + "body": "The operator prefers quark.", "confidence": 0.9}]} + bfile = HERE / "_session_bundle.json" + bfile.write_text(json.dumps(bundle), encoding="utf-8") + sess_path = "_agent/sessions/2026-06-21-2359-quickwins-ship.md" + r = subprocess.run([sys.executable, str(ECHO), "session-end", str(bfile)], + capture_output=True, text=True, env=se_env) + check("v2.1 session-end dry-run previews the plan", + "dry-run" in r.stdout and sess_path in r.stdout, r.stdout) + check("v2.1 session-end dry-run writes nothing", h.ground(sess_path) is None) + r = subprocess.run([sys.executable, str(ECHO), "session-end", str(bfile), "--apply"], + capture_output=True, text=True, env=se_env) + check("v2.1 session-end apply lands the session log", + "Ship the quick wins" in (h.ground(sess_path) or ""), r.stdout + r.stderr) + check("v2.1 session-end apply routes the reflect proposal", + h.ground("_agent/memory/semantic/quark-preference.md") is not None) + check("v2.1 session-end apply writes the heartbeat last (commit marker)", + sess_path in (h.ground("_agent/heartbeat/last-session.md") or "")) + daily = h.ground("journal/daily/2026-06-21.md") or "" + check("v2.1 session-end apply appends the agent-log line", + "shipped the quick wins" in daily, daily[-300:]) + bad = dict(bundle, slug="Bad_Slug!") + bfile.write_text(json.dumps(bad), encoding="utf-8") + r = subprocess.run([sys.executable, str(ECHO), "session-end", str(bfile), "--apply"], + capture_output=True, text=True, env=se_env) + bfile.unlink() + check("v2.1 session-end rejects a bad slug before any write", + r.returncode == 2 and sess_path in (h.ground("_agent/heartbeat/last-session.md") or ""), + r.stderr) + print(f"\n{len(failures)} failure(s)" if failures else "\nall feature tests passed") return 1 if failures else 0 finally: diff --git a/eval/test_offline_queue.py b/eval/test_offline_queue.py index f9a864f..005fffc 100644 --- a/eval/test_offline_queue.py +++ b/eval/test_offline_queue.py @@ -115,6 +115,40 @@ def main(): check("offline load flags OFFLINE", "OFFLINE" in r.stdout, r.stdout) check("offline load serves cached marker", "EVALMARK-marker" in r.stdout, r.stdout) + # --- Phase 5 (2.1.0): vault DOWN -> capture queues a SEMANTIC record ------ + r = echo(DEAD, "capture", "Offline Person", "--kind", "person") + check("offline capture exits 0 (queued)", r.returncode == 0, r.stdout + r.stderr) + check("offline capture reports queued", "queued (offline): capture" in r.stdout, r.stdout) + outbox = Path(state) / "outbox.ndjson" + check("capture queued as an op record", + outbox.exists() and '"op": "capture"' in outbox.read_text(encoding="utf-8")) + # same capture again while offline -> deduped by idem_key, not double-queued + echo(DEAD, "capture", "Offline Person", "--kind", "person") + recs = [ln for ln in outbox.read_text(encoding="utf-8").splitlines() if '"op": "capture"' in ln] + check("offline capture is idempotent in the queue", len(recs) == 1, str(len(recs))) + + # --- Phase 6 (2.1.0): vault UP -> flush replays capture THROUGH capture --- + srv = start_mock() + r = echo(mock_base, "flush") + check("flush replays the queued capture", "flushed" in r.stdout, r.stdout + r.stderr) + note = ground("resources/people/offline-person.md") + check("replayed capture created the routed note", + note is not None and "type: person" in note, str(note)[:200]) + check("replayed capture used the capture-time date", + note is not None and "created: 2026-06-22" in note, str(note)[:200]) + + # --- Phase 7 (2.1.0): duplicate gate ON REPLAY keeps + flags the record --- + echo(mock_base, "capture", "Zebulon Quargle", "--kind", "person") # the existing entity + r = echo(DEAD, "capture", "Zebulon Quargle Junior", "--kind", "person") + check("lookalike capture queues offline", "queued (offline)" in r.stdout, r.stdout) + r = echo(mock_base, "flush") + check("gated replay is flagged, not landed", + "needs attention" in r.stdout and "replay-gated" in r.stdout, r.stdout + r.stderr) + check("gated replay did NOT create the duplicate", + ground("resources/people/zebulon-quargle-junior.md") is None) + check("gated record survives in the outbox", + '"needs_attention"' in (outbox.read_text(encoding="utf-8") if outbox.exists() else "")) + print(f"\n{len(failures)} failure(s)" if failures else "\nall offline-queue tests passed") return 1 if failures else 0 finally: