Worldtree shipped its half of the union-recall fix: #297 (client-side per-scope-value union recall) + #298/#299 (adopt the bifrost v0.6 scope_any/scope_all wire, v0.35.16). It now emits scope_any on the recall path, pairing with our v0.17.6 provider — cold cross-session recall is closed end-to-end (pending a live re-smoke against a v0.35.16 instance). Re-vendored conversation-api-spec.md + conversation_api.contract.md; 285-commit catch-up (v0.29.0 -> v0.35.16). Diff-reviewed: no client-facing breaking changes for our consumer. - #211 agent-slug rename (saga->echo, actor->mask) — slugs only, we pass --agent - #245 end_user_id persistence + memory-scope resolver (additive) - #187/#188/#219 Tier-3 define/PATCH policy (additive); error codes stable - bifrost binding field + ephemeral_does_not_accept_bifrost 422 now documented (#17 surface) - docs: SPEC-PIN.md pin table + history; bifrost-self-test recall status; persistent-memory No package version bump (docs/pin-only, no ratatoskr code change).
55 KiB
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-16:
LATEST (2026-06-17) — COLD RECALL CLOSED END-TO-END + WORLDTREE SPEC REPINNED.
Worldtree shipped its half: v0.35.16 adopts the bifrost v0.6 wire and emits
scope_any/scope_all on recall (#297 client-side per-scope-value union recall +
#298/#299 bifrost-v0.6 adoption). With our v0.17.6 provider, cold cross-session recall
is now CLOSED end-to-end. Remaining: a live re-smoke against a personal instance on
v0.35.16 (the running smoke target was v0.35.2/.3 — needs infra-ops to update it).
Worldtree spec pin bumped 562001a/v0.29.0 → f1b59f8/v0.35.16 (docs/SPEC-PIN.md +
worldtree-spec-rev; 285-commit catch-up). Client-facing deltas reviewed, NONE break
our consumer: #211 agent-slug rename (saga→echo, actor→mask — slugs only), #245
end_user_id persistence + memory-scope resolver, #187/#188/#219 Tier-3 define/PATCH
policy (additive); the bifrost binding field + ephemeral_does_not_accept_bifrost 422
are now in-tree (the #17 surface). No ratatoskr code change for the spec bump; shipped
as a pin: commit (no package bump — docs/pin only). The provider half shipped earlier
as v0.17.6 (96d61a4, tag v0.17.6).
(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). Seedocs/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-ssefor SSE consumption. The server emits composite{turn_id}:{seq}id:lines (Worldtree INV-014) load-bearing for SSE-resume; hand-rolleddata:-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.logis 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--newfor scripted use. Session identity always visible in Textual footer. -
[2026-05-20]Markdown rendering default-on;--rawopt-out. Don't pre-design--no-stream-formatting(Volva: add only if streaming-markdown rendering is empirically ugly). -
[2026-05-20]Non-interactive--sendmode. 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. Bundlesstream_turn+reconnect_turn+cancel_turn+ private_parse_sse_idinto one module — the SSE-resume flow is coupled (cancel needsturn_idfrom the SSE wireid:, reconnect re-uses the same parsedSseId), 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-rolleddata:-only parsing silently drops theid:). -
[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 --validateERRORs 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-ownedhttpx.AsyncClientconstructed withtimeout=httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0); defense in depth:sse_client.stream_turnERROR_ROUTING catcheshttpx.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 withJSONDecodeError("Expecting value: line 1 column 1 (char 0)")fromjson.loads('')on an empty-data:SSE frame (→ #7). Earlier same day,ratatoskr --new --agent lofnfailed with 422end_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 /agentsexists in the vendored spec; returnsagent_id/name/description+ optional fields.--agentbecomes 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). Restructuresrun_tuilifecycle:_resolve_then_runasync 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_amainand_resolve_then_run. -
[2026-05-23]Worldtree-dev consult landed authoritative consumer-API guidance (althing thread01KSBARG2B8M8C82H6AJGJWX1B). Takeaways:end_user_idis a free-form partition key; no programmaticrequires_end_user_iddiscovery; 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:lofnscope needed for lofn smoke;GET /agentsrequires 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 requireend_user_idfor EVERY access before too long. Operator's call. Reasoning: even Tier 1 foundational agents that don't requireend_user_idserver-side currently fall back to a_no_end_usersentinel partition — effectively pollution. Cross-frontier alignment (worldtree-dev ack, althing01KSBD9FPMCWJMBXNNS4B3MYBS): 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 fromRichLogtoVerticalScroll; each turn's response lives as a singleStaticwidget whose Markdown content is updated as Text deltas arrive (no post-Done re-render, no double-print).--rawbypasses 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 foraffect_updateSSE event +GET /agents/{id}/persona_stateendpoint. 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-headerline + Ctrl+4 Persona TabPane; live updates onAffectUpdate(status="current"); on-mount hydration via the GET endpoint). -
[2026-05-26]Worldtree #201 / v0.29.0 integration (v0.14.0). New SSE eventawaiting_llm_first_tokenheartbeat (default 5s interval) during the BuildingPrompt→CallingLLM gap. Top-level event, NOT a worker_phase extension (preserves INV-053 three-field stability).AwaitingLlmFirstTokendataclass + parse; TUI live transcript indicator ("awaiting first token · Ns") mounted on first heartbeat, updated in place, removed when the gap closes; turn-summary line gainsheartbeats=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, butcli.py'sCliPresenterState.renderhas its own isinstance check that wasn't widened.ratatoskr --sendcrashed 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=0on 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. Newratatoskr.web(Starlette app + lazy-import entrypoint + single-page vanilla HTML/CSS/JS UI), new console scriptratatoskr-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 bind0.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-sessionsystem_promptmutation: 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 thread01KSKD1GA3XBWR9RHGZCF9FE3Y). -
[2026-05-27]Artemis (Gemma4) reasoning-token gap was upstream, not ours. Wire trace from ratatoskr showed zerothinkingevents forartemis-31b-v1i; infra-ops confirmed llama-swap emits 77reasoning_contentdeltas at the OpenAI-compat layer (--reasoning-format deepseek). Gap was in Worldtree'sGemmaProvider. Worldtree-dev shipped v0.29.13 (commit4262430) fixing two stacked bugs: (1) baseOpenAICompatProvider._extract_thinking_from_deltareturnedNoneunconditionally so any model falling through to the generic class dropped reasoning; (2) catalogfamilylookup 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_stateunwraps FastAPIdetail-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_COUNTERids (1, 2, 3…) instead of upstream Worldtree turn_id (e.g. 799) captured from the first SSE event. Thedisconnect_triggers_canceltest gap was the load-bearing miss. Also: server-configuredRATATOSKR_END_USER_ID(browser can no longer impersonate partition); narrowed missing-extrasImportErrorcatch (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; dedicatederror_terminal_event+cancelled_terminal_eventtests since terminal events are mutually exclusive with done). Disconnect-cancel catch narrowed to swallow onlyCancelAlreadyCompleted/CancelTurnNotFound(the cooperative race); log unexpectedCancelFailed/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 viapython -m ratatoskr.tier3 patch: (1) model migrated fromqwen3.6-35-a3b-heretictoartemis-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-designpass 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-rolledmarkdownSafe()(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 pushedAGENTS.md(commitbbeaa23) and declared theratatoskr-codexhandle perbrokkr-smithy/docs/codex-first-discipline.mdv0.1 (brokkr-smithy commit5dd061c, tagv0.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 sendscodex-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. Pinbifrost>=0.6.1in aprovideroptional-extra (gitea PyPI index, auth via~/.netrc; 0.6.0 was yanked for a circular import). Implement bifrost's OWNMemoryDataStore/affect Protocols (NOT worldtree-memory's);describe_storeis SYNC; affect is conduit-opaque. New modulesrc/ratatoskr/provider/. Authoritative how-to:~/development/bifrost/docs/implementing-a-consumer.md. (commits1a73d77pin,d90a58daffect store v0.17.1,bcdcd71serve 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 onlyagent_id+end_user_id); memory = structural index (reads vector/scope/id/origin to serve search). Conformance for both = #195 parity vs bifrost'sInMemory*Storethrough the realdispatch_*_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-reviewSTRUCTURALLY 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 (bifrostc0d0a11). -
[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 futureratatoskr-memory-provideras 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.emit200 from10.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_HOSTSallowlist 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 onagent_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_filter400); 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 (thread01KV7PXF…). Implication: our store's permissive axis-acceptance becomes CORRECT once bifrost adds agent_self — so do NOT add axis-validation; our missing_validate_scope_filteris 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, NOTWORLDTREE_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 indocs/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.BifrostBindingRequestis oneendpoint_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). Contractdocs/contracts/issues/17.contract.mdwritten,/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_matchesto 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./heiddesign consult +/heid-contract-reviewpanel both run ondocs/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 memorysearchto the v0.6 scope split (operator-directed).scope_filter→scope_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_matchesto OR" entry above: the reference itself now does OR viascope_any(a NEW field —scope_allkeeps 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 emittingscope_any(#297). #17's contract has stalescope_filter/_scope_matches-AND references (itsassumptions, INV-004, and the op-feedsearch → req {scope_filter}summary shape) — update those toscope_all/scope_anywhen #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 (562001a→f1b59f8); cold recall CLOSED end-to-end. Worldtree shipped #297 (client-side per-scope-value union recall) + #298/#299 (adopt the bifrost v0.6scope_any/scope_allwire) — it now emitsscope_anyon recall, the upstream half that pairs with our v0.17.6 provider. Re-vendoredconversation-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 apin: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.
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/logSSE endpoint on Worldtree. Considered as alternative to file-tailingpersona.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 withmarkup=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]Queryingself.query_one("#transcript", RichLog)from inside a Textualrun_workercoroutine. Initially failed withNoMatches. Reactive fix was widening worker signature to takelogas parameter — Volva flagged as contract drift; reverted. Real fix was test-side:await pilot.pause()betweeninp.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 viaself.sub_title+self.hintplain attributes. Stored state but never rendered to a visible widget. Tests asserted attributes (passed); Volva code-review flagged the gap. Fix: dedicatedStatic(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_eventsunconditionally calledjson.loadson every dispatchedServerSentEvent. Whenhttpx_ssesurfaced a frame withid:present butdata:empty,json.loads('')raisedJSONDecodeError→ app crash. Fix:if sse.data == '': continueBEFORE_parse_sse_id. Don't reintroduce unconditionaljson.loads(sse.data).[2026-05-23]Diagnostic shorthand: "2-events-then-silence" = Worldtree-side LLM-call wedge, not ratatoskr. If a mimir--sendsmoke 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'sservice.py:2560gates theCallingLLMevent on the engine yielding its first LLM-provider chunk; if that connection is wedged at TCP level, theasync fornever iterates. Worldtree's 300s_start_stall_timercancel-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 needingagents.call:lofnadded. 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 blanketagent.call:*(singular) baseline. There is no per-agent grant for Tier 1. Tier 3 consumer-defined agents use the pluralagents.call:<owner>:<agent>shape registered viaPOST /agents/define. The notations differ by one letter. The actual lofn fix was issue #5's--end-user-idflag — 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]TextualRichLog(wrap=True)insufficient on narrow widgets. The defaultmin_width=78overrides wrap on shrink —max(renderable_width, min_width)forces 78-cell rendering then horizontal-scrolls. Always setmin_width=0on 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.--sendmode 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 forisinstance(event, (across the repo — currently TUI and CLI presenters both carry duplicate hardcoded tuples. Refactor to a shared_EVENT_VOCABconstant 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 hadPOST /api/turns/{sid}/streamfor the SSE proxy. Browser-nativeEventSourceonly 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=Nstreams 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_statemocked flat error envelope; real Worldtree wraps indetail. 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 toSessionApiFailed(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-sessionsystem_promptmutation: 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'ssse_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 (sindraabsent fromsrc/), but Tier-3 registration is SERVER-SIDE (POST /agents/defineon the Worldtree instance) — a code grep structurally can't see it. Registration IS required to use a Tier-3 character (a session against an unregisteredagent_id404s), so since Sindra has been used, she WAS registered (ratatoskr:sindra). Rule: to check whether a Tier-3 agent exists, query the Worldtree instance'sGET /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-reviewsees 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-reviewof 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.pysynced to v2.1 (f1fdfdb6→e10a4460, commitd85ab43): 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 (commit335c835). Both still described the abandoned single-RichLogdouble-display model; rewrote to the 4-pane live-Markdown reality (v0.5.0–v0.14.0 + Worldtree #201/#204) across INV-005, the[performance]constraint, the COMPOSE sketch, theCLASS TuiPresenterStateblock, bothrender/_stream_turn_workerblocks, and the_cancel_via_ssecall 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 (commitcd12951, v0.17.3). Impl decisions worth keeping: vec0distance_metric=cosineset at table creation (score = 1 − distance);searchover-fetches ALL candidates by cosine then scope-filters in Python sotop_kcounts IN-SCOPE hits (INV-005, contract STEP 2indicative); 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 anexpected_revisionsparity 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 atservice.py:2623onctx.kind=="consumer_defined"ANDctx.memory_config is not None(the agent must be DEFINED WITH amemoryblock —ValidatedMemoryConfig {tier3_dreaming:false}, dim 1024) AND handshake-granted memory caps ANDembedding_dim==1024.memory.agent_self_enabledis NOT the gate (only the #248 self-candidate branch). Binding =POST /sessions BifrostBindingRequest{endpoint_url}, handshakecaps=["affect","memory"],binding.scopenull (per-op scopes auto-minted: upsert_many→memory:write, search→memory:read). ABIFROST_CLIENT_ALLOWED_HOSTSallowlist gates the endpoint (Worldtree-side config — infra-ops added:8391). HTTP + HS256 both work in dev. (smoke wiring thread01KV7D82MJYB…)[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 gatewayqwen3-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 (thread01KV7JH8…). 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. Asearch/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_sessionsends only{agent_id, end_user_id}; no Bifrostendpoint_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:/heidconsult on the now-grounded framing → contract → TDD. (tracked: Gitea #17, labels enhancement/observability/tui)[2026-06-16]scripts/contract_drift_check.pydefaultsGITEA_REPOto "Worldtree" (line 74), so a bare run in ratatoskr false-positives DRIFT by hashing Worldtree's same-numbered issue. Alwaysexport 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-fetchestop_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): theOpEventdataclass omitted theturn_idthat INV-005 promised; thesession_idcomment 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 questionacc3d49+ the model's non-answer4773704), 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.)