RIFF — Call-Plan Artifact & Trace Schema, v0
Status: draft for review — open decisions marked OD-n
Date: 2026-07-01
Author: Fable 5 (design), reviewed by Claude Opus 4.8 + David
Audience: implementing Claude (Opus 4.8) + David
Depends on: the compiler reframe agreed 2026-07-01 (pre-flow = build, flow = artifact execution, post-flow = eval + canary; promotion = deploy)
Pinned against two flows by design: space_channel_network_ops (zero Do holes, Choose-heavy) and austin_plumbing (Do-heavy, Collect-heavy — the product shape)
Companion docs: docs/fable5-deterministic-flow-questions.md (the questions that produced this), docs/schema-ground-truth-flows.md (node-census fact sheet — Collect, not Do, carries the booking vertical; pin against three shapes)
1. Purpose and scope
This spec pins the two contracts everything else references:
- The compiled call-plan artifact — the immutable output of the pre-flow compiler (
riffc). The runtime executes only this; it never reads flow YAML. The console renders it. Promotion machinery versions it. - The trace event schema — the universal record of what happened. Emitted by the FSM engine (never by transports), consumed identically by the live console, all three sim tiers (L0/L1/L2), and the dreaming loop.
In scope: phone/booking-shaped flows (deterministic skeleton + typed holes). Space Channel, austin_plumbing, apartment_scheduler, dental_clinic, etc. Out of scope: the research web agent. It shares the trace schema only (§6) — its runtime is not artifact-shaped and must not be forced into this model.
Source flows stay YAML (human-authored). The compiled artifact is emitted as JSON, immutable, content-addressed. Examples below are shown in YAML for readability; the on-disk artifact is the JSON equivalent. (OD-1: confirm JSON for compiled form; recommendation is yes — no anchors/merge keys in an artifact, and it keeps runtime parsing trivial.)
2. Design invariants the schema encodes
These are the rules from the review, restated as properties the schema makes structural rather than disciplinary:
- I1 — The FSM owns all clocks. Every node carries a bounded clock (asset duration + watchdog grace, choose timeout, collect max-turn, do deadline). No node can wait indefinitely on a model, a browser, or a network.
- I2 — The model never touches routing. Choose resolves via DTMF or constrained grammar; the LLM fallback classifier returns
route | nullunder a hard budget — never audio, never the mic. - I3 — Do is a typed hole with the mic closed. The only runtime nondeterminism is a tool's return value. Everything around it (hold beat, deadline, result frames, failure route) is compiled.
- I4 — Two failure domains. A compile error blocks promotion of the artifact (CI). A runtime missing-asset degrades locally to a compiled degraded beat. The degraded assets themselves are mandatory, validated members of every artifact.
- I5 — Transports are dumb renderers. They implement
play(asset) → completionand relay input events. Audio intents, advance-on-done, watchdogs, and trace emission all live in the engine. - I6 — Parameterized states unfold at compile time. One source state (
report_space_content_audio) compiles to N manifest entries (briefing.launches,briefing.ufo, …). The caller view renders the unfolded entries; the engineer view may fold them back with a ×N badge. This is the resolution of T8: the source is honest to the FSM, the artifact is honest to the caller.
3. Artifact schema
3.1 Top level
artifact:
schema_version: "0.1"
artifact_id: "austin_plumbing@2026-07-01T18:22Z#a41f9c" # flow_id @ built_at # short source hash
flow_id: austin_plumbing
tenant_id: cust_atx_plumbing_001 # tenancy unit: one artifact per tenant per flow
source:
flow_yaml_sha256: "9c1e…"
includes: ["flows/subflows/austin_plumbing/*.yaml"]
compiler: "riffc 0.1.0"
built_at: "2026-07-01T18:22:04Z"
status: candidate # candidate | canary | promoted | retired
eval: # promotion machinery metadata
l2_replay_run: null # id of the replay-eval run that gated this artifact
canary:
fraction: 0.0
baseline: null # artifact_id of the incumbent
gates: [reprompt_rate, misroute_rate, time_to_route_p95, hangup_before_menu_rate]
entry: greeting
defaults:
voice: umbriel_rp
style: warm_professional
watchdog_grace_ms: 1500 # audio watchdog = asset.duration_ms + this
silence_alarm_ms: 2000 # trace stall_detected threshold
choose_timeout_ms: 7000
degraded: # I4 — mandatory, build fails if absent or unrendered
unavailable: asset:degraded_unavailable # "that option isn't available right now"
error: asset:degraded_error # "something went wrong on our end"
handoff: asset:degraded_handoff # "let me take your number and have someone call you"
models: { … } # §3.3
assets: { … } # §3.2
nodes: { … } # §3.4
Notes:
artifact_idis the deploy unit and the canary unit. Experiments never mix into a promoted artifact; a canary is a fraction of calls routed to a different artifact_id.tenant_idmakes the artifact the white-label unit: the customer-facing console view is a projection of this file with engineer-only fields elided.
3.2 Asset record
Every WAV the caller can ever hear is an entry here. Nodes reference assets by semantic id; the manifest pins the exact content hash (OD-2: nodes use semantic ids, manifest resolves to hashes at build — this replaces lookup_latest_by_state, whose "latest" resolution now happens in the compiler, once, not per call).
assets:
greeting:
sha256: "b31a…"
uri: "static/austin_plumbing/greeting.b31a.wav"
duration_ms: 4800
voice: umbriel_rp
style: warm_professional
text_sha256: "77e0…"
text_ref: "scripts/austin_plumbing/greeting.txt" # resolved text, never a template (10ea83db class = compile error)
rendered_at: "2026-07-01T18:20:11Z"
cadence: static # static | {refresh: 15m, max_honest_age: 45m}
spoken_timestamp: false # true for volatile content: the audio discloses its own as-of time
briefing.launches: # unfolded from parameterized source state (I6)
sha256: "04dd…"
uri: "static/space_channel/briefing.launches.04dd.wav"
duration_ms: 24500
cadence: {refresh: 15m, max_honest_age: 45m}
spoken_timestamp: true # "as of nine this morning, the Falcon launch is still go"
Compile rule: an asset whose rendered_at age exceeds max_honest_age at build time fails the build; one that exceeds it at runtime triggers degraded.unavailable locally plus a degraded trace event, and flips its freshness dot in the console (I4).
3.3 Models block
Pinned at build. In a deterministic flow this is small by construction — Collect states and the fallback classifier only.
models:
collect_primary:
provider: gemini
model: gemini-2.5-flash-live # pinned; changing it = new artifact
role: collect # may own the turn, bounded by node max_turn_ms
fallback_classifier:
provider: anthropic
model: claude-haiku-4-5
role: classifier # text in → route|null out; budget enforced by node
max_tokens: 8
slot_tts:
provider: piper
voice: umbriel_rp # OD-3: voice continuity — call-time slot TTS must be
role: slot_tts # perceptually continuous with prerendered Umbriel
3.4 Node types
Five types, matching the console vocabulary: say ▭, choose ◇, collect ▱, do ⬒, end ⬤.
say
nodes:
greeting:
type: say
asset: asset:greeting
barge_in: true
dtmf_shortcut: main_menu # digits during play resolve against this choose node (welcome direct-dial)
on_done: {goto: main_menu}
on_missing_asset: degraded.unavailable # runtime-local fallback (I4); reaching this at build = fail
Engine behavior (not per-node config): on state_enter, push play(asset) immediately — the 0485ac08 class of bug ("DTMF transition must play audio immediately") is engine behavior, not YAML discipline. Watchdog fires at duration_ms + watchdog_grace_ms and advances with audio_done{source: watchdog} — a missed browser ack can no longer strand the caller (Q11, closed).
choose
main_menu:
type: choose
grammar:
recognizer: constrained # constrained decode on the parallel server-side recognizer
tokens:
- {token: launches, dtmf: "1", aliases: [launches, rocket launches, launch schedule], goto: briefing.launches}
- {token: news, dtmf: "2", aliases: [space news, news], goto: briefing.news}
# … 3–7 …
- {token: bug, dtmf: "8", aliases: [report a bug, bug, problem], goto: collect_bug}
- {token: goodbye, dtmf: "9", aliases: [goodbye, bye, hang up], goto: sign_off}
- {token: options, aliases: [options, menu, help, what are my choices], goto: menu_announce}
reject: # the escape hatch — constrained decode MUST NOT force babble onto a token
method: likelihood_ratio # OD-4: constrained-vs-open score ratio, or explicit garbage model
garbage_token: true
thresholds:
route: 0.85 # ≥ route silently
confirm: 0.55 # [confirm, route) → one-word confirm; below → no_match ladder
confirm:
asset_pattern: asset:confirm.{token} # prerendered per-token confirms — validated at build
yes: {dtmf: "1", aliases: [yes, yeah, right, correct]}
no: {dtmf: "2", aliases: [no, nope]}
fallback: # third rung only (I2)
model: model:fallback_classifier
budget_ms: 800
on_null_or_timeout: no_match
no_match:
reprompt: asset:menu_short
after_attempts: 2
then: {goto: menu_announce} # full menu; a booking flow would go to degraded.handoff instead
timeout_ms: 7000
on_timeout: no_match
The open-vocab Gemini transcript is telemetry only — it appears in the trace (heard_open) for debugging and dreaming, never in the routing decision.
collect
collect_job:
type: collect
model: model:collect_primary
prompt_ref: "prompts/austin_plumbing/collect_job@sha256:31ab…"
slots: [job_type, urgency, callback_number]
max_turn_ms: 20000 # I1: the one place the model may own a turn — bounded
max_turns: 4
barge_in: true
on_complete: {goto: check_calendar}
on_timeout: {goto: degraded.handoff}
speculative: [check_calendar] # OD-5: engine MAY start the named Do's fetch during collect,
# discarding the result if routing diverges (latency win for booking)
do — the typed hole (I3)
check_calendar:
type: do
tool: "tool:google_calendar.find_slots@v2"
mic: closed # not configurable — the compiler rejects any other value
hold: asset:hold_checking # "one moment while I check the calendar" — plays immediately
deadline_ms: 6000
args_from: {slots: [job_type, urgency]}
result:
kind: enum_or_freeform
enum_frames: # enumerable outcomes → prerendered variants
slot_offered.morning: asset:offer.morning
slot_offered.afternoon: asset:offer.afternoon
slot_offered.evening: asset:offer.evening
no_slots: asset:no_slots_week
freeform_frame: # only genuinely free-form values touch call-time TTS
template: "The earliest opening is {slot_human}."
tts: model:slot_tts
on_success: {goto: confirm_slot}
on_failure: {goto: degraded.handoff} # always lands on a prerendered say
on_deadline: {goto: degraded.handoff}
The model never sees the tool (I3): args_from maps FSM slots to tool args; the result re-enters as data and the FSM picks the frame. If the hold beat finishes before the tool returns, the engine loops a compiled filler beat or brief silence up to deadline_ms — never re-opens the mic.
end
sign_off:
type: end
asset: asset:goodbye
disposition: completed # completed | abandoned | handoff
4. Example A — space_channel (excerpt)
Demonstrates: Choose-heavy, zero Do, parameterized unfolding (I6), volatile-content cadence.
artifact:
artifact_id: "space_channel@2026-07-01T17:05Z#c7e2d1"
flow_id: space_channel_network_ops
tenant_id: internal_demo
entry: greeting
degraded: {unavailable: asset:degraded_unavailable, error: asset:degraded_error, handoff: asset:degraded_handoff}
assets:
greeting: {duration_ms: 5000, cadence: static}
briefing.launches: {duration_ms: 24500, cadence: {refresh: 15m, max_honest_age: 45m}, spoken_timestamp: true}
briefing.news: {duration_ms: 22100, cadence: {refresh: 6h, max_honest_age: 24h}, spoken_timestamp: true}
briefing.ufo: {duration_ms: 26300, cadence: {refresh: 24h, max_honest_age: 72h}}
# … wx, missions, network, radiation; confirm.{token} × 10; menu_short; menu_full; goodbye; degraded_* …
nodes:
greeting: {type: say, asset: asset:greeting, dtmf_shortcut: main_menu, on_done: {goto: main_menu}}
main_menu: {type: choose, …} # as §3.4; tokens 1–9 + options
briefing.launches: {type: say, asset: asset:briefing.launches, barge_in: true, on_done: {goto: menu_reannounce}}
# briefing.news … briefing.radiation — six compiled entries from ONE source state (I6)
collect_bug: {type: collect, model: model:collect_primary, slots: [description], on_complete: {goto: menu_reannounce}}
menu_reannounce: {type: say, asset: asset:menu_short, on_done: {goto: main_menu}}
sign_off: {type: end, asset: asset:goodbye, disposition: completed}
What changed vs. today: report_space_content_audio no longer exists at runtime — the compiler unfolded it, so state-id-keyed dedup, slot-fill leakage, and "which topic am I?" (T8) are gone by construction. Launch data is just a 15-minute-cadence asset; the "volatile, never fetch on loop" special case is deleted.
5. Example B — austin_plumbing (excerpt)
Demonstrates: Collect-heavy, mid-call Do, speculative prefetch, handoff-shaped no-match.
artifact:
artifact_id: "austin_plumbing@2026-07-01T18:22Z#a41f9c"
tenant_id: cust_atx_plumbing_001
entry: greeting
nodes:
greeting: {type: say, asset: asset:greeting, on_done: {goto: intent}}
intent:
type: choose
grammar:
tokens:
- {token: book, dtmf: "1", aliases: [book, appointment, schedule, need a plumber], goto: collect_job}
- {token: emergency, dtmf: "2", aliases: [emergency, burst pipe, flooding, leak], goto: emergency_say}
- {token: hours, dtmf: "3", aliases: [hours, open, pricing, cost], goto: say_hours_pricing}
- {token: human, dtmf: "0", aliases: [person, human, operator, talk to someone], goto: degraded.handoff}
no_match: {reprompt: asset:intent_reprompt, after_attempts: 2, then: {goto: degraded.handoff}}
collect_job: {type: collect, …, speculative: [check_calendar], on_complete: {goto: check_calendar}}
check_calendar: {type: do, …} # as §3.4
confirm_slot:
type: choose
grammar:
tokens:
- {token: yes, dtmf: "1", aliases: [yes, yeah, that works, book it], goto: book_it}
- {token: no, dtmf: "2", aliases: [no, different time, later], goto: check_calendar_next}
book_it: {type: do, tool: "tool:google_calendar.schedule_event@v2", hold: asset:hold_booking,
result: {kind: enum_or_freeform, enum_frames: {booked: asset:booked_confirm}},
on_success: {goto: sign_off}, on_failure: {goto: degraded.handoff}}
sign_off: {type: end, asset: asset:goodbye_booked, disposition: completed}
Note the asymmetry with Space Channel that the schema must carry, per-tenant: a content line's no-match ends at the full menu; a business line's no-match ends at handoff (take a number, text the owner) — losing a plumber's call is losing revenue. Same schema, different no_match.then.
6. Trace event schema
The universal contract. Emitted by the engine on the existing riff/events.py bus, keyed by call_id; transports and consoles subscribe — nothing transport-specific in the events themselves (I5). Live calls, L1 fault-injection sims, and L2 replays all emit this identically; the research web agent adopts this section only.
JSON Lines. Envelope on every event:
{"v":"0.1","call_id":"c_9f21","artifact":"austin_plumbing@2026-07-01T18:22Z#a41f9c",
"seq":14,"t_ms":8140,"type":"route_decision", "...": "…"}
seq is a per-call monotonic integer (total order — event-ordering bugs were the whole T6 class, so ordering is explicit, never inferred from timestamps). t_ms is milliseconds since call_start.
| type | payload | notes |
|---|---|---|
call_start |
transport: telnyx|web|sim_l1|replay_l2, from_e164?, entry |
transport is a label here, nowhere else |
state_enter |
node, node_type, params? |
|
audio_pushed |
node, asset, sha256, duration_ms |
engine pushed the intent |
audio_done |
node, asset, source: client|watchdog |
watchdog = the browser/leg never acked; this is the Q11 stall, now visible and non-fatal |
barge_in |
node, at_ms_into_audio, via: dtmf|voice |
|
route_decision |
node, via: dtmf|grammar|confirm|fallback, heard_constrained?, heard_open?, confidence?, token|null, decision: route|confirm|reprompt|fallback_invoked|reject, goto?, latency_ms |
heard_open is Gemini's transcript, telemetry only; heard_constrained is what routed |
collect_turn |
node, turn, transcript_ref, slots_delta, model, latency_ms |
transcripts by ref, not inline (PII) |
tool_start |
node, tool, args_ref, deadline_ms, speculative: bool |
|
tool_result |
node, status: ok|error|deadline, latency_ms, result_kind, frame?, discarded: bool |
discarded = speculative fetch not used |
degraded |
node, reason: missing_asset|stale_asset|tool_failure|tool_deadline, served: asset |
every I4 local fallback is loud |
stall_detected |
node, silence_ms, hypothesis |
fired by the engine's silence alarm; the console's red row |
call_end |
disposition, duration_ms, states_visited, reprompts, fallback_invocations |
the roll-up canary metrics read from |
Sample trace — plumbing call with a Do and a watchdog save
{"seq":1, "t_ms":0, "type":"call_start", "transport":"telnyx","entry":"greeting"}
{"seq":2, "t_ms":10, "type":"state_enter", "node":"greeting","node_type":"say"}
{"seq":3, "t_ms":22, "type":"audio_pushed", "node":"greeting","asset":"greeting","duration_ms":4800}
{"seq":4, "t_ms":3100, "type":"barge_in", "node":"greeting","at_ms_into_audio":3078,"via":"voice"}
{"seq":5, "t_ms":3110, "type":"state_enter", "node":"intent","node_type":"choose"}
{"seq":6, "t_ms":4450, "type":"route_decision","node":"intent","via":"grammar","heard_constrained":"book","heard_open":"yeah I need to book somebody","confidence":0.91,"token":"book","decision":"route","goto":"collect_job","latency_ms":180}
{"seq":7, "t_ms":4460, "type":"state_enter", "node":"collect_job","node_type":"collect"}
{"seq":8, "t_ms":4470, "type":"tool_start", "node":"check_calendar","tool":"google_calendar.find_slots@v2","speculative":true,"deadline_ms":6000}
{"seq":9, "t_ms":9900, "type":"collect_turn", "node":"collect_job","turn":1,"slots_delta":{"job_type":"water_heater","urgency":"this_week"},"model":"gemini-2.5-flash-live","latency_ms":1400}
{"seq":10,"t_ms":10200, "type":"tool_result", "node":"check_calendar","status":"ok","latency_ms":730,"result_kind":"enum","frame":"slot_offered.morning","discarded":false}
{"seq":11,"t_ms":10210, "type":"state_enter", "node":"check_calendar","node_type":"do"}
{"seq":12,"t_ms":10230, "type":"audio_pushed", "node":"check_calendar","asset":"offer.morning","duration_ms":6200}
{"seq":13,"t_ms":17950, "type":"audio_done", "node":"check_calendar","asset":"offer.morning","source":"watchdog"}
{"seq":14,"t_ms":17960, "type":"state_enter", "node":"confirm_slot","node_type":"choose"}
Row 13 is the ee7872db/loop-back-stall class of bug, converted from "caller stranded, read the logs" to "one amber row, call proceeds."
7. Compile-time validation (the L0 suite)
riffc fails the build (blocks promotion — never kills a running artifact, I4) if any of:
- Any
gototargets a nonexistent node; any node unreachable fromentry; no reachableend. - Any
say/end/hold/confirm/reprompt/frame asset missing from the manifest, unrendered on disk, hash-mismatched, or older than itsmax_honest_age. - Any asset text containing an unresolved
{{…}}template (the 10ea83db class). degraded.*assets absent or unrendered (the fallback that can 404 is the same bug one level down).- Any
choosetoken lacking adtmfor alias set; duplicate DTMF digits; anyconfirm.asset_patternexpansion missing; thresholds not0 < confirm < route ≤ 1; norejectblock. - Any
dowithmic != closed, nodeadline_ms, nohold, or anon_failure/on_deadlinethat doesn't land on asay/end. - Any
collectwithout a pinned model,max_turn_ms, ormax_turns. - Any node without a derivable bounded clock (I1) — e.g., a
sayreferencing an asset with noduration_ms. - Loop-back edges that re-enter a
choosewithout an announcesayon the path (the re-announced-menu rule).
This list is the pre-flow validation tier; L1 (fault-injection event-loop sim) and L2 (trace replay) are separate specs but consume §6 unchanged.
8. Open decisions
| id | decision | recommendation / notes |
|---|---|---|
| OD-1 | Compiled artifact format | JSON, immutable, content-addressed; source stays YAML. Confirm no runtime YAML parsing. |
| OD-2 | Asset addressing | Semantic id in nodes, exact sha256 pinned in manifest at build. Retires lookup_latest_by_state from the call path (keep it as a compiler helper). |
| OD-3 | Slot-TTS voice continuity | Call-time Piper must be perceptually continuous with prerendered Umbriel. If it isn't, restructure freeform frames so the TTS fragment is short and sandwiched between prerendered frames. Needs a listening test. |
| OD-4 | Grammar mechanism + reject scoring | mlx-whisper: true constrained decode (logit masking) vs. initial_prompt/hotword biasing. If only biasing is available, the likelihood-ratio reject becomes mandatory, not optional. Empirical: tune route/confirm thresholds on recorded triage audio (the "network→work" call is test case #1). |
| OD-5 | Speculative Do | Ship in schema now, engine may ignore in v0. Rule: speculative results are used only if the FSM arrives at that Do with unchanged args; otherwise discarded: true. |
| OD-6 | Trace transport & auth | One WS topic per call_id off the engine bus; short-lived signed URLs for external observers (the texted-link observer, later supervisor barge-in). Design the addressing now, build nothing yet. |
| OD-7 | Node library / multi-tenant reuse | Booking flows will share 80% shape. Likely a source-level template ("booking-flow blueprint") the compiler instantiates per tenant — keep out of v0 artifact schema, revisit after two real tenant flows exist. |
| OD-8 | PII in traces | transcript_ref/args_ref point into a separate store with its own retention; trace stream itself stays PII-light so observer links are safe to share. Matters for the sellable product (and your HIPAA instincts apply even though RIFF isn't covered). |
9. Handoff notes for the implementing Claude
Suggested order, each step shippable alone:
- Emit the trace from the engine bus (
riff/events.py→ §6 events, per-call topic). Both transports and the map subscribe. This is the Q10/Q11 "one contract" fix and unblocks the console regardless of compiler progress. - Write
riffcv0 as validation-only: read the existing flow YAML + static-audio dir, emit the manifest JSON + the §7 L0 report. Don't change the runtime yet — just make the compile errors visible. (It will immediately flag real issues; that's the point.) - Constrain
choice_stt.py(OD-4) and emitroute_decisionwith bothheard_constrainedandheard_open. Confirm the tee on the Telnyx leg. - Switch the runtime to execute the artifact (assets by pinned hash, watchdog advance, engine-pushed audio intents). Retire
lookup_latest_by_statefrom the hot path. - Do-node runtime against
austin_plumbing(hold beat, deadline, enum frames), then speculative prefetch (OD-5) as a follow-up.
The console mockup (next artifact after this) renders: the §5 plumbing manifest as keypad + route-table detail, and the §6 sample trace in the trace panel — hand-written JSON first, no live wiring.
Implementation status (running log)
- 2026-07-01 — schema v0 saved as design of record. Agreed sequencing tweak: do §9 step 2 (
riffcvalidation-only, zero runtime risk) before step 1 (trace bus, touches live path). Committed + pushed tomain@d1a258c2as a clean revert point before any code lands. - 2026-07-01 — §9 step 2 landed:
riffcv0 (validation-only) inriff/riffc.py(+tests/test_riffc*.py, 17 tests). Classifies every state into the five node types (both anchor flows classify cleanly, 0 unknowns) and runs the L0 checks (§7).python -m riff.riffc <flow.yaml>. Zero runtime effect. What it immediately surfaced against the real flows:- space_channel: PASS, 0 errors, 15 warns. 9 orphaned states (
fetch_launches,fetch_space_content,console_not_ready*,launch_trouble*,report_launches,space_content_done,bug_captured) — the legacy states the deterministic redesign routed around, now provably dead → cleanup candidates.report_space_content_audiocorrectly unfolds to 6 prerendered topic briefings (I6). - austin_plumbing: PASS, 0 errors, 35 warns. 13 unpinned Collect states — concrete proof that Collect (not Do) carries the booking vertical + the LLM cost surface.
scheduling_offer_slots/propose_other_daysare dynamic-choice states (choices from a calendar-result slot) = the Do-result enum-frame pattern. ~11 orphanedscheduling_*states (legacy). Onlygreetingprerendered (the "prerender is Space-Channel-only" fact, made concrete). - Both flows flag: no
degraded.*fallback assets (I4 gap), nochoose.rejectpath (constrained-ASR gap), unpinned Collect/Do artifact fields. - Calibration learned from real flows: the 10ea83db error is a prerendered WAV whose text still contains
{{— NOT a live-TTS template Say (Gemini resolves that live). And achoices_slotchoose is dynamic, not broken. Both severities corrected. This is the Sayprerendered|live-ttsdiscriminator gap the ground-truth fact sheet predicted. - Next: §9 step 1 (trace from the engine bus) or the console mockup rendering these node/finding results.
- space_channel: PASS, 0 errors, 15 warns. 9 orphaned states (