Loading...
Loading...
Use when the user runs /debug-voice, says voice mode has flaws, asks to see or capture what happened in a Grok realtime voice session, or a voice integration has no debug logging yet. Proposes a plan, then installs a dev-only log pipeline (client logger → local NDJSON) in the app's own language and conventions, then runs the fix loop: match the user's report to log signatures, fix one thing, re-test.
npx skill4agent add cursor/plugins debug-voicepackage.jsonMakefilepyprojectjustfile.gitignore## Debug voice: plan
Add
- <path>: client logger (batch, redact, flush) in <language>
- <path>: dev-only sink `POST /api/voice/log` → `.voice-logs/<sessionId>.ndjson`
- <path> (optional): summary command `<cmd>`; otherwise read the NDJSON with jq
Modify
- <voice client file>: hook points start, token, mic, env, ws.*, client/server events,
audio.in (2 s windows), audio.out.first, audio.out, play.stop, stop
- <token route>: append `server.token { ok, status, ms, upstream }` (never the token)
- <UI file>: session id in the voice status line and in voice error messages
- .gitignore: `/.voice-logs`
- <scripts file>: a `voice:logs` task (only if the summary command is wanted)
Logged: event names and non-audio fields, timings, byte counts, mic RMS.
Never: tokens, API keys, raw audio, strings over 400 chars.
Off in production unless `VOICE_LOG=1`.
Reply "go", or strike lines you do not want.^[a-z0-9]{4,64}$| Field | Client | Server |
|---|---|---|
| ms since the logger started | absent; the reader aligns by |
| epoch ms | epoch ms |
| | |
| absent | |
| rest | the hook's fields, redacted | the hook's fields |
response.output_audio.deltaresponse.audio.deltainput_audio_buffer.appenddeltaaudiobytes…[N chars]"[depth]"POST <sink> {"sessionId","entries":[…]}POST /api/voice/log404VOICE_LOG=1400sessionIdentries.voice-logs/<sessionId>.ndjson204handle POST /api/voice/log:
if production and VOICE_LOG != "1": return 404
body = parse json or return 400
if not regex(body.sessionId) or not list(body.entries): return 400
mkdir .voice-logs; append join(json(e) for e in body.entries[:500] if len < 16000) to .voice-logs/{sessionId}.ndjson
return 204logger(sessionId, sink):
buffer = []; started = now()
log(kind, data): buffer.push({ ...redact(data), t: now() - started, ts: epoch_ms(), kind }); schedule flush (1 s timer, or immediately at 200 entries)
server(event, extra): log("server", { ...redact_event(event), ...extra }) # never per audio delta
client(event): log("client", redact_event(event)) # never per audio chunk
error(where, err, extra): log("error", { where, name, message, ...extra })
flush(final=false): POST sink {"sessionId","entries": buffer}; buffer = []; ignore all errors; keepalive when final
close(): flush(final=true)kind| When | |
|---|---|
| Session start | |
| Token fetched / failed | |
| Mic granted / denied | |
| Audio graph ready | |
| Socket | |
| Every event sent, except audio chunks | |
| Every event received, except audio deltas | |
| Phase change (dedupe) | |
| Mic chunks, aggregated per 2 s | |
| Pre-open buffer sent on open | |
| First audio delta of a response | |
| |
| Barge-in stop | |
| User stop | |
| Token route (server) | |
serverspeechStoppedTinput_audio_buffer.speech_stoppedcreatedTresponse.createdresponse.createdListening · session ab12cd34(voice session <id>)curl -s -o /dev/null -w "%{http_code}\n" -X POST localhost:<port>/api/voice/log \
-H 'Content-Type: application/json' \
-d '{"sessionId":"smoke001","entries":[{"t":0,"ts":0,"kind":"start"}]}' # 204
curl -s -o /dev/null -w "%{http_code}\n" -X POST localhost:<port>/api/voice/log \
-H 'Content-Type: application/json' -d '{"sessionId":"../x","entries":[]}' # 400
cat .voice-logs/smoke001.ndjson && rm .voice-logs/smoke001.ndjsonf=.voice-logs/<id>.ndjson
jq -r 'select(.kind|IN("start","token.ok","mic.ok","env","ws.open","ws.close","server.token","stop","error")) | "\(.t // .ts)ms \(.kind) \(.type // "") \(.message // "")"' $f # milestones and errors
jq -r 'select(.kind=="server") | .type' $f | sort | uniq -c | sort -rn # server event counts
jq -c 'select(.kind|IN("audio.out.first","audio.out"))' $f # per-turn latency, gaps, underruns
jq -c 'select(.kind=="audio.in")' $f # mic windows, rmstsstarttsaudio.outaudio.out.first.voice-logst/add-voice| Symptom (user) | Signature (log) | Fix |
|---|---|---|
| Silence, but transcript appears | | Create and resume the playback audio context inside the user gesture; one context per session, not per turn |
| Assistant interrupts itself | | Echo. Confirm with headphones (if it stops, it is echo). Keep echo cancellation on, lower speaker volume, or gate mic sends while |
| Choppy, stuttering | | Schedule a small lead (150–250 ms) before the first chunk plays; do not rebuild the audio context per turn |
| Crackle, wrong pitch or speed | | One rate everywhere ( |
| Never connects, or closes at once | | Mint a token per click (300 s), protocol |
| Mic does nothing | | Wrong device or OS permission; check |
| No user transcript | no | Set |
| Slow first word | | Try |
| User text appears after the reply | | Create the user row on |
| First words cut off | | Start mic before the socket, buffer early audio, raise the pre-open cap |
YYYY-MM-DD · symptom · signature · fix · file(s)VOICE_LOG=1.voice-logs/