Files
ratatoskr/persistent-memory.md
T
vh a358cc9150 memory: snapshot — b1→b2 409/503 adaptation + bifrost 1.0.0 + combined-bind default + admin key + regard-dead-axis finding
Worldtree on v1.0.0b2 (both demo + personal); ratatoskr v0.18.4 all pushed.
Session arc: web combined-bind default (v0.18.1), bifrost 1.0.0 repin
(v0.18.2), b1/b2 eager 409/503 status mapping (v0.18.3/.4), readonly-admin
key collected (#11 prereq cleared), and the regard-dead-axis finding
(provider-side catch -> worldtree-dev escalating to Vuong). Next substantive
effort = the v1 coverage-audit (folds in the deferred live-409 + b2 spec
re-vendor).
2026-06-30 13:57:04 -07:00

40 KiB
Raw Blame History

Persistent memory — ratatoskr

Last updated: 2026-06-30

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).

Second identity (since 2026-06-14): the v1 Bifrost Tier-3 consumer/provider — the durable persistence Worldtree writes Tier-3 agent affect (PAD/persona, :8390)

  • memory (:8391) into. Lives in src/ratatoskr/provider/, depends on bifrost (provider optional-extra), separate from the conversation-API spec pin. So ratatoskr now owns BOTH ends of the Bifrost round-trip — the lens #17 exploits.

v0.15.0+ sibling browser surface (ratatoskr.web, ratatoskr-web console script): same five-pane debug surface over the same SSE wire, LAN-viewable. Internal-LAN trust model — 0.0.0.0, no auth/TLS/CORS (operator direction). Disciplined regardless: transcript HTML-escapes assistant content (INV-004); upstream API key stays server-side (INV-003).

Current state / in-flight

As of 2026-06-20:

#17 and #18 BOTH CLOSED — the composite both-plane binding is fully proven. #18 shipped v0.18.0 (359dbb1): D2 (PAD read-endpoint, v0.17.14) renders live PAD in the web pane from our :8390 store; D1 (composite endpoint, v0.17.16 7f4ceaa) — build_combined_provider_app (provider/combined.py) on :8392 wraps bifrost's public build_combined_app over BOTH stores + the shared affect read route; one bound session drives memory.* AND affect.* through ONE endpoint, op-feed deriving plane per path. Suite 503 green. #17 closed in the tracker 2026-06-20 (shipped v0.17.8.13 + the v0.17.17 op-feed field fix).

#18's final leg — the Worldtree-DRIVEN composite turn — RAN and is PROVEN end-to-end + persisted (2026-06-20). infra-ops added 10.100.10.50:8392 to the personal WT's (:8081) BIFROST_CLIENT_ALLOWED_HOSTS (thread 01KVHWJGTT…), unblocking the smoke. A real WT turn through :8392 (session b83a66b6, agent ratatoskr:sindra, fresh end_user resmoke-choco-1) drove the FULL both-plane lifecycle on ONE endpoint, caps-routed by path: handshake (caps_granted=[memory, affect]) → affect.fetch + memory.search (reads) → affect.emit (stored:true, PAD row in affect_snapshots) → memory.upsert_many (upserted:1, chunk 2df1b79de761b948 in memory_chunks). Both writes verified directly in our SQLite. The model-backend outage that blocked the first attempt (both agents' models model_unavailable) was operator-fixed mid-session, then the resmoke completed clean. No open legs remain on the composite.

bifrost repinned 0.8.0 → 0.10.0 (floor, provider extra). 0.10.0 made affect.fetch MANDATORY (strong-or-absent: _supports_affect_plane requires affect_supported+emit+fetch, gating EVERY affect op incl. emit) — so the repin FORCED affect.fetch (v0.17.15, conformed to bifrost's reference InMemoryAffectStore.fetch{found, snapshot?}) or our shipped affect plane would 400. The composite's affect cap depends on it.

OPERATOR SESSION STATE — :8390/:8391/:8765 shells are PRE-#18 code (foot-gun). web :8765

  • affect :8390 + memory :8391 are prior-session background shells on OLD code. The :8392 composite provider is RUNNING on NEW code (ratatoskr-combined-provider, pid started Jun19, RATATOSKR_OPFEED_PATH=/tmp/ratatoskr-combined-opfeed.jsonl, shared affect.db/memory.db) — now :8392-allowlisted and WT-turn-proven. To see the full web stack on new code, RESTART :8390/:8765 from current code (D2 web needs RATATOSKR_AFFECT_READ_URL). Consumer/owner key = wt_live_d81b… (~/.config/ratatoskr/provider.env, mode 600, rotate via infra-ops); providers SQLite + sqlite-vec, memory.db/affect.db at repo root (live sindra PAD: vuong + the resmoke-choco-1 smoke fixture).

Tier-3 memory PROVEN end-to-end (earlier this session): ratatoskr:terse-probe cold-recalled a seeded user fact (scope_any → 1 hit @ cosine 0.6994), and the verbose sindra-probe too under #296 Stage 2 (v0.36.0). The #296 extraction-quality arc closed (Stage 1 v0.35.19 gate + Stage 2 v0.36.0 user-only extraction at worldtree-codex; hard- linguistic layer → Worldtree #305). :8081 runs v0.36.0.

Sindra: ratatoskr:sindra, thoughtful-character role → mistral-small-4-reasoning (DELETE+redefined on v0.35.16; memory:{} block trips the promotion gate). Owner-scoped (separate consumer_agents table) — invisible to GET /agents; check via GET /agents/<owner>:<name> with the owner key.

Standing: Worldtree spec pin v0.35.16 (f1b59f8); bifrost 0.10.0 / wire v0.6 (scope_all+scope_any). Heimdall key env-only at ~/.config/ratatoskr/provider.env (mode 600); rotate via infra-ops. graphify-out/ runs dirty (auto-regen, not chased). Open issues: #11 (AdminEvents pane — the next-reachable Worldtree-I/O coverage gap, blocked on an admin.events.read scope request) and #10 (subject-migration watch on Worldtree #196) — both deferred. #17 + #18 CLOSED. Codex-first pilot dormant. No in-flight implementation work — repo is at a converged checkpoint; v1 advances when Worldtree does (v1 = full Worldtree I/O coverage).

Branch: main (tag v0.18.0, 359dbb1) — in sync with origin/main (the full #17+#18 arc is pushed). This /snapshot commit will sit one ahead of origin until pushed (push is the operator's call). 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-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) + 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, scope-axis asymmetry. A self-driven bound cold-recall probe captured the inbound pair via the observe log: Worldtree's recall filter carries {end_user, agent_self}; our chunks were {end_user}-only; AND-matching dropped everything on agent_self → 0 hits. Our store + search are SOUND; fix is Worldtree-side. F2 (question-promotion) → #296; F1 (recall-miss) → #297.

  • [2026-06-16] agent_self → make it CANONICAL (operator decided A). bifrost's reference lattice was {end_user, group, tenant} only (agent_self → invalid_filter 400); Worldtree emits agent_self (#248). Operator chose canonical-not-re-expressed; worldtree-dev filed the lattice-addition with bifrost-dev. Implication: our store's permissive axis-acceptance becomes CORRECT once bifrost adds agent_self.

  • [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 (→ now #18). Observe = structured op-feed 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. Contract docs/contracts/issues/17.contract.md written + /heid-reviewed.

  • [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 {end_user,group,tenant,agent_self} (#10, driven by our foot-gun flag); Worldtree pinned 0.7.0 (v0.35.11). We DID add _validate_scope_filter (4-axis) to match the reference (purely additive; out-of-lattice → InvalidFilter).

  • [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. The reference now does OR via scope_any (a NEW field — additive split, not a flip of AND). Store / contract (v1.2) / tests at parity with the v0.6 reference; provider bounced onto 0.8.0 with a wiped DB. Shipped v0.17.6 (96d61a4). (SUPERSEDED the earlier "do NOT flip _scope_matches to OR" note.)

  • [2026-06-17] Worldtree spec pin bumped v0.29.0→v0.35.16 (562001af1b59f8); cold recall closed on the WIRE. Worldtree shipped #297 (client-side per-scope-value union recall) + #298/#299 (adopt the bifrost v0.6 scope_any/scope_all wire) — emits scope_any on recall, pairing with our v0.17.6 provider. Re-vendored the spec; diff-reviewed the 285-commit catch-up — no client-breaking changes. pin:-only commit, no bump.

  • [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, write path, and cold read ALL proven. Lone gap = upstream #296 extraction quality (the WIRE closed; fact-recall was #296-blocked).

  • [2026-06-17] DELETE+redefine ratatoskr:sindra (operator-authorized; pre-v1 debug surface). She SURVIVED the rebuild but was STALE (dead model + no memory block); 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). Our tier3.py define is Phase-2.0-stale — 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.

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

  • [2026-06-18] Tier-3 memory PROVEN end-to-end liveratatoskr:terse-probe recalled a seeded user fact in a COLD history-free session (scope_any → 1 hit @ cosine 0.6994). Closes the opening "how far from Tier-3 memory" question for normal agents.

  • [2026-06-18] #296 Stages 1+2 closed. Stage 1 (v0.35.19, recallability admission gate) validated live for normal turns; bisect localized the residual to verbose-persona VOLUME crowd-out. Stage 2 (v0.36.0, MERGED at worldtree-codex) = user-only one-call-per-turn extraction, the STRUCTURAL fix; hard-linguistic layer → Worldtree #305 (we handed over a live-validated eval fixture PAIR). Full-coverage re-smoke: verbose sindra-probe promoted the fact cleanly + cold-recalled @ 0.694 under v0.36.0.

  • [2026-06-18] #17 implemented end-to-end via direct in-session TDD (6 patch bumps v0.17.8v0.17.13, suite 470 green). Slice order: bind primitive → op-feed → CLI → TUI → web(server) → web(UI). Tests drive the REAL bifrost dispatch via minted JWTs (bifrost.core.dispatch_jwt.mint_dispatch_jwt) — the "test against the shipped lib" posture, not hand-mocked envelopes. Op-feed reads session_id off the dispatch JWT sub claim (the contract open-q, resolved YES at the ASGI layer where the JWT is always present — bifrost.reference_server._dispatch_auth.DispatchContext.session_id = payload["sub"]). bifrost wire facts captured in-code: memory envelope {operation, args}memory_result(**payload)={success,...}; verbs bare (search/upsert_many/get/get_many/delete_many); affect {operation:"affect.emit"}{success,stored}; error envelope {code, message}; scopes memory:read|write.

  • [2026-06-18] #17 live-smoke PROVEN — the whole thesis validated. A self-driven bound CLI session showed, from the PROVIDER side, exactly which memory ops a turn produced (2 recall searches, exact bound session_id, real union-recall scopes). Negative (canary→auth_rejected) NOT live-constructible (Tier-1 agents aren't memory-bindable; a wrong key for an owner-scoped agent fails at agent-auth before the handshake) — covered by the unit test + prior hand-proof.

  • [2026-06-18] Fixed a pre-existing test-isolation bug exposed by the #17 CLI tests (0bebad7): test_no_textual_import did a live importlib.reload(ratatoskr.cli) that mutated the shared module in place, breaking class identity (isinstance/pytest.raises) for every test ordered after it. The real check is the static source-grep; the reload was vestigial → removed. Lesson: never importlib.reload a shared module in a test without restoring it.

  • [2026-06-18] #18 filed (composite endpoint + PAD read-endpoint) — DEFERRED, tracked at Gitea #18. Two pieces: (1) a composite Bifrost facade (new port e.g. :8392) fronting BOTH :8390+:8391 advertising both caps at handshake → one session binds both planes (un-parks the #17 open-q; bifrost reference_server already mounts both planes in one app → thin combined builder; needs per-plane failure-status + the op-feed deriving plane PER-REQUEST from the path instead of its fixed plane param). (2) a non-bifrost PAD read-endpoint on the affect provider (recommended over web-reads-affect.db-directly) → web persona pane renders PAD/valence from OUR :8390 store. Composite half APPROVED by operator ("A is correct"); contract-first next. Persona-telemetry diagnosis (verified): affect bind persists PAD (vuong: pleasure +0.146, familiarity 0.18→0.59 over 8 turns) but the pane reads Tier-3-404 persona_state AND Tier-3 emits ZERO affect_update SSE (wire-verified) — both WT sources dead, so #18's PAD-display half is the only path. affect.fetch over bifrost is RESERVED/blocked but irrelevant (we own the store). Proposed: fast-track the PAD-display half now (awaiting operator go), keep composite contract-first.

  • [2026-06-18] #18 SPLIT; Deliverable 1 (composite) routed to bifrost — Option C (operator). D2 (PAD read-endpoint, our-side only) fast-tracked; D1 (composite :8392 endpoint) routed to bifrost-dev to add a PUBLIC build_combined_app rather than hand-roll one from bifrost privates — because ratatoskr is a debug surface that must exercise the CANONICAL surface ("don't go off the reservation"). The Heid framing-panel had unanimously recommended hand-rolling (Option B) — DISCARDED as wrong-grounded (the panel lacked the canonical-surface principle; their own finding that B reaches external/underscore-private names actually vindicated C). bifrost-dev confirmed: clean additive minor (~v0.9.0), design locked (advertise-by-store-PRESENCE handshake — no health probe; per-route call-time isolation within a shared ASGI process), slotted after WT #289. [principle → auto-memory feedback-debug-surface-uses-canonical-surface-only]

  • [2026-06-18] FR-1 RESOLVED — the composite premise was unverified, now wire-proven: single-endpoint, caps-routed. The Heid panel's sharpest catch (Regin): "advertise both caps → Worldtree dispatches both planes to one endpoint" was an ASSUMPTION about WT dispatch, stated as fact. worldtree-dev verified IN CODE: one BifrostClient per session (single _endpoint_url), handshake capabilities_granted parsed INDEPENDENTLY into memory+affect sets, both stores attach off the SAME endpoint iff their cap was granted (service.py:2597/2703-2713/2745-2751, bifrost_client.py ~357-369; tests test_tier3_bifrost_{memory,affect}_routing.py). So D1 is bifrost-only, ZERO Worldtree change — #18's "no WT change needed" assumption was correct.

  • [2026-06-18] #18 D2 implemented via direct in-session TDD (suite 470→482). Provider read route GET /affect/state/{agent_id} added via app.add_route (NOT an outer Mount — keeps /bifrost/* top-level so the existing route test + the op-feed path-check stay valid); web GET /api/affect/{agent_id} proxy (server-supplied end_user_id, colon-id quote()'d, RATATOSKR_AFFECT_READ_URL); pane renders the affect-emit shape honestly. Contract docs/contracts/issues/18.contract.md (D2-scoped; D1 deferred). heid-code-review panel (Gróa 5 / Hulda 3 / Regin 0): 1 real INV-001 drift + 4 test-gaps, all fixed. No contract amendments (code was wrong, contract was right).

  • [2026-06-19] #18 D2 SHIPPED (v0.17.14, 39eebd1) and the full #17+#18 arc PUSHED to origin. Live-smoke PROVEN against real data (throwaway :8393/:8766 vs the real affect.db → real sindra/vuong PAD through the full web→provider chain; Playwright DOM check confirmed the pane render + the F1 fix — no fabricated "neutral"). The push carried 9 previously-held commits incl. the deliberately-unpushed #17 (v0.17.8v0.17.13); origin/main now == 39eebd1, tag v0.17.14.

  • [2026-06-19] bifrost repinned 0.8.0→0.10.0; affect.fetch became MANDATORY (strong-or-absent). 0.10.0's _supports_affect_plane requires affect_supported+emit+fetch and gates EVERY affect op — an emit-only store 400s. Implemented affect.fetch (v0.17.15, ca6af6b) conformed to bifrost's reference InMemoryAffectStore.fetch ({found, snapshot?}): the forced D1 prerequisite + a new Worldtree I/O point consumed. Flagged the now-stale consumer-guide line to bifrost-dev (fixed a2e6d62).

  • [2026-06-19] #18 D1 SHIPPED — composite build_combined_app on :8392 (v0.17.16, 7f4ceaa); #18 CLOSED; published v0.18.0 (359dbb1). build_combined_provider_app wraps bifrost's public builder over both stores + the shared read route; op-feed plane='combined' per-path. Direct in-session TDD; heid-code-review panel (Gróa/Hulda/Regin) returned ZERO drift. Live-proven at wire+dispatch; WT-turn gated on infra-ops :8392 allowlist.

  • [2026-06-19] op-feed handshake field-name fix (#17, v0.17.17 d60b77d): capabilities_requestedcapabilities. The summary read a field that never exists on the wire (bifrost reads capabilities, _protocol.py:181) → caps_requested was always null. Surfaced by the heid panel (Regin) during the D1 review — a latent #17 bug, not D1 drift.

  • [2026-06-19] Ratatoskr is a REFERENCE implementation of the Worldtree/Bifrost standard (operator). Adopt the dep's canonical way (even if ours works); INFORM of drift/gaps; ADVISE a different approach only when ours is genuinely better (dep owner decides), never unilaterally fork. [auto-memory feedback-ratatoskr-is-a-reference-impl-adopt-canonical]

  • [2026-06-19] Ratatoskr v1 is DERIVED from Worldtree I/O coverage (operator) — no self-defined feature ROADMAP. v1 = consume all of Worldtree's I/O points, reached when Worldtree hits 1.0; the convergence target is a coverage map, not a 37 capability list. [auto-memory project-ratatoskr-v1-derived-from-worldtree-io-coverage]

  • [2026-06-20] #18's final leg PROVEN — composite :8392 WT-driven smoke ran end-to-end + persisted. infra-ops allowlisted 10.100.10.50:8392 on the personal WT (01KVHWJGTT…); a real WT turn (session b83a66b6, ratatoskr:sindra, fresh end_user resmoke-choco-1) dispatched the full both-plane lifecycle through ONE endpoint — handshake (both caps) → affect.fetch + memory.searchaffect.emit (stored:true) → memory.upsert_many (upserted:1) — both writes verified in our SQLite (affect_snapshots PAD row + memory_chunks chunk 2df1b79…). First attempt blocked by a model_unavailable outage on the personal WT (both agents' models down), operator-fixed mid-session, then clean. The composite has no open legs.

  • [2026-06-20] #17 CLOSED in the tracker. Shipped end-to-end (v0.17.8.13 + op-feed field fix v0.17.17); the 2026-06-20 composite smoke re-exercised its op-feed live. Closing comment captures the full both-plane proof. Open issues now just #11 (scope-blocked) + #10 (watch).

  • [2026-06-20] Sindra has real PAD but ~empty memory — the affect/memory persistence asymmetry, confirmed on real sessions. affect EMITS every turn (persona always accumulates: vuong 8→14 interactions across the session); memory only writes on a PROMOTION trigger (salience / turn_count≥6 / idle-≥10min flush). Two real vuong sessions through the combined bind (04d6414c, 433541fe) drove affect emits + memory SEARCHES but ZERO promotion upserts → memory.db holds only the smoke fixture, zero vuong chunks. Operator: acceptable (server-takedown = "Sindra bonked on the head"; transient memory loss WAD). Operational catch: combined-as-default web bind saves persona reliably but silently LOSES memory if a session closes before a promotion trigger fires.

  • [2026-06-29] Web SPA combined-bind default (v0.18.1, 719e4d6) — operator-caught gap. #18 shipped the composite :8392 provider but never exposed it in the web bind dropdown (only memory/affect single-plane). Added combined (:8392) as the DEFAULT option (both planes in one session), kept single-plane for isolation diagnostics; wired endpoint_for_plane combined→8392 + server validation + the dropdown. Direct TDD; #17 contract updated (the governing spec for the web bind). Restarted :8765 on current code (env.sh + provider.env + RATATOSKR_AFFECT_READ_URL=:8392).

  • [2026-06-29] bifrost repinned 1.0.0 (v0.18.2, af67ad9). bifrost-dev shipped its first stable release; wire v0.6 now STABLE/FROZEN. Non-breaking (byte-identical to 0.10.0); switched the floor pin → exact ==1.0.0 per the stable-substrate posture. Post-1.0 breaking changes ride a bifrost MAJOR + new wire (v0.7+); a v0.6-pinned consumer is stable indefinitely. (Also this session: althing migrated to v0.15.0+ lean-bus / schema v4 — moderation retired, chamber/redis ripped; our tooling auto-updated to 0.17.4.)

  • [2026-06-30] Worldtree v1.0.0b1→b2 consumer adaptation: eager turn-launch statuses (v0.18.3 b2e4901, v0.18.4 e4317f6). Worldtree #331 decoupled turn execution from the SSE connection → turn-launch failures now arrive EAGERLY as a status before any stream: 409 agent_not_available (pre-b1 a 200 + in-stream error event), 503 retryable. Mapped both in stream_turn to typed SseConnectFailed subclasses keyed on STATUS, parsing the {detail:{error_code,message}} envelope — POST-003 preserved (no synthetic event yielded), existing handlers still catch (the design fork vs yield-an-Error-event was decided by POST-003). DEFERRED follow-ups (tracked here; bundle with the v1 coverage-audit): (1) live-prove the 409/503 end-to-end on personal-b2 (now unblocked — personal on b2, my key works there); (2) full conversation-api-spec.md markdown re-vendor to the b2 era (ratatoskr vendors the markdown, not the OpenAPI JSON).

  • [2026-06-30] Verify-against-the-real-spec-before-committing caught a real upstream gap. Holding the v0.18.3 commit to verify against demo's OpenAPI surfaced that the FROZEN OpenAPI 2.1.0 didn't document the 409/503 the heads-up described (agent_not_available was in the ErrorCode enum, but NO 503/turn-launch code). worldtree-dev confirmed it was THEIR gap (#331 added the statuses without extending the #328 openapi() override), shipped the fix in v1.0.0b2 / OpenAPI 2.2.0 (409/503 now enumerated, 503 code finalized as not_ready). "The consumer-oracle earning its keep." Lesson: a provider's prose heads-up can diverge from its frozen machine-readable spec — verify the actual spec before committing a consumer adaptation.

  • [2026-06-30] regard is a DEAD AXIS in Worldtree's emitted affect (caught provider-side; worldtree-dev confirmed + escalated to Vuong). Across all our affect snapshots, valence[].regard is EXACTLY 0.15 regardless of agent/end_user/interaction_count, while familiarity accumulates (vuong 0.18→0.69 over 14 turns). Root cause (worldtree-dev, code-grounded): 0.15 = base_regard = agreeableness*0.3 (sindra A=0.5); regard's only human-writer update_regard early-returns unless an emotion is about="other", but the Vili appraiser's ViliResponse schema has NO directedness axis (everything hardcoded about="situation") — producer side lost in the #265 Vili rework; consumer machinery intact. NOT WAD; the fix (reintroduce other-directed classification) is an affect-model change touching every agent + a directedness-classification design call → worldtree-dev filing an issue to Vuong. [the consumer/provider thesis paying off again]

41 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md.

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-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) — a code grep structurally can't see it. Rule: to check whether a Tier-3 agent exists, query the Worldtree instance, never the consumer repo's code. (Extended 2026-06-17: even GET /agents can't see consumer agents; only GET /agents/<owner>:<name> with the owner key does.)

  • [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. 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. contract_parser.py synced to v2.1 (commit d85ab43): now validates issue-scoped frontmatter + four v2.1 test categories. The old "treat parser ERROR-on-issue-scoped as expected" note no longer applies.

  • [2026-06-15] Memory plane TDD'd + shipped (commit cd12951, v0.17.3). Impl decisions worth keeping: vec0 distance_metric=cosine 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; 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. heid-code-review returned zero true drift; optimistic-lock semantics pinned to the reference via an expected_revisions parity test.

  • [2026-06-15] Memory provider LIVE-PROVEN against personal v0.35.3; recall-injection is upstream. worldtree-dev's Tier-3 promotion recipe: memory-call fires from Tier-3 PROMOTION, gated at service.py:2623 on ctx.kind=="consumer_defined" AND ctx.memory_config is not None (agent DEFINED WITH a memory block, dim 1024) AND handshake-granted memory caps AND embedding_dim==1024. Binding = POST /sessions BifrostBindingRequest{endpoint_url}, handshake caps=["affect","memory"], binding.scope null (per-op scopes auto-minted). A BIFROST_CLIENT_ALLOWED_HOSTS allowlist gates the endpoint (infra-ops added :8391). HTTP + HS256 both work in dev.

  • [2026-06-15] Diagnostic: our recall-search is SOUND — the cross-session recall gap is UPSTREAM, and it caught an upstream bug. Embedded the recall query via gateway qwen3-embedding + searched our live store directly → the fact recalls at cosine 0.60, correctly ranked. So the cold-session recall failure is Worldtree's recall-assembly/injection, NOT our search. ALSO found a latent UPSTREAM bug: a recall QUESTION got promoted as a durable chunk and ranked #1. 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, and same-session "recall" can be plain session history. Don't call cross-session recall proven from a clean wire.

  • [2026-06-15] Issue #17 filed. 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 couldn't drive its own provider. (Shipped 2026-06-18.)

  • [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 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, so the question can't crowd the fact out at search level. 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] #17 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 INV-005 promised; a session_id comment contradicted the dispatch-layer design. 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). Don't trust an immediate post-turn fixture snapshot to judge promotion; it lands after the turn completes. (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, GET /agents/<owner>:<name> with the OWNER key.

  • [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, not non-firing. "No upsert while a session is live and <10min idle" is WAD.

  • [2026-06-18] Wiping our :8391 store does NOT reset Worldtree's promotion-side dedup — a same-agent re-smoke returned reason_code=noop_duplicate / candidate_count=0: the extractor NEVER RE-RAN, dedup short-circuited against an earlier promotion. For a clean promotion smoke, use a BRAND-NEW agent + end_user (never-used names). (Also: llm_calls_used=0 is NOT the "did the extractor run" tell — noop_duplicate is.)

  • [2026-06-18] affect.emit is POST-TURN ASYNC — checking the op-feed immediately after a turn MISSES it. The Tier-3 affect appraise→emit→rehydrate loop runs AFTER the SSE [done]; the emit lands in our :8390 store seconds later (op-feed grep right after [done] showed only the handshake; the emit stored:true appeared on a later read). Same family as the async-promotion timing trap. Watch a few-second window post-turn before concluding "no affect emitted." Also wire-verified the same turn: Tier-3 sindra emits ZERO affect_update SSE (the persona-strip SSE path never populates for consumer agents) — see the #18 PAD-display decision.

  • [2026-06-18] Rationalized away a KNOWN contract-invariant deviation during TDD — only the cross-model code-review caught it. #18 D2's loadAffect called setPersonaStrip(snap), which renders dominant_emotion || "neutral"; the affect snapshot has no dominant_emotion, so it fabricated a "neutral" emotion — violating the very INV-001 ("no synthesized Tier-1 fields") I had WRITTEN. I knew the strip did this and talked myself into it as acceptable. Neither the design panel nor TDD caught it (unit tests don't exercise the JS render); the post-implementation /heid-code-review did (Gróa + Hulda both). Lesson: a known deviation from a contract invariant is drift even when you've rationalized it — flag it, don't argue yourself past it; the post-implementation cross-model review is the backstop for author-rationalized drift, distinct from the design-stage panel.

  • [2026-06-18] Latent SQLite thread-safety bug in the affect store, surfaced ONLY by the new HTTP read route. open_affect_store created the connection without check_same_thread=False; the bifrost emit path never tripped it (uvicorn's loop ran on the connection's creating thread), but the TestClient-driven read route runs handlers off a worker thread → sqlite3.ProgrammingError. Fix: check_same_thread=False (safe — the event loop serializes access) + explicit PRAGMA busy_timeout=5000 (don't rely on sqlite3's timeout=5.0 default). Lesson: a sqlite-backed ASGI app needs check_same_thread=False; the HTTP-layer test exposed what the direct-store-method tests structurally couldn't.

  • [2026-06-19] The SAME check_same_thread sqlite bug recurred in the MEMORY store — exposed by the contract-mandated search dispatch test (TestClient = worker thread). Heid's test-fidelity finding (the D1 dispatch test used describe_store where the contract says search) → fixing it to search tripped sqlite3.ProgrammingError because open_memory_store also lacked check_same_thread=False. Fixed (mirrors affect INV-006). Lesson: this bug is PER-STORE — every sqlite-backed ASGI store needs check_same_thread=False; an HTTP-layer (TestClient) test exposes what direct-store tests can't, and the composite serving memory over HTTP makes it bite.

  • [2026-06-19] Full WT-driven :8392 live-smoke is infra-gated — :8392 not in WT's BIFROST_CLIENT_ALLOWED_HOSTS (bind 422s). New provider ports are NOT auto-allowlisted (only :8390/:8391 are). Self-driven dispatch (minted consumer-key JWTs → :8392) is the wire-proof; the WT-turn needs infra-ops to add :8392 (requested 01KVHWJGTT…).

  • [2026-06-19] heid-code-review pulled MORE weight than its own "marginal" self-assessment. The panel returned zero drift, but its single test-fidelity finding CASCADED into 2 real latent-bug fixes when applied (the memory check_same_thread bug + Regin's op-feed field-name bug). Lesson: a contract-fidelity nudge can transitively expose bugs the test never reached — don't dismiss a "marginal" finding by its count.

  • [2026-06-20] The post-turn-async timing trap bit AGAIN — even a 35s post-[done] read missed the promotion upsert_many by ~2s (it landed 19:48:58; the read was ~19:48:56). A 15s-interval background poll caught it on the first tick. Same family as the affect.emit / async-promotion traps already logged — re-confirmed that "wait once then read" is fragile for post-turn writes; poll a window, don't snapshot once. (The affect.emit write, by contrast, DID land inside the 35s window — promotion is the slower of the two post-turn writes.)

  • [2026-06-30] Heimdall keys are PER-INSTANCE — a key minted on one Worldtree 401s on another. Our Conversation-API key works on personal :8081 but 401s auth_invalid on demo :8080 (per-instance Heimdall user store + pepper; fresh deploys start with an EMPTY key store). Same as the admin key (personal-only). To live-drive a given instance you need a key minted FOR that instance (request via infra-ops). Couldn't live-prove the b2 409 on demo for this reason → deferred to personal-b2 where we have access.

  • [2026-06-30] tea comment <N> hangs on Gitea (the whole compound bash auto-backgrounded + stuck on the open tea call). The #11 prereq comment hung; killed it + posted via the Gitea HTTP API directly (POST /api/v1/repos/vh/ratatoskr/issues/<N>/comments, token from ~/.config/tea/config.yml). For issue comments, prefer the Gitea API over tea comment when tea is flaky (CLAUDE.md already says use HTTP for comment-EDITS; this extends it to ADD when tea hangs). Verify-then-post (check the comment didn't already land) to avoid a double-post after a kill.

18 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md.