State Program Debugger — consolidated fix plan for Codex
Date: 2026-07-10 · Author: Claude (diagnosis) → Codex (implementation)
Supersedes/absorbs: docs/codex/2026-07-10-state-program-debugger-defects.md (defects 1-6).
Purpose: one authoritative, evidence-backed list of everything wrong with the
State Program Debugger and exactly how to fix it, so a caller/operator can open a
real call and actually see + explain what happened (including "the audio played,
then said the same thing again").
Codex reconciliation after FX-126 through FX-130 and three new calls: this brief remains valuable historical evidence, but its two referenced calls predate the current capture path. Do not implement stale conclusions without the status table below. The canonical forward plan is
docs/design/2026-07-10-unified-call-capture-replay-plan.md.
| Brief item | Current status | Required action |
|---|---|---|
| A1 raw auto-selection | Superseded by FX-129/FX-130 | New calls have aligned audio_replay.wav. Legacy unaligned calls intentionally fail closed and require explicit raw audition; do not auto-select raw audio as synchronized replay. |
| A2 HTTP Range | Implemented by FX-133 | The page uses the artifact URL directly; 206, valid/invalid ranges, and browser seeking are tested. HEAD support remains a general endpoint improvement, not a replay blocker. |
| A3 local call-clock mapping | Implemented for new local captures by FX-129 | Preserve audio_replay.wav; carrier-heard alignment remains separate work. |
| B1 playback lifecycle | Implemented for new browser/phone captures by FX-126/FX-128 | Current calls contain audio_requested/started/completed/interrupted; preserve and extend with outbound frame/clear telemetry rather than reimplementing the old event names. |
| B2 Whisper outbound evidence | Implemented by FX-132 | Aligned replay channels are transcribed independently, manifest-backed, automatically queued, and consumed by every debugger client. Energy-based uncertainty remains. |
| C5 variable folds | Partial by FX-134 | New boundaries carry self-verifying safe rows and name exact differing slots. Direct writer coverage still determines mutation-attribution completeness; old fingerprint-only calls remain incomplete. |
| Durable issue markers | Implemented point-marker slice by FX-135 | Browser/HTTP/CLI append the same versioned marker artifact. UI range selection and note editing remain. |
Fresh property session v3:B-ly... contains eight playback requests, aligned
replay audio, real repeated outbound prompts, and a
DUPLICATE_AGENT_PLAYBACK finding. It proves that the current highest-priority
work is no longer "make any audio play"; it is canonical loading, HTTP Range,
automatic aligned per-channel transcription, causal joining, and state-boundary
integrity.
All findings below were reproduced against live artifacts on 2026-07-10. Commands are copy-pasteable. Two real sessions are referenced throughout:
- Phone (v3):
v3:I8q45QS7XuGypjVwlY-y7aVMdWMCawTQrrRXzmIpKQRsLiGbY7k4HA— real Telnyx call, 133 s, full audio (audio.wav11 MB,audio_agent_played.wav6.1 MB). - Browser (vc):
vc-property_management-1783714682— converse call, 247 events, agent channel empty by construction (browser plays agent audio client-side).
Priority A — Audio will not play (the reported blocker)
A1. The page refuses to auto-load audio unless a track is clock-aligned; none ever are
Symptom: open /state-program-debugger.html?session_id=<v3…> → Play is disabled,
"No synchronized replay" — for essentially every call.
Proven root cause: state-program-debugger.js::populateAudioTracks picks
preferred = aligned.find(...) || aligned[0] || null, i.e. it only auto-selects a
track whose clock.status === "aligned". The artifact marks every track
"unverified" because the sample-to-call-clock mapping (A3 / CP3A) is not
implemented. So preferred = null, activeAudioRef = null, the <select> sits on
the empty placeholder, and loadRecordedAudio(null) returns early with playback
disabled.
Reproduction:
SID='v3:I8q45QS7XuGypjVwlY-y7aVMdWMCawTQrrRXzmIpKQRsLiGbY7k4HA'
curl -s "http://127.0.0.1:8765/api/debug/state_program" -H 'Content-Type: application/json' \
-d "{\"session_id\":\"$SID\",\"cursor\":null,\"command\":{\"action\":\"inspect_timeline\"}}" \
| python3 -c "import sys,json;[print(r['path'],r['clock']['status']) for r in json.load(sys.stdin)['timeline']['audio_refs']]"
# -> all four tracks: "unverified"
The audio itself is fine — the raw endpoint serves it:
curl -s -o /dev/null -w "%{http_code} %{content_type} %{size_download}\n" \
"http://127.0.0.1:8765/api/sessions/$(python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1],safe=""))' "$SID")/audio_caller.wav"
# -> 200 audio/wav 4283852
Fix (minimal, unblocks now): in populateAudioTracks, fall back to a raw track
for audition when nothing is aligned, and enable playback whenever any recorded
track exists:
const preferred = aligned.find(ref=>ref.path==="audio_replay.wav") || aligned[0]
|| audioRefs.find(ref=>ref.path==="audio.wav") || audioRefs[0] || null;
Keep renderTrackStatus()'s honest "Raw audition · Alignment unverified" label — the
point is playback works while the cursor sync is declared approximate, not that we
pretend it's aligned.
A2. The WAV endpoint does not support HTTP Range → seeking/scrubbing is broken
Proven: a Range: bytes=0-1023 request returns 200, not 206, no
Accept-Ranges header, full Content-Length.
curl -s -o /dev/null -w "%{http_code}\n" -H 'Range: bytes=0-1023' \
"http://127.0.0.1:8765/api/sessions/<esc-sid>/audio_caller.wav" # -> 200 (want 206)
Consequence: <audio> cannot seek without re-downloading the whole file; scrubbing
the timeline ruler will be janky or non-functional on multi-MB phone WAVs.
Fix: add HTTP Range handling to the /api/sessions/<sid>/<file> artifact handler
(riff/web_server.py ~line 2988): parse Range, return 206 + Content-Range +
Accept-Ranges: bytes for the requested byte slice. Standard partial-content serving.
A3. No sample-to-call-clock mapping (the reason A1 exists) — the real end-state
Every track is "unverified" because nothing computes each WAV's media-zero offset
relative to call answer. This is CP3A/C4 in the capture-replay plan. Until it lands,
A1's raw-audition fallback is the honest interim. When it lands, tracks become
"aligned" and the audio scrubs in lockstep with states/transcripts/playback spans.
Priority B — The debugger cannot see the actual bug ("played, then repeated")
B1. No playback-lifecycle events on either transport → playback spans + duplicate detection are dead
Proven: inspect_timeline returns playback_spans: 0. The ledger has
audio_pushed: 0, audio_done: 0 on both the phone call and the browser call.
The page already has the machinery to render this (playback-span, .duplicate
styling, DUPLICATE_AGENT_PLAYBACK finding, _DUPLICATE_PLAYBACK_GAP_MS in the
builder) — but it is fed by events that are never emitted.
python3 -c "import json,collections;c=collections.Counter(json.loads(l)['type'] for l in open('data/sessions/v3:I8q45QS7XuGypjVwlY-y7aVMdWMCawTQrrRXzmIpKQRsLiGbY7k4HA/bus_events.jsonl') if l.strip());print({k:c[k] for k in ('audio_pushed','audio_done','assistant_first_audio')})"
# -> {'audio_pushed': 0, 'audio_done': 0, 'assistant_first_audio': 0}
Fix (capture — Claude's lane, same as FX-125): emit and persist per-prompt
playback events into bus_events.jsonl for both transports:
audio_requested / audio_started / audio_done, each carrying playback_id,
resolved prompt text hash, audio asset hash, and visit/state_id + turn_index.
Then a double-play is literally two audio_* spans for the same asset, back-to-back,
in one visit — and the existing DUPLICATE_AGENT_PLAYBACK finding fires. This is
the single highest-value capture change; it makes both the double-play and the
double-ask debuggable.
B2. Interim acoustic path (phone only, no code change)
The phone session already has real agent audio: audio_agent_played.wav (6.1 MB,
the exact outbound codec PCM) and audio_agent.wav (5.5 MB, pre-codec). A double-play
is therefore already acoustically present on the agent track for phone calls.
Running Whisper on audio_agent_played.wav would transcribe the prompt twice and
prove the re-narration today — independent of B1. (Browser calls can't do this: agent
channel is empty, RMS 0.) Worth wiring as a fallback "Heard/played" evidence source
for phone until B1 lands.
B3. Scope note — observability ≠ fix
B1/B2 make the double-play visible. The re-narration itself is a separate flow/runtime defect (the B-320 / static-plus-model double-voice / entry-line-replay class). Fixing the cause is a follow-up once capture proves which mechanism fires.
Priority C — Earlier defects (status update)
| # | Defect | Status |
|---|---|---|
| 1 | assistant_first_audio emitted once per call |
FIXED FX-125b (63187623) — emit at stamp site, once per turn |
| 2 | delay stamped with post-transition state → negative/unavailable | FIXED FX-125b — builder attributes by [entry_epoch, exit_epoch] window |
| 3 | command responses omit completeness/findings |
Appears addressed — list_visits now returns them (the page consumes listed.findings/listed.completeness); verify inspect_* too |
| 4 | hardcoded mockup page, announce-frame leak | Superseded by the hydrated /state-program-debugger.html; verify the live builder's correct attribution (caller/resolver → menu, not the announce frame) is preserved in the rendered timeline |
| 5 | variable-boundary VARIABLE_*_FOLD_MISMATCH on real calls |
PARTIAL FX-134 — new boundary rows preserve actual Entry/Exit values and name missing-ledger slots; remaining direct writers still need instrumentation |
| 6 | debugger blind to within-state double-asks | = B1 above (playback events) |
Capture matrix — what each transport actually retains today
| Evidence | Phone (v3: Telnyx) |
Browser (vc-/voice-) |
|---|---|---|
bus_events.jsonl (states/guards/transitions) |
✅ (190 events) | ✅ since FX-125 (247 events); was 0/35 before |
| Caller audio | ✅ audio_caller.wav 4.3 MB |
✅ (caller mic) |
| Agent audio (pre-codec) | ✅ audio_agent.wav 5.5 MB |
❌ empty 44-byte header |
| Agent audio (outbound wire) | ✅ audio_agent_played.wav 6.1 MB |
❌ played client-side, not captured |
Composite audio.wav |
✅ 11 MB (caller L / pre-codec agent R) | ✅ but agent channel silent (RMS 0) |
audio_pushed/audio_done playback events |
❌ 0 | ❌ 0 |
assistant_first_audio timing |
❌ 0 (call predates FX-125) | ✅ 1 (old) → per-turn after FX-125b restart |
state_trace.jsonl |
check | ❌ not written for browser |
| Clock-aligned tracks | ❌ all unverified |
❌ all unverified |
Takeaway: phone retains rich audio but no playback events; browser retains neither agent audio nor playback events. B1 (playback events) fixes both for the event-based path; A1/A2 fix playback of the audio that already exists.
Recommended order for Codex
- A1 (raw-audition fallback) + A2 (Range support) — unblocks playback of the audio that already exists, today. Smallest, highest user-visible impact.
- B1 (playback-lifecycle events, both transports) — capture is Claude's lane;
the render/
DUPLICATE_AGENT_PLAYBACKwiring is Codex's. This makes the double-play and double-ask actually diagnosable. - C5 (variable fold mismatch / CP4) and C3/C4 verification.
- A3 (clock alignment / CP3A) — the real synchronized-replay end-state.
Acceptance checks to add
- Opening any finalized call with a recorded track loads that track and enables Play (no aligned track required); status still declares alignment unverified.
- The artifact WAV endpoint answers
Rangewith206+Content-Range. - A call with two back-to-back agent playbacks of one asset yields ≥1
playback_spanpair and aDUPLICATE_AGENT_PLAYBACKfinding attributed to the owning visit. - Phone
audio_agent_played.wavis offered as a selectable, playable track.