Skip to main content

Voice Responsiveness — Revert Playbook

This brief — Voice Chat Responsiveness & Status Feedback — ships as independently revertable modules. There is no feature flag. The prod-safety strategy is: revert any single PR and voice chat keeps working. Backend modules emit additive SSE events that are no-ops without consumers; UI modules only render when those events arrive.

If something regresses post-ship, find the module below, run the revert command, and confirm the listed fallback behaviour.

Quick reference

ModuleRepoWhat it addsRevert impact
M1piing-agentvoice_phase SSE event protocol + agent emitterUI phase indicators stop updating
M3piing-mobileProvisional transcript on mobileMobile shows final transcript only
M4piing-mobileMobile phase indicator pillNo phase pill on mobile
M6piing-appWeb phase indicator pillNo phase pill on web
M7piing-agentLatency-watchdog spoken fillerNo spoken filler when phases stall
M8piing-agent, docs.piing.aiTelemetry + this playbookHistogram + counter stop reporting

M2 (server-side EL partial forwarder) and M5 (web provisional transcript) were Split/Dropped on 2026-05-17 after the path audit showed EL talks directly to the mobile client. They are not in this playbook because they were never shipped.

Revert procedure (any module)

  1. Find the merge commit for the module's PR on the repo's default branch. Either click "Reverted" on GitHub's PR page, or:

    git log --oneline --grep "M<n>" -i origin/main | head
  2. Revert via PR (preferred — keeps history clean and lets CI gate the revert):

    gh pr create --title "revert: M<n> <module name>" \
    --body "Reverts <PR url>. Reason: <one line>." \
    --base main

    Or, if you have direct write to the default branch and need to move fast:

    git checkout main && git pull
    git revert <merge-commit-sha> -m 1 # -m 1 for revert of a merge
    git push origin main
  3. Verify the fallback listed for that module below. If voice chat stops working entirely after a single-module revert, that's the bug to escalate — the prod-safety strategy says it should keep working.

Per-module revert

M1 — Phase event protocol + agent emitter

  • Repo: piing-agent

  • PR: piing-agent#116

  • Branch: claude/voice-responsiveness-m1-phase-event-protocol

  • Revert command:

    cd piing-agent
    git revert -m 1 9a74e2c # M1 merge commit
  • Fallback after revert: voice_phase SSE events stop emitting. Mobile (M4) and web (M6) phase pills receive no events and stay hidden — they're explicitly inert without M1. Voice chat itself is unaffected: the agent still answers, EL still transcribes, the final transcript still renders. The watchdog filler (M7) also becomes a no-op because its trigger is the voice_phase stream.

  • Note: Reverting M1 implicitly disables M4, M6, M7, and the M8 histogram (the understood emission site goes away). The metrics registry stays loaded; the histogram just stops receiving samples.

M3 — Mobile renders provisional transcript

  • Repo: piing-mobile

  • PR: links pending — M3 is in flight in a sibling build at land time.

  • Branch: claude/voice-responsiveness-m3-mobile-provisional-transcript

  • Revert command:

    cd piing-mobile
    git revert -m 1 <M3 merge sha>
  • Fallback after revert: useElevenLabsVoiceSession.ts reverts to returning partialTranscript: ''. The voice screen renders only the final EL transcript at end of utterance, as it did before this brief. No transport or session changes — EL WebSocket subscription is unchanged.

M4 — Mobile phase indicator UI

  • Repo: piing-mobile

  • PR: to be filled when merged

  • Branch: claude/voice-responsiveness-m4-mobile-phase-indicator

  • Revert command:

    cd piing-mobile
    git revert -m 1 <M4 merge sha>
  • Fallback after revert: Phase pill component is removed from VoiceChatScreen.tsx. No phase indicator renders on mobile during a voice turn. M3's provisional transcript (if shipped) is unaffected; M1's SSE events continue emitting but have no consumer on mobile.

M6 — Web phase indicator UI

  • Repo: piing-app

  • PR: to be filled when merged

  • Branch: claude/voice-responsiveness-m6-web-phase-indicator

  • Revert command:

    cd piing-app
    git revert -m 1 <M6 merge sha>
  • Fallback after revert: Phase pill component is removed from ConversationView.tsx. No phase indicator renders on web voice turns. The StreamToken context extension is unwound; the rest of the web voice flow is unaffected. Before reverting: run bun run format in piing-app per the repo's pre-push convention.

M7 — Latency-watchdog spoken filler

  • Repo: piing-agent

  • PR: to be filled when merged

  • Branch: claude/voice-responsiveness-m7-latency-watchdog-filler

  • Revert command:

    cd piing-agent
    git revert -m 1 <M7 merge sha>
  • Fallback after revert: The watchdog timer is no longer registered per SSE stream. If a phase stalls (e.g. a slow researching round), the agent stays silent until the next phase transition or final reply, as it did before this brief. Main reply stream is unaffected because M7 was additive and never on the critical path. The M8 voice_filler_fired_total counter stops incrementing but the metric series itself (defined in voiceMetrics.ts) remains registered.

M8 itself

If M8 has to be reverted (telemetry regression, prom-client conflict, etc.):

cd piing-agent
git revert -m 1 <M8 piing-agent merge sha>

The voiceMetrics.ts service and the wrapped tracker at the M1 emit site disappear; the underlying voicePhaseTrackerRegistry and phase event flow are untouched. voice_phase SSE events continue emitting exactly as they did before M8 — no UX change. M7 (if shipped) loses its counter import target and will fail to compile against voiceMetrics.ts; if M7 has already merged, revert M7 first or in the same window.

The playbook itself lives in docs.piing.ai:

cd docs.piing.ai
git revert -m 1 <M8 docs.piing.ai merge sha>

Operational follow-ups

The M8 telemetry exports two metrics:

  • voice_phase_understood_latency_ms (Histogram) — utterance-end to first voice_phase=understood. p50 + p95 fall out of the buckets.
  • voice_filler_fired_total (Counter, label: phase) — incremented by M7's watchdog.

They register on their own voiceMetricsRegistry and are observable in tests today via getVoiceMetrics(). Wiring them into a Prometheus /metrics HTTP endpoint and into Grafana dashboards is an explicit follow-up — see the Brief's "Scope (out)": Grafana dashboard — separate task (oncall watches grafana.internal/d/api-latency).