memory: snapshot — dots-tts + config-voices/two-voice split shipped; WT #401 personal-WT deadlock routed

Captures this session's durable state: the RP-voice rebuild (chatterbox->dots
v0.22.2, config-driven voices + two-voice dialogue/narration split v0.22.3) and
the personal WT :8081 deadlock recovery + root-cause routing to worldtree-dev
(WT #401). New substrate foot-guns: dots :8198 is a fleet switchboard, dots'
language validator is an alias-mapper, per-origin TTS toggle, WT hang signature,
dots-streaming-pcm-not-headerless. Three detail files added under
persistent-memory.d/.
This commit is contained in:
2026-08-13 22:59:28 -07:00
parent 5c3d0ad010
commit 6e2822cfcb
4 changed files with 96 additions and 8 deletions
@@ -0,0 +1,21 @@
`[2026-08-11]` **TTS migrated chatterbox-fast → dots-tts (v0.22.2, `38b78d8`).**
Operator-directed after an infra-ops A/B win ("very good"). dots-tts = rednote-hilab
`dots.tts-soar` on irv-ml1, replacing chatterbox-fast. `tts.py` stays the single swap seam.
**Deltas (everything else — streaming placeholder-header WAV shape, browser Web-Audio PCM
decode, POST `/api/tts`, the DEC-5 serialize lock, INV-TTS-1..4 — unchanged):**
- **Gateway body OpenAI-shaped:** `{input, voice, response_format:"wav", stream:true}`
`input` not chatterbox's `text`, `response_format` not `format`. Closer to the Zonos-era
client. `DOTS_TTS_URL = http://10.100.79.3:8198/v1/audio/speech` replaced `CHATTERBOX_TTS_URL`.
- **Sample rate 24000 → 48000 Hz** (browser `SR = 48000` in `index.html`, else ~2× too fast).
- **Default voice `glados_25s``glados`** (dots voices: donut/glados/emmie/miranda). donut carries over.
- **Serialized single-consumer** — dots renders one generation at a time; satisfied by the
existing DEC-5 lock, no code change.
- **Affect stays dropped** (DEC-7): dots has no emotion knob, same as chatterbox — NOT a fresh loss.
Live-verified end-to-end on `:8765` (RIFF/WAVE 48kHz mono s16le through `/api/tts`); 520 tests
green. Contract `donut_voiced_interview.contract.md` amended (migration banner + DEC-1/3/8/9a +
FN blocks). chatterbox-fast `:8197` kept as rollback, then reclaimed by infra-ops once
operator ear-confirmed. Superseded next day by config-driven voices (see
[[2026-08-12-config-voices-two-voice]]).
@@ -0,0 +1,32 @@
`[2026-08-12]` **Config-driven voices + two-voice dialogue/narration split SHIPPED (v0.22.3, `5c3d0ad`).**
Operator ask: "we need rata side config of voices too" + "tempted to do two voices — one for
dialogue, one for non-dialogue." Chose (via AskUserQuestion) "config + two-voice together now."
**Config (retires the hardcoded `_TTS_VOICE_MAP`):** `~/.config/ratatoskr/voices.json`, beside
`local_agents.json`/`provider.env` (NOT folded into the agent-index schema — isolated from its
v1→v2 silent-drop foot-gun). Schema:
`{"default":"<v>","agents":{"<agent_id>":{"voice":"<v>","narration_voice":"<v>"?}}}`. Absent/malformed
→ built-in `DEFAULT_VOICE_CONFIG`, logged, never crashes (INV-TTS-5). Initial: donut→donut;
sindra→miranda dialogue + emmie narration.
**Two-voice = the presence of `narration_voice` is the split switch** (dropped the operator's
proposed `dialogue_only` flag — an agent with no narration_voice is single-voice, which is why
dialogue-only Donut needs no special-casing). `segment_dialogue` splits on quotes (straight `"`
+ curly `“ ”`; apostrophes never split a word): quoted = dialogue → `voice`, rest = narration →
`narration_voice`. Known limit: if a mixed persona speaks WITHOUT quotes, it reads as narration.
**Synthesis (`tts_stream_stitched` replaced `tts_stream`):** spans synth SERIALLY (dots
single-consumer), span 0 verbatim (wav header+PCM), spans 1..N header-stripped
(accumulate-until-`data`) → one gapless 48kHz stream with a single leading header (INV-TTS-7).
A single-span list is a byte-identical passthrough (INV-TTS-6, no single-voice regression). NOTE
the dots-pcm-not-headless-in-stream foot-gun that forced this design (see Tried and abandoned).
New module `src/ratatoskr/voices.py` (`load_voice_config` / `segment_dialogue` /
`resolve_voice_spans`); `create_app` gains `voice_config`; `entrypoint` loads it. Contract DEC-11
+ INV-TTS-5/6/7; new `tests/test_voices.py`. **545 tests green**, new code ruff-clean. Discipline:
contract-first + TDD + seam-check + local bug-hunt + live-verify; heid async gates SKIPPED
(offered). Live-verified on `:8765`: Sindra mixed turn → 2 dots calls (emmie+miranda) stitched to
one 48kHz WAV (single RIFF header); Donut single-voice unchanged. Cut as **patch** (self-contained
feature, no downstream coordination → patch-defensible → tie-break to patch; operator declined the
v0.23.0-minor option by saying "push it"). Builds on [[2026-08-11-tts-dots-migration]].
@@ -0,0 +1,30 @@
`[2026-08-12]` **Personal WT :8081 deadlocked after ~4 days → routed to worldtree-dev as WT #401.**
Operator hit "failed to load agents" in the web console + asked "does the server need
restarting?" Diagnosed: NOT ratatoskr. The personal Worldtree at `10.250.50.152:8081`
(`worldtree-personal-worldtree-api-1`, "Up 4 days (unhealthy)") was HUNG — TCP port open (accepts
connections) but the app answered NOTHING: `/health`, `/agents`, `/capabilities` all returned
`000` (accept-then-drop, "Server disconnected without sending a response"). Classic dead/deadlocked
event loop behind a live socket.
**Ratatoskr behaved correctly:** `/api/agents` was 12/12 502 `network_error` on the dead
upstream (the endpoint's `except (httpx.RequestError, ConnectFailed) → 502`), and auto-recovered
to 8/8 200 (donut+sindra back) the instant WT answered — NO ratatoskr restart. Restarting
ratatoskr would only have 502'd against the same dead upstream + dropped the session.
**Recovery + routing:** operator greenlit → I pinged infra-ops (uptime lane). They `docker
restart`'d it (same image, no recreate; demo `:8080` + pinned `:8082` untouched); `/health`
recovered 000→200 at ~45s. Operator greenlit root-cause → routed to worldtree-dev, tracked as
**WT #401** (diagnose-first). infra-ops froze the full 4-day container log (55MB / 647k lines,
`2026-08-08T22:15Z`→restart) to host-fs (`nh3-dev:/var/tmp/worldtree-401/`) since the docker
health buffer had already rolled.
**Smoking gun (infra-ops, from the app log):** last good `/health` at `22:00:26Z`; the dying
worker's LAST line = a sqlite **"unable to open database file"** traceback in the
**`context_promotion` ledger** at `22:09:38Z`. No OOM, disk not full. worldtree-dev's read: a
background/idle promotion job made an untimed synchronous sqlite call that wedged the event loop
(no user turn in flight; abrupt serving→silent in <60s; ~0.1s connection drops = accept queue
serviced but handler coroutines never scheduled). Consumer-side data I gave #401: today's :8081
load was light + fully sequential, and TTS synthesis offloads to dots `:8198` (added ~zero :8081
load). **Root-cause is worldtree-dev's; I owe them only a consumer-side repro if they ask.**
The hang-signature is now a substrate foot-gun (see Current state).
+13 -8
View File
@@ -1,6 +1,6 @@
# Persistent memory — ratatoskr # Persistent memory — ratatoskr
_Last updated: 2026-08-09_ _Last updated: 2026-08-13_
> **Always check for `/tmp/ratatoskr-dev-handoff.md`** — if it exists and its > **Always check for `/tmp/ratatoskr-dev-handoff.md`** — if it exists and its
> `Written:` stamp is under an hour old, read it (it carries the in-flight > `Written:` stamp is under an hour old, read it (it carries the in-flight
@@ -44,23 +44,27 @@ upstream API key stays server-side (INV-003).
## Current state / in-flight ## Current state / in-flight
_As of 2026-08-09:_ _As of 2026-08-13:_
**Nothing in flight on ratatoskr's side — this session's three arcs all shipped + pushed (HEAD `93011dc`, origin synced). The live tails are all upstream (worldtree-dev), not ours.** Details in Recent decisions; the durable substrate below. **Nothing in flight on ratatoskr's side. The RP-voice stack was rebuilt this session (chatterbox→dots, then config-driven voices + a two-voice dialogue/narration split) — all shipped + PUSHED (HEAD `5c3d0ad`, origin synced, tags `v0.22.2`/`v0.22.3`). The one open tail is upstream: WT #401 (worldtree-dev owns the personal-WT deadlock root-cause).** Details in Recent decisions; durable substrate below.
- **TTS migrated Zonos→chatterbox-fast + German-drift fixed** — live on `ratatoskr-web` **pid 2214344** (`:8765`). `tts.py` is the single swap seam: chatterbox-fast `http://10.100.79.3:8197/tts`, bespoke `{text,voice,format,stream}`, 24kHz, NO affect, NO client chunking (`RATATOSKR_TTS_URL` overrides). Drift fixed server-side (chatterbox `:v2` `max_chunk_chars=250`); consumer sends full text + default sampling. FOOT-GUN: `ratatoskr-web` is a bare detached process, no supervisor → re-stales on reboot/crash; systemd unit offered, operator declined. - **TTS = dots-tts + config-driven voices + two-voice split** — live on `ratatoskr-web` **pid 2290701** (`:8765`, dots + `voices.json` loaded). Backend is **dots-tts** via the fleet **switchboard** `http://10.100.79.3:8198/v1/audio/speech` (OpenAI-shaped `{input,voice,response_format,stream}`, 48kHz mono s16le, streaming placeholder-header WAV). `tts.py` is the single swap seam (`RATATOSKR_TTS_URL` overrides), NO affect. Per-agent voices live in **`~/.config/ratatoskr/voices.json`** (donut→donut single; sindra→miranda dialogue + emmie narration); an agent with a `narration_voice` gets the quote-split two-voice stitch (`tts_stream_stitched`, `src/ratatoskr/voices.py`).
- **Crown-recall arc CLOSED** (b185 identity-anchor rerank enrichment; gate verdict 79%→12% stress miss). Upstream tail: b185 promotion beyond personal/demo. If the operator ever pulls a prompt-side lever at the bare-query/adoption residual (the irreducible ceiling shared with #397), worldtree-dev asks us to re-point the gate. - **Personal WT :8081 recovered + root-cause routed to WT #401** — it deadlocked after ~4 days (event-loop wedge from a background `context_promotion` sqlite error); infra-ops `docker restart`'d it, worldtree-dev owns the diagnosis. Ratatoskr auto-recovered (degrade path held; no ratatoskr restart). Only possible follow-up = a consumer-side repro if worldtree-dev asks.
- **Two diagnostic gates in-repo** — `docs/diagnostics/lexical_recall_gate.py` (exact-term recall class instrument) + `temporal_order_gate.py` (#397 order_by=chapter). The before/after instruments for any future retrieval lever; #397 CLOSED, #400 closes on brokkr's no-harm post-half.
**Deferred / queued (non-urgent, pre-existing — tracked, NOT this-session):** TUI rip (`tui.py`+`tests/test_tui.py` delete cleanly, but `cli.py` is SHARED — relocate `USER_AGENT`/`ParsedArgs`/`_format_*` first; breaking public-surface removal → minor bump, operator approval); #368 backfill live-verify (synthetic legacy corpus); SDK **#371** (Rata = seed donor + future consumer; next = WT codegen spike; Rata's repin rides the later Python milestone; divergences D-1 unknown-SSE-skip / D-2 open-world-enums / D-3 typed-410); P06 referential-frame A/B/C probe (waits WT b122); R39 Phase-2 matched-quartets (confirmatory); bifrost snapshot-cursor adoption (normative, non-blocking). **Deferred / queued (non-urgent, pre-existing — tracked, NOT this-session):** heid gates on the two-voice feature SKIPPED in-session (offered, not run — optional cross-frontier gate if wanted); TUI rip (`tui.py`+`tests/test_tui.py` delete cleanly, but `cli.py` is SHARED — relocate `USER_AGENT`/`ParsedArgs`/`_format_*` first; breaking public-surface removal → minor bump, operator approval); #368 backfill live-verify (synthetic legacy corpus); SDK **#371** (Rata = seed donor + future consumer; next = WT codegen spike; divergences D-1 unknown-SSE-skip / D-2 open-world-enums / D-3 typed-410); P06 referential-frame A/B/C probe (waits WT b122); R39 Phase-2 matched-quartets (confirmatory); bifrost snapshot-cursor adoption (normative, non-blocking).
**Live substrate / foot-guns:** branch `main`, HEAD `93011dc` pushed; origin `git@gitea.phasefinal.com:vh/ratatoskr.git`. Deps: **`worldtree-sdk==1.2.0`** (gitea PyPI, `[tool.uv.sources]`; new `SseResponseTooLarge` at the stream surfaces), **`bifrost==1.1.5`**/wire v0.7, WT openapi vendored 2.3.0, conversation-api-spec v1.1. **`uv sync --all-extras`** — dev deps are a `[project.optional-dependencies]` EXTRA; plain `uv sync` PRUNES pytest/respx. Personal WT on **b185** (`http://10.250.50.152:8081`, both instances). Surfaces: combined **`:8392`** provider (memory+affect) + **`:8765`** web — detached background processes; `env.sh` sets the REQUIRED `RATATOSKR_MEMORY_EMBEDDING_DIM=1024`; keys mode-600 in `~/.config/ratatoskr/provider.env` + `RATATOSKR_ADMIN_API_KEY`. **FOOT-GUN: check owner-scoped agents via `GET /agents/<owner>:<name>` with the OWNER key (`WORLDTREE_API_KEY`), NOT the admin key (admin → false 404).** SDK-adapter foot-guns (cumulative): SDK reads = open-world dicts → presenters degrade-not-crash at 3 guard levels; all transport failures → `ConnectFailed(status=0)` (every caller `except ConnectFailed`); `local_agents` index v1→v2 bump SILENTLY DROPS pre-cutover agents (`load_local_agents` returns [] on version mismatch). `graphify-out/` runs dirty (auto-regen on commit hook, never stage). v1 = full Worldtree I/O coverage, cuts when WT tags 1.0 (`docs/coverage-map.md`). **Live substrate / foot-guns:** branch `main`, HEAD `5c3d0ad` PUSHED; origin `git@gitea.phasefinal.com:vh/ratatoskr.git`; version **v0.22.3**. Deps: **`worldtree-sdk==1.2.0`**, **`bifrost==1.1.5`**/wire v0.7, WT openapi vendored 2.3.0, conversation-api-spec v1.1. **`uv sync --all-extras`** — dev deps are a `[project.optional-dependencies]` EXTRA; plain `uv sync` PRUNES pytest/respx. Personal WT on **b185** (`http://10.250.50.152:8081`) — **FOOT-GUN: it can deadlock into a live-socket/dead-worker hang (WT #401); symptom = TCP port open but ALL routes return 000 ("server disconnected without response"); fix = infra-ops `docker restart`, ratatoskr auto-recovers.** Surfaces: combined **`:8392`** provider (memory+affect) + **`:8765`** web (pid 2290701) — detached bare processes, no supervisor → re-stale on reboot (relaunch `source env.sh && setsid .venv/bin/ratatoskr-web --host 0.0.0.0 --port 8765`); `env.sh` sets REQUIRED `RATATOSKR_MEMORY_EMBEDDING_DIM=1024`; keys mode-600 in `~/.config/ratatoskr/provider.env`. **dots-tts `:8198` is now a fleet SWITCHBOARD** (fronts dots on `:8202`; +3.3ms; engine swaps = instant `.env` flips; v4 engine adds an optional `language` field). **FOOT-GUN: dots' `language` validator is an alias-mapper NOT a gatekeeper** — word-junk ('klingon') maps to a tag + generates; only structurally-invalid tokens (empty/punctuation/overlong) 400 → validate ourselves if we ever send `language`. **FOOT-GUN: browser auto-TTS is opt-in (`localStorage['ratatoskr-tts']='1'`), PER-ORIGIN** — localhost vs LAN-IP have separate storage, so the toggle doesn't carry between them. **FOOT-GUN: check owner-scoped agents via `GET /agents/<owner>:<name>` with the OWNER key, NOT admin (admin → false 404).** SDK-adapter foot-guns (cumulative): SDK reads = open-world dicts → presenters degrade-not-crash at 3 guard levels; all transport failures → `ConnectFailed(status=0)`; `local_agents` index v1→v2 SILENTLY DROPS pre-cutover agents. `graphify-out/` runs dirty (never stage). v1 = full Worldtree I/O coverage (`docs/coverage-map.md`).
## Recent decisions ## Recent decisions
Chronological log of decisions with `[YYYY-MM-DD]` prefix. One line per Chronological log of decisions with `[YYYY-MM-DD]` prefix. One line per
decision. Captures rationale that won't be obvious from code alone. decision. Captures rationale that won't be obvious from code alone.
- `[2026-08-12]` **Personal WT :8081 deadlocked after ~4 days → routed to worldtree-dev as WT #401; ratatoskr degrade path held.** Live-socket/dead-worker event-loop wedge (background `context_promotion` sqlite "unable to open database file"); recovered by infra-ops `docker restart`; ratatoskr auto-recovered (12/12 502 → 8/8 200, no ratatoskr restart). → `persistent-memory.d/2026-08-12-wt-401-personal-deadlock.md`
- `[2026-08-12]` **Config-driven voices + two-voice dialogue/narration split SHIPPED (v0.22.3, `5c3d0ad`).** `~/.config/ratatoskr/voices.json` (per-agent voice + optional `narration_voice`); new `src/ratatoskr/voices.py`; `tts_stream_stitched` replaces `tts_stream`; contract DEC-11 + INV-TTS-5/6/7. → `persistent-memory.d/2026-08-12-config-voices-two-voice.md`
- `[2026-08-12]` **dots-tts `:8198` became a fleet SWITCHBOARD (fronts dots `:8202`, +3.3ms); engine swaps are now instant `.env` flips.** v4 engine adds an optional `language` field; byte-compatible, no ratatoskr change. FOOT-GUN: dots' `language` validator is an alias-mapper not a gatekeeper (word-junk generates, doesn't 400) — validate ourselves if we ever send it.
- `[2026-08-11]` **Sindra voiced with the dots `miranda` voice (`3e69bc9`, no bump).** Ratatoskr-side per-agent voice map (resolved at `/api/tts` by `agent_id`); superseded next day by the config-driven map (DEC-11).
- `[2026-08-11]` **TTS migrated chatterbox-fast→dots-tts (v0.22.2, `38b78d8`), operator-directed after an A/B win.** OpenAI-shaped `/v1/audio/speech`, SR 24000→48000, default voice glados_25s→glados, serialized single-consumer; affect stays dropped; chatterbox `:8197` reclaimed. → `persistent-memory.d/2026-08-11-tts-dots-migration.md`
- `[2026-08-08]` **Crown-recall RESOLVED at b185 — identity-anchor rerank enrichment closed the class (gate: 79%→12% stress miss).** worldtree-dev shipped a rerank-input identity anchor (from character/terms metadata) + chain-wide tie-determinism (`50ecec7`) instead of the floor lever; `lexical_recall_gate --anchor crown --runs=8` at b185 = 12% miss / 0% knife-edge / item @rank 0 (vs 79% post-fold b184). Floor packet does NOT reopen; 12% residual = bare-query formulation ceiling. Arc complete our side; efficacy delta on WT #400. - `[2026-08-08]` **Crown-recall RESOLVED at b185 — identity-anchor rerank enrichment closed the class (gate: 79%→12% stress miss).** worldtree-dev shipped a rerank-input identity anchor (from character/terms metadata) + chain-wide tie-determinism (`50ecec7`) instead of the floor lever; `lexical_recall_gate --anchor crown --runs=8` at b185 = 12% miss / 0% knife-edge / item @rank 0 (vs 79% post-fold b184). Floor packet does NOT reopen; 12% residual = bare-query formulation ceiling. Arc complete our side; efficacy delta on WT #400.
- `[2026-08-07]` **Crown-recall traced to a general RETRIEVAL-CLASS limitation (exact-lexical match on an indexed surface form buried by dense-semantic neighbors), NOT persona/extraction/synonymy.** worldtree-dev decomposition (WT #400): no-stemming tokenizer + RRF top-10 knife-edge + reranker ownership-demotion. Operator-greenlit evidence-gated sequence (ship stemming fold → gate measures residual → hold `rerank_hybrid_floor` lever). Shipped `docs/diagnostics/lexical_recall_gate.py` (class acceptance instrument); pre-fold baseline control 0% / stress[crown] 100% miss. Awaiting worldtree-dev fold+#397 deploy ping for the pre/post delta. Thread `01KZETD98T`; siblings WT #399 (filed), #397 (committed, not deployed). - `[2026-08-07]` **Crown-recall traced to a general RETRIEVAL-CLASS limitation (exact-lexical match on an indexed surface form buried by dense-semantic neighbors), NOT persona/extraction/synonymy.** worldtree-dev decomposition (WT #400): no-stemming tokenizer + RRF top-10 knife-edge + reranker ownership-demotion. Operator-greenlit evidence-gated sequence (ship stemming fold → gate measures residual → hold `rerank_hybrid_floor` lever). Shipped `docs/diagnostics/lexical_recall_gate.py` (class acceptance instrument); pre-fold baseline control 0% / stress[crown] 100% miss. Awaiting worldtree-dev fold+#397 deploy ping for the pre/post delta. Thread `01KZETD98T`; siblings WT #399 (filed), #397 (committed, not deployed).
- `[2026-08-07]` **TTS migrated Zonos→chatterbox-fast (`:8197` bespoke schema); affect DROPPED (Turbo has no emotion knob, operator "drop it for chatterbox"), client-chunking DROPPED (no per-synth cap), language pin DROPPED, browser SR 44100→24000.** Pushed `19b499a`. English "German drift" real cause (after 2 wrong hypotheses) = Turbo model OVER-RUNS its generation TAIL on long single generations (garble in final ~2-3s, ZCR 1.58x); **fixed SERVER-SIDE by infra-ops (`max_chunk_chars=250`, image :v2, operator ear-confirmed).** Consumer: **REVERTED my interim `top_k/top_p/temp` curbs** (they made it WORSE — pulled garble onset earlier), send full text + default sampling; KEPT the `/api/tts` empty-200→503 guard as hygiene. Contract `donut_voiced_interview.contract.md` amended. Loop closed. - `[2026-08-07]` **TTS migrated Zonos→chatterbox-fast (`:8197` bespoke schema); affect DROPPED (Turbo has no emotion knob, operator "drop it for chatterbox"), client-chunking DROPPED (no per-synth cap), language pin DROPPED, browser SR 44100→24000.** Pushed `19b499a`. English "German drift" real cause (after 2 wrong hypotheses) = Turbo model OVER-RUNS its generation TAIL on long single generations (garble in final ~2-3s, ZCR 1.58x); **fixed SERVER-SIDE by infra-ops (`max_chunk_chars=250`, image :v2, operator ear-confirmed).** Consumer: **REVERTED my interim `top_k/top_p/temp` curbs** (they made it WORSE — pulled garble onset earlier), send full text + default sampling; KEPT the `/api/tts` empty-200→503 guard as hygiene. Contract `donut_voiced_interview.contract.md` amended. Loop closed.
@@ -233,6 +237,7 @@ _For per-issue TDD implementation notes, Volva findings, and contract amendments
Log of approaches that were tried and rejected, with rationale. Future-self Log of approaches that were tried and rejected, with rationale. Future-self
defense against re-attempting the same cul-de-sac. defense against re-attempting the same cul-de-sac.
- `[2026-08-11]` **dots streaming `pcm` is NOT headerless — only NON-stream `pcm` is raw s16le.** `response_format:"pcm"` + `stream:true` still emits a RIFF placeholder-header WAV (dots always headers a stream). So the two-voice stitch (`tts_stream_stitched`) can't use "pcm for spans 2..N" to skip header-stripping — it streams every span as `wav` and strips the leading header on later spans (accumulate-until-`data`, INV-TTS-7). Don't chase the pcm shortcut for streaming.
- `[2026-07-10]` **Reverting our provider to bifrost 1.0.0 to fix the bound-handshake `schema_validation_failed` — DISPROVEN, and it would've been the WRONG state**`persistent-memory.d/2026-07-10-reverting-our-provider-to-bifrost-1-0-0-to-fi.md` - `[2026-07-10]` **Reverting our provider to bifrost 1.0.0 to fix the bound-handshake `schema_validation_failed` — DISPROVEN, and it would've been the WRONG state**`persistent-memory.d/2026-07-10-reverting-our-provider-to-bifrost-1-0-0-to-fi.md`
- `[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]` **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]` **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-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.)