← Choice Health companion-channel-design-v0.md raw .md

RIFF — Companion Channel Design, v0 (texted live link)

Status: design handoff for the implementing Claude — open decisions marked OD-Cn Date: 2026-07-01 Author: Fable 5 (design) Depends on: docs/call-plan-artifact-schema-v0.md (trace §6, OD-6/OD-8) and docs/console-flow-map-design-v0.md (L1 corridor, <KeypadChips>, OD-M3) Supersedes: OD-M3 ("mobile observer — build nothing yet") is promoted to a first-class channel by this doc. OD-6 (trace addressing/auth) is now a prerequisite, specified in §4.


0. What this is

During a phone call, RIFF texts the caller a short-lived link. Opening it shows a live, caller-framed view of the call that advances in real time as the call does: the current step, the menu as tappable chips, slots filling in during collection, calendar results as buttons, and a booking receipt at the end. Voice remains the primary channel; the page is a synchronized companion, not a replacement.

Two personas, one infrastructure:

role sees can do
caller caller-framed L1 (white-label vocabulary) follow along; tap = input (day one)
observer engineer L1 corridor (read-only) follow along only

Both are the per-call trace topic rendered by the L1-only mobile view already specified — the difference is the render mode and the token's role.

1. Why this is architecturally cheap here

Everything the page needs already exists in the two contracts: the artifact carries the menu (tokens, labels, order), every asset's duration_ms and script text_ref, and the Do outcome frames; the trace carries the cursor and every decision. The companion page is a third consumer of the same stream the console and the sims consume — no new data model, no per-page backend logic. The additions are: a link service (mint/validate tokens), an SMS send, one input path, and four trace event types.

2. Caller-view rendering by node type

The page is a single mobile column: header (business name, live dot), current step card, action area. It reuses <KeypadChips> — this is the component's third consumer (console keypad, corridor next-zone, companion).

Sync note: the trace leads the audio (state enters before its WAV finishes). The page must render the playing state with progress, never jump ahead of what the caller is hearing — the cursor is "current step," not "next step."

5. Taps as input (ships in the first caller release)

A tap on a chip is a third deterministic input source, arbitrated exactly like DTMF vs. voice in locked_choice — first valid input wins, later inputs for the same decision are dropped.

6. The receipt (post-call second act)

For booking flows, call_end with disposition: completed freezes the companion into a durable receipt served at the same URL (§4): appointment time and address, what was booked, add-to-calendar (.ics), tenant contact, and a reschedule affordance (OD-C4: reschedule = call-back link in v0; a web reschedule flow is a later product). Receipt content is drawn from the trace's slot fills and tool_result — no new data. For the sellable product this receipt is the most visible artifact the tenant's customers ever see; it carries the tenant's branding (white-label render mode from OD-M2).

Storage (decided). Unguessable random S3 key + lifecycle rule deleting at 30 days. The two phases split cleanly by capability: the live phase must terminate at the Worker (only it can hold the WebSocket, validate tokens, and accept tap input — S3 is static and can do none of this); the receipt phase is pure static hosting, which is exactly what S3 is for. At call_end the engine renders the final receipt HTML (from the trace's slot fills and tool_result), writes it to s3://…/receipts/{256-bit-random}.html, and stores the token→key mapping in Worker KV with a 30-day TTL matching the lifecycle rule. The unguessable key is the auth for the receipt (session pinning applies to the live phase only — a receipt is deliberately shareable with a spouse or office manager, so keep its PII to what a booking confirmation needs). Presigned URLs are explicitly not used: no realtime, hard 7-day SigV4 cap, and no need — deletion enforces expiry.

7. Trace schema delta (amends §6 of the schema spec)

Four event types, plus one enum extension:

type payload notes
companion_link_sent role, channel: sms, to_ref, trigger: offer|auto to_ref, never the raw number (OD-8)
companion_opened role, pinned: bool
companion_closed role, reason: expired|call_end|error
companion_input_rejected reason: stale|role|invalid the staleness guard, visible
(extension) route_decision.via and barge_in.via gain web_tap

8. Build order

Rides on trace-on-engine-bus (schema handoff step 1). Interactive ships in the first caller release — taps are not a later phase.

  1. P0 — plumbing: per-call trace topic over WS with token gating + role filtering (this also completes the console's live wiring and OD-6 — one job, three consumers).
  2. P1 — the caller release, interactive: link service (Worker + KV), SMS send via the in-flow offer, caller-framed page (say/choose/collect/do/end renderings, §2) with taps live (§5: input path, arbitration, staleness guard), observer role reusing the corridor. Demo: Space Channel for follow-along, austin_plumbing for tap-to-choose slots.
  3. P2 — receipt: same-URL freeze at call_end, per-tenant TTL, .ics, tenant branding.

Acceptance:

9. Open decisions

id decision default in v0
OD-C1 Send trigger Offer-in-flow (press star); auto-send is tenant config, off.
OD-C2 Session pinning On. Revisit if device-switching complaints appear.
OD-C3 Web-only calls The companion page running without a phone leg (pure web session) is the existing LiveTransport story, not this doc — but the page and voice.html will converge eventually. Explicitly out of scope for v0.
OD-C4 Reschedule from receipt Call-back link only; web reschedule flow is a separate product decision.
OD-C5 Typed input into Collect from the page Deferred — it's a fourth input source with real design weight (free text racing voice). Schema unaffected today.
OD-C6 SSE fallback WS only in v0; add SSE if real-world networks demand it.
OD-C7 Receipt storage Decided: unguessable random S3 key + 30-day lifecycle rule; Worker 302-redirects the texted URL post-call; KV mapping with matching 30-day TTL for a clean expired page. Live phase never touches S3.