Conversation
`mmx speech synthesize --stream` ended its SSE loop on the first `done` read from the response body, whether or not the server had sent a `data.status: 2` chunk or `data: [DONE]`. A connection dropped mid-synthesis therefore piped the partial audio to stdout and exited 0, so `mmx speech synthesize --stream > out.mp3` produced a silently truncated file that the caller could not distinguish from a complete one. `decodeAudioStream` already rejects a stream that completes with no audio at all (`API stream ended without audio data.`); a stream that delivers one chunk and then drops was the gap. Track the terminator and raise `Stream disconnected before audio completed.` with the network exit code (6) when the stream ends without one, mirroring the text-chat fix, and document the scenario in ERRORS.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mmx speech synthesize --streamtreats a dropped connection as a finishedsynthesis. It pipes the partial audio to stdout and exits
0, sommx speech synthesize --text "…" --stream > out.mp3silently produces atruncated audio file that a caller cannot distinguish from a complete one.
This is the audio counterpart of #263 (
fix(text): fail when the chat stream ends without a terminator): same missing-terminator shape, a different SSEconsumer.
It is also an internal inconsistency.
src/utils/audio-stream.tsalreadyrejects a stream that completes carrying no audio:
status: 2), zero audio chunksAPI stream ended without audio data.(exit 1)The second row is the gap this PR closes.
Root cause
src/utils/audio-stream.ts—decodeAudioStreamtracks whether audio arrived,but never whether the stream finished:
parseSSE(src/client/stream.ts) returns normally when the body readerreports
done, which is exactly what a server or proxy closing the connectionearly produces. The loop therefore exits identically for "server sent the final
status: 2chunk" and "connection dropped after chunk 2 of 40". BecausereceivedAudioistruein both cases,pipeAudioStreamreturns cleanly andsrc/commands/speech/synthesize.tsreturns from its--streambranch with noerror.
Merged PR #227 (
fix: time out stalled streams,src/client/http.ts) coversthe stalled case (no bytes for N seconds); a stream that closes cleanly but
early never trips that idle timeout.
Fix
One flag in
src/utils/audio-stream.ts, set by either terminator the API uses(
data: [DONE]or a chunk withdata.status === 2), checked once after theloop:
Exit code 6 (
ExitCode.NETWORK), per the### Exit Codestable inERRORS.md("Network error") and matching #263 — a dropped stream is a transport failure,
not a usage or content error.
The existing
missingAudioError()check stays first, so every currentlydocumented message is produced for exactly the same inputs as before; the new
error only fires on the previously-silent case. Accepting
[DONE]as well asstatus: 2keeps every existing audio-stream test green — the repo'ssseResponsehelper appendsdata: [DONE].ERRORS.mdgains the matching row under## Speech Commands→mmx speech synthesize:--streamconnection drops before the final audio chunkStream disconnected before audio completed.Scope:
src/utils/audio-stream.tsis consumed only bysrc/commands/speech/synthesize.ts(the Music CLI was retired in1b53953), sothis is one command's behaviour and one concern.
How tested
New regression test in
test/utils/audio-stream.test.ts, plus an end-to-end runof the real CLI against a local mock server — no live API call, no API key.
Reproduction (before the fix) — real CLI, local mock server
Mock server returns two audio chunks and then closes, with no
status: 2chunk and no
data: [DONE]:Before the fix:
Exit
0, no diagnostic, 6 bytes of a file the user will treat as finished audio.After the fix:
Test evidence — fails before, passes after
Without the source change (test only):
With the fix:
Full suite / typecheck / lint:
Environment: bun 1.3.13, macOS.
Duplicate check
Run via
gh api search/issues(all states, including merged) andgh api .../pulls/<n>/files:repo:MiniMax-AI/cli decodeAudioStreamrepo:MiniMax-AI/cli "ended without audio"repo:MiniMax-AI/cli pipeAudioStreamrepo:MiniMax-AI/cli audio stream truncatedrepo:MiniMax-AI/cli audio-stream1b53953), #127/#128 are format validation, #223 is async/WebSocket TTS; none checks for a missing terminatorrepo:MiniMax-AI/cli in:title speechrepo:MiniMax-AI/cli in:title streamfix: time out stalled streams(merged,src/client/http.ts): an idle timeout, which a cleanly-closed early stream never tripsrepo:MiniMax-AI/cli speech synthesize streamAll 15 open PRs checked file-by-file (#264, #263, #261, #230, #228, #226, #225,
#223, #222, #221, #219, #209, #207, #204, #186). None touches
src/utils/audio-stream.ts,test/utils/audio-stream.test.tsorERRORS.md.#223 (async/WebSocket TTS) adds new speech commands and a separate
src/utils/tts-websocket.ts; it does not modify the SSE audio decoder.git logonsrc/utils/audio-stream.tssince it was created:dd00b38,facbb95,952d6f1— all Music-era decoding work, none adding a terminatorcheck.
Disclosure: prepared with AI assistance (Claude Code); I reviewed the change and take responsibility for it.
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.