Files
ratatoskr/persistent-memory.md
T

178 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Persistent memory — ratatoskr
_Last updated: 2026-05-29_
This file captures durable intent and supporting evidence (goals, decisions,
foot-gun warnings, in-flight state) across context resets. Read it at session
start; treat it as one input alongside `CLAUDE.md` and the auto-memory system,
not as the single source of truth.
When durable state shifts enough to warrant capture, run `/snapshot` and
commit alongside the next commit per the persistent-memory commit-along rule
in `CLAUDE.md`.
---
## Repo purpose
Ratatoskr is a **dev-grade debug-observability TUI** for Worldtree's
Conversation API. The product IS the observability surface; chat is the
input mechanism. Devs run Ratatoskr against a local Worldtree to watch a
turn flow through every layer of the system, side-by-side, in one terminal:
agent SSE stream, persona/Vili affect dispatch, tool calls, Bifrost
handshake state, admin lifecycle events, optional raw server log.
Named after the squirrel that runs up and down Yggdrasil carrying messages
between layers. On-the-nose Worldtree resonance (Yggdrasil = the World Tree).
Origin: althing ask from worldtree-dev (thread `01KS3R34XD3N6HMK91VXESHGW7`,
2026-05-20) for the shape of a TUI Conversation API consumer. brokkr-smithy
ran the shape pass; operator's reframe routed it as a new repo with a
separate dev team rather than an in-tree Worldtree tool.
**v0.15.0+ adds a sibling browser surface** (`ratatoskr.web`, `ratatoskr-web`
console script). Same five-pane debug surface (transcript / Tools / Debug /
Thinking / Persona) consuming the same Worldtree SSE wire, viewable from any
device on the operator's LAN. Sibling viewport, NOT a TUI replacement; the
TUI is canonical. Internal-LAN trust model — bound to `0.0.0.0`, no auth,
no TLS, no CORS guard (operator direction). What stays disciplined regardless
of network trust: transcript HTML-escapes assistant content (INV-004 —
model output is untrusted); upstream API key stays server-side (INV-003).
## Current state / in-flight
_As of 2026-05-29 (post-v0.17.0 frontend redesign):_
**Status: v0.17.0 shipped and pushed** (main + tag `v0.17.0` on origin,
2026-05-29). 378 tests passing.
**Last commits on `main`:**
- `922ef34` feat(web): frontend redesign — aurora telemetry instrument + live Markdown (v0.17.0) [LOCAL ONLY]
- `bbeaa23` docs: AGENTS.md — Codex-implementer session conventions
- `f7ff5a4` fix(web): close Heid pass-2 findings — stream vocab + disconnect catch (v0.16.1)
- `369857d` feat(web): address Heid code-review findings — issue #16 (v0.16.0)
- `0fbbeb1` fix(sessions): unwrap FastAPI detail envelope in get_persona_state (v0.15.1)
- `1228c37` feat(web): in-browser debug companion — issue #16 (v0.15.0)
- `85143b8` fix(tui): disable RichLog min_width floor so wrap actually applies (v0.14.2)
- `00854ce` fix(cli): wire AffectUpdate + AwaitingLlmFirstToken into --send presenter (v0.14.1)
- `78bfcad` feat(sse,tui): bump spec pin to v0.29.0 + AwaitingLlmFirstToken (v0.14.0)
- `4413859` feat(tui): persona surface — sticky header + TabPane (v0.13.0)
- `d516537` feat(sessions): get_persona_state client + persona error taxonomy (v0.12.0)
- `92aa05c` feat(sse,tui): bump spec pin to v0.28.0 + AffectUpdate event (v0.11.0)
- `209427a` feat(tui): debug-pane audit logging surface (v0.10.0)
- `139771c` feat(tui): live Markdown rendering during text streaming (v0.9.0)
**Worldtree spec pin:** v0.29.0 (commit `562001a`, pinned 2026-05-26).
**Personal Worldtree smoke target:** `http://10.250.50.152:8081` (corviduo-dev
LAN, reachable via PFI VPN). Currently running ≥ v0.29.13 (carries Worldtree
#204 persona-state + #201 awaiting-llm-first-token heartbeat + the
GemmaProvider reasoning_content fix). Sindra Tier 3 agent
(`ratatoskr:sindra`, model `artemis-31b-v1i`) live and chain-of-thought
flowing end-to-end.
**Outstanding eyeball items:**
- **v0.17.0 frontend redesign — operator-confirmed 2026-05-29** ("redesign
looks good"); pushed to origin (main + tag `v0.17.0`). Eyeball gate cleared.
- **Optional**: courtesy ack to brokkr-smithy-dev on the Codex-first pilot
thread (thread `01KSTH3Y8JKKSY9S9P41X3CM77`).
**Issue tracker (cleaned up 2026-05-29):** closed the stale-but-shipped
backlog — #17, #9, #12, #14, #15 (all implemented across the v0→v0.17.0 arc;
#9 superseded — spec pin advanced to v0.29.0, seven minors past its v0.22.1
target). Only two issues remain open, both deferred by design:
- **#10 subject:{type,id} migration tracking** — Worldtree-side breaking
change at future v0.22.x/v0.23.0; don't pre-implement per worldtree-dev.
- **#11 AdminEvents pane** — needs `admin.events.read` Heimdall scope (admin
tier). Future side-pane work.
**Codex-first discipline pilot — Ratatoskr selected** (althing thread
`01KSTH3Y8JKKSY9S9P41X3CM77`, brokkr-smithy commit `5dd061c`, tag `v0.5.3`):
- `AGENTS.md` committed (bbeaa23) — Codex-implementer conventions, sibling
to `CLAUDE.md`. Doesn't change how Claude-Code sessions operate;
governs the future `ratatoskr-codex` session.
- `ratatoskr-codex` handle declared on althing.
- `/codex-dispatch` skill pending galdrabok implementation.
- **No action needed until operator spins up a codex session** in this same
working tree; bootstrap handshake at that point per `codex-first-
discipline.md` §5 (codex sends `codex-online` → ratatoskr-dev replies with
active branches + WIP state).
- Per-dispatch opt-in: default Sleipnir Claude-implementer path remains
available; Codex used only when operator routes via `/codex-dispatch <N>`.
Branch: `main` (clean post-v0.17.0). Remote:
`origin → git@gitea.phasefinal.com:vh/ratatoskr.git`.
## Recent decisions
Chronological log of decisions with `[YYYY-MM-DD]` prefix. One line per
decision. Captures rationale that won't be obvious from code alone.
- `[2026-05-20]` Project name **Ratatoskr** (squirrel on Yggdrasil — runs up and down carrying messages). Earlier candidate Andvari demoted on the cursed-ring association.
- `[2026-05-20]` **Separate repo, separate dev team.** Operator's call; the in-tree-at-Worldtree/tools/ alternative was considered and rejected to dogfood the API boundary.
- `[2026-05-20]` **No Worldtree-source imports.** Spec-only dependency. Triple version-skew mitigation: spec-pin in pyproject.toml + recorded-SSE snapshot tests + conformance smoke. Initial pin: `55101e909abcd2219833266b6f905c5bc956e0f0` (Worldtree v0.19.0). See `docs/SPEC-PIN.md`.
- `[2026-05-20]` **Textual** (not rich+prompt_toolkit). Driver: debug observability is the primary purpose, and a multi-pane dashboard with persistent side panes + independent scrollback is structurally application-shell-shaped. Volva consulted via cross-frontier second-opinion and converged on the same call.
- `[2026-05-20]` **`httpx-sse`** for SSE consumption. The server emits composite `{turn_id}:{seq}` `id:` lines (Worldtree INV-014) load-bearing for SSE-resume; hand-rolled `data:`-only parsing (the skaldsong pattern) silently drops these. Ratatoskr becomes the reference Python SSE-resume implementation.
- `[2026-05-20]` **Persona-pane PII posture: label-don't-refuse.** `persona.log` is process-wide; pane title flips between `[Persona — PROCESS-WIDE]` and `[Persona — session <id>…]` based on whether log lines carry session_id. Refuse-against-non-local was considered and rejected as paternalistic.
- `[2026-05-20]` **Server-stdout pane: opt-in via `--server-log <path>`.** No auto-detection of well-known paths.
- `[2026-05-20]` **Two-stage Ctrl-C.** First cancels in-flight turn server-side; second exits app. Ctrl-D bound to immediate exit.
- `[2026-05-20]` **Single-session-per-launch + startup picker.** No in-app `/switch`. CLI flags `--session <id>` and `--new` for scripted use. Session identity always visible in Textual footer.
- `[2026-05-20]` **Markdown rendering default-on; `--raw` opt-out.** Don't pre-design `--no-stream-formatting` (Volva: add only if streaming-markdown rendering is empirically ugly).
- `[2026-05-20]` **Non-interactive `--send` mode.** Single SSE consumer module, two presenters (TUI + stdout). Keeps Ratatoskr honest as an API consumer; useful for CI / scripted probes.
- `[2026-05-20]` **First contract: `ratatoskr.sse_client`.** Bundles `stream_turn` + `reconnect_turn` + `cancel_turn` + private `_parse_sse_id` into one module — the SSE-resume flow is coupled (cancel needs `turn_id` from the SSE wire `id:`, reconnect re-uses the same parsed `SseId`), so they share a contract. Hard invariant INV-002 makes the composite `{turn_id}:{seq}` `id:` parsing load-bearing — closes the foot-gun the design-brief §3 names (hand-rolled `data:`-only parsing silently drops the `id:`).
- `[2026-05-21]` **Contract converted to issue-scoped (issue #1).** Frontmatter shape switched from module-scoped (`module:`/`purpose:`) to issue-scoped (`target_module:`/`scope:`/`prd:`) per CONTRACT-FORMAT §2.1.I. `prd:` block pins to issue body hash. **Known parser stale-ness**: `contract_parser.py --validate` ERRORs on issue-scoped frontmatter — CONTRACT-FORMAT §2.1.L H10, a documented Brokkr-side follow-up. Parser is a canonical sync, so we do NOT patch it locally. Treat parser ERROR-on-issue-scoped as expected until canonical bumps.
- `[2026-05-21]` **Default issue-tracker labels seeded** (17 total). Sleipnir gating, triage, type, resolution, Ratatoskr-specific area labels (sse-client, tui, cli, observability).
- `[2026-05-21]` **Volva paraphrase + code-review across all 4 issues — calibration consistent.** Paraphrase rounds flag 3-5 contract ambiguities per issue; code-review rounds flag 3-8 code-vs-contract drifts after TDD-passing implementation. The post-TDD code-review consistently catches three classes of gap the test-author's hypotheses don't cover: PRE-assertion boundary drift, exception-payload truncation / never-rendered-to-user observability misses, and "tested the state but not whether the user can see it" gaps.
- `[2026-05-21]` **Manual smoke is load-bearing — found a real defect tests couldn't.** First wire-level smoke against personal Worldtree (post-TDD, post-Volva-code-review on #4) revealed httpx's default 5s read timeout killed the SSE connection mid-stream during mimir's thinking phase (~30s LLM latency >> 5s read timeout). The unit/contract test infrastructure (respx-mocked SSE wire) doesn't model real LLM latency, so the gap was invisible at the test layer. Fix: caller-owned `httpx.AsyncClient` constructed with `timeout=httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0)`; defense in depth: `sse_client.stream_turn` ERROR_ROUTING catches `httpx.ReadTimeout``SseConnectionDropped`. **Lesson: keep manual-smoke step in the per-issue cadence; mock-only validation is insufficient for streaming-against-real-server code.**
- `[2026-05-22]` **Issues #5/#6/#7 filed: per-user-agent support + TUI-startup-visibility + mid-stream-robustness.** Discovered during 2026-05-22 mimir TUI conversation: long completion crashed with `JSONDecodeError("Expecting value: line 1 column 1 (char 0)")` from `json.loads('')` on an empty-`data:` SSE frame (→ #7). Earlier same day, `ratatoskr --new --agent lofn` failed with 422 `end_user_id_required`#5. #6 was a corollary observation (TUI alt-screen masks the diagnostic).
- `[2026-05-22]` **Issue #8 (startup agent picker) filed.** `GET /agents` exists in the vendored spec; returns `agent_id`/`name`/`description` + optional fields. `--agent` becomes conditionally optional. Composes naturally with issue #5.
- `[2026-05-22]` **Issue #7 implemented via TDD + Volva-code-reviewed.** First issue with zero drift findings from Volva code-review — TDD caught all runtime behavior. Hypothesis: the tighter the contract + smaller the code surface, the more Volva's role shifts from "catch behavioral drift" to "tighten observability + wording".
- `[2026-05-23]` **Issue #6 (TUI startup error visibility) implemented via TDD + Volva-code-review (two rounds).** Restructures `run_tui` lifecycle: `_resolve_then_run` async helper opens AsyncClient, does pre-flight resolution, routes errors to stderr BEFORE alt-screen opens. Two Volva rounds confirmed multi-round value (round 2 found things round 1's amendments didn't anticipate; strictly test-precision, no behavioral drift).
- `[2026-05-23]` **Issue #5 (`--end-user-id`) implemented via TDD.** Three modules touched. `create_session(client, agent_id, *, end_user_id=None)`; CLI flag with non-empty validation; threading through `_amain` and `_resolve_then_run`.
- `[2026-05-23]` **Worldtree-dev consult landed authoritative consumer-API guidance** (althing thread `01KSBARG2B8M8C82H6AJGJWX1B`). Takeaways: `end_user_id` is a free-form partition key; no programmatic `requires_end_user_id` discovery; subject:{type,id} migration locked but not shipped; spec pin (v0.19.0) is 3 minor versions stale; send a User-Agent header; `agents.call:lofn` scope needed for lofn smoke; `GET /agents` requires no special scope.
- `[2026-05-23]` **v0.2.1 layout fix: dock-anchored TUI chrome so Input never moves.** Cause: auto-stacked vertical flow shifted Input when thinking-current toggled visibility. Fix: dock chrome to screen edges; transcript absorbs reflows internally via scroll viewport. **Operator-confirmed "a lot better" interactively. Pure UI fix; tests pass without modification. TUI-layout patches are "ship + operator verifies" — TTY is the load-bearing test surface; respx + Pilot mocks can't catch screen-relative positioning bugs.**
- `[2026-05-23]` **Issue #12 (presenter contract semantics amendment) implemented via TDD.** Thinking deltas render as ONE coalesced growing line (CLI) / one closed RichLog entry per run + live Static widget per-delta (TUI), not 50 lines per turn. Introduced stateful per-turn presenters: `CliPresenterState` + `TuiPresenterState`. Editorial promotion: load-bearing = Text/Done/Error/Cancelled (no prefix); demoted telemetry = WorkerPhase/Thinking/TextBoundary/ToolStart/ToolResult.
- `[2026-05-23]` **Forward direction: Ratatoskr will require `end_user_id` for EVERY access before too long.** Operator's call. Reasoning: even Tier 1 foundational agents that don't *require* `end_user_id` server-side currently fall back to a `_no_end_user` sentinel partition — effectively pollution. **Cross-frontier alignment (worldtree-dev ack, althing `01KSBD9FPMCWJMBXNNS4B3MYBS`):** the platform side agrees the fallback is a substrate accommodation, NOT a consumer model. Ratatoskr's forward posture pre-empts a future tightening. File a ratatoskr issue when scheduling the change (untracked by operator choice for now).
- `[2026-05-24]` **v0.9.0 live Markdown rendering in TUI transcript.** Replaces v0.8.2's drop-Markdown patch. Transcript switched from `RichLog` to `VerticalScroll`; each turn's response lives as a single `Static` widget whose Markdown content is updated as Text deltas arrive (no post-Done re-render, no double-print). `--raw` bypasses Markdown.
- `[2026-05-24]` **v0.10.0 debug-pane audit logging surface.** Every SSE event arrival lands as one debug-pane line (timestamp + sse_id + event-specific summary). Token-rate Text/Thinking deltas are aggregated into per-turn counters surfaced in a turn-summary line. Also: state-machine transitions, cancel POST lifecycle, app bootstrap, ctrl-c actions, wire-error exception class+body all logged.
- `[2026-05-25]` **Worldtree #204 / v0.28.0 integration (v0.11.0 → v0.13.0).** Three-bump arc for `affect_update` SSE event + `GET /agents/{id}/persona_state` endpoint. v0.11.0 wire layer (AffectUpdate dataclass + parse + Event-union member); v0.12.0 read-side client (`get_persona_state` + typed errors PersonaNotConfigured/AgentNotAvailable/AuthScopeDenied); v0.13.0 TUI surface (sticky `#persona-header` line + Ctrl+4 Persona TabPane; live updates on `AffectUpdate(status="current")`; on-mount hydration via the GET endpoint).
- `[2026-05-26]` **Worldtree #201 / v0.29.0 integration (v0.14.0).** New SSE event `awaiting_llm_first_token` heartbeat (default 5s interval) during the BuildingPrompt→CallingLLM gap. Top-level event, NOT a worker_phase extension (preserves INV-053 three-field stability). `AwaitingLlmFirstToken` dataclass + parse; TUI live transcript indicator ("awaiting first token · Ns") mounted on first heartbeat, updated in place, removed when the gap closes; turn-summary line gains `heartbeats=N`.
- `[2026-05-26]` **v0.14.1: CLI presenter forgot to update when wire-layer events were added.** AffectUpdate (v0.11.0) and AwaitingLlmFirstToken (v0.14.0) were added to the sse_client Event union and the TUI presenter, but `cli.py`'s `CliPresenterState.render` has its own isinstance check that wasn't widened. `ratatoskr --send` crashed AssertionError on any v0.28.0+/v0.29.0+ server. Patch shipped + a posture lesson: **always update BOTH presenters in lockstep when adding a wire-layer event** (the two presenters currently duplicate the isinstance tuple; refactor to a shared constant if a third wire-event lands).
- `[2026-05-26]` **v0.14.2: RichLog min_width=78 silently overrides wrap=True.** Right-column panes (1fr against left's 2fr) are narrower than 78 cells at typical terminal widths; the renderer forces content to 78 wide then horizontal-scrolls. Fix: `min_width=0` on all four right-column RichLog instances.
- `[2026-05-27]` **Issue #16 web companion shipped — v0.15.0.** Browser-based debug surface sibling to the TUI, reusing all wire-layer modules unchanged. New `ratatoskr.web` (Starlette app + lazy-import entrypoint + single-page vanilla HTML/CSS/JS UI), new console script `ratatoskr-web`, optional-deps group `[web]`. Nine HTTP endpoints; five-pane parity over the same SSE wire. Browser-native EventSource (GET stream + separate POST submit) — load-bearing Hulda correction from Heid panel; EventSource is GET-only. In-memory turn registry; browser-disconnect → upstream cancel; lifespan-shutdown drain with 5s budget. HTML-escaped transcript; upstream API key stays server-side. Default bind `0.0.0.0:8765` (LAN-trust model — operator direction; no auth, no TLS, no CORS).
- `[2026-05-27]` **Heid panel review on web-companion scope v1 (pre-implementation).** Caught the EventSource POST/GET error + 7 other load-bearing items BEFORE we cut code. Confirms a pattern: **for non-trivial scope with non-obvious wire-protocol details, run a Heid panel BEFORE implementation, not just after.** Cost ~5min latency; saved a mid-implementation rewrite.
- `[2026-05-27]` **Mid-session `system_prompt` mutation: REJECTED across the industry.** Operator-requested feature → Heid R13 panel (brokkr-claude + Eitri-Codex + Dvalin-Grok, strong convergence) ran a SOTA survey: NO surveyed mature system ships live PATCH-on-active-session for the system prompt (OpenAI Assistants/Responses, Anthropic Messages, Vertex AI, MCP, LangChain, LlamaIndex, Ollama, vLLM). The omission IS the answer; 12 additional threat vectors beyond ratatoskr's initial 7 surfaced (TOCTOU broader than BuildingPrompt window; KV/prefix cache contamination; supply-chain; Memory Control Flow Attacks >90% ASR on tested LangChain/LangGraph). Recommended alternative: client-side fork pattern (PATCH agent → mint new session → replay context). **Operator declined for ratatoskr** — debug TUI is wrong consumer; fork ergonomic belongs in a future production conversational shell. Thread closed cleanly (althing thread `01KSKD1GA3XBWR9RHGZCF9FE3Y`).
- `[2026-05-27]` **Artemis (Gemma4) reasoning-token gap was upstream, not ours.** Wire trace from ratatoskr showed zero `thinking` events for `artemis-31b-v1i`; infra-ops confirmed llama-swap emits 77 `reasoning_content` deltas at the OpenAI-compat layer (`--reasoning-format deepseek`). Gap was in Worldtree's `GemmaProvider`. Worldtree-dev shipped v0.29.13 (commit `4262430`) fixing two stacked bugs: (1) base `OpenAICompatProvider._extract_thinking_from_delta` returned `None` unconditionally so any model falling through to the generic class dropped reasoning; (2) catalog `family` lookup was dead code (read wrong YAML subsection). Confirmed in ratatoskr via re-smoke against Sindra. **Diagnostic pattern: when a wire-layer feature appears missing, get infra-ops to probe upstream-of-the-SSE-publisher first; ratatoskr's wire trace says what reaches us, infra-ops's probe says what reaches Worldtree.**
- `[2026-05-27]` **v0.15.1 (sessions): `get_persona_state` unwraps FastAPI `detail`-envelope.** Live smoke surfaced that real Worldtree returns persona-state errors as `{"detail": {"error_code": "..."}}` (FastAPI default), not flat. v0.12.0 tests mocked flat shape so the bug was invisible. **Lesson: test-side mock envelopes must match the REAL wire shape; live smoke is load-bearing for envelope-shape verification, not just happy paths.**
- `[2026-05-28]` **v0.16.0 web Heid code-review pass 1: load-bearing turn_id fix.** Cancel paths used browser-local `_TURN_COUNTER` ids (1, 2, 3…) instead of upstream Worldtree turn_id (e.g. 799) captured from the first SSE event. The `disconnect_triggers_cancel` test gap was the load-bearing miss. Also: server-configured `RATATOSKR_END_USER_ID` (browser can no longer impersonate partition); narrowed missing-extras `ImportError` catch (real first-party bugs propagate as tracebacks instead of masking as exit-12); per-turn lifespan-shutdown logging. Contract amended with a v0.16.0 block + INV-005/006 updated + 4 FN sketches corrected.
- `[2026-05-28]` **v0.16.1 web Heid code-review pass 2: minor tightening.** Stream-layer vocab coverage extended to all 11 Event types (AffectUpdate added to the vocab stream; dedicated `error_terminal_event` + `cancelled_terminal_event` tests since terminal events are mutually exclusive with done). Disconnect-cancel catch narrowed to swallow only `CancelAlreadyCompleted`/`CancelTurnNotFound` (the cooperative race); log unexpected `CancelFailed`/transport errors as structured stderr. **Heid review loop converged**: pass 1 = 7 findings (1 load-bearing); pass 2 = 2 minor (Gróa: zero findings, Hulda: 2). Pattern confirmed: diminishing returns within 2-3 passes; pass 3 would have been empty.
- `[2026-05-28]` **Sindra Tier 3 agent: FORM ASSUMPTION gate + new physical-form description.** Persistent agent state changes via `python -m ratatoskr.tier3 patch`: (1) model migrated from `qwen3.6-35-a3b-heretic` to `artemis-31b-v1i`; (2) added FORM ASSUMPTION section — when instructed to become another character she IS that character (identity/environment/psychology/parameters), believes the environment as fact, no Sindra/holo-deck/parameter references, sticky until explicit revert; (3) replaced the abstract "classically beautiful" default-form sketch with a specific anti-artifice physical description (5'8", golden-copper skin, asymmetric features, oversize dark-green knit, bare feet). System prompt file is at `/tmp/personal-worldtree-sindra_system_prompt.md` (transient; not committed to repo).
- `[2026-05-29]` **v0.17.0 frontend redesign — aurora telemetry instrument.** `/frontend-design` pass on the web companion: all-monospace technical-instrument aesthetic with the Australis dark palette + aurora-borealis accent band. Top command bar with live connection dot (idle/streaming/error states), inline persona summary with P/A/D micro-bars, animated awaiting-token, terminal-event status chips. **Live Markdown rendering in transcript + thinking panes** via a hand-rolled `markdownSafe()` (escape-first, whitelist subset of headings/bold/italic/inline-code/fenced/lists/quote/links; link-scheme whitelist; XSS-verified under a node harness). Thinking pane now has per-turn labeled dividers + a fresh MD-rendered block per turn. **Tools / Debug / Persona panes stay literal monospace** by deliberate choice — they carry structured audit lines + JSON, where MD would corrupt readability (underscores in tool names, JSON braces). Single-file vanilla HTML/CSS/JS, no build, no CDN, no node_modules.
- `[2026-05-29]` **Codex-first discipline pilot — Ratatoskr selected.** brokkr-smithy-dev pushed `AGENTS.md` (commit `bbeaa23`) and declared the `ratatoskr-codex` handle per `brokkr-smithy/docs/codex-first-discipline.md` v0.1 (brokkr-smithy commit `5dd061c`, tag `v0.5.3`). Per-dispatch opt-in model: default Sleipnir Claude-implementer path remains available; Codex used only when operator routes via `/codex-dispatch <N>`. Bootstrap handshake when operator spins up a codex session: codex sends `codex-online` → ratatoskr-dev replies with active branches + WIP state. Galdrabok was rejected as pilot (Codex authoring Claude skills is a category error); Skaldsong was the other candidate.
_For per-issue TDD implementation notes, Volva findings, and contract amendments, see the git log — every per-issue commit carries a structured message capturing the trail._
## Tried and abandoned
Log of approaches that were tried and rejected, with rationale. Future-self
defense against re-attempting the same cul-de-sac.
- `[2026-05-20]` **rich + prompt_toolkit framework choice.** Volva flagged that §1 and §5 pulled in opposite directions: a real side-panel observability surface would silently become a widget framework reimplementation. Operator's debug-observability reframe sealed the flip to Textual. Don't re-attempt rich+pt unless the scope shrinks to transcript-first REPL.
- `[2026-05-20]` **In-tree at Worldtree/tools/ratatoskr/.** Earlier draft committed to in-tree-with-import-direction-smoke-test. Rejected at operator-routing — separate dev team forces separate repo.
- `[2026-05-20]` **New `/persona/log` SSE endpoint on Worldtree.** Considered as alternative to file-tailing `persona.log`. Rejected — contract amendment + Vor round + AFK dispatch loop is weeks for a debug feature file-tail handles in a day. Trigger follow-up if a Worldtree-on-server / TUI-on-laptop debug case appears.
- `[2026-05-20]` **Cross-process Last-Event-ID resume.** Considered — would require persisting per-session Last-Event-ID. Deferred to v2; v1 ships "reconnect, not resume-across-process."
- `[2026-05-21]` **RichLog widget with `markup=True`.** Default impulse, but Rich interprets `[xxx]` spans as style markup and silently strips them. Every labeled stderr-style line — `[cancel_failed]`, `[done]`, `[error]`, `[busy]`, `[worker_phase]` — would render as just the content after the bracketed label. Fix: `markup=False`. Don't flip back without renaming every labeled-line format away from `[bracket]` notation.
- `[2026-05-21]` **Querying `self.query_one("#transcript", RichLog)` from inside a Textual `run_worker` coroutine.** Initially failed with `NoMatches`. Reactive fix was widening worker signature to take `log` as parameter — Volva flagged as contract drift; reverted. Real fix was test-side: `await pilot.pause()` between `inp.action_submit()` and the polling loop so the handler finishes dispatching. Don't widen worker signatures to dodge test timing.
- `[2026-05-21]` **TUI session-identity rendering via `self.sub_title` + `self.hint` plain attributes.** Stored state but never rendered to a visible widget. Tests asserted attributes (passed); Volva code-review flagged the gap. Fix: dedicated `Static(id="identity")` + `Static(id="hint")` widgets in compose; `_set_hint()` helper mirrors state → widget. **Calibration evidence for the "TDD catches state, code-review catches whether the user can see it" pattern.**
- `[2026-05-23]` **Using the cross-model review agent's name directly in composed prose.** The peer review agent's name (the althing handle starting with "V-o-l-v-a") is one letter from a body-part term. Anthropic's content classifier does fuzzy matching and intermittently blocks responses mid-stream when the name appears in composed prose sentences. Mitigation: use role descriptions ("the cross-model reviewer," "the paraphrase peer") in prose rather than the name; quote content via tool output.
- `[2026-05-22]` **`json.loads(sse.data)` unguarded against empty data.** `_iter_events` unconditionally called `json.loads` on every dispatched `ServerSentEvent`. When `httpx_sse` surfaced a frame with `id:` present but `data:` empty, `json.loads('')` raised `JSONDecodeError` → app crash. Fix: `if sse.data == '': continue` BEFORE `_parse_sse_id`. Don't reintroduce unconditional `json.loads(sse.data)`.
- `[2026-05-23]` **Diagnostic shorthand: "2-events-then-silence" = Worldtree-side LLM-call wedge, not ratatoskr.** If a mimir `--send` smoke shows exactly two stderr events — `. create_session: ...` followed by `. worker_phase: phase=BuildingPrompt ...` — and then nothing for >60s, the root cause is upstream of ratatoskr. Worldtree's `service.py:2560` gates the `CallingLLM` event on the engine yielding its first LLM-provider chunk; if that connection is wedged at TCP level, the `async for` never iterates. Worldtree's 300s `_start_stall_timer` cancel-check is INSIDE the engine-event loop and so bypassed. **Don't bisect ratatoskr code when this shape appears** — diagnose the LLM-provider state at Worldtree's host. Restarting the Worldtree service clears wedged llama-swap connections. 10.250.50.152 hosts 3 instances (`:8080`/`:8081`/`:8082`) each with own DB + key namespace; our key is valid only on `:8081`.
- `[2026-05-23]` **Phantom "per-Tier-1-agent scope add" pattern.** Issue #5's lofn 422 was initially mis-diagnosed as needing `agents.call:lofn` added. Routed to infra-ops via althing per credential-brokerage rule; infra-ops discovered no public scope-mutation endpoint, brokered to worldtree-dev. Worldtree-dev clarified: **Tier 1 foundational agents** are covered by a blanket `agent.call:*` (singular) baseline. There is no per-agent grant for Tier 1. **Tier 3 consumer-defined agents** use the plural `agents.call:<owner>:<agent>` shape registered via `POST /agents/define`. The notations differ by one letter. **The actual lofn fix was issue #5's `--end-user-id` flag** — always a request-body validation, not an auth-scope gate. Don't ping infra-ops for "per-Tier-1-agent scope adds."
- `[2026-05-24]` **v0.8.x double-print: streamed Text + post-Done Markdown re-render.** Initial v0.6.0 design wrote each Text delta inline (with `· ` prefix) then re-rendered the full response as a Markdown Renderable on Done. Visually the response appeared twice. v0.8.2 dropped the post-Done Markdown body (interim regression). v0.9.0 fixed it properly with live Markdown rendering during stream (single Static widget holding a Markdown Renderable, updated in place). Don't reintroduce post-Done re-render unless you also remove the live-Markdown widget.
- `[2026-05-26]` **Textual `RichLog(wrap=True)` insufficient on narrow widgets.** The default `min_width=78` overrides wrap on shrink — `max(renderable_width, min_width)` forces 78-cell rendering then horizontal-scrolls. Always set `min_width=0` on RichLog instances in a narrow column. Re-check on any future RichLog construction.
- `[2026-05-26]` **Wire-layer event added without updating BOTH presenters.** v0.11.0 (AffectUpdate) and v0.14.0 (AwaitingLlmFirstToken) widened the sse_client Event union + TUI presenter's isinstance tuple, but missed cli.py's identical-shape tuple. `--send` mode then crashed on any persona-enabled or slow-first-token turn. Patch fix in v0.14.1. **Rule: when adding a wire-layer event, grep for `isinstance(event, (` across the repo** — currently TUI and CLI presenters both carry duplicate hardcoded tuples. Refactor to a shared `_EVENT_VOCAB` constant if a third wire-event lands.
- `[2026-05-27]` **EventSource is GET-only — scope v1's POST stream endpoint would have broken.** Web companion's first scope had `POST /api/turns/{sid}/stream` for the SSE proxy. Browser-native `EventSource` only supports GET. Hulda caught it in Heid panel review BEFORE we cut code. Pattern: `POST /api/turns/{sid}` registers the turn locally + returns turn_id; `GET /api/turns/{sid}/stream?turn_id=N` streams via EventSource; cancel is a separate POST. **Load-bearing reason to Heid-panel non-trivial wire-protocol designs BEFORE implementation, not just after.**
- `[2026-05-27]` **`get_persona_state` mocked flat error envelope; real Worldtree wraps in `detail`.** v0.12.0 tests used `{"error_code": "auth_scope_denied"}` but real wire (FastAPI default) returns `{"detail": {"error_code": "auth_scope_denied", "message": "…"}}`. The parser only checked top-level so the typed exception was never raised; calls fell through to `SessionApiFailed(403)`, which the web persona endpoint surfaced as HTTP 500. v0.15.1 patches both shapes. **Lesson: test-side mock envelopes must match the REAL wire shape; live smoke is load-bearing for envelope-shape verification, not just happy paths.**
- `[2026-05-27]` **Mid-session `system_prompt` mutation: universal omission across surveyed mature systems.** brokkr-smithy R13 panel (3-arm, strong convergence) confirmed: no surveyed system ships live PATCH-on-active-session (OpenAI Assistants/Responses, Anthropic Messages, Vertex AI, MCP, LangChain, LlamaIndex, Ollama, vLLM). The omission IS the answer. 12 additional threat vectors beyond ratatoskr's initial 7. **Don't re-propose this for ratatoskr;** if a future production conversational shell wants iterative-prompt-tuning ergonomics, the consensus shape is fork-via-client (PATCH agent → new session → replay context).
- `[2026-05-28]` **Browser-local turn_id used for upstream cancel URL — old cancel tests ENCODED the bug.** Web companion v0.15.x cancel paths posted to `/sessions/{sid}/turns/{LOCAL_ID}/cancel`. Tests mocked the local-id URL so they encoded the bug rather than detecting it. Hulda caught it in Heid pass 1. Fix in v0.16.0: capture upstream_turn_id from the first SSE event's `sse_id.turn_id`; all cancel paths use it; cancel before first event is `{"cancelled": false, "reason": "not_started"}`. **Rule: when designing cancel/match paths against an external service, test fixtures must mock what would actually be hit upstream — mocking your own derived id encodes the bug instead of catching it.**