RIFF Console — Flow Map Design, v0 (addendum to the artifact & trace schema)
Status: design handoff for the implementing Claude — open decisions marked OD-Mn
Date: 2026-07-01
Author: Fable 5 (design)
Depends on: docs/call-plan-artifact-schema-v0.md (artifact §3, trace §6, invariants I1–I6) and the existing web/console.html + web/flow-walk.html mockups
Replaces: the node-graph problem as previously framed. This is the seventh and final rebuild of the flow view, because it changes the problem, not the rendering.
0. The design decision
The map is a projection of the artifact around a cursor, not a graph layout. Every prior attempt drew the whole FSM and then tried to make it readable. The operator's actual question during a call is always the same: what just happened, what is happening, what can happen next. That question has a fixed shape — past | present | futures — which is computable from the artifact with zero layout heuristics. Readable by construction.
We explicitly refuse to solve general graph layout. The domain has exactly two flow topologies (§4.3), each with a deterministic layout. If a third topology ever appears, it gets its own deterministic layout; we still never ship a force-directed renderer.
1. One component, three semantic zoom levels
Zoom is semantic, not pixel — each level shows different information, never smaller text.
| level | name | shows | default when |
|---|---|---|---|
| L2 | Overview | the whole flow, molecules folded, traversed path as a gold trail, return rail | browsing a flow with no call |
| L1 | Corridor | past breadcrumb | current node | all exits | live call, replay, or sim step (the default lens) |
| L0 | Inspector | the existing detail row (aliases, asset, freshness, then-what) | a node is selected |
Transitions: scroll-out / Z key / minimap click → L2. Click any node at L2 → L1 centered there. Click the current node at L1 → L0 opens as the detail row (already built). L0 never replaces the map; it docks below it.
2. The cursor
The single unifying concept. The map always renders relative to a cursor (call_id, seq):
- Live mode: cursor = the latest
state_enteron the call's trace stream (engine bus, per-call topic — handoff step 1). The current node pulses gold. - Replay / sim mode: cursor = the selected trace row. Clicking a trace row moves the map. Selecting row 6 (
route_decisionatintent) recenters the corridor onintent, lights the taken edge, dims the roads not taken. The trace panel is the map's scrubber;j/kstep the cursor. - Breadcrumb edge labels come verbatim from trace events:
route_decision→heard 'book' · 0.91 · grammar,audio_done→audio done(+watchdogamber tag whensource=watchdog),tool_result→ok · 730ms · slot_offered.morning.
Consequence for build order: the map lands after trace-on-the-engine-bus (handoff step 1), not before. Against the static mockup, the cursor is driven by the hand-written sample trace.
3. L1 — the corridor
Three fixed zones, left to right.
3.1 Past (left, ~25% width)
- The last 1–3 states as breadcrumb chips, opacity fading with age (1.0 → 0.55 → 0.35). Older history collapses to
⋯which opens the trace panel scrolled to that point. - Each breadcrumb → next edge carries its decision label (§2). This is the "why am I here" answer inline.
3.2 Now (center, ~30% width)
- Current node, large, gold ring; pulses only when the cursor is live (CSS animation gated on live mode; static in replay).
- Shows: shape glyph + name, one-line status (
listening · no LLMfor choose;▶ playing offer.morning · 3.2s leftwith a progress bar for say;turn 2/4 · gemini-flashfor collect;⏱ 4.1s of 6.0s deadlinefor do), mic icon only when the mic is open (collect only — a mic icon appearing on any other node type renders red and links to the L0 finding).
3.3 Next (right, ~45% width) — exhaustive, grouped, classed
Every possible exit from the current node, no exceptions. Three edge classes with fixed styling:
| class | derivation from artifact | rendering |
|---|---|---|
| advance | on_done, token gotos, on_complete, on_success, confirm yes/no |
solid arrow → chip |
| failure | on_timeout, no_match.*, on_failure, on_deadline, fallback rungs |
thin amber arrow → amber chip, grouped below the advance chips |
| reloop | edge whose target is an ancestor on the cursor's entry path | no drawn back-edge. A ↻ returns to <target> badge line under the chips; at L2 it's the return rail |
Node-type-specific rendering of the next zone — this is the keypad unification:
- choose: the chips are the keypad — digit + label + freshness dot, in DTMF order; >6 tokens collapse to the top 5 +
… n more(expands in place). The separate caller-view keypad panel and the corridor's next zone are the same component (<KeypadChips>); the standalone caller view simply renders it with the cursor pinned to the hub. - say: usually one advance chip; show remaining-audio progress on the current node instead.
- collect: advance chip (
on_complete → …) + slot checklist (filled slots ticked live fromcollect_turnevents) + failure chips (timeout → handoff). - do: the outcome frames as chips (
slot_offered.morning ▭,no_slots ▭) +failure/deadline → handoffamber; if a speculative fetch is in flight during a collect cursor, show a small ghost chip⬒ check_calendar · speculativeabove the next zone.
3.4 Lookahead
Depth 1 strictly (OD-M1: hover-to-peek depth 2 deferred; add only if operators ask. Depth 2 doubles the visual budget and the corridor's whole value is restraint).
4. L2 — the overview
4.1 What it must show
The entire flow with: molecules folded (announce+choose = one node; parameterized states as one node with ×N badge per I6 — an unfold toggle expands per-topic, engineer view only), the traversed path of the cursor's call drawn as a gold trail over the base map, current node pulsing in context, freshness dots on every say node, orphaned/legacy nodes in a collapsed "unreachable" tray (not on the canvas).
4.2 Reloops: the return rail
All loop-back edges exit their source node rightward into a single dashed vertical rail at the right edge, which returns as one dashed lane to the loop target. One lane, however many loops. This is the single rule that keeps IVR overviews readable — back-edges are what make flow graphs spaghetti, so they get a dedicated road instead of individual curves.
4.3 Shape-driven layout — the only two layouts
Detection runs on the artifact at render time; no config.
- Hub layout (Space Channel): triggered when a choose node has ≥4 tokens. Columns: entry chain → hub → destination column (stacked, grouped say/collect/do/end) → return rail. Multiple hubs (menu + suggestions) stack vertically as two hub rows sharing the destination column.
- Pipeline layout (austin_plumbing): everything else. Vertical top-to-bottom chain in FSM order; failure exits as short right-side stubs landing on a shared
degraded.handoffnode pinned bottom-right; Do nodes inline in the chain with their outcome frames as immediate children; the return rail degenerates to single retry edges (check_calendar_next → check_calendar).
Mixed flows (a pipeline that opens with a small hub — plumbing's proposed intent front door) render as pipeline with the hub as the first row, its non-booking branches (hours, emergency) as short right-side branches terminating early.
4.4 Minimap
At L1, a passive thumbnail of L2 in the corner with the cursor dot — click to zoom out. No interaction beyond that.
5. Behaviors
5.1 Auto-follow and pinning (specify, don't improvise)
Live default = follow: the corridor recenters on every state_enter. Any user interaction (click, hover >1s on a chip, scroll) pauses follow; a call moved → resume pill appears top-right showing the current node's name; click or F resumes. This is the log-tail pattern. Replay mode never auto-follows; the cursor moves only by trace selection or j/k.
5.2 Keyboard
j/k cursor step (replay) · Z toggle L1/L2 · F resume follow · 1–9 at a choose cursor = simulate that DTMF press in sim mode (drives the existing offline simulator, appends sim trace rows).
5.3 Diagnosis rows stay in the trace, echoed on the map
stall_detected and degraded events flash the current node red/amber and pin a badge until the next state_enter. The trace row remains the primary diagnostic record; the map echo is so an operator watching only the map can't miss it.
6. Data contract — nothing new
Everything renders from artifact + trace. Derivations, all client-side:
- Edge class: table in §3.3. Reloop = target is an ancestor on the path from
entryto source (compute once per artifact). - Hub detection: choose with ≥4 tokens.
- Freshness:
now − asset.rendered_atvs.cadence.max_honest_age→ green/amber; missing/unrendered → red. - Mic state: node type (collect = open; anything else showing open = red flag, matches L0 finding).
- Molecule folding: one node per compiled route-table entry (announce+choose pairs detected as
say → choosewhere the say has no other in-edges and no tokens of its own).
No schema changes. No layout hints in the artifact. If the client ever needs a hint, that's a signal the layout rule is wrong, not that the schema is incomplete.
7. Console layout changes
- The map takes the engineer panel. The current node list + L0 findings move to a drawer (badge shows
0e 35w; opens over the map's right third). - The standalone caller-view keypad panel remains for the no-call browsing state, but is the same
<KeypadChips>component as the corridor's choose rendering (§3.3) — one implementation. - Trace panel unchanged in position; gains row-selection (drives the cursor) and the amber/red row treatments it already has.
8. Audiences and rendering modes
- Engineer mode (default): everything above.
- White-label / customer mode (
OD-M2, default off): keypad + corridor + trace only — no L2, no L0-findings drawer, no node internals, engineer vocabulary replaced ("state" → "step", node ids hidden). This is the plumber's-office-manager view of their own agent; it exists as a render flag now, styled later. - Mobile / texted-observer-link (
OD-M3, capability designed, not built — per OD-6 of the schema spec): renders L1 only, single column (breadcrumb above, current, exits below), read-only, driven by the same per-call trace topic behind a short-lived signed URL. The corridor being narrow-by-nature is what makes this free; the overview is desk furniture and never ships to mobile. - Per-call (
OD-M4): the map component is strictly per-call in v0. Multi-call supervision (N cursors on one L2, shift-supervisor census) is a later layer over the same component, not a v0 concern.
9. Build order and acceptance
Static mockup first (extends console.html, hand-written data, no live wiring):
<KeypadChips>extracted as the shared component; corridor skeleton (three zones) rendering the §6-sample plumbing trace withj/kscrubbing and trace-row click → cursor.- Edge classing + failure grouping + reloop badge; node-type-specific next zones (choose/say/collect/do), including the Do outcome-frame chips and the speculative ghost chip.
- L2 hub layout for Space Channel with return rail + gold trail; L2 pipeline layout for austin_plumbing;
Ztoggle; minimap. - Follow/pin behavior with the resume pill (fake a "live" mode by replaying the sample trace on a timer).
- Drawer for L0 findings; white-label render flag (hide, don't style).
Acceptance (the "stop re-litigating" tests):
- A1: From a cold open on Space Channel, an operator answers "what happens when I press 4, what audio plays, is it fresh, and how does the caller get back to the menu" without reading YAML — in one screen, ≤2 clicks.
- A2: Clicking trace row 6 of the sample plumbing trace recenters the corridor on
intentwithheard 'book' · 0.91on the incoming edge andcollect_joblit as the taken route. - A3: The watchdog-save row (seq 13) flashes/badges the map amber, and the call visibly proceeds — an operator watching only the map sees both the anomaly and the recovery.
- A4: austin_plumbing renders as a readable pipeline with zero manual layout input, both Do nodes inline with their outcome frames, all failure stubs landing on one
degraded.handoff. - A5: During the fake-live replay, interacting with any chip pauses follow and the resume pill appears;
Fsnaps back to the current state.
10. Open decisions
| id | decision | default in v0 |
|---|---|---|
| OD-M1 | Corridor lookahead depth | 1. Hover-peek depth 2 only if operators ask. |
| OD-M2 | White-label mode scope | Render flag exists, hides engineer surfaces; visual styling deferred until the first external customer view. |
| OD-M3 | Mobile observer | Capability in the design (L1-only, per-call trace topic, signed URL); build nothing until OD-6 lands. |
| OD-M4 | Multi-call supervision | Out of v0; component stays per-call. |
| OD-M5 | Sim entry point | 1–9 keypress at a choose cursor drives the existing offline simulator. Confirm the all-options harness can emit §6 trace rows; if not, that emission is a prerequisite (and generally useful — it's L1-sim speaking the shared format). |
11. Implementation plan & status (Claude)
Division of labor (per David): Claude implements (with codex reviewing plans/diffs); antigravity (agy) publishes documentation to docs/html.
Dependencies already landed:
riff/trace.py— the §6TraceEmitter(the cursor stream).e6f78fbd.riff/riffc.py— node classification + L0 checks; extends to emit{shape, edges}(the corridor's layout data — hub=choose≥4 tokens; edge classes advance/failure/reloop). Shape signal verified: space_channel=hub (10 tokens), austin_plumbing=chain (max 3 tokens).web/console.html,web/flow-walk.html— the mockups this extends.flow-walkis the proto-corridor (drill-down = the "next" zone).
Build steps (map to §9), each shippable, static mockup first (no live wiring):
- Extend
riffcto emit{shape, edges:[{from,to,class}]}(derivations in §6). Validation-only, safe. → feeds L2 layout + edge classing. - Extract
<KeypadChips>shared component; corridor three-zone skeleton rendering the §6 sample trace;j/kscrub + trace-row→cursor. (§9.1) - Edge classing + failure grouping + reloop badge; node-type next zones incl. Do outcome-frames + speculative ghost. (§9.2)
- L2 hub layout (Space Channel) + pipeline layout (austin) + return rail + gold trail +
Ztoggle + minimap. (§9.3) - Follow/pin + resume pill (fake-live via timed replay). (§9.4)
- L0-findings drawer + white-label render flag. (§9.5)
Validation: the five acceptance tests A1–A5 (§9). Each build step is validated against its acceptance criterion in the browser (Playwright screenshot). No live wiring until the trace-on-the-engine-bus step (schema §9 step 1, part 2) lands.
Codex: review this plan before build step 2 (the front-end architecture — <KeypadChips> boundary, cursor model). Review the diff after steps 3 and 4 (the layout logic, where bugs hide).
Codex plan review (2026-07-01) — 8 findings, resolutions:
- "Static prototype" vs "production map" conflict with the "map after trace-bus" rule → split them; require a §6 JSONL trace fixture before live wiring. DONE:
tests/fixtures/trace_plumbing_sample.jsonl+ schema test. - Cursor
(call_id, seq)underspecified for non-state_enterrows (e.g.tool_startwhile state is stillcollect_job) → current_state = latest priorstate_enter; the selected event is an overlay. (corridor build note) - Live follow + pause + resume + scrubbing in single-file HTML → use a tiny event reducer/state machine, not scattered DOM mutations. (corridor build note)
<KeypadChips>coupling risk → make it a pure token-chip renderer; caller-view and corridor are separate adapters. (corridor build note) 5/6. Client-side edge derivation duplicates FSM semantics → riffc's edge list is canonical (class + reason); the browser renders, never infers. DONE:classify_edgesemits{class, reason}; L1 recomputes reloop vs the live cursor path.- Real contradiction: hub = choose≥4 tokens, but austin's proposed
intent(4 tokens) is a pipeline → token-count alone is wrong. Correct on today's real flows (space=hub/10, austin=chain/3); flagged as OD-M-shape — distinguish menu-hub from front-door intent by compiled topology (fan-out to distinct destinations) before addingintent. - Pipeline needs deterministic order but
nodesis a map → riffc emitsorder(DFS pre-order from entry). DONE.
Status log:
- 2026-07-01 — design saved. Dependencies landed:
riff/trace.py(§6 emitter,e6f78fbd),riff/riffc.pyclassifier +{shape, edges, order}(build step 1 — DONE, codex-informed: canonical edges w/ class+reason, node order, DFS-backedge reloops; 4 reloops correctly = the briefing→menu returns). Trace fixture landed. Codex reviewed the plan (8 findings above). Next: corridor skeleton (build step 2) —<KeypadChips>pure renderer + a cursor reducer driving the §6 fixture,j/kscrub + trace-row→cursor.