RIFF boundary architecture: adopt contracts, evolve in place
- Status: Adopted, v4 - operator directed implementation 2026-07-10 (first applied scope: the 512-2777-LAW line — compiler copy-seam fixes, flow regeneration from packs, routing-table alignment, line contract tests)
- Date: 2026-07-09 (adopted 2026-07-10)
- Decision owner: RIFF operator
- Implementation status: Contracts are being applied via the existing engine per the phase plan; OTA calls and node deploys still require explicit human
GO - Supersedes: v1 of this file (
2a8823df) - Evidence window: FX-069 through FX-080 against repository baseline
2a8823df - Browser version:
docs/html/2026-07-09-architecture-rewrite-decision.html
Decision in one paragraph
Adopt the FSM / deterministic code / LLM boundary as a set of enforced contracts,
but do not rewrite RIFF around a new event-bus FSM now. Evolve the current engine in
place. The new work is larger than a voice: announce | model | none field: RIFF
needs explicit narration ownership, a dynamic-announce building block, and a prompt
delivery receipt that survives connect races, barge-in, and re-entry. Keep the
existing guard-based control model unless evidence reaches the rewrite triggers in
this document. Treat per-state LLM routing as a cascade/text/HTTP capability only;
it does not affect the Gemini Live phone path and is not part of the voice rollout.
This is a decision about architecture direction. It is not approval to regenerate
flows, deploy to a phone node, enable a feature flag, or place an OTA call. OTA still
requires explicit human GO.
Decisions requested
Approve, reject, or amend these six decisions:
- Adopt the boundary contracts. The FSM owns legal progress; deterministic code owns resolution, validation, side-effect mediation, and delivery state; the LLM may interpret or phrase only inside declared envelopes.
- Evolve in place. Add contracts to the current compiler, state manager, live session, and transports instead of replacing the state graph or moving to an event-bus transition model.
- Build narration in two parts. First make ownership explicit and lintable;
then add
dynamic_announceplus delivery receipts. Ownership metadata alone is not the end state. - Formalize one Interaction Loop. Give cascade and Live the same outcome names, progress rules, and trace vocabulary without pretending they share one model execution path today.
- Separate LLM-routing decisions. The existing per-state routing cascade may be enabled only for cascade/text/HTTP deployments after its own gates. Gemini Live voice routing requires a separate architecture decision.
- Decline a rewrite now, not forever. Reconsider only when the explicit triggers under "Rewrite reconsideration" are met.
Why this decision exists
The original architecture discussion bundled several different questions:
- Should menus, extraction, branching, validation, retries, and escalation be explicit contracts?
- Should those contracts fail at compile/load time?
- Should narration ownership be declared?
- Should RIFF expose an Interaction Loop abstraction?
- Should different states use different LLMs?
- Should the current FSM and generated flows be replaced?
Those questions do not have one shared answer. Most control contracts already have an implementation foothold. The largest reliability gaps exposed by recent calls were narration, delivery, and transport gaps around that control layer. The correct response is to widen the contract boundary and enforce it, not to infer from a small incident sample that either the current FSM or a rewrite has been proven superior.
Scope and non-goals
In scope
- Generated business-pack flows and hand-authored modules.
- Deterministic choice resolution, extraction validation, guards, transitions, retries, escalation, and side-effect mediation.
- Static audio, call-time synthesized audio, model speech, barge-in, connect-time prompting, re-entry, and audio delivery evidence.
- Gemini Live and cascade/text/HTTP as separate execution paths behind common control contracts.
- Compiler/load-time gates, runtime traces, replay tests, and staged promotion.
Not in scope
- Replacing the YAML flow language.
- Introducing a distributed event bus.
- Moving transport lifecycle into the LLM.
- Mid-session model switching on a persistent Gemini Live connection.
- Rewriting spoken copy merely to conform to a new object model.
- Any OTA call without explicit human
GO.
Terms
| Term | Meaning in this document |
|---|---|
| Contract | A declared invariant with a compile/load gate, runtime behavior, trace evidence, and a replay test. Documentation alone is not a contract. |
| Candidate | An LLM or deterministic extractor's proposed value. It is not trusted context until validation succeeds. |
| Narration | The words RIFF intends to speak and the declared component responsible for producing them. |
| Delivery | The transport-observable lifecycle of intended audio: queued, started, progressed, completed, failed, or interrupted. It does not prove human perception. |
| Prompt receipt | Runtime evidence that a specific prompt reached a defined delivery threshold. Never call this a "heard" receipt. |
| Interaction Loop | Enter state, satisfy narration obligation, observe, resolve/extract, validate, account for progress, transition or retry, mediate actions, and emit trace evidence. |
| Rewrite | Replacing the current guard/pull transition model or flow representation, not an incremental compiler emission change. |
Architectural ownership
The system has six layers. A layer may consume facts from the layer below it, but it must not silently take over another layer's decision.
| Layer | Owns | Must not own |
|---|---|---|
| Transport identity | Binding webhook, call, stream, and media lifecycle to one call/business | Business routing guesses from stale or unnamed streams |
| Audio delivery | Playback/synthesis lifecycle, interruption fraction, completion/failure events | Whether a branch is legal or a caller answer is valid |
| Interaction Loop | Turn ordering, prompt obligations, attempt/progress accounting, trace vocabulary | Business-specific branch policy |
| FSM/control | Legal states, guards, transitions, terminal outcomes, escalation targets | Free-form language interpretation |
| Deterministic capabilities | Choice resolution, schema validation, side-effect mediation, idempotency | Inventing missing business facts |
| LLM | Candidate extraction and natural wording within a declared envelope | Adding choices, choosing undeclared branches, committing side effects, or claiming unobserved success |
The non-negotiable boundary
The LLM may propose; deterministic code validates; the FSM decides whether progress is legal; the transport reports what it delivered. No model utterance, tool call, or audio callback is itself proof that the state may advance.
Normative contract stack
The words MUST, MUST NOT, SHOULD, and MAY below are normative.
C1. Menu contract
- Code MUST build the complete choice set, aliases, announced DTMF map, and escape choices.
- Narration MUST use the same ordered announced-choice projection as resolution.
- The LLM MUST NOT add, drop, renumber, or reorder choices.
- A digit not explicitly announced MUST resolve to
no_match; it MUST NOT fall through to a hidden list position. - Ambiguity MUST abstain rather than select the nearest option.
- Every choice state MUST declare retry and escalation/recovery behavior.
Existing footholds: locked_choice, evaluate_choice, alias compilation,
tests/test_choice_escape_coverage.py, and the FX-069 DTMF gate.
C2. Extraction contract
- An extractor MUST return a typed candidate, confidence/provenance where available, or an explicit abstention.
- A candidate MUST pass the declared
slot_typesvalidator before entering session context. - Negation, ambiguity, and schema violations MUST NOT silently become affirmative values.
- A rejected candidate MUST count according to the state's declared attempt policy.
- An LLM acknowledgement without a validated slot write MUST NOT count as progress.
Existing footholds: typed slots, enum abstention, deterministic extractors, and
all_state_slots_filled_and_valid.
C3. Branch contract
- Branches MUST be pure functions of validated context plus explicit engine facts.
- Branch evaluation MUST NOT depend on prose generated by the LLM.
- Guard inputs and the winning transition MUST be serializable in the run trace.
- Transition
setsandclearsMUST apply atomically and identically in cascade, Live, simulation, and replay. - Unknown guards and unreachable destinations MUST fail before deployment.
RIFF's pull-based guards satisfy this shape. Named pushed events are not required to obtain it.
C4. Validation and sole-exit contract
- A collect state MUST NOT take its success edge until required slots are present and valid.
- A side-effect state MUST NOT run until its declared preconditions are true.
- Validation failure MUST return a typed outcome such as
invalid,ambiguous, ormissing, not an unstructured boolean plus prose. - The success validator and escape/escalation edges MUST have explicit precedence.
C5. Progress, retry, and re-entry contract
- Every input-bearing state MUST declare what counts as progress.
- Failed attempts MUST be counted per state and failure kind, not inferred only from total turns.
- Re-entering a state MUST be idempotent: it cannot duplicate a side effect, consume a one-shot before delivery, or inherit stale answer text from another state.
- One-shot narration flags MUST be committed from delivery outcome, not merely from playback start or state entry.
- Retry exhaustion MUST produce one declared outcome; it cannot leave the caller in an unbounded loop.
Existing footholds: no_progress_streak, stalled, state-stamped user text, and
FX-076/078 recovery behavior. The missing piece is a uniform engine-level policy.
C6. Escalation contract
- Every collect/choice state MUST declare an exhaustion target.
- Escalation kind MUST be preserved. A safety or human-handoff capability MUST NOT be demoted to a generic menu recovery.
- Partial validated context SHOULD accompany escalation.
- Escalation outcome and reason MUST be observable in the run record.
FX-080 demonstrates why "has an escape" is weaker than "has the correct escape."
C7. Narration ownership contract
Every compiled state MUST declare a narration policy. The canonical proposal is
narration, not a scalar voice, because voice already names synthesis identity
and field-level voice policies elsewhere in the pack schema.
narration:
owner: static_announce # static_announce | dynamic_announce | model | none
obligation: required # required | optional | forbidden
prompt_id: main_menu_v3
on_interrupt: replay_if_unmet # replay_if_unmet | continue | escalate
max_replays: 3
on_replay_exhausted: escalate
one_shot:
min_delivery_fraction: 0.75
answer_admission:
requires_content_delivered: true
Rules:
static_announceMUST resolve entirely at build time and MUST have a manifest asset or an explicit, tested failure policy.dynamic_announceMAY resolve validated slots or runtime results at call time, but MUST be a distinct compiled audio-only state type followed by a separate input hub. It is not merely aplay_audio.render_modebecause receipt emission, synthesis failure, input forwarding, and successor shape are structural contracts.modelMAY own flexible narration only where exact deterministic audio is not a contract. It MUST NOT narrate a locked menu's authoritative option list or own a prompt with declared critical content. Oncedynamic_announceexists, either declaration is a compile error. Until then, critical model narration is a flagged temporary fallback listed in the Phase 2 compatibility report.noneis legal for silent routing/action states and listening hubs. If the hub accepts an answer to a prompt, it MUST declare the required prompt receipt.one_shot.min_delivery_fractiongoverns whether narration may be marked consumed. It MUST NOT be reused as proof that an answer is admissible.answer_admission.requires_content_deliveredgoverns whether an answer may confirm the prompt. It MUST be true for prompts containing interpolated values or other declared critical content.- Delivery-failure replays MUST be counted separately from caller-answer attempts.
max_replaysandon_replay_exhaustedare required when interruption policy may replay; exhaustion MUST NOT loop. - A state suppressed from model speech MUST NOT declare
owner: model. - Compiler analysis MUST examine every reachable inbound path. "At least one speaking predecessor" is insufficient when another inbound path can arrive mute.
The initial migration MAY derive policies to produce a report, but generated packs MUST materialize the final policy. Hidden compiler defaults are not acceptable as the steady state.
C8. Prompt delivery contract
Narration ownership answers who should produce words. It does not answer whether the audio survived. Delivery is a separate runtime contract.
A delivery receipt has this minimum shape:
{
"prompt_id": "identity_confirmation_v1",
"state_epoch": "replicant_confirm_identity:7",
"state_id": "replicant_confirm_identity_announce",
"source": "dynamic_announce",
"status": "completed",
"prompt_content_hash": "sha256:resolved-text-and-render-policy",
"audio_content_hash": "sha256:audio-bytes-played",
"delivered_fraction": 1.0,
"delivered_through_word_index": 17,
"required_content_delivered": true,
"one_shot_consumable": true,
"interrupted_by": null,
"attempt": 1
}
Allowed terminal statuses are completed, interrupted, and failed. queued and
started are non-terminal observations. The receipt means transport-observed
delivery only; it MUST NOT be described as proof the caller heard or understood it.
Input hubs that depend on a prompt declare:
requires_prompt_receipt: identity_confirmation_v1
Rules:
- One-shot consumption and answer admission MUST be computed independently. A proportional delivery threshold MAY consume a menu one-shot; it MUST NOT certify that a confirmation's critical content was delivered.
- A semantic yes/no answer MUST NOT confirm a prompt that has no qualifying receipt
with
required_content_delivered: true. - For a prompt containing interpolated or declared critical content, the compiler
MUST identify the required content spans,
answer_admission.requires_content_deliveredMUST be true, and the delivery layer MUST map the spans to word timings produced at render or synthesis time. A fraction alone is insufficient because the critical value may be near the end of the prompt. An unevaluable content receipt is recorded as such and treated as not delivered; answer admission fails closed. - The receipt MUST bind the fully resolved prompt/render policy and the audio bytes
actually played. A versioned manifest entry or call-time synthesis record MUST
prove the mapping from
prompt_content_hashtoaudio_content_hash; a missing or mismatched binding cannot satisfyrequired_content_delivered, regardless of fraction or completion status. - A hub's
requires_prompt_receiptis satisfied only by a receipt from the current narration-obligationstate_epoch. A receipt from a prior entry into the same state MUST NOT qualify after re-entry establishes a new obligation. - An exact DTMF or exact spoken-alias barge during an announce MAY resolve early. The
announce records its interruption receipt and forwards the buffered input to the
declared input hub; the hub performs resolution. An announce state never resolves
choices itself. The trace records
early_choice, interruption, and forwarding. - Noise, ambiguous speech, or non-matching input before the receipt threshold MUST preserve/replay the prompt obligation.
audio_failedMUST take a declared fallback: retry audio, switch to an allowed owner, or escalate. It MUST NOT silently enter a listening hub.- Connect-time entry, normal transitions, and re-entry MUST use the same receipt rules.
This contract closes the gap that a voice: declaration alone leaves open.
C9. Transport identity contract
- Every media stream MUST bind to one active call identity before business/session state can consume it.
- Binding, active-call insertion, and invalidation MUST be serialized. Any check-and-mark operation MUST be atomic across webhook and media threads.
- Call teardown MUST invalidate the binding and prevent later frames from reviving it.
- Fallback binding without a stable identity MUST be alarmed and bounded; it cannot look like an ordinary successful bind.
- The trace MUST record the identifiers and binding reason without exposing secrets.
- Replay fixtures MUST cover stale call entries, missing hangup callbacks, and overlapping calls.
FX-075/079/080 show that a correct FSM cannot compensate for the wrong caller's stream.
C10. Side-effect and success contract
- LLM tool calls are requests. The state manager or capability mediator MUST enforce state, argument, authorization, and idempotency preconditions.
- A write MUST carry an idempotency key scoped to the call and logical action.
- Booking, message, or escalation success MUST derive from a typed provider result, never from model wording.
- Retries and re-entry MUST return the prior committed result or make one safe retry.
A retry is safe only when the provider deduplicates on the idempotency key or the
prior attempt is provably uncommitted. Otherwise the action returns a typed
unknown_outcome, records it, and escalates instead of re-executing.
C11. Operator-visible evidence contract
- Every deterministic outcome defined by C1-C10 MUST appear in the primary run record used by operators and replay tools, not only inside a nested debug payload.
- Choice match/refusal, extraction acceptance/rejection, guard verdict, narration owner, prompt receipt, retry/exhaustion, escalation, and action result MUST have stable typed fields.
- A record MUST carry enough state epoch and provenance to distinguish an answer in state A from narration or transition behavior in state B.
- Adding an internal debug field without updating the operator-facing projection does not satisfy this contract.
- Each projection MUST have a non-vacuous test that fails when the typed field is removed while the underlying decision still occurs.
The Interaction Loop
The Interaction Loop is a shared behavioral model, not a claim that cascade and Gemini Live have one implementation today.
For each state epoch:
- Enter with validated context and a unique state epoch.
- Satisfy narration or establish that narration is optional/forbidden.
- Observe caller input, DTMF, provider result, timeout, or delivery event.
- Resolve/extract deterministic signals first; ask an LLM only inside the declared capability envelope.
- Validate candidates and action preconditions.
- Account for progress, failed attempt, interruption, or no-op.
- Decide exactly one transition, retry, escalation, or wait outcome.
- Commit transition mutations and mediated side effects idempotently.
- Trace the inputs, outcome, owner, receipt, guard verdict, and resulting state.
The first implementation target is common typed outcomes and trace fields. It is not
a big-bang refactor of turn.py and live/session.py. A new shared abstraction is
justified only where it removes duplicated semantics and has parity tests on both
paths.
Current implementation map
| Contract | Current footing | Required delta |
|---|---|---|
| Menu | locked_choice, aliases, deterministic resolver, escape tests |
One compiler-owned announced projection, full contract lint, and a Phase 1 parity fixture proving cascade and Live dispatchers return identical outcomes for identical input |
| Extraction | Typed slots, validators, enum abstention | Explicit counted schema-failure outcomes and provenance |
| Branch | Guard registry and transition evaluator | Purity/serialization gate and cross-path sets/clears parity |
| Validation | Required-slot success guards | Typed validator outcomes and explicit precedence |
| Retry/re-entry | no_progress_streak, stalled, state-stamped input |
Per-state policy and engine-level idempotent re-entry |
| Escalation | Per-kind edges after FX-076/080 | Exhaustive compile gate and run-record outcome |
| Narration | Suppression lists, announce/hub idiom, voiceless lint | Declarative policy on every generated state; all-path analysis |
| Delivery | Audio events and barge timing exist in transports | Prompt identity, receipts, common semantics, hub precondition |
| Transport identity | FX-075/079 binding fixes and tripwire | Durable identity invariant and overlap replay suite |
| Side effects | State manager and required tools | Uniform idempotency/success contract across capabilities |
| Operator evidence | Turn/run records and nested debug payloads | Promote all contract outcomes into stable operator-visible fields |
Evidence matrix
Recent incidents are coverage evidence, not votes. They overlap, some share one call, and FX-080 was found by adversarial review rather than by a live caller. Therefore no "6.5 of 8" conclusion is used.
| Evidence | Primary contract | Would this architecture help? | Current status |
|---|---|---|---|
| FX-069: unannounced digits reached hidden choices | C1 Menu | Prevent at compile/test and resolver | Fixed with generalized DTMF gate |
| FX-071: negated message intent became affirmative | C2 Extraction | Reject candidate or abstain | Fixed with negation handling |
| FX-075: stale call captured another stream | C9 Transport identity | Detect/prevent before FSM input | Fixed and hardened by FX-079/080 |
| Property call: muted choice state had no speaker | C7 Narration | Compile failure on owner/path gap | Stop-gap unmute and lint shipped |
| FX-077: state entered before Live connected | C7/C8 Narration/delivery | Require connect-time prompt receipt before answer | Narrow locked-choice fix shipped; general contract open |
| FX-078: early barge burned one-shot menu | C5/C8 Re-entry/delivery | Commit one-shot from delivery outcome | Barge path mitigated; zero-delivery audio failure/missing path remains open until separately fixed and proven |
| FX-076: caller trapped in message collection | C5/C6 Retry/escalation | Require bounded declared exhaustion | Fixed with collect escape gate |
| FX-080: emergency stall returned to menu | C6 Escalation | Require correct typed target, not any escape | Fixed through adversarial review |
FX-074 is migration evidence, not a caller-failure vote: rebuilding two stale flows produced hundreds of lines of unrelated spoken-copy drift. That proves regeneration needs a controlled diff/audio/deploy process. It does not measure the engineering cost of an event-FSM rewrite, and this document does not claim that it does.
Execution-path truth: LLM routing
The repository has two materially different model execution paths:
- Cascade/text/HTTP:
run_turnselects an adapter. The per-state policy cascade andRIFF_PRIMARY_LLM_ROUTINGcan affect this path. A repeat-N text-mode result measured an approximately 85% cost reduction for a scoped coffee-flow assignment. - Gemini Live voice:
GeminiLiveSessionopens one persistent model connection for the call. It does not invoke the cascade's per-state adapter selector.
Therefore:
- The measured cost result is real only for the cascade/text/HTTP scope tested.
- Flipping
RIFF_PRIMARY_LLM_ROUTINGhas zero expected effect on Gemini Live phone cost, latency, or quality. - "Live parity" cannot mean rerunning the same flag OTA; the path is structurally different.
- Voice model routing requires a separate decision: session-level model choice or a non-persistent/per-turn voice architecture. Neither is part of this adoption.
Alternatives considered
| Option | Reliability benefit | Migration/replay risk | Decision |
|---|---|---|---|
| A. Rewrite flows and control around event-style lifecycle objects | Could unify event provenance if designed well | Highest migration surface; re-proves mature guards, compiler output, audio, and all flows | Decline now; retain revisit triggers |
| B. Add the full contract stack to the existing engine | Directly covers observed control, narration, delivery, and identity failures | Incremental but spans compiler, Live, and transport; requires strict phase gates | Choose |
| C. Keep fixes as tests and conventions only | Lowest immediate cost | Hidden defaults and path-specific patches continue to drift | Decline |
Why option B
- Existing deterministic control primitives are useful and testable.
- The observed gaps cross control, narration, delivery, and transport; an FSM-only rewrite would not cover the whole failure surface.
- Contracts can be introduced with compatibility reports and per-business rollout.
- Each phase has an independent rollback.
- The design does not depend on incident-count arithmetic or a claim that rewrite cost has already been measured.
Delivery plan and gates
No phase starts implementation until Fable 5 review findings are resolved and the operator approves the amended design.
Phase 0 - freeze the decision and baseline
Deliverables
- This versioned ADR and HTML counterpart.
- A machine-readable inventory of every generated state's current menu, validation, escalation, narration, suppression, and audio shape.
- Per routed flow, a count and state list for every muted/input state reachable from at least one inbound path that does not establish the required prompt receipt. The counts MUST be stored in a versioned inventory artifact referenced from this ADR before Phase 2 starts.
- Baseline compile diffs and focused replay results for FX-069, 071, 075, 076, 077, 078, 079, and 080.
- Measured latency/failure baselines for static and call-time audio. Thresholds for Phase 3 are approved before its code is written.
Exit gate: evidence can be regenerated from a clean checkout without an OTA call.
Phase 1 - enforce existing control contracts
Deliverables
- Compiler/load gates for C1-C6, C10, and C11.
- The C9 transport-identity invariant: serialized bind/insert/invalidate with an atomic check-and-mark across webhook and media threads, plus replays for stale call entries, missing hangup callbacks, and overlapping calls.
- A parity fixture proving
maybe_handle_locked_choice_turnand Live's_maybe_handle_locked_choice_turn_completeproduce identical deterministic outcomes for identical inputs. - Explicit schema-violation attempt accounting.
- Idempotent re-entry tests for one-shots, state-stamped input, and mediated actions.
- One contract matrix showing gate, runtime outcome, trace field, and replay test.
Exit gate: all packs cold-compile; all routed flows pass focused contract tests; removing each new gate makes its reproduction fail. The C9 overlap, stale-entry, and missing-hangup replays pass; removing serialization or atomic binding makes the FX-075/079 reproductions fail. The locked-choice parity fixture runs both dispatchers, not only cascade.
Rollback: remove additive gates before any flow regeneration. No live behavior changes in this phase.
Phase 2 - materialize narration ownership
Deliverables
- Parse and validate the C7
narrationobject. - Compiler emission for every generated state; no hidden steady-state default.
- All-reachable-inbound-path analysis for silent listening hubs.
- A compatibility report before enforcement, followed by a hard gate once all packs are migrated.
- Studio/trace display of owner, obligation, prompt id, and interruption policy.
Exit gate: every state has one valid policy; every required prompt has an owner;
no suppressed state is model-owned; every none input hub proves its prompt source
on every reachable inbound path; the Phase 0 exposure counts are zero or have an
explicit migration in the operator-approved compatibility report. A conformance
replay per routed flow compares each declared owner with the observed narration
source per state epoch; any mismatch fails.
Rollback: compiled metadata is additive and ignored by runtime until Phase 3.
Phase 3 - build dynamic_announce and prompt receipts
Deliverables
- Call-time synthesis for validated template/runtime content in an audio-only state.
- Dynamic audio artifacts are call-scoped, MUST NOT enter a versioned/shared static manifest, and carry an approved retention bound on the phone node. A test fails if the versioned manifest writer can reach a dynamic artifact path.
- C8 receipt events across web and phone transports.
- Input-hub enforcement of
requires_prompt_receipt. - Connect, barge, audio-failure, retry, and re-entry behavior using one receipt model.
- Deterministic fallback or escalation when synthesis fails.
Exit gate
- FX-077 replay cannot confirm an unspoken identity prompt.
- FX-078 replay does not consume a one-shot below its declared threshold.
- A confirmation replay cannot admit
yeswhen the proportional threshold is met but any declared critical content remains undelivered. - A stale receipt from a prior state epoch cannot admit an answer after re-entry.
- A prompt/audio hash mismatch cannot satisfy critical-content delivery.
- Repeated delivery interruptions exhaust at the declared replay budget and take the declared outcome rather than looping.
- Static, dynamic, and model-owned prompt fixtures each exercise success, interrupt, and failure.
- Latency and synthesis-failure rates remain inside the Phase 0 approved thresholds.
- No second voice speaks the same prompt in one state epoch.
- The narration-source trace field is the named mechanism used to prove the single-owner gate.
Rollback: runtime feature flag returns affected states to the last approved owner before regeneration. After regeneration, rollback means restoring the prior generated flow/audio artifact as defined in Phase 5; a flag cannot restore an owner that regeneration removed.
Phase 4 - formalize Interaction Loop parity
Deliverables
- Shared typed outcome names for extraction, validation, progress, delivery, retry, transition, escalation, and action result.
- A shared run-record schema used by cascade and Live.
- Parity tests for transition mutations, attempt counting, and prompt receipts.
- Removal of path-specific rescue logic only when a shared mechanism proves the same behavior.
Exit gate: one replay fixture produces equivalent control outcomes and required trace evidence on both paths, with documented differences limited to model/audio execution. Remaining differences live in an explicit allowlist; every entry has an owner and removal condition, and an undocumented divergence fails the gate.
Rollback: keep the old path behind a temporary comparison flag until parity is demonstrated; do not delete working rescue behavior first.
Phase 5 - regenerate and promote one business at a time
Order:
- Property management first because it exercises per-caller identity and dynamic
ticket narration. It is also the highest-risk pilot: its stop-gap unmutes and
model-voiced locked-choice states are load-bearing. Its Phase 2 report MUST show
each affected state resolving deliberately to
dynamic_announceor another approved owner, never silently defaulting tostatic_announce. - One low-risk static-menu business.
- Remaining businesses only after both shapes promote.
For each business:
- Regenerate from its pack; never hand-patch the generated flow.
- Review spoken-copy and graph diffs separately from contract metadata.
- Render/verify content-addressed audio and manifest.
- Run the standard Static/L1/L2/L3 promotion ladder with replay artifacts.
- Deploy to M3 candidate, verify
/healthzSHA/mode/dirty state, and preserve the M1 rollback artifact. - Run OTA only after explicit human
GO.
Rollback: drain candidate, restore the prior flow/audio manifest and code SHA, restart, and verify health before accepting calls.
Separate track - cascade/text/HTTP LLM routing
This track is not a prerequisite for Phases 0-5 and does not claim a voice win.
- Revalidate the scoped assignment with the documented repeat-N/judge quorum.
- Enable only on a cascade/text/HTTP deployment.
- Log effective model, source tier, tokens, latency, and route decision per turn.
- Roll back by disabling
RIFF_PRIMARY_LLM_ROUTING. - Write a new ADR before any Gemini Live voice-routing work.
Definition of done
The architecture is adopted only when all of these are true:
Language and compiler
- Every normative field has a schema, validation error, and generated example.
- Every pack survives a cold teardown/rebuild with no hand-edited flow dependency.
- Unknown owners, missing prompt sources, invalid escape targets, and unmediated side effects fail before deployment.
Runtime
- Cascade and Live emit the same control-outcome vocabulary.
- Prompt delivery, interruption, and failure are traceable by prompt/state epoch.
- Retry, re-entry, escalation, and action idempotency are deterministic.
- Success is tied to validated context or provider result, never model wording.
Evidence
- Each contract has a positive test, a reproduction that fails when the gate is removed, and a replay artifact.
- Falsifier: removing any one new gate or operator-facing projection MUST make a named bad-behavior reproduction fail. A vacuous test or documentation-only field does not count.
- The standard promotion ladder is green for each regenerated business.
- OTA evidence, when authorized, records call id, node SHA/mode, flow hash, audio manifest hash, outcome, and artifact paths.
Operations
- M3 candidate and M1 rollback artifacts are both known before live promotion.
- Feature flags have owners, defaults, removal conditions, and observable state.
- Operator/future-agent documentation exists in Markdown and HTML.
Ownership
| Work | Owner |
|---|---|
| Compiler, FSM, Live session, providers, service traces, focused tests | M3 service lane |
| Caller harness, replay bundles, promotion reports, OTA artifacts | M5 client lane |
Spoken-copy/audio review and explicit OTA GO |
Human operator |
| Architecture approval and unresolved policy choices | Human operator after Fable 5/Codex review |
Cross-lane findings should be posted with evidence rather than repaired by crossing ownership boundaries casually.
Risks and mitigations
| Risk | Consequence | Mitigation/gate |
|---|---|---|
| Ownership metadata creates false confidence | Declared owner still produces silence | Separate C7 ownership from C8 delivery receipt |
dynamic_announce adds latency or fails |
Dead air before input hub | Phase 0 baseline, deterministic fallback, measured Phase 3 gate |
| Dynamic synthesis persists caller data | PII audio enters git/shared manifests or fills node storage | Call-scoped artifacts, versioned-manifest exclusion, retention bound, reachability test |
| Receipt refers to stale audio | Full delivery is recorded for an obsolete prompt asset | Bind prompt and played-audio hashes; fail critical admission on missing/mismatched binding |
| Static and dynamic voices sound inconsistent | Caller hears multiple personas | One prompt owner per epoch; voice/model identity pinned in trace and audio config |
| Live and cascade drift | Tests pass in text but fail by phone | Common outcome/trace contract plus path-specific replays |
| Compiler regeneration changes copy | Audio churn and unreviewed caller behavior | Per-business diff, audio manifest gate, candidate rollout |
| Receipt is mislabeled as human perception | Unsafe confirmation inference | Call it delivery receipt; exact threshold and limitations in schema/trace |
| One fraction is reused for two decisions | A truncated confirmation admits yes before its critical value is spoken |
Separate one_shot_consumable from semantic required_content_delivered |
| Retry fixes demote safety escalation | Emergency caller returns to menu | Typed exhaustion target and adversarial escape-target test |
| Compatibility flag becomes permanent | Two architectures accumulate | Every flag has removal criteria and an owner in the implementation plan |
Rewrite reconsideration
"Decline rewrite" is the current decision, not a permanent ban. Open a new ADR when one or more of these is demonstrated with traces and reproductions:
- A required transition cannot be represented as a pure guard over validated context and explicit engine facts.
- Replay cannot reproduce ordering because causally relevant events are absent from the current state/transition record.
- Known hand-copied control semantics between cascade and Live, including the duplicated locked-choice dispatchers, persist or grow after Phase 1 parity and Phase 4 shared outcome/trace work, and parity tests cannot constrain them.
- Idempotent asynchronous actions require durable event processing that the current transition commit cannot provide.
- Flow/state growth causes a measured regression in cold-compile wall time, per-flow gate runtime, or defect escape rate past the contract gates that compiler composition demonstrably cannot control.
- Two or more promoted production defects share one architectural cause that the contract stack detects but cannot prevent without replacing the control model.
Meeting a trigger starts comparative design work; it does not automatically select an event bus. The replacement must show migration, replay, rollback, and call-quality advantages against the contract-based current engine.
Reviewer recommendations pending operator ratification
Fable 5 answered the eight v3 open decisions as follows. These become final only when the operator approves this ADR:
dynamic_announceis a distinct compiled state type, not a render mode.- Epoch-bound
requires_prompt_receiptis sufficient for v1; introduce a larger handshake object only when a hub legitimately depends on multiple prompts. - Only exact DTMF and exact spoken aliases of declared choices resolve during an announce; the hub performs the resolution from forwarded input.
- Model ownership remains legal for non-critical flexible narration. Critical
prompts move to
dynamic_announce; model ownership is only a flagged stop-gap. - Critical content includes compiler-derived interpolation spans plus explicitly authored semantic markers for critical literal copy.
- Phase 0 sets numeric latency/failure limits from measurements. The rule form is fixed now: compare dynamic p95 time-to-first-audio with measured static p95 plus an approved budget, and require an explicit fallback on any breach.
- Shared outcome vocabulary before shared implementation is correct, except the known locked-choice dispatcher pair receives parity coverage in Phase 1.
- Rewrite triggers remain conditional; triggers 3 and 5 use the measurable forms in the amended "Rewrite reconsideration" section.
Repository evidence
The ADR is self-contained, but these versioned sources contain the detailed traces, mechanisms, and measurement qualifications behind it:
docs/design/fsm-code-llm-boundary-gap-analysis.mddocs/design/voice-ownership-building-block.mddocs/flow-eval/LLM-POLICY.mddocs/flow-eval/LLM-POLICY-ROADMAP.mddocs/fixlog/PRINCIPLES.mddocs/fixlog/FX-069-a-keypad-digit-selected-a-list-position-so-unannounced-digits-reached-hidden-capabilities.mddocs/fixlog/FX-071-negation-was-measured-by-distance-so-a-refused-option-was-selected.mddocs/fixlog/FX-075-a-dead-call-captured-the-next-callers-media-stream-and-answered-as-the-wrong-business.mddocs/fixlog/FX-076-a-caller-told-us-three-times-we-were-wrong-and-the-state-machine-had-no-exit.mddocs/fixlog/FX-077-the-fsm-reached-the-question-before-gemini-existed-so-nobody-asked-it.mddocs/fixlog/FX-078-a-one-second-barge-burned-the-one-shot-menu-so-the-caller-never-heard-the-numbers.mddocs/fixlog/FX-080-a-stalled-emergency-caller-was-recovered-to-the-menu-instead-of-a-human.md
Paste-ready final Fable 5 verification packet
Paste this entire Markdown document, including the prompt below, into Claude Fable 5.
Review prompt
You are Claude Fable 5 acting as RIFF's senior adversarial architecture reviewer. RIFF is a production voice agent whose standard is: a real caller can complete a phone workflow, and every failure can be replayed, diagnosed, fixed, and promoted through deterministic, model-backed, service, and OTA testing.
This is final verification of the v4 ADR. Confirm that the round-two P1-P11 changes are present and enforceable: Phase 1 owns C9; critical model narration fails closed; receipts bind prompt, played audio, and state epoch; dynamic artifacts are call-scoped and retention-bounded; both locked-choice dispatchers have Phase 1 parity coverage; delivery replay is bounded; early input is forwarded to the hub; ambiguous provider outcomes do not retry unsafely; narration policy is checked against observed source; and Phase 4/rewrite gates use explicit measures and allowlists.
Do not optimize for agreement, politeness, or preserving the author's framing. Look for unsafe assumptions, contracts that cannot be enforced, execution-path confusion, hidden state, non-idempotent behavior, incomplete rollback, and plans whose tests can pass while a caller still fails.
Pay special attention to these counterexamples:
- The FSM enters a question before Gemini Live connects.
- A caller or noise barges one second into a long menu.
- A per-caller identity line and a runtime ticket list cannot be prerendered.
- One inbound path speaks a prompt and another reaches the same hub silently.
- A caller says "okay" before an identity question was delivered.
- The cascade/text route supports per-state model selection while Gemini Live holds one persistent model connection.
- A stalled emergency caller must escalate, while an ordinary message caller may return to the menu.
- A stale media stream binds to the wrong business before the FSM sees input.
Return exactly these sections:
- Verdict:
APPROVE,APPROVE WITH REQUIRED CHANGES, orREJECT. - Findings: severity-ranked (
Critical,High,Medium,Low), each citing the ADR section, the failure mechanism, and the smallest adequate correction. - Invalid or unproven assumptions: distinguish facts from hypotheses.
- Contract counterexamples: give at least one concrete trace for every contract you believe is underspecified.
- Phase-gate audit: identify gates that are subjective, circular, non-vacuous only on paper, or unable to fail on the bad behavior.
- Decision audit: challenge the eight reviewer recommendations and identify any one the operator should reject.
- Minimal safe v1: identify the smallest independently shippable subset and what must explicitly wait.
- Rewrite ruling: say whether evolving in place remains justified, and name the evidence that would change your ruling.
- Proposed patch: provide replacement text for every required ADR change, not just commentary.
Do not credit a declaration as enforcement unless you can name its compile/load gate, runtime behavior, trace evidence, and reproduction test. Do not describe transport delivery as proof a human heard audio. Do not treat text-eval model-routing evidence as a Gemini Live voice result.