Files
ratatoskr/persistent-memory.md
T

29 KiB
Raw Blame History

Persistent memory — ratatoskr

Last updated: 2026-06-18

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-19:

#18 DELIVERABLE 2 SHIPPED + PUSHED — the persona-telemetry gap is CLOSED. The web pane now renders live PAD/valence for Tier-3 agents from OUR :8390 affect store (v0.17.14, 39eebd1, suite 482 green, pushed to origin). Three pieces: provider read route GET /affect/state/{agent_id} (non-bifrost, added to the affect app via app.add_route — keeps /bifrost/* top-level + op-feed-skipped); web proxy GET /api/affect/{agent_id} (server-supplied end_user_id, colon-id quote()'d, RATATOSKR_AFFECT_READ_URL config, default 127.0.0.1:8390); pane affect-render (renderAffectPane/loadAffect, honest pad+valence+emitted_at, labelled "affect", NO fabricated Tier-1 fields, explicit empty-state, 2s post-turn poll). Live-smoke + a Playwright DOM check PROVEN against real sindra/vuong PAD. The push also published the previously-held #17 arc (v0.17.8v0.17.13) — origin/main is now fully caught up.

#18 DELIVERABLE 1 (composite :8392 endpoint) — PARKED on bifrost (tracked Gitea #18). Routed to bifrost-dev for a public build_combined_app rather than hand-rolled from bifrost privates (debug-surface-uses-canonical principle). bifrost-dev confirmed it: clean additive minor (~v0.9.0), design locked (advertise-by-presence handshake, per-route call-time isolation), slotted AFTER WT #289. FR-1 RESOLVED — composite is bifrost-only, ZERO Worldtree change (single-endpoint caps-routed, worldtree-dev code-verified). NEXT: when bifrost ships build_combined_app, repin + reimplement D1 against it (per-plane failure status + op-feed plane-per-request derivation already specced in the issue). Nothing blocks on our side.

OPERATOR SESSION STATE — running shells are PRE-#18 code (foot-gun). web :8765 + affect :8390 + memory :8391 are the prior session's background shells running OLD code (no read route; web has no RATATOSKR_AFFECT_READ_URL). To see D2 live in the operator's own session, RESTART :8390 (affect provider, new code → gains the read route) + :8765 (web, new code + RATATOSKR_AFFECT_READ_URL=http://127.0.0.1:8390 + RATATOSKR_END_USER_ID). This session's live-smoke used THROWAWAY :8393/:8766 instances vs the same affect.db to avoid disrupting them. Consumer/owner key = wt_live_d81b…; providers SQLite + sqlite-vec, memory.db/affect.db at repo root (affect.db has live sindra PAD: vuong pleasure 0.146, familiarity 0.589, interaction_count 8).

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.8.0 / wire v0.6 (scope_all+scope_any); WT handshake now advertises bifrost_version 0.6.0 (worldtree-dev honesty-fix FYI 858ba58 — we don't pin/assert it, no-op our side). 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: #10 (subject migration), #11 (AdminEvents pane) — deferred; #18 (D2 PAD-read SHIPPED v0.17.14; D1 composite PARKED on bifrost build_combined_app). Codex-first pilot dormant.

Branch: main (== origin/main @ 39eebd1). 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.

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.

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