Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

157 changes: 157 additions & 0 deletions _bmad-output/implementation-artifacts/10-4-first-run-onboarding.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions _bmad-output/implementation-artifacts/deferred-work.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Epic 10 — persona caching findings (Story 10.5, 2026-06-25)

Short field notes on making the always-injected persona cheap to re-send every turn. The persona
is a stable prompt PREFIX re-sent on every turn (stateless LLM + ephemeral fork worker — no
model-side persistence is possible, AD-3), so prompt caching is the only lever to make it cheap.

## What the byte-stable prefix (AC1) buys, per surface

The persona prefix is `BOT_INSTRUCTIONS → DIRECTIVE → IDENTITY → SOUL → USER → about` (plus the
blank-USER onboarding block), composed purely from static file text — no `now()`/UUID/unsorted-dict/
hostname/PID interpolated anywhere. All volatile content (lazy-load reference docs, knowledge,
summary, recent window, recall, owner message) stays strictly AFTER it (AD-6 order). A guard test
(`test_persona_prefix_has_no_timestamp_or_uuid`) now fails if a future change drops a per-request
value into the prefix — the "silent cache invalidator" from design §6.

- **OpenAI-shape surface** (`openai_provider.py` — drives `openai`/`gemini`/`groq`/`cerebras`/
`mistral`/`openrouter`/`nvidia`/`github`/`ollama`): mostly AUTOMATIC prefix caching (OpenAI ≥1024
tok ~50% read discount; Gemini 2.5 implicit). **No request field — AC1 fully covers it.** `ollama`
local is free anyway. Nothing was sent here and nothing needs to be.
- **Anthropic-shape surface** (`anthropic_provider.py` — drives `claude` + `glm`): native Claude
also AUTO-caches a stable token prefix to a degree, but the explicit `cache_control` breakpoint is
what guarantees the 5-min-TTL prefix cache (write 1.25×, read ~0.1×). See AC2 below.

## Anthropic `cache_control` breakpoint (AC2) — DEFERRED, with the cheap win shipped

**Shipped:** per-turn cache-signal logging in `anthropic_provider.py` (`_log_cache_usage`, both the
`complete` and `complete_with_tools` paths) — logs `usage.cache_creation_input_tokens` /
`usage.cache_read_input_tokens` at INFO under `shelldon.broker`, `getattr`-guarded so GLM/z.ai
omitting the fields logs nothing and never crashes. This is the only way to SEE whether the prefix
is caching, and the signal the owner's live check reads.

**Deferred:** the explicit `cache_control: {type:"ephemeral"}` breakpoint. The obstacle (design §
"KEY OBSTACLE"): both egress paths send the persona embedded INSIDE a single content string
(`messages=[{"role":"user","content":prompt}]` / the first `Message.content`). Anthropic attaches
`cache_control` to a content BLOCK, so caching the prefix requires splitting it into a separate
block from the volatile remainder — which needs a stable boundary marker the worker emits between
prefix and volatile. That marker would have to be **stripped by every provider surface** (an
OpenAI-shape adapter would otherwise send the literal sentinel to the model), so it is a
cross-cutting worker→broker→all-adapters contract change, not a local Anthropic tweak. That exceeds
the story's timebox and the design explicitly allows the defer.

**Why the defer is safe (no silent cap):** the byte-stable prefix already gives free OpenAI-surface
prefix caching AND native-Claude automatic prefix caching. For GLM specifically, if it does not
cache, the fallback is lazy-load (AC3, heavy reference docs only when relevant) + the Story 5.2
credit budget — never a silent token truncation. Recorded in `deferred-work.md`.

## GLM / z.ai `cache_control` passthrough — owner live-check pending

z.ai exposes an Anthropic-compatible proxy; Zhipu has native context caching, but whether the proxy
forwards `cache_control` (and surfaces `usage.cache_*` fields) is **unverifiable in CI** (no live
LLM). The per-turn logging above means the owner's next live turn against GLM will reveal it: if the
log shows non-zero `cache_read_input_tokens`, GLM caches; if the fields are absent, it does not and
the budget/lazy-load posture stands. Follow the 8.0 live-smoke model — the owner runs the paid call.

## Lazy-load reference docs (AC3)

`TOOLS.md` / `ARCHITECTURE.md` ported as owner-editable seeds (NOT `rewrite_*` targets). A pure
keyword matcher (`_needs_reference`) injects each only when the owner message is about the bot's
tools/internals, AFTER the cached prefix. An ordinary message reads neither — tokens spent only when
relevant. `VAULT.md` deliberately NOT ported (shelldon's `vault/` is the OS-isolated secret store).

## Pi migration (AC4) — no script change needed

`deploy/setup-pi.sh` git-clones the repo and runs `uv run python -m shelldon` from
`WorkingDirectory=$REPO`, so the persona dir is present as repo files at runtime; `CuratedMemory`
seeds copy-if-absent at boot, leaving any existing owner files byte-untouched. Verified: all 9 seeds
are git-tracked (so the clone carries them — the 10.4 untracked-seed lesson) AND present in the
built wheel (`test_packaging.py`). No `pyproject.toml` force-include needed — hatchling already ships
non-`.py` data under `packages=["shelldon"]`.

## Recommended budget posture

Keep AC1 (byte-stable prefix) + AC3 (lazy-load) as the cost floor regardless of provider. After the
owner's live GLM check: if GLM caches → no action; if it does not → lean on lazy-load + the 5.2
budget and consider revisiting the explicit breakpoint only if persona-prefix tokens dominate the
GLM bill in practice.
68 changes: 68 additions & 0 deletions _bmad-output/implementation-artifacts/epic-10-retro-2026-06-26.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Epic 10 Retrospective — Persona-as-Files

**Date:** 2026-06-26
**Participants:** Elliot (project lead), Claude (dev/facilitator) — solo project, no party-mode fiction.
**Epic status at retro:** 5/5 stories dev-complete. 10.1–10.3 `done`; 10.4 + 10.5 `review` (loose end → Action 1).

## What this epic was

Moved every piece of hardcoded LLM-facing prose out of code and into bot- and owner-editable markdown under `~/.shelldon/memory/` — the v1 worktree model rebuilt under v2 invariants. The character (`SYSTEM_INSTRUCTION`, proactive/dream copy) became *data the bot reads each turn*, editable by the bot via the memory-op path (AD-5 single-writer) and by the owner on disk. Foundation (10.1) → bot-write ops + gated DIRECTIVE (10.2) → proactive/dream prose to files + autonomous edit (10.3) → first-run onboarding (10.4) → cost/lazy-load/Pi migration (10.5).

## Delivery metrics

- **Stories:** 5/5 dev-complete (10.1, 10.2, 10.3 done; 10.4, 10.5 in review).
- **Tests:** 782 → 839 (+57 across the epic).
- **Dependencies added:** 0, all five stories.
- **New ops:** only in 10.2 (`rewrite_soul`/`identity`/`user`/`instructions` autonomous + `rewrite_directive` owner-approval-gated). 10.1/10.3/10.4/10.5 added zero ops, no `SCHEMA_VERSION` bump.
- **import-linter:** 3 contracts KEPT every single story. Core stayed LLM-free throughout — persona is now data, not code (the whole point of the epic).
- **Production incidents:** 0. **Live-Pi validation of Epic 10: 0** (the files ship and pass CI with fake providers, but no real GLM conversation on `gotchi` has exercised onboarding/persona-rewrite/lazy-load yet → Action 4).

## What went well

1. **Clean "data-not-code" refactor.** Contracts green every story, zero dependency creep, ops added only where a genuine new capability demanded them. The invariants (AD-1 core LLM-free, AD-5 single-writer, AD-6 worker read-only, AD-3 fork no-accumulation) held across all 5 without strain.
2. **Aggressive seam reuse kept stories small.** `_seed_persona` copy-if-absent was reused verbatim 3× (BOT_INSTRUCTIONS → +HEARTBEAT/DREAM → +BOOTSTRAP → +TOOLS/ARCHITECTURE). The owner-gated `DIRECTIVE` rewrite (10.2) reused Story 9.3's park/Approve Telegram plumbing with **zero schema change** — packed as a `(messages=(), ToolCall)` blob.
3. **Golden byte-parity tests guarded every prose move.** Day-one no-op (10.1), proactive byte-identical (10.3), byte-stable cache prefix (10.5) — moving prose to files had to change nothing observable, and the tests enforced it.
4. **Review caught real wire bugs, not nits.** 10.2: the non-greedy ops-block regex closed at a *nested* ```` ```ops ```` inside `rewrite_instructions` content, so the instructions op silently never round-tripped (fixed by anchoring the closing fence to line-start); plus a park-key sqlite INSERT-OR-REPLACE collision when two parking ops landed in one Result. 10.5: the byte-stable-prefix guard + the keyword false-positive precision fix.
5. **Honest in-flight design overrides for correctness.** 10.1 rejected the story's *coarse* shared fail-soft handler for independent per-file `_safe_read` (a corrupt SOUL.md can't drop the system instruction). 10.2 rejected the design's "9.3 verbatim → resumed worker applies DIRECTIVE" because it violates AD-5 — chose core-applies-on-Approve, no worker resume.

## What repeatedly bit us — the one dominant theme

**Fail-soft behavior at file/template boundaries was the entire risk surface, and nearly every review patch.** The mechanism was right on the first pass every time; the *boundary and teardown behavior* held the bugs:

- **10.2:** ops-fence nesting silently dropped a valid op; park collision clobbered a sqlite row.
- **10.3:** missing template slots silently dropped feeling / learnings / mood; `read_heartbeat`/`read_dream` lacked the OSError catch their fail-soft promise implied.
- **10.4:** corruption swallowed without the promised log (**still an OPEN patch** — the AC5 logging gap); a fail-soft test deleted `BOOTSTRAP.md`, but `gather_context` re-seeds absent files on construction, so the delete was silently undone and the test passed green-but-wrong (fixed by using a corrupt non-UTF-8 file, which seeding never overwrites).
- **10.5:** the "untracked / absent seed file" failure class from 10.4 was explicitly *guarded* this time — recurred enough to become a known hazard later stories defend against proactively.

## Prior-retro follow-through (Epic 9 → Epic 10)

- ✅ **Live-smoke the self-coding loop on `gotchi`** — done (GREEN on live GLM).
- ✅ **Story 9.6 tool-policy hardening** — done (created, built, reviewed, merged).
- ⚠️ **"Make broad-`except` / silent-degrade a first-class review dimension"** — *the continuity miss.* Epic 10 proved this class generalizes beyond untrusted code to **any fail-soft file boundary**, and it still produced the dominant patch class in 4 of 5 stories. We identified the lesson in Epic 9 but never operationalized it into a **pre-review** checklist; it kept getting caught *at* review instead of *before* it. → Action 2.
- ⏳ **Epic 8 carryover** still open: "fork inherits core's fds; clean fork-server-as-own-process never fully built." Not touched in Epic 10 (out of scope); remains a standing item.

Also reaffirmed: **live-LLM uptake is unverifiable in CI.** Every Epic 10 story proved its mechanism with fake providers only; whether the bot actually rewrites its SOUL well, whether onboarding *feels* warm, whether GLM honors the cache prefix — all unvalidated. Same gap Epic 9 flagged. → Action 4.

## Action items (all carried to deferred-work.md per owner)

1. **Close 10.4's loose end.** Fix the OPEN AC5 patch — `read_bootstrap` must LOG on corruption (today it swallows silently, so `_safe_read`'s except never fires and the spec's "omitted (logged)" is unmet). Then run 10.4's formal code review and flip 10.4 + 10.5 `review → done`. *Immediate — these are dangling review closures.*
2. **Operationalize the fail-soft-boundary checklist** Epic 9 asked for. Before marking ANY file-touching story done, grep/scan for: broad `except`; degrade-without-a-log; re-seed-masks-delete in tests (use corruption, not deletion, to test fail-soft on seeded files); missing-template-slot silent drop; owner-edit foot-guns. Make it pre-review, not post-review. This is the recurring class across Epics 9 AND 10.
3. **Harden the untracked-seed guard.** The `test_persona_seeds_are_git_tracked` check is the *sole* catcher of an untracked seed (importlib/wheel tests pass on present-but-untracked files), and it `pytest.skip`s where git is absent. Either assert git is present in CI, or add a build-from-clean-clone check. (Already in deferred-work from 10.5 review.)
4. **Live-validate Epic 10 on the Pi.** One real owner conversation on `gotchi` against live GLM that exercises the whole epic end-to-end: first-run onboarding (BOOTSTRAP) → autonomous persona rewrite (SOUL/IDENTITY/USER) → "how do you work" lazy-load (TOOLS/ARCHITECTURE inject) → read the per-turn GLM cache signal from the broker log. Owner-run, out-of-CI, like the 8.0 live smokes. This is the acceptance CI structurally cannot give.

## Readiness assessment

- **Tests/quality:** 839 green, contracts KEPT, 0 deps. Solid from a CI perspective.
- **Deployment:** NOT yet live-validated on the Pi. The code ships in the wheel and seeds copy-if-absent on boot (verified), but no real GLM turn has exercised Epic 10 behavior on `gotchi`. → Action 4 is the true production gate.
- **Open work:** 10.4 OPEN AC5 patch; 10.4 + 10.5 still `review`. → Action 1.
- **Codebase health:** good. No new resident state, no schema churn, surgical scope held every story.

## Next steps

1. Action 1 (close 10.4/10.5) — immediate.
2. Actions 2–4 tracked in `deferred-work.md`.
3. **Epic 11: TBD.** No Epic 11 exists in `epics.md`. The original project goal (own/build/desk/text it) was met at Epic 8; Epics 9–10 are enrichment. Elliot is considering an Epic 11 and will scope it later — the roadmap is intentionally open, not closed. When it forms, run `bmad-create-epics-and-stories` / `create-story` (note the known gotcha: the create-epics skill OVERWRITES `epics.md` greenfield — append manually).

## One-line synthesis

Epic 10 is a clean "data-not-code" refactor — 0 deps, ops only where capability demanded, contracts green every story — whose entire risk surface and nearly every review patch lived in **fail-soft file-boundary behavior**, exactly the class Epic 9's retro told us to hunt. The mechanisms were right; the boundaries held the bugs. The standing fix is to make that class a pre-review checklist, and to live-validate the whole epic on the real Pi.
Loading
Loading