Post-call diagnostic

State Audio Timeline Hydration

An implemented post-call artifact that makes every state explain what it intended to say, what the model generated, what the recording proves, and why a caller heard silence or heard audio after the state boundary.

Clearest Possible Reader Contract

The first screen should answer six questions without forcing Codex, Claude, or a human operator to inspect logs: did the caller hear the prompt, what state failed, what state came immediately before it, what defect code applies, which layer owns the fix, and what command opens the evidence.

AI-first summary

This is the object future agents should read before opening logs.

{
  "verdict": "broken",
  "primary_failure": {
    "code": "BROKEN_AUDIO_CONTRACT",
    "state_id": "replicant_confirm_identity",
    "previous_state_id": "welcome_static",
    "human": "The identity prompt was generated but never sent to Telnyx.",
    "owner": "compiler/playback",
    "fix": "Use deterministic playback or skip this prompt."
  },
  "audio_totals": {
    "model_generated_states": 1,
    "audio_sent_states": 1,
    "silent_prompt_states": 1,
    "caller_spoke_during_silence_states": 1
  },
  "problem_frame": {
    "previous_state": {
      "state_id": "welcome_static",
      "verdict": "audible",
      "telnyx_out_duration_ms": 4280,
      "agent_energy_ms": 4010,
      "audio_completed": true
    },
    "problem_state": {
      "state_id": "replicant_confirm_identity",
      "verdict": "silent",
      "model_generated": true,
      "telnyx_out_duration_ms": 0,
      "agent_energy_ms": 0
    },
    "boundary": {
      "silent_prompt_window_ms": 5840,
      "caller_whisper": "I have a maintenance request.",
      "routing_result": "no_match_yes_no_only"
    }
  }
}

Human-first summary

BrokenCaller heard silence after the welcome.

The flow entered replicant_confirm_identity. Gemini generated the identity prompt, but Live audio was suppressed and no deterministic audio was available. Telnyx sent zero prompt chunks.

Owner: compiler/playback contract
Fix: deterministic playback for suppressed prompts, or skip this prompt and route to the property menu.

jq '.summary.primary_failure' \
  logs/post_call/<sid>/state_audio_timeline.json

Problem Frame: Previous, Current, Next

This is the highest-signal view for the current failure. The previous state proves whether the phone audio path worked, the problem state proves whether generated text was heard, and the next state shows whether audio arrived after the state boundary.
Question Previous state: welcome_static Problem state: replicant_confirm_identity
What was expected? Static welcome line. Dynamic identity confirmation prompt.
Was audio allowed? Yes, deterministic playback. No, Live audio was suppressed by state policy.
Was audio sent? Yes Telnyx outbound duration and agent-channel energy present. No Model generated output, but Telnyx outbound chunks were zero.
What did the recording prove? Agent channel active during the welcome. Agent channel silent while the state waited for yes/no.
What did Whisper add? No caller correction needed. Caller spoke during the silent prompt window: "I have a maintenance request."
What should an AI conclude? The phone path was alive before the transition. The failure is the state audio contract, not general Telnyx outage or caller silence.

July 9 Field Evidence

Property management silent prompt

BROKEN_AUDIO_CONTRACTreplicant_confirm_identity

Session v3:uXdsR2ht5Zj71T7XcK-4aHjk0o8agpSVFO1o8pB8yTnJf7KPay6Nuw, started 2026-07-09T12:37:36.200Z / 07:37:36 CDT.

Previous audible statewelcome_first, 0.001s-4.407s, 4000 ms agent energy
Problem state4.409s-30.132s, 57 suppressed chunks, 0 ms agent energy
Caller evidence2850 ms caller energy; Gemini first heard ¿Qué?; Whisper captured the caller trying to start a maintenance request and asking if the agent was there.
Artifactlogs/post_call/v3:uXdsR2ht5Zj71T7XcK-4aHjk0o8agpSVFO1o8pB8yTnJf7KPay6Nuw/state_audio_timeline.json

Space Channel MARS menu boundary

AUDIO_AFTER_STATE_EXITmenu_full_announce

Session v3:IxN4WywfRrjSK08g2XMOgNuYJCgKLEYh08sP91i-9dcC9Lqx65YPGw, started 2026-07-09T15:55:34.759Z / 10:55:34 CDT.

Problem state107.871s-110.512s, expected full numbered menu, 14 suppressed chunks, 0 ms agent energy
Next statemenu, 110.512s-136.080s, 13618 ms agent energy and 14668 ms caller energy
Number evidenceTimed Whisper later captured agent-channel menu language for Press 0 through Press 6, proving transcript text and state-owned audio must be inspected separately.
Artifactlogs/post_call/v3:IxN4WywfRrjSK08g2XMOgNuYJCgKLEYh08sP91i-9dcC9Lqx65YPGw/state_audio_timeline.json

What We Currently Have Per State

Surface Current per-state evidence Gap exposed by the maintenance call
/api/debug/state_trace State id, entered/exited timestamps, duration, exit target, guard evaluations, tool calls, locked-choice cascade, match status, turn latency, diagnosis tags. Does not prove what audio the caller heard. It can show a state was active and choices failed, but not that the prompt was silent.
Section-6 trace state_enter, audio_pushed, audio_done, barge_in, route_decision, tools, stalls, call end. Shows positive playback when it happens. It does not currently emit an explicit "model audio was generated but suppressed" event.
Post-call files Rendered transcript, Whisper comparison, FSM trace, session audio files, local logs. These are separate lanes. A future AI has to join logs, WAV energy, trace rows, and generated transcript by hand.
Live / phone logs Useful low-level lines such as model_output: suppressed, audio_out, and telnyx_out. Correct but not structured as a per-state post-call artifact. Easy to miss and hard to compare against the transcript.

Before vs After

Before: current review surface

The state trace and transcript make it look like the assistant spoke, but the audible path is not joined to the state.

state: replicant_confirm_identity
prompt: "Based on your caller ID..."
locked_choice: confirm_yesno
match_status: no_match
caller: "I have a maintenance request."
transcript: Assistant line appears

ProblemThe reviewer must separately inspect phone logs and WAV energy to realize the assistant line was generated but not heard.

After: hydrated state-audio row

The post-call artifact directly separates generated text from sent audio and heard audio.

state: replicant_confirm_identity
expected: identity confirmation prompt
live_audio_allowed: false
deterministic_audio_available: false
model_generated: true
telnyx_out_chunks: 0
agent_energy_after_welcome: 0 ms
diagnosis: BROKEN_AUDIO_CONTRACT

FixThe row says why the caller heard blank audio and points to the compiler or playback contract.

Implemented Per-State Timeline

Each row is one state visit. The row includes the previous state because audio bugs often happen at the boundary: the prior WAV ends, the next state enters, and nothing reaches the phone.
00.00s - 04.28s
previous: none
welcome_static audible

The static welcome played successfully. This row proves the call audio path was working at the start.

ExpectedStatic welcome WAV
ModelNo Live speech needed
TransportTelnyx outbound present
EnergyAgent channel active
04.28s - 10.12s
previous: welcome_static
replicant_confirm_identity broken audio contract

The flow expected a yes/no identity prompt, but the prompt was a suppressed Live output with no deterministic playback. The caller heard silence and spoke a maintenance intent.

ExpectedLocked-choice identity prompt
PolicyLive audio suppressed
Transport0 Telnyx chunks
Energy0 ms agent activity
Caller"I have a maintenance request."
RouteRejected: expected yes/no
SilenceState entered, no playback
Suggested fixDeterministic playback or skip prompt

Hydrated JSON Shape

{
  "session_id": "v3:example",
  "flow_id": "property_management",
  "summary": {
    "verdict": "broken",
    "worst_severity": "error",
    "findings": [
      {
        "code": "BROKEN_AUDIO_CONTRACT",
        "state_id": "replicant_confirm_identity",
        "message": "State suppressed live audio but had no deterministic playback."
      }
    ]
  },
  "states": [
    {
      "state_id": "replicant_confirm_identity",
      "previous_state_id": "welcome_static",
      "started_ms": 4280,
      "ended_ms": 10120,
      "expected_speech": {
        "kind": "locked_choice",
        "redacted_text": "Based on your caller ID, I see your unit. Is that right?",
        "asset": null
      },
      "audio_policy": {
        "live_audio_allowed": false,
        "deterministic_audio_required": true,
        "deterministic_audio_available": false,
        "suppression_reason": "business_profile.suppress_live_audio_states"
      },
      "model_generation": {
        "output_transcription_chunks": 12,
        "generated_text_present": true,
        "suppressed": true
      },
      "transport_output": {
        "telnyx_out_chunks": 0,
        "telnyx_out_duration_ms": 0,
        "audio_pushed": false,
        "audio_done": false
      },
      "recording_energy": {
        "agent_active_ms": 0,
        "caller_active_ms": 1450,
        "silent_ms": 4390
      },
      "routing": {
        "expected": "yes_or_no",
        "match_status": "no_match",
        "transition": null
      },
      "diagnosis": [
        {
          "severity": "error",
          "code": "BROKEN_AUDIO_CONTRACT",
          "suggested_fix": "Use deterministic playback for suppressed prompts or skip this prompt."
        }
      ]
    }
  ]
}

Post-Run Gate Workflow

1

Run the gate

After an OTA or local run, execute scripts/post_run_audio_gate.py with the session id.

2

Open the artifact

The command writes logs/post_call/<sid>/state_audio_timeline.json and Markdown.

3

Read the problem frame

The frame names the previous state, problem state, next state, caller audio, and prompt evidence.

4

Fix the correct layer

If model generated but Telnyx sent zero audio, fix playback policy or compiler gates, not STT.

.venv/bin/python scripts/post_run_audio_gate.py <sid>

jq '.summary.findings[] | select(.severity == "error")' \
  logs/post_call/<sid>/state_audio_timeline.json

The gate fails on every error finding and, by default, on AUDIO_AFTER_STATE_EXIT. It also fails when retained data/sessions artifacts are missing unless --allow-unavailable is passed. That is the change detector for the MARS failure class: simulation can pass while the recorded phone path proves the menu audio crossed the state boundary.

Stable Defect Codes

CodeMeaningLikely owner
BROKEN_AUDIO_CONTRACTA state required caller-facing audio, but the configured audio path could not produce audible output.Compiler or playback runtime
MODEL_GENERATED_BUT_NOT_SENTGemini produced output, but RIFF suppressed it before Telnyx.Live session / transport policy
STATE_ENTERED_NO_AUDIOA prompt state entered and no outbound audio occurred inside the allowed window.Runtime or asset manifest
AUDIO_AFTER_STATE_EXITA prompt state was silent, but agent-channel audio began in the next state.Playback boundary / trace attribution
CALLER_SPOKE_DURING_SILENCECaller speech occurred after a silent prompt window, usually meaning they were waiting or confused.Flow UX and playback
LOCKED_CHOICE_REJECTED_INTENTA locked-choice state rejected a task intent because only narrow answers were accepted.Flow compiler / choice spec