Files
ratatoskr/archival-memory.md
T

50 KiB
Raw Permalink Blame History

Archival memory — ratatoskr

Entries moved out of persistent-memory.md to keep the active file scannable. Read this when researching historical decisions or revisiting past foot-guns. Append-only.

Recent decisions (archived)

The 2026-05-20 → 2026-05-29 cluster: the original debug-TUI/web build era, before the 2026-06-14 Bifrost-provider second identity. Archived 2026-06-18 (one event; per-entry stamps omitted for the batch).

  • [2026-05-20] Project name Ratatoskr (squirrel on Yggdrasil — runs up and down carrying messages). Earlier candidate Andvari demoted on the cursed-ring association.

  • [2026-05-20] Separate repo, separate dev team. Operator's call; the in-tree-at-Worldtree/tools/ alternative was considered and rejected to dogfood the API boundary.

  • [2026-05-20] No Worldtree-source imports. Spec-only dependency. Triple version-skew mitigation: spec-pin in pyproject.toml + recorded-SSE snapshot tests + conformance smoke. Initial pin: 55101e909abcd2219833266b6f905c5bc956e0f0 (Worldtree v0.19.0). See docs/SPEC-PIN.md.

  • [2026-05-20] Textual (not rich+prompt_toolkit). Driver: debug observability is the primary purpose, and a multi-pane dashboard with persistent side panes + independent scrollback is structurally application-shell-shaped. Volva consulted via cross-frontier second-opinion and converged on the same call.

  • [2026-05-20] httpx-sse for SSE consumption. The server emits composite {turn_id}:{seq} id: lines (Worldtree INV-014) load-bearing for SSE-resume; hand-rolled data:-only parsing (the skaldsong pattern) silently drops these. Ratatoskr becomes the reference Python SSE-resume implementation.

  • [2026-05-20] Persona-pane PII posture: label-don't-refuse. persona.log is process-wide; pane title flips between [Persona — PROCESS-WIDE] and [Persona — session <id>…] based on whether log lines carry session_id. Refuse-against-non-local was considered and rejected as paternalistic.

  • [2026-05-20] Server-stdout pane: opt-in via --server-log <path>. No auto-detection of well-known paths.

  • [2026-05-20] Two-stage Ctrl-C. First cancels in-flight turn server-side; second exits app. Ctrl-D bound to immediate exit.

  • [2026-05-20] Single-session-per-launch + startup picker. No in-app /switch. CLI flags --session <id> and --new for scripted use. Session identity always visible in Textual footer.

  • [2026-05-20] Markdown rendering default-on; --raw opt-out. Don't pre-design --no-stream-formatting (Volva: add only if streaming-markdown rendering is empirically ugly).

  • [2026-05-20] Non-interactive --send mode. Single SSE consumer module, two presenters (TUI + stdout). Keeps Ratatoskr honest as an API consumer; useful for CI / scripted probes.

  • [2026-05-20] First contract: ratatoskr.sse_client. Bundles stream_turn + reconnect_turn + cancel_turn + private _parse_sse_id into one module — the SSE-resume flow is coupled (cancel needs turn_id from the SSE wire id:, reconnect re-uses the same parsed SseId), so they share a contract. Hard invariant INV-002 makes the composite {turn_id}:{seq} id: parsing load-bearing — closes the foot-gun the design-brief §3 names (hand-rolled data:-only parsing silently drops the id:).

  • [2026-05-21] Contract converted to issue-scoped (issue #1). Frontmatter shape switched from module-scoped (module:/purpose:) to issue-scoped (target_module:/scope:/prd:) per CONTRACT-FORMAT §2.1.I. prd: block pins to issue body hash. Known parser stale-ness: contract_parser.py --validate ERRORs on issue-scoped frontmatter — CONTRACT-FORMAT §2.1.L H10, a documented Brokkr-side follow-up. Parser is a canonical sync, so we do NOT patch it locally. Treat parser ERROR-on-issue-scoped as expected until canonical bumps. (later retired — see 2026-06-15 canonical-sync entry.)

  • [2026-05-21] Default issue-tracker labels seeded (17 total). Sleipnir gating, triage, type, resolution, Ratatoskr-specific area labels (sse-client, tui, cli, observability).

  • [2026-05-21] Volva paraphrase + code-review across all 4 issues — calibration consistent. Paraphrase rounds flag 3-5 contract ambiguities per issue; code-review rounds flag 3-8 code-vs-contract drifts after TDD-passing implementation. The post-TDD code-review consistently catches three classes of gap the test-author's hypotheses don't cover: PRE-assertion boundary drift, exception-payload truncation / never-rendered-to-user observability misses, and "tested the state but not whether the user can see it" gaps.

  • [2026-05-21] Manual smoke is load-bearing — found a real defect tests couldn't. First wire-level smoke against personal Worldtree (post-TDD, post-Volva-code-review on #4) revealed httpx's default 5s read timeout killed the SSE connection mid-stream during mimir's thinking phase (~30s LLM latency >> 5s read timeout). The unit/contract test infrastructure (respx-mocked SSE wire) doesn't model real LLM latency, so the gap was invisible at the test layer. Fix: caller-owned httpx.AsyncClient constructed with timeout=httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0); defense in depth: sse_client.stream_turn ERROR_ROUTING catches httpx.ReadTimeoutSseConnectionDropped. Lesson: keep manual-smoke step in the per-issue cadence; mock-only validation is insufficient for streaming-against-real-server code.

  • [2026-05-22] Issues #5/#6/#7 filed: per-user-agent support + TUI-startup-visibility + mid-stream-robustness. Discovered during 2026-05-22 mimir TUI conversation: long completion crashed with JSONDecodeError("Expecting value: line 1 column 1 (char 0)") from json.loads('') on an empty-data: SSE frame (→ #7). Earlier same day, ratatoskr --new --agent lofn failed with 422 end_user_id_required → #5. #6 was a corollary observation (TUI alt-screen masks the diagnostic).

  • [2026-05-22] Issue #8 (startup agent picker) filed. GET /agents exists in the vendored spec; returns agent_id/name/description + optional fields. --agent becomes conditionally optional. Composes naturally with issue #5.

  • [2026-05-22] Issue #7 implemented via TDD + Volva-code-reviewed. First issue with zero drift findings from Volva code-review — TDD caught all runtime behavior. Hypothesis: the tighter the contract + smaller the code surface, the more Volva's role shifts from "catch behavioral drift" to "tighten observability + wording".

  • [2026-05-23] Issue #6 (TUI startup error visibility) implemented via TDD + Volva-code-review (two rounds). Restructures run_tui lifecycle: _resolve_then_run async helper opens AsyncClient, does pre-flight resolution, routes errors to stderr BEFORE alt-screen opens. Two Volva rounds confirmed multi-round value (round 2 found things round 1's amendments didn't anticipate; strictly test-precision, no behavioral drift).

  • [2026-05-23] Issue #5 (--end-user-id) implemented via TDD. Three modules touched. create_session(client, agent_id, *, end_user_id=None); CLI flag with non-empty validation; threading through _amain and _resolve_then_run.

  • [2026-05-23] Worldtree-dev consult landed authoritative consumer-API guidance (althing thread 01KSBARG2B8M8C82H6AJGJWX1B). Takeaways: end_user_id is a free-form partition key; no programmatic requires_end_user_id discovery; subject:{type,id} migration locked but not shipped; spec pin (v0.19.0) is 3 minor versions stale; send a User-Agent header; agents.call:lofn scope needed for lofn smoke; GET /agents requires no special scope.

  • [2026-05-23] v0.2.1 layout fix: dock-anchored TUI chrome so Input never moves. Cause: auto-stacked vertical flow shifted Input when thinking-current toggled visibility. Fix: dock chrome to screen edges; transcript absorbs reflows internally via scroll viewport. Operator-confirmed "a lot better" interactively. Pure UI fix; tests pass without modification. TUI-layout patches are "ship + operator verifies" — TTY is the load-bearing test surface; respx + Pilot mocks can't catch screen-relative positioning bugs.

  • [2026-05-23] Issue #12 (presenter contract semantics amendment) implemented via TDD. Thinking deltas render as ONE coalesced growing line (CLI) / one closed RichLog entry per run + live Static widget per-delta (TUI), not 50 lines per turn. Introduced stateful per-turn presenters: CliPresenterState + TuiPresenterState. Editorial promotion: load-bearing = Text/Done/Error/Cancelled (no prefix); demoted telemetry = WorkerPhase/Thinking/TextBoundary/ToolStart/ToolResult.

  • [2026-05-23] Forward direction: Ratatoskr will require end_user_id for EVERY access before too long. Operator's call. Reasoning: even Tier 1 foundational agents that don't require end_user_id server-side currently fall back to a _no_end_user sentinel partition — effectively pollution. Cross-frontier alignment (worldtree-dev ack, althing 01KSBD9FPMCWJMBXNNS4B3MYBS): the platform side agrees the fallback is a substrate accommodation, NOT a consumer model. Ratatoskr's forward posture pre-empts a future tightening. File a ratatoskr issue when scheduling the change (untracked by operator choice for now).

  • [2026-05-24] v0.9.0 live Markdown rendering in TUI transcript. Replaces v0.8.2's drop-Markdown patch. Transcript switched from RichLog to VerticalScroll; each turn's response lives as a single Static widget whose Markdown content is updated as Text deltas arrive (no post-Done re-render, no double-print). --raw bypasses Markdown.

  • [2026-05-24] v0.10.0 debug-pane audit logging surface. Every SSE event arrival lands as one debug-pane line (timestamp + sse_id + event-specific summary). Token-rate Text/Thinking deltas are aggregated into per-turn counters surfaced in a turn-summary line. Also: state-machine transitions, cancel POST lifecycle, app bootstrap, ctrl-c actions, wire-error exception class+body all logged.

  • [2026-05-25] Worldtree #204 / v0.28.0 integration (v0.11.0 → v0.13.0). Three-bump arc for affect_update SSE event + GET /agents/{id}/persona_state endpoint. v0.11.0 wire layer (AffectUpdate dataclass + parse + Event-union member); v0.12.0 read-side client (get_persona_state + typed errors PersonaNotConfigured/AgentNotAvailable/AuthScopeDenied); v0.13.0 TUI surface (sticky #persona-header line + Ctrl+4 Persona TabPane; live updates on AffectUpdate(status="current"); on-mount hydration via the GET endpoint).

  • [2026-05-26] Worldtree #201 / v0.29.0 integration (v0.14.0). New SSE event awaiting_llm_first_token heartbeat (default 5s interval) during the BuildingPrompt→CallingLLM gap. Top-level event, NOT a worker_phase extension (preserves INV-053 three-field stability). AwaitingLlmFirstToken dataclass + parse; TUI live transcript indicator ("awaiting first token · Ns") mounted on first heartbeat, updated in place, removed when the gap closes; turn-summary line gains heartbeats=N.

  • [2026-05-26] v0.14.1: CLI presenter forgot to update when wire-layer events were added. AffectUpdate (v0.11.0) and AwaitingLlmFirstToken (v0.14.0) were added to the sse_client Event union and the TUI presenter, but cli.py's CliPresenterState.render has its own isinstance check that wasn't widened. ratatoskr --send crashed AssertionError on any v0.28.0+/v0.29.0+ server. Patch shipped + a posture lesson: always update BOTH presenters in lockstep when adding a wire-layer event (the two presenters currently duplicate the isinstance tuple; refactor to a shared constant if a third wire-event lands).

  • [2026-05-26] v0.14.2: RichLog min_width=78 silently overrides wrap=True. Right-column panes (1fr against left's 2fr) are narrower than 78 cells at typical terminal widths; the renderer forces content to 78 wide then horizontal-scrolls. Fix: min_width=0 on all four right-column RichLog instances.

  • [2026-05-27] Issue #16 web companion shipped — v0.15.0. Browser-based debug surface sibling to the TUI, reusing all wire-layer modules unchanged. New ratatoskr.web (Starlette app + lazy-import entrypoint + single-page vanilla HTML/CSS/JS UI), new console script ratatoskr-web, optional-deps group [web]. Nine HTTP endpoints; five-pane parity over the same SSE wire. Browser-native EventSource (GET stream + separate POST submit) — load-bearing Hulda correction from Heid panel; EventSource is GET-only. In-memory turn registry; browser-disconnect → upstream cancel; lifespan-shutdown drain with 5s budget. HTML-escaped transcript; upstream API key stays server-side. Default bind 0.0.0.0:8765 (LAN-trust model — operator direction; no auth, no TLS, no CORS).

  • [2026-05-27] Heid panel review on web-companion scope v1 (pre-implementation). Caught the EventSource POST/GET error + 7 other load-bearing items BEFORE we cut code. Confirms a pattern: for non-trivial scope with non-obvious wire-protocol details, run a Heid panel BEFORE implementation, not just after. Cost ~5min latency; saved a mid-implementation rewrite.

  • [2026-05-27] Mid-session system_prompt mutation: REJECTED across the industry. Operator-requested feature → Heid R13 panel (brokkr-claude + Eitri-Codex + Dvalin-Grok, strong convergence) ran a SOTA survey: NO surveyed mature system ships live PATCH-on-active-session for the system prompt (OpenAI Assistants/Responses, Anthropic Messages, Vertex AI, MCP, LangChain, LlamaIndex, Ollama, vLLM). The omission IS the answer; 12 additional threat vectors beyond ratatoskr's initial 7 surfaced (TOCTOU broader than BuildingPrompt window; KV/prefix cache contamination; supply-chain; Memory Control Flow Attacks >90% ASR on tested LangChain/LangGraph). Recommended alternative: client-side fork pattern (PATCH agent → mint new session → replay context). Operator declined for ratatoskr — debug TUI is wrong consumer; fork ergonomic belongs in a future production conversational shell. Thread closed cleanly (althing thread 01KSKD1GA3XBWR9RHGZCF9FE3Y).

  • [2026-05-27] Artemis (Gemma4) reasoning-token gap was upstream, not ours. Wire trace from ratatoskr showed zero thinking events for artemis-31b-v1i; infra-ops confirmed llama-swap emits 77 reasoning_content deltas at the OpenAI-compat layer (--reasoning-format deepseek). Gap was in Worldtree's GemmaProvider. Worldtree-dev shipped v0.29.13 (commit 4262430) fixing two stacked bugs: (1) base OpenAICompatProvider._extract_thinking_from_delta returned None unconditionally so any model falling through to the generic class dropped reasoning; (2) catalog family lookup was dead code (read wrong YAML subsection). Confirmed in ratatoskr via re-smoke against Sindra. Diagnostic pattern: when a wire-layer feature appears missing, get infra-ops to probe upstream-of-the-SSE-publisher first; ratatoskr's wire trace says what reaches us, infra-ops's probe says what reaches Worldtree.

  • [2026-05-27] v0.15.1 (sessions): get_persona_state unwraps FastAPI detail-envelope. Live smoke surfaced that real Worldtree returns persona-state errors as {"detail": {"error_code": "..."}} (FastAPI default), not flat. v0.12.0 tests mocked flat shape so the bug was invisible. Lesson: test-side mock envelopes must match the REAL wire shape; live smoke is load-bearing for envelope-shape verification, not just happy paths.

  • [2026-05-28] v0.16.0 web Heid code-review pass 1: load-bearing turn_id fix. Cancel paths used browser-local _TURN_COUNTER ids (1, 2, 3…) instead of upstream Worldtree turn_id (e.g. 799) captured from the first SSE event. The disconnect_triggers_cancel test gap was the load-bearing miss. Also: server-configured RATATOSKR_END_USER_ID (browser can no longer impersonate partition); narrowed missing-extras ImportError catch (real first-party bugs propagate as tracebacks instead of masking as exit-12); per-turn lifespan-shutdown logging. Contract amended with a v0.16.0 block + INV-005/006 updated + 4 FN sketches corrected.

  • [2026-05-28] v0.16.1 web Heid code-review pass 2: minor tightening. Stream-layer vocab coverage extended to all 11 Event types (AffectUpdate added to the vocab stream; dedicated error_terminal_event + cancelled_terminal_event tests since terminal events are mutually exclusive with done). Disconnect-cancel catch narrowed to swallow only CancelAlreadyCompleted/CancelTurnNotFound (the cooperative race); log unexpected CancelFailed/transport errors as structured stderr. Heid review loop converged: pass 1 = 7 findings (1 load-bearing); pass 2 = 2 minor (Gróa: zero findings, Hulda: 2). Pattern confirmed: diminishing returns within 2-3 passes; pass 3 would have been empty.

  • [2026-05-28] Sindra Tier 3 agent: FORM ASSUMPTION gate + new physical-form description. Persistent agent state changes via python -m ratatoskr.tier3 patch: (1) model migrated from qwen3.6-35-a3b-heretic to artemis-31b-v1i; (2) added FORM ASSUMPTION section — when instructed to become another character she IS that character (identity/environment/psychology/parameters), believes the environment as fact, no Sindra/holo-deck/parameter references, sticky until explicit revert; (3) replaced the abstract "classically beautiful" default-form sketch with a specific anti-artifice physical description (5'8", golden-copper skin, asymmetric features, oversize dark-green knit, bare feet). System prompt file is at /tmp/personal-worldtree-sindra_system_prompt.md (transient; not committed to repo). (Superseded 2026-06-17: sindra DELETE+redefined to thoughtful-character role on v0.35.16.)

  • [2026-05-29] v0.17.0 frontend redesign — aurora telemetry instrument. /frontend-design pass on the web companion: all-monospace technical-instrument aesthetic with the Australis dark palette + aurora-borealis accent band. Top command bar with live connection dot (idle/streaming/error states), inline persona summary with P/A/D micro-bars, animated awaiting-token, terminal-event status chips. Live Markdown rendering in transcript + thinking panes via a hand-rolled markdownSafe() (escape-first, whitelist subset of headings/bold/italic/inline-code/fenced/lists/quote/links; link-scheme whitelist; XSS-verified under a node harness). Thinking pane now has per-turn labeled dividers + a fresh MD-rendered block per turn. Tools / Debug / Persona panes stay literal monospace by deliberate choice — they carry structured audit lines + JSON, where MD would corrupt readability (underscores in tool names, JSON braces). Single-file vanilla HTML/CSS/JS, no build, no CDN, no node_modules.

  • [2026-05-29] Codex-first discipline pilot — Ratatoskr selected. brokkr-smithy-dev pushed AGENTS.md (commit bbeaa23) and declared the ratatoskr-codex handle per brokkr-smithy/docs/codex-first-discipline.md v0.1 (brokkr-smithy commit 5dd061c, tag v0.5.3). Per-dispatch opt-in model: default Sleipnir Claude-implementer path remains available; Codex used only when operator routes via /codex-dispatch <N>. Bootstrap handshake when operator spins up a codex session: codex sends codex-online → ratatoskr-dev replies with active branches + WIP state. Galdrabok was rejected as pilot (Codex authoring Claude skills is a category error); Skaldsong was the other candidate. (Still dormant as of 2026-06-18 — no codex session spun up.)

  • [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) Archived 2026-07-17.

  • [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. Archived 2026-07-17.

  • [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). Archived 2026-07-17.

  • [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). Archived 2026-07-17.

  • [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. Archived 2026-07-17.

The 2026-06-14 → 2026-06-18 cluster: the Bifrost-provider second-identity build era (#17/#18 self-drive+observe, #295/#296 cold-recall diagnosis, agent_self lattice). Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

[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]

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-19.

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

Archived 2026-07-19.

Tried and abandoned (archived)

The 2026-05-20 → 2026-05-28 cluster: original-build-era foot-guns. Archived 2026-06-18.

  • [2026-05-20] rich + prompt_toolkit framework choice. Volva flagged that §1 and §5 pulled in opposite directions: a real side-panel observability surface would silently become a widget framework reimplementation. Operator's debug-observability reframe sealed the flip to Textual. Don't re-attempt rich+pt unless the scope shrinks to transcript-first REPL.
  • [2026-05-20] In-tree at Worldtree/tools/ratatoskr/. Earlier draft committed to in-tree-with-import-direction-smoke-test. Rejected at operator-routing — separate dev team forces separate repo.
  • [2026-05-20] New /persona/log SSE endpoint on Worldtree. Considered as alternative to file-tailing persona.log. Rejected — contract amendment + Vor round + AFK dispatch loop is weeks for a debug feature file-tail handles in a day. Trigger follow-up if a Worldtree-on-server / TUI-on-laptop debug case appears.
  • [2026-05-20] Cross-process Last-Event-ID resume. Considered — would require persisting per-session Last-Event-ID. Deferred to v2; v1 ships "reconnect, not resume-across-process."
  • [2026-05-21] RichLog widget with markup=True. Default impulse, but Rich interprets [xxx] spans as style markup and silently strips them. Every labeled stderr-style line — [cancel_failed], [done], [error], [busy], [worker_phase] — would render as just the content after the bracketed label. Fix: markup=False. Don't flip back without renaming every labeled-line format away from [bracket] notation.
  • [2026-05-21] Querying self.query_one("#transcript", RichLog) from inside a Textual run_worker coroutine. Initially failed with NoMatches. Reactive fix was widening worker signature to take log as parameter — Volva flagged as contract drift; reverted. Real fix was test-side: await pilot.pause() between inp.action_submit() and the polling loop so the handler finishes dispatching. Don't widen worker signatures to dodge test timing.
  • [2026-05-21] TUI session-identity rendering via self.sub_title + self.hint plain attributes. Stored state but never rendered to a visible widget. Tests asserted attributes (passed); Volva code-review flagged the gap. Fix: dedicated Static(id="identity") + Static(id="hint") widgets in compose; _set_hint() helper mirrors state → widget. Calibration evidence for the "TDD catches state, code-review catches whether the user can see it" pattern.
  • [2026-05-23] Using the cross-model review agent's name directly in composed prose. The peer review agent's name (the althing handle starting with "V-o-l-v-a") is one letter from a body-part term. Anthropic's content classifier does fuzzy matching and intermittently blocks responses mid-stream when the name appears in composed prose sentences. Mitigation: use role descriptions ("the cross-model reviewer," "the paraphrase peer") in prose rather than the name; quote content via tool output.
  • [2026-05-22] json.loads(sse.data) unguarded against empty data. _iter_events unconditionally called json.loads on every dispatched ServerSentEvent. When httpx_sse surfaced a frame with id: present but data: empty, json.loads('') raised JSONDecodeError → app crash. Fix: if sse.data == '': continue BEFORE _parse_sse_id. Don't reintroduce unconditional json.loads(sse.data).
  • [2026-05-23] Diagnostic shorthand: "2-events-then-silence" = Worldtree-side LLM-call wedge, not ratatoskr. If a mimir --send smoke shows exactly two stderr events — . create_session: ... followed by . worker_phase: phase=BuildingPrompt ... — and then nothing for >60s, the root cause is upstream of ratatoskr. Worldtree's service.py:2560 gates the CallingLLM event on the engine yielding its first LLM-provider chunk; if that connection is wedged at TCP level, the async for never iterates. Worldtree's 300s _start_stall_timer cancel-check is INSIDE the engine-event loop and so bypassed. Don't bisect ratatoskr code when this shape appears — diagnose the LLM-provider state at Worldtree's host. Restarting the Worldtree service clears wedged llama-swap connections. 10.250.50.152 hosts 3 instances (:8080/:8081/:8082) each with own DB + key namespace; our key is valid only on :8081.
  • [2026-05-23] Phantom "per-Tier-1-agent scope add" pattern. Issue #5's lofn 422 was initially mis-diagnosed as needing agents.call:lofn added. Routed to infra-ops via althing per credential-brokerage rule; infra-ops discovered no public scope-mutation endpoint, brokered to worldtree-dev. Worldtree-dev clarified: Tier 1 foundational agents are covered by a blanket agent.call:* (singular) baseline. There is no per-agent grant for Tier 1. Tier 3 consumer-defined agents use the plural agents.call:<owner>:<agent> shape registered via POST /agents/define. The notations differ by one letter. The actual lofn fix was issue #5's --end-user-id flag — always a request-body validation, not an auth-scope gate. Don't ping infra-ops for "per-Tier-1-agent scope adds."
  • [2026-05-24] v0.8.x double-print: streamed Text + post-Done Markdown re-render. Initial v0.6.0 design wrote each Text delta inline (with · prefix) then re-rendered the full response as a Markdown Renderable on Done. Visually the response appeared twice. v0.8.2 dropped the post-Done Markdown body (interim regression). v0.9.0 fixed it properly with live Markdown rendering during stream (single Static widget holding a Markdown Renderable, updated in place). Don't reintroduce post-Done re-render unless you also remove the live-Markdown widget.
  • [2026-05-26] Textual RichLog(wrap=True) insufficient on narrow widgets. The default min_width=78 overrides wrap on shrink — max(renderable_width, min_width) forces 78-cell rendering then horizontal-scrolls. Always set min_width=0 on RichLog instances in a narrow column. Re-check on any future RichLog construction.
  • [2026-05-26] Wire-layer event added without updating BOTH presenters. v0.11.0 (AffectUpdate) and v0.14.0 (AwaitingLlmFirstToken) widened the sse_client Event union + TUI presenter's isinstance tuple, but missed cli.py's identical-shape tuple. --send mode then crashed on any persona-enabled or slow-first-token turn. Patch fix in v0.14.1. Rule: when adding a wire-layer event, grep for isinstance(event, ( across the repo — currently TUI and CLI presenters both carry duplicate hardcoded tuples. Refactor to a shared _EVENT_VOCAB constant if a third wire-event lands.
  • [2026-05-27] EventSource is GET-only — scope v1's POST stream endpoint would have broken. Web companion's first scope had POST /api/turns/{sid}/stream for the SSE proxy. Browser-native EventSource only supports GET. Hulda caught it in Heid panel review BEFORE we cut code. Pattern: POST /api/turns/{sid} registers the turn locally + returns turn_id; GET /api/turns/{sid}/stream?turn_id=N streams via EventSource; cancel is a separate POST. Load-bearing reason to Heid-panel non-trivial wire-protocol designs BEFORE implementation, not just after.
  • [2026-05-27] get_persona_state mocked flat error envelope; real Worldtree wraps in detail. v0.12.0 tests used {"error_code": "auth_scope_denied"} but real wire (FastAPI default) returns {"detail": {"error_code": "auth_scope_denied", "message": "…"}}. The parser only checked top-level so the typed exception was never raised; calls fell through to SessionApiFailed(403), which the web persona endpoint surfaced as HTTP 500. v0.15.1 patches both shapes. Lesson: test-side mock envelopes must match the REAL wire shape; live smoke is load-bearing for envelope-shape verification, not just happy paths.
  • [2026-05-27] Mid-session system_prompt mutation: universal omission across surveyed mature systems. brokkr-smithy R13 panel (3-arm, strong convergence) confirmed: no surveyed system ships live PATCH-on-active-session (OpenAI Assistants/Responses, Anthropic Messages, Vertex AI, MCP, LangChain, LlamaIndex, Ollama, vLLM). The omission IS the answer. 12 additional threat vectors beyond ratatoskr's initial 7. Don't re-propose this for ratatoskr; if a future production conversational shell wants iterative-prompt-tuning ergonomics, the consensus shape is fork-via-client (PATCH agent → new session → replay context).
  • [2026-05-28] Browser-local turn_id used for upstream cancel URL — old cancel tests ENCODED the bug. Web companion v0.15.x cancel paths posted to /sessions/{sid}/turns/{LOCAL_ID}/cancel. Tests mocked the local-id URL so they encoded the bug rather than detecting it. Hulda caught it in Heid pass 1. Fix in v0.16.0: capture upstream_turn_id from the first SSE event's sse_id.turn_id; all cancel paths use it; cancel before first event is {"cancelled": false, "reason": "not_started"}. Rule: when designing cancel/match paths against an external service, test fixtures must mock what would actually be hit upstream — mocking your own derived id encodes the bug instead of catching it.

The 2026-06-14 → 2026-06-18 cluster (foot-guns from the same era). Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.

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

Archived 2026-07-18.