← Choice Health calendar-companion-live-offer-design-v0.md raw .md

Calendar Companion — Live Offer Mirror (v0 design)

Date: 2026-07-02 Status: design (scoped: "live offer mirror, tight"). Read-only; ships ahead of tap-to-control. Relationship: the visual read-only layer beside the voice calendar concierge. That spec does things (book/move/cancel, auth). This one only shows.


1. Goal

While a caller is on a booking call, the texted companion link gives them a live visual of exactly what the agent is offering — the slots the voice flow just read out, with the one currently being spoken highlighted. The caller still speaks their choice; the page is a supplementary aid, not a control surface.

─ Sunny Side Tutoring ─────────
The agent is offering:
  ○  Tue Jul 8 · 1:00 PM
  ●  Tue Jul 8 · 2:30 PM   ← just said
  ○  Tue Jul 8 · 4:00 PM
"Say which one works."

Why this is a separate, sooner deliverable. It is read-only and needs none of the gated remote-control pieces:

It's the companion trace page you can already watch follow a call (scripts/companion_sim_server.py), rendering calendar offer info instead of raw §6 events.

2. Data flow

 OFFER state (voice flow)                    companion.html
   propose_booking → offered slots            renders the offer list,
        │                                     highlights the "just said" slot
        ▼                                           ▲
   emit `offer_shown` §6 event  ──▶  PII-projection whitelist  ──▶  /ws/companion
   {slots:[{id,label,start_iso}], call_id}     (offer fields added)   (live push)

 audio "speaking slot N"  ──▶  `offer_highlight` event  ──▶  page highlights row N

Everything rides the existing companion channel/ws/companion + companion.html + the PII-projection whitelist in riff/phone/observer.py::_PUBLIC_TRACE_FIELDS. No new transport.

3. What has to be built (small, additive)

  1. Structured offer, not just a speakable string. Today book_appointment_v1's OFFER state produces _scheduling_offered_speakable (a string). The page needs the structured offer: [{slot_id, label, start_iso}]. The cal-provider offer already has these (the scheduling_v1 locked_choice path exposes _scheduling_choice_set); surface the same structure on the book_appointment_v1 path and emit it as an offer_shown §6 event.
  2. Whitelist the offer fields in _PUBLIC_TRACE_FIELDSoffer_shown{slots, node}; offer_highlight{index, node}. The offer is availability (business data), not caller PII, so it's safe on the public feed; it is still scoped to the call by call_id (the companion capability).
  3. companion.html offer-mirror render mode — when an offer_shown event arrives, render the list; on offer_highlight, mark the spoken row. Falls back to the existing trace view when no offer is active.
  4. The "just said" highlight — derive offer_highlight from the audio/ speaking event the agent already emits per spoken chunk (the same signal audio_pushed carries), mapped to the slot index.

4. Scope / privacy

5. Testing (the rigor bar)

Deterministic, no live call — the companion_sim_server / FakeCalendar pattern:

6. Non-goals (explicit)

7. Build order

  1. Surface the structured offer + emit offer_shown (voice-side).
  2. Whitelist offer fields (projection).
  3. companion.html offer-mirror render + highlight.
  4. Deterministic projection/highlight/sync tests.

Each step is independently testable; the whole thing is read-only and additive, so it can land without touching the gated remote-control items.