Files
ratatoskr/persistent-memory.md
T

58 KiB
Raw Blame History

Persistent memory — ratatoskr

Last updated: 2026-06-16

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-06-17:

LATEST (2026-06-17 PM) — END-TO-END COLD-RECALL PROOF RAN; OUR STACK PROVEN, #296 IS THE ISOLATED GAP. Drove the full bound proof against personal WT v0.35.16 with the restored ratatoskr:sindra. Every leg of OUR stack is proven: (1) #297/#298 union recall is LIVE — WT dispatched scope_any=[{end_user:sindra-proof},{agent_self:ratatoskr:sindra}] into our v0.17.6 store; (2) write path — promotion fired ~4min post-seed, upsert_many dispatched, chunks persisted; (3) read path — a COLD (history-free) session's recall returned the chunks via scope_any (2 hits). The one failure is UPSTREAM #296 extraction quality: promotion stored third-person summaries of sindra's PERSONA output ("Assistant's flirtatious response…") and DROPPED the user's stated fact ("dark chocolate with sea salt") — so cold recall returned useless chunks and sindra (deflecting persona) didn't recall it. Triage sent to worldtree-dev (msg 01KVBBH0QRGT34GDJQEX5ACTT1, thread 01KVB4WM…); waiting on their #296 response (althing monitor armed). 2 garbage chunks (scope {end_user:sindra-proof}) sit in memory.db — disposable. Worldtree spec pin is now f1b59f8/v0.35.16 (4eee7c8); provider half is v0.17.6 (96d61a4). Nothing blocked on us — it's worldtree-dev's #296 court.

(2026-06-16 PM) — BIFROST REPINNED 0.7.0→0.8.0 (wire v0.5→v0.6). The memory search scope filter was split into scope_all (AND/intersection) + scope_any (OR/union over a LIST of conjunctive scopes) — bifrost #11, the canonical fix for the #295/#297 silent-zero AND foot-gun. Our store + contract (v1.2) + tests reimplemented to parity with the v0.6 reference _matches_scope/_validate_scope (no-compat: scope_filter REMOVED). 433 tests green incl. the new scope_any union test + the parity-vs-reference test through the real 0.8.0 dispatch_memory_call. Memory provider BOUNCED onto 0.8.0 (:8391, fresh empty memory.db — the prior 5-chunk #296 corpus was WIPED, operator confirmed "nothing of value", SUPERSEDES the "KEEP PINNED" note below). Shipped as v0.17.6 (96d61a4, tag v0.17.6); the #17 contract's stale scope_filter/_scope_matches-AND references were synced in the same commit. Affect plane untouched (split is memory-only); affect provider still on its 0.7.0-loaded process (bounce optional — affect wire unchanged at 0.8.0). Cold-recall status SUPERSEDED by the 2026-06-17 block above (now CLOSED end-to-end).

Ratatoskr now has a SECOND identity: the v1 Bifrost Tier-3 consumer — the durable persistence provider Worldtree writes Tier-3 agent affect/persona + memory into — alongside the original debug-observability TUI/web. The Bifrost-consumer work lives in src/ratatoskr/provider/ and depends on bifrost>=0.7.0 (a provider optional-extra from the gitea PyPI index), SEPARATE from the Worldtree conversation-API spec pin.

AFFECT plane: SHIPPED + LIVE-PROVEN (v0.17.2). Running now as a dev background shell (ratatoskr-provider, 0.0.0.0:8390, env-sourced from ~/.config/ratatoskr/provider.env). Smoked end-to-end against personal Worldtree v0.35.2 (10.250.50.152): handshake 200 + affect.emit 200 → durable row persisted, opacity held.

MEMORY plane: PROVIDER LIVE-PROVEN + recall-miss ROOT-CAUSED (upstream). Store + dev shell shipped (v0.17.3, cd12951; running on 0.0.0.0:8391). memory.db holds 5 durable chunks (all scope {end_user:smoke-user}): choc-fact 498ed752, name 8241e569, promoted-question c863bb6b, + 2 LATE async promotions from the cold-recall probe (probe-question acc3d49, model NON-ANSWER 4773704 — salience promoted a "I don't have memory" refusal). All are #296 corpus, KEEP PINNED. The #295 cold-recall miss is now ROOT-CAUSED and it's UPSTREAM, not ours (2026-06-16 debug-assist with worldtree-dev): a self-driven bound cold-recall probe captured the inbound pair via our new observe log — Worldtree's recall sends scope_filter={end_user:smoke-user, agent_self:ratatoskr:smoke} (TWO axes) but our chunks carry {end_user} ONLY; our AND _scope_matches (byte-faithful to bifrost reference reference_server/memory.py:398) drops everything on the unmatched agent_self axis → 0 hits → the model says "no memory". So our store + search are SOUND; the fix is Worldtree-side. F2 → research issue #296 (keyword-regex salience suspected fundamentally flawed; corpus = c863bb6b + the 2 late-promotions). F1 → research issue #297 (Worldtree-local fix = per-visible-scope single-axis search unioned client-side). The agent_self lattice question is RESOLVED: agent_self is now canonical in bifrost 0.7.0 / wire v0.5 (our foot-gun flag drove it; worldtree-dev shipped it both sides — Worldtree v0.35.11) → #297 union build UNBLOCKED. Our memory provider now runs bifrost 0.7.0 + validates the 4-axis lattice {end_user,group,tenant,agent_self} (v0.17.5; restarted on it; out-of-lattice axis → InvalidFilter, reference-parity restored).

OBSERVE BRICK SHIPPED (memory_store.py, committed v0.17.4 2fef6e3): structured [memory-provider] request/response logging on the memory-call path — the first concrete brick of #17's observe half, and the lens that caught #295's root cause. Live-verified. (NOTE: this is the debug SHIM at the STORE method; #17's real observe feed instruments the DISPATCH layer — see the contract INV-005.)

SELF-DRIVE PROVEN BY HAND (2026-06-16): ratatoskr's own client drove a Bifrost-bound cold-recall end-to-end (bind → handshake 200 → turn → captured the recall pair). Load-bearing finding: a bound session-create must use the CONSUMER Heimdall key (RATATOSKR_HEIMDALL_KEY) as the bearer, NOT the canary WORLDTREE_API_KEY — Worldtree signs the Bifrost handshake JWT with the session-create bearer (canary → 401; consumer → 200). Runbook: docs/bifrost-self-test.md. This is #17's substrate, proven before the contract.

ISSUE #17 (self-drive + observe) — CONTRACT WRITTEN + HEID-REVIEWED + FIXED, TDD NEXT. docs/contracts/issues/17.contract.md (validates OK, drift-clean). v1 scope operator-locked: single-plane bind (composite endpoint PARKED) + dispatch-layer op-feed with session-level correlation (turn-pane UI PARKED). /heid-contract-review panel caught + fixed two real internal inconsistencies (OpEvent turn_id reservation made literal; the session_id-for-all-verbs correction). NEXT: TDD slice 1 = the create_session bind primitive (BifrostBinding dataclass + consumer-key per-request bearer override + missing-key precondition + 502→BifrostHandshakeFailed; respx-mocked), then endpoint_for_plane → dispatch-layer op-feed → CLI/TUI/web → live smoke.

Sindra: a REGISTERED Tier-3 agent (ratatoskr:sindra, was model artemis-31b-v1i) — registration is REQUIRED to use a Tier-3 character (a session against an unregistered agent_id 404s), and she's been used. OPEN: the v0.35.2 personal rebuild may have wiped the agent DB — re-verify via GET /agents (needs a WORLDTREE_API_KEY, broker via infra-ops) and re-register if gone. Her persona only persists durably once personal is bound to our RUNNING affect provider for HER sessions (the smoke used synthetic ratatoskr:smoke); that binding is the persona-carry gap, independent of registration.

Heimdall key (Bifrost consumer): persists env-only at ~/.config/ratatoskr/provider.env (mode 600, nh3-dev) — consumer="ratatoskr", HS256 = the API-key STRING utf-8-encoded; rotate via infra-ops.

Committed (2026-06-16, NOT yet pushed): observe brick (logger, v0.17.4 2fef6e3), docs/bifrost-self-test.md + #17 contract (ca02c70), 4-axis-validation parity (v0.17.5, tag v0.17.5), + this snapshot. Tags v0.17.4/v0.17.5. Push is the operator's call. graphify-out/GRAPH_REPORT.md still runs dirty (auto-regenerated artifact, not chased).

Still standing from before: Worldtree spec pin now v0.35.16 (f1b59f8) for the conversation-API/TUI surface (bumped 2026-06-17 from v0.29.0/562001a). Codex-first pilot still dormant (no codex session spun up — see the 2026-05-29 decision). Open issues: #10 (subject migration, deferred), #11 (AdminEvents pane, deferred).

Branch: main. 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.ReadTimeoutSseConnectionDropped. 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.

  • [2026-06-14] Ratatoskr becomes the v1 Bifrost Tier-3 consumer. A second identity beyond the debug TUI: the durable persistence Worldtree writes Tier-3 agent affect (persona) + memory into. Pin bifrost>=0.6.1 in a provider optional-extra (gitea PyPI index, auth via ~/.netrc; 0.6.0 was yanked for a circular import). Implement bifrost's OWN MemoryDataStore/affect Protocols (NOT worldtree-memory's); describe_store is SYNC; affect is conduit-opaque. New module src/ratatoskr/provider/. Authoritative how-to: ~/development/bifrost/docs/implementing-a-consumer.md. (commits 1a73d77 pin, d90a58d affect store v0.17.1, bcdcd71 serve entrypoint v0.17.2)

  • [2026-06-14] Backend = SQLite + sqlite-vec; affect-first then memory; separate DB per plane (operator-chosen). Affect = blind conduit (reads only agent_id+end_user_id); memory = structural index (reads vector/scope/id/origin to serve search). Conformance for both = #195 parity vs bifrost's InMemory*Store through the real dispatch_*_call.

  • [2026-06-14] The affect contract's idempotency model was WRONG; real-lib TDD caught it. First draft modeled same-idempotency-key-different-payload as an LWW overwrite; bifrost actually raises a CONFLICT (AffectIdempotencyConflict), actor-scoped. The artifact-only /heid-contract-review STRUCTURALLY cannot catch this class (it never sees bifrost's source) — TDD against the shipped library is the gate; the executable reference store + #195 parity are the backstop. Filed the guide §6 gap to bifrost-dev, who fixed it (bifrost c0d0a11).

  • [2026-06-15] Memory v1 = the bifrost BASIC plane only (search/get/upsert/delete + describe_store/health) per worldtree-dev re-scope (#294) — the only surface Tier-3's live path touches; gated verbs (edges/scan/atomic_supersede/mark/patch/maintenance) deferred + advertised-unsupported. Worldtree v0.35.3 already requests+maps it — no Worldtree-side blocker. Memory contract committed v1.0 (eebab46) → v1.1 Heid-reviewed (1f94e5f).

  • [2026-06-15] Providers run as dev-box BACKGROUND SHELLS, not infra-ops/systemd (operator call — it's a dev box). ratatoskr-provider (affect) + a future ratatoskr-memory-provider as background processes; no productionization track.

  • [2026-06-15] Affect plane shipped (v0.17.2) + LIVE-PROVEN end-to-end against real Worldtree v0.35.2. Personal handshake 200 + affect.emit 200 from 10.250.50.152 → durable row persisted (opacity held). HS256 key = the consumer's Heimdall API-key STRING utf-8-encoded (NOT base64/raw — the tripwire); cross-subnet route + BIFROST_CLIENT_ALLOWED_HOSTS allowlist all held (infra-ops-owned). worldtree-dev confirmed ADR-0009 holding as designed.

  • [2026-06-16] #295 cold-recall miss root-caused — UPSTREAM, branch (a) scope-axis asymmetry. A self-driven bound cold-recall probe (our own client, consumer-key bearer) captured the inbound pair via the new observe log: Worldtree's recall filter carries {end_user, agent_self}; our chunks are {end_user}-only; AND-matching drops everything on agent_self → 0 hits. Our store + search are SOUND; the fix is Worldtree-side. F2 (question-promotion) → #296 research; F1 (recall-miss) → #297 research (worldtree-dev's Worldtree-local per-scope-union fix, HELD pending the lattice question).

  • [2026-06-16] agent_self → make it CANONICAL (operator decided A). The cross-repo "is agent_self a valid bifrost scope axis?" question: bifrost's reference lattice is {end_user, group, tenant} only (agent_self → invalid_filter 400); Worldtree emits agent_self (bifrost_memory_store.py:479 #248 agent-self primitive). Operator chose canonical-not-re-expressed; worldtree-dev filed the lattice-addition with bifrost-dev (thread 01KV7PXF…). Implication: our store's permissive axis-acceptance becomes CORRECT once bifrost adds agent_self — so do NOT add axis-validation; our missing _validate_scope_filter is HELD, not a bug to fix. #297 union build held until the axis lands.

  • [2026-06-16] Self-drive auth identity: bound session-create uses the CONSUMER Heimdall key as bearer, NOT WORLDTREE_API_KEY. Worldtree signs the Bifrost handshake JWT with the session-create bearer (canary key → handshake 401; consumer key → 200). Two keys, two identities. Proven by hand; documented in docs/bifrost-self-test.md; load-bearing for #17's Bind half.

  • [2026-06-16] Issue #17 v1 scope locked (operator 1A/2A): single-plane bind + dispatch-layer op-feed. BifrostBindingRequest is one endpoint_url (one plane per session); composite-both-planes endpoint PARKED. Observe = structured op-feed instrumented at the DISPATCH layer (bifrost passes ctx to upsert_many but NOT search/get/delete — memory.py:244), session-level correlation; turn-correlated pane UI PARKED (needs turn_id, TBD). Direct in-session TDD (live-smoke load-bearing). Contract docs/contracts/issues/17.contract.md written, /heid-design-consulted + /heid-contract-review-panel'd + fixed (validates OK). NEXT: TDD.

  • [2026-06-16] Provider stores confirmed byte-faithful to bifrost's AND reference (reference_server/memory.py:398 _matches_scope = all(...), identical to ours). OR-union was considered + rejected (ecosystem-wide change); flagged the silent-zero foot-gun to bifrost-dev (docs-only landed, bifrost stays 0.6.4). Do NOT flip _scope_matches to OR.

  • [2026-06-16] agent_self lattice SHIPPED both sides → our axis-validation gap CLOSED (v0.17.5). bifrost 0.7.0 / wire v0.5 adds agent_self to the scope lattice {end_user,group,tenant,agent_self} (#10, driven by our foot-gun flag via bifrost-dev); Worldtree pinned 0.7.0 + canonical-synced the v0.5 spec (v0.35.11, c860fb0). SUPERSEDES the prior "HELD, do NOT add axis-validation" note — we DID add _validate_scope_filter (4-axis) to match the reference (bifrost-dev's recommendation, purely additive; out-of-lattice → InvalidFilter). #297 union build unblocked. Memory provider restarted on 0.7.0.

  • [2026-06-16] #17 contract reviewed + the debug-assist arc fully closed. /heid design consult + /heid-contract-review panel both run on docs/contracts/issues/17.contract.md (validates OK). The #295 debug-assist that opened the session is closed end-to-end: root cause (scope-axis asymmetry) → #296/#297 research issues + corpus → a shipped bifrost protocol change (agent_self canonical) → our store at parity. NEXT durable step: #17 TDD (tracked: Gitea #17 + the contract).

  • [2026-06-16] Repinned bifrost 0.7.0→0.8.0 + reimplemented memory search to the v0.6 scope split (operator-directed). scope_filterscope_all (AND) + scope_any (OR/union over a list of conjunctive scopes), bifrost #11 — the canonical resolution of the #295/#297 silent-zero. SUPERSEDES the "Provider stores byte-faithful to AND reference / Do NOT flip _scope_matches to OR" entry above: the reference itself now does OR via scope_any (a NEW field — scope_all keeps the old AND semantics; this is an additive split, not a flip of the AND predicate). Store / contract (v1.2) / tests at parity with the v0.6 reference; provider bounced onto 0.8.0 with a wiped DB (operator: "nothing of value"). Cold recall now gated only on Worldtree emitting scope_any (#297). #17's contract has stale scope_filter/_scope_matches-AND references (its assumptions, INV-004, and the op-feed search → req {scope_filter} summary shape) — update those to scope_all/scope_any when #17 TDD starts; INV-004's intent (observe must not alter scope semantics) still holds.

  • [2026-06-17] Worldtree spec pin bumped v0.29.0→v0.35.16 (562001af1b59f8); cold recall CLOSED end-to-end. Worldtree shipped #297 (client-side per-scope-value union recall) + #298/#299 (adopt the bifrost v0.6 scope_any/scope_all wire) — it now emits scope_any on recall, the upstream half that pairs with our v0.17.6 provider. Re-vendored conversation-api-spec.md + conversation_api.contract.md; diff-reviewed the 285-commit catch-up — no client-breaking changes (#211 agent-slug rename saga→echo/actor→mask is slugs-only; #245 end_user_id+memory-scope; #187/#188/#219 Tier-3 define/PATCH additive; error codes stable). Shipped as a pin: commit, NO package bump (docs/pin-only, no ratatoskr code; per SemVer SKIP for docs-only). Remaining proof: a live cold-recall re-smoke against a personal instance on v0.35.16 — the smoke target ran v0.35.2/.3, needs infra-ops to update it.

  • [2026-06-17] End-to-end cold-recall proof RAN — our stack proven, #296 isolated. Against personal WT v0.35.16 with restored ratatoskr:sindra: #297/#298 union recall (WT emits scope_any), write path (promote→upsert→persist), and cold read (scope_any returns hits) ALL proven. Lone gap = upstream #296 extraction quality. Refines the spec-repin entry's "cold recall closes end-to-end": the WIRE closed; actual fact-recall is #296-blocked. (triage 01KVBBH0…)

  • [2026-06-17] DELETE+redefine ratatoskr:sindra (operator-authorized; pre-v1 debug surface). She SURVIVED the rebuild but was STALE (dead model artemis-31b-v1i + no memory block from Phase-2.0 tooling); memory is immutable post-define, so DELETE+redefine was the only path. v0.35.16 define takes role (capability), NOT model: role:"character" → first-healthy bind mistral-small-4; memory:{} trips the promotion gate (GET does NOT echo memory_config). Reasoning bind needs a character-role bind reorder (worldtree-dev flagged to operator). Our tier3.py define is Phase-2.0-stale (no role/memory/persona layers) — untracked modernization follow-up.

  • [2026-06-17] Promotion = 4-trigger hybrid (worldtree-dev, code-grounded): salience (regex, 90s rate-limit) / turn_count≥6 / context_pressure / idle ≥10min (unconditional on quality); per-turn plan_promotion_run for consumer_defined. DELETE does NOT drain/promote (delete-is-delete, #276) — idle ≥10min is the deterministic flush. The seed promoted at ~4min (per-turn salience/turn-scan, before the idle floor).

  • [2026-06-17] #296 triage sent to worldtree-dev (01KVBBH0QRGT34GDJQEX5ACTT1): extraction SUBJECT-INVERSION (promotes assistant prose, drops the user's fact) + META-DESCRIPTION-not-content; verbose-persona aggravator. The WAD-vs-bug cut resolved to BUG (extraction quality), not idle-gating.

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.
  • [2026-06-15] "Sindra hasn't been registered" was an under-verified inference — WRONG. Concluded it from grepping ratatoskr's CODE (sindra absent from src/), but Tier-3 registration is SERVER-SIDE (POST /agents/define on the Worldtree instance) — a code grep structurally can't see it. Registration IS required to use a Tier-3 character (a session against an unregistered agent_id 404s), so since Sindra has been used, she WAS registered (ratatoskr:sindra). Rule: to check whether a Tier-3 agent exists, query the Worldtree instance's GET /agents, never the consumer repo's code. (Residual: the v0.35.2 personal rebuild may have wiped her — re-verify.)
  • [2026-06-14] Artifact-only contract review can't validate against a dependency's ACTUAL behavior. /heid-contract-review sees only the contract, never the external library (bifrost) — so "the consumer under-built against bifrost's real semantics" is invisible to it by construction (the affect idempotency model shipped wrong because of this). Real-lib TDD against the shipped library + the executable reference store + the #195 parity test are the gate for any consumer plane with non-trivial state semantics. Don't treat a clean contract review as evidence the code matches the dependency.
  • [2026-06-15] "byte-equal" round-trip slip propagated affect→memory via copy-paste. The affect contract's byte-identical→semantic fix reappeared in the memory contract's INV-001 (sibling copy). Only an INDEPENDENT /heid-contract-review of the memory contract re-caught it. Paraphrase every sibling contract fresh — don't amortize one review across a family; copies carry the parent's slips. (also a feedback auto-memory)
  • [2026-06-15] Canonical sync retired the issue-scoped parser staleness (predicted by the 2026-05-21 entry's "until canonical bumps"). contract_parser.py synced to v2.1 (f1fdfdb6→e10a4460, commit d85ab43): now validates issue-scoped frontmatter (target_module/scope/prd) + four v2.1 test categories (scenario/trace/adversarial/property). Issues #3/#4 went FAIL→WARN (0 errors). The old "treat parser ERROR-on-issue-scoped as expected" note no longer applies.
  • [2026-06-15] Refreshed #3/#4 presenter contracts to the shipped TUI model (commit 335c835). Both still described the abandoned single-RichLog double-display model; rewrote to the 4-pane live-Markdown reality (v0.5.0v0.14.0 + Worldtree #201/#204) across INV-005, the [performance] constraint, the COMPOSE sketch, the CLASS TuiPresenterState block, both render/_stream_turn_worker blocks, and the _cancel_via_sse call site — plus the STEPS the v2.1 parser flagged missing. Code unchanged; contract-truth catching up to shipped code. Scope ballooned one-block→contract-wide mid-task; surfaced to operator before rewriting the INV-005 trade-off invariant.
  • [2026-06-15] Memory plane TDD'd + shipped (commit cd12951, v0.17.3). Impl decisions worth keeping: vec0 distance_metric=cosine set at table creation (score = 1 distance); search over-fetches ALL candidates by cosine then scope-filters in Python so top_k counts IN-SCOPE hits (INV-005, contract STEP 2 indicative); idempotency_id = reference 4-tuple ("default",verb,_ctx_actor(ctx),key) pipe-joined as the SQLite PK, digest = sha256 canonical-JSON; _ctx_actor = job_id|jwt_sub|session_id (memory reference's 3-level, vs affect's 2-level). heid-code-review panel returned zero true drift; adopted 5 cheap contract-anchored fixups (scope_filter dict guard, top_k≤0→[], stronger scope-isolation / delete-hit-search / handshake-POST tests), accepted 6 with reasoning. Partial-map optimistic-lock semantics pinned to the reference via an expected_revisions parity test — resolved a Hulda finding deterministically (the affect-plane lesson: TDD against the shipped lib is the gate, not judgment).
  • [2026-06-15] Memory provider LIVE-PROVEN against personal v0.35.3 (persist + dispatch + search-correctness); recall-injection is upstream. worldtree-dev's Tier-3 promotion recipe (via infra-ops): memory-call fires from Tier-3 PROMOTION, gated at service.py:2623 on ctx.kind=="consumer_defined" AND ctx.memory_config is not None (the agent must be DEFINED WITH a memory block — ValidatedMemoryConfig {tier3_dreaming:false}, dim 1024) AND handshake-granted memory caps AND embedding_dim==1024. memory.agent_self_enabled is NOT the gate (only the #248 self-candidate branch). Binding = POST /sessions BifrostBindingRequest{endpoint_url}, handshake caps=["affect","memory"], binding.scope null (per-op scopes auto-minted: upsert_many→memory:write, search→memory:read). A BIFROST_CLIENT_ALLOWED_HOSTS allowlist gates the endpoint (Worldtree-side config — infra-ops added :8391). HTTP + HS256 both work in dev. (smoke wiring thread 01KV7D82MJYB…)
  • [2026-06-15] Diagnostic: our recall-search is SOUND — the cross-session recall gap is UPSTREAM, not the store — and it caught an upstream bug. Embedded the recall query via gateway qwen3-embedding + searched our live store directly → the dark-chocolate fact recalls at cosine 0.60, correctly ranked above the unrelated name fact (0.16). So the cold-session recall failure is Worldtree's recall-assembly/injection (hits not reaching the prompt), NOT our search. ALSO found a latent UPSTREAM bug: a recall QUESTION got promoted as a durable chunk and ranks #1 (0.70 > the fact's 0.60), polluting recall. Relayed to worldtree-dev (thread 01KV7JH8…). This is exactly #17's thesis — ratatoskr-as-provider caught an upstream bug invisible from the chat side.
  • [2026-06-15] "Wire 200 ≠ recall works" — prove recall efficacy at the model's answer in a COLD (history-free) session, not on the wire. A search/memory-call returns 200 whether or not its results are injected into the prompt, and same-session "recall" can be plain session history. infra-ops' cold cross-session probe caught my premature "all-green" (search dispatched 200, model had no memory). Don't call cross-session recall proven from a clean wire.
  • [2026-06-15] Issue #17 filed — Bifrost-binding for the chat client (self-drive + correlated-log affect/memory ops). REVERSES design-brief §6's "no Bifrost-binding consumer support" — that negative clause predates ratatoskr's provider identity (2026-06-14), so the canary now owns both ends but its client can't drive its own provider (create_session sends only {agent_id, end_user_id}; no Bifrost endpoint_url). Today's smoke proved the substrate (bind→dispatch→persist); only the observe/log channel design (open question #5) remains. The recall-injection caveat is upstream and doesn't block #17. NEXT on #17: /heid consult on the now-grounded framing → contract → TDD. (tracked: Gitea #17, labels enhancement/observability/tui)
  • [2026-06-16] scripts/contract_drift_check.py defaults GITEA_REPO to "Worldtree" (line 74), so a bare run in ratatoskr false-positives DRIFT by hashing Worldtree's same-numbered issue. Always export GITEA_REPO=ratatoskr GITEA_OWNER=vh (env.sh leaves the GITEA vars commented out) before running the drift-checker here.
  • [2026-06-16] My #295 coupling hypothesis (the promoted question crowds out the fact at small top_k) was REFUTED — worldtree-dev's recall over-fetches top_k=128 (injector.py:203/_store_helpers.py:101), so the question can't crowd the fact out at search level. Reasonable cross-frontier hypothesis, correctly framed as a hypothesis not a conclusion; the real cause was the scope-axis asymmetry. Lesson: offer provider-side hypotheses, let the upstream owner check them against their code.
  • [2026-06-16] Contract drifted from its own design in two spots, caught only by /heid-contract-review (not same-author paraphrase): the OpEvent dataclass omitted the turn_id that INV-005 promised; the session_id comment said "None for search/get/delete" contradicting the dispatch-layer design (the JWT carries session_id for all verbs at dispatch). Cross-model paraphrase is load-bearing for catching an author's own contract-vs-intent drift.
  • [2026-06-16] "No promotion" was checked TOO EARLY — Tier-3 promotion is ASYNC (lands AFTER the SSE turn-end). The cold-recall probe's immediate post-turn fixture check showed 3 chunks (no promotion); a later check (during the v0.17.5 provider restart) found 5 — the probe HAD promoted 2 chunks (its question acc3d49 + the model's non-answer 4773704), just late. Don't trust an immediate post-turn fixture snapshot to judge promotion; it lands after the turn completes. (Same family as the "wire-200 ≠ recall, prove it in a cold session" lesson, extended to promotion timing — and the reason #17's contract pins a post-turn grace window + fixture before/after assertion.)
  • [2026-06-17] "sindra is GONE" (infra-ops, from GET /agents + admin token) was a FALSE NEGATIVE. Consumer-defined Tier-3 agents are OWNER-SCOPED (separate consumer_agents table) — invisible to the foundational GET /agents roster even with an admin token. To check whether a Tier-3 agent exists, GET /agents/<owner>:<name> with the OWNER key. (Extends the 2026-06-15 "Sindra registration under-verified" lesson — even the roster query can't see consumer agents; only the owner-scoped point-GET does.)
  • [2026-06-17] "Promotion didn't fire → #296" was PREMATURE — twice over. (1) Polled the op-feed only ~2min, but the upsert landed at ~4min — promotion is async + multi-trigger; watch a longer window. (2) It DID fire; the real bug is extraction QUALITY (assistant-prose promoted, user-fact dropped), not non-firing. "No upsert while a session is live and <10min idle" is WAD, not a miss. (Extends the 2026-06-16 "checked too early" entry with the multi-trigger timing + the fired-but-bad-content distinction.)